From 07c3900bb349084c0bd542095d6587fbf5f1a42d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 03:55:05 -0700 Subject: [PATCH 001/102] test(github): reproduce trailing-slash API base failure --- test/exchange-success-path-coverage.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/exchange-success-path-coverage.test.ts b/test/exchange-success-path-coverage.test.ts index 5df4dae02..3e2efd330 100644 --- a/test/exchange-success-path-coverage.test.ts +++ b/test/exchange-success-path-coverage.test.ts @@ -63,7 +63,10 @@ afterEach(() => { }); describe("exchange success-path coverage through the public worker", () => { - it("accepts workflow_ref-only claims without inventing an OIDC subject", async () => { + it.each([ + "https://api.github.com", + "https://api.github.com/", + ])("accepts workflow_ref-only claims with GitHub API base %s", async (githubApiBase) => { const now = Math.floor(Date.now() / 1000); const { token: oidcToken, jwk } = await createSignedJwt({ iss: env.ALLOWED_ISSUER, @@ -125,6 +128,7 @@ describe("exchange success-path coverage through the public worker", () => { }), { ...env, + GITHUB_API_BASE: githubApiBase, GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKey, }, ); From 0a5a6596b244d5e0a8fe58dcec94a2c8274582ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:00:01 -0700 Subject: [PATCH 002/102] fix(github): canonicalize accepted API root URLs --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index d50a080ea..066942fed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -502,7 +502,7 @@ type GitHubJsonRequestInit = RequestInit & { }; async function githubJson(path: string, init: GitHubJsonRequestInit, env: Env): Promise { - const response = await fetch(`${env.GITHUB_API_BASE}${path}`, { + const response = await fetch(new URL(path, env.GITHUB_API_BASE), { ...init, headers: { accept: "application/vnd.github+json", From 652c660c2148164e5f6bbe7d5ceb6cfda4fea8b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:03:05 -0700 Subject: [PATCH 003/102] test(github): cover every accepted API root spelling --- test/exchange-success-path-coverage.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/exchange-success-path-coverage.test.ts b/test/exchange-success-path-coverage.test.ts index 3e2efd330..209d9961c 100644 --- a/test/exchange-success-path-coverage.test.ts +++ b/test/exchange-success-path-coverage.test.ts @@ -66,6 +66,7 @@ describe("exchange success-path coverage through the public worker", () => { it.each([ "https://api.github.com", "https://api.github.com/", + "https://api.github.com:443/", ])("accepts workflow_ref-only claims with GitHub API base %s", async (githubApiBase) => { const now = Math.floor(Date.now() / 1000); const { token: oidcToken, jwk } = await createSignedJwt({ From e774d18770ac0ea5a60a034266d7168752ce7efc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:07:55 -0700 Subject: [PATCH 004/102] test(github): reproduce malformed success JSON classification --- test/github-api-malformed-json.test.ts | 167 +++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 test/github-api-malformed-json.test.ts diff --git a/test/github-api-malformed-json.test.ts b/test/github-api-malformed-json.test.ts new file mode 100644 index 000000000..349c020d1 --- /dev/null +++ b/test/github-api-malformed-json.test.ts @@ -0,0 +1,167 @@ +import { beforeAll, describe, expect, it, vi, afterEach } from "vitest"; +import worker, { type Env } from "../src/index"; + +const configuredRef = + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; +const configuredWorkflowSha = "a".repeat(40); + +const baseEnv: Env = { + ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", + ALLOWED_AUDIENCE: "cwl-noema-review", + ALLOWED_REPOSITORY_OWNER: "ContextualWisdomLab", + ALLOWED_WORKFLOW_REPOSITORY: "ContextualWisdomLab/.github", + ALLOWED_WORKFLOW_REF_PREFIX: configuredRef, + ALLOWED_WORKFLOW_SHA: configuredWorkflowSha, + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: "initialized-in-beforeAll", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", +}; + +let oidcKeyPair: CryptoKeyPair; +let oidcPublicJwk: JsonWebKey; +let appPrivateKeyPem: string; + +function encodeSegment(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +function encodeBytes(bytes: ArrayBuffer): string { + return Buffer.from(bytes).toString("base64url"); +} + +function pemFromPkcs8(pkcs8: ArrayBuffer): string { + const base64 = Buffer.from(pkcs8).toString("base64"); + const lines = base64.match(/.{1,64}/g)?.join("\n") ?? base64; + return `-----BEGIN PRIVATE KEY-----\n${lines}\n-----END PRIVATE KEY-----`; +} + +async function generateRsaKeyPair(): Promise { + return crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + ); +} + +beforeAll(async () => { + oidcKeyPair = await generateRsaKeyPair(); + oidcPublicJwk = await crypto.subtle.exportKey("jwk", oidcKeyPair.publicKey); + const appKeyPair = await generateRsaKeyPair(); + appPrivateKeyPem = pemFromPkcs8( + await crypto.subtle.exportKey("pkcs8", appKeyPair.privateKey), + ); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +async function signedOidcToken() { + const kid = `github-json-${crypto.randomUUID()}`; + const now = Math.floor(Date.now() / 1000); + const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); + const payload = encodeSegment({ + iss: baseEnv.ALLOWED_ISSUER, + aud: baseEnv.ALLOWED_AUDIENCE, + repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, + repository: "ContextualWisdomLab/.github", + job_workflow_ref: configuredRef, + job_workflow_sha: configuredWorkflowSha, + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }); + const signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + oidcKeyPair.privateKey, + new TextEncoder().encode(`${header}.${payload}`), + ); + return { + token: `${header}.${payload}.${encodeBytes(signature)}`, + jwk: { ...oidcPublicJwk, kid, kty: "RSA" }, + }; +} + +async function exchangeWith( + targetRepository: string, + env: Env, + githubHandler: (url: string) => Promise | Response, +): Promise { + const { token, jwk } = await signedOidcToken(); + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { + return Response.json({ + jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", + }); + } + if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { + return Response.json({ keys: [jwk] }); + } + return githubHandler(url); + }); + + return worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json", + "cf-connecting-ip": `203.0.113.${Math.floor(Math.random() * 100) + 100}`, + }, + body: JSON.stringify({ target_repository: targetRepository }), + }), + { ...env, GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKeyPem }, + ); +} + +describe("GitHub API success-response parsing", () => { + it("classifies malformed installation JSON as an upstream GitHub API failure", async () => { + const targetRepository = "ContextualWisdomLab/malformed-installation-json"; + const response = await exchangeWith(targetRepository, baseEnv, (url) => { + if (url === `https://api.github.com/repos/${targetRepository}/installation`) { + return new Response("{", { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + return new Response("unexpected GitHub request", { status: 500 }); + }); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_API", + message: "GitHub API returned malformed JSON", + }); + }); + + it("classifies malformed installation-token JSON as an upstream GitHub API failure", async () => { + const response = await exchangeWith( + "ContextualWisdomLab/malformed-token-json", + { ...baseEnv, GITHUB_APP_INSTALLATION_ID: "92345" }, + (url) => { + if (url === "https://api.github.com/app/installations/92345/access_tokens") { + return new Response("{", { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + return new Response("unexpected GitHub request", { status: 500 }); + }, + ); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_API", + message: "GitHub API returned malformed JSON", + }); + }); +}); From 2344eaf621329f77c1ffe16b42b9e675fa988c32 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:10:14 -0700 Subject: [PATCH 005/102] fix(github): classify malformed success JSON upstream --- src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 066942fed..96cd7cf92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -520,7 +520,11 @@ async function githubJson(path: string, init: GitHubJsonRequestInit, env: Env): } throw new ApiError("ERR_GITHUB_API", response.status >= 400 ? 400 : 500, "GitHub API request failed"); } - return response.json(); + try { + return await response.json(); + } catch { + throw new ApiError("ERR_GITHUB_API", 502, "GitHub API returned malformed JSON"); + } } async function resolveInstallationId(appJwt: string, repository: string, env: Env): Promise { From 51913c1a8cbecee8855e3bf2b29cc43a2de9e92c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:13:04 -0700 Subject: [PATCH 006/102] test(github): reproduce invalid success JSON shapes --- test/github-api-malformed-json.test.ts | 32 +++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/test/github-api-malformed-json.test.ts b/test/github-api-malformed-json.test.ts index 349c020d1..d70b4d4ac 100644 --- a/test/github-api-malformed-json.test.ts +++ b/test/github-api-malformed-json.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it, vi, afterEach } from "vitest"; +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import worker, { type Env } from "../src/index"; const configuredRef = @@ -92,6 +92,7 @@ async function exchangeWith( targetRepository: string, env: Env, githubHandler: (url: string) => Promise | Response, + clientIp: string, ): Promise { const { token, jwk } = await signedOidcToken(); vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { @@ -113,7 +114,7 @@ async function exchangeWith( headers: { authorization: `Bearer ${token}`, "content-type": "application/json", - "cf-connecting-ip": `203.0.113.${Math.floor(Math.random() * 100) + 100}`, + "cf-connecting-ip": clientIp, }, body: JSON.stringify({ target_repository: targetRepository }), }), @@ -132,7 +133,7 @@ describe("GitHub API success-response parsing", () => { }); } return new Response("unexpected GitHub request", { status: 500 }); - }); + }, "203.0.113.240"); expect(response.status).toBe(502); await expect(response.json()).resolves.toMatchObject({ @@ -155,6 +156,7 @@ describe("GitHub API success-response parsing", () => { } return new Response("unexpected GitHub request", { status: 500 }); }, + "203.0.113.241", ); expect(response.status).toBe(502); @@ -164,4 +166,28 @@ describe("GitHub API success-response parsing", () => { message: "GitHub API returned malformed JSON", }); }); + + it.each([ + ["null", "203.0.113.242"], + ["[]", "203.0.113.243"], + ["\"unexpected\"", "203.0.113.244"], + ])("classifies non-object installation JSON %s as an upstream GitHub API failure", async (body, clientIp) => { + const targetRepository = `ContextualWisdomLab/invalid-installation-${clientIp.split(".").at(-1)}`; + const response = await exchangeWith(targetRepository, baseEnv, (url) => { + if (url === `https://api.github.com/repos/${targetRepository}/installation`) { + return new Response(body, { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + return new Response("unexpected GitHub request", { status: 500 }); + }, clientIp); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_API", + message: "GitHub API returned invalid JSON shape", + }); + }); }); From 90153e89db1a09b95e9a812076dbbcaea24e2f11 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:10:10 -0700 Subject: [PATCH 007/102] fix(github): reject non-object success JSON --- src/index.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 96cd7cf92..735e45805 100644 --- a/src/index.ts +++ b/src/index.ts @@ -501,7 +501,7 @@ type GitHubJsonRequestInit = RequestInit & { headers: Record; }; -async function githubJson(path: string, init: GitHubJsonRequestInit, env: Env): Promise { +async function githubJson(path: string, init: GitHubJsonRequestInit, env: Env): Promise> { const response = await fetch(new URL(path, env.GITHUB_API_BASE), { ...init, headers: { @@ -520,11 +520,16 @@ async function githubJson(path: string, init: GitHubJsonRequestInit, env: Env): } throw new ApiError("ERR_GITHUB_API", response.status >= 400 ? 400 : 500, "GitHub API request failed"); } + let value: unknown; try { - return await response.json(); + value = await response.json(); } catch { throw new ApiError("ERR_GITHUB_API", 502, "GitHub API returned malformed JSON"); } + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new ApiError("ERR_GITHUB_API", 502, "GitHub API returned invalid JSON shape"); + } + return value as Record; } async function resolveInstallationId(appJwt: string, repository: string, env: Env): Promise { @@ -776,4 +781,4 @@ export default { return withOperationalHeaders(response, traceId, latency_ms); } }, -}; +}; \ No newline at end of file From 390421f4680c4ba234e9e5897c50eba510b23872 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:11:58 -0700 Subject: [PATCH 008/102] test(github): reject malformed installation fields --- test/github-api-malformed-json.test.ts | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/github-api-malformed-json.test.ts b/test/github-api-malformed-json.test.ts index d70b4d4ac..17216f37b 100644 --- a/test/github-api-malformed-json.test.ts +++ b/test/github-api-malformed-json.test.ts @@ -190,4 +190,45 @@ describe("GitHub API success-response parsing", () => { message: "GitHub API returned invalid JSON shape", }); }); + + it("rejects a non-numeric installation id before token minting", async () => { + const targetRepository = "ContextualWisdomLab/invalid-installation-id"; + const response = await exchangeWith(targetRepository, baseEnv, (url) => { + if (url === `https://api.github.com/repos/${targetRepository}/installation`) { + return Response.json({ id: { attacker_controlled: true } }); + } + return new Response("unexpected GitHub request", { status: 500 }); + }, "203.0.113.245"); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_API", + message: "GitHub API returned invalid installation response", + }); + }); + + it("rejects non-string installation token material instead of coercing it into a credential", async () => { + const response = await exchangeWith( + "ContextualWisdomLab/invalid-token-shape", + { ...baseEnv, GITHUB_APP_INSTALLATION_ID: "92345" }, + (url) => { + if (url === "https://api.github.com/app/installations/92345/access_tokens") { + return Response.json({ + token: { attacker_controlled: true }, + expires_at: "2099-01-01T00:00:00Z", + }); + } + return new Response("unexpected GitHub request", { status: 500 }); + }, + "203.0.113.246", + ); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_API", + message: "GitHub API returned invalid installation-token response", + }); + }); }); From 261a22fbe0a3e89715bfa181660de7d6faf43c75 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:13:54 -0700 Subject: [PATCH 009/102] fix(github): validate installation response fields --- src/index.ts | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 735e45805..89c3afd9d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -547,7 +547,16 @@ async function resolveInstallationId(appJwt: string, repository: string, env: En const installation = await githubJson(`/repos/${repository}/installation`, { headers: { authorization: `Bearer ${appJwt}` }, }, env); - if (!installation.id) throw new ApiError("ERR_GITHUB_INSTALLATION", 500, "GitHub App installation id was not found"); + if (installation.id === undefined || installation.id === null) { + throw new ApiError("ERR_GITHUB_INSTALLATION", 500, "GitHub App installation id was not found"); + } + if ( + typeof installation.id !== "number" + || !Number.isSafeInteger(installation.id) + || installation.id <= 0 + ) { + throw new ApiError("ERR_GITHUB_API", 502, "GitHub API returned invalid installation response"); + } const installationId = String(installation.id); installationIdCache.set(cacheKey, { value: installationId, @@ -564,21 +573,33 @@ async function createInstallationToken(repository: string, env: Env): Promise Date: Wed, 19 Aug 2026 06:37:03 -0700 Subject: [PATCH 010/102] test(egress): fail closed on response stream errors --- test/outbound-fetch-policy.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/outbound-fetch-policy.test.ts b/test/outbound-fetch-policy.test.ts index 17e18ae5e..f50f5f641 100644 --- a/test/outbound-fetch-policy.test.ts +++ b/test/outbound-fetch-policy.test.ts @@ -169,6 +169,22 @@ describe("credential-bearing outbound fetch policy", () => { expect(cancel).toHaveBeenCalledOnce(); }); + it("converts a response stream failure into a bodyless fail-closed gateway response", async () => { + const body = new ReadableStream({ + pull(controller) { + controller.error(new Error("upstream body failed")); + }, + }); + const rawFetch = vi.fn(async () => new Response(body)); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped("https://api.github.com/meta"); + + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-response-read"); + expect(await response.text()).toBe(""); + }); + it("accepts a chunked response exactly at the one-megabyte boundary", async () => { const body = new ReadableStream({ start(controller) { From f096ebca47f565e14b57bad34faffc5fbf552a63 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 06:39:06 -0700 Subject: [PATCH 011/102] fix(egress): classify response stream failures --- src/outbound-fetch-policy.ts | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index d30096248..c698408be 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -20,7 +20,7 @@ type FetchInstallation = { wrapped: FetchLike; }; -type BlockReason = "destination" | "request-policy" | "redirect" | "response-size" | "timeout"; +type BlockReason = "destination" | "request-policy" | "redirect" | "response-size" | "response-read" | "timeout"; type GitHubApiOperation = | "repository-installation" @@ -173,19 +173,23 @@ async function boundedOutboundResponse(response: Response): Promise { const reader = response.body.getReader(); const chunks: Uint8Array[] = []; let totalBytes = 0; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - totalBytes += value.byteLength; - if (totalBytes > MAX_OUTBOUND_RESPONSE_BYTES) { - try { - await reader.cancel("Noema outbound response exceeds byte limit"); - } catch { - // Cancellation is best-effort after the response has already been rejected. + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + totalBytes += value.byteLength; + if (totalBytes > MAX_OUTBOUND_RESPONSE_BYTES) { + try { + await reader.cancel("Noema outbound response exceeds byte limit"); + } catch { + // Cancellation is best-effort after the response has already been rejected. + } + return blockedResponse("response-size"); } - return blockedResponse("response-size"); + chunks.push(value); } - chunks.push(value); + } catch { + return blockedResponse("response-read"); } const boundedBody = new Uint8Array(totalBytes); From 6ecc8f316b387d93856d1b9dcc970d591b433120 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 07:25:15 -0700 Subject: [PATCH 012/102] test(egress): reject without awaiting cleanup --- test/outbound-fetch-cleanup-liveness.test.ts | 40 ++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/outbound-fetch-cleanup-liveness.test.ts diff --git a/test/outbound-fetch-cleanup-liveness.test.ts b/test/outbound-fetch-cleanup-liveness.test.ts new file mode 100644 index 000000000..545f39bdc --- /dev/null +++ b/test/outbound-fetch-cleanup-liveness.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it, vi } from "vitest"; +import { createFailClosedFetch, type FetchLike } from "../src/outbound-fetch-policy"; + +const trustedUrl = "https://api.github.com/meta"; + +async function boundedOutcome(response: Response): Promise { + const rawFetch = vi.fn(async () => response); + const wrapped = createFailClosedFetch(rawFetch); + return Promise.race([ + wrapped(trustedUrl).then((value) => value.headers.get("x-noema-egress-policy") ?? "allowed"), + new Promise((resolve) => { + setTimeout(() => resolve("cleanup-timeout"), 500); + }), + ]); +} + +describe("outbound response cleanup liveness", () => { + it("does not await a never-settling cancellation after declared oversize rejection", async () => { + const cancel = vi.fn(() => new Promise(() => {})); + const response = new Response(new ReadableStream({ cancel }), { + headers: { "content-length": "1048577" }, + }); + + expect(await boundedOutcome(response)).toBe("blocked-response-size"); + expect(cancel).toHaveBeenCalledOnce(); + }); + + it("does not await a never-settling reader cancellation after streamed overflow", async () => { + const cancel = vi.fn(() => new Promise(() => {})); + const response = new Response(new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(1_048_577)); + }, + cancel, + })); + + expect(await boundedOutcome(response)).toBe("blocked-response-size"); + expect(cancel).toHaveBeenCalledOnce(); + }); +}); From be991e18acba7cfec985aff7eacdf684b13aed06 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 07:26:14 -0700 Subject: [PATCH 013/102] fix(egress): never await rejected-body cleanup --- src/outbound-fetch-policy.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index c698408be..70ecb89a0 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -151,6 +151,14 @@ function boundedOutboundSignal( return AbortSignal.any(signals); } +function ignoreCancellationBestEffort(cancel: () => Promise): void { + try { + void cancel().catch(() => undefined); + } catch { + // Cleanup is best-effort after the response has already crossed a fail-closed rejection boundary. + } +} + async function boundedOutboundResponse(response: Response): Promise { const declaredLength = response.headers.get("content-length"); if ( @@ -159,11 +167,9 @@ async function boundedOutboundResponse(response: Response): Promise { && Number(declaredLength) > MAX_OUTBOUND_RESPONSE_BYTES ) { if (response.body !== null) { - try { - await response.body.cancel("Noema outbound response exceeds byte limit"); - } catch { - // Cancellation is best-effort after the response has already been rejected. - } + ignoreCancellationBestEffort(() => response.body!.cancel( + "Noema outbound response exceeds byte limit", + )); } return blockedResponse("response-size"); } @@ -179,11 +185,9 @@ async function boundedOutboundResponse(response: Response): Promise { if (done) break; totalBytes += value.byteLength; if (totalBytes > MAX_OUTBOUND_RESPONSE_BYTES) { - try { - await reader.cancel("Noema outbound response exceeds byte limit"); - } catch { - // Cancellation is best-effort after the response has already been rejected. - } + ignoreCancellationBestEffort(() => reader.cancel( + "Noema outbound response exceeds byte limit", + )); return blockedResponse("response-size"); } chunks.push(value); From 8ecccaeae7eebaceec0cf4bcf6c2c7f0ef4d10f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 12:33:43 -0700 Subject: [PATCH 014/102] test(github): reject expired installation credentials --- test/github-api-malformed-json.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/github-api-malformed-json.test.ts b/test/github-api-malformed-json.test.ts index 17216f37b..420f40df0 100644 --- a/test/github-api-malformed-json.test.ts +++ b/test/github-api-malformed-json.test.ts @@ -231,4 +231,29 @@ describe("GitHub API success-response parsing", () => { message: "GitHub API returned invalid installation-token response", }); }); + + it("rejects an already-expired installation token instead of returning unusable credential material", async () => { + vi.spyOn(Date, "now").mockReturnValue(Date.parse("2030-01-01T00:00:00Z")); + const response = await exchangeWith( + "ContextualWisdomLab/expired-installation-token", + { ...baseEnv, GITHUB_APP_INSTALLATION_ID: "92345" }, + (url) => { + if (url === "https://api.github.com/app/installations/92345/access_tokens") { + return Response.json({ + token: "ghs_expired", + expires_at: "2029-12-31T23:59:59Z", + }); + } + return new Response("unexpected GitHub request", { status: 500 }); + }, + "203.0.113.247", + ); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_API", + message: "GitHub API returned expired installation-token response", + }); + }); }); From 05ea61cf0536f60ed2b1f1f410f867ced0d6e421 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 12:35:34 -0700 Subject: [PATCH 015/102] fix(github): reject expired installation credentials --- src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 89c3afd9d..fbaa32268 100644 --- a/src/index.ts +++ b/src/index.ts @@ -591,12 +591,16 @@ async function createInstallationToken(repository: string, env: Env): Promise Date: Wed, 19 Aug 2026 13:09:31 -0700 Subject: [PATCH 016/102] test(github): reject implausibly long-lived installation tokens --- test/github-api-malformed-json.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/github-api-malformed-json.test.ts b/test/github-api-malformed-json.test.ts index 420f40df0..09a0f3167 100644 --- a/test/github-api-malformed-json.test.ts +++ b/test/github-api-malformed-json.test.ts @@ -256,4 +256,29 @@ describe("GitHub API success-response parsing", () => { message: "GitHub API returned expired installation-token response", }); }); + + it("rejects an installation token whose declared lifetime exceeds GitHub's one-hour contract", async () => { + vi.spyOn(Date, "now").mockReturnValue(Date.parse("2030-01-01T00:00:00Z")); + const response = await exchangeWith( + "ContextualWisdomLab/overlong-installation-token", + { ...baseEnv, GITHUB_APP_INSTALLATION_ID: "92345" }, + (url) => { + if (url === "https://api.github.com/app/installations/92345/access_tokens") { + return Response.json({ + token: "ghs_overlong", + expires_at: "2030-01-01T02:00:00Z", + }); + } + return new Response("unexpected GitHub request", { status: 500 }); + }, + "203.0.113.248", + ); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_API", + message: "GitHub API returned implausible installation-token expiry", + }); + }); }); From dd0794ca0104ae60a6fe61c2076fc3c254acf6fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 13:12:07 -0700 Subject: [PATCH 017/102] fix(github): bound installation token lifetime --- src/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index fbaa32268..75f7df8bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -142,6 +142,7 @@ const trustedHeaderValuePattern = /^[A-Za-z0-9._:-]+$/; const clientIdentifierPattern = /^[A-Za-z0-9.:%_,-]+$/; const exactWorkflowSourceShaPattern = /^[0-9a-f]{40}$/; const maxTrustedHeaderLength = 128; +const maxInstallationTokenLifetimeMs = 65 * 60_000; function jsonResponse(body: StandardErrorResponse | StandardSuccessResponse, status = 200): Response { return new Response(JSON.stringify(body), { @@ -598,9 +599,13 @@ async function createInstallationToken(repository: string, env: Env): Promise nowMs + maxInstallationTokenLifetimeMs) { + throw new ApiError("ERR_GITHUB_API", 502, "GitHub API returned implausible installation-token expiry"); + } return { token: token.token, expires_at: token.expires_at, @@ -806,4 +811,4 @@ export default { return withOperationalHeaders(response, traceId, latency_ms); } }, -}; +}; \ No newline at end of file From e6544aef748565d9ab2e2acedc2213096c9de9b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 13:12:58 -0700 Subject: [PATCH 018/102] test(github): keep successful token fixtures within lifetime --- test/github-app-runtime-coverage.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/github-app-runtime-coverage.test.ts b/test/github-app-runtime-coverage.test.ts index 8774ea1ea..933a47fc1 100644 --- a/test/github-app-runtime-coverage.test.ts +++ b/test/github-app-runtime-coverage.test.ts @@ -124,23 +124,27 @@ async function exchange( ); } -function successfulTokenResponse(token = "ghs_runtime_coverage") { +function successfulTokenResponse( + token = "ghs_runtime_coverage", + expiresAt = new Date(Date.now() + 60 * 60_000).toISOString(), +) { return Response.json({ token, - expires_at: "2030-01-01T00:00:00Z", + expires_at: expiresAt, }); } describe("GitHub App runtime coverage through the public exchange boundary", () => { it("uses an explicit installation id and requests one repository with least privilege", async () => { const calls: Array<{ url: string; init?: RequestInit }> = []; + const expiresAt = new Date(Date.now() + 60 * 60_000).toISOString(); const response = await exchange( "ContextualWisdomLab/noema", { ...baseEnv, GITHUB_APP_INSTALLATION_ID: "12345" }, (url, init) => { calls.push({ url, init }); if (url === "https://api.github.com/app/installations/12345/access_tokens") { - return successfulTokenResponse(); + return successfulTokenResponse("ghs_runtime_coverage", expiresAt); } return new Response("unexpected GitHub request", { status: 500 }); }, @@ -153,7 +157,7 @@ describe("GitHub App runtime coverage through the public exchange boundary", () data: { repository: "ContextualWisdomLab/noema", token: "ghs_runtime_coverage", - token_expires_at: "2030-01-01T00:00:00Z", + token_expires_at: expiresAt, }, }); expect(calls).toHaveLength(1); From 13d9e76e28ed7cbe6a8932b877622bb8120a96b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 13:13:28 -0700 Subject: [PATCH 019/102] test(github): use bounded success token expiry --- test/exchange-success-path-coverage.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/exchange-success-path-coverage.test.ts b/test/exchange-success-path-coverage.test.ts index 209d9961c..383280f97 100644 --- a/test/exchange-success-path-coverage.test.ts +++ b/test/exchange-success-path-coverage.test.ts @@ -69,6 +69,7 @@ describe("exchange success-path coverage through the public worker", () => { "https://api.github.com:443/", ])("accepts workflow_ref-only claims with GitHub API base %s", async (githubApiBase) => { const now = Math.floor(Date.now() / 1000); + const expiresAt = new Date(Date.now() + 60 * 60_000).toISOString(); const { token: oidcToken, jwk } = await createSignedJwt({ iss: env.ALLOWED_ISSUER, aud: env.ALLOWED_AUDIENCE, @@ -111,7 +112,7 @@ describe("exchange success-path coverage through the public worker", () => { if (url === "https://api.github.com/app/installations/12345/access_tokens") { return Response.json({ token: "ghs_exchange_success_token", - expires_at: "2030-01-01T00:00:00Z", + expires_at: expiresAt, }); } return new Response("not found", { status: 404 }); @@ -141,7 +142,7 @@ describe("exchange success-path coverage through the public worker", () => { token: "ghs_exchange_success_token", repository: "ContextualWisdomLab/noema", workflow_ref: configuredRef, - token_expires_at: "2030-01-01T00:00:00Z", + token_expires_at: expiresAt, }, }); const logOutput = logSpy.mock.calls.flat().join("\n"); From d7fa067f16c64d1f764528fe6b70964aa641bf00 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 13:14:04 -0700 Subject: [PATCH 020/102] test(github): bound replay success token lifetime --- test/replay-request-core-coverage.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/replay-request-core-coverage.test.ts b/test/replay-request-core-coverage.test.ts index 77b64dd7f..f60bd2c15 100644 --- a/test/replay-request-core-coverage.test.ts +++ b/test/replay-request-core-coverage.test.ts @@ -113,7 +113,7 @@ function installOidcFetch(jwk: JsonWebKey, env: Env, installationToken = false) if (installationToken && url === `${env.GITHUB_API_BASE}/app/installations/12345/access_tokens`) { return Response.json({ token: "ghs_replay_coverage_token", - expires_at: "2030-01-01T00:00:00Z", + expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), }); } return new Response("not found", { status: 404 }); From 69b9a7f2dec81780ea1956a5049a24fb924ed1fe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:02:42 -0700 Subject: [PATCH 021/102] fix(ci): restore canonical source newline --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 75f7df8bd..54930541c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -811,4 +811,4 @@ export default { return withOperationalHeaders(response, traceId, latency_ms); } }, -}; \ No newline at end of file +}; From 4b2ef4b0de70738a046ae75e7758ab16b4a0a5bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:03:38 -0700 Subject: [PATCH 022/102] test(github): keep exchange success expiry current --- test/worker.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/worker.test.ts b/test/worker.test.ts index b605d8f98..390a1e815 100644 --- a/test/worker.test.ts +++ b/test/worker.test.ts @@ -374,6 +374,7 @@ describe("Noema worker", () => { ); const appPrivateKey = pemFromPkcs8(await crypto.subtle.exportKey("pkcs8", appKeyPair.privateKey)); const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); + const tokenExpiresAt = new Date(Date.now() + 60 * 60_000).toISOString(); const requests: Array<{ url: string; method: string; body?: string }> = []; vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => { const url = String(input); @@ -390,7 +391,7 @@ describe("Noema worker", () => { if (url === "https://api.github.com/app/installations/12345/access_tokens") { return Response.json({ token: "ghs_installation_token", - expires_at: "2026-07-02T05:00:00Z", + expires_at: tokenExpiresAt, }); } return new Response("not found", { status: 404 }); @@ -423,7 +424,7 @@ describe("Noema worker", () => { token: "ghs_installation_token", repository: "ContextualWisdomLab/noema", workflow_ref: "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main", - token_expires_at: "2026-07-02T05:00:00Z", + token_expires_at: tokenExpiresAt, }, }); const tokenRequest = requests.find((request) => request.url.endsWith("/app/installations/12345/access_tokens")); From 23cc641dc8dfe2878e99b49c64385cbea36258e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:12:33 -0700 Subject: [PATCH 023/102] test(github): reject invalid configured installation ids --- ...xplicit-installation-id-validation.test.ts | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 test/github-app-explicit-installation-id-validation.test.ts diff --git a/test/github-app-explicit-installation-id-validation.test.ts b/test/github-app-explicit-installation-id-validation.test.ts new file mode 100644 index 000000000..ca6ddc2f7 --- /dev/null +++ b/test/github-app-explicit-installation-id-validation.test.ts @@ -0,0 +1,150 @@ +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import worker, { type Env } from "../src/index"; + +const configuredRef = + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; +const configuredWorkflowSha = "a".repeat(40); + +const baseEnv: Env = { + ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", + ALLOWED_AUDIENCE: "cwl-noema-review", + ALLOWED_REPOSITORY_OWNER: "ContextualWisdomLab", + ALLOWED_WORKFLOW_REPOSITORY: "ContextualWisdomLab/.github", + ALLOWED_WORKFLOW_REF_PREFIX: configuredRef, + ALLOWED_WORKFLOW_SHA: configuredWorkflowSha, + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: "initialized-in-beforeAll", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", +}; + +let oidcKeyPair: CryptoKeyPair; +let oidcPublicJwk: JsonWebKey; +let appPrivateKeyPem: string; + +function encodeSegment(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +function encodeBytes(bytes: ArrayBuffer): string { + return Buffer.from(bytes).toString("base64url"); +} + +function pemFromPkcs8(pkcs8: ArrayBuffer): string { + const base64 = Buffer.from(pkcs8).toString("base64"); + const lines = base64.match(/.{1,64}/g)?.join("\n") ?? base64; + return `-----BEGIN PRIVATE KEY-----\n${lines}\n-----END PRIVATE KEY-----`; +} + +async function generateRsaKeyPair(): Promise { + return crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + ); +} + +beforeAll(async () => { + oidcKeyPair = await generateRsaKeyPair(); + oidcPublicJwk = await crypto.subtle.exportKey("jwk", oidcKeyPair.publicKey); + const appKeyPair = await generateRsaKeyPair(); + appPrivateKeyPem = pemFromPkcs8( + await crypto.subtle.exportKey("pkcs8", appKeyPair.privateKey), + ); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +async function signedOidcToken() { + const kid = `explicit-installation-${crypto.randomUUID()}`; + const now = Math.floor(Date.now() / 1000); + const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); + const payload = encodeSegment({ + iss: baseEnv.ALLOWED_ISSUER, + aud: baseEnv.ALLOWED_AUDIENCE, + repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, + repository: "ContextualWisdomLab/.github", + job_workflow_ref: configuredRef, + job_workflow_sha: configuredWorkflowSha, + sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }); + const signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + oidcKeyPair.privateKey, + new TextEncoder().encode(`${header}.${payload}`), + ); + return { + token: `${header}.${payload}.${encodeBytes(signature)}`, + jwk: { ...oidcPublicJwk, kid, kty: "RSA" }, + }; +} + +async function exchangeWithConfiguredInstallationId(installationId: string, clientIp: string) { + const { token, jwk } = await signedOidcToken(); + let githubApiCalls = 0; + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { + return Response.json({ + jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", + }); + } + if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { + return Response.json({ keys: [jwk] }); + } + githubApiCalls += 1; + return new Response("configured installation id must fail before GitHub App egress", { status: 500 }); + }); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json", + "cf-connecting-ip": clientIp, + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + { + ...baseEnv, + GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKeyPem, + GITHUB_APP_INSTALLATION_ID: installationId, + }, + ); + + return { response, githubApiCalls }; +} + +describe("configured GitHub App installation id", () => { + it.each([ + ["0", "203.0.113.250"], + ["-1", "203.0.113.251"], + ["1.5", "203.0.113.252"], + ["12345/../../repos", "203.0.113.253"], + ])("fails closed before GitHub App egress for invalid id %s", async (installationId, clientIp) => { + const { response, githubApiCalls } = await exchangeWithConfiguredInstallationId(installationId, clientIp); + + expect(response.status).toBe(500); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_INSTALLATION", + message: "GitHub App installation id configuration is invalid", + details: { + field: "GITHUB_APP_INSTALLATION_ID", + reason: "must be a positive integer", + }, + }); + expect(githubApiCalls).toBe(0); + }); +}); From 545080422c2ba6a77a83238814a8e847b48e154c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 01:13:39 -0700 Subject: [PATCH 024/102] fix(github): validate configured installation id --- src/index.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 54930541c..719ec7f7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -534,7 +534,17 @@ async function githubJson(path: string, init: GitHubJsonRequestInit, env: Env): } async function resolveInstallationId(appJwt: string, repository: string, env: Env): Promise { - if (env.GITHUB_APP_INSTALLATION_ID) return env.GITHUB_APP_INSTALLATION_ID; + if (env.GITHUB_APP_INSTALLATION_ID) { + const configuredInstallationId = env.GITHUB_APP_INSTALLATION_ID; + if (!/^[1-9]\d*$/.test(configuredInstallationId)) { + throw new ApiError("ERR_GITHUB_INSTALLATION", 500, "GitHub App installation id configuration is invalid"); + } + const numericInstallationId = Number(configuredInstallationId); + if (!Number.isSafeInteger(numericInstallationId) || String(numericInstallationId) !== configuredInstallationId) { + throw new ApiError("ERR_GITHUB_INSTALLATION", 500, "GitHub App installation id configuration is invalid"); + } + return configuredInstallationId; + } const now = Date.now(); const cacheKey = `${env.GITHUB_API_BASE}:${env.GITHUB_APP_ID}:${repository}`; const cached = installationIdCache.get(cacheKey); From f751f7c1b40796eb635d3d1dddb74618758cb483 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 02:17:26 -0700 Subject: [PATCH 025/102] test(github): align configured installation-id contract --- test/github-app-explicit-installation-id-validation.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/github-app-explicit-installation-id-validation.test.ts b/test/github-app-explicit-installation-id-validation.test.ts index ca6ddc2f7..cc0095cb6 100644 --- a/test/github-app-explicit-installation-id-validation.test.ts +++ b/test/github-app-explicit-installation-id-validation.test.ts @@ -132,6 +132,8 @@ describe("configured GitHub App installation id", () => { ["-1", "203.0.113.251"], ["1.5", "203.0.113.252"], ["12345/../../repos", "203.0.113.253"], + ["01", "203.0.113.254"], + ["9007199254740992", "203.0.113.255"], ])("fails closed before GitHub App egress for invalid id %s", async (installationId, clientIp) => { const { response, githubApiCalls } = await exchangeWithConfiguredInstallationId(installationId, clientIp); @@ -140,10 +142,6 @@ describe("configured GitHub App installation id", () => { ok: false, error_code: "ERR_GITHUB_INSTALLATION", message: "GitHub App installation id configuration is invalid", - details: { - field: "GITHUB_APP_INSTALLATION_ID", - reason: "must be a positive integer", - }, }); expect(githubApiCalls).toBe(0); }); From 1d6cf24a19f3ed9e3a82f166524306c748fcc521 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 07:03:32 -0700 Subject: [PATCH 026/102] test(readiness): reject unsafe GitHub numeric identifiers --- test/runtime-readiness-id-range.test.ts | 56 +++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 test/runtime-readiness-id-range.test.ts diff --git a/test/runtime-readiness-id-range.test.ts b/test/runtime-readiness-id-range.test.ts new file mode 100644 index 000000000..4e9b3e27d --- /dev/null +++ b/test/runtime-readiness-id-range.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; + +import { + evaluateRuntimeReadiness, + type RuntimeReadinessEnv, +} from "../src/runtime-readiness"; + +function dummyNamespace(): DurableObjectNamespace { + return { + idFromName(name: string) { + return { toString: () => name } as DurableObjectId; + }, + get() { + return {} as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; +} + +function baseEnv(): RuntimeReadinessEnv { + return { + ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", + ALLOWED_AUDIENCE: "cwl-noema-review", + ALLOWED_REPOSITORY_OWNER: "ContextualWisdomLab", + ALLOWED_WORKFLOW_REPOSITORY: "ContextualWisdomLab/.github", + ALLOWED_WORKFLOW_REF_PREFIX: + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main", + ALLOWED_WORKFLOW_SHA: "0123456789abcdef0123456789abcdef01234567", + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "123456", + // Deliberately non-importable. These tests assert the identifier-specific + // failure remains present in addition to the independent key failure. + GITHUB_APP_PRIVATE_KEY_PEM: "not-a-private-key", + GITHUB_APP_INSTALLATION_ID: "987654", + NOEMA_RATE_LIMITER: dummyNamespace(), + NOEMA_OIDC_REPLAY_GUARD: dummyNamespace(), + }; +} + +describe("runtime readiness GitHub numeric identifier bounds", () => { + it.each([ + ["GITHUB_APP_ID", "github_app_id"], + ["GITHUB_APP_INSTALLATION_ID", "github_app_installation_id"], + ] as const)( + "rejects a %s value outside JavaScript's exact safe-integer range", + async (field, expectedFailure) => { + const env = baseEnv(); + env[field] = "9007199254740992"; + + const result = await evaluateRuntimeReadiness(env); + + expect(result.ready).toBe(false); + expect(result.failedChecks).toContain(expectedFailure); + expect(result.failedChecks).toContain("github_app_private_key"); + }, + ); +}); From e7b5c87da2320d1e41dd48376bc5bc774e2537d1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 07:04:17 -0700 Subject: [PATCH 027/102] fix(readiness): bound GitHub numeric identifiers --- src/runtime-readiness.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/runtime-readiness.ts b/src/runtime-readiness.ts index 62e53114b..d64cea3f3 100644 --- a/src/runtime-readiness.ts +++ b/src/runtime-readiness.ts @@ -95,6 +95,12 @@ function isExactWorkflowRef(value: string, repository: string): boolean { return exactCommitPattern.test(refName) || trustedNamedRefPattern.test(refName); } +function isCanonicalPositiveSafeInteger(value: string | undefined): boolean { + if (!positiveDecimalPattern.test(value ?? "")) return false; + const numericValue = Number(value); + return Number.isSafeInteger(numericValue) && String(numericValue) === value; +} + function isDurableObjectNamespace(value: unknown): value is DurableObjectNamespace { if (!value || (typeof value !== "object" && typeof value !== "function")) { return false; @@ -178,7 +184,7 @@ export async function evaluateRuntimeReadiness( if (!isTrustedGithubApiBase(env.GITHUB_API_BASE)) { failedChecks.push("github_api_base"); } - if (!positiveDecimalPattern.test(env.GITHUB_APP_ID ?? "")) { + if (!isCanonicalPositiveSafeInteger(env.GITHUB_APP_ID)) { failedChecks.push("github_app_id"); } if (!await cachedPrivateKeyImportability(env)) { @@ -186,7 +192,7 @@ export async function evaluateRuntimeReadiness( } if ( env.GITHUB_APP_INSTALLATION_ID !== undefined - && !positiveDecimalPattern.test(env.GITHUB_APP_INSTALLATION_ID) + && !isCanonicalPositiveSafeInteger(env.GITHUB_APP_INSTALLATION_ID) ) { failedChecks.push("github_app_installation_id"); } From f0425cea115d29292c2794b4b03ae07403c8586e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 07:09:43 -0700 Subject: [PATCH 028/102] test(github): reject invalid App ids before egress --- test/github-app-id-validation.test.ts | 148 ++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 test/github-app-id-validation.test.ts diff --git a/test/github-app-id-validation.test.ts b/test/github-app-id-validation.test.ts new file mode 100644 index 000000000..e6614a74b --- /dev/null +++ b/test/github-app-id-validation.test.ts @@ -0,0 +1,148 @@ +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import worker, { type Env } from "../src/index"; + +const configuredRef = + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; +const configuredWorkflowSha = "a".repeat(40); + +const baseEnv: Env = { + ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", + ALLOWED_AUDIENCE: "cwl-noema-review", + ALLOWED_REPOSITORY_OWNER: "ContextualWisdomLab", + ALLOWED_WORKFLOW_REPOSITORY: "ContextualWisdomLab/.github", + ALLOWED_WORKFLOW_REF_PREFIX: configuredRef, + ALLOWED_WORKFLOW_SHA: configuredWorkflowSha, + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: "initialized-in-beforeAll", + GITHUB_APP_INSTALLATION_ID: "12345", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", +}; + +let oidcKeyPair: CryptoKeyPair; +let oidcPublicJwk: JsonWebKey; +let appPrivateKeyPem: string; + +function encodeSegment(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +function encodeBytes(bytes: ArrayBuffer): string { + return Buffer.from(bytes).toString("base64url"); +} + +function pemFromPkcs8(pkcs8: ArrayBuffer): string { + const base64 = Buffer.from(pkcs8).toString("base64"); + const lines = base64.match(/.{1,64}/g)?.join("\n") ?? base64; + return `-----BEGIN PRIVATE KEY-----\n${lines}\n-----END PRIVATE KEY-----`; +} + +async function generateRsaKeyPair(): Promise { + return crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + ); +} + +beforeAll(async () => { + oidcKeyPair = await generateRsaKeyPair(); + oidcPublicJwk = await crypto.subtle.exportKey("jwk", oidcKeyPair.publicKey); + const appKeyPair = await generateRsaKeyPair(); + appPrivateKeyPem = pemFromPkcs8( + await crypto.subtle.exportKey("pkcs8", appKeyPair.privateKey), + ); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +async function signedOidcToken() { + const kid = `invalid-app-id-${crypto.randomUUID()}`; + const now = Math.floor(Date.now() / 1000); + const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); + const payload = encodeSegment({ + iss: baseEnv.ALLOWED_ISSUER, + aud: baseEnv.ALLOWED_AUDIENCE, + repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, + repository: "ContextualWisdomLab/.github", + job_workflow_ref: configuredRef, + job_workflow_sha: configuredWorkflowSha, + sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }); + const signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + oidcKeyPair.privateKey, + new TextEncoder().encode(`${header}.${payload}`), + ); + return { + token: `${header}.${payload}.${encodeBytes(signature)}`, + jwk: { ...oidcPublicJwk, kid, kty: "RSA" }, + }; +} + +async function exchangeWithAppId(appId: string, clientIp: string) { + const { token, jwk } = await signedOidcToken(); + let githubApiCalls = 0; + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { + return Response.json({ + jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", + }); + } + if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { + return Response.json({ keys: [jwk] }); + } + githubApiCalls += 1; + return new Response("invalid App id must fail before GitHub App egress", { status: 500 }); + }); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json", + "cf-connecting-ip": clientIp, + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + { + ...baseEnv, + GITHUB_APP_ID: appId, + GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKeyPem, + }, + ); + + return { response, githubApiCalls }; +} + +describe("configured GitHub App id", () => { + it.each([ + ["0", "203.0.113.210"], + ["-1", "203.0.113.211"], + ["1.5", "203.0.113.212"], + ["01", "203.0.113.213"], + ["9007199254740992", "203.0.113.214"], + ])("fails closed before GitHub App egress for invalid id %s", async (appId, clientIp) => { + const { response, githubApiCalls } = await exchangeWithAppId(appId, clientIp); + + expect(response.status).toBe(500); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_INSTALLATION", + message: "GitHub App id configuration is invalid", + }); + expect(githubApiCalls).toBe(0); + }); +}); From 760b61ca6b854e516f4d67d32425b0c8c73b0745 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 07:12:02 -0700 Subject: [PATCH 029/102] test(github): bind App id rejection to public edge --- test/github-app-id-validation.test.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/github-app-id-validation.test.ts b/test/github-app-id-validation.test.ts index e6614a74b..fd29f7513 100644 --- a/test/github-app-id-validation.test.ts +++ b/test/github-app-id-validation.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; -import worker, { type Env } from "../src/index"; +import entrypoint, { type Env } from "../src/entrypoint"; const configuredRef = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; @@ -107,7 +107,7 @@ async function exchangeWithAppId(appId: string, clientIp: string) { return new Response("invalid App id must fail before GitHub App egress", { status: 500 }); }); - const response = await worker.fetch( + const response = await entrypoint.fetch( new Request("https://noema.example/exchange", { method: "POST", headers: { @@ -134,14 +134,17 @@ describe("configured GitHub App id", () => { ["1.5", "203.0.113.212"], ["01", "203.0.113.213"], ["9007199254740992", "203.0.113.214"], - ])("fails closed before GitHub App egress for invalid id %s", async (appId, clientIp) => { + ])("fails closed at the public edge before GitHub App egress for invalid id %s", async (appId, clientIp) => { const { response, githubApiCalls } = await exchangeWithAppId(appId, clientIp); - expect(response.status).toBe(500); + expect(response.status).toBe(503); await expect(response.json()).resolves.toMatchObject({ ok: false, - error_code: "ERR_GITHUB_INSTALLATION", - message: "GitHub App id configuration is invalid", + error_code: "ERR_GITHUB_API", + message: "GitHub API trust configuration unavailable", + details: { + policy: "github-app-id-canonical", + }, }); expect(githubApiCalls).toBe(0); }); From a09a21e5eb20e3090dda0e6777d18f48e45c9a91 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 07:13:01 -0700 Subject: [PATCH 030/102] fix(github): reject invalid App ids at public edge --- src/entrypoint.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/entrypoint.ts b/src/entrypoint.ts index 4b0f71eb7..691c77433 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -19,6 +19,7 @@ const TRUSTED_GITHUB_API_ORIGIN = "https://api.github.com"; const trustedGithubApiBasePattern = /^https:\/\/api\.github\.com(?::443)?\/?$/; const trustedTracePattern = /^[A-Za-z0-9._:-]+$/; const jwtSegmentPattern = /^[A-Za-z0-9_-]+$/; +const positiveDecimalPattern = /^[1-9][0-9]*$/; const MAX_TRACE_LENGTH = 128; const MAX_AUTHORIZATION_HEADER_LENGTH = 16_384; const MAX_JWT_HEADER_SEGMENT_LENGTH = 2_048; @@ -29,7 +30,7 @@ const MAX_EXCHANGE_JSON_BODY_BYTES = 8_192; type EgressFailure = { hint: string; outcome: "misconfigured" | "policy_unavailable"; - policy: "github-cloud-exact-origin" | "credential-fetch-no-redirect"; + policy: "github-cloud-exact-origin" | "github-app-id-canonical" | "credential-fetch-no-redirect"; }; type ExchangeBodyFailure = { @@ -78,6 +79,12 @@ export function isTrustedGithubApiBase(value: unknown): value is string { } } +function isCanonicalPositiveSafeInteger(value: string): boolean { + if (!positiveDecimalPattern.test(value)) return false; + const numericValue = Number(value); + return Number.isSafeInteger(numericValue) && String(numericValue) === value; +} + /** * Accept only a compact, bounded JWT envelope before any decoding or credential use. * Missing and non-Bearer authorization values are delegated to the normal API error path. @@ -499,6 +506,16 @@ export default { } request = boundedRequest.request; + const appIdFailure: EgressFailure = { + hint: "Configure GITHUB_APP_ID as a canonical positive decimal safe integer.", + outcome: "misconfigured", + policy: "github-app-id-canonical", + }; + if (!isCanonicalPositiveSafeInteger(env.GITHUB_APP_ID)) { + recordConfigurationFailure(request, appIdFailure); + return githubApiConfigurationResponse(request, appIdFailure); + } + const originFailure: EgressFailure = { hint: "Configure GITHUB_API_BASE as the exact GitHub Cloud REST API origin.", outcome: "misconfigured", From 589c977794a81fbc3df92a4555cc062cd9ece325 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 09:07:16 -0700 Subject: [PATCH 031/102] test(exchange): reject oversized body without awaiting cleanup --- test/exchange-body-cleanup-liveness.test.ts | 48 +++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 test/exchange-body-cleanup-liveness.test.ts diff --git a/test/exchange-body-cleanup-liveness.test.ts b/test/exchange-body-cleanup-liveness.test.ts new file mode 100644 index 000000000..e1e5d6432 --- /dev/null +++ b/test/exchange-body-cleanup-liveness.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { boundExchangeJsonBody } from "../src/entrypoint"; + +function streamedJsonRequest(stream: ReadableStream): Request { + return new Request("https://noema.example/exchange", { + method: "POST", + headers: { "content-type": "application/json" }, + body: stream, + duplex: "half", + } as RequestInit & { duplex: "half" }); +} + +describe("exchange JSON body cleanup liveness", () => { + it("does not await a never-settling stream cancellation after the body is already oversized", async () => { + let observeCancel: (() => void) | undefined; + const cancelObserved = new Promise((resolve) => { + observeCancel = resolve; + }); + let emitted = false; + const request = streamedJsonRequest(new ReadableStream({ + pull(controller) { + if (emitted) return; + emitted = true; + controller.enqueue(new Uint8Array(8_193)); + }, + cancel() { + observeCancel?.(); + return new Promise(() => undefined); + }, + })); + + let settled = false; + const resultPromise = boundExchangeJsonBody(request).then((result) => { + settled = true; + return result; + }); + + await cancelObserved; + await Promise.resolve(); + await Promise.resolve(); + + expect(settled).toBe(true); + await expect(resultPromise).resolves.toEqual({ + ok: false, + failure: { reason: "too_large", status: 413 }, + }); + }); +}); From 04d8c9241839261d21cd9020558a4fd09b27807e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 09:09:33 -0700 Subject: [PATCH 032/102] fix(exchange): never await rejected-body cleanup --- src/entrypoint.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/entrypoint.ts b/src/entrypoint.ts index 691c77433..758122d90 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -221,7 +221,9 @@ export async function boundExchangeJsonBody(request: Request): Promise MAX_EXCHANGE_JSON_BODY_BYTES) { try { - await reader.cancel("Noema exchange JSON body exceeds byte limit"); + void reader + .cancel("Noema exchange JSON body exceeds byte limit") + .catch(() => undefined); } catch { // Cancellation is best-effort after the request has already been rejected. } From 07c3bdb581756dbdf24888d9cec25224130069db Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 10:04:32 -0700 Subject: [PATCH 033/102] test(exchange): require early rejection body cleanup --- ...hange-body-early-rejection-cleanup.test.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 test/exchange-body-early-rejection-cleanup.test.ts diff --git a/test/exchange-body-early-rejection-cleanup.test.ts b/test/exchange-body-early-rejection-cleanup.test.ts new file mode 100644 index 000000000..904ef1b70 --- /dev/null +++ b/test/exchange-body-early-rejection-cleanup.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import { boundExchangeJsonBody } from "../src/entrypoint"; + +function requestWithStream( + stream: ReadableStream, + headers: HeadersInit, +): Request { + return new Request("https://noema.example/exchange", { + method: "POST", + headers, + body: stream, + duplex: "half", + } as RequestInit & { duplex: "half" }); +} + +async function expectBoundedEarlyRejection( + request: Request, + expected: { reason: "too_large" | "unsupported_media_type"; status: 413 | 415 }, +): Promise { + const result = await Promise.race([ + boundExchangeJsonBody(request), + new Promise((_, reject) => { + setTimeout(() => reject(new Error("early rejection waited for request-body cleanup")), 100); + }), + ]); + + expect(result).toEqual({ ok: false, failure: expected }); +} + +describe("exchange JSON body early-rejection cleanup", () => { + it("cancels a declared-oversized request body without awaiting cancellation", async () => { + let cancelObserved = false; + const request = requestWithStream( + new ReadableStream({ + cancel() { + cancelObserved = true; + return new Promise(() => undefined); + }, + }), + { + "content-type": "application/json", + "content-length": "8193", + }, + ); + + await expectBoundedEarlyRejection(request, { reason: "too_large", status: 413 }); + expect(cancelObserved).toBe(true); + }); + + it("cancels an unsupported-media request body without awaiting cancellation", async () => { + let cancelObserved = false; + const request = requestWithStream( + new ReadableStream({ + cancel() { + cancelObserved = true; + return new Promise(() => undefined); + }, + }), + { "content-type": "text/plain" }, + ); + + await expectBoundedEarlyRejection(request, { reason: "unsupported_media_type", status: 415 }); + expect(cancelObserved).toBe(true); + }); +}); From 6a26142c561d49090bd959cd85141dc5daed44a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 10:06:06 -0700 Subject: [PATCH 034/102] fix(exchange): release rejected request bodies --- src/entrypoint.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/entrypoint.ts b/src/entrypoint.ts index 758122d90..d78d78225 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -174,6 +174,15 @@ function hasDuplicateTargetRepositoryKey(body: Uint8Array): boolean { return false; } +function cancelRequestBodyBestEffort(request: Request, reason: string): void { + try { + if (request.body === null) return; + void request.body.cancel(reason).catch(() => undefined); + } catch { + // Cancellation is best-effort after the request has already been rejected. + } +} + /** * Consume and rebuild only JSON POST bodies within the exchange API's byte budget. * Streaming consumption prevents a chunked request from bypassing Content-Length checks. @@ -193,6 +202,7 @@ export async function boundExchangeJsonBody(request: Request): Promise MAX_EXCHANGE_JSON_BODY_BYTES ) { + cancelRequestBodyBestEffort(request, "Noema exchange JSON body exceeds declared byte limit"); return { ok: false, failure: { reason: "too_large", status: 413 }, From c918efdc17fa717c26545ad036c9296ae67bfaab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 11:05:08 -0700 Subject: [PATCH 035/102] test(egress): classify credential transport failures --- test/outbound-fetch-transport-failure.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test/outbound-fetch-transport-failure.test.ts diff --git a/test/outbound-fetch-transport-failure.test.ts b/test/outbound-fetch-transport-failure.test.ts new file mode 100644 index 000000000..7c24011a9 --- /dev/null +++ b/test/outbound-fetch-transport-failure.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { createFailClosedFetch } from "../src/outbound-fetch-policy"; + +describe("outbound credential transport failures", () => { + it("classifies a trusted GitHub API transport rejection as a bounded 502 response", async () => { + const protectedFetch = createFailClosedFetch(async () => { + throw new TypeError("synthetic network transport failure"); + }); + + const response = await protectedFetch( + "https://api.github.com/repos/ContextualWisdomLab/noema/installation", + { + method: "GET", + headers: { authorization: "Bearer synthetic-app-jwt" }, + }, + ); + + expect(response.status).toBe(502); + expect(response.headers.get("cache-control")).toBe("no-store"); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-transport"); + await expect(response.text()).resolves.toBe(""); + }); +}); From 339d752dc607b24911b76af773b3f6c35fc3efa8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 11:06:09 -0700 Subject: [PATCH 036/102] fix(egress): fail closed on transport rejection --- src/outbound-fetch-policy.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 70ecb89a0..a6d9b94b3 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -20,7 +20,7 @@ type FetchInstallation = { wrapped: FetchLike; }; -type BlockReason = "destination" | "request-policy" | "redirect" | "response-size" | "response-read" | "timeout"; +type BlockReason = "destination" | "request-policy" | "redirect" | "response-size" | "response-read" | "timeout" | "transport"; type GitHubApiOperation = | "repository-installation" @@ -304,9 +304,9 @@ export function isTrustedCredentialEgressRequest( } /** - * Wraps fetch with fail-closed credential destination, request-role, redirect, response-size, and timeout enforcement. + * Wraps fetch with fail-closed credential destination, request-role, redirect, response-size, timeout, and transport enforcement. * @param rawFetch Trusted underlying fetch implementation that performs only requests admitted by the wrapper. - * @returns A fetch-compatible function that blocks redirects and timeout violations instead of leaking credentials. + * @returns A fetch-compatible function that blocks redirects, transport failures, and timeout violations instead of leaking credentials or surfacing ambiguous internal errors. */ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { return async (input, init) => { @@ -342,7 +342,10 @@ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { if (signal.aborted && signal.reason === timeoutReason) { return blockedResponse("timeout"); } - throw error; + if (signal.aborted) { + throw error; + } + return blockedResponse("transport"); } finally { clearTimeout(timeoutHandle); } From 2fd23956dc4938ee97ab4290efbd8054a207de20 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 11:20:25 -0700 Subject: [PATCH 037/102] fix(egress): scope transport failure to credentials --- src/outbound-fetch-policy.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index a6d9b94b3..15ae8d979 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -304,9 +304,9 @@ export function isTrustedCredentialEgressRequest( } /** - * Wraps fetch with fail-closed credential destination, request-role, redirect, response-size, timeout, and transport enforcement. + * Wraps fetch with fail-closed credential destination, request-role, redirect, response-size, timeout, and credential-bearing transport enforcement. * @param rawFetch Trusted underlying fetch implementation that performs only requests admitted by the wrapper. - * @returns A fetch-compatible function that blocks redirects, transport failures, and timeout violations instead of leaking credentials or surfacing ambiguous internal errors. + * @returns A fetch-compatible function that blocks credential-bearing transport failures and policy violations while preserving caller cancellation and ordinary unauthenticated transport errors. */ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { return async (input, init) => { @@ -345,7 +345,10 @@ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { if (signal.aborted) { throw error; } - return blockedResponse("transport"); + if (outboundHeaders(input, init).has("authorization")) { + return blockedResponse("transport"); + } + throw error; } finally { clearTimeout(timeoutHandle); } From f13e73da5af91b0be592d01e8784a49e2450aed9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 12:04:31 -0700 Subject: [PATCH 038/102] test(readiness): require immutable workflow source coherence --- ...eadiness-workflow-source-coherence.test.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 test/runtime-readiness-workflow-source-coherence.test.ts diff --git a/test/runtime-readiness-workflow-source-coherence.test.ts b/test/runtime-readiness-workflow-source-coherence.test.ts new file mode 100644 index 000000000..6d58a09bd --- /dev/null +++ b/test/runtime-readiness-workflow-source-coherence.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; + +import { + evaluateRuntimeReadiness, + type RuntimeReadinessEnv, +} from "../src/runtime-readiness"; + +function dummyNamespace(): DurableObjectNamespace { + return { + idFromName(name: string) { + return { toString: () => name } as DurableObjectId; + }, + get() { + return {} as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; +} + +function baseEnv(): RuntimeReadinessEnv { + return { + ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", + ALLOWED_AUDIENCE: "cwl-noema-review", + ALLOWED_REPOSITORY_OWNER: "ContextualWisdomLab", + ALLOWED_WORKFLOW_REPOSITORY: "ContextualWisdomLab/.github", + ALLOWED_WORKFLOW_REF_PREFIX: + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@0123456789abcdef0123456789abcdef01234567", + ALLOWED_WORKFLOW_SHA: "0123456789abcdef0123456789abcdef01234567", + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "123456", + GITHUB_APP_PRIVATE_KEY_PEM: "not-a-private-key", + GITHUB_APP_INSTALLATION_ID: "987654", + NOEMA_RATE_LIMITER: dummyNamespace(), + NOEMA_OIDC_REPLAY_GUARD: dummyNamespace(), + }; +} + +describe("runtime readiness immutable workflow-source coherence", () => { + it("fails closed when an immutable workflow ref commit disagrees with ALLOWED_WORKFLOW_SHA", async () => { + const env = baseEnv(); + env.ALLOWED_WORKFLOW_SHA = "89abcdef0123456789abcdef0123456789abcdef"; + + const result = await evaluateRuntimeReadiness(env); + + expect(result.ready).toBe(false); + expect(result.failedChecks).toContain("allowed_workflow_sha"); + expect(result.failedChecks).not.toContain("allowed_workflow_ref"); + }); + + it("accepts a coherent immutable workflow ref and source SHA at the workflow-source boundary", async () => { + const result = await evaluateRuntimeReadiness(baseEnv()); + + expect(result.failedChecks).not.toContain("allowed_workflow_ref"); + expect(result.failedChecks).not.toContain("allowed_workflow_sha"); + expect(result.failedChecks).toContain("github_app_private_key"); + }); +}); From 934fe0a2486da6b2418b60df9d05d7a0c9a36b6c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 12:07:28 -0700 Subject: [PATCH 039/102] fix(readiness): bind immutable workflow ref to source sha --- src/runtime-readiness.ts | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/runtime-readiness.ts b/src/runtime-readiness.ts index d64cea3f3..0ad58ee73 100644 --- a/src/runtime-readiness.ts +++ b/src/runtime-readiness.ts @@ -83,18 +83,25 @@ function isTrustedWorkflowRepository(value: string, owner: string): boolean { return new RegExp(`^${escapedOwner}/[A-Za-z0-9_.-]{1,100}$`).test(value); } -function isExactWorkflowRef(value: string, repository: string): boolean { +function workflowRefName(value: string, repository: string): string | undefined { const escapedRepository = escapeRegularExpression(repository); const workflowRefPattern = new RegExp( `^${escapedRepository}/\\.github/workflows/[A-Za-z0-9_.-]{1,100}\\.ya?ml@(.+)$`, ); - const match = workflowRefPattern.exec(value); - if (!match) return false; + return workflowRefPattern.exec(value)?.[1]; +} - const refName = match[1]; +function isExactWorkflowRef(value: string, repository: string): boolean { + const refName = workflowRefName(value, repository); + if (!refName) return false; return exactCommitPattern.test(refName) || trustedNamedRefPattern.test(refName); } +function immutableWorkflowCommit(value: string, repository: string): string | undefined { + const refName = workflowRefName(value, repository); + return refName && exactCommitPattern.test(refName) ? refName.toLowerCase() : undefined; +} + function isCanonicalPositiveSafeInteger(value: string | undefined): boolean { if (!positiveDecimalPattern.test(value ?? "")) return false; const numericValue = Number(value); @@ -144,13 +151,13 @@ function cachedPrivateKeyImportability(env: RuntimeReadinessEnv): Promise Date: Thu, 20 Aug 2026 12:29:12 -0700 Subject: [PATCH 040/102] test(github): reject invalid installation id at request edge --- ...pp-installation-id-edge-validation.test.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 test/github-app-installation-id-edge-validation.test.ts diff --git a/test/github-app-installation-id-edge-validation.test.ts b/test/github-app-installation-id-edge-validation.test.ts new file mode 100644 index 000000000..3e53324ab --- /dev/null +++ b/test/github-app-installation-id-edge-validation.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import entrypoint, { type Env } from "../src/entrypoint"; + +const baseEnv: Env = { + ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", + ALLOWED_AUDIENCE: "cwl-noema-review", + ALLOWED_REPOSITORY_OWNER: "ContextualWisdomLab", + ALLOWED_WORKFLOW_REPOSITORY: "ContextualWisdomLab/.github", + ALLOWED_WORKFLOW_REF_PREFIX: + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main", + ALLOWED_WORKFLOW_SHA: "a".repeat(40), + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: "not-used-before-request-edge-validation", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", +}; + +describe("configured GitHub App installation id at the public request edge", () => { + it.each([ + "0", + "-1", + "1.5", + "01", + "9007199254740992", + "12345/../../repos", + ])("fails closed before authentication or replay work for invalid id %s", async (installationId) => { + const response = await entrypoint.fetch( + new Request("https://noema.example/exchange", { method: "POST" }), + { + ...baseEnv, + GITHUB_APP_INSTALLATION_ID: installationId, + }, + ); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_API", + message: "GitHub API trust configuration unavailable", + details: { + policy: "github-app-installation-id-canonical", + }, + }); + }); +}); From c5d9b764f8f5c62834cfa0f9911bf88604dc16e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 12:36:11 -0700 Subject: [PATCH 041/102] fix(github): reject invalid installation id at request edge --- src/entrypoint.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/entrypoint.ts b/src/entrypoint.ts index d78d78225..cf9f942dc 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -30,7 +30,11 @@ const MAX_EXCHANGE_JSON_BODY_BYTES = 8_192; type EgressFailure = { hint: string; outcome: "misconfigured" | "policy_unavailable"; - policy: "github-cloud-exact-origin" | "github-app-id-canonical" | "credential-fetch-no-redirect"; + policy: + | "github-cloud-exact-origin" + | "github-app-id-canonical" + | "github-app-installation-id-canonical" + | "credential-fetch-no-redirect"; }; type ExchangeBodyFailure = { @@ -529,6 +533,17 @@ export default { return githubApiConfigurationResponse(request, appIdFailure); } + const installationId = env.GITHUB_APP_INSTALLATION_ID; + if (installationId !== undefined && !isCanonicalPositiveSafeInteger(installationId)) { + const installationIdFailure: EgressFailure = { + hint: "Configure GITHUB_APP_INSTALLATION_ID as a canonical positive decimal safe integer when set.", + outcome: "misconfigured", + policy: "github-app-installation-id-canonical", + }; + recordConfigurationFailure(request, installationIdFailure); + return githubApiConfigurationResponse(request, installationIdFailure); + } + const originFailure: EgressFailure = { hint: "Configure GITHUB_API_BASE as the exact GitHub Cloud REST API origin.", outcome: "misconfigured", From 257063a4a4a842af5216dd83df958dcd9edf6037 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 13:11:39 -0700 Subject: [PATCH 042/102] test(github): bound installation id in credential egress path --- ...tbound-fetch-installation-id-range.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 test/outbound-fetch-installation-id-range.test.ts diff --git a/test/outbound-fetch-installation-id-range.test.ts b/test/outbound-fetch-installation-id-range.test.ts new file mode 100644 index 000000000..6f81777a6 --- /dev/null +++ b/test/outbound-fetch-installation-id-range.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { isTrustedCredentialEgressRequest } from "../src/outbound-fetch-policy"; + +const body = JSON.stringify({ + repositories: ["noema"], + permissions: { + contents: "read", + pull_requests: "write", + checks: "read", + }, +}); + +function requestFor(id: string) { + return isTrustedCredentialEgressRequest( + `https://api.github.com/app/installations/${id}/access_tokens`, + { + method: "POST", + headers: { + authorization: "Bearer app-jwt", + "content-type": "application/json", + }, + body, + }, + ); +} + +describe("credential egress installation-id authority", () => { + it("accepts the maximum canonical safe integer installation id", () => { + expect(requestFor(String(Number.MAX_SAFE_INTEGER))).toBe(true); + }); + + it.each([ + "9007199254740992", + "9999999999999999999999999999999999999999", + ])("rejects an installation id outside the JavaScript safe-integer boundary: %s", (id) => { + expect(requestFor(id)).toBe(false); + }); +}); From 9e6f766457160fd9dce36687bc6c6d5f69737b00 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 13:12:27 -0700 Subject: [PATCH 043/102] fix(github): bound installation id in credential egress path --- src/outbound-fetch-policy.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 15ae8d979..be20ae1a3 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -42,7 +42,7 @@ const githubRepositoryInstallationPathPattern = new RegExp( ); const githubAppInstallationsPathPattern = /^\/app\/installations$/; const githubInstallationTokenPathPattern = - /^\/app\/installations\/[1-9][0-9]*\/access_tokens$/; + /^\/app\/installations\/([1-9][0-9]*)\/access_tokens$/; const githubRepositoryNamePattern = /^(?!\.{1,2}$)[A-Za-z0-9_.-]+$/; const installations = new WeakMap(); @@ -211,6 +211,17 @@ async function boundedOutboundResponse(response: Response): Promise { }); } +function canonicalInstallationIdFromTokenPath(url: URL): string | undefined { + const match = url.pathname.match(githubInstallationTokenPathPattern); + if (!match) return undefined; + const installationId = match[1]; + const numericId = Number(installationId); + if (!Number.isSafeInteger(numericId) || String(numericId) !== installationId) { + return undefined; + } + return installationId; +} + function githubApiOperation(url: URL): GitHubApiOperation | undefined { if (url.search !== "") return undefined; if (githubRepositoryInstallationPathPattern.test(url.pathname)) { @@ -219,7 +230,7 @@ function githubApiOperation(url: URL): GitHubApiOperation | undefined { if (githubAppInstallationsPathPattern.test(url.pathname)) { return "app-installations"; } - if (githubInstallationTokenPathPattern.test(url.pathname)) { + if (canonicalInstallationIdFromTokenPath(url) !== undefined) { return "installation-token"; } return undefined; From 9e5925b5d1511d12d531adbe7fceccf59728e830 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:06:45 -0700 Subject: [PATCH 044/102] test(egress): clean up blocked redirect bodies --- test/outbound-fetch-cleanup-liveness.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/outbound-fetch-cleanup-liveness.test.ts b/test/outbound-fetch-cleanup-liveness.test.ts index 545f39bdc..87d5ab176 100644 --- a/test/outbound-fetch-cleanup-liveness.test.ts +++ b/test/outbound-fetch-cleanup-liveness.test.ts @@ -37,4 +37,15 @@ describe("outbound response cleanup liveness", () => { expect(await boundedOutcome(response)).toBe("blocked-response-size"); expect(cancel).toHaveBeenCalledOnce(); }); + + it("does not retain or await a blocked redirect response body", async () => { + const cancel = vi.fn(() => new Promise(() => {})); + const response = new Response(new ReadableStream({ cancel }), { + status: 302, + headers: { location: "https://example.invalid/redirect-target" }, + }); + + expect(await boundedOutcome(response)).toBe("blocked-redirect"); + expect(cancel).toHaveBeenCalledOnce(); + }); }); From cabdf672f0c7aa35411971528094f860d6b3638a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:07:24 -0700 Subject: [PATCH 045/102] fix(egress): clean up blocked redirect bodies --- src/outbound-fetch-policy.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index be20ae1a3..062b18be2 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -346,6 +346,11 @@ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { signal, }); if (response.redirected || (response.status >= 300 && response.status < 400)) { + if (response.body !== null) { + ignoreCancellationBestEffort(() => response.body!.cancel( + "Noema outbound redirect response is not accepted", + )); + } return blockedResponse("redirect"); } return await boundedOutboundResponse(response); From 92a954cfeab1d35984089f4390834229580209e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:37:38 -0700 Subject: [PATCH 046/102] test(github): clean up outbound read failures --- test/outbound-fetch-cleanup-liveness.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/outbound-fetch-cleanup-liveness.test.ts b/test/outbound-fetch-cleanup-liveness.test.ts index 87d5ab176..1462bf8d3 100644 --- a/test/outbound-fetch-cleanup-liveness.test.ts +++ b/test/outbound-fetch-cleanup-liveness.test.ts @@ -38,6 +38,20 @@ describe("outbound response cleanup liveness", () => { expect(cancel).toHaveBeenCalledOnce(); }); + it("cleans up a response stream that fails while being read without replacing the fail-closed result", async () => { + const response = new Response("ignored"); + const cancel = vi.fn(async () => undefined); + vi.spyOn(response.body!, "getReader").mockReturnValue({ + read: vi.fn(async () => { + throw new Error("synthetic outbound response read failure"); + }), + cancel, + } as unknown as ReadableStreamDefaultReader); + + expect(await boundedOutcome(response)).toBe("blocked-response-read"); + expect(cancel).toHaveBeenCalledOnce(); + }); + it("does not retain or await a blocked redirect response body", async () => { const cancel = vi.fn(() => new Promise(() => {})); const response = new Response(new ReadableStream({ cancel }), { From 8e95badfeffd7feabe27f816e4a08452161b9ae8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:38:30 -0700 Subject: [PATCH 047/102] fix(github): clean up outbound read failures --- src/outbound-fetch-policy.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 062b18be2..b6c7763dc 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -193,6 +193,9 @@ async function boundedOutboundResponse(response: Response): Promise { chunks.push(value); } } catch { + ignoreCancellationBestEffort(() => reader.cancel( + "Noema outbound response body could not be read", + )); return blockedResponse("response-read"); } From 5ef4b5564d88f83a4959ed989d4b4c0e62a30204 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:44:06 -0700 Subject: [PATCH 048/102] test(github): clean up exchange read failures --- test/exchange-body-cleanup-liveness.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/test/exchange-body-cleanup-liveness.test.ts b/test/exchange-body-cleanup-liveness.test.ts index e1e5d6432..2c8c7b2ca 100644 --- a/test/exchange-body-cleanup-liveness.test.ts +++ b/test/exchange-body-cleanup-liveness.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { boundExchangeJsonBody } from "../src/entrypoint"; function streamedJsonRequest(stream: ReadableStream): Request { @@ -45,4 +45,21 @@ describe("exchange JSON body cleanup liveness", () => { failure: { reason: "too_large", status: 413 }, }); }); + + it("cleans up a request stream that fails while being read without replacing the unreadable rejection", async () => { + const request = streamedJsonRequest(new ReadableStream()); + const cancel = vi.fn(async () => undefined); + vi.spyOn(request.body!, "getReader").mockReturnValue({ + read: vi.fn(async () => { + throw new Error("synthetic exchange request read failure"); + }), + cancel, + } as unknown as ReadableStreamDefaultReader); + + await expect(boundExchangeJsonBody(request)).resolves.toEqual({ + ok: false, + failure: { reason: "unreadable", status: 400 }, + }); + expect(cancel).toHaveBeenCalledOnce(); + }); }); From 7484ffdd913ced124ec2db2b4bb2872e24c55c46 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:45:09 -0700 Subject: [PATCH 049/102] fix(github): clean up exchange read failures --- src/entrypoint.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/entrypoint.ts b/src/entrypoint.ts index cf9f942dc..f55dbf748 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -187,6 +187,14 @@ function cancelRequestBodyBestEffort(request: Request, reason: string): void { } } +function cancelReaderBestEffort(reader: ReadableStreamDefaultReader, reason: string): void { + try { + void reader.cancel(reason).catch(() => undefined); + } catch { + // Cancellation is best-effort after the request has already been rejected. + } +} + /** * Consume and rebuild only JSON POST bodies within the exchange API's byte budget. * Streaming consumption prevents a chunked request from bypassing Content-Length checks. @@ -235,13 +243,7 @@ export async function boundExchangeJsonBody(request: Request): Promise MAX_EXCHANGE_JSON_BODY_BYTES) { - try { - void reader - .cancel("Noema exchange JSON body exceeds byte limit") - .catch(() => undefined); - } catch { - // Cancellation is best-effort after the request has already been rejected. - } + cancelReaderBestEffort(reader, "Noema exchange JSON body exceeds byte limit"); return { ok: false, failure: { reason: "too_large", status: 413 }, @@ -250,6 +252,7 @@ export async function boundExchangeJsonBody(request: Request): Promise Date: Thu, 20 Aug 2026 17:25:40 -0700 Subject: [PATCH 050/102] test(oidc): reject non-numeric not-before claims --- test/oidc-verification-residual-coverage.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/oidc-verification-residual-coverage.test.ts b/test/oidc-verification-residual-coverage.test.ts index 9587aea6a..95b7540dc 100644 --- a/test/oidc-verification-residual-coverage.test.ts +++ b/test/oidc-verification-residual-coverage.test.ts @@ -254,6 +254,19 @@ describe("OIDC verification residual coverage", () => { }); }); + it("rejects a non-numeric not-before claim instead of silently ignoring it", async () => { + const claims = baseClaims(); + claims.nbf = "not-a-numeric-date"; + const { response } = await exchange(await signedJwt(claims)); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_INVALID", + message: "OIDC not-before claim is invalid", + }); + }); + it("rejects a token without a numeric expiration", async () => { const claims = baseClaims(); delete claims.exp; From 2c8573ee76acc3835ed465cd24318354d65bc86d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 17:39:09 -0700 Subject: [PATCH 051/102] fix(oidc): reject non-numeric not-before claims --- src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.ts b/src/index.ts index 719ec7f7c..514cf591a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -452,6 +452,9 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise { match_policy: "exact-ref-and-source-sha" }, ); } + if (payload.nbf !== undefined && typeof payload.nbf !== "number") { + throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC not-before claim is invalid"); + } if (typeof payload.nbf === "number" && payload.nbf > now + 30) { throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC token is not valid yet"); } From a8fb0ee254320860373fadb7c33c786988b14f28 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 18:05:01 -0700 Subject: [PATCH 052/102] test(github): reject impossible token expiry --- ...on-token-expiry-calendar-integrity.test.ts | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 test/github-installation-token-expiry-calendar-integrity.test.ts diff --git a/test/github-installation-token-expiry-calendar-integrity.test.ts b/test/github-installation-token-expiry-calendar-integrity.test.ts new file mode 100644 index 000000000..c53f67149 --- /dev/null +++ b/test/github-installation-token-expiry-calendar-integrity.test.ts @@ -0,0 +1,136 @@ +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import worker, { type Env } from "../src/index"; + +const configuredRef = + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; +const configuredWorkflowSha = "a".repeat(40); + +const baseEnv: Env = { + ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", + ALLOWED_AUDIENCE: "cwl-noema-review", + ALLOWED_REPOSITORY_OWNER: "ContextualWisdomLab", + ALLOWED_WORKFLOW_REPOSITORY: "ContextualWisdomLab/.github", + ALLOWED_WORKFLOW_REF_PREFIX: configuredRef, + ALLOWED_WORKFLOW_SHA: configuredWorkflowSha, + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: "initialized-in-beforeAll", + GITHUB_APP_INSTALLATION_ID: "92345", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", +}; + +let oidcKeyPair: CryptoKeyPair; +let oidcPublicJwk: JsonWebKey; +let appPrivateKeyPem: string; + +function encodeSegment(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +function encodeBytes(bytes: ArrayBuffer): string { + return Buffer.from(bytes).toString("base64url"); +} + +function pemFromPkcs8(pkcs8: ArrayBuffer): string { + const base64 = Buffer.from(pkcs8).toString("base64"); + const lines = base64.match(/.{1,64}/g)?.join("\n") ?? base64; + return `-----BEGIN PRIVATE KEY-----\n${lines}\n-----END PRIVATE KEY-----`; +} + +async function generateRsaKeyPair(): Promise { + return crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + ); +} + +beforeAll(async () => { + oidcKeyPair = await generateRsaKeyPair(); + oidcPublicJwk = await crypto.subtle.exportKey("jwk", oidcKeyPair.publicKey); + const appKeyPair = await generateRsaKeyPair(); + appPrivateKeyPem = pemFromPkcs8( + await crypto.subtle.exportKey("pkcs8", appKeyPair.privateKey), + ); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +async function signedOidcToken(): Promise<{ token: string; jwk: JsonWebKey }> { + const kid = `github-expiry-calendar-${crypto.randomUUID()}`; + const now = Math.floor(Date.now() / 1000); + const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); + const payload = encodeSegment({ + iss: baseEnv.ALLOWED_ISSUER, + aud: baseEnv.ALLOWED_AUDIENCE, + repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, + repository: "ContextualWisdomLab/.github", + job_workflow_ref: configuredRef, + job_workflow_sha: configuredWorkflowSha, + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }); + const signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + oidcKeyPair.privateKey, + new TextEncoder().encode(`${header}.${payload}`), + ); + return { + token: `${header}.${payload}.${encodeBytes(signature)}`, + jwk: { ...oidcPublicJwk, kid, kty: "RSA" }, + }; +} + +describe("GitHub installation-token expiry calendar integrity", () => { + it("rejects an impossible calendar expiry that Date.parse would normalize into a live credential", async () => { + vi.spyOn(Date, "now").mockReturnValue(Date.parse("2030-03-02T00:00:00Z")); + const { token, jwk } = await signedOidcToken(); + + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { + return Response.json({ + jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", + }); + } + if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { + return Response.json({ keys: [jwk] }); + } + if (url === "https://api.github.com/app/installations/92345/access_tokens") { + return Response.json({ + token: "ghs_impossible_calendar_expiry", + expires_at: "2030-02-30T00:30:00Z", + }); + } + return new Response("unexpected GitHub request", { status: 500 }); + }); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json", + "cf-connecting-ip": "203.0.113.249", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/expiry-calendar" }), + }), + { ...baseEnv, GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKeyPem }, + ); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_API", + message: "GitHub API returned invalid installation-token expiry", + }); + }); +}); From 77c97b4850401bf2ef22529aa7c71c32a6e65578 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 18:07:37 -0700 Subject: [PATCH 053/102] fix(github): validate token expiry calendar --- src/index.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/index.ts b/src/index.ts index 514cf591a..4512e3e36 100644 --- a/src/index.ts +++ b/src/index.ts @@ -141,6 +141,7 @@ const errorHints: Record = { const trustedHeaderValuePattern = /^[A-Za-z0-9._:-]+$/; const clientIdentifierPattern = /^[A-Za-z0-9.:%_,-]+$/; const exactWorkflowSourceShaPattern = /^[0-9a-f]{40}$/; +const githubInstallationTokenExpiryPattern = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/; const maxTrustedHeaderLength = 128; const maxInstallationTokenLifetimeMs = 65 * 60_000; @@ -192,6 +193,18 @@ function valueType(value: unknown): string { return typeof value; } +function canonicalGithubInstallationTokenExpiry(value: string, parsedMs: number): boolean { + const match = value.match(githubInstallationTokenExpiryPattern); + if (!match) return false; + const milliseconds = (match[7] ?? "").padEnd(3, "0"); + const normalized = `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}.${milliseconds}Z`; + try { + return new Date(parsedMs).toISOString() === normalized; + } catch { + return false; + } +} + function requestClientKey(request: Request, route: string): string { const client = request.headers.get("cf-connecting-ip") || request.headers.get("x-real-ip") @@ -612,6 +625,9 @@ async function createInstallationToken(repository: string, env: Env): Promise Date: Thu, 20 Aug 2026 23:13:50 -0700 Subject: [PATCH 054/102] test(oidc): reject non-finite NumericDate claims --- test/oidc-numeric-date-finite.test.ts | 141 ++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 test/oidc-numeric-date-finite.test.ts diff --git a/test/oidc-numeric-date-finite.test.ts b/test/oidc-numeric-date-finite.test.ts new file mode 100644 index 000000000..cfdb588c2 --- /dev/null +++ b/test/oidc-numeric-date-finite.test.ts @@ -0,0 +1,141 @@ +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import type { Env } from "../src/index"; + +const configuredWorkflowRef = + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; +const configuredWorkflowSha = "a".repeat(40); +const trustedDiscoveryUrl = + "https://token.actions.githubusercontent.com/.well-known/openid-configuration"; +const trustedJwksUrl = "https://token.actions.githubusercontent.com/.well-known/jwks"; +const signingKid = "oidc-numeric-date-finite"; + +const env: Env = { + ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", + ALLOWED_AUDIENCE: "cwl-noema-review", + ALLOWED_REPOSITORY_OWNER: "ContextualWisdomLab", + ALLOWED_WORKFLOW_REPOSITORY: "ContextualWisdomLab/.github", + ALLOWED_WORKFLOW_REF_PREFIX: configuredWorkflowRef, + ALLOWED_WORKFLOW_SHA: configuredWorkflowSha, + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: "unused-before-request-validation", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", +}; + +let signingPrivateKey: CryptoKey; +let signingPublicJwk: JsonWebKey; + +function encodeJson(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +function encodeBytes(value: Uint8Array): string { + return Buffer.from(value).toString("base64url"); +} + +async function signedRawPayloadJwt(payloadJson: string): Promise { + const encodedHeader = encodeJson({ alg: "RS256", kid: signingKid }); + const encodedPayload = Buffer.from(payloadJson).toString("base64url"); + const signature = new Uint8Array( + await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + signingPrivateKey, + new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`), + ), + ); + return `${encodedHeader}.${encodedPayload}.${encodeBytes(signature)}`; +} + +function rawClaimsWithNumericDate( + field: "exp" | "nbf", + rawNumericDate: string, + now = Math.floor(Date.now() / 1000), +): string { + const claims: Record = { + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository: "ContextualWisdomLab/.github", + job_workflow_ref: configuredWorkflowRef, + job_workflow_sha: configuredWorkflowSha, + sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }; + const finiteValue = claims[field]; + const encoded = JSON.stringify(claims); + return encoded.replace(`"${field}":${finiteValue}`, `"${field}":${rawNumericDate}`); +} + +async function exchange(token: string): Promise { + vi.resetModules(); + const { default: worker } = await import("../src/index"); + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === trustedDiscoveryUrl) return Response.json({ jwks_uri: trustedJwksUrl }); + if (url === trustedJwksUrl) { + return Response.json({ keys: [{ ...signingPublicJwk, kid: signingKid, kty: "RSA" }] }); + } + return new Response("unexpected privileged egress", { status: 500 }); + }); + + return worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json", + "cf-connecting-ip": "203.0.113.122", + }, + body: JSON.stringify({ + target_repository: { owner: "ContextualWisdomLab", repo: "noema" }, + }), + }), + env, + ); +} + +beforeAll(async () => { + const keyPair = (await crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + )) as CryptoKeyPair; + signingPrivateKey = keyPair.privateKey; + signingPublicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey); +}); + +afterEach(() => { + vi.restoreAllMocks(); + vi.resetModules(); +}); + +describe("OIDC NumericDate finiteness", () => { + it("rejects a signed expiration that overflows JSON numeric range instead of treating Infinity as unexpired", async () => { + const token = await signedRawPayloadJwt(rawClaimsWithNumericDate("exp", "1e400")); + const response = await exchange(token); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_INVALID", + }); + }); + + it("rejects a signed negative not-before overflow instead of treating negative Infinity as already valid", async () => { + const token = await signedRawPayloadJwt(rawClaimsWithNumericDate("nbf", "-1e400")); + const response = await exchange(token); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_INVALID", + }); + }); +}); From 9a906179775007fc9bfddeac7cf4581e90fcf637 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 23:15:46 -0700 Subject: [PATCH 055/102] fix(oidc): require finite NumericDate claims --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4512e3e36..f49b90b83 100644 --- a/src/index.ts +++ b/src/index.ts @@ -465,13 +465,13 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise { match_policy: "exact-ref-and-source-sha" }, ); } - if (payload.nbf !== undefined && typeof payload.nbf !== "number") { + if (payload.nbf !== undefined && (typeof payload.nbf !== "number" || !Number.isFinite(payload.nbf))) { throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC not-before claim is invalid"); } if (typeof payload.nbf === "number" && payload.nbf > now + 30) { throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC token is not valid yet"); } - if (typeof payload.exp !== "number" || payload.exp < now - 30) { + if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp < now - 30) { throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC token is expired"); } From 075f0b6f9ff3d1233a5d4a914a5f93853060d886 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 00:32:59 -0700 Subject: [PATCH 056/102] test(oidc): reject invalid issued-at claims --- test/oidc-numeric-date-finite.test.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/oidc-numeric-date-finite.test.ts b/test/oidc-numeric-date-finite.test.ts index cfdb588c2..c4d9d748f 100644 --- a/test/oidc-numeric-date-finite.test.ts +++ b/test/oidc-numeric-date-finite.test.ts @@ -47,7 +47,7 @@ async function signedRawPayloadJwt(payloadJson: string): Promise { } function rawClaimsWithNumericDate( - field: "exp" | "nbf", + field: "exp" | "nbf" | "iat", rawNumericDate: string, now = Math.floor(Date.now() / 1000), ): string { @@ -138,4 +138,27 @@ describe("OIDC NumericDate finiteness", () => { error_code: "ERR_AUTH_INVALID", }); }); + + it("rejects a signed issued-at value that overflows JSON numeric range", async () => { + const token = await signedRawPayloadJwt(rawClaimsWithNumericDate("iat", "1e400")); + const response = await exchange(token); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_INVALID", + }); + }); + + it("rejects a signed issued-at value that is materially in the future", async () => { + const now = Math.floor(Date.now() / 1000); + const token = await signedRawPayloadJwt(rawClaimsWithNumericDate("iat", String(now + 300), now)); + const response = await exchange(token); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_INVALID", + }); + }); }); From b90e12ec8443dde1e73512892ee800bf96dd6460 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 00:35:31 -0700 Subject: [PATCH 057/102] fix(oidc): validate issued-at claims --- src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/index.ts b/src/index.ts index f49b90b83..7400513fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -471,6 +471,12 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise if (typeof payload.nbf === "number" && payload.nbf > now + 30) { throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC token is not valid yet"); } + if (payload.iat !== undefined && (typeof payload.iat !== "number" || !Number.isFinite(payload.iat))) { + throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC issued-at claim is invalid"); + } + if (typeof payload.iat === "number" && payload.iat > now + 30) { + throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC token was issued in the future"); + } if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp < now - 30) { throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC token is expired"); } From de1388ec2fec4cc45c978a69a70c3609aaf9ab6b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 02:05:47 -0700 Subject: [PATCH 058/102] test(oidc): bind repository owner immutable id --- test/oidc-repository-owner-id-binding.test.ts | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 test/oidc-repository-owner-id-binding.test.ts diff --git a/test/oidc-repository-owner-id-binding.test.ts b/test/oidc-repository-owner-id-binding.test.ts new file mode 100644 index 000000000..c31730634 --- /dev/null +++ b/test/oidc-repository-owner-id-binding.test.ts @@ -0,0 +1,169 @@ +import { beforeAll, afterEach, describe, expect, it, vi } from "vitest"; +import worker, { type Env } from "../src/index"; + +const configuredRef = + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; +const configuredWorkflowSha = "a".repeat(40); +const expectedRepositoryOwnerId = "295022177"; + +let oidcKeyPair: CryptoKeyPair; +let oidcPublicJwk: JsonWebKey; +let appPrivateKeyPem: string; + +function encodeSegment(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +function encodeBytes(bytes: ArrayBuffer): string { + return Buffer.from(bytes).toString("base64url"); +} + +function pemFromPkcs8(pkcs8: ArrayBuffer): string { + const base64 = Buffer.from(pkcs8).toString("base64"); + const lines = base64.match(/.{1,64}/g)?.join("\n") ?? base64; + return `-----BEGIN PRIVATE KEY-----\n${lines}\n-----END PRIVATE KEY-----`; +} + +async function generateRsaKeyPair(): Promise { + return crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + ); +} + +beforeAll(async () => { + oidcKeyPair = await generateRsaKeyPair(); + oidcPublicJwk = await crypto.subtle.exportKey("jwk", oidcKeyPair.publicKey); + const appKeyPair = await generateRsaKeyPair(); + appPrivateKeyPem = pemFromPkcs8( + await crypto.subtle.exportKey("pkcs8", appKeyPair.privateKey), + ); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +async function signedOidcToken(repositoryOwnerId: string) { + const kid = `github-owner-id-${crypto.randomUUID()}`; + const now = Math.floor(Date.now() / 1000); + const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); + const payload = encodeSegment({ + iss: "https://token.actions.githubusercontent.com", + aud: "cwl-noema-review", + repository_owner: "ContextualWisdomLab", + repository_owner_id: repositoryOwnerId, + repository: "ContextualWisdomLab/noema", + job_workflow_ref: configuredRef, + job_workflow_sha: configuredWorkflowSha, + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }); + const signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + oidcKeyPair.privateKey, + new TextEncoder().encode(`${header}.${payload}`), + ); + return { + token: `${header}.${payload}.${encodeBytes(signature)}`, + jwk: { ...oidcPublicJwk, kid, kty: "RSA" }, + }; +} + +function runtimeEnv(): Env { + return { + ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", + ALLOWED_AUDIENCE: "cwl-noema-review", + ALLOWED_REPOSITORY_OWNER: "ContextualWisdomLab", + ALLOWED_WORKFLOW_REPOSITORY: "ContextualWisdomLab/.github", + ALLOWED_WORKFLOW_REF_PREFIX: configuredRef, + ALLOWED_WORKFLOW_SHA: configuredWorkflowSha, + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKeyPem, + GITHUB_APP_INSTALLATION_ID: "92345", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", + }; +} + +describe("OIDC immutable repository-owner identity", () => { + it("rejects a signed same-name owner carrying a different GitHub owner id before GitHub App egress", async () => { + const { token, jwk } = await signedOidcToken("1"); + let githubAppEgressCount = 0; + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { + return Response.json({ + jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", + }); + } + if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { + return Response.json({ keys: [jwk] }); + } + githubAppEgressCount += 1; + return new Response("unexpected GitHub App egress", { status: 500 }); + }); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json", + "cf-connecting-ip": "203.0.113.250", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + runtimeEnv(), + ); + + expect(response.status).toBe(403); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_REPO_NOT_ALLOWED", + message: "OIDC repository owner identity is not allowed", + }); + expect(githubAppEgressCount).toBe(0); + }); + + it("allows the current organization id through the owner-identity boundary", async () => { + const { token, jwk } = await signedOidcToken(expectedRepositoryOwnerId); + let githubAppEgressCount = 0; + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { + return Response.json({ + jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", + }); + } + if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { + return Response.json({ keys: [jwk] }); + } + githubAppEgressCount += 1; + return new Response("expected downstream boundary", { status: 500 }); + }); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json", + "cf-connecting-ip": "203.0.113.251", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + runtimeEnv(), + ); + + expect(response.status).not.toBe(403); + expect(githubAppEgressCount).toBe(1); + }); +}); From b7e82359df688d8ef4d87cf2b9287a6b92b2c503 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 02:08:17 -0700 Subject: [PATCH 059/102] fix(oidc): reject mismatched repository owner identity --- src/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/index.ts b/src/index.ts index 7400513fd..100871f9a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,7 @@ type JwtPayload = { aud?: string | string[]; repository?: string; repository_owner?: string; + repository_owner_id?: string; workflow_ref?: string; workflow_sha?: string; job_workflow_ref?: string; @@ -142,6 +143,7 @@ const trustedHeaderValuePattern = /^[A-Za-z0-9._:-]+$/; const clientIdentifierPattern = /^[A-Za-z0-9.:%_,-]+$/; const exactWorkflowSourceShaPattern = /^[0-9a-f]{40}$/; const githubInstallationTokenExpiryPattern = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/; +const expectedRepositoryOwnerId = "295022177"; const maxTrustedHeaderLength = 128; const maxInstallationTokenLifetimeMs = 65 * 60_000; @@ -439,6 +441,12 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud]; if (!audiences.includes(env.ALLOWED_AUDIENCE)) throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC audience is not allowed"); if (payload.repository_owner !== env.ALLOWED_REPOSITORY_OWNER) throw new ApiError("ERR_REPO_NOT_ALLOWED", 403, "OIDC repository owner is not allowed"); + if ( + payload.repository_owner_id !== undefined + && payload.repository_owner_id !== expectedRepositoryOwnerId + ) { + throw new ApiError("ERR_REPO_NOT_ALLOWED", 403, "OIDC repository owner identity is not allowed"); + } const workflowRef = payload.job_workflow_ref || payload.workflow_ref || ""; if (workflowRef !== env.ALLOWED_WORKFLOW_REF_PREFIX) { From 9988d75061b7da8b77c281af4f3eaf6589241488 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:04:59 -0700 Subject: [PATCH 060/102] test(oidc): bind repository immutable identity --- test/oidc-repository-owner-id-binding.test.ts | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/test/oidc-repository-owner-id-binding.test.ts b/test/oidc-repository-owner-id-binding.test.ts index c31730634..d4c6fe70d 100644 --- a/test/oidc-repository-owner-id-binding.test.ts +++ b/test/oidc-repository-owner-id-binding.test.ts @@ -5,6 +5,7 @@ const configuredRef = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; const configuredWorkflowSha = "a".repeat(40); const expectedRepositoryOwnerId = "295022177"; +const expectedRepositoryId = "1285107801"; let oidcKeyPair: CryptoKeyPair; let oidcPublicJwk: JsonWebKey; @@ -50,7 +51,7 @@ afterEach(() => { vi.restoreAllMocks(); }); -async function signedOidcToken(repositoryOwnerId: string) { +async function signedOidcToken(repositoryOwnerId: string, repositoryId = expectedRepositoryId) { const kid = `github-owner-id-${crypto.randomUUID()}`; const now = Math.floor(Date.now() / 1000); const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); @@ -60,6 +61,7 @@ async function signedOidcToken(repositoryOwnerId: string) { repository_owner: "ContextualWisdomLab", repository_owner_id: repositoryOwnerId, repository: "ContextualWisdomLab/noema", + repository_id: repositoryId, job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, exp: now + 300, @@ -93,7 +95,7 @@ function runtimeEnv(): Env { }; } -describe("OIDC immutable repository-owner identity", () => { +describe("OIDC immutable repository identity", () => { it("rejects a signed same-name owner carrying a different GitHub owner id before GitHub App egress", async () => { const { token, jwk } = await signedOidcToken("1"); let githubAppEgressCount = 0; @@ -133,7 +135,46 @@ describe("OIDC immutable repository-owner identity", () => { expect(githubAppEgressCount).toBe(0); }); - it("allows the current organization id through the owner-identity boundary", async () => { + it("rejects a signed same-name repository carrying a different GitHub repository id before GitHub App egress", async () => { + const { token, jwk } = await signedOidcToken(expectedRepositoryOwnerId, "1"); + let githubAppEgressCount = 0; + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { + return Response.json({ + jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", + }); + } + if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { + return Response.json({ keys: [jwk] }); + } + githubAppEgressCount += 1; + return new Response("unexpected GitHub App egress", { status: 500 }); + }); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json", + "cf-connecting-ip": "203.0.113.252", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + runtimeEnv(), + ); + + expect(response.status).toBe(403); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_REPO_NOT_ALLOWED", + message: "OIDC repository identity is not allowed", + }); + expect(githubAppEgressCount).toBe(0); + }); + + it("allows the current organization and repository ids through the immutable-identity boundary", async () => { const { token, jwk } = await signedOidcToken(expectedRepositoryOwnerId); let githubAppEgressCount = 0; vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { From b61113cfb2cf94441417272acf1b567321ee68f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:06:59 -0700 Subject: [PATCH 061/102] fix(oidc): bind repository immutable identity --- src/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/index.ts b/src/index.ts index 100871f9a..ed07d05de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,7 @@ type JwtPayload = { iss?: string; aud?: string | string[]; repository?: string; + repository_id?: string; repository_owner?: string; repository_owner_id?: string; workflow_ref?: string; @@ -144,6 +145,7 @@ const clientIdentifierPattern = /^[A-Za-z0-9.:%_,-]+$/; const exactWorkflowSourceShaPattern = /^[0-9a-f]{40}$/; const githubInstallationTokenExpiryPattern = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/; const expectedRepositoryOwnerId = "295022177"; +const expectedRepositoryId = "1285107801"; const maxTrustedHeaderLength = 128; const maxInstallationTokenLifetimeMs = 65 * 60_000; @@ -447,6 +449,12 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise ) { throw new ApiError("ERR_REPO_NOT_ALLOWED", 403, "OIDC repository owner identity is not allowed"); } + if ( + payload.repository_id !== undefined + && payload.repository_id !== expectedRepositoryId + ) { + throw new ApiError("ERR_REPO_NOT_ALLOWED", 403, "OIDC repository identity is not allowed"); + } const workflowRef = payload.job_workflow_ref || payload.workflow_ref || ""; if (workflowRef !== env.ALLOWED_WORKFLOW_REF_PREFIX) { From 77895d0e097085946d5fbfd04fd1194d80653b01 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:10:53 -0700 Subject: [PATCH 062/102] test(oidc): preserve central workflow repository identity --- test/oidc-repository-owner-id-binding.test.ts | 140 +++++++----------- 1 file changed, 55 insertions(+), 85 deletions(-) diff --git a/test/oidc-repository-owner-id-binding.test.ts b/test/oidc-repository-owner-id-binding.test.ts index d4c6fe70d..08898e44a 100644 --- a/test/oidc-repository-owner-id-binding.test.ts +++ b/test/oidc-repository-owner-id-binding.test.ts @@ -5,7 +5,8 @@ const configuredRef = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; const configuredWorkflowSha = "a".repeat(40); const expectedRepositoryOwnerId = "295022177"; -const expectedRepositoryId = "1285107801"; +const expectedNoemaRepositoryId = "1285107801"; +const expectedWorkflowRepositoryId = "1274066402"; let oidcKeyPair: CryptoKeyPair; let oidcPublicJwk: JsonWebKey; @@ -51,7 +52,11 @@ afterEach(() => { vi.restoreAllMocks(); }); -async function signedOidcToken(repositoryOwnerId: string, repositoryId = expectedRepositoryId) { +async function signedOidcToken( + repositoryOwnerId: string, + repository = "ContextualWisdomLab/noema", + repositoryId = expectedNoemaRepositoryId, +) { const kid = `github-owner-id-${crypto.randomUUID()}`; const now = Math.floor(Date.now() / 1000); const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); @@ -60,7 +65,7 @@ async function signedOidcToken(repositoryOwnerId: string, repositoryId = expecte aud: "cwl-noema-review", repository_owner: "ContextualWisdomLab", repository_owner_id: repositoryOwnerId, - repository: "ContextualWisdomLab/noema", + repository, repository_id: repositoryId, job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, @@ -95,36 +100,41 @@ function runtimeEnv(): Env { }; } +async function exerciseToken(token: string, jwk: JsonWebKey, clientIp: string) { + let githubAppEgressCount = 0; + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { + return Response.json({ + jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", + }); + } + if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { + return Response.json({ keys: [jwk] }); + } + githubAppEgressCount += 1; + return new Response("expected downstream boundary", { status: 500 }); + }); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json", + "cf-connecting-ip": clientIp, + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + runtimeEnv(), + ); + return { response, githubAppEgressCount }; +} + describe("OIDC immutable repository identity", () => { it("rejects a signed same-name owner carrying a different GitHub owner id before GitHub App egress", async () => { const { token, jwk } = await signedOidcToken("1"); - let githubAppEgressCount = 0; - vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { - const url = String(input); - if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { - return Response.json({ - jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", - }); - } - if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { - return Response.json({ keys: [jwk] }); - } - githubAppEgressCount += 1; - return new Response("unexpected GitHub App egress", { status: 500 }); - }); - - const response = await worker.fetch( - new Request("https://noema.example/exchange", { - method: "POST", - headers: { - authorization: `Bearer ${token}`, - "content-type": "application/json", - "cf-connecting-ip": "203.0.113.250", - }, - body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), - }), - runtimeEnv(), - ); + const { response, githubAppEgressCount } = await exerciseToken(token, jwk, "203.0.113.250"); expect(response.status).toBe(403); await expect(response.json()).resolves.toMatchObject({ @@ -135,35 +145,9 @@ describe("OIDC immutable repository identity", () => { expect(githubAppEgressCount).toBe(0); }); - it("rejects a signed same-name repository carrying a different GitHub repository id before GitHub App egress", async () => { - const { token, jwk } = await signedOidcToken(expectedRepositoryOwnerId, "1"); - let githubAppEgressCount = 0; - vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { - const url = String(input); - if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { - return Response.json({ - jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", - }); - } - if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { - return Response.json({ keys: [jwk] }); - } - githubAppEgressCount += 1; - return new Response("unexpected GitHub App egress", { status: 500 }); - }); - - const response = await worker.fetch( - new Request("https://noema.example/exchange", { - method: "POST", - headers: { - authorization: `Bearer ${token}`, - "content-type": "application/json", - "cf-connecting-ip": "203.0.113.252", - }, - body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), - }), - runtimeEnv(), - ); + it("rejects a signed same-name Noema repository carrying a different GitHub repository id before GitHub App egress", async () => { + const { token, jwk } = await signedOidcToken(expectedRepositoryOwnerId, "ContextualWisdomLab/noema", "1"); + const { response, githubAppEgressCount } = await exerciseToken(token, jwk, "203.0.113.252"); expect(response.status).toBe(403); await expect(response.json()).resolves.toMatchObject({ @@ -174,35 +158,21 @@ describe("OIDC immutable repository identity", () => { expect(githubAppEgressCount).toBe(0); }); - it("allows the current organization and repository ids through the immutable-identity boundary", async () => { + it("allows the current Noema organization and repository ids through the immutable-identity boundary", async () => { const { token, jwk } = await signedOidcToken(expectedRepositoryOwnerId); - let githubAppEgressCount = 0; - vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { - const url = String(input); - if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { - return Response.json({ - jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", - }); - } - if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { - return Response.json({ keys: [jwk] }); - } - githubAppEgressCount += 1; - return new Response("expected downstream boundary", { status: 500 }); - }); + const { response, githubAppEgressCount } = await exerciseToken(token, jwk, "203.0.113.251"); + + expect(response.status).not.toBe(403); + expect(githubAppEgressCount).toBe(1); + }); - const response = await worker.fetch( - new Request("https://noema.example/exchange", { - method: "POST", - headers: { - authorization: `Bearer ${token}`, - "content-type": "application/json", - "cf-connecting-ip": "203.0.113.251", - }, - body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), - }), - runtimeEnv(), + it("does not compare the central workflow repository id to Noema's repository id", async () => { + const { token, jwk } = await signedOidcToken( + expectedRepositoryOwnerId, + "ContextualWisdomLab/.github", + expectedWorkflowRepositoryId, ); + const { response, githubAppEgressCount } = await exerciseToken(token, jwk, "203.0.113.253"); expect(response.status).not.toBe(403); expect(githubAppEgressCount).toBe(1); From ea81c9efc5a5269b2adb38fae5c49a3584dd9ebc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:12:45 -0700 Subject: [PATCH 063/102] fix(oidc): scope immutable repository id to Noema claim --- src/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index ed07d05de..de5d08477 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,7 +145,8 @@ const clientIdentifierPattern = /^[A-Za-z0-9.:%_,-]+$/; const exactWorkflowSourceShaPattern = /^[0-9a-f]{40}$/; const githubInstallationTokenExpiryPattern = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/; const expectedRepositoryOwnerId = "295022177"; -const expectedRepositoryId = "1285107801"; +const expectedNoemaRepositoryName = "ContextualWisdomLab/noema"; +const expectedNoemaRepositoryId = "1285107801"; const maxTrustedHeaderLength = 128; const maxInstallationTokenLifetimeMs = 65 * 60_000; @@ -450,8 +451,9 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise throw new ApiError("ERR_REPO_NOT_ALLOWED", 403, "OIDC repository owner identity is not allowed"); } if ( - payload.repository_id !== undefined - && payload.repository_id !== expectedRepositoryId + payload.repository === expectedNoemaRepositoryName + && payload.repository_id !== undefined + && payload.repository_id !== expectedNoemaRepositoryId ) { throw new ApiError("ERR_REPO_NOT_ALLOWED", 403, "OIDC repository identity is not allowed"); } From cdbdc5f694aa9d7639af51319a689c55143db82e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:20:55 -0700 Subject: [PATCH 064/102] test(oidc): bind central workflow repository identity --- test/oidc-repository-owner-id-binding.test.ts | 71 ++++++++----------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/test/oidc-repository-owner-id-binding.test.ts b/test/oidc-repository-owner-id-binding.test.ts index 08898e44a..2d1d3c151 100644 --- a/test/oidc-repository-owner-id-binding.test.ts +++ b/test/oidc-repository-owner-id-binding.test.ts @@ -28,12 +28,7 @@ function pemFromPkcs8(pkcs8: ArrayBuffer): string { async function generateRsaKeyPair(): Promise { return crypto.subtle.generateKey( - { - name: "RSASSA-PKCS1-v1_5", - modulusLength: 2048, - publicExponent: new Uint8Array([1, 0, 1]), - hash: "SHA-256", - }, + { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, true, ["sign", "verify"], ); @@ -43,14 +38,10 @@ beforeAll(async () => { oidcKeyPair = await generateRsaKeyPair(); oidcPublicJwk = await crypto.subtle.exportKey("jwk", oidcKeyPair.publicKey); const appKeyPair = await generateRsaKeyPair(); - appPrivateKeyPem = pemFromPkcs8( - await crypto.subtle.exportKey("pkcs8", appKeyPair.privateKey), - ); + appPrivateKeyPem = pemFromPkcs8(await crypto.subtle.exportKey("pkcs8", appKeyPair.privateKey)); }); -afterEach(() => { - vi.restoreAllMocks(); -}); +afterEach(() => vi.restoreAllMocks()); async function signedOidcToken( repositoryOwnerId: string, @@ -78,10 +69,7 @@ async function signedOidcToken( oidcKeyPair.privateKey, new TextEncoder().encode(`${header}.${payload}`), ); - return { - token: `${header}.${payload}.${encodeBytes(signature)}`, - jwk: { ...oidcPublicJwk, kid, kty: "RSA" }, - }; + return { token: `${header}.${payload}.${encodeBytes(signature)}`, jwk: { ...oidcPublicJwk, kid, kty: "RSA" } }; } function runtimeEnv(): Env { @@ -105,25 +93,16 @@ async function exerciseToken(token: string, jwk: JsonWebKey, clientIp: string) { vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { const url = String(input); if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { - return Response.json({ - jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", - }); - } - if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { - return Response.json({ keys: [jwk] }); + return Response.json({ jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks" }); } + if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") return Response.json({ keys: [jwk] }); githubAppEgressCount += 1; return new Response("expected downstream boundary", { status: 500 }); }); - const response = await worker.fetch( new Request("https://noema.example/exchange", { method: "POST", - headers: { - authorization: `Bearer ${token}`, - "content-type": "application/json", - "cf-connecting-ip": clientIp, - }, + headers: { authorization: `Bearer ${token}`, "content-type": "application/json", "cf-connecting-ip": clientIp }, body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), }), runtimeEnv(), @@ -131,11 +110,26 @@ async function exerciseToken(token: string, jwk: JsonWebKey, clientIp: string) { return { response, githubAppEgressCount }; } +async function expectRepositoryIdentityRejection( + repository: string, + repositoryId: string, + clientIp: string, +) { + const { token, jwk } = await signedOidcToken(expectedRepositoryOwnerId, repository, repositoryId); + const { response, githubAppEgressCount } = await exerciseToken(token, jwk, clientIp); + expect(response.status).toBe(403); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_REPO_NOT_ALLOWED", + message: "OIDC repository identity is not allowed", + }); + expect(githubAppEgressCount).toBe(0); +} + describe("OIDC immutable repository identity", () => { it("rejects a signed same-name owner carrying a different GitHub owner id before GitHub App egress", async () => { const { token, jwk } = await signedOidcToken("1"); const { response, githubAppEgressCount } = await exerciseToken(token, jwk, "203.0.113.250"); - expect(response.status).toBe(403); await expect(response.json()).resolves.toMatchObject({ ok: false, @@ -145,35 +139,28 @@ describe("OIDC immutable repository identity", () => { expect(githubAppEgressCount).toBe(0); }); - it("rejects a signed same-name Noema repository carrying a different GitHub repository id before GitHub App egress", async () => { - const { token, jwk } = await signedOidcToken(expectedRepositoryOwnerId, "ContextualWisdomLab/noema", "1"); - const { response, githubAppEgressCount } = await exerciseToken(token, jwk, "203.0.113.252"); + it("rejects a same-name Noema repository carrying a different immutable repository id", async () => { + await expectRepositoryIdentityRejection("ContextualWisdomLab/noema", "1", "203.0.113.252"); + }); - expect(response.status).toBe(403); - await expect(response.json()).resolves.toMatchObject({ - ok: false, - error_code: "ERR_REPO_NOT_ALLOWED", - message: "OIDC repository identity is not allowed", - }); - expect(githubAppEgressCount).toBe(0); + it("rejects a same-name central workflow repository carrying a different immutable repository id", async () => { + await expectRepositoryIdentityRejection("ContextualWisdomLab/.github", "1", "203.0.113.254"); }); it("allows the current Noema organization and repository ids through the immutable-identity boundary", async () => { const { token, jwk } = await signedOidcToken(expectedRepositoryOwnerId); const { response, githubAppEgressCount } = await exerciseToken(token, jwk, "203.0.113.251"); - expect(response.status).not.toBe(403); expect(githubAppEgressCount).toBe(1); }); - it("does not compare the central workflow repository id to Noema's repository id", async () => { + it("allows the current central workflow repository id instead of comparing it to Noema's repository id", async () => { const { token, jwk } = await signedOidcToken( expectedRepositoryOwnerId, "ContextualWisdomLab/.github", expectedWorkflowRepositoryId, ); const { response, githubAppEgressCount } = await exerciseToken(token, jwk, "203.0.113.253"); - expect(response.status).not.toBe(403); expect(githubAppEgressCount).toBe(1); }); From dd619bb28d1cbc90fdb84d36bc852ee4ed1a3bb2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:23:30 -0700 Subject: [PATCH 065/102] fix(oidc): bind trusted repository identity pairs --- src/index.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index de5d08477..d5739cc70 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,8 +145,10 @@ const clientIdentifierPattern = /^[A-Za-z0-9.:%_,-]+$/; const exactWorkflowSourceShaPattern = /^[0-9a-f]{40}$/; const githubInstallationTokenExpiryPattern = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/; const expectedRepositoryOwnerId = "295022177"; -const expectedNoemaRepositoryName = "ContextualWisdomLab/noema"; -const expectedNoemaRepositoryId = "1285107801"; +const expectedRepositoryIds = new Map([ + ["ContextualWisdomLab/noema", "1285107801"], + ["ContextualWisdomLab/.github", "1274066402"], +]); const maxTrustedHeaderLength = 128; const maxInstallationTokenLifetimeMs = 65 * 60_000; @@ -450,10 +452,11 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise ) { throw new ApiError("ERR_REPO_NOT_ALLOWED", 403, "OIDC repository owner identity is not allowed"); } + const expectedRepositoryId = payload.repository ? expectedRepositoryIds.get(payload.repository) : undefined; if ( - payload.repository === expectedNoemaRepositoryName + expectedRepositoryId !== undefined && payload.repository_id !== undefined - && payload.repository_id !== expectedNoemaRepositoryId + && payload.repository_id !== expectedRepositoryId ) { throw new ApiError("ERR_REPO_NOT_ALLOWED", 403, "OIDC repository identity is not allowed"); } From ce5d32726b4978f90d8c17ea14bbb3c7f44f4a71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 12:41:46 -0700 Subject: [PATCH 066/102] test(oidc): require current central workflow source --- test/trusted-workflow-source-rollforward.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/trusted-workflow-source-rollforward.test.ts b/test/trusted-workflow-source-rollforward.test.ts index 637eb62e4..2185cbfe5 100644 --- a/test/trusted-workflow-source-rollforward.test.ts +++ b/test/trusted-workflow-source-rollforward.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; const auditedCentralWorkflowSourceSha = - "fce028b4c3bf8e2e5e4819c1c5622e90cfa6ab39"; + "0156282022134484ea9d7541d5ba0730ba14fd96"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 58a4b1381ee97fbd002f11db4172fcb7a7024b84 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 12:42:06 -0700 Subject: [PATCH 067/102] fix(oidc): trust current central workflow source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index c7a9e3490..b4d547159 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -24,7 +24,7 @@ ALLOWED_AUDIENCE = "cwl-noema-review" ALLOWED_REPOSITORY_OWNER = "ContextualWisdomLab" ALLOWED_WORKFLOW_REPOSITORY = "ContextualWisdomLab/.github" ALLOWED_WORKFLOW_REF_PREFIX = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main" -ALLOWED_WORKFLOW_SHA = "fce028b4c3bf8e2e5e4819c1c5622e90cfa6ab39" +ALLOWED_WORKFLOW_SHA = "0156282022134484ea9d7541d5ba0730ba14fd96" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 03a6cec6a1641768e3eb7fbc2bb80829fadf65e0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 13:06:35 -0700 Subject: [PATCH 068/102] test(oidc): reject bearer credential whitespace at edge --- test/oidc-bearer-whitespace-envelope.test.ts | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/oidc-bearer-whitespace-envelope.test.ts diff --git a/test/oidc-bearer-whitespace-envelope.test.ts b/test/oidc-bearer-whitespace-envelope.test.ts new file mode 100644 index 000000000..dbef51bab --- /dev/null +++ b/test/oidc-bearer-whitespace-envelope.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it, vi } from "vitest"; +import entrypoint, { isBoundedOidcBearer, type Env } from "../src/entrypoint"; + +describe("OIDC bearer envelope whitespace boundary", () => { + it("rejects embedded credential whitespace before downstream JWT parsing", async () => { + const authorization = "Bearer one.two .three"; + expect(isBoundedOidcBearer(authorization)).toBe(false); + + const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); + const response = await entrypoint.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization, + "x-request-id": "bearer-whitespace-envelope", + }, + }), + { GITHUB_API_BASE: "https://api.github.com" } as Env, + ); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + details: { policy: "bounded-oidc-jwt-envelope" }, + trace_id: "bearer-whitespace-envelope", + }); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('"event":"oidc_token_envelope"')); + expect(logSpy.mock.calls.flat().join("\n")).not.toContain(authorization); + }); +}); From 2a65dc9d256927ca29aad7939d16a935acf2fa62 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 13:07:46 -0700 Subject: [PATCH 069/102] fix(oidc): reject malformed bearer whitespace at edge --- src/entrypoint.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/entrypoint.ts b/src/entrypoint.ts index f55dbf748..dadc4abc9 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -91,16 +91,18 @@ function isCanonicalPositiveSafeInteger(value: string): boolean { /** * Accept only a compact, bounded JWT envelope before any decoding or credential use. - * Missing and non-Bearer authorization values are delegated to the normal API error path. + * Missing and non-Bearer authorization values are delegated to the normal API error path; + * a value using the Bearer scheme must itself be one exact compact JWT envelope. * @param value Authorization header value observed at the request edge, or null when absent. * @returns False only when a Bearer JWT envelope is structurally invalid or exceeds limits. */ export function isBoundedOidcBearer(value: string | null): boolean { if (value === null) return true; + if (!/^Bearer(?:\s|$)/i.test(value)) return true; + if (value.length > MAX_AUTHORIZATION_HEADER_LENGTH) return false; const match = value.match(/^Bearer\s+(\S+)$/i); - if (!match) return true; - if (value.length > MAX_AUTHORIZATION_HEADER_LENGTH) return false; + if (!match) return false; const segments = match[1].split("."); if (segments.length !== 3) return false; From 5382b9e2fa068b1a3094a63c4d190e5014d80822 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 13:37:41 -0700 Subject: [PATCH 070/102] style(core): restore canonical newline From 22747723a4dc5f2964a61e723d5ca284b0da73b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 15:02:57 -0700 Subject: [PATCH 071/102] test(oidc): require current central workflow source --- test/trusted-workflow-source-rollforward.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/trusted-workflow-source-rollforward.test.ts b/test/trusted-workflow-source-rollforward.test.ts index 2185cbfe5..c3f25da4b 100644 --- a/test/trusted-workflow-source-rollforward.test.ts +++ b/test/trusted-workflow-source-rollforward.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; const auditedCentralWorkflowSourceSha = - "0156282022134484ea9d7541d5ba0730ba14fd96"; + "4c33442021d63b09f35a874c5e7a779dd46ef8f2"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From bf3e60f314bac3d44f81000b4b74370179760f70 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 15:03:28 -0700 Subject: [PATCH 072/102] fix(oidc): roll forward trusted workflow source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index b4d547159..8c897075a 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -24,7 +24,7 @@ ALLOWED_AUDIENCE = "cwl-noema-review" ALLOWED_REPOSITORY_OWNER = "ContextualWisdomLab" ALLOWED_WORKFLOW_REPOSITORY = "ContextualWisdomLab/.github" ALLOWED_WORKFLOW_REF_PREFIX = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main" -ALLOWED_WORKFLOW_SHA = "0156282022134484ea9d7541d5ba0730ba14fd96" +ALLOWED_WORKFLOW_SHA = "4c33442021d63b09f35a874c5e7a779dd46ef8f2" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From b11b2c5e7128a0f83bfa4cac885f71a8146a5aa3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 16:05:42 -0700 Subject: [PATCH 073/102] test(oidc): require current central workflow source SHA --- test/trusted-workflow-source-rollforward.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/trusted-workflow-source-rollforward.test.ts b/test/trusted-workflow-source-rollforward.test.ts index c3f25da4b..58054d8aa 100644 --- a/test/trusted-workflow-source-rollforward.test.ts +++ b/test/trusted-workflow-source-rollforward.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; const auditedCentralWorkflowSourceSha = - "4c33442021d63b09f35a874c5e7a779dd46ef8f2"; + "da12d10130189a8b0c40fd6752b3b30da54dbc0e"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 23b4dc4c55942919a21a217de76266ae71b924a0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 16:06:01 -0700 Subject: [PATCH 074/102] fix(oidc): roll forward central workflow source SHA --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 8c897075a..0d4c43849 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -24,7 +24,7 @@ ALLOWED_AUDIENCE = "cwl-noema-review" ALLOWED_REPOSITORY_OWNER = "ContextualWisdomLab" ALLOWED_WORKFLOW_REPOSITORY = "ContextualWisdomLab/.github" ALLOWED_WORKFLOW_REF_PREFIX = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main" -ALLOWED_WORKFLOW_SHA = "4c33442021d63b09f35a874c5e7a779dd46ef8f2" +ALLOWED_WORKFLOW_SHA = "da12d10130189a8b0c40fd6752b3b30da54dbc0e" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 74341bbe4ec5944ac71d7e284ff2f19171de7ba1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 16:13:50 -0700 Subject: [PATCH 075/102] test(ci): require observable release verifier boundaries --- test/ci-exact-head-contract.test.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/test/ci-exact-head-contract.test.ts b/test/ci-exact-head-contract.test.ts index 0eb8a25f6..55ebe62b7 100644 --- a/test/ci-exact-head-contract.test.ts +++ b/test/ci-exact-head-contract.test.ts @@ -72,10 +72,32 @@ describe("pull-request verification exact-head checkout contract", () => { expect(workflow).not.toContain('test "$live_base_sha" = "$NOEMA_PR_BASE_SHA"'); }); + it("keeps each release verifier visible as its own failing CI boundary", () => { + const workflow = readWorkflow(workflowPaths[0]); + const releaseSteps = [ + ["- name: release typecheck", "run: npm run typecheck"], + ["- name: release tests", "run: npm run test"], + ["- name: release security scan", "run: npm run security:scan"], + ["- name: release KPI verification", "run: npm run kpi:verify"], + ["- name: release acquisition manifest", "run: npm run acquisition:manifest"], + ["- name: release acquisition integrity", "run: npm run acquisition:integrity"], + ] as const; + + let previousIndex = workflow.indexOf("- name: install"); + expect(previousIndex).toBeGreaterThanOrEqual(0); + for (const [stepName, command] of releaseSteps) { + const stepIndex = workflow.indexOf(stepName); + expect(stepIndex).toBeGreaterThan(previousIndex); + expect(workflow.slice(stepIndex)).toContain(command); + previousIndex = stepIndex; + } + expect(workflow).not.toContain("run: npm run release:verify"); + }); + it("binds reviewer CI to the immutable pull-request head before reviewer dependency installation", () => { expectExactHeadContract( readWorkflow(workflowPaths[1]), "- name: install (hash-pinned dependencies)", ); }); -}); \ No newline at end of file +}); From e9e11164b27c1c4427b47fd0c7180639c3171d57 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 16:14:29 -0700 Subject: [PATCH 076/102] fix(ci): expose release verifier failure boundaries --- .github/workflows/ci.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87b04c415..b5627d1c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,8 +149,23 @@ jobs: - name: install run: npm ci --legacy-peer-deps=false --install-links=false - - name: release verify - run: npm run release:verify + - name: release typecheck + run: npm run typecheck + + - name: release tests + run: npm run test + + - name: release security scan + run: npm run security:scan + + - name: release KPI verification + run: npm run kpi:verify + + - name: release acquisition manifest + run: npm run acquisition:manifest + + - name: release acquisition integrity + run: npm run acquisition:integrity - name: refuse pull-request base drift after verification if: github.event_name == 'pull_request' From 4b5ec4ee7ad2e19c58f90efe1b463f5dbf119ad9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 16:19:41 -0700 Subject: [PATCH 077/102] test(ci): align reproducibility contract with observable release gates --- test/package-manager-reproducibility.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/package-manager-reproducibility.test.ts b/test/package-manager-reproducibility.test.ts index 24d291a67..fd29ce6f1 100644 --- a/test/package-manager-reproducibility.test.ts +++ b/test/package-manager-reproducibility.test.ts @@ -192,13 +192,15 @@ describe("package-manager reproducibility contract", () => { it("binds lockfile validation to one fresh live base and refuses base movement during verification", () => { const beforeGate = ciWorkflow.indexOf("name: verify live pull-request base before lockfile control"); const lockfileGate = ciWorkflow.indexOf("name: verify lockfile change control"); - const releaseVerify = ciWorkflow.indexOf("name: release verify"); + const releaseStart = ciWorkflow.indexOf("name: release typecheck"); + const releaseEnd = ciWorkflow.indexOf("name: release acquisition integrity"); const afterGate = ciWorkflow.indexOf("name: refuse pull-request base drift after verification"); expect(beforeGate).toBeGreaterThan(-1); expect(lockfileGate).toBeGreaterThan(beforeGate); - expect(releaseVerify).toBeGreaterThan(lockfileGate); - expect(afterGate).toBeGreaterThan(releaseVerify); + expect(releaseStart).toBeGreaterThan(lockfileGate); + expect(releaseEnd).toBeGreaterThan(releaseStart); + expect(afterGate).toBeGreaterThan(releaseEnd); expect(ciWorkflow).toContain("NOEMA_PR_BASE_REF: ${{ github.event.pull_request.base.ref }}"); expect(ciWorkflow).toContain( 'git merge-base --is-ancestor "$live_base_sha" "$NOEMA_EXPECTED_HEAD_SHA"', From add4243e12fa92ec85492a53d62adf91e7af3c54 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 16:20:14 -0700 Subject: [PATCH 078/102] test(oidc): require latest central workflow source SHA --- test/trusted-workflow-source-rollforward.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/trusted-workflow-source-rollforward.test.ts b/test/trusted-workflow-source-rollforward.test.ts index 58054d8aa..e877580bf 100644 --- a/test/trusted-workflow-source-rollforward.test.ts +++ b/test/trusted-workflow-source-rollforward.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; const auditedCentralWorkflowSourceSha = - "da12d10130189a8b0c40fd6752b3b30da54dbc0e"; + "aba287b541237c688f9db516165c1e4331d0ca29"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 0f10cacc8c0412164fbc47e9281a87a48b1eeb7e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 16:20:31 -0700 Subject: [PATCH 079/102] fix(oidc): follow latest central workflow source SHA --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 0d4c43849..6840f715f 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -24,7 +24,7 @@ ALLOWED_AUDIENCE = "cwl-noema-review" ALLOWED_REPOSITORY_OWNER = "ContextualWisdomLab" ALLOWED_WORKFLOW_REPOSITORY = "ContextualWisdomLab/.github" ALLOWED_WORKFLOW_REF_PREFIX = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main" -ALLOWED_WORKFLOW_SHA = "da12d10130189a8b0c40fd6752b3b30da54dbc0e" +ALLOWED_WORKFLOW_SHA = "aba287b541237c688f9db516165c1e4331d0ca29" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 5ff6612f3f55613a9cbdd07762567940729566ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 16:24:20 -0700 Subject: [PATCH 080/102] test(ci): require bounded release-test diagnostics --- test/ci-exact-head-contract.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ci-exact-head-contract.test.ts b/test/ci-exact-head-contract.test.ts index 55ebe62b7..66312ec79 100644 --- a/test/ci-exact-head-contract.test.ts +++ b/test/ci-exact-head-contract.test.ts @@ -72,11 +72,11 @@ describe("pull-request verification exact-head checkout contract", () => { expect(workflow).not.toContain('test "$live_base_sha" = "$NOEMA_PR_BASE_SHA"'); }); - it("keeps each release verifier visible as its own failing CI boundary", () => { + it("keeps each release verifier visible as its own failing CI boundary with bounded test diagnostics", () => { const workflow = readWorkflow(workflowPaths[0]); const releaseSteps = [ ["- name: release typecheck", "run: npm run typecheck"], - ["- name: release tests", "run: npm run test"], + ["- name: release tests", "run: npm run test -- --reporter=dot"], ["- name: release security scan", "run: npm run security:scan"], ["- name: release KPI verification", "run: npm run kpi:verify"], ["- name: release acquisition manifest", "run: npm run acquisition:manifest"], From 7affdc52d9852ade8c7ce94df25cc8c691cb56f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 16:25:07 -0700 Subject: [PATCH 081/102] fix(ci): bound release-test diagnostic output --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5627d1c7..7c4287686 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,7 +153,7 @@ jobs: run: npm run typecheck - name: release tests - run: npm run test + run: npm run test -- --reporter=dot - name: release security scan run: npm run security:scan From ca9a587f37af2f4791466c758d215909074e5db2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 16:36:11 -0700 Subject: [PATCH 082/102] fix(ci): restore canonical source newline --- src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index d5739cc70..c511fd79f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -247,7 +247,6 @@ function enforceRateLimit(request: Request, env: Env, route: string) { bucket.count += 1; } - function cleanupRateLimitBuckets(now: number) { if (rateLimitBuckets.size < 10_000) return; for (const [key, bucket] of rateLimitBuckets) { From 697728cfb8f574009a3cea9f6725c6cbc4303db3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 16:38:12 -0700 Subject: [PATCH 083/102] fix(ci): preserve source formatting while restoring newline --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index c511fd79f..d5739cc70 100644 --- a/src/index.ts +++ b/src/index.ts @@ -247,6 +247,7 @@ function enforceRateLimit(request: Request, env: Env, route: string) { bucket.count += 1; } + function cleanupRateLimitBuckets(now: number) { if (rateLimitBuckets.size < 10_000) return; for (const [key, bucket] of rateLimitBuckets) { From be569dd949fe22e88d0a0aff59e0032c59ce9c0a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 23:04:54 -0700 Subject: [PATCH 084/102] test(oidc): require current central workflow source --- test/trusted-workflow-source-rollforward.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/trusted-workflow-source-rollforward.test.ts b/test/trusted-workflow-source-rollforward.test.ts index e877580bf..684b3044c 100644 --- a/test/trusted-workflow-source-rollforward.test.ts +++ b/test/trusted-workflow-source-rollforward.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; const auditedCentralWorkflowSourceSha = - "aba287b541237c688f9db516165c1e4331d0ca29"; + "634a41a4ecbbb4a6dfc4ff86b4c6273e2be01553"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From eff0b3adf0f6ebbe90ad0d061a3701d0bdc878a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 23:05:21 -0700 Subject: [PATCH 085/102] fix(oidc): roll forward central workflow source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 6840f715f..0182fa2fb 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -24,7 +24,7 @@ ALLOWED_AUDIENCE = "cwl-noema-review" ALLOWED_REPOSITORY_OWNER = "ContextualWisdomLab" ALLOWED_WORKFLOW_REPOSITORY = "ContextualWisdomLab/.github" ALLOWED_WORKFLOW_REF_PREFIX = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main" -ALLOWED_WORKFLOW_SHA = "aba287b541237c688f9db516165c1e4331d0ca29" +ALLOWED_WORKFLOW_SHA = "634a41a4ecbbb4a6dfc4ff86b4c6273e2be01553" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 3c9ede754597db5c9eb333a3cc30fe7c52245645 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:07:47 -0700 Subject: [PATCH 086/102] test(ci): require bounded failure diagnostics --- test/ci-exact-head-contract.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/ci-exact-head-contract.test.ts b/test/ci-exact-head-contract.test.ts index 66312ec79..3ceec21e7 100644 --- a/test/ci-exact-head-contract.test.ts +++ b/test/ci-exact-head-contract.test.ts @@ -76,7 +76,7 @@ describe("pull-request verification exact-head checkout contract", () => { const workflow = readWorkflow(workflowPaths[0]); const releaseSteps = [ ["- name: release typecheck", "run: npm run typecheck"], - ["- name: release tests", "run: npm run test -- --reporter=dot"], + ["- name: release tests", 'log="$RUNNER_TEMP/noema-release-tests.log"'], ["- name: release security scan", "run: npm run security:scan"], ["- name: release KPI verification", "run: npm run kpi:verify"], ["- name: release acquisition manifest", "run: npm run acquisition:manifest"], @@ -91,6 +91,9 @@ describe("pull-request verification exact-head checkout contract", () => { expect(workflow.slice(stepIndex)).toContain(command); previousIndex = stepIndex; } + expect(workflow).toContain('npm run test -- --reporter=dot >"$log" 2>&1'); + expect(workflow).toContain('tail -c 32768 "$log" | tail -n 160'); + expect(workflow).not.toContain("run: npm run test -- --reporter=dot"); expect(workflow).not.toContain("run: npm run release:verify"); }); From 4f957d297abe62a909d349f8caef26fe346c5c51 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:08:29 -0700 Subject: [PATCH 087/102] fix(ci): bound release-test failure diagnostics --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c4287686..6faad1257 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,7 +153,15 @@ jobs: run: npm run typecheck - name: release tests - run: npm run test -- --reporter=dot + shell: bash + run: | + set -euo pipefail + log="$RUNNER_TEMP/noema-release-tests.log" + if npm run test -- --reporter=dot >"$log" 2>&1; then + exit 0 + fi + tail -c 32768 "$log" | tail -n 160 + exit 1 - name: release security scan run: npm run security:scan From 054e671ce53175aa4fb140f7163d18a5a28b9f75 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:13:25 -0700 Subject: [PATCH 088/102] test(oidc): align Bearer whitespace with bounded envelope --- ...untime-workflow-prefilter-coverage.test.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/test/runtime-workflow-prefilter-coverage.test.ts b/test/runtime-workflow-prefilter-coverage.test.ts index 68704ee2a..462d2fdcd 100644 --- a/test/runtime-workflow-prefilter-coverage.test.ts +++ b/test/runtime-workflow-prefilter-coverage.test.ts @@ -63,7 +63,23 @@ describe("runtime workflow-source prefilter coverage", () => { await expectMissingAuth(); }); - it("does not treat whitespace-only bearer credentials as a source-policy JWT", async () => { - await expectMissingAuth({ authorization: "Bearer " }); + it("rejects whitespace-only Bearer credentials as a malformed bounded JWT envelope", async () => { + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + "cf-connecting-ip": "203.0.113.126", + authorization: "Bearer ", + }, + }), + env, + ); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + details: { policy: "bounded-oidc-jwt-envelope" }, + }); }); }); From bbf8fcf7df2f87d0fdd9c0683f5ced3ecab83447 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:13:51 -0700 Subject: [PATCH 089/102] test(readiness): keep immutable workflow ref and SHA coherent --- test/runtime-readiness-ref-format.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/runtime-readiness-ref-format.test.ts b/test/runtime-readiness-ref-format.test.ts index 9e2d3116a..b6a956899 100644 --- a/test/runtime-readiness-ref-format.test.ts +++ b/test/runtime-readiness-ref-format.test.ts @@ -79,10 +79,14 @@ describe("runtime-readiness exact Git ref validation", () => { const env = await readyEnvironment(); env.ALLOWED_WORKFLOW_REF_PREFIX = `ContextualWisdomLab/.github/.github/workflows/noema-review.yml@${refName}`; + if (/^[0-9a-f]{40}$/.test(refName)) { + env.ALLOWED_WORKFLOW_SHA = refName; + } const result = await evaluateRuntimeReadiness(env); expect(result.ready).toBe(true); expect(result.failedChecks).not.toContain("allowed_workflow_ref"); + expect(result.failedChecks).not.toContain("allowed_workflow_sha"); }); }); From 8e4ebc327ed77df114ab83f8b336080220e547de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:14:34 -0700 Subject: [PATCH 090/102] test(github): satisfy earlier App-id trust gate in egress fixtures --- test/github-api-egress.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/github-api-egress.test.ts b/test/github-api-egress.test.ts index 3463b4c3d..163d94083 100644 --- a/test/github-api-egress.test.ts +++ b/test/github-api-egress.test.ts @@ -129,7 +129,7 @@ describe("GitHub API egress policy", () => { method: "POST", headers: { "x-request-id": "egress-policy-test" }, }), - { GITHUB_API_BASE: rawBase } as Env, + { GITHUB_API_BASE: rawBase, GITHUB_APP_ID: "123456" } as Env, ); expect(response.status).toBe(503); @@ -159,7 +159,7 @@ describe("GitHub API egress policy", () => { method: "POST", headers: { "x-request-id": "redirect-policy-test" }, }), - { GITHUB_API_BASE: "https://api.github.com" } as Env, + { GITHUB_API_BASE: "https://api.github.com", GITHUB_APP_ID: "123456" } as Env, ); expect(response.status).toBe(503); @@ -216,7 +216,7 @@ describe("GitHub API egress policy", () => { method: "POST", headers: { "cf-connecting-ip": "203.0.113.10" }, }), - { GITHUB_API_BASE: "https://api.github.com" } as Env, + { GITHUB_API_BASE: "https://api.github.com", GITHUB_APP_ID: "123456" } as Env, ); expect(response.status).toBe(503); From 9081b5e281909497bc330fcf7263089c0558b8d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:15:21 -0700 Subject: [PATCH 091/102] test(exchange): satisfy App-id preflight in egress-boundary fixture --- test/exchange-body-limit.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/exchange-body-limit.test.ts b/test/exchange-body-limit.test.ts index 385e44f20..8b6461187 100644 --- a/test/exchange-body-limit.test.ts +++ b/test/exchange-body-limit.test.ts @@ -287,7 +287,7 @@ describe("exchange JSON body boundary", () => { }, body: '{"target_repository":"ContextualWisdomLab/noema"}', }), - { GITHUB_API_BASE: "https://example.com" } as Env, + { GITHUB_API_BASE: "https://example.com", GITHUB_APP_ID: "123456" } as Env, ); expect(response.status).toBe(503); @@ -296,4 +296,4 @@ describe("exchange JSON body boundary", () => { details: { policy: "github-cloud-exact-origin" }, }); }); -}); \ No newline at end of file +}); From 233aa7a6034bb3e2b3f31efd51cc51f158da847a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:19:38 -0700 Subject: [PATCH 092/102] test(exchange): cover synchronous cleanup failure --- test/exchange-body-cleanup-sync-throw.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 test/exchange-body-cleanup-sync-throw.test.ts diff --git a/test/exchange-body-cleanup-sync-throw.test.ts b/test/exchange-body-cleanup-sync-throw.test.ts new file mode 100644 index 000000000..ca6920801 --- /dev/null +++ b/test/exchange-body-cleanup-sync-throw.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { boundExchangeJsonBody } from "../src/entrypoint"; + +describe("exchange request-body cleanup defensive boundary", () => { + it("preserves an already-decided unsupported-media rejection if host cancellation throws synchronously", async () => { + const request = new Request("https://noema.example/exchange", { + method: "POST", + headers: { "content-type": "text/plain" }, + body: "ignored", + }); + if (request.body === null) throw new Error("expected request body"); + Object.defineProperty(request.body, "cancel", { + configurable: true, + value() { + throw new Error("synthetic host cancellation failure"); + }, + }); + + await expect(boundExchangeJsonBody(request)).resolves.toEqual({ + ok: false, + failure: { reason: "unsupported_media_type", status: 415 }, + }); + }); +}); From a23b0857e0aa7a1173ac65d21d067aa2f12fbdb0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:20:08 -0700 Subject: [PATCH 093/102] test(github): cover expiry defensive branches --- ...allation-expiry-defensive-coverage.test.ts | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 test/github-installation-expiry-defensive-coverage.test.ts diff --git a/test/github-installation-expiry-defensive-coverage.test.ts b/test/github-installation-expiry-defensive-coverage.test.ts new file mode 100644 index 000000000..6aa15ef38 --- /dev/null +++ b/test/github-installation-expiry-defensive-coverage.test.ts @@ -0,0 +1,75 @@ +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import worker, { type Env } from "../src/index"; + +const configuredRef = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; +const configuredWorkflowSha = "a".repeat(40); +let oidcKeyPair: CryptoKeyPair; +let oidcPublicJwk: JsonWebKey; +let appPrivateKeyPem: string; + +function encodeSegment(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} +function encodeBytes(bytes: ArrayBuffer): string { + return Buffer.from(bytes).toString("base64url"); +} +function pemFromPkcs8(pkcs8: ArrayBuffer): string { + const base64 = Buffer.from(pkcs8).toString("base64"); + return `-----BEGIN PRIVATE KEY-----\n${base64.match(/.{1,64}/g)?.join("\n") ?? base64}\n-----END PRIVATE KEY-----`; +} +async function generateRsaKeyPair(): Promise { + return crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, true, ["sign", "verify"]); +} + +const baseEnv: Env = { + ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", + ALLOWED_AUDIENCE: "cwl-noema-review", + ALLOWED_REPOSITORY_OWNER: "ContextualWisdomLab", + ALLOWED_WORKFLOW_REPOSITORY: "ContextualWisdomLab/.github", + ALLOWED_WORKFLOW_REF_PREFIX: configuredRef, + ALLOWED_WORKFLOW_SHA: configuredWorkflowSha, + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: "initialized-in-beforeAll", + GITHUB_APP_INSTALLATION_ID: "92345", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", +}; + +beforeAll(async () => { + oidcKeyPair = await generateRsaKeyPair(); + oidcPublicJwk = await crypto.subtle.exportKey("jwk", oidcKeyPair.publicKey); + const appKeyPair = await generateRsaKeyPair(); + appPrivateKeyPem = pemFromPkcs8(await crypto.subtle.exportKey("pkcs8", appKeyPair.privateKey)); +}); +afterEach(() => vi.restoreAllMocks()); + +async function exchangeWithTokenResponse(tokenBody: unknown, clientIp: string): Promise { + const kid = `github-expiry-${crypto.randomUUID()}`; + const now = Math.floor(Date.now() / 1000); + const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); + const payload = encodeSegment({ iss: baseEnv.ALLOWED_ISSUER, aud: baseEnv.ALLOWED_AUDIENCE, repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, repository: "ContextualWisdomLab/.github", job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, exp: now + 300, nbf: now - 30, iat: now - 30 }); + const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", oidcKeyPair.privateKey, new TextEncoder().encode(`${header}.${payload}`)); + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url.endsWith("/.well-known/openid-configuration")) return Response.json({ jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks" }); + if (url.endsWith("/.well-known/jwks")) return Response.json({ keys: [{ ...oidcPublicJwk, kid, kty: "RSA" }] }); + if (url === "https://api.github.com/app/installations/92345/access_tokens") return Response.json(tokenBody); + return new Response("unexpected", { status: 500 }); + }); + return worker.fetch(new Request("https://noema.example/exchange", { method: "POST", headers: { authorization: `Bearer ${header}.${payload}.${encodeBytes(signature)}`, "content-type": "application/json", "cf-connecting-ip": clientIp }, body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }) }), { ...baseEnv, GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKeyPem }); +} + +describe("GitHub installation expiry defensive coverage", () => { + it("rejects a non-string expires_at instead of coercing timestamp authority", async () => { + const response = await exchangeWithTokenResponse({ token: "ghs_value", expires_at: 123 }, "203.0.113.249"); + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ error_code: "ERR_GITHUB_API", message: "GitHub API returned invalid installation-token response" }); + }); + + it("fails closed if canonical timestamp serialization itself is unavailable", async () => { + vi.spyOn(Date.prototype, "toISOString").mockImplementation(() => { throw new RangeError("date serialization unavailable"); }); + const response = await exchangeWithTokenResponse({ token: "ghs_value", expires_at: "2030-01-01T00:30:00Z" }, "203.0.113.250"); + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ error_code: "ERR_GITHUB_API", message: "GitHub API returned invalid installation-token expiry" }); + }); +}); From 1552405124d42c488bcce0abb834322bfa3259b7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:30:09 -0700 Subject: [PATCH 094/102] test(exchange): cover bodyless early rejection cleanup --- test/exchange-body-early-rejection-cleanup.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/exchange-body-early-rejection-cleanup.test.ts b/test/exchange-body-early-rejection-cleanup.test.ts index 904ef1b70..d008568af 100644 --- a/test/exchange-body-early-rejection-cleanup.test.ts +++ b/test/exchange-body-early-rejection-cleanup.test.ts @@ -47,6 +47,19 @@ describe("exchange JSON body early-rejection cleanup", () => { expect(cancelObserved).toBe(true); }); + it("rejects a declared-oversized request even when the runtime exposes no body stream to cancel", async () => { + const request = new Request("https://noema.example/exchange", { + method: "POST", + headers: { + "content-type": "application/json", + "content-length": "8193", + }, + }); + + expect(request.body).toBeNull(); + await expectBoundedEarlyRejection(request, { reason: "too_large", status: 413 }); + }); + it("cancels an unsupported-media request body without awaiting cancellation", async () => { let cancelObserved = false; const request = requestWithStream( From 00ad0aaef2519f363d421f80bfed6aa1276b557c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:30:31 -0700 Subject: [PATCH 095/102] test(github): cover noncanonical token expiry format --- test/github-installation-expiry-defensive-coverage.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/github-installation-expiry-defensive-coverage.test.ts b/test/github-installation-expiry-defensive-coverage.test.ts index 6aa15ef38..19f34cc67 100644 --- a/test/github-installation-expiry-defensive-coverage.test.ts +++ b/test/github-installation-expiry-defensive-coverage.test.ts @@ -66,6 +66,13 @@ describe("GitHub installation expiry defensive coverage", () => { await expect(response.json()).resolves.toMatchObject({ error_code: "ERR_GITHUB_API", message: "GitHub API returned invalid installation-token response" }); }); + it("rejects a parseable but non-canonical offset expiry before granting credential authority", async () => { + const expiresAt = new Date(Date.now() + 30 * 60 * 1000).toISOString().replace(/Z$/, "+00:00"); + const response = await exchangeWithTokenResponse({ token: "ghs_value", expires_at: expiresAt }, "203.0.113.251"); + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ error_code: "ERR_GITHUB_API", message: "GitHub API returned invalid installation-token expiry" }); + }); + it("fails closed if canonical timestamp serialization itself is unavailable", async () => { vi.spyOn(Date.prototype, "toISOString").mockImplementation(() => { throw new RangeError("date serialization unavailable"); }); const response = await exchangeWithTokenResponse({ token: "ghs_value", expires_at: "2030-01-01T00:30:00Z" }, "203.0.113.250"); From 9a51491cdc9e9c1f4eff440836d54d9123e58a85 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:34:07 -0700 Subject: [PATCH 096/102] test(exchange): align bodyless request coverage --- test/exchange-body-early-rejection-cleanup.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/exchange-body-early-rejection-cleanup.test.ts b/test/exchange-body-early-rejection-cleanup.test.ts index d008568af..bd794a506 100644 --- a/test/exchange-body-early-rejection-cleanup.test.ts +++ b/test/exchange-body-early-rejection-cleanup.test.ts @@ -47,7 +47,7 @@ describe("exchange JSON body early-rejection cleanup", () => { expect(cancelObserved).toBe(true); }); - it("rejects a declared-oversized request even when the runtime exposes no body stream to cancel", async () => { + it("returns the original POST unchanged when the runtime exposes no body stream", async () => { const request = new Request("https://noema.example/exchange", { method: "POST", headers: { @@ -57,7 +57,7 @@ describe("exchange JSON body early-rejection cleanup", () => { }); expect(request.body).toBeNull(); - await expectBoundedEarlyRejection(request, { reason: "too_large", status: 413 }); + await expect(boundExchangeJsonBody(request)).resolves.toEqual({ ok: true, request }); }); it("cancels an unsupported-media request body without awaiting cancellation", async () => { From 9bddeccf06aea875658e8d3e1c23ff7bb60dfb55 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:37:08 -0700 Subject: [PATCH 097/102] test(oidc): require current central workflow source --- test/trusted-workflow-source-rollforward.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/trusted-workflow-source-rollforward.test.ts b/test/trusted-workflow-source-rollforward.test.ts index 684b3044c..b673c7146 100644 --- a/test/trusted-workflow-source-rollforward.test.ts +++ b/test/trusted-workflow-source-rollforward.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; const auditedCentralWorkflowSourceSha = - "634a41a4ecbbb4a6dfc4ff86b4c6273e2be01553"; + "b04a40807a71e807700bb67f2a0ea4d776f58b22"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From fecd911fc71fd7fe0517e74a1d6e25a71e9c6555 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:37:34 -0700 Subject: [PATCH 098/102] fix(oidc): trust current central workflow source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 0182fa2fb..ed0d847b0 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -24,7 +24,7 @@ ALLOWED_AUDIENCE = "cwl-noema-review" ALLOWED_REPOSITORY_OWNER = "ContextualWisdomLab" ALLOWED_WORKFLOW_REPOSITORY = "ContextualWisdomLab/.github" ALLOWED_WORKFLOW_REF_PREFIX = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main" -ALLOWED_WORKFLOW_SHA = "634a41a4ecbbb4a6dfc4ff86b4c6273e2be01553" +ALLOWED_WORKFLOW_SHA = "b04a40807a71e807700bb67f2a0ea4d776f58b22" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 4df846ecb85b0a731ee8b32c772dce2dd3eef86d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:41:03 -0700 Subject: [PATCH 099/102] test(exchange): cover disappearing body cleanup race --- ...exchange-body-early-rejection-cleanup.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/exchange-body-early-rejection-cleanup.test.ts b/test/exchange-body-early-rejection-cleanup.test.ts index bd794a506..a161af21c 100644 --- a/test/exchange-body-early-rejection-cleanup.test.ts +++ b/test/exchange-body-early-rejection-cleanup.test.ts @@ -60,6 +60,22 @@ describe("exchange JSON body early-rejection cleanup", () => { await expect(boundExchangeJsonBody(request)).resolves.toEqual({ ok: true, request }); }); + it("keeps an already-decided rejection when the runtime body becomes unavailable before cleanup", async () => { + const admittedBody = new ReadableStream(); + let bodyReads = 0; + const request = { + method: "POST", + headers: new Headers({ "content-type": "text/plain" }), + get body() { + bodyReads += 1; + return bodyReads === 1 ? admittedBody : null; + }, + } as unknown as Request; + + await expectBoundedEarlyRejection(request, { reason: "unsupported_media_type", status: 415 }); + expect(bodyReads).toBe(2); + }); + it("cancels an unsupported-media request body without awaiting cancellation", async () => { let cancelObserved = false; const request = requestWithStream( From 2cebfe2fdaa6f3ebc3110ce1b6c0460676a52699 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:43:05 -0700 Subject: [PATCH 100/102] test(exchange): cover rejected cleanup promise --- ...xchange-body-early-rejection-cleanup.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/exchange-body-early-rejection-cleanup.test.ts b/test/exchange-body-early-rejection-cleanup.test.ts index a161af21c..63ee9c394 100644 --- a/test/exchange-body-early-rejection-cleanup.test.ts +++ b/test/exchange-body-early-rejection-cleanup.test.ts @@ -47,6 +47,23 @@ describe("exchange JSON body early-rejection cleanup", () => { expect(cancelObserved).toBe(true); }); + it("keeps rejection authoritative when asynchronous body cancellation rejects", async () => { + let cancelObserved = false; + const request = requestWithStream( + new ReadableStream({ + cancel() { + cancelObserved = true; + return Promise.reject(new Error("cleanup rejected")); + }, + }), + { "content-type": "text/plain" }, + ); + + await expectBoundedEarlyRejection(request, { reason: "unsupported_media_type", status: 415 }); + await Promise.resolve(); + expect(cancelObserved).toBe(true); + }); + it("returns the original POST unchanged when the runtime exposes no body stream", async () => { const request = new Request("https://noema.example/exchange", { method: "POST", From 2a8e3ce4272fbd8165916d676daf47408eec4b06 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 01:00:09 -0700 Subject: [PATCH 101/102] test(oidc): require current central workflow source --- test/trusted-workflow-source-rollforward.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/trusted-workflow-source-rollforward.test.ts b/test/trusted-workflow-source-rollforward.test.ts index b673c7146..22ae50354 100644 --- a/test/trusted-workflow-source-rollforward.test.ts +++ b/test/trusted-workflow-source-rollforward.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; const auditedCentralWorkflowSourceSha = - "b04a40807a71e807700bb67f2a0ea4d776f58b22"; + "5a8b83773bd5190d972eec3d7c76ac9504665f21"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 31062bba5cceb6d1b718f1d47fe9fbdf30f52df6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 01:00:43 -0700 Subject: [PATCH 102/102] fix(oidc): roll forward central workflow source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index ed0d847b0..b4d9b457a 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -24,7 +24,7 @@ ALLOWED_AUDIENCE = "cwl-noema-review" ALLOWED_REPOSITORY_OWNER = "ContextualWisdomLab" ALLOWED_WORKFLOW_REPOSITORY = "ContextualWisdomLab/.github" ALLOWED_WORKFLOW_REF_PREFIX = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main" -ALLOWED_WORKFLOW_SHA = "b04a40807a71e807700bb67f2a0ea4d776f58b22" +ALLOWED_WORKFLOW_SHA = "5a8b83773bd5190d972eec3d7c76ac9504665f21" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60"