# Implementation Plan: OIDC Proxy Script Authentication **Branch**: `001-oidc-proxy-script` | **Date**: 2025-07-17 | **Spec**: [spec.md](spec.md) **Input**: Feature specification from `specs/001-oidc-proxy-script/spec.md` ## Summary Create `src/globalVariables/adapter_settings.json` with OIDC credentials and implement `src/proxyScripts/proxy.js` — a zero-import/export Node.js VM-sandbox script that: - Reads OIDC credentials exclusively from the injected `adapter_settings` context variable (FR-001) - POSTs to the configured `tokenUrl` with `application/x-www-form-urlencoded` body and a 5-second timeout (FR-003, FR-014) - Extracts the bearer token from the `id_token` field of the JSON response (FR-004) - Caches the token in **Redis** (`redis.hSet('authorization', 'token', ...)` / `redis.hSet('authorization', 'expiry', ...)`) using `expires_in` as an **absolute Unix epoch timestamp** (FR-005, FR-006) - Queues concurrent callers on a shared promise to prevent token-fetch stampedes (FR-013) - Returns `200 OK / Authorized` on success and `401 Unauthorized` with a descriptive message on any failure (FR-007, FR-008) No modifications to `server.js` are required. The existing `loadGlobalVariables()` pattern automatically picks up `adapter_settings.json` and injects it as `adapter_settings` into every VM context. ## Technical Context **Language/Version**: Node.js 18+ (ES Modules; `"type": "module"` in package.json) **Primary Dependencies**: `axios` ^1.13.6, `uuid` ^13.0.0, `jsonwebtoken` ^9.0.3, `xmlbuilder2` ^4.0.3 — all already present in `package.json`; no new packages required **Storage**: Redis — token and expiry stored in hash key `authorization` via `redis.hSet`/`hGet`; `redis` is injected into the VM context as a global. An in-process `adapter_settings._pendingFetch` guards against stampede (Promises cannot be serialised to Redis). **Testing**: Node.js built-in `node:test` runner (`node --test tests/**/*.test.js`); `t.mock.fn()`, `t.mock.timers` for fakes; no external test framework needed **Target Platform**: Linux/macOS, long-running Node.js 18+ server process **Project Type**: HTTP proxy adapter — VM-sandboxed script (IVA Studio proxy script pattern) **Performance Goals**: Every request responds within 5 s (SC-001); zero token-service round-trips when a valid cached token exists (SC-002) **Constraints**: `proxy.js` MUST have zero `import`/`export` statements; MUST NOT reference `config`, `global.config`, or `process.env`; all dependencies injected via `vm.createContext()` **Scale/Scope**: Single-process, single-tenant; one OIDC token shared across all concurrent requests ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* | Principle | Status | Notes | |-----------|--------|-------| | **I. Monolithic Architecture** | ✅ PASS | All auth + cache logic in `proxy.js`; no helper extraction | | **I. Zero Imports/Exports** | ✅ PASS | `proxy.js` uses only VM-injected globals; zero `import`/`export` | | **I.0 Forbidden Globals** | ✅ PASS | No `config`, `global.config`, or `process.env` in `proxy.js` | | **I.I What MUST be in proxy.js** | ✅ PASS | Authentication, token cache, stampede queue all in `proxy.js` | | **I.II Allowed Separate Files** | ✅ PASS | Only adding `adapter_settings.json` to `src/globalVariables/` | | **I.IV Configuration** | ✅ PASS | Credentials in `src/globalVariables/adapter_settings.json`, not `config/default.json` | | **I.V VM Context Injection** | ✅ PASS | `adapter_settings` auto-loaded by existing `loadGlobalVariables()` — no server.js changes | | **II. API-First Design** | ✅ PASS | HTTP response contract and VM context contract documented before implementation | | **III. Test-First Development** | ✅ PASS | Test scenarios defined; tests written before implementation code | **No violations. Complexity Tracking not required.** ### Post-Design Re-check | Principle | Status | Notes | |-----------|--------|-------| | **Cache state isolation** | ✅ PASS | Token/expiry stored in Redis hash `authorization`; in-process `adapter_settings._pendingFetch` holds stampede guard (Promise not serialisable to Redis) | | **Cross-realm Promise** | ✅ PASS | Stampede guard uses duck-type check (`typeof .then === 'function'`) rather than `instanceof Promise`; `await` uses the Promises/A+ thenable protocol which is cross-realm safe | | **Async error containment** | ✅ PASS | `script.runInContext()` is not awaited by server.js; proxy.js top-level async IIFE catches all errors internally and always sends a response | ## Project Structure ### Documentation (this feature) ```text specs/001-oidc-proxy-script/ ├── plan.md # This file ├── research.md # Phase 0 — resolved unknowns ├── data-model.md # Phase 1 — entities and state transitions ├── quickstart.md # Phase 1 — setup and testing guide ├── contracts/ │ ├── proxy-http.md # HTTP response contract (success + error) │ └── vm-context.md # VM dependency injection contract └── tasks.md # Phase 2 — task list (/speckit.tasks, not created here) ``` ### Source Code (repository root) ```text src/ ├── proxyScripts/ │ └── proxy.js # NEW — OIDC authentication proxy (VM sandbox) ├── globalVariables/ │ └── adapter_settings.json # NEW — OIDC credentials and token endpoint ├── logger.js # EXISTING — no changes └── server.js # EXISTING — no changes required tests/ ├── unit/ │ └── proxy.test.js # NEW — unit tests: cache, expiry, stampede, error paths └── contract/ └── proxy-http.test.js # NEW — contract tests: HTTP 200/401 response shape ``` **Structure Decision**: Single-project layout (Option 1). Two new source files, two new test files. No new directories (both `src/proxyScripts/` and `src/globalVariables/` already exist as defined directories in the constitution; `tests/unit/` and `tests/contract/` match the existing `package.json` test scripts). ## Complexity Tracking > No constitution violations — this section is intentionally empty.