Files
kme_content_adapter/specs/001-oidc-proxy-script/contracts/vm-context.md

7.1 KiB

Contract: VM Context Dependencies

Feature: 001-oidc-proxy-script
File: src/proxyScripts/proxy.js
Injector: src/server.js via vm.createContext()
Date: 2025-07-17


Overview

proxy.js runs in a Node.js VM sandbox. It has zero access to the Node.js module system (require, import). All dependencies are injected by server.js through the vm.createContext() object. This document specifies exactly what proxy.js depends on and what server.js must provide.


Required Context Variables

These MUST be present in every vm.createContext() call. Their absence will cause a runtime ReferenceError inside the sandbox.

adapter_settings — OIDC Configuration

Loaded from src/globalVariables/adapter_settings.json by loadGlobalVariables().

Property Type Required Used for
tokenUrl string OIDC token endpoint URL (POST target)
username string ROPC grant — resource owner username
password string ROPC grant — resource owner password
clientId string OAuth 2.0 client_id parameter
scope string OAuth 2.0 scope parameter
_pendingFetch Promise | null | undefined In-flight stampede guard, initialised by proxy.js at runtime

The _pendingFetch property is NOT in the JSON file; proxy.js adds it at runtime. Token and expiry are stored in Redis, not on this object.

redis — Redis Client

Injected as a global. Used for persistent token caching across VM invocations.

Method used Signature Purpose
redis.hSet(key, field, value) Promise<number> Store token or expiry in hash
redis.hGet(key, field) Promise<string | null> Read token or expiry from hash

Usage pattern in proxy.js:

// Write
await redis.hSet('authorization', 'token', idToken);
await redis.hSet('authorization', 'expiry', String(expiresIn));

// Read
const token  = await redis.hGet('authorization', 'token');
const expiry = parseFloat(await redis.hGet('authorization', 'expiry') ?? '0');

axios — HTTP Client

Standard axios instance from the axios npm package (v1.x).

Method used Signature Purpose
axios.post(url, data, config) Promise<AxiosResponse> POST to token endpoint

Config properties used by proxy.js:

{
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  timeout: 5000   // milliseconds — FR-014
}

URLSearchParams — Form Body Builder

Global Web API (Node.js 18+ built-in).

Usage Purpose
new URLSearchParams({ ... }) Build application/x-www-form-urlencoded body

console — Structured Logger

Custom logger from src/logger.js (injected as console so proxy.js uses it transparently).

Method Used when
console.error({ message, error }) Authentication failure or unexpected error

req — HTTP Request Object

Node.js http.IncomingMessage, fresh per request.

Property Type Usage
(none) proxy.js does not read any request properties in this feature; req is present by convention

res — HTTP Response Object

Node.js http.ServerResponse, fresh per request.

Method Signature Called when
res.writeHead(statusCode, headers) void Before sending body
res.end(body) void Send response body and close

Optional / Unused Context Variables

These are injected by server.js (globalVMContext) and are available to proxy.js but not used by this feature's implementation:

Variable Type Reason unused
URL Web API URL parsing not needed
crypto Web Crypto API No UUID or crypto ops in this script
jwt jsonwebtoken No JWT signing/verification needed
uuidv4 uuid function No request-ID generation needed
xmlBuilder xmlbuilder2 No XML output

Injection Pattern (server.js)

// Static VM context (compiled once at startup)
const globalVMContext = {
  URLSearchParams,       // ← used by proxy.js
  URL,
  console: logger,       // ← used by proxy.js
  crypto,
  axios,                 // ← used by proxy.js
  uuidv4,
  jwt,
  xmlBuilder,
  redis,                 // ← used by proxy.js (token cache)
};

// Dynamic data (loaded from src/globalVariables/ at startup)
let globalVariableContext = {};
loadGlobalVariables(); // populates globalVariableContext.adapter_settings

// Per-request context (fresh sandbox each time)
const context = vm.createContext({
  ...globalVMContext,           // spread — object refs, not clones
  ...globalVariableContext,     // includes adapter_settings ← used by proxy.js
  req,                          // fresh per request
  res,                          // fresh per request
});
script.runInContext(context);

State Ownership

State Lives on Lifetime Owned by
OIDC credentials adapter_settings (JSON properties) Process lifetime server.js (loads from file)
Cached token Redis hash authorization, field token Until expiry / Redis flush proxy.js (writes on fetch)
Token expiry Redis hash authorization, field expiry Until expiry / Redis flush proxy.js (writes on fetch)
In-flight fetch promise adapter_settings._pendingFetch (runtime property) Duration of one fetch proxy.js

Key invariant: adapter_settings is the same JS object reference in every vm.createContext() call. The _pendingFetch property written by proxy.js persists across requests for stampede guarding. Token data is read from and written to Redis, making it durable across adapter restarts.


Test Contract (Minimal Fake Context)

A test context satisfying this contract:

// tests/unit/proxy.test.js
import vm from 'node:vm';

function makeContext(t, overrides = {}) {
  // In-memory Redis hash store fake
  const _store = {};
  const redis = {
    hSet: t.mock.fn(async (key, field, value) => {
      _store[`${key}:${field}`] = value;
      return 1;
    }),
    hGet: t.mock.fn(async (key, field) => _store[`${key}:${field}`] ?? null),
  };

  let statusCode = null;
  let body = '';
  const res = {
    writeHead: t.mock.fn((code) => { statusCode = code; }),
    end: t.mock.fn((b = '') => { body += String(b); }),
    get statusCode() { return statusCode; },
    get body() { return body; },
  };
  const adapter_settings = {
    tokenUrl: 'https://auth.example.com/token',
    username: 'testuser', password: 'testpass',
    clientId: 'test-client', scope: 'openid',
  };
  return vm.createContext({
    URLSearchParams,
    console,
    axios: {
      post: t.mock.fn(async () => ({
        data: { id_token: 'mock-token', expires_in: 9_999_999_999 },
      })),
    },
    redis,
    adapter_settings,
    req: { url: '/', method: 'GET', headers: {} },
    res,
    ...overrides,
  });
}