From 39d8f330f1e8f53af66261b5f7c26e2dc1b85f36 Mon Sep 17 00:00:00 2001 From: Ayobami Haastrup <47716486+AyobamiH@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:04:38 +0100 Subject: [PATCH 1/2] Add effect-free authenticated readiness for exact-version cloud acceptance Separate dependency checks from the scheduler and job execution. Probe only three read-only schema RPCs with bounded responses and cancellation; require an exact version/SHA and fresh nonce, redact errors and prohibit caching. Add 73 synthetic regressions and a credential-free standard-runner check. No production deployment, schema change, model request or provider call. --- .github/workflows/readiness-contract.yml | 36 +++++ src/cloudflare-worker.ts | 5 + src/worker-readiness.ts | 159 ++++++++++++++++++ test/cloudflare-readiness.test.ts | 196 +++++++++++++++++++++++ 4 files changed, 396 insertions(+) create mode 100644 .github/workflows/readiness-contract.yml create mode 100644 src/worker-readiness.ts create mode 100644 test/cloudflare-readiness.test.ts diff --git a/.github/workflows/readiness-contract.yml b/.github/workflows/readiness-contract.yml new file mode 100644 index 0000000..653e478 --- /dev/null +++ b/.github/workflows/readiness-contract.yml @@ -0,0 +1,36 @@ +name: Authenticated readiness contract + +on: + pull_request: + paths: ['src/**', 'test/**', '.github/workflows/readiness-contract.yml'] + push: + branches: [main, codex/cloud-readiness-20260929] + paths: ['src/**', 'test/**', '.github/workflows/readiness-contract.yml'] + +permissions: + contents: read + +concurrency: + group: readiness-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + readiness: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.sha }} + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: '24' + - run: npm ci --ignore-scripts --no-audit + - run: npm run typecheck + - name: Effect-free authenticated readiness and existing liveness contracts + run: | + node --import tsx --test test/cloudflare-readiness.test.ts + node --import tsx test/cloudflare-health.test.ts + - name: No generated or fixture files changed the source + run: git diff --exit-code diff --git a/src/cloudflare-worker.ts b/src/cloudflare-worker.ts index b9b0bb7..40b99a8 100644 --- a/src/cloudflare-worker.ts +++ b/src/cloudflare-worker.ts @@ -1,3 +1,4 @@ +import { handleWorkerReadiness } from './worker-readiness'; import { createExclusiveRunGate } from './exclusive-run-gate'; import { installScopedConfig, runWithRuntimeScope } from './runtime-scope'; @@ -193,6 +194,10 @@ export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); + if (url.pathname === '/readyz') { + return handleWorkerReadiness(request, env); + } + if (url.pathname === '/healthz') { applyCloudflareEnv(env); return Response.json(healthPayload(env)); diff --git a/src/worker-readiness.ts b/src/worker-readiness.ts new file mode 100644 index 0000000..478a340 --- /dev/null +++ b/src/worker-readiness.ts @@ -0,0 +1,159 @@ +import { timingSafeEqual } from 'node:crypto'; + +// Operator-only, effect-free probe. Do not import config, scheduler, job claims, +// provider clients or loggers here: liveness must never initialise execution. +export interface ReadinessEnv { + WORKER_TICK_TOKEN?: string; + SUPABASE_URL?: string; + SUPABASE_SERVICE_ROLE_KEY?: string; + SUPABASE_SECRET_KEY?: string; + SERVICE_ROLE_KEY?: string; + CREDENTIAL_ENCRYPTION_KEY?: string; + CF_VERSION_METADATA?: { id: string; tag?: string; timestamp: string }; + SUPABASE_WORKER_GENERATION_ENABLED?: string; + SUPABASE_PROVIDER_DISPATCH_ENABLED?: string; +} + +export const READINESS_CONTRACTS = [ + { + name: 'worker_claims', rpc: 'get_worker_schema_contract', contract: 'worker-claims-v1', + capabilities: ['source-targeted-claim-v1', 'angle-targeted-claim-v1', 'angle-exhaust-fenced-v1', + 'source-angle-atomic-commit-v1', 'angle-queue-atomic-commit-v1', 'queue-angle-identity-v1', + 'legacy-queue-revision-hold-v1'], + }, + { + name: 'publication', rpc: 'get_publication_schema_contract', contract: 'publication-ledger-v1', + capabilities: ['publication-intent-claim-v1', 'publication-dispatch-boundary-v1', + 'publication-attempt-outcome-v1', 'publication-unknown-reconciliation-v1', + 'publication-exact-history-receipt-v1', 'publication-queue-compatibility-fence-v1', + 'publication-provenance-snapshot-v1', 'publication-legacy-queue-hold-v1', 'publication-queue-lock-order-v1'], + }, + { + name: 'agent_jobs', rpc: 'get_agent_jobs_contract', contract: 'agent-jobs-v1', + capabilities: ['durable-enqueue-v1', 'fenced-terminal-v1', 'reconciliation-only-recovery-v1', 'rpc-only-job-writes-v1'], + }, +] as const; + +const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i; +const SHA = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i; +const HEADERS = { 'Cache-Control': 'no-store, private, max-age=0', Vary: 'Authorization, X-OCPF-Readiness-Nonce' }; +const MAX_BODY = 65536; +const TIMEOUT_MS = 10000; + +function authorised(request: Request, token: string | undefined): boolean { + if (!token || token.length > 4096) return false; + const received = request.headers.get('Authorization') || ''; + const expected = `Bearer ${token}`; + const a = Buffer.from(received), b = Buffer.from(expected); + return a.length === b.length && timingSafeEqual(a, b); +} + +async function jsonBody(response: Response): Promise> { + const declared = response.headers.get('content-length'); + if (declared !== null && (!/^\d+$/.test(declared) || Number(declared) > MAX_BODY)) { + await response.body?.cancel(); + throw new Error('invalid_body'); + } + if (!response.body) throw new Error('invalid_body'); + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let size = 0; + try { + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + size += value.byteLength; + if (size > MAX_BODY) throw new Error('invalid_body'); + chunks.push(value); + } + const parsed: unknown = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks))); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('invalid_body'); + return parsed as Record; + } finally { + await reader.cancel().catch(() => {}); + reader.releaseLock(); + } +} + +export async function handleWorkerReadiness( + request: Request, + env: ReadinessEnv, + fetchImpl: typeof fetch = fetch, +): Promise { + // Invalid auth is indistinguishable from a missing route. No dependency reads. + if (request.method !== 'GET' || !authorised(request, env.WORKER_TICK_TOKEN)) { + return new Response('Not found', { status: 404, headers: HEADERS }); + } + const nonce = request.headers.get('X-OCPF-Readiness-Nonce'); + if (!nonce || !UUID.test(nonce)) { + return Response.json({ ok: false, code: 'readiness_nonce_required' }, { status: 400, headers: HEADERS }); + } + const metadata = env.CF_VERSION_METADATA; + const identity = { + workerVersionId: metadata && UUID.test(metadata.id) ? metadata.id : null, + gitSha: metadata?.tag && SHA.test(metadata.tag) ? metadata.tag : null, + }; + const envelope = { + schema: 'ocpf.worker-readiness.v1', readOnly: true, executionAuthorised: false, + nonce, release: identity, + controls: { + generationDisabled: env.SUPABASE_WORKER_GENERATION_ENABLED === 'false', + publishingDisabled: env.SUPABASE_PROVIDER_DISPATCH_ENABLED === 'false', + }, + }; + const blocked = (code: string) => Response.json( + { ...envelope, ok: false, readiness: 'blocked', code }, { status: 503, headers: HEADERS }, + ); + if (!identity.workerVersionId || !identity.gitSha) return blocked('release_identity_unavailable'); + // Bind readiness to the caller's exact reviewed version, not any healthy code. + if (request.headers.get('X-OCPF-Expected-Version') !== identity.workerVersionId + || request.headers.get('X-OCPF-Expected-Sha') !== identity.gitSha) return blocked('release_identity_mismatch'); + const key = env.SUPABASE_SERVICE_ROLE_KEY || env.SUPABASE_SECRET_KEY || env.SERVICE_ROLE_KEY; + if (!key?.trim() || !env.CREDENTIAL_ENCRYPTION_KEY?.trim()) return blocked('runtime_configuration_incomplete'); + let origin: string; + try { + const u = new URL(env.SUPABASE_URL || ''); + // The URL comes solely from trusted bindings, never from the request. + if (u.protocol !== 'https:' || u.username || u.password || u.search || u.hash || u.pathname !== '/') { + return blocked('database_origin_invalid'); + } + origin = u.origin; + } catch { return blocked('database_origin_invalid'); } + if (request.signal.aborted) return blocked('readiness_cancelled'); + const controller = new AbortController(); + const abort = () => controller.abort(); + request.signal.addEventListener('abort', abort, { once: true }); + const timeout = setTimeout(abort, TIMEOUT_MS); + try { + const dependencies = await Promise.all(READINESS_CONTRACTS.map(async expected => { + let response: Response | undefined; + try { + response = await fetchImpl(`${origin}/rest/v1/rpc/${expected.rpc}`, { + method: 'POST', body: '{}', redirect: 'error', cache: 'no-store', signal: controller.signal, + headers: { Authorization: `Bearer ${key}`, apikey: key, 'Content-Type': 'application/json', Accept: 'application/json' }, + }); + if (response.status !== 200) { + await response.body?.cancel(); + return { name: expected.name, state: 'http_error', status: response.status }; + } + const body = await jsonBody(response); + const capabilities = body.capabilities; + let compatible = body.contract === expected.contract && Array.isArray(capabilities) + && expected.capabilities.every(c => capabilities.includes(c)); + if (expected.name === 'publication') compatible = compatible + && body.migration === '20260907054000' && body.lock_order_migration === '20260913061000'; + return { name: expected.name, state: compatible ? 'verified' : 'contract_mismatch' }; + } catch { + // Never return DB bodies, URLs, SQL messages, exception text or secrets. + return { name: expected.name, state: controller.signal.aborted ? 'cancelled_or_timeout' : response ? 'invalid_response' : 'transport_error' }; + } + })); + const ready = !controller.signal.aborted && dependencies.every(d => d.state === 'verified'); + return Response.json({ ...envelope, ok: ready, readiness: ready ? 'dependencies_verified' : 'blocked', dependencies }, { + status: ready ? 200 : 503, headers: HEADERS, + }); + } finally { + clearTimeout(timeout); + request.signal.removeEventListener('abort', abort); + } +} diff --git a/test/cloudflare-readiness.test.ts b/test/cloudflare-readiness.test.ts new file mode 100644 index 0000000..e86959c --- /dev/null +++ b/test/cloudflare-readiness.test.ts @@ -0,0 +1,196 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { readFileSync } from 'node:fs'; +import worker from '../src/cloudflare-worker'; +import { handleWorkerReadiness, READINESS_CONTRACTS, type ReadinessEnv } from '../src/worker-readiness'; + +const version = '11111111-1111-4111-8111-111111111111'; +const nonce = '22222222-2222-4222-8222-222222222222'; +const sha = 'a'.repeat(40); +const env: ReadinessEnv = { + WORKER_TICK_TOKEN: 'tick-secret-not-for-output', SUPABASE_URL: 'https://database.example', + SUPABASE_SERVICE_ROLE_KEY: 'database-secret-not-for-output', CREDENTIAL_ENCRYPTION_KEY: 'encryption-secret-not-for-output', + CF_VERSION_METADATA: { id: version, tag: sha, timestamp: '2026-09-29T00:00:00Z' }, + SUPABASE_WORKER_GENERATION_ENABLED: 'false', SUPABASE_PROVIDER_DISPATCH_ENABLED: 'false', +}; +function request(headers: Record = {}, method = 'GET', signal?: AbortSignal) { + return new Request('https://staging.example/readyz', { method, signal, headers: { + Authorization: `Bearer ${env.WORKER_TICK_TOKEN}`, 'X-OCPF-Readiness-Nonce': nonce, + 'X-OCPF-Expected-Version': version, 'X-OCPF-Expected-Sha': sha, ...headers, + } }); +} +function fixture(change: (body: Record, rpc: string) => Response | void = () => {}) { + const calls: { url: string; init?: RequestInit }[] = []; + const fn = async (input: Parameters[0], init?: RequestInit): Promise => { + const url = String(input); calls.push({ url, init }); + const rpc = url.split('/').pop()!; + const spec = READINESS_CONTRACTS.find(s => s.rpc === rpc); + assert.ok(spec, 'A probe must never call an unapproved endpoint'); + const body: Record = { contract: spec.contract, capabilities: [...spec.capabilities] }; + if (spec.name === 'publication') Object.assign(body, { migration: '20260907054000', lock_order_migration: '20260913061000' }); + return change(body, rpc) || Response.json(body); + }; + return { calls, fetch: fn as typeof fetch }; +} +async function probe(change?: Parameters[0], e: ReadinessEnv = env, req = request()) { + const f = fixture(); + const selected = change ? fixture(change) : f; + const response = await handleWorkerReadiness(req, e, selected.fetch); + return { response, body: await response.json() as any, calls: selected.calls }; +} + +test('readiness proves exact identity and all schema capabilities without executing a job', async () => { + const { response, body, calls } = await probe(); + assert.equal(response.status, 200); assert.equal(body.ok, true); + assert.equal(body.readOnly, true); assert.equal(body.executionAuthorised, false); + assert.equal(body.nonce, nonce); assert.equal(body.release.workerVersionId, version); assert.equal(body.release.gitSha, sha); + assert.equal(body.readiness, 'dependencies_verified'); assert.equal(calls.length, 3); + for (const { url, init } of calls) { + assert.ok(READINESS_CONTRACTS.some(c => url === `https://database.example/rest/v1/rpc/${c.rpc}`)); + assert.equal(init?.method, 'POST'); assert.equal(init?.body, '{}'); assert.equal(init?.redirect, 'error'); + assert.equal(init?.cache, 'no-store'); assert.ok(init?.signal instanceof AbortSignal); + assert.equal((init?.headers as Record).apikey, env.SUPABASE_SERVICE_ROLE_KEY); + } +}); + +for (const auth of ['', 'Bearer wrong', 'bearer tick-secret-not-for-output']) { + test(`invalid authorisation makes zero calls (${auth.length})`, async () => { + const f = fixture(); const r = await handleWorkerReadiness(request({ Authorization: auth }), env, f.fetch); + assert.equal(r.status, 404); assert.equal(await r.text(), 'Not found'); assert.equal(f.calls.length, 0); + }); +} +for (const method of ['POST', 'PUT', 'DELETE', 'HEAD']) test(`${method} cannot turn readiness into execution`, async () => { + const f = fixture(); const r = await handleWorkerReadiness(request({}, method), env, f.fetch); + assert.equal(r.status, 404); assert.equal(f.calls.length, 0); +}); + +test('missing server token is rejected even when other configuration is absent', async () => { + const f = fixture(); const r = await handleWorkerReadiness(request(), {}, f.fetch); + assert.equal(r.status, 404); assert.equal(f.calls.length, 0); +}); +for (const invalid of ['', 'x', 'not-a-uuid', 'a'.repeat(1000)]) test(`invalid nonce (${invalid.length}) is rejected before database access`, async () => { + const { response, calls } = await probe(undefined, env, request({ 'X-OCPF-Readiness-Nonce': invalid })); + assert.equal(response.status, 400); assert.equal(calls.length, 0); +}); +for (const header of ['X-OCPF-Expected-Version', 'X-OCPF-Expected-Sha']) test(`${header} mismatch is not readiness`, async () => { + const { response, body, calls } = await probe(undefined, env, request({ [header]: 'wrong' })); + assert.equal(response.status, 503); assert.equal(body.code, 'release_identity_mismatch'); assert.equal(calls.length, 0); +}); +for (const metadata of [undefined, { id: 'wrong', tag: sha, timestamp: '' }, { id: version, tag: 'main', timestamp: '' }]) test(`missing/unattested runtime identity cannot pass ${JSON.stringify(metadata)}`, async () => { + const { response, calls } = await probe(undefined, { ...env, CF_VERSION_METADATA: metadata }); + assert.equal(response.status, 503); assert.equal(calls.length, 0); +}); +for (const key of ['SUPABASE_SERVICE_ROLE_KEY', 'CREDENTIAL_ENCRYPTION_KEY'] as const) test(`missing ${key} fails without a dependency call`, async () => { + const { response, calls } = await probe(undefined, { ...env, [key]: '' }); + assert.equal(response.status, 503); assert.equal(calls.length, 0); +}); +for (const url of ['', 'not-a-url', 'http://127.0.0.1:54321', 'https://name:secret@example.com', 'https://example.com/rest/v1', 'https://example.com/?key=secret', 'https://example.com/#key']) { + test(`unusable configured origin fails closed (${url})`, async () => { + const { response, calls } = await probe(undefined, { ...env, SUPABASE_URL: url }); + assert.equal(response.status, 503); assert.equal(calls.length, 0); + }); +} +for (const spec of READINESS_CONTRACTS) { + for (const capability of spec.capabilities) test(`missing ${capability} cannot pass`, async () => { + const { response, body } = await probe((b, rpc) => { if (rpc === spec.rpc) b.capabilities = b.capabilities.filter((c: string) => c !== capability); }); + assert.equal(response.status, 503); assert.ok(body.dependencies.some((d: any) => d.name === spec.name && d.state === 'contract_mismatch')); + }); + test(`wrong ${spec.name} contract fails closed`, async () => { + const { response } = await probe((b, rpc) => { if (rpc === spec.rpc) b.contract = 'invented'; }); + assert.equal(response.status, 503); + }); +} +for (const field of ['migration', 'lock_order_migration']) test(`wrong publication ${field} cannot pass`, async () => { + const { response } = await probe((b, rpc) => { if (rpc === 'get_publication_schema_contract') b[field] = '20200101000000'; }); + assert.equal(response.status, 503); +}); +for (const status of [301, 401, 403, 404, 429, 500]) test(`dependency HTTP ${status} is not retried or disclosed`, async () => { + const { response, body, calls } = await probe(() => new Response('secret: '+env.SUPABASE_SERVICE_ROLE_KEY, { status })); + assert.equal(response.status, 503); assert.equal(calls.length, 3); + assert.ok(body.dependencies.every((d: any) => d.state === 'http_error' && d.status === status)); + assert.ok(!JSON.stringify(body).includes(env.SUPABASE_SERVICE_ROLE_KEY!)); +}); +for (const invalid of ['bad gateway', '[]', 'null', '1', 'x'.repeat(65537)]) test(`invalid response (${invalid.length}) is bounded and blocked`, async () => { + const { response } = await probe(() => new Response(invalid)); assert.equal(response.status, 503); +}); + +test('streaming response exceeding the cap is cancelled', async () => { + let cancelled = 0; + const { response } = await probe(() => new Response(new ReadableStream({ + pull(c) { c.enqueue(new Uint8Array(40000)); }, cancel() { cancelled++; }, + }))); + assert.equal(response.status, 503); assert.equal(cancelled, 3); +}); + +test('database transport exceptions never disclose credentials or raw errors', async () => { + const f = async () => { throw new Error('private ' + env.SUPABASE_SERVICE_ROLE_KEY); }; + const r = await handleWorkerReadiness(request(), env, f as typeof fetch); + const text = await r.text(); assert.equal(r.status, 503); + assert.ok(!text.includes(env.SUPABASE_SERVICE_ROLE_KEY!)); assert.ok(text.includes('transport_error')); +}); + +test('pre-cancelled request cannot read dependencies', async () => { + const { response, calls } = await probe(undefined, env, request({}, 'GET', AbortSignal.abort())); + assert.equal(response.status, 503); assert.equal(calls.length, 0); +}); + +test('cancellation aborts dependency reads and cannot return a late success', async () => { + const c = new AbortController(); let aborted = 0; + const f = (_: Parameters[0], init?: RequestInit) => new Promise((_, reject) => { + init!.signal!.addEventListener('abort', () => { aborted++; reject(new Error('cancelled')); }, { once: true }); + }); + const pending = handleWorkerReadiness(request({}, 'GET', c.signal), env, f as typeof fetch); + c.abort(); const r = await pending; assert.equal(r.status, 503); assert.equal(aborted, 3); +}); + +test('readiness has a finite shared dependency deadline', async () => { + const start = Date.now(); + const f = (_: Parameters[0], init?: RequestInit) => new Promise((_, reject) => { + init!.signal!.addEventListener('abort', () => reject(new Error('timeout')), { once: true }); + }); + const r = await handleWorkerReadiness(request(), env, f as typeof fetch); + assert.equal(r.status, 503); assert.ok(Date.now() - start < 13000); +}); + +test('concurrent probes use their own environment and do not change process.env', async () => { + const before = { ...process.env }; const a = fixture(); const b = fixture(); + await Promise.all([ + handleWorkerReadiness(request(), env, a.fetch), + handleWorkerReadiness(request(), { ...env, SUPABASE_SERVICE_ROLE_KEY: 'second-private-key', SUPABASE_URL: 'https://second.example' }, b.fetch), + ]); + assert.deepEqual(Object.keys({ ...before, ...process.env }).filter(k => before[k] !== process.env[k]), []); + assert.ok(a.calls.every(c => c.url.startsWith('https://database.example/') && (c.init!.headers as any).apikey === env.SUPABASE_SERVICE_ROLE_KEY)); + assert.ok(b.calls.every(c => c.url.startsWith('https://second.example/') && (c.init!.headers as any).apikey === 'second-private-key')); +}); + +test('responses are never cacheable and ignore unrelated database fields', async () => { + const { response, body } = await probe(b => { b.secret = env.SUPABASE_SERVICE_ROLE_KEY; b.tenants = ['private-id']; }); + assert.match(response.headers.get('cache-control')!, /no-store/); + assert.match(response.headers.get('vary')!, /Authorization/); + assert.ok(!JSON.stringify(body).includes('private-id')); assert.ok(!JSON.stringify(body).includes(env.SUPABASE_SERVICE_ROLE_KEY!)); +}); + +test('enabled execution controls are reported truthfully, never presented as inert', async () => { + const { body } = await probe(undefined, { ...env, SUPABASE_PROVIDER_DISPATCH_ENABLED: 'true', SUPABASE_WORKER_GENERATION_ENABLED: 'false ' }); + assert.equal(body.controls.generationDisabled, false); assert.equal(body.controls.publishingDisabled, false); + assert.equal(body.executionAuthorised, false); +}); + +test('worker routes /readyz through the effect-free handler without initialising execution', async () => { + const before = { ...process.env }; const original = globalThis.fetch; const f = fixture(); + globalThis.fetch = f.fetch; + try { + const r = await worker.fetch(request(), env as Parameters[1]); + assert.equal(r.status, 200); assert.equal(f.calls.length, 3); + assert.deepEqual(Object.keys({ ...before, ...process.env }).filter(k => before[k] !== process.env[k]), []); + } finally { globalThis.fetch = original; } +}); + +test('readiness capability lists track existing source/angle/publication requirements', () => { + for (const [name, file] of [['worker_claims', 'src/worker-claims.ts'], ['publication', 'src/publication-ledger.ts']]) { + const source = readFileSync(file, 'utf8'); + const block = source.match(/export const REQUIRED_[A-Z_]+ = \[([\s\S]*?)\] as const;/)![1]; + const expected = [...block.matchAll(/'([^']+)'/g)].map(m => m[1]).sort(); + assert.deepEqual([...READINESS_CONTRACTS.find(c => c.name === name)!.capabilities].sort(), expected); + } +}); From 261adcd649b3c320a74ef93417122270508ff0a5 Mon Sep 17 00:00:00 2001 From: Ayobami Haastrup <47716486+AyobamiH@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:28:59 +0100 Subject: [PATCH 2/2] Fix readiness transport in workerd without permitting redirects Reproduce redirect:error throwing before transport in the actual Workers runtime. Use manual mode and retain exact HTTP 200/contract validation. Preserve all 73 synthetic tests and add ten real workerd regressions, including cross-origin redirect rejection and zero-request auth failures. No production deployment or schema mutation. --- .github/workflows/readiness-contract.yml | 2 + src/worker-readiness.ts | 4 +- test/cloudflare-readiness.test.ts | 2 +- test/cloudflare-readiness.workerd.mjs | 67 ++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 test/cloudflare-readiness.workerd.mjs diff --git a/.github/workflows/readiness-contract.yml b/.github/workflows/readiness-contract.yml index 653e478..a556136 100644 --- a/.github/workflows/readiness-contract.yml +++ b/.github/workflows/readiness-contract.yml @@ -32,5 +32,7 @@ jobs: run: | node --import tsx --test test/cloudflare-readiness.test.ts node --import tsx test/cloudflare-health.test.ts + - name: Actual workerd transport and redirect-security regressions + run: node --test test/cloudflare-readiness.workerd.mjs - name: No generated or fixture files changed the source run: git diff --exit-code diff --git a/src/worker-readiness.ts b/src/worker-readiness.ts index 478a340..c9e489b 100644 --- a/src/worker-readiness.ts +++ b/src/worker-readiness.ts @@ -128,8 +128,10 @@ export async function handleWorkerReadiness( const dependencies = await Promise.all(READINESS_CONTRACTS.map(async expected => { let response: Response | undefined; try { + // workerd rejects redirect:error before transport. manual plus the exact + // status check below preserves no-follow/no-credential-forwarding safety. response = await fetchImpl(`${origin}/rest/v1/rpc/${expected.rpc}`, { - method: 'POST', body: '{}', redirect: 'error', cache: 'no-store', signal: controller.signal, + method: 'POST', body: '{}', redirect: 'manual', cache: 'no-store', signal: controller.signal, headers: { Authorization: `Bearer ${key}`, apikey: key, 'Content-Type': 'application/json', Accept: 'application/json' }, }); if (response.status !== 200) { diff --git a/test/cloudflare-readiness.test.ts b/test/cloudflare-readiness.test.ts index e86959c..2cddb83 100644 --- a/test/cloudflare-readiness.test.ts +++ b/test/cloudflare-readiness.test.ts @@ -47,7 +47,7 @@ test('readiness proves exact identity and all schema capabilities without execut assert.equal(body.readiness, 'dependencies_verified'); assert.equal(calls.length, 3); for (const { url, init } of calls) { assert.ok(READINESS_CONTRACTS.some(c => url === `https://database.example/rest/v1/rpc/${c.rpc}`)); - assert.equal(init?.method, 'POST'); assert.equal(init?.body, '{}'); assert.equal(init?.redirect, 'error'); + assert.equal(init?.method, 'POST'); assert.equal(init?.body, '{}'); assert.equal(init?.redirect, 'manual'); assert.equal(init?.cache, 'no-store'); assert.ok(init?.signal instanceof AbortSignal); assert.equal((init?.headers as Record).apikey, env.SUPABASE_SERVICE_ROLE_KEY); } diff --git a/test/cloudflare-readiness.workerd.mjs b/test/cloudflare-readiness.workerd.mjs new file mode 100644 index 0000000..fe47608 --- /dev/null +++ b/test/cloudflare-readiness.workerd.mjs @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { createRequire } from 'node:module'; +import { test } from 'node:test'; + +// Resolve the runtime/bundler from locked Wrangler dependencies. Never download +// a different runtime, contact a real database, or inject a fake Worker fetch. +const require = createRequire(import.meta.url); +const wranglerRequire = createRequire(require.resolve('wrangler/package.json')); +const { Miniflare } = wranglerRequire('miniflare'); +const { build } = wranglerRequire('esbuild'); +const sourcePath = resolve('src/worker-readiness.ts'); +const result = await build({stdin:{contents:`import {handleWorkerReadiness} from ${JSON.stringify(sourcePath)}; export default {fetch(request, env) {return handleWorkerReadiness(request,env)}};`, resolveDir:process.cwd()}, bundle:true, write:false, format:'esm',platform:'neutral',external:['node:crypto']}); +const script = result.outputFiles[0].text; +const version = '11111111-1111-4111-8111-111111111111'; +const nonce = '22222222-2222-4222-8222-222222222222'; +const sha = 'a'.repeat(40); +const env = {WORKER_TICK_TOKEN:'synthetic-tick-key',SUPABASE_URL:'https://database.invalid',SUPABASE_SERVICE_ROLE_KEY:'synthetic-db-key',CREDENTIAL_ENCRYPTION_KEY:'synthetic-encryption-key',SUPABASE_WORKER_GENERATION_ENABLED:'false',SUPABASE_PROVIDER_DISPATCH_ENABLED:'false',CF_VERSION_METADATA:{id:version,tag:sha,timestamp:'2026-09-29T00:00:00Z'}}; +const headers = {Authorization:'Bearer '+env.WORKER_TICK_TOKEN,'X-OCPF-Readiness-Nonce':nonce,'X-OCPF-Expected-Version':version,'X-OCPF-Expected-Sha':sha}; +const source = readFileSync(sourcePath,'utf8'); +const specs = { + get_worker_schema_contract:{contract:'worker-claims-v1',capabilities:['source-targeted-claim-v1','angle-targeted-claim-v1','angle-exhaust-fenced-v1','source-angle-atomic-commit-v1','angle-queue-atomic-commit-v1','queue-angle-identity-v1','legacy-queue-revision-hold-v1']}, + get_publication_schema_contract:{contract:'publication-ledger-v1',migration:'20260907054000',lock_order_migration:'20260913061000',capabilities:['publication-intent-claim-v1','publication-dispatch-boundary-v1','publication-attempt-outcome-v1','publication-unknown-reconciliation-v1','publication-exact-history-receipt-v1','publication-queue-compatibility-fence-v1','publication-provenance-snapshot-v1','publication-legacy-queue-hold-v1','publication-queue-lock-order-v1']}, + get_agent_jobs_contract:{contract:'agent-jobs-v1',capabilities:['durable-enqueue-v1','fenced-terminal-v1','reconciliation-only-recovery-v1','rpc-only-job-writes-v1']}, +}; +async function runtime({code=script,status=200,auth=headers,invalidBody=false}={}) { + const calls=[]; + const mf = new Miniflare({modules:true,script:code,compatibilityDate:'2026-05-07',compatibilityFlags:['nodejs_compat'],bindings:env,outboundService:async request=>{ + const u=new URL(request.url); calls.push({origin:u.origin,path:u.pathname,method:request.method,authorisation:request.headers.get('authorization'),body:await request.text()}); + assert.equal(u.origin,'https://database.invalid','credentials must not reach redirects'); + assert.equal(request.method,'POST'); + const spec=specs[u.pathname.split('/').at(-1)]; assert.ok(spec,'only schema RPCs allowed'); + if(status!==200)return new Response('not accepted',{status,headers:status>=300&&status<400?{Location:'https://forbidden.invalid/steal'}:{}}); + return Response.json(invalidBody?{}:spec); + }}); + try{const r=await mf.dispatchFetch('https://probe.invalid/readyz',{headers:auth});return {status:r.status,body:await r.text(),calls};} + finally{await mf.dispose();} +} + +test('workerd reproduces the previous redirect:error defect before transport',async()=>{ + assert.match(source,/redirect: 'manual'/); + const old=script.replace('redirect: "manual"','redirect: "error"'); assert.notEqual(old,script); + const r=await runtime({code:old});assert.equal(r.status,503);assert.equal(r.calls.length,0); + assert.ok(JSON.parse(r.body).dependencies.every(d=>d.state==='transport_error')); +}); +test('the repaired native handler reaches all three schema RPCs in workerd',async()=>{ + const r=await runtime();assert.equal(r.status,200);assert.equal(r.calls.length,3); + const b=JSON.parse(r.body);assert.equal(b.nonce,nonce);assert.equal(b.readOnly,true);assert.equal(b.executionAuthorised,false); + assert.equal(b.release.workerVersionId,version);assert.equal(b.release.gitSha,sha);assert.ok(b.dependencies.every(d=>d.state==='verified')); + assert.ok(r.calls.every(c=>c.authorisation==='Bearer '+env.SUPABASE_SERVICE_ROLE_KEY && c.body==='{}')); +}); +for(const status of [301,302,303,307,308]) test(`workerd rejects ${status} without following Location or resending credentials`,async()=>{ + const r=await runtime({status});assert.equal(r.status,503);assert.equal(r.calls.length,3); + assert.ok(JSON.parse(r.body).dependencies.every(d=>d.state==='http_error'&&d.status===status)); + assert.ok(!r.body.includes('forbidden.invalid')); +}); +test('workerd invalid auth sends no database request',async()=>{ + const r=await runtime({auth:{...headers,Authorization:'Bearer wrong'}});assert.equal(r.status,404);assert.equal(r.calls.length,0); +}); +test('workerd exact-version mismatch sends no database request',async()=>{ + const r=await runtime({auth:{...headers,'X-OCPF-Expected-Version':nonce}});assert.equal(r.status,503);assert.equal(r.calls.length,0); +}); +test('workerd rejects a 200 response with an incompatible schema',async()=>{ + const r=await runtime({invalidBody:true});assert.equal(r.status,503);assert.equal(r.calls.length,3); + assert.ok(JSON.parse(r.body).dependencies.every(d=>d.state==='contract_mismatch')); +});