Files
kme_content_adapter/specs/001-oidc-proxy-script/plan.md

6.1 KiB

Implementation Plan: OIDC Proxy Script Authentication

Branch: 001-oidc-proxy-script | Date: 2025-07-17 | Spec: 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)

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)

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.