// Test-only HTTP adapter implementing the KV HTTP contract, so the shared // contract suite can run against the real client over a real request / response // boundary. It keeps its state in memory: the server-side Postgres backend is a // separate part, and this file must not quietly become one. // // It is a conformance harness, not a reference server. Its credential handling // exists to exercise the contract's authentication and authorization responses; // it is not an authentication model. Deriving a principal from an authenticated // session belongs to the reference server. import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'; import { assertJsonValue } from '../src/runtime/json-value.js'; export interface KvConformanceServer { baseUrl: string; fetch: typeof globalThis.fetch; close(): Promise; } export interface KvConformanceServerOptions { /** Bind a loopback TCP port. Tests can disable this in network-restricted sandboxes. */ listen?: boolean; /** Request body ceiling; a larger body is rejected with `413`. */ maxBodyBytes?: number; /** Return false to answer `401 UNAUTHENTICATED`. Defaults to allowing everything. */ authenticate?: (req: IncomingMessage) => boolean; /** Return false to answer `403`. Defaults to allowing everything. */ authorize?: (req: IncomingMessage, area: 'kv') => boolean; } /** One principal's view. The namespace header stands in for a principal. */ interface Namespace { kv: Map; } class ConformanceHttpError extends Error { constructor( readonly status: number, readonly code: string, message: string, ) { super(message); } } function sendJson( res: ServerResponse, status: number, body: unknown, extraHeaders: Record = {}, ): void { res.writeHead(status, { 'content-type': 'application/json', ...extraHeaders }); res.end(JSON.stringify(body)); } /** * The contract requires reads to be uncacheable, so the harness has to send the * header itself — otherwise the suite would pass against an implementation that * omits it, which is precisely the implementation the requirement exists to * catch. */ const NO_STORE = { 'cache-control': 'no-store' }; function sendNoContent(res: ServerResponse): void { res.writeHead(204); res.end(); } function errorResponse(error: unknown): { status: number; body: unknown } { if (error instanceof ConformanceHttpError) { return { status: error.status, body: { error: { code: error.code, message: error.message } } }; } // Never echo an internal failure's message: the contract's INTERNAL_ERROR row // promises the handler does not expose internal details. return { status: 500, body: { error: { code: 'INTERNAL_ERROR', message: '@openmaic/storage: internal server error' }, }, }; } async function readBytes(req: IncomingMessage, maxBodyBytes: number): Promise { const chunks: Buffer[] = []; let total = 0; for await (const chunk of req) { const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : (chunk as Buffer); total += buffer.length; if (total > maxBodyBytes) { throw new ConformanceHttpError( 413, 'PAYLOAD_TOO_LARGE', `@openmaic/storage: request body exceeds ${maxBodyBytes} bytes`, ); } chunks.push(buffer); } return Buffer.concat(chunks); } async function readJson(req: IncomingMessage, maxBodyBytes: number): Promise { const raw = await readBytes(req, maxBodyBytes); if (raw.length === 0) { throw new ConformanceHttpError(400, 'VALIDATION_FAILED', 'request body must be a JSON object'); } let body: unknown; try { body = JSON.parse(raw.toString('utf8')) as unknown; } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new ConformanceHttpError(400, 'VALIDATION_FAILED', message); } if (typeof body !== 'object' || body === null || Array.isArray(body)) { throw new ConformanceHttpError(400, 'VALIDATION_FAILED', 'request body must be a JSON object'); } return body as T; } // The server imposes **no** key-domain rules: a key arrives percent-decoded and // is stored as a plain Map key, so any string — empty, containing NUL, `/`, `\`, // a `..`, of any length — is a legitimate opaque key that traverses nothing. The // only transport-level rejections are a malformed percent-escape (handled in // pathParts) and, automatically, Node's own request-target size limit; both are // properties of the HTTP transport, not of the key. Prefixes are opaque too. function routeNotFound(res: ServerResponse): void { sendJson(res, 404, { error: { code: 'ROUTE_NOT_FOUND', message: 'route not found' } }); } function pathParts(req: IncomingMessage): { parts: string[]; url: URL } { const target = req.url ?? '/'; const url = new URL(target, 'http://conformance.invalid'); // Split the RAW request target, not `url.pathname`. The WHATWG parser resolves // dot segments before anything here can look, and it treats `%2e` as one — so // `/kv/entries/%2e%2e` arrives already collapsed, and a validator reading the // parsed path would be inspecting a request nobody sent. The rules exist to // reject what was *received*, so the segments come from the wire. const rawPath = target.split(/[?#]/, 1)[0] ?? '/'; const rawParts = rawPath.split('/'); if (rawParts[0] === '') rawParts.shift(); const parts: string[] = []; for (const part of rawParts) { let decoded: string; try { decoded = decodeURIComponent(part); } catch { // A malformed escape (`%`, `%zz`, a surrogate encoding) is a bad request, // not a server fault; the native URIError would otherwise surface as the // contract's INTERNAL_ERROR row. throw new ConformanceHttpError( 400, 'VALIDATION_FAILED', '@openmaic/storage: request path is not valid percent-encoded UTF-8', ); } parts.push(decoded); } return { parts, url }; } /** * Every header spelling that would convey a scope. The contract has no scope * header, so a request carrying any of these is trying to describe one and must * fail loud rather than have its intent silently discarded. Enumerated because * "just `x-scope`" left the others open — a client could reach for `scope`, * `kv-scope`, or the `x-` prefixed variants just as naturally. */ const PROHIBITED_SCOPE_HEADERS = ['scope', 'x-scope', 'kv-scope', 'x-kv-scope']; /** * The contract has no scope anywhere on the wire, so a client that invents one * has to fail loud. That covers every channel a scope could hide in: a path * segment, the query string, any scope-spelling header, and (elsewhere) the body. */ function assertNoScopeChannel(req: IncomingMessage, url: URL, parts: string[]): void { // A scope path segment — `/kv/device/keys`, `/kv/account/entries/k` — is an // attempt to route by scope. The contract's segment after `kv` is `entries` or // `keys`; a scope name there is rejected, not silently 404'd. (A *key* named // `device` is fine: that sits at parts[2], after `entries`.) if (parts[1] === 'device' || parts[1] === 'account') { throw new ConformanceHttpError( 400, 'VALIDATION_FAILED', '@openmaic/storage: kv requests must not carry a scope path segment — this contract is ' + 'account-scoped and the principal is derived server-side', ); } if (url.searchParams.has('scope')) { throw new ConformanceHttpError( 400, 'VALIDATION_FAILED', '@openmaic/storage: kv requests must not carry a scope query parameter — this contract is ' + 'account-scoped and the principal is derived server-side', ); } for (const header of PROHIBITED_SCOPE_HEADERS) { if (req.headers[header] !== undefined) { throw new ConformanceHttpError( 400, 'VALIDATION_FAILED', `@openmaic/storage: kv requests must not carry a scope header (${header})`, ); } } } interface RouteContext { state: Namespace; parts: string[]; url: URL; maxBodyBytes: number; } /** * A body is a scope channel too, and the two bodyless methods (GET, DELETE) have * no legitimate use for one — so any body on them is refused outright rather than * parsed for the scope it might be smuggling. A GET route that simply ignored * the body would let `GET /kv/keys` with `{"scope":"device"}` succeed, which is * exactly the "every channel is closed" guarantee failing silently. PUT reads * its body and rejects a scope *field* there instead, since it legitimately * carries `value`. */ async function assertNoRequestBody(req: IncomingMessage, maxBodyBytes: number): Promise { const body = await readBytes(req, maxBodyBytes); if (body.length > 0) { throw new ConformanceHttpError( 400, 'VALIDATION_FAILED', `@openmaic/storage: ${req.method ?? 'GET'} must not carry a request body — this contract is ` + 'account-scoped and the principal is derived server-side', ); } } async function routeKv( req: IncomingMessage, res: ServerResponse, context: RouteContext, ): Promise { const { state, parts, url, maxBodyBytes } = context; const method = req.method ?? 'GET'; assertNoScopeChannel(req, url, parts); // Close the body channel on every bodyless method up front, so no GET route // can forget to look. PUT is the one method that reads a body, and it checks // the body for a scope field itself. if (method === 'GET' || method === 'DELETE') { await assertNoRequestBody(req, maxBodyBytes); } if (method === 'GET' && parts.length === 2 && parts[1] === 'keys') { const prefix = url.searchParams.get('prefix') ?? ''; // A literal, byte-for-byte prefix comparison over opaque keys. Spelled out // because the obvious SQL translation is `LIKE prefix || '%'`, where an // unescaped `%`, `_`, or `\` in a caller-supplied prefix silently becomes a // wildcard or an escape. sendJson( res, 200, [...state.kv.keys()].filter((key) => key.startsWith(prefix)), NO_STORE, ); return true; } if (parts.length === 3 && parts[1] === 'entries') { // The decoded key is opaque and stored as a plain Map key — no validation. const key = parts[2]!; if (method === 'GET') { const raw = state.kv.get(key); if (raw === undefined) { throw new ConformanceHttpError( 404, 'KEY_NOT_FOUND', `@openmaic/storage: no kv entry ${JSON.stringify(key)}`, ); } sendJson(res, 200, { value: JSON.parse(raw) as unknown }, NO_STORE); return true; } if (method === 'PUT') { const body = await readJson<{ value?: unknown }>(req, maxBodyBytes); if (!('value' in body)) { throw new ConformanceHttpError( 400, 'VALIDATION_FAILED', '@openmaic/storage: kv write body must carry "value"', ); } if ('scope' in body) { throw new ConformanceHttpError( 400, 'VALIDATION_FAILED', '@openmaic/storage: kv write body must not carry a scope — this contract is ' + 'account-scoped and the principal is derived server-side', ); } try { assertJsonValue(body.value, `kv value for key ${JSON.stringify(key)}`); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new ConformanceHttpError(400, 'VALIDATION_FAILED', message); } state.kv.set(key, JSON.stringify(body.value)); sendNoContent(res); return true; } if (method === 'DELETE') { // The body was already refused up front (bodyless method); nothing to do // here but delete. state.kv.delete(key); sendNoContent(res); return true; } } return false; } /** * Start a test-only HTTP adapter. Each `x-storage-namespace` header selects a * fresh in-memory namespace, so factories used by the shared contract suite stay * isolated. */ export async function startKvConformanceServer( options: KvConformanceServerOptions = {}, ): Promise { const namespaces = new Map(); const maxBodyBytes = options.maxBodyBytes ?? 32 * 1024 * 1024; const authenticate = options.authenticate ?? (() => true); const authorize = options.authorize ?? (() => true); const namespaceFor = (name: string): Namespace => { let state = namespaces.get(name); if (!state) { state = { kv: new Map() }; namespaces.set(name, state); } return state; }; const namespaceNameFor = (req: IncomingMessage): string => { const header = req.headers['x-storage-namespace']; return typeof header === 'string' && header !== '' ? header : 'default'; }; let baseUrl = 'http://kv-conformance.invalid'; const route = async (req: IncomingMessage, res: ServerResponse): Promise => { const { parts, url } = pathParts(req); if (parts[0] !== 'kv') { routeNotFound(res); return; } if (!authenticate(req)) { throw new ConformanceHttpError( 401, 'UNAUTHENTICATED', '@openmaic/storage: missing or invalid credential', ); } if (!authorize(req, 'kv')) { throw new ConformanceHttpError( 403, 'FORBIDDEN_KV', '@openmaic/storage: principal may not perform this operation', ); } const context: RouteContext = { state: namespaceFor(namespaceNameFor(req)), parts, url, maxBodyBytes, }; if (!(await routeKv(req, res, context))) routeNotFound(res); }; const server = createServer((req, res) => { void route(req, res).catch((error: unknown) => { const mapped = errorResponse(error); sendJson(res, mapped.status, mapped.body); }); }); if (options.listen !== false) { await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); }); const address = server.address(); if (address === null || typeof address === 'string') { throw new Error('KV conformance server did not bind a TCP port'); } baseUrl = `http://127.0.0.1:${address.port}`; } const injectedFetch: typeof globalThis.fetch = async (input, init) => { const request = new Request(input, init); const url = new URL(request.url); const requestBody = Buffer.from(await request.arrayBuffer()); // Recover the raw request target when the caller passed a string. `Request` // normalizes dot segments — including `%2e` — exactly as `URL` does, so // building the target from the parsed URL would hide from this server the // very inputs a real client can put on the wire. const rawTarget = typeof input === 'string' ? input.slice(url.origin.length) || '/' : `${url.pathname}${url.search}`; const fakeRequest = { method: request.method, url: rawTarget, headers: Object.fromEntries(request.headers.entries()), async *[Symbol.asyncIterator]() { if (requestBody.length > 0) yield requestBody; }, } as unknown as IncomingMessage; let status = 200; let responseHeaders: Record = {}; let responseBody: string | Uint8Array | undefined; const fakeResponse = { writeHead(nextStatus: number, headers?: Record) { status = nextStatus; responseHeaders = headers ?? {}; return this; }, end(chunk?: string | Buffer) { // Copy into a plain Uint8Array: a Node `Buffer` is one structurally but // does not satisfy `BodyInit` under the DOM lib's narrowing. responseBody = typeof chunk === 'string' || chunk === undefined ? chunk : new Uint8Array(chunk); return this; }, } as unknown as ServerResponse; try { await route(fakeRequest, fakeResponse); } catch (error) { const mapped = errorResponse(error); status = mapped.status; responseHeaders = { 'content-type': 'application/json' }; responseBody = JSON.stringify(mapped.body); } return new Response(status === 204 ? null : responseBody, { status, headers: responseHeaders, }); }; return { baseUrl, fetch: injectedFetch, close: () => server.listening ? new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }) : Promise.resolve(), }; }