From c4622fce5cedb90386cde37dcb9112aef5d55097 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:03:19 -0700 Subject: [PATCH 001/564] test(oidc): reject non-canonical workflow source config --- ...oidc-workflow-sha-canonical-config.test.ts | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 test/oidc-workflow-sha-canonical-config.test.ts diff --git a/test/oidc-workflow-sha-canonical-config.test.ts b/test/oidc-workflow-sha-canonical-config.test.ts new file mode 100644 index 000000000..471ee562a --- /dev/null +++ b/test/oidc-workflow-sha-canonical-config.test.ts @@ -0,0 +1,155 @@ +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import type { Env } from "../src/runtime-entrypoint"; + +const configuredWorkflowRef = + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; +const configuredWorkflowSha = "a".repeat(40); +const signingKid = "oidc-workflow-sha-canonical-config"; +const trustedDiscoveryUrl = + "https://token.actions.githubusercontent.com/.well-known/openid-configuration"; +const trustedJwksUrl = "https://token.actions.githubusercontent.com/.well-known/jwks"; + +function allowingRateLimitNamespace(): DurableObjectNamespace { + return { + idFromName(name: string) { + return { toString: () => name } as DurableObjectId; + }, + get() { + return { + fetch: async () => new Response(JSON.stringify({ + allowed: true, + limit: 1000, + remaining: 999, + retry_after_seconds: 0, + }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + } as unknown as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; +} + +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: 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", + NOEMA_RATE_LIMITER: allowingRateLimitNamespace(), + NOEMA_OIDC_REPLAY_GUARD: {} as DurableObjectNamespace, +}; + +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 signedJwt(payload: Record): Promise { + const encodedHeader = encodeJson({ alg: "RS256", kid: signingKid }); + const encodedPayload = encodeJson(payload); + const signature = new Uint8Array( + await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + signingPrivateKey, + new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`), + ), + ); + return `${encodedHeader}.${encodedPayload}.${encodeBytes(signature)}`; +} + +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(); +}); + +async function exchangeWithConfiguredSha(configuredSha: string): Promise { + 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 }); + }); + + const now = Math.floor(Date.now() / 1000); + const token = await signedJwt({ + iss: baseEnv.ALLOWED_ISSUER, + aud: baseEnv.ALLOWED_AUDIENCE, + repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, + repository: "ContextualWisdomLab/.github", + sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", + exp: now + 300, + nbf: now - 30, + iat: now - 30, + job_workflow_ref: configuredWorkflowRef, + job_workflow_sha: configuredWorkflowSha, + }); + + vi.resetModules(); + const { default: worker } = await import("../src/runtime-entrypoint"); + 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.123", + }, + body: JSON.stringify({ target_repository: { owner: "ContextualWisdomLab" } }), + }), + { ...baseEnv, ALLOWED_WORKFLOW_SHA: configuredSha }, + ); +} + +describe("canonical workflow-source configuration authority", () => { + it.each([ + ` ${configuredWorkflowSha}`, + `${configuredWorkflowSha} `, + `\t${configuredWorkflowSha}`, + `${configuredWorkflowSha}\n`, + ])("rejects non-canonical ALLOWED_WORKFLOW_SHA bytes (%j)", async (configuredSha) => { + const response = await exchangeWithConfiguredSha(configuredSha); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_WORKFLOW_NOT_ALLOWED", + message: "Workflow source trust configuration unavailable", + details: { + match_policy: "exact-ref-and-source-sha", + }, + }); + }); +}); From 98ecd98f5371e227bcc135f371b0ad65238a1f53 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:05:39 -0700 Subject: [PATCH 002/564] fix(oidc): require canonical workflow source SHA bytes --- src/runtime-entrypoint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index 57cf68dd7..7e185442d 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -80,7 +80,7 @@ function workflowSourceDecision(request: Request, env: Env): WorkflowSourceDecis return { allowed: true }; } - const configuredSha = env.ALLOWED_WORKFLOW_SHA?.trim(); + const configuredSha = env.ALLOWED_WORKFLOW_SHA; if (!configuredSha || !exactCommitShaPattern.test(configuredSha)) { return { allowed: false, From 6ecb6f9501b210ab09bfac73dc55a97231f58a4f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:08:00 -0700 Subject: [PATCH 003/564] fix(oidc): preserve exact workflow source SHA authority --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index d5739cc70..ed51be54b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -468,7 +468,7 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise if (!workflowRef.startsWith(`${env.ALLOWED_WORKFLOW_REPOSITORY}/.github/workflows/`)) { throw new ApiError("ERR_WORKFLOW_NOT_ALLOWED", 403, "OIDC workflow repository is not allowed"); } - const configuredWorkflowSha = env.ALLOWED_WORKFLOW_SHA?.trim(); + const configuredWorkflowSha = env.ALLOWED_WORKFLOW_SHA; if (!configuredWorkflowSha || !exactWorkflowSourceShaPattern.test(configuredWorkflowSha)) { throw new ApiError( "ERR_WORKFLOW_NOT_ALLOWED", From d5418f6bc3426883be3d79a4a08646088d1d95ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:08:56 -0700 Subject: [PATCH 004/564] test(oidc): cover both workflow trust layers --- ...oidc-workflow-sha-canonical-config.test.ts | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/test/oidc-workflow-sha-canonical-config.test.ts b/test/oidc-workflow-sha-canonical-config.test.ts index 471ee562a..45901d9a7 100644 --- a/test/oidc-workflow-sha-canonical-config.test.ts +++ b/test/oidc-workflow-sha-canonical-config.test.ts @@ -89,7 +89,10 @@ afterEach(() => { vi.resetModules(); }); -async function exchangeWithConfiguredSha(configuredSha: string): Promise { +async function exchangeWithConfiguredSha( + configuredSha: string, + layer: "runtime" | "authoritative", +): Promise { vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { const url = String(input); if (url === trustedDiscoveryUrl) { @@ -118,7 +121,9 @@ async function exchangeWithConfiguredSha(configuredSha: string): Promise { it.each([ - ` ${configuredWorkflowSha}`, - `${configuredWorkflowSha} `, - `\t${configuredWorkflowSha}`, - `${configuredWorkflowSha}\n`, - ])("rejects non-canonical ALLOWED_WORKFLOW_SHA bytes (%j)", async (configuredSha) => { - const response = await exchangeWithConfiguredSha(configuredSha); + ["runtime", ` ${configuredWorkflowSha}`], + ["runtime", `${configuredWorkflowSha} `], + ["runtime", `\t${configuredWorkflowSha}`], + ["runtime", `${configuredWorkflowSha}\n`], + ["authoritative", ` ${configuredWorkflowSha}`], + ["authoritative", `${configuredWorkflowSha} `], + ["authoritative", `\t${configuredWorkflowSha}`], + ["authoritative", `${configuredWorkflowSha}\n`], + ] as const)( + "rejects non-canonical ALLOWED_WORKFLOW_SHA bytes at the %s layer (%j)", + async (layer, configuredSha) => { + const response = await exchangeWithConfiguredSha(configuredSha, layer); - expect(response.status).toBe(503); - await expect(response.json()).resolves.toMatchObject({ - ok: false, - error_code: "ERR_WORKFLOW_NOT_ALLOWED", - message: "Workflow source trust configuration unavailable", - details: { - match_policy: "exact-ref-and-source-sha", - }, - }); - }); + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_WORKFLOW_NOT_ALLOWED", + message: "Workflow source trust configuration unavailable", + details: { + match_policy: "exact-ref-and-source-sha", + }, + }); + }, + ); }); From a52acf8804d6f7dc342b0dfc8f5c3f2413dd8ac1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:09:51 -0700 Subject: [PATCH 005/564] test(oidc): reject non-canonical workflow ref config --- ...oidc-workflow-sha-canonical-config.test.ts | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/test/oidc-workflow-sha-canonical-config.test.ts b/test/oidc-workflow-sha-canonical-config.test.ts index 45901d9a7..e59254ca6 100644 --- a/test/oidc-workflow-sha-canonical-config.test.ts +++ b/test/oidc-workflow-sha-canonical-config.test.ts @@ -89,8 +89,8 @@ afterEach(() => { vi.resetModules(); }); -async function exchangeWithConfiguredSha( - configuredSha: string, +async function exchangeWithTrustConfig( + overrides: Partial, layer: "runtime" | "authoritative", ): Promise { vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { @@ -134,10 +134,22 @@ async function exchangeWithConfiguredSha( }, body: JSON.stringify({ target_repository: { owner: "ContextualWisdomLab" } }), }), - { ...baseEnv, ALLOWED_WORKFLOW_SHA: configuredSha }, + { ...baseEnv, ...overrides }, ); } +async function expectTrustConfigurationUnavailable(response: Response): Promise { + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_WORKFLOW_NOT_ALLOWED", + message: "Workflow source trust configuration unavailable", + details: { + match_policy: "exact-ref-and-source-sha", + }, + }); +} + describe("canonical workflow-source configuration authority", () => { it.each([ ["runtime", ` ${configuredWorkflowSha}`], @@ -151,15 +163,35 @@ describe("canonical workflow-source configuration authority", () => { ] as const)( "rejects non-canonical ALLOWED_WORKFLOW_SHA bytes at the %s layer (%j)", async (layer, configuredSha) => { - const response = await exchangeWithConfiguredSha(configuredSha, layer); + const response = await exchangeWithTrustConfig( + { ALLOWED_WORKFLOW_SHA: configuredSha }, + layer, + ); + + await expectTrustConfigurationUnavailable(response); + }, + ); + + it.each([ + ` ${configuredWorkflowRef}`, + `${configuredWorkflowRef} `, + `\t${configuredWorkflowRef}`, + `${configuredWorkflowRef}\n`, + ])( + "rejects non-canonical ALLOWED_WORKFLOW_REF_PREFIX bytes as configuration (%j)", + async (configuredRef) => { + const response = await exchangeWithTrustConfig( + { ALLOWED_WORKFLOW_REF_PREFIX: configuredRef }, + "runtime", + ); expect(response.status).toBe(503); await expect(response.json()).resolves.toMatchObject({ ok: false, error_code: "ERR_WORKFLOW_NOT_ALLOWED", - message: "Workflow source trust configuration unavailable", + message: "Workflow trust configuration unavailable", details: { - match_policy: "exact-ref-and-source-sha", + match_policy: "exact", }, }); }, From a1def2e8967eacbc73cdbdff23bd6357085170b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:10:34 -0700 Subject: [PATCH 006/564] fix(oidc): reject non-canonical workflow ref config --- src/worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/worker.ts b/src/worker.ts index 141cd3d55..395d881dc 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -86,7 +86,7 @@ function decodeOidcWorkflowClaims(request: Request): OidcWorkflowClaims | undefi } function configuredExactWorkflowRef(env: Env): string | undefined { - const candidate = env.ALLOWED_WORKFLOW_REF_PREFIX?.trim(); + const candidate = env.ALLOWED_WORKFLOW_REF_PREFIX; const repositoryPrefix = `${env.ALLOWED_WORKFLOW_REPOSITORY}/.github/workflows/`; if (!candidate || !candidate.startsWith(repositoryPrefix)) return undefined; From 844338cd057b9dd28bfa8436a3e942ed3a4e1ae7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:11:07 -0700 Subject: [PATCH 007/564] fix(oidc): preserve exact workflow ref authority --- src/runtime-entrypoint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index 7e185442d..193fb0129 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -74,7 +74,7 @@ function workflowSourceDecision(request: Request, env: Env): WorkflowSourceDecis : undefined; if (!workflowRef) return { allowed: true }; - const configuredRef = env.ALLOWED_WORKFLOW_REF_PREFIX?.trim(); + const configuredRef = env.ALLOWED_WORKFLOW_REF_PREFIX; if (!configuredRef || workflowRef !== configuredRef) { // The delegated hardened worker owns the exact workflow-ref error contract. return { allowed: true }; From 06d17bb3a056b18b3c29a68d8eef42d9459c327d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:16:36 -0700 Subject: [PATCH 008/564] docs(architecture): reconcile protected OIDC trust truth --- ARCHITECTURE.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e9feb9c6d..1fb72e657 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,6 +1,6 @@ # Noema Architecture & Trust Boundaries -**Status: Canonical documentation on protected `main`; this branch records Active PR #426 candidate behavior separately.** Protected source and live GitHub governance remain implementation authority. This document describes protected behavior unless explicitly marked **Active PR**, **Planned**, or **External evidence**. +**Status: Canonical documentation on protected `main`; this branch records Active PR #500 candidate behavior separately.** Protected source and live GitHub governance remain implementation authority. This document describes protected behavior unless explicitly marked **Active PR**, **Planned**, or **External evidence**. Noema is a bounded credential-exchange and automation service. Its core rule is: **verify GitHub Actions OIDC identity, mint a repository-scoped GitHub App installation token, and keep model judgement, review evidence, merge authority, release authority, and deployment authority separate.** @@ -26,9 +26,11 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi ## 2. Current workflow trust contract -Protected `main` exposes `ALLOWED_WORKFLOW_REF_PREFIX`. Despite the legacy name, `src/worker.ts` parses it as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. +Protected `main` exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, or mismatched configured source SHA fails closed. -**Active PR #426:** the candidate runtime adds `ALLOWED_WORKFLOW_SHA` as a required readiness binding and pairs it with GitHub OIDC `job_workflow_sha` or fallback `workflow_sha`. `src/runtime-entrypoint.ts` provides an early denial-only check for the configured exact workflow identity, while `src/index.ts` independently enforces the same exact ref/repository plus immutable source SHA after cryptographic verification. A missing, malformed, or mismatched configured source SHA fails closed. The candidate `wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to a specific central `.github` commit; that candidate configuration is not deployed truth until the PR integrates and deployment evidence proves the binding was rolled forward. +Protected `wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `9f8f84074d8a8bc142eafea12c5b9e1c8570ccd6`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. + +**Active PR #500:** candidate hardening additionally treats the configured workflow ref and source SHA as canonical operator bytes. The runtime prefilter, protected workflow-ref parser, and authoritative verifier no longer trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. The source-SHA prefilter is not an authorization substitute for cryptographic verification. Tokens not rejected at the wrapper continue through the existing distributed rate-limit, exact-ref trust, signature, issuer, audience, repository, time-window, replay, and GitHub App boundaries. @@ -40,7 +42,7 @@ flowchart LR B --> C{route} C -->|/health| H[Liveness] C -->|/ready| R[Readiness] - C -->|/exchange| S{Active PR #426\nexact ref + source SHA prefilter} + C -->|/exchange| S{exact ref + source SHA prefilter} S --> E[src/entrypoint.ts] E --> L[NoemaRateLimiter] L --> W[src/worker.ts\nexact workflow ref] @@ -90,7 +92,7 @@ Repository automation must: 6. reject stale/predecessor evidence as current success; 7. avoid self-modifying repair workflows and never weaken gates to manufacture green evidence. -These control-plane invariants are separate from the runtime OIDC trust contract. Active PR #426 strengthens that runtime contract with immutable workflow-source identity but does not alter the evidence-authority rules above. +These control-plane invariants are separate from the runtime OIDC trust contract. Protected `main` already binds immutable workflow-source identity; Active PR #500 strengthens only the canonical configuration-byte boundary and does not alter the evidence-authority rules above. ## 7. Credential and network boundaries From 90b80e0fe629caf4294b8b367858c2f977f7876d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:17:54 -0700 Subject: [PATCH 009/564] docs(architecture): make trust status revision-aware --- ARCHITECTURE.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1fb72e657..955a00c48 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,6 +1,6 @@ # Noema Architecture & Trust Boundaries -**Status: Canonical documentation on protected `main`; this branch records Active PR #500 candidate behavior separately.** Protected source and live GitHub governance remain implementation authority. This document describes protected behavior unless explicitly marked **Active PR**, **Planned**, or **External evidence**. +**Status: Code-current canonical architecture for the repository revision that contains it.** Protected source and live GitHub governance remain implementation authority. On protected `main`, this document is protected truth; on an active PR head, behavior that differs from its live protected base remains candidate truth until that revision integrates. **Planned** and **External evidence** claims are labeled explicitly. Noema is a bounded credential-exchange and automation service. Its core rule is: **verify GitHub Actions OIDC identity, mint a repository-scoped GitHub App installation token, and keep model judgement, review evidence, merge authority, release authority, and deployment authority separate.** @@ -26,11 +26,11 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi ## 2. Current workflow trust contract -Protected `main` exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, or mismatched configured source SHA fails closed. +This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -Protected `wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `9f8f84074d8a8bc142eafea12c5b9e1c8570ccd6`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `9f8f84074d8a8bc142eafea12c5b9e1c8570ccd6`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. -**Active PR #500:** candidate hardening additionally treats the configured workflow ref and source SHA as canonical operator bytes. The runtime prefilter, protected workflow-ref parser, and authoritative verifier no longer trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. +The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. The source-SHA prefilter is not an authorization substitute for cryptographic verification. Tokens not rejected at the wrapper continue through the existing distributed rate-limit, exact-ref trust, signature, issuer, audience, repository, time-window, replay, and GitHub App boundaries. @@ -92,7 +92,7 @@ Repository automation must: 6. reject stale/predecessor evidence as current success; 7. avoid self-modifying repair workflows and never weaken gates to manufacture green evidence. -These control-plane invariants are separate from the runtime OIDC trust contract. Protected `main` already binds immutable workflow-source identity; Active PR #500 strengthens only the canonical configuration-byte boundary and does not alter the evidence-authority rules above. +These control-plane invariants are separate from the runtime OIDC trust contract. Immutable workflow-source identity is already protected-base truth at this revision's branch point; the active source delta represented by this file strengthens only canonical configuration-byte handling until that delta integrates. ## 7. Credential and network boundaries From dc8dbbe97a051afac439435372d6df0ed2a8cb80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:18:19 -0700 Subject: [PATCH 010/564] test(architecture): reject stale workflow trust status --- test/architecture-documentation.test.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/architecture-documentation.test.ts b/test/architecture-documentation.test.ts index 1129a4f4a..22de84624 100644 --- a/test/architecture-documentation.test.ts +++ b/test/architecture-documentation.test.ts @@ -29,7 +29,7 @@ describe("authoritative architecture documentation", () => { } }); - it("binds the candidate architecture description to the deployed Wrangler entrypoint, state classes, and immutable workflow-source configuration", () => { + it("binds the code-current architecture to Wrangler state classes and immutable workflow-source configuration", () => { const wrangler = readFileSync("wrangler.toml", "utf8"); const architecture = readFileSync("ARCHITECTURE.md", "utf8"); @@ -38,13 +38,14 @@ describe("authoritative architecture documentation", () => { expect(wrangler).toContain('class_name = "NoemaRateLimiter"'); expect(wrangler).toContain('name = "NOEMA_OIDC_REPLAY_GUARD"'); expect(wrangler).toContain('class_name = "NoemaOidcReplayGuard"'); - expect(wrangler).toMatch(/ALLOWED_WORKFLOW_SHA = "[0-9a-f]{40}"/); - expect(architecture).toContain("NOEMA_RATE_LIMITER"); - expect(architecture).toContain("NOEMA_OIDC_REPLAY_GUARD"); - expect(architecture).toContain("Active PR #426"); + const workflowSha = wrangler.match(/ALLOWED_WORKFLOW_SHA = "([0-9a-f]{40})"/)?.[1]; + expect(workflowSha).toBeDefined(); + expect(architecture).toContain("Code-current canonical architecture"); expect(architecture).toContain("`ALLOWED_WORKFLOW_SHA`"); - expect(architecture).toContain("not deployed truth until the PR integrates"); + expect(architecture).toContain(workflowSha!); + expect(architecture).not.toContain("Active PR #426"); + expect(architecture).not.toContain("not deployed truth until the PR integrates"); }); it("keeps route claims anchored to their actual implementation layers", () => { @@ -68,7 +69,7 @@ describe("authoritative architecture documentation", () => { expect(coreWorker).toContain('"Endpoint not found"'); }); - it("documents exact-ref workflow trust plus the candidate immutable workflow-source binding without moving that check into the worker wrapper", () => { + it("documents exact-ref workflow trust plus immutable workflow-source binding without moving the SHA check into the worker wrapper", () => { const architecture = readFileSync("ARCHITECTURE.md", "utf8"); const runtimeEntrypoint = readFileSync("src/runtime-entrypoint.ts", "utf8"); const worker = readFileSync("src/worker.ts", "utf8"); @@ -84,7 +85,7 @@ describe("authoritative architecture documentation", () => { expect(coreWorker).toContain("workflow_sha"); expect(architecture).toContain("exact full workflow ref"); expect(architecture).toContain("immutable workflow-source SHA"); - expect(architecture).toContain("Active PR #426"); + expect(architecture).toContain("operator authority bytes"); }); it("keeps the canonical documentation audit aligned with integrated buyer/operator documentation", () => { From 9c523b6c3d758ca6ad1844dc004bd7eb8f1fcbdb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:20:07 -0700 Subject: [PATCH 011/564] docs(traceability): remove integrated PRs from current authority --- docs/TRACEABILITY.md | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index cb57689e3..c54be2635 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -4,7 +4,7 @@ This document maps requirements and architecture decisions to executable Noema surfaces and to the evidence that can legitimately prove them. File presence, PR prose, model output, queued checks, or predecessor results are never promoted into implementation, approval, merge, release, deployment, or acquisition authority. -Current protected-main reference for this refresh: `bcef225f1cf1a640a78a7c5b55b662cc5deb8ef4`. +Protected-main branch-point reference for this refresh: `270b66e592330c4f1c7d3b726779b1a6c599c70c`. This is a snapshot anchor, not evergreen current authority; live protected `main` must be refetched before any merge, release, deployment, or acquisition claim. Noema's execution rule is: @@ -42,7 +42,7 @@ Each arrow is a separate authority. Success at an earlier stage cannot fabricate | Requirement family | Canonical decision / boundary | Protected or active implementation surface | Executable proof | Residual evidence | Maturity | | --- | --- | --- | --- | --- | --- | | Credential exchange and readiness | Architecture, runtime threat model | `src/index.ts`, runtime entrypoints, OIDC/replay/rate-limit modules | runtime/API/security tests and exact configured coverage | deployed protected-main smoke where applicable | Implemented on protected main; operational evidence remains separate | -| Workflow/repository authority | Runtime threat model and Worker trust contract | protected exact workflow-ref and repository-owner validation plus cryptographic OIDC verification; Active PR #426 adds immutable `ALLOWED_WORKFLOW_SHA` binding to `job_workflow_sha` / `workflow_sha` | issuer/audience/repository/ref hostile-token tests plus #426 source-SHA mismatch/missing/configuration regressions | exact-head #426 CI/security evidence, current central workflow identity, protected deployment binding evidence | Exact-ref family implemented on protected main; immutable source-SHA binding Implemented on active PR / In review | +| Workflow/repository authority | Runtime threat model and Worker trust contract | exact workflow-ref, repository identity and cryptographic OIDC verification plus immutable configured `ALLOWED_WORKFLOW_SHA` binding to `job_workflow_sha` / `workflow_sha`; the current revision also treats configured workflow-ref/SHA values as canonical operator bytes | issuer/audience/repository/ref hostile-token tests, source-SHA mismatch/missing/configuration regressions, signed non-canonical-config regressions | current central workflow identity, exact-head CI/security evidence for any active delta, protected deployment-binding evidence | Immutable source binding is implemented on protected main; additional revision-local hardening becomes protected only when that revision integrates | | Fail-closed outbound GitHub boundary | Architecture + security docs | outbound fetch/request/response validation | origin/redirect/timeout/body/schema tests | production telemetry/incident evidence | Implemented family | | Delegated GitHub credential capability | AGENTS secret policy + closed issue #111 | `scripts/lib/delegated-github-token.mjs`, maintainer/reviewer workflow ingress | token-capability and workflow-ingress tests covering `NOEMA_MAINTAINER_TOKEN_PATH`, owner-only `0600`, symlink/race/size/content rejection, minimal child env | live App installation/key-custody/rotation/permission evidence under #29/#227 | Capability-file policy alignment is protected; external identity evidence remains separate | | Distributed rate/replay state | Architecture data boundary | Durable Object rate/replay state | concurrency/alarm/replay tests | deployed binding/storage evidence | Implemented family | @@ -50,33 +50,35 @@ Each arrow is a separate authority. Success at an earlier stage cannot fabricate | Evidence channel separation | ADR-0001 | checks/statuses/reviews/scanners/readiness scripts | collision/stale/predecessor/synthetic evidence tests | current GitHub evidence | Implemented family | | Safe repository writes | ADR-0004/0008 | bounded conditional ref/blob/PR operations | stale/ref/lease/cleanup tests | concurrent-writer exercise | Implemented/proposed depending on surface | | Work-conserving continuation | ADR-0002/0009 | scheduler contract and repository-owned execution policy | continuation/remediation contracts | actual multi-lane run evidence | Process contract; external scheduler state remains separate | -| Canonical documentation graph | protected main | PRD/TRD/Architecture/ADRs/UML/ERD/Test Strategy/Operability/Traceability | documentation architecture/fitness contracts | protected-main operational evidence remains separate by family | Implemented on protected main | -| Main governance current truth | ADR-0011 + issue #27 | `scripts/main-governance-audit.mjs`, `scripts/lib/main-governance-audit.mjs` on protected main | target-policy failures + observed-workflow evidence tests | actual live ruleset | Implemented on protected main; target governance remains weaker than desired | +| Canonical documentation graph | this repository revision | PRD/TRD/Architecture/ADRs/UML/ERD/Test Strategy/Operability/Traceability | documentation architecture/fitness contracts | protected-main operational evidence remains separate by family | Code-current by revision; protected authority depends on whether the revision is integrated | +| Main governance current truth | ADR-0011 + issue #27 | `scripts/main-governance-audit.mjs`, `scripts/lib/main-governance-audit.mjs` | target-policy failures + observed-workflow evidence tests | actual live ruleset | Implementation exists; target governance remains an external/live authority | | Machine-readable HTTP API | protected API contract | `openapi.json` | OpenAPI/documentation contract tests plus runtime route tests | deployed endpoint compatibility evidence | Implemented on protected main | | Credential/security coverage truth | protected main | protected `src/index.ts`, `docs/TEST_STRATEGY.md` and coverage contracts | exact configured 100% statement/branch/function/line gates; no broad credential/security V8-ignore contract | current protected-main CI remains observation-scoped | Implemented on protected main | -| Patch-validator image supply chain | issue #66 / PR #407 | `Dockerfile.patch-validator`, image workflow and validator evidence | exact build/runtime/smoke/SBOM/vulnerability/receipt/final-head verification | terminal exact-head image workflow; later publication/signing/activation evidence | In review on #407 | +| Patch-validator image supply chain | issue #66 + protected implementation | `Dockerfile.patch-validator`, image workflow, validator runtime/profile, SBOM/scanner/receipt validators | exact build/runtime/smoke/SBOM/vulnerability/receipt/final-head verification | protected-main operational receipt and later publication/signing/activation evidence | Source/runtime/supply-chain implementation is integrated on protected main; later operational/publication authority remains separate | | Licensing/IP authority | licensing/IP contract | rights/evidence validators | duplicate-key/UTF-8/exact-artifact and rights-metadata tests | owner/legal grant and transfer evidence | Technical controls exist; legal authority external | | Release/acquisition readiness | release/provenance/acquisition contracts | release verification and evidence scripts | exact-source package/SBOM/provenance/readiness tests | immutable release/deployment/customer/revenue/legal evidence | Incomplete; no readiness claim from docs alone | ## 3. Live governance traceability -During this refresh, the active Noema ruleset is organization-owned ruleset `18794436`, `CWL Noema central security scan`. It requires `.github/workflows/security-scan.yml` from the central repository on the default branch and has no bypass actor in the observed rule detail. +The repository has previously observed organization-owned ruleset `18794436`, `CWL Noema central security scan`, requiring the central `.github/workflows/security-scan.yml` on the default branch. That observation is not evergreen authority: live ruleset state and the central workflow revision must be refetched before merge classification. -This observation proves only that required-workflow control. It does **not** prove the stronger target policy for pull-request requirements, independent approvals, stale-review dismissal, review-thread resolution, required named statuses, strict latest-base checks, non-fast-forward protection, or deletion protection. +Even when the required workflow is observed, it proves only that required-workflow control. It does **not** prove stronger target policy for pull-request requirements, independent approvals, stale-review dismissal, review-thread resolution, required named statuses, strict latest-base checks, non-fast-forward protection, or deletion protection. -Issue #27 owns the desired governance closure. Protected main preserves the required-workflow identity under `observed_controls.required_workflows` while missing target controls remain FAIL. That merged implementation does not turn stronger desired governance into observed authority. +Issue #27 owns desired governance closure. Repository source can encode audit logic and historical observations, but it cannot promote desired governance into live authority. ## 4. Current open-owner map -Historical PR numbers are deliberately omitted unless they are still open and materially relevant. +Historical or integrated PR numbers are deliberately omitted from current ownership. Active PR identity belongs to live GitHub state and must be refetched rather than frozen into canonical prose. | Workstream | Current owner | Evidence boundary | | --- | --- | --- | -| Immutable OIDC workflow-source binding | PR #426 | Active candidate only; exact-head application/reviewer/Security evidence and later protected deployment configuration must prove the source-SHA binding before it becomes protected/deployed truth. | -| Patch-validator image verification | issue #66 / PR #407 | Current image owner; standard and dedicated image evidence must pass on one unchanged exact head before integration. | -| Historical validator-image stack | PR #67 | Stale predecessor retained only until #407 integration and unique-delta preservation/supersession are proven. | +| Main governance closure | issue #27 | Live ruleset / repository governance evidence; source audit logic is not the policy itself. | +| External Maintainer/Reviewer App identity | issues #29 / #227 | Installation, key custody/rotation, permissions, reviewer eligibility, and publication identity require current external evidence. | +| Patch-validator operational/publication proof | issue #66 | Source/image verification is integrated; protected-main operational receipt and later publication/signing/attestation/activation remain distinct authorities. | +| Authentic production KPI evidence | issue #3 | Requires real production-window data; repository fixtures or synthetic evidence cannot satisfy it. | +| Acquisition coordination | issue #5 | Coordinates evidence families without promoting earlier evidence into buyer/legal/commercial authority. | -Canonical architecture/documentation is protected-main truth and is no longer owned by a still-open documentation PR. A future update must refetch open PRs/issues before changing this table. Transient queue/green states belong to observation-scoped evidence, not timeless architecture claims. +Canonical architecture/documentation is code-current by revision and is not owned by a historical documentation PR. Transient queue/green states belong to observation-scoped evidence, not timeless architecture claims. ## 5. Coverage truth traceability — issue #84 @@ -90,7 +92,7 @@ owned credential/security production code → broad V8-ignore introduction = regression ``` -The bounded coverage/security slices that removed the broad exclusions are historical implementation lineage. Their predecessor checks do not become current evidence after source changes. Canonical protected-main documentation now records the surviving invariant rather than retaining obsolete active-PR ownership. +The bounded coverage/security slices that removed the broad exclusions are historical implementation lineage. Their predecessor checks do not become current evidence after source changes. Canonical documentation records the surviving invariant rather than retaining obsolete active-PR ownership. ## 6. Delegated credential capability traceability — closed issue #111 @@ -98,13 +100,13 @@ Protected maintenance workflows mint short-lived GitHub App credentials late, th The executable contract is covered by `test/github-credential-capability-ingress.test.ts`, `test/hourly-commercial-readiness-credential-ingress.test.ts`, `test/maintainer-app-token-capability.test.ts`, `test/actions-runner-assignment-token-capability.test.ts`, and `test/production-environment-governance-token-capability.test.ts`. Script credential sources do not inherit ambient parent-process secrets. External App installation, key custody, rotation, and live repository permission evidence remain separate operational authority. -**Issue #111 is closed.** Protected #421 explicitly reconciled `AGENTS.md` with the already-shipped narrow bootstrap contract: a pinned GitHub App token action may use one short-lived installation token as bootstrap transport into a fresh owner-only capability file, after which the secret environment value is unset and runtime scripts receive only the capability path. This does not authorize long-lived provider keys, App private keys, PATs, model credentials, or arbitrary environment-secret reads. Remaining live identity/configuration evidence belongs to #29 and #227 and must not be inferred from source. +**Issue #111 is closed.** Protected #421 reconciled `AGENTS.md` with the already-shipped narrow bootstrap contract: a pinned GitHub App token action may use one short-lived installation token as bootstrap transport into a fresh owner-only capability file, after which the secret environment value is unset and runtime scripts receive only the capability path. This does not authorize long-lived provider keys, App private keys, PATs, model credentials, or arbitrary environment-secret reads. Remaining live identity/configuration evidence belongs to #29 and #227 and must not be inferred from source. ## 7. Patch-validator image traceability ```text -protected main -→ current #407 exact head +protected source containing patch-validator implementation +→ exact protected/source revision → exact checkout and live-head refusal → static Node image build → runtime identity / no-network non-root smoke @@ -112,11 +114,11 @@ protected main → exact image/source/receipt binding → final live-head refusal → terminal dedicated image workflow -→ protected integration +→ protected-main operational receipt → separate registry publication/signing/attestation/activation evidence ``` -Standard CI/reviewer/Security evidence cannot skip the dedicated image-verification stages. PR #67's old checks and review state are predecessor evidence only. +The patch-validator image/runtime/supply-chain source family is integrated. Standard CI/reviewer/Security evidence still cannot substitute for the dedicated image-verification stages on a revision that changes that family, and integrated source does not fabricate later registry/publication or operational evidence. ## 8. Documentation maturity rules From ba190160e100dc34f1e3459f94142332c2869d4c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:20:53 -0700 Subject: [PATCH 012/564] docs(audit): reconcile integrated trust and validator work --- docs/DOCUMENTATION_GAP_AUDIT.md | 62 ++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/docs/DOCUMENTATION_GAP_AUDIT.md b/docs/DOCUMENTATION_GAP_AUDIT.md index 5e1428dc9..39c22abfa 100644 --- a/docs/DOCUMENTATION_GAP_AUDIT.md +++ b/docs/DOCUMENTATION_GAP_AUDIT.md @@ -1,46 +1,50 @@ # Noema Documentation Gap Audit -- **Audit date:** 2026-08-18 -- **Protected `main` observed:** `1ac1ccb7829a87f3e13c531db0765a7ed1e00002`. -- **Canonical documentation status:** integrated on protected main. -- **Audit state:** Protected-main documentation truth; operational and external evidence remain separately revalidated. +- **Audit refresh:** 2026-08-24. +- **Protected `main` branch-point observed:** `270b66e592330c4f1c7d3b726779b1a6c599c70c`; this is a snapshot anchor, not evergreen authority. +- **Canonical documentation status:** code-current for the repository revision containing this file; protected authority depends on whether that revision is on protected `main`. +- **Audit state:** design/documentation truth is separated from operational, governance, deployment, buyer, and legal evidence, all of which require their own current authority. ## Baseline verdict ### Design sufficiency -**DESIGN_SUFFICIENT: PASS / protected.** The canonical graph covers product requirements, technical requirements, architecture, ADRs, UML, conceptual/logical data model, API/security boundaries, test strategy, operability, release/provenance, licensing/IP transfer, and requirement-to-evidence traceability. Additional parallel architecture documents are not required merely to increase document count. +**DESIGN_SUFFICIENT: PASS / code-current.** The canonical graph covers product requirements, technical requirements, architecture, ADRs, UML, conceptual/logical data model, API/security boundaries, test strategy, operability, release/provenance, licensing/IP transfer, and requirement-to-evidence traceability. Additional parallel architecture documents are not required merely to increase document count. ### Protected-main operational sufficiency **PROTECTED_MAIN_OPERATIONALLY_SUFFICIENT: FAIL CLOSED / incomplete.** Documentation completeness does not prove live governance, reviewer eligibility, App provisioning, deployment, release, customer/revenue evidence, legal transfer authority, or external App installation/rotation/permission evidence. Those claims require their own current evidence. -## Current protected-source truth +## Current protected-source truth at the branch point -Protected `main` contains bounded source and documentation repairs that older versions of this audit described as active work: +The observed protected branch point contains bounded source and documentation repairs that older versions of this audit described as active work: - issue #84's credential-exchange coverage defect is repaired and closed: the security-critical exchange path no longer relies on broad V8-ignore regions, and exact configured statement/branch/function/line coverage remains the acceptance target; - `openapi.json` is a protected **OpenAPI 3.1** machine-readable HTTP contract; - the LLM-facing contract uses the `contextual-orchestrator` gateway and does not restore retired direct/sequential model candidates; - customer-facing root README plus contributor/agent procedure relocation are protected-main truth; - readiness/operator documentation, including the public `/ready` contract and acquisition-surveillance semantics, is protected-main truth; -- the canonical PRD/TRD/Architecture/ADR/UML/ERD/Test Strategy/Operability/licensing/traceability graph is protected-main truth rather than an active documentation PR; +- immutable GitHub Actions workflow-source SHA trust is protected source truth and `wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to a reviewed central `.github` commit; the central repository remains read-only from the Noema writer; +- the patch-validator image/runtime/supply-chain implementation is integrated on protected main; historical predecessor PR state is not current ownership; - issue #111 is closed: protected #421 reconciles the short-lived GitHub App bootstrap environment with the owner-only capability-file policy; live Maintainer/Reviewer App identity, installation, rotation, and permission evidence remains external under #29/#227; -- historical dependency, workflow-trust and validator-image predecessor PRs must not be treated as current solely because old PR numbers remain in history; - current repository governance evidence must be read from live GitHub policy, not from stale prose. -The current live Noema ruleset observed during the most recent authorized refresh is organization-owned ruleset `18794436` (`CWL Noema central security scan`). It applies to the default branch and requires `.github/workflows/security-scan.yml@refs/heads/main` from the central repository. This is **observed current control evidence**, not proof that stronger desired pull-request/review/status/non-fast-forward/deletion policy is live. Issue #27 remains the target-governance owner. +The active revision containing this audit additionally hardens workflow trust configuration so operator-provided workflow-ref/SHA authority is not whitespace-normalized before validation. If this file is read on an unmerged PR head, that delta is candidate truth; if read on protected `main` after integration, it is protected truth. + +A previously observed organization-owned ruleset `18794436` (`CWL Noema central security scan`) required the central Security Scan workflow on the default branch. Historical observation is not evergreen authority. Live ruleset state and the current central workflow revision must be refetched before merge classification. Issue #27 remains the target-governance owner. ## Current open work ownership -Only the following current work families are material to this audit. This table is intentionally small so closed work is not promoted back into current ownership. +Only durable issue-family owners are kept here so integrated or superseded PR numbers do not become false current authority. | Workstream | Current owner | Current boundary | | --- | --- | --- | -| Canonical architecture/documentation | protected main | PRD/TRD/Architecture/ADR/UML/ERD/Test Strategy/Operability/licensing/traceability are integrated; operational/external evidence remains separate. | -| External Maintainer/Reviewer App identity | issues #29 / #227 | Repository-owned capability-file policy is protected and issue #111 is closed; installation, key custody/rotation, permissions, reviewer eligibility, and publication identity require live external evidence. | -| Patch-validator image verification | issue #66 / PR #407 | Restacked onto current protected main. Standard and dedicated image workflows must pass on one unchanged exact head before integration. | -| Historical validator-image predecessor | PR #67 | Stale predecessor. Do not merge or close until #407 integrates and unique semantic preservation/supersession is proven. | +| Canonical architecture/documentation | current repository revision | PRD/TRD/Architecture/ADR/UML/ERD/Test Strategy/Operability/licensing/traceability are one code-current graph; protected status follows revision placement. | +| Main governance closure | issue #27 | Live ruleset/repository policy is the authority; source audit logic cannot fabricate policy. | +| External Maintainer/Reviewer App identity | issues #29 / #227 | Installation, key custody/rotation, permissions, reviewer eligibility, and publication identity require live external evidence. | +| Patch-validator operational/publication proof | issue #66 | Source/image verification implementation is integrated; protected-main operational receipts and later publication/signing/attestation/activation are separate evidence classes. | +| Authentic production KPI evidence | issue #3 | Requires real production-window evidence; fixtures or synthetic data cannot satisfy it. | +| Acquisition coordination | issue #5 | Coordinates remaining evidence families without promoting source/docs into buyer/legal authority. | Every run must refetch these identities. This table is navigation, not immutable authority. @@ -48,30 +52,30 @@ Every run must refetch these identities. This table is navigation, not immutable | Family | Canonical source | Current assessment | | --- | --- | --- | -| Product requirements | `docs/PRD.md` | Protected canonical design; implemented/planned/external maturity must remain evidence-bound. | -| Technical requirements | `docs/TRD.md` | Protected canonical design; exact-head/live-base/authority/evidence separation remains required. | -| Architecture | `ARCHITECTURE.md` | Protected canonical architecture; later implementation still needs its own evidence. | -| ADR lifecycle | `docs/adr/` | Status-bearing decisions are canonical; Accepted/Implemented labels require protected evidence. | +| Product requirements | `docs/PRD.md` | Canonical design; implemented/planned/external maturity must remain evidence-bound. | +| Technical requirements | `docs/TRD.md` | Canonical design; exact-head/live-base/authority/evidence separation remains required. | +| Architecture | `ARCHITECTURE.md` | Code-current architecture; protected authority follows revision placement. | +| ADR lifecycle | `docs/adr/` | Status-bearing decisions are canonical; Accepted/Implemented labels require appropriate evidence. | | UML | `docs/UML.md` | Component, sequence, state, authority and deployment views are present. | | Data model / ERD | `docs/ERD.md` | Conceptual/logical where Noema owns no relational evidence database; Durable Object persistence is not fabricated as SQL. | -| API/schema contracts | `openapi.json`, repository API docs and executable contracts | OpenAPI 3.1 and protected readiness/operator docs are current protected truth. | +| API/schema contracts | `openapi.json`, repository API docs and executable contracts | OpenAPI 3.1 and readiness/operator contracts are present; deployed compatibility remains separate evidence. | | Security/threat model | runtime and automation threat models | Design substantial; live operational controls remain independent evidence. | -| Test strategy | `docs/TEST_STRATEGY.md` | Issue #84's repaired coverage truth is protected; broad V8 exclusion is a regression, not an open protected-source gap. | +| Test strategy | `docs/TEST_STRATEGY.md` | Issue #84's repaired coverage invariant is protected; broad V8 exclusion is a regression, not an open protected-source gap. | | Operability/recovery | `docs/OPERABILITY.md` | Design baseline present; production/delegated-control proof remains external where applicable. | | Licensing/IP | `docs/LICENSING_AND_IP_TRANSFER.md` | Authority model present; no outbound license or legal transfer right is invented by automation. | -| Traceability | `docs/TRACEABILITY.md` | Tracks current protected source and current owner set without freezing transient queued/green states into timeless prose. | -| Root README / contributor procedure | protected-main `README.md`, `CONTRIBUTING.md`, `docs/development/` | Integrated protected truth. | -| Readiness/operator documentation | protected-main API/deployment/onboarding/acquisition docs | Integrated protected truth. | +| Traceability | `docs/TRACEABILITY.md` | Tracks current evidence classes and durable owner families without freezing integrated PRs or transient check states into timeless prose. | +| Root README / contributor procedure | `README.md`, `CONTRIBUTING.md`, `docs/development/` | Integrated source truth at the observed branch point; separate active writer ownership must be respected when present. | +| Readiness/operator documentation | API/deployment/onboarding/acquisition docs | Integrated design/source truth; operational authority remains separate. | ## Active residual gaps -### G-01 — Target governance stronger than the observed required workflow +### G-01 — Target governance stronger than previously observed required workflow -Issue #27 remains open. Current live evidence proves the central Security Scan workflow requirement but does not prove stronger target pull-request/review/status/non-fast-forward/deletion controls. Missing target controls remain FAIL; observed workflow evidence must never be promoted into authority it does not carry. +Issue #27 remains open. A required central Security Scan workflow, when live, does not prove stronger target pull-request/review/status/non-fast-forward/deletion controls. Missing target controls remain FAIL; historical workflow evidence must never be promoted into authority it does not carry. -### G-02 — Patch-validator image operational evidence +### G-02 — Patch-validator operational/publication evidence -PR #407 preserves the unique validator-image/runtime/supply-chain work on a current-main lineage. Standard gates are not sufficient to claim image readiness: the exact-head image workflow must finish build, static-runtime identity, no-network smoke, SBOM/vulnerability/inventory/receipt verification and final stale-head proof. PR #67 remains historical until that convergence is complete. +The patch-validator image/runtime/supply-chain source family is integrated on protected main. The residual gap is no longer source integration or historical PR convergence. Issue #66 owns current protected-main operational receipt and later registry publication/signing/attestation/activation evidence. Standard CI/reviewer/Security success cannot fabricate those later evidence classes. ### G-03 — External Maintainer/Reviewer App and publication identity evidence @@ -106,4 +110,4 @@ After every material product, governance, persistence, stack, release or operati 3. update this single canonical graph rather than creating a parallel architecture authority; 4. remove obsolete PR numbers and stale SHAs rather than preserving them as if still current; 5. keep transient check states out of timeless assertions unless they are explicitly observation-scoped; -6. convert any concrete source/test/API/operator defect discovered here into its executable owner lane before treating documentation work as complete. \ No newline at end of file +6. convert any concrete source/test/API/operator defect discovered here into its executable owner lane before treating documentation work as complete. From 132c4d33d1b11c5d88beb875a30b43c8717d3a2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:21:18 -0700 Subject: [PATCH 013/564] test(docs): reject integrated PRs as current owners --- ...ocumentation-architecture-contract.test.ts | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/test/documentation-architecture-contract.test.ts b/test/documentation-architecture-contract.test.ts index e8fc7dcea..9028f511b 100644 --- a/test/documentation-architecture-contract.test.ts +++ b/test/documentation-architecture-contract.test.ts @@ -34,13 +34,13 @@ describe("authoritative Noema documentation graph", () => { expect(index).toContain("[OpenAPI 3.1](../openapi.json)"); }); - it("separates protected, active, planned, and external evidence", () => { + it("separates revision-local, protected, planned, and external evidence", () => { const architecture = document("ARCHITECTURE.md"); const traceability = document("docs/TRACEABILITY.md"); const gapAudit = document("docs/DOCUMENTATION_GAP_AUDIT.md"); - expect(architecture).toContain("Canonical documentation on protected `main`"); - expect(architecture).toContain("Active PR #426"); - expect(architecture).toContain("candidate configuration is not deployed truth"); + expect(architecture).toContain("Code-current canonical architecture"); + expect(architecture).toContain("active PR head"); + expect(architecture).toContain("candidate truth until that revision integrates"); expect(traceability).toContain("Implemented on protected main"); expect(traceability).toContain("Implemented on active PR / In review"); expect(traceability).toContain("Planned"); @@ -48,7 +48,7 @@ describe("authoritative Noema documentation graph", () => { expect(gapAudit).toContain("PROTECTED_MAIN_OPERATIONALLY_SUFFICIENT: FAIL CLOSED"); }); - it("keeps evidence authorities and current owners explicit", () => { + it("keeps evidence authorities and durable current owners explicit", () => { const architecture = document("ARCHITECTURE.md"); const traceability = document("docs/TRACEABILITY.md"); const gapAudit = document("docs/DOCUMENTATION_GAP_AUDIT.md"); @@ -57,21 +57,23 @@ describe("authoritative Noema documentation graph", () => { } expect(traceability).toContain("RCA → feasibility → action → proof"); expect(traceability).toContain("A blocked lane is local"); - for (const owner of ["PR #407", "PR #67"]) { + for (const owner of ["issue #27", "issue #66", "issue #3", "issue #5"]) { expect(gapAudit).toContain(owner); } - expect(gapAudit).not.toContain("PR #71"); - expect(gapAudit).toContain("Canonical architecture/documentation | protected main"); + for (const staleOwner of ["PR #407", "PR #67", "Active PR #426"]) { + expect(gapAudit).not.toContain(staleOwner); + expect(traceability).not.toContain(staleOwner); + } }); - it("keeps protected exact-ref truth separate from active immutable-source trust", () => { + it("keeps immutable workflow-source trust separate from revision-local canonical-byte hardening", () => { const architecture = document("ARCHITECTURE.md"); const traceability = document("docs/TRACEABILITY.md"); expect(architecture).toContain("exact full workflow ref"); - expect(architecture).toContain("Active PR #426"); expect(architecture).toContain("`ALLOWED_WORKFLOW_SHA`"); - expect(traceability).toContain("Active PR #426 adds immutable `ALLOWED_WORKFLOW_SHA` binding"); - expect(traceability).toContain("immutable source-SHA binding Implemented on active PR / In review"); + expect(architecture).toContain("operator authority bytes"); + expect(traceability).toContain("Immutable source binding is implemented on protected main"); + expect(traceability).toContain("canonical operator bytes"); }); it("keeps licensing and issue-84 closure fail closed", () => { From e00f9bf00d56cb43a3c8c9d245251ed551a31f9b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:23:29 -0700 Subject: [PATCH 014/564] docs(prd): reconcile protected trust and validator truth --- docs/PRD.md | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/PRD.md b/docs/PRD.md index 3028b3462..4428f77cb 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -2,9 +2,9 @@ ## Status -**Canonical PRD — protected-main truth.** Protected source and live GitHub governance remain implementation authority. This PRD describes the current protected product boundary and marks active, external, or planned evidence explicitly. +**Canonical PRD — code-current by repository revision.** Protected source and live GitHub governance remain implementation authority. On protected `main`, this PRD is protected truth; on an active PR head, behavior that differs from its live protected base remains candidate truth until integration. External or planned evidence is labeled explicitly. -Noema is an evidence-producing credential and maintenance control plane. Its protected runtime verifies GitHub Actions OIDC identity, exchanges that identity for repository-scoped GitHub App capability, and keeps check, status, scanner, model, review, merge, release, deployment, and buyer/legal authorities separate. +Noema is an evidence-producing credential and maintenance control plane. Its runtime verifies GitHub Actions OIDC identity, exchanges that identity for repository-scoped GitHub App capability, and keeps check, status, scanner, model, review, merge, release, deployment, and buyer/legal authorities separate. ## 1. Users and jobs to be done @@ -52,13 +52,15 @@ Noema is an evidence-producing credential and maintenance control plane. Its pro - **No self-repair privilege escalation:** no force push, synthetic approval, branch-patching repair workflow, or gate weakening substitutes for reviewed authority. - **Evidence-backed commercial claims:** repository prose never fabricates customer, revenue, release, deployment, legal, or transfer evidence. -## 4. Protected product modes +## 4. Product modes ### 4.1 Credential exchange The Cloudflare Worker exposes `/health`, `/ready`, and `/exchange`. -Protected outer workflow trust uses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** despite the legacy variable name. Cryptographic OIDC verification separately validates issuer/audience/repository and token semantics before GitHub App token exchange. A **stronger immutable workflow-source binding is not implemented on protected main** merely because historical documentation once described SHA-paired claims. +The observed protected branch point uses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** despite the legacy variable name and pairs it with immutable `ALLOWED_WORKFLOW_SHA` trust. Cryptographic OIDC verification validates issuer, audience, repository identity, exact workflow ref, immutable workflow-source SHA, token time semantics, and replay requirements before GitHub App token exchange. `wrangler.toml` pins the trusted central `.github` workflow source commit; that dependency remains read-only from the Noema writer and any future central movement requires an explicit Noema roll-forward plus fresh evidence. + +The current revision additionally treats configured workflow-ref/SHA values as canonical operator authority bytes. Whitespace-bearing trust values are rejected rather than normalized into a different trusted identity. When this file is read on an active PR head, that delta is candidate truth until the revision integrates. ### 4.2 Independent review composition @@ -70,7 +72,7 @@ Repository-owned controls inventory exact heads, live bases, checks, statuses, f Protected maintenance workflows mint short-lived GitHub App credentials late, store them in bounded owner-only capability files, and pass paths such as `NOEMA_MAINTAINER_TOKEN_PATH` to credential-bearing scripts instead of making ambient parent-process `GH_TOKEN` the script credential source. The delegated-token boundary validates ownership, exact `0600` mode, regular-file identity, symlink/race resistance, bounded token content, and a minimal child environment. -**Issue #111 is closed on protected main.** PR #421 reconciled the repository-owned credential-source policy by explicitly defining the short-lived GitHub App bootstrap environment as transport into an owner-only capability-file credential boundary. This narrow exception does not generalize to long-lived provider keys, App private keys, PATs, model credentials, or arbitrary runtime environment secrets. Live Maintainer/Reviewer App installation, key custody, rotation, repository permission, and publication-identity evidence remain external and are owned by #29 and #227 rather than inferred from protected source. +**Issue #111 is closed.** Protected #421 reconciled the repository-owned credential-source policy by explicitly defining the short-lived GitHub App bootstrap environment as transport into an owner-only capability-file credential boundary. This narrow exception does not generalize to long-lived provider keys, App private keys, PATs, model credentials, or arbitrary runtime environment secrets. Live Maintainer/Reviewer App installation, key custody, rotation, repository permission, and publication-identity evidence remain external and are owned by #29 and #227 rather than inferred from source. ### 4.4 Product-development proposal @@ -78,7 +80,7 @@ Model-backed development may produce bounded proposals, but credential-bearing e ### 4.5 Patch quarantine and image verification -Protected source includes the patch-quarantine/control family. **PR #407** is the current Draft owner for the patch-validator image/supply-chain lifecycle and must prove its dedicated image build/smoke/SBOM/vulnerability/receipt/final-head path before integration. Historical predecessor #67 remains evidence only until unique-work preservation/supersession is complete. +The patch-validator image/runtime/supply-chain implementation is integrated on protected source. Its dedicated image pipeline performs exact checkout/head checks, static/non-root/no-network runtime validation, SBOM and vulnerability evidence, and source/image/receipt binding. Later protected-main operational receipts and registry publication/signing/attestation/activation remain separate evidence classes under issue #66 and cannot be inferred from source integration. ### 4.6 Acquisition evidence @@ -89,8 +91,8 @@ Protected acquisition-integrity controls authenticate retained evidence and exac | ID | Requirement | | --- | --- | | FR-001 | `/health`, `/ready`, and `/exchange` retain distinct liveness, readiness, and credential-exchange semantics. | -| FR-002 | Validate OIDC issuer, audience, repository/organization, and the configured exact full workflow ref using the protected runtime contract. | -| FR-003 | Do not claim workflow SHA fields or immutable workflow-source binding unless protected source and deployment configuration actually implement and prove them. | +| FR-002 | Validate OIDC issuer, audience, repository/organization, the configured exact full workflow ref, and immutable configured workflow-source SHA before credential minting. | +| FR-003 | Treat workflow-ref/SHA configuration as exact authority and fail closed on absent, malformed, mismatched, stale, or non-canonical trust identity; do not promote a central source movement without explicit Noema roll-forward evidence. | | FR-004 | Restrict credential-bearing GitHub/OIDC requests by reviewed origin, path, method, redirect, timeout, and bounded body/response behavior. | | FR-005 | Coordinate replay protection and pre-auth rate limiting through bounded cross-isolate state. | | FR-006 | Distinguish exact source head, historical PR-base snapshot, independently resolved live base, stack predecessor, and synthetic integration identity. | @@ -136,16 +138,17 @@ Protected acquisition-integrity controls authenticate retained evidence and exac - release evidence binds source, artifact, SBOM, provenance, dependency-license/NOTICE, and rights evidence without inventing legal authority; - automation never chooses an outbound license or fabricates contributor/IP ownership. -## 7. Current active owners - -Current open work is intentionally described narrowly so closed or integrated predecessor PRs are not revived as authority. +## 7. Current durable owners -- **PR #407** / issue #66 — patch-validator image/supply-chain verification and current-main convergence. -- **PR #67** — historical patch-validator image predecessor retained only until #407 integration proves unique-work preservation/supersession. +Current work is described by durable issue-family ownership so integrated or superseded PRs do not become false current authority. -Canonical documentation, buyer/operator README and readiness/operator documentation are protected-main truth rather than separate current owners. Issue #27 remains the target-governance owner, but observed-workflow implementation already merged is protected-main truth rather than an active PR. Issue #111 is closed; its repository-owned credential-source alignment is protected truth, while #29/#227 own remaining external App/publication identity evidence. +- issue #27 — target live governance closure; +- issues #29 / #227 — external Maintainer/Reviewer App installation, key custody/rotation, permission, reviewer-eligibility, and publication identity; +- issue #66 — patch-validator protected-main operational/publication proof after source integration; +- issue #3 — authentic production KPI evidence; +- issue #5 — acquisition evidence coordination. -Transient current check conclusions belong to observation-scoped evidence; the durable rule is that non-terminal or predecessor evidence never transfers into passing authority. +Canonical documentation is code-current by revision rather than owned by historical documentation PRs. Issue #111 is closed; its repository-owned credential-source alignment is protected truth. Transient current check conclusions belong to observation-scoped evidence; non-terminal or predecessor evidence never transfers into passing authority. ## 8. Protected versus external evidence @@ -179,7 +182,7 @@ An earlier stage never proves a later stage. - treating model output, comments, statuses, or scanners as formal approval; - weakening checks, coverage, security, provenance, or governance for automation convenience; - generalizing the narrow short-lived Actions App-token capability-file bootstrap into an ambient or long-lived secret transport mechanism; -- inventing workflow SHA controls absent from protected runtime; +- inventing trust controls or deployment evidence absent from the owning source/live system; - creating direct cross-service application-database coupling; - fabricating release, deployment, KPI, customer, revenue, licensing, ownership, or certification evidence; - adding a physical relational ERD before Noema owns such persistence. @@ -195,4 +198,4 @@ An earlier stage never proves a later stage. - `docs/OPERABILITY.md` — activation, incident, recovery, and operational evidence. - `docs/DOCUMENTATION_GAP_AUDIT.md` — design sufficiency versus protected-main operational sufficiency. - runtime and automation threat models — distinct threat surfaces. -- `docs/LICENSING_AND_IP_TRANSFER.md` — owner/legal and exact-release rights boundary. \ No newline at end of file +- `docs/LICENSING_AND_IP_TRANSFER.md` — owner/legal and exact-release rights boundary. From 8fcd1a3e7c8a7eb178bb7b126ef459e8bd569a11 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:24:08 -0700 Subject: [PATCH 015/564] test(docs): align active-work contract to protected truth --- ...documentation-active-work-contract.test.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/test/documentation-active-work-contract.test.ts b/test/documentation-active-work-contract.test.ts index 101f0e6fa..b9dc362a6 100644 --- a/test/documentation-active-work-contract.test.ts +++ b/test/documentation-active-work-contract.test.ts @@ -2,22 +2,22 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; describe("canonical active-work documentation", () => { - it("tracks only current material owners instead of reviving closed predecessor PRs", () => { + it("tracks durable current owners instead of reviving integrated predecessor PRs", () => { const gapAudit = readFileSync("docs/DOCUMENTATION_GAP_AUDIT.md", "utf8"); const traceability = readFileSync("docs/TRACEABILITY.md", "utf8"); const licensing = readFileSync("docs/LICENSING_AND_IP_TRANSFER.md", "utf8"); - for (const currentOwner of ["PR #407", "PR #67"]) { + for (const currentOwner of ["issue #27", "issue #66", "issue #3", "issue #5"]) { expect(gapAudit).toContain(currentOwner); expect(traceability).toContain(currentOwner); } - for (const historicalOwner of ["PR #71", "PR #90", "PR #91", "PR #92", "PR #93", "PR #94", "PR #95", "PR #97", "PR #99"]) { + for (const historicalOwner of ["PR #67", "PR #71", "PR #90", "PR #91", "PR #92", "PR #93", "PR #94", "PR #95", "PR #97", "PR #99", "PR #407", "Active PR #426"]) { expect(gapAudit).not.toContain(historicalOwner); expect(traceability).not.toContain(historicalOwner); } - expect(traceability).toContain("Historical PR numbers are deliberately omitted"); + expect(traceability).toContain("Historical or integrated PR numbers are deliberately omitted"); expect(traceability).not.toContain("Governance observed-vs-target evidence | issue #27 / PR #412"); expect(traceability).not.toContain("Buyer/operator root README | PR #413"); expect(licensing).toContain("Protected source implements an exact-release rights receipt named `artifact_rights_metadata`"); @@ -28,13 +28,15 @@ describe("canonical active-work documentation", () => { expect(licensing).toContain("UTF-8"); }); - it("keeps the PRD aligned to the current protected trust contract and open owner set", () => { + it("keeps the PRD aligned to immutable workflow-source trust and integrated validator supply chain", () => { const prd = readFileSync("docs/PRD.md", "utf8"); expect(prd).toContain("exact full workflow ref"); - expect(prd).toContain("stronger immutable workflow-source binding is not implemented on protected main"); - expect(prd).toContain("**PR #407**"); - expect(prd).toContain("**PR #67**"); + expect(prd).toContain("immutable `ALLOWED_WORKFLOW_SHA` trust"); + expect(prd).toContain("job_workflow_sha"); + expect(prd).toContain("issue #66"); + expect(prd).not.toContain("**PR #407**"); + expect(prd).not.toContain("**PR #67**"); expect(prd).not.toContain("**PR #71**"); expect(prd).not.toContain("**PR #412**"); expect(prd).not.toContain("**PR #413**"); @@ -43,16 +45,15 @@ describe("canonical active-work documentation", () => { for (const staleActiveOwner of ["PR #80", "PR #83", "PR #86", "PR #90", "PR #91", "PR #92", "PR #93", "PR #94", "PR #69", "PR #72"]) { expect(prd).not.toContain(staleActiveOwner); } - expect(prd).not.toContain("paired immutable workflow SHA"); - expect(prd).not.toContain("job_workflow_sha"); + expect(prd).not.toContain("stronger immutable workflow-source binding is not implemented on protected main"); }); - it("records completed canonical documentation and closed credential-coverage work as protected truth", () => { + it("records the code-current canonical graph and protected credential-coverage closure", () => { const gapAudit = readFileSync("docs/DOCUMENTATION_GAP_AUDIT.md", "utf8"); const traceability = readFileSync("docs/TRACEABILITY.md", "utf8"); - expect(gapAudit).toContain("Canonical architecture/documentation | protected main"); - expect(traceability).toContain("Canonical documentation graph | protected main"); + expect(gapAudit).toContain("Canonical architecture/documentation | current repository revision"); + expect(traceability).toContain("Canonical documentation graph | this repository revision"); expect(traceability).toContain("Credential/security coverage truth | protected main"); expect(traceability).not.toContain("Issue #84 remains open"); }); @@ -108,7 +109,7 @@ describe("canonical active-work documentation", () => { expect(gapAudit).toContain("DESIGN_SUFFICIENT"); expect(gapAudit).toContain("PROTECTED_MAIN_OPERATIONALLY_SUFFICIENT"); - expect(gapAudit).toMatch(/Protected `main` observed:\*\* `?[0-9a-f]{40}`?/); + expect(gapAudit).toMatch(/Protected `main` branch-point observed:\*\* `?[0-9a-f]{40}`?/); expect(gapAudit).toContain("source defect itself is no longer an open implementation gap"); expect(gapAudit).not.toContain("Direct-main dependent PRs remain blocked by protected-main audit until it integrates"); }); @@ -132,4 +133,4 @@ describe("canonical active-work documentation", () => { expect(claude).not.toContain("The entire Worker is one file: **`src/index.ts`**"); expect(claude).not.toContain("There are no KV/D1/queue/Durable Object bindings"); }); -}); \ No newline at end of file +}); From 4b3e7d41499b8879fa4ca01547381c2945b99a24 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:42:06 -0700 Subject: [PATCH 016/564] docs(oidc): name immutable workflow claim authority --- docs/PRD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/PRD.md b/docs/PRD.md index 4428f77cb..d0daf9497 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -58,7 +58,7 @@ Noema is an evidence-producing credential and maintenance control plane. Its run The Cloudflare Worker exposes `/health`, `/ready`, and `/exchange`. -The observed protected branch point uses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** despite the legacy variable name and pairs it with immutable `ALLOWED_WORKFLOW_SHA` trust. Cryptographic OIDC verification validates issuer, audience, repository identity, exact workflow ref, immutable workflow-source SHA, token time semantics, and replay requirements before GitHub App token exchange. `wrangler.toml` pins the trusted central `.github` workflow source commit; that dependency remains read-only from the Noema writer and any future central movement requires an explicit Noema roll-forward plus fresh evidence. +The observed protected branch point uses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** despite the legacy variable name and pairs it with immutable `ALLOWED_WORKFLOW_SHA` trust. Cryptographic OIDC verification validates issuer, audience, repository identity, exact workflow ref, the immutable `job_workflow_sha` workflow-source identity, token time semantics, and replay requirements before GitHub App token exchange. `wrangler.toml` pins the trusted central `.github` workflow source commit; that dependency remains read-only from the Noema writer and any future central movement requires an explicit Noema roll-forward plus fresh evidence. The current revision additionally treats configured workflow-ref/SHA values as canonical operator authority bytes. Whitespace-bearing trust values are rejected rather than normalized into a different trusted identity. When this file is read on an active PR head, that delta is candidate truth until the revision integrates. From 8574c2cb96fbafbc692e1b2abb6695bcfe9d0b69 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:42:46 -0700 Subject: [PATCH 017/564] docs(governance): preserve observed-evidence authority boundary --- docs/DOCUMENTATION_GAP_AUDIT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DOCUMENTATION_GAP_AUDIT.md b/docs/DOCUMENTATION_GAP_AUDIT.md index 39c22abfa..cbce1e97d 100644 --- a/docs/DOCUMENTATION_GAP_AUDIT.md +++ b/docs/DOCUMENTATION_GAP_AUDIT.md @@ -71,7 +71,7 @@ Every run must refetch these identities. This table is navigation, not immutable ### G-01 — Target governance stronger than previously observed required workflow -Issue #27 remains open. A required central Security Scan workflow, when live, does not prove stronger target pull-request/review/status/non-fast-forward/deletion controls. Missing target controls remain FAIL; historical workflow evidence must never be promoted into authority it does not carry. +Issue #27 remains open. A required central Security Scan workflow, when live, does not prove stronger target pull-request/review/status/non-fast-forward/deletion controls. Missing target controls remain FAIL; observed workflow evidence must never be promoted into authority it does not carry. ### G-02 — Patch-validator operational/publication evidence From e7685ea74196cea9d9e349333abe23c081120a7e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 17:18:36 -0700 Subject: [PATCH 018/564] chore(oidc): refresh protected-base convergence From 228c82dc39a5e8c4ae0211360486b175cc90361e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:07:59 -0700 Subject: [PATCH 019/564] test(oidc): require current audited central 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 e261139cf..712914895 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 = - "9f8f84074d8a8bc142eafea12c5b9e1c8570ccd6"; + "93b9cbb145bfe03453c9421dcf7e0668da0f8050"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 8cad64ec7b68a99f05a29c769b379a512bf5acd9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:08:19 -0700 Subject: [PATCH 020/564] fix(oidc): roll forward audited central workflow source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index b21d13f56..8f798ccbd 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 = "9f8f84074d8a8bc142eafea12c5b9e1c8570ccd6" +ALLOWED_WORKFLOW_SHA = "93b9cbb145bfe03453c9421dcf7e0668da0f8050" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 670fcff115e443786c5e1aeb25c407e9a8c218ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:13:00 -0700 Subject: [PATCH 021/564] docs(architecture): bind current central workflow source --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 955a00c48..0dcf8902a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `9f8f84074d8a8bc142eafea12c5b9e1c8570ccd6`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `93b9cbb145bfe03453c9421dcf7e0668da0f8050`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 453a4702a78cf0acd2d0eff9242e93902beb7a40 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 20:14:56 -0700 Subject: [PATCH 022/564] test(oidc): require current central workflow source --- test/trusted-workflow-source-rollforward.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/trusted-workflow-source-rollforward.test.ts b/test/trusted-workflow-source-rollforward.test.ts index 712914895..c762b0a48 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 = - "93b9cbb145bfe03453c9421dcf7e0668da0f8050"; + "bdd8d72e731d47879819b436670b74da0053b3ed"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { @@ -12,4 +12,4 @@ describe("trusted central workflow source revision", () => { `ALLOWED_WORKFLOW_SHA = "${auditedCentralWorkflowSourceSha}"`, ); }); -}); +}); \ No newline at end of file From 51a610019f84629ef7091771733a26534edfee8c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 20:15:21 -0700 Subject: [PATCH 023/564] fix(oidc): roll trust to current central workflow source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 8f798ccbd..789421090 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 = "93b9cbb145bfe03453c9421dcf7e0668da0f8050" +ALLOWED_WORKFLOW_SHA = "bdd8d72e731d47879819b436670b74da0053b3ed" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 2a415df8ab97e5a07c1b9aca0d5caeb1afc160f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 20:16:01 -0700 Subject: [PATCH 024/564] docs(architecture): bind current central workflow source --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0dcf8902a..6481f0458 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `93b9cbb145bfe03453c9421dcf7e0668da0f8050`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `bdd8d72e731d47879819b436670b74da0053b3ed`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 278a1ac9ff3d24ad5d724e8eee97f4d5b580a54d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 21:04:53 -0700 Subject: [PATCH 025/564] test(oidc): reject uppercase immutable workflow refs --- test/runtime-readiness-ref-format.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/runtime-readiness-ref-format.test.ts b/test/runtime-readiness-ref-format.test.ts index b6a956899..2e6bc0562 100644 --- a/test/runtime-readiness-ref-format.test.ts +++ b/test/runtime-readiness-ref-format.test.ts @@ -71,6 +71,19 @@ describe("runtime-readiness exact Git ref validation", () => { expect(result.failedChecks).toContain("allowed_workflow_ref"); }); + it("rejects uppercase immutable workflow commit refs as non-canonical authority", async () => { + const env = await readyEnvironment(); + const uppercaseCommit = "ABCDEF0123456789ABCDEF0123456789ABCDEF01"; + env.ALLOWED_WORKFLOW_REF_PREFIX = + `ContextualWisdomLab/.github/.github/workflows/noema-review.yml@${uppercaseCommit}`; + env.ALLOWED_WORKFLOW_SHA = uppercaseCommit.toLowerCase(); + + const result = await evaluateRuntimeReadiness(env); + + expect(result.ready).toBe(false); + expect(result.failedChecks).toContain("allowed_workflow_ref"); + }); + it.each([ "refs/heads/release/2026.08", "refs/tags/v0.2.0", @@ -89,4 +102,4 @@ describe("runtime-readiness exact Git ref validation", () => { expect(result.failedChecks).not.toContain("allowed_workflow_ref"); expect(result.failedChecks).not.toContain("allowed_workflow_sha"); }); -}); +}); \ No newline at end of file From f79d1d4df5d28f9c214ebe4dd2ef79a5d0a57ddc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 21:06:05 -0700 Subject: [PATCH 026/564] fix(oidc): require canonical lowercase immutable workflow refs --- src/runtime-readiness.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/runtime-readiness.ts b/src/runtime-readiness.ts index 0ad58ee73..949f21a0b 100644 --- a/src/runtime-readiness.ts +++ b/src/runtime-readiness.ts @@ -4,7 +4,7 @@ const trustedAudiencePattern = /^[A-Za-z0-9._:-]{1,128}$/; const trustedOwnerPattern = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/; const positiveDecimalPattern = /^[1-9][0-9]*$/; const privateKeyPattern = /^-----BEGIN PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END PRIVATE KEY-----$/; -const exactCommitPattern = /^[0-9a-fA-F]{40}$/; +const exactCommitPattern = /^[0-9a-f]{40}$/; const exactWorkflowShaPattern = /^[0-9a-f]{40}$/; const trustedNamedRefPattern = /^refs\/(?:heads|tags)\/(?=.{1,1024}$)(?!\.)(?![^/]*\.lock(?:\/|$))(?!.*\/\.)(?!.*\/[^/]*\.lock(?:\/|$))(?!.*(?:\.\.|\/\/|@\{|\\|[\x00-\x20\x7f~^:?*\[]))(?!.*[\/.]$)[A-Za-z0-9._/-]+$/; @@ -99,7 +99,7 @@ function isExactWorkflowRef(value: string, repository: string): boolean { function immutableWorkflowCommit(value: string, repository: string): string | undefined { const refName = workflowRefName(value, repository); - return refName && exactCommitPattern.test(refName) ? refName.toLowerCase() : undefined; + return refName && exactCommitPattern.test(refName) ? refName : undefined; } function isCanonicalPositiveSafeInteger(value: string | undefined): boolean { @@ -219,4 +219,4 @@ export async function evaluateRuntimeReadiness( ready: failedChecks.length === 0, failedChecks, }; -} +} \ No newline at end of file From 3fbb8ad7583bf60c4cc789651be4b399d0908ed1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 21:06:42 -0700 Subject: [PATCH 027/564] test(oidc): reject uppercase immutable refs at exchange boundary --- ...oidc-workflow-sha-canonical-config.test.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/test/oidc-workflow-sha-canonical-config.test.ts b/test/oidc-workflow-sha-canonical-config.test.ts index e59254ca6..96945419a 100644 --- a/test/oidc-workflow-sha-canonical-config.test.ts +++ b/test/oidc-workflow-sha-canonical-config.test.ts @@ -92,6 +92,7 @@ afterEach(() => { async function exchangeWithTrustConfig( overrides: Partial, layer: "runtime" | "authoritative", + tokenWorkflowRef = configuredWorkflowRef, ): Promise { vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { const url = String(input); @@ -116,7 +117,7 @@ async function exchangeWithTrustConfig( exp: now + 300, nbf: now - 30, iat: now - 30, - job_workflow_ref: configuredWorkflowRef, + job_workflow_ref: tokenWorkflowRef, job_workflow_sha: configuredWorkflowSha, }); @@ -196,4 +197,23 @@ describe("canonical workflow-source configuration authority", () => { }); }, ); -}); + + it.each(["runtime", "authoritative"] as const)( + "rejects uppercase immutable workflow-ref commit authority at the %s layer", + async (layer) => { + const uppercaseCommitRef = + `ContextualWisdomLab/.github/.github/workflows/noema-review.yml@${"A".repeat(40)}`; + const response = await exchangeWithTrustConfig( + { ALLOWED_WORKFLOW_REF_PREFIX: uppercaseCommitRef }, + layer, + uppercaseCommitRef, + ); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_WORKFLOW_NOT_ALLOWED", + }); + }, + ); +}); \ No newline at end of file From d49bde94803a9eafd0e734563894e4a249f733a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 21:08:54 -0700 Subject: [PATCH 028/564] fix(oidc): fail closed on non-canonical immutable ref bytes --- src/index.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 82b43864e..695806a9b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -462,7 +462,23 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise } const workflowRef = payload.job_workflow_ref || payload.workflow_ref || ""; - if (workflowRef !== env.ALLOWED_WORKFLOW_REF_PREFIX) { + const configuredWorkflowRef = env.ALLOWED_WORKFLOW_REF_PREFIX; + const configuredRefSeparator = configuredWorkflowRef.lastIndexOf("@"); + const configuredRefName = configuredRefSeparator >= 0 + ? configuredWorkflowRef.slice(configuredRefSeparator + 1) + : ""; + if ( + /^[0-9A-Fa-f]{40}$/.test(configuredRefName) + && !exactWorkflowSourceShaPattern.test(configuredRefName) + ) { + throw new ApiError( + "ERR_WORKFLOW_NOT_ALLOWED", + 503, + "Workflow source trust configuration unavailable", + { match_policy: "exact-ref-and-source-sha" }, + ); + } + if (workflowRef !== configuredWorkflowRef) { throw new ApiError("ERR_WORKFLOW_NOT_ALLOWED", 403, "OIDC workflow_ref is not allowed"); } if (!workflowRef.startsWith(`${env.ALLOWED_WORKFLOW_REPOSITORY}/.github/workflows/`)) { @@ -867,4 +883,4 @@ export default { return withOperationalHeaders(response, traceId, latency_ms); } }, -}; +}; \ No newline at end of file From 14de45e43ed27c4bc2e9f9c4b44025fca62d3c42 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 21:12:32 -0700 Subject: [PATCH 029/564] test(oidc): reject uppercase immutable ref in wrapper --- test/worker-workflow-ref-canonicality.test.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 test/worker-workflow-ref-canonicality.test.ts diff --git a/test/worker-workflow-ref-canonicality.test.ts b/test/worker-workflow-ref-canonicality.test.ts new file mode 100644 index 000000000..c9ca5a6e0 --- /dev/null +++ b/test/worker-workflow-ref-canonicality.test.ts @@ -0,0 +1,84 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../src/index", () => ({ + default: { + fetch: vi.fn(async () => Response.json({ ok: true })), + }, +})); + +import worker, { type Env } from "../src/worker"; + +function allowRateLimiter(): DurableObjectNamespace { + return { + idFromName(name: string) { + return { toString: () => name } as DurableObjectId; + }, + get() { + return { + fetch: async () => Response.json({ + allowed: true, + limit: 1000, + remaining: 999, + retry_after_seconds: 0, + }), + } as unknown as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; +} + +function encodeSegment(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +function tokenWithWorkflowRef(workflowRef: string): string { + return `${encodeSegment({ alg: "RS256", kid: "test" })}.${encodeSegment({ + job_workflow_ref: workflowRef, + jti: "canonical-ref-test", + exp: Math.floor(Date.now() / 1000) + 300, + })}.signature`; +} + +describe("wrapper workflow-ref canonical authority", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("rejects an uppercase immutable commit ref before replay or base exchange", async () => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + const uppercaseCommitRef = + `ContextualWisdomLab/.github/.github/workflows/noema-review.yml@${"A".repeat(40)}`; + 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: uppercaseCommitRef, + ALLOWED_WORKFLOW_SHA: "a".repeat(40), + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: "unused", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", + NOEMA_RATE_LIMITER: allowRateLimiter(), + }; + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${tokenWithWorkflowRef(uppercaseCommitRef)}`, + "cf-connecting-ip": "203.0.113.80", + "content-type": "application/json", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + env, + ); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_WORKFLOW_NOT_ALLOWED", + message: "Workflow trust configuration unavailable", + }); + }); +}); From 5637f07fb12b509cf5fd04b8ea1a86847c163f2e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 21:13:33 -0700 Subject: [PATCH 030/564] fix(oidc): reject non-canonical immutable ref in wrapper --- src/worker.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/worker.ts b/src/worker.ts index 395d881dc..60d3bd4f4 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -25,6 +25,8 @@ export interface Env extends BaseEnv, DistributedRateLimitEnv, OidcReplayProtect const trustedTracePattern = /^[A-Za-z0-9._:-]+$/; const trustedJtiPattern = /^[A-Za-z0-9._:-]+$/; +const canonicalCommitRefPattern = /^[0-9a-f]{40}$/; +const anyCaseCommitRefPattern = /^[0-9A-Fa-f]{40}$/; const MAX_TRACE_LENGTH = 128; const MAX_JTI_LENGTH = 256; const MAX_OIDC_PAYLOAD_SEGMENT_LENGTH = 8_192; @@ -92,11 +94,15 @@ function configuredExactWorkflowRef(env: Env): string | undefined { const workflowAndRef = candidate.slice(repositoryPrefix.length); const separatorIndex = workflowAndRef.indexOf("@"); + const refName = separatorIndex >= 0 + ? workflowAndRef.slice(separatorIndex + 1) + : ""; if ( separatorIndex <= 0 || separatorIndex !== workflowAndRef.lastIndexOf("@") || separatorIndex === workflowAndRef.length - 1 || /[\s*?,]/.test(candidate) + || (anyCaseCommitRefPattern.test(refName) && !canonicalCommitRefPattern.test(refName)) ) { return undefined; } From 7c732e90cf8f9616b8fcd499d7246b53d6e90424 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 21:59:15 -0700 Subject: [PATCH 031/564] test(oidc): require current central workflow source revision --- 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 c762b0a48..51a7a0fdb 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 = - "bdd8d72e731d47879819b436670b74da0053b3ed"; + "0c6b9a6459c9dbdf5e23fb01df7a32a8a14964b3"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 3458be693a1a69efc22e47a48c4db2c23bcaa3b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 21:59:35 -0700 Subject: [PATCH 032/564] fix(oidc): roll trust to current central workflow commit --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 789421090..ed788c559 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 = "bdd8d72e731d47879819b436670b74da0053b3ed" +ALLOWED_WORKFLOW_SHA = "0c6b9a6459c9dbdf5e23fb01df7a32a8a14964b3" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 652178060102f8265c36dbf3be9d66d759297f47 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 22:29:51 -0700 Subject: [PATCH 033/564] fix(core): 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 695806a9b..9bfcff34d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -883,4 +883,4 @@ export default { return withOperationalHeaders(response, traceId, latency_ms); } }, -}; \ No newline at end of file +}; From adc1fe4a6d66d8ec2bcd5a4bc3fa1e7d521935ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 22:30:32 -0700 Subject: [PATCH 034/564] docs(architecture): align trusted workflow source revision --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6481f0458..bd42d4687 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `bdd8d72e731d47879819b436670b74da0053b3ed`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `0c6b9a6459c9dbdf5e23fb01df7a32a8a14964b3`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From ed318fd317483064fd8c7edf3e9ab55af9649127 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:08:48 -0700 Subject: [PATCH 035/564] test(oidc): reject workflow refs without immutable ref --- test/oidc-workflow-ref-exact-match.test.ts | 61 ++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/test/oidc-workflow-ref-exact-match.test.ts b/test/oidc-workflow-ref-exact-match.test.ts index a55206b13..c3659be91 100644 --- a/test/oidc-workflow-ref-exact-match.test.ts +++ b/test/oidc-workflow-ref-exact-match.test.ts @@ -114,4 +114,65 @@ describe("cryptographic OIDC workflow identity", () => { message: "OIDC workflow_ref is not allowed", }); }); + + it("fails closed when the authoritative workflow ref omits an immutable ref delimiter", async () => { + const malformedWorkflowRef = + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml"; + const workflowSha = "a".repeat(40); + const now = Math.floor(Date.now() / 1000); + const token = await signedJwt({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository: "ContextualWisdomLab/.github", + job_workflow_ref: malformedWorkflowRef, + job_workflow_sha: workflowSha, + sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }); + + 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 }); + }); + + 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.122", + }, + body: JSON.stringify({ target_repository: { owner: "ContextualWisdomLab" } }), + }), + { + ...env, + ALLOWED_WORKFLOW_REF_PREFIX: malformedWorkflowRef, + ALLOWED_WORKFLOW_SHA: workflowSha, + }, + ); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_WORKFLOW_NOT_ALLOWED", + message: "Workflow source trust configuration unavailable", + details: { + match_policy: "exact-ref-and-source-sha", + }, + }); + }); }); From 28aedb3f3823d788979be444236929a2291ed6f9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:10:37 -0700 Subject: [PATCH 036/564] test(oidc): cover malformed workflow ref delimiters --- test/oidc-workflow-ref-exact-match.test.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/test/oidc-workflow-ref-exact-match.test.ts b/test/oidc-workflow-ref-exact-match.test.ts index c3659be91..f9e1aac7a 100644 --- a/test/oidc-workflow-ref-exact-match.test.ts +++ b/test/oidc-workflow-ref-exact-match.test.ts @@ -115,9 +115,20 @@ describe("cryptographic OIDC workflow identity", () => { }); }); - it("fails closed when the authoritative workflow ref omits an immutable ref delimiter", async () => { - const malformedWorkflowRef = - "ContextualWisdomLab/.github/.github/workflows/noema-review.yml"; + it.each([ + [ + "omits an immutable ref delimiter", + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml", + ], + [ + "contains multiple ref delimiters", + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main@refs/heads/other", + ], + [ + "ends at an empty ref delimiter", + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@", + ], + ])("fails closed when the authoritative workflow ref %s", async (_label, malformedWorkflowRef) => { const workflowSha = "a".repeat(40); const now = Math.floor(Date.now() / 1000); const token = await signedJwt({ From f18b3bba288fd67e6929535122ecfeec9a933b4b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:12:24 -0700 Subject: [PATCH 037/564] fix(oidc): reject malformed authoritative workflow refs --- src/index.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9bfcff34d..80b388549 100644 --- a/src/index.ts +++ b/src/index.ts @@ -463,10 +463,20 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise const workflowRef = payload.job_workflow_ref || payload.workflow_ref || ""; const configuredWorkflowRef = env.ALLOWED_WORKFLOW_REF_PREFIX; - const configuredRefSeparator = configuredWorkflowRef.lastIndexOf("@"); - const configuredRefName = configuredRefSeparator >= 0 - ? configuredWorkflowRef.slice(configuredRefSeparator + 1) - : ""; + const configuredRefSeparator = configuredWorkflowRef.indexOf("@"); + if ( + configuredRefSeparator <= 0 + || configuredRefSeparator !== configuredWorkflowRef.lastIndexOf("@") + || configuredRefSeparator === configuredWorkflowRef.length - 1 + ) { + throw new ApiError( + "ERR_WORKFLOW_NOT_ALLOWED", + 503, + "Workflow source trust configuration unavailable", + { match_policy: "exact-ref-and-source-sha" }, + ); + } + const configuredRefName = configuredWorkflowRef.slice(configuredRefSeparator + 1); if ( /^[0-9A-Fa-f]{40}$/.test(configuredRefName) && !exactWorkflowSourceShaPattern.test(configuredRefName) From 418f0c3d4f08b6952831ccb4e60fa5960f1f8b6d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:13:00 -0700 Subject: [PATCH 038/564] test(oidc): exercise public malformed workflow ref guard --- ...orkflow-sha-authoritative-boundary.test.ts | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/test/oidc-workflow-sha-authoritative-boundary.test.ts b/test/oidc-workflow-sha-authoritative-boundary.test.ts index 95dffae2c..95051bf19 100644 --- a/test/oidc-workflow-sha-authoritative-boundary.test.ts +++ b/test/oidc-workflow-sha-authoritative-boundary.test.ts @@ -89,6 +89,7 @@ afterEach(() => { async function exchangeWithAuthoritativeConfig( workflowSha: string | undefined, + workflowRef = configuredWorkflowRef, ): Promise { vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { const url = String(input); @@ -110,7 +111,7 @@ async function exchangeWithAuthoritativeConfig( repository_owner: envWithoutWorkflowSha.ALLOWED_REPOSITORY_OWNER, repository: "ContextualWisdomLab/.github", sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", - job_workflow_ref: configuredWorkflowRef, + job_workflow_ref: workflowRef, job_workflow_sha: "a".repeat(40), exp: now + 300, nbf: now - 30, @@ -129,7 +130,11 @@ async function exchangeWithAuthoritativeConfig( }, body: JSON.stringify({ target_repository: { owner: "ContextualWisdomLab" } }), }), - { ...envWithoutWorkflowSha, ALLOWED_WORKFLOW_SHA: workflowSha }, + { + ...envWithoutWorkflowSha, + ALLOWED_WORKFLOW_REF_PREFIX: workflowRef, + ALLOWED_WORKFLOW_SHA: workflowSha, + }, ); } @@ -157,4 +162,18 @@ describe("authoritative OIDC workflow source configuration", () => { it("fails closed when the base exchange worker receives a non-canonical workflow source SHA", async () => { await expectWorkflowSourceConfigurationFailure("A".repeat(40)); }); + + it("fails closed at the public worker when configured workflow identity omits its ref delimiter", async () => { + const response = await exchangeWithAuthoritativeConfig( + "a".repeat(40), + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml", + ); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_WORKFLOW_NOT_ALLOWED", + message: "Workflow trust configuration unavailable", + }); + }); }); From 25cf21fdbdeecc4c06e4cdd43552cede46b8490c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:27:07 -0700 Subject: [PATCH 039/564] test(oidc): reject delimiter-ambiguous readiness ref --- test/runtime-readiness-ref-format.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/runtime-readiness-ref-format.test.ts b/test/runtime-readiness-ref-format.test.ts index 2e6bc0562..3aa0da0c4 100644 --- a/test/runtime-readiness-ref-format.test.ts +++ b/test/runtime-readiness-ref-format.test.ts @@ -60,7 +60,8 @@ describe("runtime-readiness exact Git ref validation", () => { "refs/heads/release/.hidden", "refs/tags/release.lock", "refs/heads/release.", - ])("rejects Git-invalid workflow ref %s", async (refName) => { + "refs/heads/release@candidate", + ])("rejects Git-invalid or delimiter-ambiguous workflow ref %s", async (refName) => { const env = await readyEnvironment(); env.ALLOWED_WORKFLOW_REF_PREFIX = `ContextualWisdomLab/.github/.github/workflows/noema-review.yml@${refName}`; @@ -102,4 +103,4 @@ describe("runtime-readiness exact Git ref validation", () => { expect(result.failedChecks).not.toContain("allowed_workflow_ref"); expect(result.failedChecks).not.toContain("allowed_workflow_sha"); }); -}); \ No newline at end of file +}); From da8ff990c08de785548f3a0ddbeffd72db7b20eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:28:35 -0700 Subject: [PATCH 040/564] fix(oidc): align readiness workflow delimiter authority --- src/runtime-readiness.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/runtime-readiness.ts b/src/runtime-readiness.ts index 949f21a0b..4a9b4655a 100644 --- a/src/runtime-readiness.ts +++ b/src/runtime-readiness.ts @@ -84,6 +84,14 @@ function isTrustedWorkflowRepository(value: string, owner: string): boolean { } function workflowRefName(value: string, repository: string): string | undefined { + const separator = value.indexOf("@"); + if ( + separator <= 0 + || separator !== value.lastIndexOf("@") + || separator === value.length - 1 + ) { + return undefined; + } const escapedRepository = escapeRegularExpression(repository); const workflowRefPattern = new RegExp( `^${escapedRepository}/\\.github/workflows/[A-Za-z0-9_.-]{1,100}\\.ya?ml@(.+)$`, @@ -219,4 +227,4 @@ export async function evaluateRuntimeReadiness( ready: failedChecks.length === 0, failedChecks, }; -} \ No newline at end of file +} From adfc6bce7cbe1d1f8a5620462f7ecbc90a7bade8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 02:18:59 -0700 Subject: [PATCH 041/564] 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 51a7a0fdb..50352d25b 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 = - "0c6b9a6459c9dbdf5e23fb01df7a32a8a14964b3"; + "613a33e0cb1c6db9790fae99f6253445712ac37a"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 8803e84cc6b1f414356971e9a8f944b06e53b71a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 02:19:34 -0700 Subject: [PATCH 042/564] fix(oidc): roll trusted workflow source to current central main --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index ed788c559..b55856503 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 = "0c6b9a6459c9dbdf5e23fb01df7a32a8a14964b3" +ALLOWED_WORKFLOW_SHA = "613a33e0cb1c6db9790fae99f6253445712ac37a" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 161965bd71d7fbab65c9bf64e78c455de10a9446 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 02:20:49 -0700 Subject: [PATCH 043/564] docs(architecture): align current central workflow source --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bd42d4687..2625e0875 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `0c6b9a6459c9dbdf5e23fb01df7a32a8a14964b3`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `613a33e0cb1c6db9790fae99f6253445712ac37a`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 484001078a0c1ad84d45180c8d637f838029f7eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 15:05:11 -0700 Subject: [PATCH 044/564] test(oidc): reject Git-invalid configured workflow refs --- test/worker-workflow-ref-canonicality.test.ts | 78 +++++++++++++------ 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/test/worker-workflow-ref-canonicality.test.ts b/test/worker-workflow-ref-canonicality.test.ts index c9ca5a6e0..05c63342e 100644 --- a/test/worker-workflow-ref-canonicality.test.ts +++ b/test/worker-workflow-ref-canonicality.test.ts @@ -38,6 +38,37 @@ function tokenWithWorkflowRef(workflowRef: string): string { })}.signature`; } +function workflowEnvironment(workflowRef: string): 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: workflowRef, + ALLOWED_WORKFLOW_SHA: "a".repeat(40), + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: "unused", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", + NOEMA_RATE_LIMITER: allowRateLimiter(), + }; +} + +async function exchangeFromWorkflowRef(workflowRef: string): Promise { + return worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${tokenWithWorkflowRef(workflowRef)}`, + "cf-connecting-ip": "203.0.113.80", + "content-type": "application/json", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + workflowEnvironment(workflowRef), + ); +} + describe("wrapper workflow-ref canonical authority", () => { afterEach(() => { vi.restoreAllMocks(); @@ -47,32 +78,29 @@ describe("wrapper workflow-ref canonical authority", () => { vi.spyOn(console, "log").mockImplementation(() => undefined); const uppercaseCommitRef = `ContextualWisdomLab/.github/.github/workflows/noema-review.yml@${"A".repeat(40)}`; - 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: uppercaseCommitRef, - ALLOWED_WORKFLOW_SHA: "a".repeat(40), - GITHUB_API_BASE: "https://api.github.com", - GITHUB_APP_ID: "1", - GITHUB_APP_PRIVATE_KEY_PEM: "unused", - NOEMA_RATE_LIMIT_PER_MINUTE: "1000", - NOEMA_RATE_LIMITER: allowRateLimiter(), - }; - const response = await worker.fetch( - new Request("https://noema.example/exchange", { - method: "POST", - headers: { - authorization: `Bearer ${tokenWithWorkflowRef(uppercaseCommitRef)}`, - "cf-connecting-ip": "203.0.113.80", - "content-type": "application/json", - }, - body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), - }), - env, - ); + const response = await exchangeFromWorkflowRef(uppercaseCommitRef); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_WORKFLOW_NOT_ALLOWED", + message: "Workflow trust configuration unavailable", + }); + }); + + it.each([ + "refs/heads/release..candidate", + "refs/heads/release//candidate", + "refs/heads/.hidden", + "refs/tags/release.lock", + "refs/heads/release.", + ])("rejects Git-invalid named workflow ref %s before replay or base exchange", async (refName) => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + const workflowRef = + `ContextualWisdomLab/.github/.github/workflows/noema-review.yml@${refName}`; + + const response = await exchangeFromWorkflowRef(workflowRef); expect(response.status).toBe(503); await expect(response.json()).resolves.toMatchObject({ From d0477999072bf316a97605424600f69bb44b1f6f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 15:07:23 -0700 Subject: [PATCH 045/564] fix(oidc): reject Git-invalid configured workflow refs --- src/worker.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/worker.ts b/src/worker.ts index 60d3bd4f4..e4b047400 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -27,6 +27,7 @@ const trustedTracePattern = /^[A-Za-z0-9._:-]+$/; const trustedJtiPattern = /^[A-Za-z0-9._:-]+$/; const canonicalCommitRefPattern = /^[0-9a-f]{40}$/; const anyCaseCommitRefPattern = /^[0-9A-Fa-f]{40}$/; +const trustedNamedRefPattern = /^refs\/(?:heads|tags)\/(?=.{1,1024}$)(?!\.)(?![^/]*\.lock(?:\/|$))(?!.*\/\.)(?!.*\/[^/]*\.lock(?:\/|$))(?!.*(?:\.\.|\/\/|@\{|\\|[\x00-\x20\x7f~^:?*\[]))(?!.*[\/.]$)[A-Za-z0-9._/-]+$/; const MAX_TRACE_LENGTH = 128; const MAX_JTI_LENGTH = 256; const MAX_OIDC_PAYLOAD_SEGMENT_LENGTH = 8_192; @@ -103,6 +104,7 @@ function configuredExactWorkflowRef(env: Env): string | undefined { || separatorIndex === workflowAndRef.length - 1 || /[\s*?,]/.test(candidate) || (anyCaseCommitRefPattern.test(refName) && !canonicalCommitRefPattern.test(refName)) + || (!canonicalCommitRefPattern.test(refName) && !trustedNamedRefPattern.test(refName)) ) { return undefined; } From b673c723dcda3f387d792071619599074e4174ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 16:04:46 -0700 Subject: [PATCH 046/564] test(readiness): reject dot workflow repository identities --- test/runtime-readiness-ref-format.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/runtime-readiness-ref-format.test.ts b/test/runtime-readiness-ref-format.test.ts index 3aa0da0c4..ecb666abb 100644 --- a/test/runtime-readiness-ref-format.test.ts +++ b/test/runtime-readiness-ref-format.test.ts @@ -53,6 +53,18 @@ async function readyEnvironment(): Promise { } describe("runtime-readiness exact Git ref validation", () => { + it.each([".", ".."])("rejects invalid workflow repository name %s", async (repositoryName) => { + const env = await readyEnvironment(); + env.ALLOWED_WORKFLOW_REPOSITORY = `ContextualWisdomLab/${repositoryName}`; + env.ALLOWED_WORKFLOW_REF_PREFIX = + `ContextualWisdomLab/${repositoryName}/.github/workflows/noema-review.yml@refs/heads/main`; + + const result = await evaluateRuntimeReadiness(env); + + expect(result.ready).toBe(false); + expect(result.failedChecks).toContain("allowed_workflow_repository"); + }); + it.each([ "refs/heads/release..candidate", "refs/heads/release//candidate", From 33da00082464a8c8772ef52a5e837cff45250b39 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 16:05:19 -0700 Subject: [PATCH 047/564] fix(readiness): reject invalid workflow repository identities --- src/runtime-readiness.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/runtime-readiness.ts b/src/runtime-readiness.ts index 4a9b4655a..affa84084 100644 --- a/src/runtime-readiness.ts +++ b/src/runtime-readiness.ts @@ -79,8 +79,12 @@ function escapeRegularExpression(value: string): string { } function isTrustedWorkflowRepository(value: string, owner: string): boolean { - const escapedOwner = escapeRegularExpression(owner); - return new RegExp(`^${escapedOwner}/[A-Za-z0-9_.-]{1,100}$`).test(value); + const prefix = `${owner}/`; + if (!value.startsWith(prefix)) return false; + const repositoryName = value.slice(prefix.length); + return repositoryName !== "." + && repositoryName !== ".." + && /^[A-Za-z0-9_.-]{1,100}$/.test(repositoryName); } function workflowRefName(value: string, repository: string): string | undefined { From 3254929864ca2583565ee2d56c77e3c05665fd65 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 16:34:50 -0700 Subject: [PATCH 048/564] test(oidc): reject invalid workflow repository at exchange --- test/worker-workflow-ref-canonicality.test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/worker-workflow-ref-canonicality.test.ts b/test/worker-workflow-ref-canonicality.test.ts index 05c63342e..9f1d7ee19 100644 --- a/test/worker-workflow-ref-canonicality.test.ts +++ b/test/worker-workflow-ref-canonicality.test.ts @@ -69,6 +69,26 @@ async function exchangeFromWorkflowRef(workflowRef: string): Promise { ); } +async function exchangeFromWorkflowRepository(repositoryName: string): Promise { + const workflowRepository = `ContextualWisdomLab/${repositoryName}`; + const workflowRef = `${workflowRepository}/.github/workflows/noema-review.yml@refs/heads/main`; + const env = workflowEnvironment(workflowRef); + env.ALLOWED_WORKFLOW_REPOSITORY = workflowRepository; + + return worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${tokenWithWorkflowRef(workflowRef)}`, + "cf-connecting-ip": "203.0.113.80", + "content-type": "application/json", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + env, + ); +} + describe("wrapper workflow-ref canonical authority", () => { afterEach(() => { vi.restoreAllMocks(); @@ -89,6 +109,22 @@ describe("wrapper workflow-ref canonical authority", () => { }); }); + it.each([".", ".."]) ( + "rejects invalid workflow repository name %s before replay or base exchange", + async (repositoryName) => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + + const response = await exchangeFromWorkflowRepository(repositoryName); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_WORKFLOW_NOT_ALLOWED", + message: "Workflow trust configuration unavailable", + }); + }, + ); + it.each([ "refs/heads/release..candidate", "refs/heads/release//candidate", From 31c0d039e064658359d9e167c96f7ed8cf348044 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 16:35:30 -0700 Subject: [PATCH 049/564] test(oidc): bind verifier to valid workflow repository --- test/oidc-workflow-ref-exact-match.test.ts | 65 ++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/oidc-workflow-ref-exact-match.test.ts b/test/oidc-workflow-ref-exact-match.test.ts index f9e1aac7a..37e7fad9e 100644 --- a/test/oidc-workflow-ref-exact-match.test.ts +++ b/test/oidc-workflow-ref-exact-match.test.ts @@ -115,6 +115,71 @@ describe("cryptographic OIDC workflow identity", () => { }); }); + it.each([".", ".."]) ( + "fails closed when authoritative workflow repository name is %s", + async (repositoryName) => { + const workflowSha = "a".repeat(40); + const workflowRepository = `ContextualWisdomLab/${repositoryName}`; + const workflowRef = `${workflowRepository}/.github/workflows/noema-review.yml@refs/heads/main`; + const now = Math.floor(Date.now() / 1000); + const token = await signedJwt({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository: "ContextualWisdomLab/noema", + job_workflow_ref: workflowRef, + job_workflow_sha: workflowSha, + sub: "repo:ContextualWisdomLab/noema:ref:refs/heads/main", + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }); + + 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 }); + }); + + 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.122", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + { + ...env, + ALLOWED_WORKFLOW_REPOSITORY: workflowRepository, + ALLOWED_WORKFLOW_REF_PREFIX: workflowRef, + ALLOWED_WORKFLOW_SHA: workflowSha, + }, + ); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_WORKFLOW_NOT_ALLOWED", + message: "Workflow source trust configuration unavailable", + details: { + match_policy: "exact-ref-and-source-sha", + }, + }); + }, + ); + it.each([ [ "omits an immutable ref delimiter", From fa2814b8b126650e2644bfa478060fe61421b04e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 16:37:02 -0700 Subject: [PATCH 050/564] fix(oidc): reject invalid workflow repository at exchange --- src/worker.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/worker.ts b/src/worker.ts index e4b047400..521228ca8 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -88,7 +88,19 @@ function decodeOidcWorkflowClaims(request: Request): OidcWorkflowClaims | undefi } } +function isTrustedWorkflowRepository(value: string, owner: string): boolean { + const prefix = `${owner}/`; + if (!value.startsWith(prefix)) return false; + const repositoryName = value.slice(prefix.length); + return repositoryName !== "." + && repositoryName !== ".." + && /^[A-Za-z0-9_.-]{1,100}$/.test(repositoryName); +} + function configuredExactWorkflowRef(env: Env): string | undefined { + if (!isTrustedWorkflowRepository(env.ALLOWED_WORKFLOW_REPOSITORY, env.ALLOWED_REPOSITORY_OWNER)) { + return undefined; + } const candidate = env.ALLOWED_WORKFLOW_REF_PREFIX; const repositoryPrefix = `${env.ALLOWED_WORKFLOW_REPOSITORY}/.github/workflows/`; if (!candidate || !candidate.startsWith(repositoryPrefix)) return undefined; From 3e3180074aaf794355086e1fffb9f0a49ff936cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 16:38:41 -0700 Subject: [PATCH 051/564] test(oidc): keep repository validity at exchange boundary --- test/oidc-workflow-ref-exact-match.test.ts | 65 ---------------------- 1 file changed, 65 deletions(-) diff --git a/test/oidc-workflow-ref-exact-match.test.ts b/test/oidc-workflow-ref-exact-match.test.ts index 37e7fad9e..f9e1aac7a 100644 --- a/test/oidc-workflow-ref-exact-match.test.ts +++ b/test/oidc-workflow-ref-exact-match.test.ts @@ -115,71 +115,6 @@ describe("cryptographic OIDC workflow identity", () => { }); }); - it.each([".", ".."]) ( - "fails closed when authoritative workflow repository name is %s", - async (repositoryName) => { - const workflowSha = "a".repeat(40); - const workflowRepository = `ContextualWisdomLab/${repositoryName}`; - const workflowRef = `${workflowRepository}/.github/workflows/noema-review.yml@refs/heads/main`; - const now = Math.floor(Date.now() / 1000); - const token = await signedJwt({ - iss: env.ALLOWED_ISSUER, - aud: env.ALLOWED_AUDIENCE, - repository_owner: env.ALLOWED_REPOSITORY_OWNER, - repository: "ContextualWisdomLab/noema", - job_workflow_ref: workflowRef, - job_workflow_sha: workflowSha, - sub: "repo:ContextualWisdomLab/noema:ref:refs/heads/main", - exp: now + 300, - nbf: now - 30, - iat: now - 30, - }); - - 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 }); - }); - - 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.122", - }, - body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), - }), - { - ...env, - ALLOWED_WORKFLOW_REPOSITORY: workflowRepository, - ALLOWED_WORKFLOW_REF_PREFIX: workflowRef, - ALLOWED_WORKFLOW_SHA: workflowSha, - }, - ); - - expect(response.status).toBe(503); - await expect(response.json()).resolves.toMatchObject({ - ok: false, - error_code: "ERR_WORKFLOW_NOT_ALLOWED", - message: "Workflow source trust configuration unavailable", - details: { - match_policy: "exact-ref-and-source-sha", - }, - }); - }, - ); - it.each([ [ "omits an immutable ref delimiter", From 4dd75ccb2f26ca2a7a4a734fe87bee9824ce9af2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 16:42:46 -0700 Subject: [PATCH 052/564] test(oidc): cover workflow repository authority branches --- test/worker-workflow-ref-canonicality.test.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/test/worker-workflow-ref-canonicality.test.ts b/test/worker-workflow-ref-canonicality.test.ts index 9f1d7ee19..0bcd3288b 100644 --- a/test/worker-workflow-ref-canonicality.test.ts +++ b/test/worker-workflow-ref-canonicality.test.ts @@ -69,8 +69,7 @@ async function exchangeFromWorkflowRef(workflowRef: string): Promise { ); } -async function exchangeFromWorkflowRepository(repositoryName: string): Promise { - const workflowRepository = `ContextualWisdomLab/${repositoryName}`; +async function exchangeFromWorkflowRepository(workflowRepository: string): Promise { const workflowRef = `${workflowRepository}/.github/workflows/noema-review.yml@refs/heads/main`; const env = workflowEnvironment(workflowRef); env.ALLOWED_WORKFLOW_REPOSITORY = workflowRepository; @@ -109,12 +108,17 @@ describe("wrapper workflow-ref canonical authority", () => { }); }); - it.each([".", ".."]) ( - "rejects invalid workflow repository name %s before replay or base exchange", - async (repositoryName) => { + it.each([ + "ContextualWisdomLab/.", + "ContextualWisdomLab/..", + "ContextualWisdomLab/bad/name", + "OtherOrg/.github", + ])( + "rejects invalid workflow repository authority %s before replay or base exchange", + async (workflowRepository) => { vi.spyOn(console, "log").mockImplementation(() => undefined); - const response = await exchangeFromWorkflowRepository(repositoryName); + const response = await exchangeFromWorkflowRepository(workflowRepository); expect(response.status).toBe(503); await expect(response.json()).resolves.toMatchObject({ From 40a923c6ad267586cb1d9780284b92fadaada2d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 19:12:10 -0700 Subject: [PATCH 053/564] 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 50352d25b..b4f1d985d 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 = - "613a33e0cb1c6db9790fae99f6253445712ac37a"; + "8fd471a31399a914d9cb22a840f4a4c68e010ea6"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 48f83501f07ebd4d07de0c33ae437a588fad3b65 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 19:12:50 -0700 Subject: [PATCH 054/564] fix(oidc): roll trusted workflow source to current central main --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index b55856503..2ceee753b 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 = "613a33e0cb1c6db9790fae99f6253445712ac37a" +ALLOWED_WORKFLOW_SHA = "8fd471a31399a914d9cb22a840f4a4c68e010ea6" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 0404abd11537d60c8529ab7971950a2b8d98b85b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 19:13:36 -0700 Subject: [PATCH 055/564] docs(architecture): align current central workflow source pin --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2625e0875..ee483082d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `613a33e0cb1c6db9790fae99f6253445712ac37a`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `8fd471a31399a914d9cb22a840f4a4c68e010ea6`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From ef36983633ebcc8ae64583f9a200cf03f3362f9d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 02:59:23 -0700 Subject: [PATCH 056/564] test(oidc): reject invalid reusable workflow file authority --- test/worker-workflow-ref-canonicality.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/worker-workflow-ref-canonicality.test.ts b/test/worker-workflow-ref-canonicality.test.ts index 0bcd3288b..30be55402 100644 --- a/test/worker-workflow-ref-canonicality.test.ts +++ b/test/worker-workflow-ref-canonicality.test.ts @@ -108,6 +108,23 @@ describe("wrapper workflow-ref canonical authority", () => { }); }); + it.each([ + "ContextualWisdomLab/.github/.github/workflows/nested/noema-review.yml@refs/heads/main", + "ContextualWisdomLab/.github/.github/workflows/noema-review.yaml/extra@refs/heads/main", + "ContextualWisdomLab/.github/.github/workflows/noema-review.txt@refs/heads/main", + ])("rejects invalid reusable workflow file authority %s before replay or base exchange", async (workflowRef) => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + + const response = await exchangeFromWorkflowRef(workflowRef); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_WORKFLOW_NOT_ALLOWED", + message: "Workflow trust configuration unavailable", + }); + }); + it.each([ "ContextualWisdomLab/.", "ContextualWisdomLab/..", From b3f5b8abccd16eebc4e47b4c7a70fca0a3ee4c5b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 03:01:06 -0700 Subject: [PATCH 057/564] fix(oidc): validate exact reusable workflow file authority --- src/worker.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/worker.ts b/src/worker.ts index 521228ca8..df4b0e52d 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -107,6 +107,9 @@ function configuredExactWorkflowRef(env: Env): string | undefined { const workflowAndRef = candidate.slice(repositoryPrefix.length); const separatorIndex = workflowAndRef.indexOf("@"); + const workflowFile = separatorIndex >= 0 + ? workflowAndRef.slice(0, separatorIndex) + : ""; const refName = separatorIndex >= 0 ? workflowAndRef.slice(separatorIndex + 1) : ""; @@ -114,6 +117,7 @@ function configuredExactWorkflowRef(env: Env): string | undefined { separatorIndex <= 0 || separatorIndex !== workflowAndRef.lastIndexOf("@") || separatorIndex === workflowAndRef.length - 1 + || !/^[A-Za-z0-9_.-]{1,100}\.ya?ml$/.test(workflowFile) || /[\s*?,]/.test(candidate) || (anyCaseCommitRefPattern.test(refName) && !canonicalCommitRefPattern.test(refName)) || (!canonicalCommitRefPattern.test(refName) && !trustedNamedRefPattern.test(refName)) @@ -470,4 +474,4 @@ export default { headers, }), decision); }, -}; +}; \ No newline at end of file From 1e2544b31f9f2092e3e56f3e69608961248b1573 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 03:01:57 -0700 Subject: [PATCH 058/564] fix(oidc): restore canonical worker source bytes --- src/worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/worker.ts b/src/worker.ts index df4b0e52d..7733b46fd 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -474,4 +474,4 @@ export default { headers, }), decision); }, -}; \ No newline at end of file +}; From daebf3803387f7b764107e53fc1e3ceab95f875f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 05:10:29 -0700 Subject: [PATCH 059/564] test(oidc): require current central workflow source --- test/trusted-workflow-source-rollforward.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/trusted-workflow-source-rollforward.test.ts b/test/trusted-workflow-source-rollforward.test.ts index b4f1d985d..0bce0c4c6 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 = - "8fd471a31399a914d9cb22a840f4a4c68e010ea6"; + "d2c554dbbc04854db6215970fabb70cef1ceb690"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { @@ -12,4 +12,4 @@ describe("trusted central workflow source revision", () => { `ALLOWED_WORKFLOW_SHA = "${auditedCentralWorkflowSourceSha}"`, ); }); -}); \ No newline at end of file +}); From 7b6354e1ea9d22fc13bf76b013e1645f7aa7475a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 05:10:56 -0700 Subject: [PATCH 060/564] fix(oidc): roll trust pin to current central source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 2ceee753b..02e530d22 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 = "8fd471a31399a914d9cb22a840f4a4c68e010ea6" +ALLOWED_WORKFLOW_SHA = "d2c554dbbc04854db6215970fabb70cef1ceb690" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 491e11fa8bbb15d8fcb854a69d02aa3d8966cbe6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 05:11:54 -0700 Subject: [PATCH 061/564] docs(architecture): align current central trust source --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ee483082d..1577b119a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `8fd471a31399a914d9cb22a840f4a4c68e010ea6`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `d2c554dbbc04854db6215970fabb70cef1ceb690`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 707fdb345a6a827ae7a4f929c10c4448f912f10c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 06:08:11 -0700 Subject: [PATCH 062/564] test(oidc): reject invalid workflow owner authority --- test/worker-workflow-ref-canonicality.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/worker-workflow-ref-canonicality.test.ts b/test/worker-workflow-ref-canonicality.test.ts index 30be55402..89535e9ab 100644 --- a/test/worker-workflow-ref-canonicality.test.ts +++ b/test/worker-workflow-ref-canonicality.test.ts @@ -88,6 +88,27 @@ async function exchangeFromWorkflowRepository(workflowRepository: string): Promi ); } +async function exchangeFromWorkflowOwner(workflowOwner: string): Promise { + const workflowRepository = `${workflowOwner}/.github`; + const workflowRef = `${workflowRepository}/.github/workflows/noema-review.yml@refs/heads/main`; + const env = workflowEnvironment(workflowRef); + env.ALLOWED_REPOSITORY_OWNER = workflowOwner; + env.ALLOWED_WORKFLOW_REPOSITORY = workflowRepository; + + return worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${tokenWithWorkflowRef(workflowRef)}`, + "cf-connecting-ip": "203.0.113.80", + "content-type": "application/json", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + env, + ); +} + describe("wrapper workflow-ref canonical authority", () => { afterEach(() => { vi.restoreAllMocks(); @@ -146,6 +167,27 @@ describe("wrapper workflow-ref canonical authority", () => { }, ); + it.each([ + "ContextualWisdomLab/extra", + "-ContextualWisdomLab", + "ContextualWisdomLab-", + "a".repeat(40), + ])( + "rejects invalid workflow owner authority %s before replay or base exchange", + async (workflowOwner) => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + + const response = await exchangeFromWorkflowOwner(workflowOwner); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_WORKFLOW_NOT_ALLOWED", + message: "Workflow trust configuration unavailable", + }); + }, + ); + it.each([ "refs/heads/release..candidate", "refs/heads/release//candidate", From d3df15cce33adb7f6a6d0460c8b927fdfc29c158 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 06:11:29 -0700 Subject: [PATCH 063/564] fix(oidc): reject invalid workflow owner authority --- src/worker.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/worker.ts b/src/worker.ts index 7733b46fd..386e6d876 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -25,6 +25,7 @@ export interface Env extends BaseEnv, DistributedRateLimitEnv, OidcReplayProtect const trustedTracePattern = /^[A-Za-z0-9._:-]+$/; const trustedJtiPattern = /^[A-Za-z0-9._:-]+$/; +const trustedOwnerPattern = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/; const canonicalCommitRefPattern = /^[0-9a-f]{40}$/; const anyCaseCommitRefPattern = /^[0-9A-Fa-f]{40}$/; const trustedNamedRefPattern = /^refs\/(?:heads|tags)\/(?=.{1,1024}$)(?!\.)(?![^/]*\.lock(?:\/|$))(?!.*\/\.)(?!.*\/[^/]*\.lock(?:\/|$))(?!.*(?:\.\.|\/\/|@\{|\\|[\x00-\x20\x7f~^:?*\[]))(?!.*[\/.]$)[A-Za-z0-9._/-]+$/; @@ -89,6 +90,7 @@ function decodeOidcWorkflowClaims(request: Request): OidcWorkflowClaims | undefi } function isTrustedWorkflowRepository(value: string, owner: string): boolean { + if (!trustedOwnerPattern.test(owner)) return false; const prefix = `${owner}/`; if (!value.startsWith(prefix)) return false; const repositoryName = value.slice(prefix.length); From 591a2cd774e635714151b3ef8ca312790cd53ecc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 06:34:37 -0700 Subject: [PATCH 064/564] test(oidc): require immutable owner and known repository ids --- test/oidc-repository-owner-id-binding.test.ts | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/test/oidc-repository-owner-id-binding.test.ts b/test/oidc-repository-owner-id-binding.test.ts index 2d1d3c151..6a966611e 100644 --- a/test/oidc-repository-owner-id-binding.test.ts +++ b/test/oidc-repository-owner-id-binding.test.ts @@ -44,9 +44,9 @@ beforeAll(async () => { afterEach(() => vi.restoreAllMocks()); async function signedOidcToken( - repositoryOwnerId: string, + repositoryOwnerId: string | undefined, repository = "ContextualWisdomLab/noema", - repositoryId = expectedNoemaRepositoryId, + repositoryId: string | undefined = expectedNoemaRepositoryId, ) { const kid = `github-owner-id-${crypto.randomUUID()}`; const now = Math.floor(Date.now() / 1000); @@ -112,7 +112,7 @@ async function exerciseToken(token: string, jwk: JsonWebKey, clientIp: string) { async function expectRepositoryIdentityRejection( repository: string, - repositoryId: string, + repositoryId: string | undefined, clientIp: string, ) { const { token, jwk } = await signedOidcToken(expectedRepositoryOwnerId, repository, repositoryId); @@ -139,10 +139,26 @@ describe("OIDC immutable repository identity", () => { expect(githubAppEgressCount).toBe(0); }); + it("rejects a signed same-name owner when the immutable GitHub owner id claim is missing", async () => { + const { token, jwk } = await signedOidcToken(undefined); + const { response, githubAppEgressCount } = await exerciseToken(token, jwk, "203.0.113.249"); + 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("rejects a same-name Noema repository carrying a different immutable repository id", async () => { await expectRepositoryIdentityRejection("ContextualWisdomLab/noema", "1", "203.0.113.252"); }); + it("rejects a known Noema repository when the immutable repository id claim is missing", async () => { + await expectRepositoryIdentityRejection("ContextualWisdomLab/noema", undefined, "203.0.113.248"); + }); + it("rejects a same-name central workflow repository carrying a different immutable repository id", async () => { await expectRepositoryIdentityRejection("ContextualWisdomLab/.github", "1", "203.0.113.254"); }); From bf9a67795bf17151fb057658f0a938472cead308 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 06:42:23 -0700 Subject: [PATCH 065/564] fix(oidc): require immutable repository identity claims --- src/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 80b388549..55c7edc49 100644 --- a/src/index.ts +++ b/src/index.ts @@ -446,16 +446,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 - ) { + if (payload.repository_owner_id !== expectedRepositoryOwnerId) { 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 ( expectedRepositoryId !== undefined - && payload.repository_id !== undefined && payload.repository_id !== expectedRepositoryId ) { throw new ApiError("ERR_REPO_NOT_ALLOWED", 403, "OIDC repository identity is not allowed"); From fa483066c426dd2afa1c66efe54d1976b8569a07 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 07:09:09 -0700 Subject: [PATCH 066/564] test(oidc): preserve immutable identity in trust fixtures --- test/oidc-workflow-sha-canonical-config.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/oidc-workflow-sha-canonical-config.test.ts b/test/oidc-workflow-sha-canonical-config.test.ts index 96945419a..ac356dd6e 100644 --- a/test/oidc-workflow-sha-canonical-config.test.ts +++ b/test/oidc-workflow-sha-canonical-config.test.ts @@ -4,6 +4,8 @@ import type { Env } from "../src/runtime-entrypoint"; const configuredWorkflowRef = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; const configuredWorkflowSha = "a".repeat(40); +const expectedRepositoryOwnerId = "295022177"; +const expectedWorkflowRepositoryId = "1274066402"; const signingKid = "oidc-workflow-sha-canonical-config"; const trustedDiscoveryUrl = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"; @@ -112,7 +114,9 @@ async function exchangeWithTrustConfig( iss: baseEnv.ALLOWED_ISSUER, aud: baseEnv.ALLOWED_AUDIENCE, repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", exp: now + 300, nbf: now - 30, From 7045092565458ca364f903ed396bcaa1b38c3dd6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 07:10:28 -0700 Subject: [PATCH 067/564] test(readiness): reject unpinned repository owner authority --- test/runtime-readiness-ref-format.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/runtime-readiness-ref-format.test.ts b/test/runtime-readiness-ref-format.test.ts index ecb666abb..7ba4302dd 100644 --- a/test/runtime-readiness-ref-format.test.ts +++ b/test/runtime-readiness-ref-format.test.ts @@ -53,6 +53,19 @@ async function readyEnvironment(): Promise { } describe("runtime-readiness exact Git ref validation", () => { + it("rejects a syntactically valid repository owner that cannot match the pinned immutable owner id", async () => { + const env = await readyEnvironment(); + env.ALLOWED_REPOSITORY_OWNER = "OtherOrg"; + env.ALLOWED_WORKFLOW_REPOSITORY = "OtherOrg/.github"; + env.ALLOWED_WORKFLOW_REF_PREFIX = + "OtherOrg/.github/.github/workflows/noema-review.yml@refs/heads/main"; + + const result = await evaluateRuntimeReadiness(env); + + expect(result.ready).toBe(false); + expect(result.failedChecks).toContain("allowed_repository_owner"); + }); + it.each([".", ".."])("rejects invalid workflow repository name %s", async (repositoryName) => { const env = await readyEnvironment(); env.ALLOWED_WORKFLOW_REPOSITORY = `ContextualWisdomLab/${repositoryName}`; @@ -115,4 +128,4 @@ describe("runtime-readiness exact Git ref validation", () => { expect(result.failedChecks).not.toContain("allowed_workflow_ref"); expect(result.failedChecks).not.toContain("allowed_workflow_sha"); }); -}); +}); \ No newline at end of file From b85a34dd4a8c2978452578e9a2698e3691f40db1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 07:12:15 -0700 Subject: [PATCH 068/564] fix(readiness): bind repository owner to immutable identity --- src/runtime-readiness.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/runtime-readiness.ts b/src/runtime-readiness.ts index affa84084..d87071865 100644 --- a/src/runtime-readiness.ts +++ b/src/runtime-readiness.ts @@ -2,6 +2,7 @@ import { isTrustedGithubApiBase } from "./entrypoint"; const trustedAudiencePattern = /^[A-Za-z0-9._:-]{1,128}$/; const trustedOwnerPattern = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/; +const expectedRepositoryOwner = "ContextualWisdomLab"; const positiveDecimalPattern = /^[1-9][0-9]*$/; const privateKeyPattern = /^-----BEGIN PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END PRIVATE KEY-----$/; const exactCommitPattern = /^[0-9a-f]{40}$/; @@ -190,7 +191,7 @@ export async function evaluateRuntimeReadiness( if (!trustedAudiencePattern.test(env.ALLOWED_AUDIENCE ?? "")) { failedChecks.push("allowed_audience"); } - if (!trustedOwnerPattern.test(owner)) { + if (owner !== expectedRepositoryOwner || !trustedOwnerPattern.test(owner)) { failedChecks.push("allowed_repository_owner"); } if (!isTrustedWorkflowRepository(workflowRepository, owner)) { From 6089e9ee620aab8809dbb409142fe5760bc6a202 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:05:23 -0700 Subject: [PATCH 069/564] test(oidc): preserve immutable identity in target authorization fixtures --- test/replay-target-authorization-coverage.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/replay-target-authorization-coverage.test.ts b/test/replay-target-authorization-coverage.test.ts index 031210428..a1051a064 100644 --- a/test/replay-target-authorization-coverage.test.ts +++ b/test/replay-target-authorization-coverage.test.ts @@ -4,6 +4,9 @@ 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"; +const expectedNoemaRepositoryId = "1285107801"; +const expectedWorkflowRepositoryId = "1274066402"; const env: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -35,12 +38,20 @@ async function createToken(repository: string | undefined) { ); const kid = `target-auth-${crypto.randomUUID()}`; const now = Math.floor(Date.now() / 1000); + const repositoryId = + repository === "ContextualWisdomLab/.github" + ? expectedWorkflowRepositoryId + : repository === "ContextualWisdomLab/noema" + ? expectedNoemaRepositoryId + : undefined; const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); const body = encodeSegment({ iss: env.ALLOWED_ISSUER, aud: env.ALLOWED_AUDIENCE, repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository, + ...(repositoryId ? { repository_id: repositoryId } : {}), job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, exp: now + 300, From bbd5eef585b4e86757eb8d51f4164cfbfc1eb1d8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:06:13 -0700 Subject: [PATCH 070/564] test(oidc): preserve immutable identity in replay fixtures --- test/replay-request-core-coverage.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/replay-request-core-coverage.test.ts b/test/replay-request-core-coverage.test.ts index f60bd2c15..990ad29e1 100644 --- a/test/replay-request-core-coverage.test.ts +++ b/test/replay-request-core-coverage.test.ts @@ -16,6 +16,8 @@ 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"; +const expectedWorkflowRepositoryId = "1274066402"; const baseEnv: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -84,7 +86,9 @@ async function validOidcToken(overrides: Record = {}) { iss: baseEnv.ALLOWED_ISSUER, aud: baseEnv.ALLOWED_AUDIENCE, repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", From 0f73946f5b2f3a0c691af91c6a2ac00cb38c0dbf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:08:29 -0700 Subject: [PATCH 071/564] test(oidc): require current central workflow source commit --- 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 0bce0c4c6..20261206e 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 = - "d2c554dbbc04854db6215970fabb70cef1ceb690"; + "95b05b393c75931044b405ae491079d336d89293"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From eca264150baed388feafa1a7075cdcd2f7f34a63 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:08:48 -0700 Subject: [PATCH 072/564] fix(oidc): roll trust pin to current central workflow source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 02e530d22..fd4831a04 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 = "d2c554dbbc04854db6215970fabb70cef1ceb690" +ALLOWED_WORKFLOW_SHA = "95b05b393c75931044b405ae491079d336d89293" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 2b8f4b3ca6ebc8846926d2cd5074a9d75ce1bf5f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:09:29 -0700 Subject: [PATCH 073/564] test(oidc): bind immutable identity in exchange success fixtures --- test/exchange-success-path-coverage.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/exchange-success-path-coverage.test.ts b/test/exchange-success-path-coverage.test.ts index 383280f97..1349fd43a 100644 --- a/test/exchange-success-path-coverage.test.ts +++ b/test/exchange-success-path-coverage.test.ts @@ -4,6 +4,8 @@ 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"; +const expectedWorkflowRepositoryId = "1274066402"; const env: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -74,7 +76,9 @@ describe("exchange success-path coverage through the public worker", () => { iss: env.ALLOWED_ISSUER, aud: env.ALLOWED_AUDIENCE, repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, workflow_ref: configuredRef, workflow_sha: configuredWorkflowSha, exp: now + 300, From d737c0a972c2cb55bce161ec633b412958deef69 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:10:11 -0700 Subject: [PATCH 074/564] test(oidc): preserve immutable identity in JWKS cache fixtures --- test/oidc-jwks-cache-expiry.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/oidc-jwks-cache-expiry.test.ts b/test/oidc-jwks-cache-expiry.test.ts index 64697a6a5..20b5e63ba 100644 --- a/test/oidc-jwks-cache-expiry.test.ts +++ b/test/oidc-jwks-cache-expiry.test.ts @@ -4,6 +4,8 @@ import type { Env } from "../src/index"; const configuredWorkflowRef = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; const configuredWorkflowSha = "a".repeat(40); +const expectedRepositoryOwnerId = "295022177"; +const expectedWorkflowRepositoryId = "1274066402"; const env: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -44,7 +46,9 @@ async function createSignedJwt(nowEpochSeconds: number) { iss: env.ALLOWED_ISSUER, aud: env.ALLOWED_AUDIENCE, repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, job_workflow_ref: configuredWorkflowRef, job_workflow_sha: configuredWorkflowSha, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", From 2b1c86243d39f4e3e3233eb66f94dc4380047da2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:11:00 -0700 Subject: [PATCH 075/564] test(oidc): bind immutable identity in replay mint fixtures --- test/replay-before-token-mint.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/replay-before-token-mint.test.ts b/test/replay-before-token-mint.test.ts index a2c8f0c75..5c692bdc8 100644 --- a/test/replay-before-token-mint.test.ts +++ b/test/replay-before-token-mint.test.ts @@ -4,6 +4,8 @@ import worker, { type Env } from "../src/worker"; const configuredRef = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; const configuredSha = "e71fdab2ab088001f218765ecb5e3b7fabfee11a"; +const expectedRepositoryOwnerId = "295022177"; +const expectedWorkflowRepositoryId = "1274066402"; type MockFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise; @@ -92,7 +94,9 @@ describe("verified OIDC replay claim ordering", () => { iss: "https://token.actions.githubusercontent.com", aud: "cwl-noema-review", repository_owner: "ContextualWisdomLab", + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, job_workflow_ref: configuredRef, job_workflow_sha: configuredSha, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", @@ -184,7 +188,9 @@ describe("verified OIDC replay claim ordering", () => { iss: "https://token.actions.githubusercontent.com", aud: "cwl-noema-review", repository_owner: "ContextualWisdomLab", + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, job_workflow_ref: configuredRef, job_workflow_sha: configuredSha, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", From b7f4a54b0b94bdac79f1d8989ce69e17ff222012 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:11:47 -0700 Subject: [PATCH 076/564] test(oidc): preserve immutable identity in operational fixtures --- test/operational-helper-coverage.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/operational-helper-coverage.test.ts b/test/operational-helper-coverage.test.ts index 0fd4af007..0f3b1159c 100644 --- a/test/operational-helper-coverage.test.ts +++ b/test/operational-helper-coverage.test.ts @@ -2,6 +2,8 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import worker, { type Env } from "../src/index"; const configuredWorkflowSha = "a".repeat(40); +const expectedRepositoryOwnerId = "295022177"; +const expectedWorkflowRepositoryId = "1274066402"; const baseEnv: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -128,7 +130,9 @@ describe("operational helper coverage", () => { iss: baseEnv.ALLOWED_ISSUER, aud: baseEnv.ALLOWED_AUDIENCE, repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, job_workflow_ref: "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main", job_workflow_sha: configuredWorkflowSha, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", From adad79cdd8742952f83a36ef4bdb7d1bc36dd31e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:12:48 -0700 Subject: [PATCH 077/564] test(oidc): preserve immutable identity in request helper fixtures --- test/credential-request-helper-coverage.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/credential-request-helper-coverage.test.ts b/test/credential-request-helper-coverage.test.ts index ff31422ca..f9f1bfda2 100644 --- a/test/credential-request-helper-coverage.test.ts +++ b/test/credential-request-helper-coverage.test.ts @@ -4,6 +4,9 @@ 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"; +const expectedNoemaRepositoryId = "1285107801"; +const expectedWorkflowRepositoryId = "1274066402"; const env: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -39,11 +42,19 @@ async function createSignedJwt(repository: string) { ); const kid = `credential-request-${crypto.randomUUID()}`; const now = Math.floor(Date.now() / 1000); + const repositoryId = + repository === "ContextualWisdomLab/.github" + ? expectedWorkflowRepositoryId + : repository === "ContextualWisdomLab/noema" + ? expectedNoemaRepositoryId + : undefined; const payload = { iss: env.ALLOWED_ISSUER, aud: env.ALLOWED_AUDIENCE, repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository, + ...(repositoryId ? { repository_id: repositoryId } : {}), job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", From 4dafabec370ad663c7bcb77318e0f519c7cb52d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:16:40 -0700 Subject: [PATCH 078/564] test(oidc): preserve immutable identity in worker fixtures --- test/worker.test.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/test/worker.test.ts b/test/worker.test.ts index 390a1e815..2f95eb051 100644 --- a/test/worker.test.ts +++ b/test/worker.test.ts @@ -45,7 +45,18 @@ async function createSignedJwt(payload: Record) { ); const kid = `oidc-test-key-${crypto.randomUUID()}`; const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); - const body = encodeSegment(payload); + const repository = typeof payload.repository === "string" ? payload.repository : undefined; + const repositoryId = + repository === "ContextualWisdomLab/.github" + ? "1274066402" + : repository === "ContextualWisdomLab/noema" + ? "1285107801" + : undefined; + const body = encodeSegment({ + repository_owner_id: "295022177", + ...(repositoryId ? { repository_id: repositoryId } : {}), + ...payload, + }); const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", keyPair.privateKey, new TextEncoder().encode(`${header}.${body}`)); const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey); return { From 8e7d7c6361b5ba3d82424e3302bbdad6bf4892f9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:18:19 -0700 Subject: [PATCH 079/564] docs(architecture): align central workflow trust revision --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1577b119a..d95d413b6 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `d2c554dbbc04854db6215970fabb70cef1ceb690`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `95b05b393c75931044b405ae491079d336d89293`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 538b095eac0e90cc81667a9f89b7a5dd19fb3162 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:19:12 -0700 Subject: [PATCH 080/564] test(oidc): preserve immutable identity in app-id fixtures --- test/github-app-id-validation.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/github-app-id-validation.test.ts b/test/github-app-id-validation.test.ts index fd29f7513..72855e9da 100644 --- a/test/github-app-id-validation.test.ts +++ b/test/github-app-id-validation.test.ts @@ -4,6 +4,8 @@ import entrypoint, { type Env } from "../src/entrypoint"; const configuredRef = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; const configuredWorkflowSha = "a".repeat(40); +const expectedRepositoryOwnerId = "295022177"; +const expectedWorkflowRepositoryId = "1274066402"; const baseEnv: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -71,7 +73,9 @@ async function signedOidcToken() { iss: baseEnv.ALLOWED_ISSUER, aud: baseEnv.ALLOWED_AUDIENCE, repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", From 11537da4e4aabdb109ffdf6539d559bbe9611e1a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:20:01 -0700 Subject: [PATCH 081/564] test(oidc): preserve immutable identity in installation fixtures --- test/github-app-explicit-installation-id-validation.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/github-app-explicit-installation-id-validation.test.ts b/test/github-app-explicit-installation-id-validation.test.ts index cc0095cb6..373e64afc 100644 --- a/test/github-app-explicit-installation-id-validation.test.ts +++ b/test/github-app-explicit-installation-id-validation.test.ts @@ -4,6 +4,8 @@ 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"; +const expectedWorkflowRepositoryId = "1274066402"; const baseEnv: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -70,7 +72,9 @@ async function signedOidcToken() { iss: baseEnv.ALLOWED_ISSUER, aud: baseEnv.ALLOWED_AUDIENCE, repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", From 2a1b7246a20a0b19cbf9fca8a2940635fbe4496f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:22:29 -0700 Subject: [PATCH 082/564] test(oidc): preserve immutable identity in GitHub API fixtures --- test/github-api-malformed-json.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/github-api-malformed-json.test.ts b/test/github-api-malformed-json.test.ts index 09a0f3167..873dbedbc 100644 --- a/test/github-api-malformed-json.test.ts +++ b/test/github-api-malformed-json.test.ts @@ -4,6 +4,8 @@ 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"; +const expectedWorkflowRepositoryId = "1274066402"; const baseEnv: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -70,7 +72,9 @@ async function signedOidcToken() { iss: baseEnv.ALLOWED_ISSUER, aud: baseEnv.ALLOWED_AUDIENCE, repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, exp: now + 300, From 88d9185d00b539b683f0193e8bb08cc14b0ebf9d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 09:06:54 -0700 Subject: [PATCH 083/564] test(oidc): preserve immutable identity in workflow-ref fixtures --- test/oidc-workflow-ref-exact-match.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/oidc-workflow-ref-exact-match.test.ts b/test/oidc-workflow-ref-exact-match.test.ts index f9e1aac7a..8e809abcf 100644 --- a/test/oidc-workflow-ref-exact-match.test.ts +++ b/test/oidc-workflow-ref-exact-match.test.ts @@ -7,6 +7,8 @@ const trustedDiscoveryUrl = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"; const trustedJwksUrl = "https://token.actions.githubusercontent.com/.well-known/jwks"; const signingKid = "oidc-exact-workflow-ref"; +const expectedRepositoryOwnerId = "295022177"; +const expectedWorkflowRepositoryId = "1274066402"; const env: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -71,7 +73,9 @@ describe("cryptographic OIDC workflow identity", () => { iss: env.ALLOWED_ISSUER, aud: env.ALLOWED_AUDIENCE, repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, job_workflow_ref: `${configuredWorkflowRef}-attacker-controlled-suffix`, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", exp: now + 300, @@ -135,7 +139,9 @@ describe("cryptographic OIDC workflow identity", () => { iss: env.ALLOWED_ISSUER, aud: env.ALLOWED_AUDIENCE, repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, job_workflow_ref: malformedWorkflowRef, job_workflow_sha: workflowSha, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", From e928ae9ad1527e9f2d891a013fdbffbc5d23664c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 09:07:28 -0700 Subject: [PATCH 084/564] test(oidc): preserve immutable identity in source-authority fixtures --- test/oidc-workflow-sha-authoritative-boundary.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/oidc-workflow-sha-authoritative-boundary.test.ts b/test/oidc-workflow-sha-authoritative-boundary.test.ts index 95051bf19..1b4706690 100644 --- a/test/oidc-workflow-sha-authoritative-boundary.test.ts +++ b/test/oidc-workflow-sha-authoritative-boundary.test.ts @@ -7,6 +7,8 @@ const signingKid = "oidc-workflow-sha-authoritative-boundary"; const trustedDiscoveryUrl = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"; const trustedJwksUrl = "https://token.actions.githubusercontent.com/.well-known/jwks"; +const expectedRepositoryOwnerId = "295022177"; +const expectedWorkflowRepositoryId = "1274066402"; function allowingRateLimitNamespace(): DurableObjectNamespace { return { @@ -109,7 +111,9 @@ async function exchangeWithAuthoritativeConfig( iss: envWithoutWorkflowSha.ALLOWED_ISSUER, aud: envWithoutWorkflowSha.ALLOWED_AUDIENCE, repository_owner: envWithoutWorkflowSha.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", job_workflow_ref: workflowRef, job_workflow_sha: "a".repeat(40), From fe3df61dd36d3981793b420723e6166f623559fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 09:08:21 -0700 Subject: [PATCH 085/564] test(oidc): preserve immutable identity in source-binding fixtures --- test/oidc-workflow-sha-binding.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/oidc-workflow-sha-binding.test.ts b/test/oidc-workflow-sha-binding.test.ts index 92759d3d3..991b5fa63 100644 --- a/test/oidc-workflow-sha-binding.test.ts +++ b/test/oidc-workflow-sha-binding.test.ts @@ -8,6 +8,8 @@ const signingKid = "oidc-workflow-sha-binding"; const trustedDiscoveryUrl = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"; const trustedJwksUrl = "https://token.actions.githubusercontent.com/.well-known/jwks"; +const expectedRepositoryOwnerId = "295022177"; +const expectedWorkflowRepositoryId = "1274066402"; function allowingRateLimitNamespace(): DurableObjectNamespace { return { @@ -135,7 +137,9 @@ async function exchangeWithClaims(claims: Record): Promise): Promi iss: env.ALLOWED_ISSUER, aud: env.ALLOWED_AUDIENCE, repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", exp: now + 300, nbf: now - 30, From 62126b90ba8ccad46ca385ef0bccac198c7def0d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 09:08:50 -0700 Subject: [PATCH 086/564] test(oidc): preserve immutable identity in cryptographic fixture --- test/oidc-workflow-sha-cryptographic.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/oidc-workflow-sha-cryptographic.test.ts b/test/oidc-workflow-sha-cryptographic.test.ts index 7c4e24eca..59be08a5c 100644 --- a/test/oidc-workflow-sha-cryptographic.test.ts +++ b/test/oidc-workflow-sha-cryptographic.test.ts @@ -8,6 +8,8 @@ const trustedDiscoveryUrl = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"; const trustedJwksUrl = "https://token.actions.githubusercontent.com/.well-known/jwks"; const signingKid = "oidc-workflow-sha-cryptographic"; +const expectedRepositoryOwnerId = "295022177"; +const expectedWorkflowRepositoryId = "1274066402"; const env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -73,7 +75,9 @@ describe("cryptographic OIDC workflow source identity", () => { iss: env.ALLOWED_ISSUER, aud: env.ALLOWED_AUDIENCE, repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, job_workflow_ref: configuredWorkflowRef, job_workflow_sha: "b".repeat(40), sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", From c95af799f61dd76ba57e2910d77f372dde0b697a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 09:09:43 -0700 Subject: [PATCH 087/564] test(oidc): preserve immutable identity in residual verifier fixtures --- test/oidc-verification-residual-coverage.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/oidc-verification-residual-coverage.test.ts b/test/oidc-verification-residual-coverage.test.ts index 95b7540dc..c2edbfcc5 100644 --- a/test/oidc-verification-residual-coverage.test.ts +++ b/test/oidc-verification-residual-coverage.test.ts @@ -8,6 +8,8 @@ const trustedDiscoveryUrl = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"; const trustedJwksUrl = "https://token.actions.githubusercontent.com/.well-known/jwks"; const signingKid = "oidc-residual-coverage"; +const expectedRepositoryOwnerId = "295022177"; +const expectedWorkflowRepositoryId = "1274066402"; const env: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -38,7 +40,9 @@ function baseClaims(now = Math.floor(Date.now() / 1000)): Record Date: Tue, 25 Aug 2026 09:15:05 -0700 Subject: [PATCH 088/564] test(oidc): preserve immutable identity in NumericDate fixtures --- test/oidc-numeric-date-finite.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/oidc-numeric-date-finite.test.ts b/test/oidc-numeric-date-finite.test.ts index c4d9d748f..ec9834ebe 100644 --- a/test/oidc-numeric-date-finite.test.ts +++ b/test/oidc-numeric-date-finite.test.ts @@ -8,6 +8,8 @@ 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 expectedRepositoryOwnerId = "295022177"; +const expectedWorkflowRepositoryId = "1274066402"; const env: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -55,7 +57,9 @@ function rawClaimsWithNumericDate( iss: env.ALLOWED_ISSUER, aud: env.ALLOWED_AUDIENCE, repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, job_workflow_ref: configuredWorkflowRef, job_workflow_sha: configuredWorkflowSha, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", From 7d654df57444eccc3a12c111fa043f932326d730 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 09:16:20 -0700 Subject: [PATCH 089/564] test(oidc): preserve immutable identity in installation-expiry fixtures --- test/github-installation-expiry-defensive-coverage.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/github-installation-expiry-defensive-coverage.test.ts b/test/github-installation-expiry-defensive-coverage.test.ts index 19f34cc67..3e008fddd 100644 --- a/test/github-installation-expiry-defensive-coverage.test.ts +++ b/test/github-installation-expiry-defensive-coverage.test.ts @@ -3,6 +3,8 @@ 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"; +const expectedWorkflowRepositoryId = "1274066402"; let oidcKeyPair: CryptoKeyPair; let oidcPublicJwk: JsonWebKey; let appPrivateKeyPem: string; @@ -47,7 +49,7 @@ async function exchangeWithTokenResponse(tokenBody: unknown, clientIp: string): 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 payload = encodeSegment({ iss: baseEnv.ALLOWED_ISSUER, aud: baseEnv.ALLOWED_AUDIENCE, repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", repository_id: expectedWorkflowRepositoryId, 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); From 17c87374bca92639d087800bcce3639b80f5d67c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 09:17:42 -0700 Subject: [PATCH 090/564] test(oidc): preserve immutable identity in app runtime fixtures --- test/github-app-runtime-coverage.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/github-app-runtime-coverage.test.ts b/test/github-app-runtime-coverage.test.ts index 933a47fc1..7861ba410 100644 --- a/test/github-app-runtime-coverage.test.ts +++ b/test/github-app-runtime-coverage.test.ts @@ -4,6 +4,8 @@ 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"; +const expectedWorkflowRepositoryId = "1274066402"; const baseEnv: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -71,7 +73,9 @@ async function createOidcToken() { iss: baseEnv.ALLOWED_ISSUER, aud: baseEnv.ALLOWED_AUDIENCE, repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", From 871762575b18ebf227a2d7b652e6fa39c6c2d74d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 10:04:17 -0700 Subject: [PATCH 091/564] test(oidc): preserve immutable identity in expiry fixture --- ...ithub-installation-token-expiry-calendar-integrity.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/github-installation-token-expiry-calendar-integrity.test.ts b/test/github-installation-token-expiry-calendar-integrity.test.ts index c53f67149..1354249a0 100644 --- a/test/github-installation-token-expiry-calendar-integrity.test.ts +++ b/test/github-installation-token-expiry-calendar-integrity.test.ts @@ -4,6 +4,8 @@ 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"; +const expectedWorkflowRepositoryId = "1274066402"; const baseEnv: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -71,7 +73,9 @@ async function signedOidcToken(): Promise<{ token: string; jwk: JsonWebKey }> { iss: baseEnv.ALLOWED_ISSUER, aud: baseEnv.ALLOWED_AUDIENCE, repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, exp: now + 300, From 76ac6d23b8732cf6ad1e38b5c2b03b0494839f22 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 10:04:57 -0700 Subject: [PATCH 092/564] test(oidc): preserve explicit missing repository id --- test/oidc-repository-owner-id-binding.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/oidc-repository-owner-id-binding.test.ts b/test/oidc-repository-owner-id-binding.test.ts index 6a966611e..cc15ef549 100644 --- a/test/oidc-repository-owner-id-binding.test.ts +++ b/test/oidc-repository-owner-id-binding.test.ts @@ -46,8 +46,11 @@ afterEach(() => vi.restoreAllMocks()); async function signedOidcToken( repositoryOwnerId: string | undefined, repository = "ContextualWisdomLab/noema", - repositoryId: string | undefined = expectedNoemaRepositoryId, + ...repositoryIdArgs: [] | [string | undefined] ) { + const repositoryId = repositoryIdArgs.length === 0 + ? expectedNoemaRepositoryId + : repositoryIdArgs[0]; const kid = `github-owner-id-${crypto.randomUUID()}`; const now = Math.floor(Date.now() / 1000); const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); From d232171575f52b51f6329bc33296ccd5029e48e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 10:59:13 -0700 Subject: [PATCH 093/564] 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 20261206e..ab0846791 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 = - "95b05b393c75931044b405ae491079d336d89293"; + "564058ce103335763c42b5f49ba07a1d040d2ae3"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 01e969085fe738d6de047951c7b48849cdad5026 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 10:59:31 -0700 Subject: [PATCH 094/564] fix(oidc): roll forward central workflow trust pin --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index fd4831a04..69af95d94 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 = "95b05b393c75931044b405ae491079d336d89293" +ALLOWED_WORKFLOW_SHA = "564058ce103335763c42b5f49ba07a1d040d2ae3" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From d902d32320cf5904954799b283c29a5bc009c565 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 11:00:33 -0700 Subject: [PATCH 095/564] docs(architecture): align central workflow trust source --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d95d413b6..ffe8168ca 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `95b05b393c75931044b405ae491079d336d89293`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `564058ce103335763c42b5f49ba07a1d040d2ae3`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From fd3b25c45dcb4c715b670cfb8183f6490500aa3b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 12:04:46 -0700 Subject: [PATCH 096/564] test(oidc): bind trust pin to current central 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 ab0846791..8ed806eb6 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 = - "564058ce103335763c42b5f49ba07a1d040d2ae3"; + "e3b7ece44ba8e891e4c948e9b5b75773f330cd0e"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 14b7a21ea68b7203ab5ca2634769b85b8d8def5f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 12:06:31 -0700 Subject: [PATCH 097/564] fix(oidc): roll trust pin to current central source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 69af95d94..b7f24c212 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 = "564058ce103335763c42b5f49ba07a1d040d2ae3" +ALLOWED_WORKFLOW_SHA = "e3b7ece44ba8e891e4c948e9b5b75773f330cd0e" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From c2a19bf25fb55574a1d7e89b9a84f37de930658b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 12:07:25 -0700 Subject: [PATCH 098/564] docs(architecture): align current central workflow trust source --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ffe8168ca..7e4620003 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `564058ce103335763c42b5f49ba07a1d040d2ae3`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `e3b7ece44ba8e891e4c948e9b5b75773f330cd0e`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From c4b020c24528f72a26c6c80ffb2b7a5dd4b8000a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 13:04:28 -0700 Subject: [PATCH 099/564] test(oidc): require current central workflow commit --- 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 8ed806eb6..7db29b9bf 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 = - "e3b7ece44ba8e891e4c948e9b5b75773f330cd0e"; + "fecceba0c2be660665253cc39c8bebde2b3f3259"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 865701c1d0d983c3f7419302715daa1a09dd7572 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 13:04:59 -0700 Subject: [PATCH 100/564] fix(oidc): roll workflow trust to current central commit --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index b7f24c212..a56808a3b 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 = "e3b7ece44ba8e891e4c948e9b5b75773f330cd0e" +ALLOWED_WORKFLOW_SHA = "fecceba0c2be660665253cc39c8bebde2b3f3259" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From b7aee4f780b10c1b03d9f31d7c056188ec6cdbb3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 13:05:41 -0700 Subject: [PATCH 101/564] docs(architecture): track current central workflow trust --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 7e4620003..f7a793013 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `e3b7ece44ba8e891e4c948e9b5b75773f330cd0e`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `fecceba0c2be660665253cc39c8bebde2b3f3259`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 0b8b1a3949e4accd892bb1b5c5df7d52d5688753 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 14:38:09 -0700 Subject: [PATCH 102/564] 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 7db29b9bf..e5180d638 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 = - "fecceba0c2be660665253cc39c8bebde2b3f3259"; + "33dc57d7984b937e4f5ab915d5eae69a0f42e3a5"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 94d74d83248e9a92361c440ac56f42f5805c7775 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 14:38:45 -0700 Subject: [PATCH 103/564] fix(oidc): track current central workflow source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index a56808a3b..12f640a9b 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 = "fecceba0c2be660665253cc39c8bebde2b3f3259" +ALLOWED_WORKFLOW_SHA = "33dc57d7984b937e4f5ab915d5eae69a0f42e3a5" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From b12a5a70d1e8840d9520ad61504e3b940cf827cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 14:39:28 -0700 Subject: [PATCH 104/564] docs(architecture): track current central workflow trust --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f7a793013..86af0f266 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `fecceba0c2be660665253cc39c8bebde2b3f3259`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `33dc57d7984b937e4f5ab915d5eae69a0f42e3a5`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From a62b86306a4bc3d46318a92b7d6d3a8f75900fd7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 15:03:30 -0700 Subject: [PATCH 105/564] test(security): reject control bytes in minted tokens --- test/github-api-malformed-json.test.ts | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/github-api-malformed-json.test.ts b/test/github-api-malformed-json.test.ts index 873dbedbc..36422905c 100644 --- a/test/github-api-malformed-json.test.ts +++ b/test/github-api-malformed-json.test.ts @@ -236,6 +236,33 @@ describe("GitHub API success-response parsing", () => { }); }); + it.each([ + ["ghs_line\nfeed", "203.0.113.249"], + ["ghs_carriage\rreturn", "203.0.113.250"], + ])("rejects installation token material containing control bytes", async (token, clientIp) => { + const response = await exchangeWith( + "ContextualWisdomLab/control-byte-installation-token", + { ...baseEnv, GITHUB_APP_INSTALLATION_ID: "92345" }, + (url) => { + if (url === "https://api.github.com/app/installations/92345/access_tokens") { + return Response.json({ + token, + expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), + }); + } + 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 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( From eabe4b11c1d59ef9e2331eb3cd7f7aef657d966a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 15:05:52 -0700 Subject: [PATCH 106/564] fix(security): reject control bytes in minted tokens --- src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 55c7edc49..d85cba4d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -655,7 +655,10 @@ async function createInstallationToken(repository: string, env: Env): Promise Date: Tue, 25 Aug 2026 16:07:06 -0700 Subject: [PATCH 107/564] test(security): reject whitespace in minted tokens --- test/github-api-malformed-json.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/github-api-malformed-json.test.ts b/test/github-api-malformed-json.test.ts index 36422905c..c200e2dd0 100644 --- a/test/github-api-malformed-json.test.ts +++ b/test/github-api-malformed-json.test.ts @@ -239,7 +239,10 @@ describe("GitHub API success-response parsing", () => { it.each([ ["ghs_line\nfeed", "203.0.113.249"], ["ghs_carriage\rreturn", "203.0.113.250"], - ])("rejects installation token material containing control bytes", async (token, clientIp) => { + ["ghs leading", "203.0.113.251"], + ["ghs_trailing ", "203.0.113.252"], + ["ghs_non\u00a0breaking", "203.0.113.253"], + ])("rejects installation token material containing non-canonical credential bytes", async (token, clientIp) => { const response = await exchangeWith( "ContextualWisdomLab/control-byte-installation-token", { ...baseEnv, GITHUB_APP_INSTALLATION_ID: "92345" }, From c5b13237f82cebfdfd941564cac007c94665b470 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 16:10:56 -0700 Subject: [PATCH 108/564] fix(security): require canonical minted token bytes --- src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index d85cba4d1..2813d5262 100644 --- a/src/index.ts +++ b/src/index.ts @@ -144,6 +144,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 githubInstallationTokenPattern = /^[\x21-\x7e]+$/; const expectedRepositoryOwnerId = "295022177"; const expectedRepositoryIds = new Map([ ["ContextualWisdomLab/noema", "1285107801"], @@ -657,7 +658,7 @@ async function createInstallationToken(repository: string, env: Env): Promise Date: Tue, 25 Aug 2026 17:03:00 -0700 Subject: [PATCH 109/564] test(oidc): reject non-canonical bearer separators --- test/oidc-bearer-whitespace-envelope.test.ts | 33 ++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/oidc-bearer-whitespace-envelope.test.ts b/test/oidc-bearer-whitespace-envelope.test.ts index dbef51bab..c891cce52 100644 --- a/test/oidc-bearer-whitespace-envelope.test.ts +++ b/test/oidc-bearer-whitespace-envelope.test.ts @@ -28,4 +28,37 @@ describe("OIDC bearer envelope whitespace boundary", () => { expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('"event":"oidc_token_envelope"')); expect(logSpy.mock.calls.flat().join("\n")).not.toContain(authorization); }); + + it("rejects non-canonical bearer separators before credential parsing", async () => { + const nonCanonicalAuthorizations = [ + "Bearer\tone.two.three", + "Bearer\u00a0one.two.three", + ]; + + for (const authorization of nonCanonicalAuthorizations) { + expect(isBoundedOidcBearer(authorization)).toBe(false); + } + + const authorization = "Bearer\u00a0one.two.three"; + 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-noncanonical-separator", + }, + }), + { 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-noncanonical-separator", + }); + expect(logSpy.mock.calls.flat().join("\n")).not.toContain(authorization); + }); }); From c0ea7b2c3df0bcb296ff72423fe51ab0927a36c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 17:05:27 -0700 Subject: [PATCH 110/564] fix(oidc): require canonical bearer separator --- src/entrypoint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/entrypoint.ts b/src/entrypoint.ts index dadc4abc9..7ff107161 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -101,7 +101,7 @@ export function isBoundedOidcBearer(value: string | null): boolean { 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); + const match = value.match(/^Bearer (\S+)$/i); if (!match) return false; const segments = match[1].split("."); From bd1ea43ab20f757b50d2a3ebc9c1d14425e8e04b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 18:01:30 -0700 Subject: [PATCH 111/564] test(oidc): bind trust to 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 e5180d638..2eba8633e 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 = - "33dc57d7984b937e4f5ab915d5eae69a0f42e3a5"; + "826b92394c63deb6981c3a8d16a724d71f85a0d7"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 5096cab039296e85a0ad7a983659acde734efdd6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 18:01:55 -0700 Subject: [PATCH 112/564] fix(oidc): roll forward trusted central workflow source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 12f640a9b..28d01db83 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 = "33dc57d7984b937e4f5ab915d5eae69a0f42e3a5" +ALLOWED_WORKFLOW_SHA = "826b92394c63deb6981c3a8d16a724d71f85a0d7" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 97a592f6c379215f2fc8a98e037d472c16a5b9da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 18:02:32 -0700 Subject: [PATCH 113/564] docs(architecture): align central workflow trust source --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 86af0f266..6d41b30bb 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `33dc57d7984b937e4f5ab915d5eae69a0f42e3a5`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `826b92394c63deb6981c3a8d16a724d71f85a0d7`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From f4326362ded9c1eaa864fe9817026a6e252b5305 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 19:03:10 -0700 Subject: [PATCH 114/564] fix(source): restore canonical index newline --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 2813d5262..208e9b340 100644 --- a/src/index.ts +++ b/src/index.ts @@ -893,4 +893,4 @@ export default { return withOperationalHeaders(response, traceId, latency_ms); } }, -}; \ No newline at end of file +}; From a936bb56c64a8e54efaab71edde859699cd8eb0e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 19:10:47 -0700 Subject: [PATCH 115/564] test(auth): require exact bearer framing --- test/bearer-authorization.test.ts | 63 +++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 test/bearer-authorization.test.ts diff --git a/test/bearer-authorization.test.ts b/test/bearer-authorization.test.ts new file mode 100644 index 000000000..ae4ec0f29 --- /dev/null +++ b/test/bearer-authorization.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import baseWorker, { type Env } from "../src/index"; +import { parseExactBearerToken } from "../src/bearer-authorization"; + +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: "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: "unused", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", +}; + +describe("canonical OIDC bearer framing", () => { + it("accepts exactly one ASCII space and preserves the token bytes", () => { + expect(parseExactBearerToken("Bearer header.payload.signature")).toBe("header.payload.signature"); + expect(parseExactBearerToken("bearer header.payload.signature")).toBe("header.payload.signature"); + }); + + it.each([ + "Bearer\theader.payload.signature", + "Bearer\u00a0header.payload.signature", + "Bearer header.payload.signature", + "Bearer \theader.payload.signature", + "Bearer header.payload.signature ", + ])("rejects non-canonical bearer framing: %j", (authorization) => { + expect(parseExactBearerToken(authorization)).toBeUndefined(); + }); + + it.each([ + "Bearer\tmalformed", + "Bearer malformed", + ])("rejects non-canonical public exchange framing before JWT parsing: %j", async (authorization) => { + const response = await baseWorker.fetch(new Request("https://noema.example/exchange", { + method: "POST", + headers: { authorization }, + }), env); + + expect(response.status).toBe(401); + expect(response.headers.get("www-authenticate")).toBe('Bearer realm="noema", error="invalid_request"'); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_MISSING", + }); + }); + + it("keeps canonical malformed JWTs on the malformed-token boundary", async () => { + const response = await baseWorker.fetch(new Request("https://noema.example/exchange", { + method: "POST", + headers: { authorization: "Bearer malformed" }, + }), env); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + }); + }); +}); From d0968407c6e3b4d58fd0f970de7fe0231e981690 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 19:11:05 -0700 Subject: [PATCH 116/564] fix(auth): centralize exact bearer framing --- src/bearer-authorization.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/bearer-authorization.ts diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts new file mode 100644 index 000000000..0255688f6 --- /dev/null +++ b/src/bearer-authorization.ts @@ -0,0 +1,10 @@ +const canonicalBearerAuthorizationPattern = /^Bearer ([\x21-\x7e]+)$/i; + +/** + * Return the bearer credential only when the Authorization field is already in the + * canonical `Bearer ` form. The parser never trims or normalizes + * attacker-controlled credential framing. + */ +export function parseExactBearerToken(authorization: string): string | undefined { + return canonicalBearerAuthorizationPattern.exec(authorization)?.[1]; +} From 5a297a13eec6f2bb05403ceb561eaab639c1ae70 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 19:13:06 -0700 Subject: [PATCH 117/564] fix(auth): enforce exact bearer framing in base exchange --- src/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 208e9b340..abb2a9365 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { parseExactBearerToken } from "./bearer-authorization"; import { configuredTtlMs } from "./cache-ttl"; import { claimOidcTokenUsage, @@ -771,9 +772,9 @@ async function handleExchange(request: Request, env: Env, traceId: string): Prom throw new ApiError("ERR_VALIDATION_INPUT", 405, "Method not allowed", { allowed_methods: "POST" }); } const authorization = request.headers.get("authorization") || ""; - const match = authorization.match(/^Bearer\s+(.+)$/i); - if (!match) throw new ApiError("ERR_AUTH_MISSING", 401, "Missing bearer token"); - const claims = await verifyGithubOidcJwt(match[1], env); + const bearerToken = parseExactBearerToken(authorization); + if (!bearerToken) throw new ApiError("ERR_AUTH_MISSING", 401, "Missing bearer token"); + const claims = await verifyGithubOidcJwt(bearerToken, env); const oidc_sub = claims.sub ? safeHash(claims.sub).slice(0, 16) : undefined; const { repository, token, token_expires_at, replay_protected } = await createRepositoryInstallationToken(request, claims, env); const workflow_ref = claims.job_workflow_ref || claims.workflow_ref!; From e0c075a2965393839c1a3742cba9df30bfd1a253 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 19:13:56 -0700 Subject: [PATCH 118/564] fix(auth): enforce exact bearer framing in protected worker --- src/worker.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/worker.ts b/src/worker.ts index 386e6d876..232b31d12 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,4 +1,5 @@ import baseWorker, { type Env as BaseEnv } from "./index"; +import { parseExactBearerToken } from "./bearer-authorization"; import { claimOidcTokenUsage, NoemaOidcReplayGuard, @@ -66,10 +67,10 @@ function traceIdFromRequest(request: Request): string { function decodeOidcWorkflowClaims(request: Request): OidcWorkflowClaims | undefined { const authorization = request.headers.get("authorization") ?? ""; - const match = authorization.match(/^Bearer\s+(.+)$/i); - if (!match) return undefined; + const bearerToken = parseExactBearerToken(authorization); + if (!bearerToken) return undefined; - const parts = match[1].split("."); + const parts = bearerToken.split("."); if (parts.length !== 3 || parts[1].length > MAX_OIDC_PAYLOAD_SEGMENT_LENGTH) { return undefined; } From 2e619036eeb899bfb1a2329f7c8a21f1d09fd6e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 19:20:33 -0700 Subject: [PATCH 119/564] test(oidc): bound bearer credential bytes --- test/bearer-authorization.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/bearer-authorization.test.ts b/test/bearer-authorization.test.ts index ae4ec0f29..0c6a749a0 100644 --- a/test/bearer-authorization.test.ts +++ b/test/bearer-authorization.test.ts @@ -21,6 +21,25 @@ describe("canonical OIDC bearer framing", () => { expect(parseExactBearerToken("bearer header.payload.signature")).toBe("header.payload.signature"); }); + it("bounds bearer credential bytes before downstream JWT parsing", async () => { + const maximumToken = "a".repeat(16_384); + const oversizedAuthorization = `Bearer ${"a".repeat(16_385)}`; + + expect(parseExactBearerToken(`Bearer ${maximumToken}`)).toBe(maximumToken); + expect(parseExactBearerToken(oversizedAuthorization)).toBeUndefined(); + + const response = await baseWorker.fetch(new Request("https://noema.example/exchange", { + method: "POST", + headers: { authorization: oversizedAuthorization }, + }), env); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_MISSING", + }); + }); + it.each([ "Bearer\theader.payload.signature", "Bearer\u00a0header.payload.signature", From de639a15d7e385a3f14bcb5b1b25f3086fc1a6ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 19:20:48 -0700 Subject: [PATCH 120/564] fix(oidc): bound bearer credential bytes --- src/bearer-authorization.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index 0255688f6..9b6cc908f 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -1,10 +1,12 @@ +const maximumBearerTokenLength = 16_384; const canonicalBearerAuthorizationPattern = /^Bearer ([\x21-\x7e]+)$/i; /** * Return the bearer credential only when the Authorization field is already in the - * canonical `Bearer ` form. The parser never trims or normalizes - * attacker-controlled credential framing. + * canonical `Bearer ` form and remains within the bounded OIDC + * credential envelope. The parser never trims or normalizes attacker-controlled framing. */ export function parseExactBearerToken(authorization: string): string | undefined { + if (authorization.length > "Bearer ".length + maximumBearerTokenLength) return undefined; return canonicalBearerAuthorizationPattern.exec(authorization)?.[1]; } From f3120fb7d5071e1ea9e72231fe00bf55ec57fb64 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 19:59:34 -0700 Subject: [PATCH 121/564] fix(oidc): document bearer parser contract --- src/bearer-authorization.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index 9b6cc908f..b4c7a3f7f 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -5,6 +5,9 @@ const canonicalBearerAuthorizationPattern = /^Bearer ([\x21-\x7e]+)$/i; * Return the bearer credential only when the Authorization field is already in the * canonical `Bearer ` form and remains within the bounded OIDC * credential envelope. The parser never trims or normalizes attacker-controlled framing. + * + * @param authorization Raw HTTP Authorization field bytes decoded as a JavaScript string. + * @returns The exact bearer credential when framing and bounds are canonical; otherwise undefined. */ export function parseExactBearerToken(authorization: string): string | undefined { if (authorization.length > "Bearer ".length + maximumBearerTokenLength) return undefined; From 5e27eb849edf6fccb78fd7a8ef34482c0665bf0c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 20:00:52 -0700 Subject: [PATCH 122/564] 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 2eba8633e..604647ca7 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 = - "826b92394c63deb6981c3a8d16a724d71f85a0d7"; + "5ac41e0b8515f1143e6501c23a949798ff374093"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 820c5956d2ec1e12b81215bdad80361879ca2737 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 20:01:14 -0700 Subject: [PATCH 123/564] fix(oidc): roll trusted workflow source forward --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 28d01db83..65c98fa07 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 = "826b92394c63deb6981c3a8d16a724d71f85a0d7" +ALLOWED_WORKFLOW_SHA = "5ac41e0b8515f1143e6501c23a949798ff374093" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 4bee8a4e83eb708f7f3ab83d1b0e38f775bcddf7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 20:02:02 -0700 Subject: [PATCH 124/564] docs(architecture): align trusted central source revision --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6d41b30bb..2d02b953e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `826b92394c63deb6981c3a8d16a724d71f85a0d7`. That repository remains a read-only dependency from Noema; a future central source revision requires an explicit Noema trust roll-forward and fresh exact-head evidence. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `5ac41e0b8515f1143e6501c23a949798ff374093`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 8cd0c93ca0fbfd5747855ed3b806c5d2c5a2278f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 21:04:05 -0700 Subject: [PATCH 125/564] test(reviewer): require non-OpenSSL sandbox substrate --- .../test_codegraph_sandbox_image_contract.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 reviewer/tests/test_codegraph_sandbox_image_contract.py diff --git a/reviewer/tests/test_codegraph_sandbox_image_contract.py b/reviewer/tests/test_codegraph_sandbox_image_contract.py new file mode 100644 index 000000000..766717087 --- /dev/null +++ b/reviewer/tests/test_codegraph_sandbox_image_contract.py @@ -0,0 +1,31 @@ +"""Security contract for the CodeGraph sandbox runtime substrate.""" + +from pathlib import Path + +from noema_reviewer import sandbox + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +REVIEWER_WORKFLOW = REPOSITORY_ROOT / ".github" / "workflows" / "reviewer-ci.yml" +CENTRAL_WORKFLOW = REPOSITORY_ROOT / ".github" / "workflows" / "central-review.yml" + + +def test_codegraph_sandbox_uses_scanned_non_openssl_runtime_substrate() -> None: + """Reviewer workflows must scan java-base and invoke the bundled Node explicitly.""" + expected_source = "gcr.io/distroless/java-base-debian13:nonroot" + expected_repository = "gcr.io/distroless/java-base-debian13" + + for workflow_path in (REVIEWER_WORKFLOW, CENTRAL_WORKFLOW): + workflow = workflow_path.read_text(encoding="utf-8") + assert f"NOEMA_CODEGRAPH_SANDBOX_SOURCE_IMAGE: {expected_source}" in workflow + assert f"{expected_repository}@sha256:" in workflow + assert "gcr.io/distroless/nodejs24-debian13" not in workflow + assert "trivy image" in workflow + assert "--severity MEDIUM,HIGH,CRITICAL" in workflow + + assert sandbox.TRUSTED_CODEGRAPH_IMAGE_REPOSITORY == expected_repository + source = (REPOSITORY_ROOT / "reviewer" / "noema_reviewer" / "sandbox.py").read_text( + encoding="utf-8" + ) + assert '"/tooling/node_modules/@colbymchenry/codegraph-linux-x64/node"' in source + assert "image,\n BUNDLED_CODEGRAPH_NODE," in source From 04ec841ef586bc6ada2c55b3f45c8899fef64c98 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 21:04:35 -0700 Subject: [PATCH 126/564] fix(reviewer): use bundled Node on non-OpenSSL substrate --- reviewer/noema_reviewer/sandbox.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reviewer/noema_reviewer/sandbox.py b/reviewer/noema_reviewer/sandbox.py index c3658a505..7efa67165 100644 --- a/reviewer/noema_reviewer/sandbox.py +++ b/reviewer/noema_reviewer/sandbox.py @@ -17,7 +17,7 @@ from pathlib import Path -TRUSTED_CODEGRAPH_IMAGE_REPOSITORY = "gcr.io/distroless/nodejs24-debian13" +TRUSTED_CODEGRAPH_IMAGE_REPOSITORY = "gcr.io/distroless/java-base-debian13" TRUSTED_CODEGRAPH_IMAGE_RE = re.compile( rf"^{re.escape(TRUSTED_CODEGRAPH_IMAGE_REPOSITORY)}@sha256:[0-9a-f]{{64}}$" ) @@ -32,6 +32,7 @@ / "@colbymchenry" / "codegraph-linux-x64" ) +BUNDLED_CODEGRAPH_NODE = "/tooling/node_modules/@colbymchenry/codegraph-linux-x64/node" ProcessRunner = Callable[..., subprocess.CompletedProcess[str]] NameFactory = Callable[[], str] @@ -186,6 +187,7 @@ def _run_sandbox(self, explore_prompt: str) -> str: "--env=DO_NOT_TRACK=1", "--env=NO_COLOR=1", image, + BUNDLED_CODEGRAPH_NODE, "/sandbox/sandbox-runner.mjs", explore_prompt, ] From 4691fa854b31692fcb00681bde38a4db813503b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 21:05:00 -0700 Subject: [PATCH 127/564] fix(reviewer): scan java-base sandbox substrate --- .github/workflows/reviewer-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reviewer-ci.yml b/.github/workflows/reviewer-ci.yml index d3f37e7ed..c999450a2 100644 --- a/.github/workflows/reviewer-ci.yml +++ b/.github/workflows/reviewer-ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 env: - NOEMA_CODEGRAPH_SANDBOX_SOURCE_IMAGE: gcr.io/distroless/nodejs24-debian13:nonroot + NOEMA_CODEGRAPH_SANDBOX_SOURCE_IMAGE: gcr.io/distroless/java-base-debian13:nonroot defaults: run: working-directory: reviewer @@ -76,7 +76,7 @@ jobs: docker pull "$NOEMA_CODEGRAPH_SANDBOX_SOURCE_IMAGE" resolved="$(docker image inspect "$NOEMA_CODEGRAPH_SANDBOX_SOURCE_IMAGE" --format '{{index .RepoDigests 0}}')" case "$resolved" in - gcr.io/distroless/nodejs24-debian13@sha256:????????????????????????????????????????????????????????????????) ;; + gcr.io/distroless/java-base-debian13@sha256:????????????????????????????????????????????????????????????????) ;; *) printf '::error::Unexpected CodeGraph sandbox image identity: %s\n' "${resolved:-missing}"; exit 1 ;; esac cosign verify "$resolved" \ From a2bb842c47b4ba5dda297daaf77e67f825231561 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 21:06:37 -0700 Subject: [PATCH 128/564] fix(reviewer): harden central sandbox substrate --- .github/workflows/central-review.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/central-review.yml b/.github/workflows/central-review.yml index 1da77ed50..e38198a06 100644 --- a/.github/workflows/central-review.yml +++ b/.github/workflows/central-review.yml @@ -34,7 +34,7 @@ jobs: TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository || '' }} PR_NUMBER: ${{ github.event.client_payload.pr_number || '' }} EXPECTED_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || '' }} - NOEMA_CODEGRAPH_SANDBOX_SOURCE_IMAGE: gcr.io/distroless/nodejs24-debian13:nonroot + NOEMA_CODEGRAPH_SANDBOX_SOURCE_IMAGE: gcr.io/distroless/java-base-debian13:nonroot steps: - name: Validate target repository identifier id: target @@ -184,7 +184,7 @@ jobs: docker pull "$NOEMA_CODEGRAPH_SANDBOX_SOURCE_IMAGE" resolved="$(docker image inspect "$NOEMA_CODEGRAPH_SANDBOX_SOURCE_IMAGE" --format '{{index .RepoDigests 0}}')" case "$resolved" in - gcr.io/distroless/nodejs24-debian13@sha256:????????????????????????????????????????????????????????????????) ;; + gcr.io/distroless/java-base-debian13@sha256:????????????????????????????????????????????????????????????????) ;; *) printf '::error::Unexpected CodeGraph sandbox image identity: %s\n' "${resolved:-missing}"; exit 1 ;; esac cosign verify "$resolved" \ From cf5906dbe7e2c9f211e1fa456ff4ca4685f930cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 21:07:24 -0700 Subject: [PATCH 129/564] test(reviewer): align central sandbox image contract --- reviewer/tests/test_central_review_workflow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reviewer/tests/test_central_review_workflow.py b/reviewer/tests/test_central_review_workflow.py index 5d40b4f65..9dff1251d 100644 --- a/reviewer/tests/test_central_review_workflow.py +++ b/reviewer/tests/test_central_review_workflow.py @@ -121,14 +121,14 @@ def test_production_review_requires_contextual_orchestrator_gateway() -> None: def test_untrusted_codegraph_analysis_uses_an_authenticated_quarantine_image() -> None: """Target parsing must use a signed, scanned, immutable no-network image.""" workflow = _workflow() - source_image = "gcr.io/distroless/nodejs24-debian13:nonroot" + source_image = "gcr.io/distroless/java-base-debian13:nonroot" assert f"NOEMA_CODEGRAPH_SANDBOX_SOURCE_IMAGE: {source_image}" in workflow resolve_index = workflow.index("Resolve, authenticate, and scan CodeGraph sandbox image") collect_index = workflow.index("Collect bounded current-head review manifest") assert resolve_index < collect_index assert 'docker pull "$NOEMA_CODEGRAPH_SANDBOX_SOURCE_IMAGE"' in workflow - assert "gcr.io/distroless/nodejs24-debian13@sha256:" in workflow + assert "gcr.io/distroless/java-base-debian13@sha256:" in workflow assert "sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6" in workflow assert "aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567" in workflow assert "--certificate-oidc-issuer=https://accounts.google.com" in workflow From 46f8e7b01fa45c98e5218585a049fb88461f801c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 21:08:09 -0700 Subject: [PATCH 130/564] test(reviewer): assert bundled Node invocation --- reviewer/tests/test_sandbox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/reviewer/tests/test_sandbox.py b/reviewer/tests/test_sandbox.py index acc32789b..07e659df8 100644 --- a/reviewer/tests/test_sandbox.py +++ b/reviewer/tests/test_sandbox.py @@ -87,8 +87,9 @@ def fake_run(args, **kwargs): "dst=/sandbox/sandbox-runner.mjs,readonly" ) in command assert not any("docker.sock" in part for part in command) - assert command[-3:] == [ + assert command[-4:] == [ TEST_IMAGE, + sandbox.BUNDLED_CODEGRAPH_NODE, "/sandbox/sandbox-runner.mjs", prompt, ] From 47eafc20142544e8f90c08f71d3007e3b275eb37 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 22:00:45 -0700 Subject: [PATCH 131/564] test(oidc): reject non-UTF-8 JWT payloads --- test/oidc-invalid-utf8.test.ts | 108 +++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 test/oidc-invalid-utf8.test.ts diff --git a/test/oidc-invalid-utf8.test.ts b/test/oidc-invalid-utf8.test.ts new file mode 100644 index 000000000..962db7366 --- /dev/null +++ b/test/oidc-invalid-utf8.test.ts @@ -0,0 +1,108 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import worker, { type Env } from "../src/index"; + +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: "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: "unused", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", +}; + +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 payloadWithInvalidUtf8(value: Record): Uint8Array { + const marker = "__INVALID_UTF8__"; + const serialized = Buffer.from(JSON.stringify({ ...value, padding: marker }), "utf8"); + const markerBytes = Buffer.from(marker, "utf8"); + const offset = serialized.indexOf(markerBytes); + if (offset < 0) throw new Error("invalid UTF-8 marker missing from fixture"); + return Buffer.concat([ + serialized.subarray(0, offset), + Buffer.from([0xff]), + serialized.subarray(offset + markerBytes.length), + ]); +} + +async function createSignedJwtWithRawPayload(payloadBytes: Uint8Array) { + 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"], + ); + const kid = `oidc-invalid-utf8-${crypto.randomUUID()}`; + const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); + const body = Buffer.from(payloadBytes).toString("base64url"); + const signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + keyPair.privateKey, + new TextEncoder().encode(`${header}.${body}`), + ); + const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey); + return { + token: `${header}.${body}.${encodeBytes(signature)}`, + jwk: { ...publicJwk, kid, kty: "RSA" }, + }; +} + +describe("OIDC UTF-8 canonicality", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("rejects a correctly signed JWT whose payload is not valid UTF-8", async () => { + const now = Math.floor(Date.now() / 1000); + const payloadBytes = payloadWithInvalidUtf8({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + job_workflow_ref: env.ALLOWED_WORKFLOW_REF_PREFIX, + job_workflow_sha: env.ALLOWED_WORKFLOW_SHA, + sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }); + const { token, jwk } = await createSignedJwtWithRawPayload(payloadBytes); + + 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 new Response("unexpected external request", { status: 500 }); + }); + + const response = await worker.fetch(new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json", + }, + body: JSON.stringify({ target_repository: 42 }), + }), env); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + }); + }); +}); From 8f31870ef88afa262de802b9cd84914d1fb4b811 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 22:03:30 -0700 Subject: [PATCH 132/564] fix(oidc): reject non-UTF-8 JWT JSON --- src/index.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index abb2a9365..a98413aca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -352,7 +352,14 @@ function base64UrlEncode(bytes: ArrayBuffer | Uint8Array): stri } function decodeJson(segment: string): T { - return JSON.parse(new TextDecoder().decode(base64UrlDecode(segment))) as T; + const bytes = base64UrlDecode(segment); + let decoded: string; + try { + decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes); + } catch { + throw new SyntaxError("JWT segment is not valid UTF-8"); + } + return JSON.parse(decoded) as T; } async function fetchGithubOidcKeys(env: Env, forceRefresh = false): Promise { From e163be1ff651ff163de0ca331f271fd369780184 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 23:04:19 -0700 Subject: [PATCH 133/564] fix(oidc): satisfy strict TextDecoder constructor contract --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index a98413aca..d644bb9a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -355,7 +355,7 @@ function decodeJson(segment: string): T { const bytes = base64UrlDecode(segment); let decoded: string; try { - decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes); + decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes); } catch { throw new SyntaxError("JWT segment is not valid UTF-8"); } From 0845f62c354ce0b6fb4a179c14ee199326b189d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 23:09:08 -0700 Subject: [PATCH 134/564] 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 604647ca7..efde2321e 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 = - "5ac41e0b8515f1143e6501c23a949798ff374093"; + "5972c9ca08befa60501004d9650316a85b83db5c"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 5791f9d04dddc36ae5f20979b393d7a20c34acd1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 23:09:26 -0700 Subject: [PATCH 135/564] fix(oidc): roll trust to current central source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 65c98fa07..210090a93 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 = "5ac41e0b8515f1143e6501c23a949798ff374093" +ALLOWED_WORKFLOW_SHA = "5972c9ca08befa60501004d9650316a85b83db5c" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From b2d84c025c7bc0119dfc5bcac8f6425d7e8ec811 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 23:10:14 -0700 Subject: [PATCH 136/564] docs(architecture): align current central workflow source --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2d02b953e..3c7d46261 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `5ac41e0b8515f1143e6501c23a949798ff374093`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `5972c9ca08befa60501004d9650316a85b83db5c`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From f4734c1db2ba9176ae8ab88e2d43e3e9bf3904fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 00:04:15 -0700 Subject: [PATCH 137/564] test(oidc): reject non-canonical signature encoding --- test/oidc-workflow-sha-cryptographic.test.ts | 71 ++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/test/oidc-workflow-sha-cryptographic.test.ts b/test/oidc-workflow-sha-cryptographic.test.ts index 59be08a5c..c347917c7 100644 --- a/test/oidc-workflow-sha-cryptographic.test.ts +++ b/test/oidc-workflow-sha-cryptographic.test.ts @@ -10,6 +10,7 @@ const trustedJwksUrl = "https://token.actions.githubusercontent.com/.well-known/ const signingKid = "oidc-workflow-sha-cryptographic"; const expectedRepositoryOwnerId = "295022177"; const expectedWorkflowRepositoryId = "1274066402"; +const base64UrlAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; const env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -48,6 +49,22 @@ async function signedJwt(payload: Record): Promise { return `${encodedHeader}.${encodedPayload}.${encodeBytes(signature)}`; } +function nonCanonicalSignatureAlias(token: string): string { + const parts = token.split("."); + const signature = parts[2]; + const lastCharacter = signature.at(-1); + if (!lastCharacter) throw new Error("expected JWT signature segment"); + const lastIndex = base64UrlAlphabet.indexOf(lastCharacter); + if (lastIndex < 0 || lastIndex % 16 !== 0 || lastIndex + 1 >= base64UrlAlphabet.length) { + throw new Error("expected canonical one-byte-tail base64url signature"); + } + const aliasedSignature = `${signature.slice(0, -1)}${base64UrlAlphabet[lastIndex + 1]}`; + if (!Buffer.from(signature, "base64url").equals(Buffer.from(aliasedSignature, "base64url"))) { + throw new Error("expected non-canonical signature alias to decode to identical bytes"); + } + return `${parts[0]}.${parts[1]}.${aliasedSignature}`; +} + beforeAll(async () => { const keyPair = (await crypto.subtle.generateKey( { @@ -121,4 +138,58 @@ describe("cryptographic OIDC workflow source identity", () => { message: "OIDC workflow source revision is not allowed", }); }); + + it("rejects a signature segment whose non-canonical tail bits decode to the signed bytes", async () => { + const now = Math.floor(Date.now() / 1000); + const canonicalToken = await signedJwt({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, + repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, + 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 aliasedToken = nonCanonicalSignatureAlias(canonicalToken); + + 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 }); + }); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${aliasedToken}`, + "content-type": "application/json", + "cf-connecting-ip": "203.0.113.126", + }, + body: JSON.stringify({ target_repository: { owner: "ContextualWisdomLab" } }), + }), + env, + ); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + message: "OIDC token is malformed", + }); + }); }); From 9dc3212e4a53554eff695bff2b72cd79dec0f044 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 00:08:04 -0700 Subject: [PATCH 138/564] fix(oidc): require canonical JWT segment encoding --- src/index.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index d644bb9a2..79fa7959a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -351,8 +351,21 @@ function base64UrlEncode(bytes: ArrayBuffer | Uint8Array): stri return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); } +function decodeCanonicalJwtSegment(segment: string): Uint8Array { + let bytes: Uint8Array; + try { + bytes = base64UrlDecode(segment); + } catch { + throw new SyntaxError("JWT segment is not valid base64url"); + } + if (base64UrlEncode(bytes) !== segment) { + throw new SyntaxError("JWT segment is not canonical base64url"); + } + return bytes; +} + function decodeJson(segment: string): T { - const bytes = base64UrlDecode(segment); + const bytes = decodeCanonicalJwtSegment(segment); let decoded: string; try { decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes); @@ -446,7 +459,7 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise throw new ApiError("ERR_OIDC_VERIFICATION", 502, "GitHub OIDC JWKS did not include valid key entries"); } const signed = new TextEncoder().encode(`${parts[0]}.${parts[1]}`); - const signature = base64UrlDecode(parts[2]); + const signature = decodeCanonicalJwtSegment(parts[2]); const verified = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, signature, signed); if (!verified) throw new ApiError("ERR_OIDC_VERIFICATION", 401, "OIDC signature verification failed"); From fd8fc4eddb7db37b33cab38e44b85843bd22605d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 00:29:28 -0700 Subject: [PATCH 139/564] test(oidc): keep runtime prefilter behind exact bearer framing --- test/oidc-bearer-whitespace-envelope.test.ts | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/oidc-bearer-whitespace-envelope.test.ts b/test/oidc-bearer-whitespace-envelope.test.ts index c891cce52..1a1347b13 100644 --- a/test/oidc-bearer-whitespace-envelope.test.ts +++ b/test/oidc-bearer-whitespace-envelope.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it, vi } from "vitest"; import entrypoint, { isBoundedOidcBearer, type Env } from "../src/entrypoint"; +import runtimeEntrypoint, { type Env as RuntimeEnv } from "../src/runtime-entrypoint"; + +function encodeJsonSegment(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} describe("OIDC bearer envelope whitespace boundary", () => { it("rejects embedded credential whitespace before downstream JWT parsing", async () => { @@ -61,4 +66,37 @@ describe("OIDC bearer envelope whitespace boundary", () => { }); expect(logSpy.mock.calls.flat().join("\n")).not.toContain(authorization); }); + + it("does not let the runtime source prefilter treat a non-canonical bearer separator as workflow authority", async () => { + const workflowRef = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; + const payload = encodeJsonSegment({ + job_workflow_ref: workflowRef, + job_workflow_sha: "b".repeat(40), + }); + const authorization = `Bearer\te30.${payload}.c2ln`; + const response = await runtimeEntrypoint.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization, + "x-request-id": "runtime-bearer-separator", + }, + }), + { + GITHUB_API_BASE: "https://api.github.com", + ALLOWED_REPOSITORY_OWNER: "ContextualWisdomLab", + ALLOWED_WORKFLOW_REPOSITORY: "ContextualWisdomLab/.github", + ALLOWED_WORKFLOW_REF_PREFIX: workflowRef, + ALLOWED_WORKFLOW_SHA: "a".repeat(40), + } as RuntimeEnv, + ); + + 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: "runtime-bearer-separator", + }); + }); }); From 1e155750555998c4295dc39208a2647ff7b88dc9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 00:30:10 -0700 Subject: [PATCH 140/564] fix(oidc): reuse exact bearer parser in runtime prefilter --- src/runtime-entrypoint.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index 193fb0129..a5e70463d 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -3,6 +3,7 @@ import entrypoint, { NoemaRateLimiter, type Env as BaseEnv, } from "./entrypoint"; +import { parseExactBearerToken } from "./bearer-authorization"; import { evaluateRuntimeReadiness } from "./runtime-readiness"; export { NoemaOidcReplayGuard, NoemaRateLimiter }; @@ -38,11 +39,10 @@ type WorkflowSourceDecision = }; function decodedReusableWorkflowClaims(request: Request): ReusableWorkflowClaims | undefined { - const authorization = request.headers.get("authorization") ?? ""; - const match = authorization.match(/^Bearer\s+(\S+)$/i); - if (!match) return undefined; + const bearerToken = parseExactBearerToken(request.headers.get("authorization") ?? ""); + if (!bearerToken) return undefined; - const parts = match[1].split("."); + const parts = bearerToken.split("."); if (parts.length !== 3 || parts[1].length > MAX_OIDC_PAYLOAD_SEGMENT_LENGTH) { return undefined; } From f297346c5fcd4679366c755ea7e0eb86a30c8fa0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 01:00:46 -0700 Subject: [PATCH 141/564] test(oidc): require current central workflow source commit --- 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 efde2321e..1e11823c0 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 = - "5972c9ca08befa60501004d9650316a85b83db5c"; + "60a34005e59efe2b622897e3c7b06882bdf63aee"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From a05873d6525a47926d6db94d587584bf88161158 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 01:01:11 -0700 Subject: [PATCH 142/564] fix(oidc): roll trusted central workflow source forward --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 210090a93..61b1ea1dc 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 = "5972c9ca08befa60501004d9650316a85b83db5c" +ALLOWED_WORKFLOW_SHA = "60a34005e59efe2b622897e3c7b06882bdf63aee" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 83deb6a7e3dc424fbeaaf83773956fc90bdb9c04 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 01:01:55 -0700 Subject: [PATCH 143/564] docs(architecture): reconcile current central workflow trust pin --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3c7d46261..ffe2c8893 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `5972c9ca08befa60501004d9650316a85b83db5c`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `60a34005e59efe2b622897e3c7b06882bdf63aee`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 558505c936094684d62cfedcb076d529092caa24 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 02:04:14 -0700 Subject: [PATCH 144/564] test(oidc): cover undecodable base64url segments --- test/oidc-verification-residual-coverage.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/oidc-verification-residual-coverage.test.ts b/test/oidc-verification-residual-coverage.test.ts index c2edbfcc5..670fc7edf 100644 --- a/test/oidc-verification-residual-coverage.test.ts +++ b/test/oidc-verification-residual-coverage.test.ts @@ -325,6 +325,20 @@ describe("OIDC verification residual coverage", () => { }); }); + it("classifies an undecodable base64url payload segment as a malformed token before upstream access", async () => { + const encodedHeader = encodeJson({ alg: "RS256", kid: signingKid }); + const token = `${encodedHeader}.A.AA`; + const { response, fetchedUrls } = await exchange(token); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + message: "OIDC token is malformed", + }); + expect(fetchedUrls).toEqual([]); + }); + it("classifies malformed payload JSON as a malformed token before upstream access", async () => { const encodedHeader = encodeJson({ alg: "RS256", kid: signingKid }); const malformedPayload = Buffer.from("{").toString("base64url"); From ea1b2f9502ba63675fab718893e07d6b3612090d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 03:01:09 -0700 Subject: [PATCH 145/564] test(oidc): reject invalid utf8 in runtime prefilters --- ...untime-workflow-prefilter-coverage.test.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/runtime-workflow-prefilter-coverage.test.ts b/test/runtime-workflow-prefilter-coverage.test.ts index 462d2fdcd..fc03e5fcd 100644 --- a/test/runtime-workflow-prefilter-coverage.test.ts +++ b/test/runtime-workflow-prefilter-coverage.test.ts @@ -36,6 +36,27 @@ const env: Env = { NOEMA_RATE_LIMITER: allowingRateLimiter(), }; +function encodeJsonSegment(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +function invalidUtf8WorkflowPayloadSegment(): string { + const marker = "__INVALID_UTF8__"; + const serialized = Buffer.from(JSON.stringify({ + job_workflow_ref: env.ALLOWED_WORKFLOW_REF_PREFIX, + job_workflow_sha: "b".repeat(40), + padding: marker, + }), "utf8"); + const markerBytes = Buffer.from(marker, "utf8"); + const offset = serialized.indexOf(markerBytes); + if (offset < 0) throw new Error("invalid UTF-8 marker missing from fixture"); + return Buffer.concat([ + serialized.subarray(0, offset), + Buffer.from([0xff]), + serialized.subarray(offset + markerBytes.length), + ]).toString("base64url"); +} + async function expectMissingAuth(headers: Record = {}): Promise { const response = await worker.fetch( new Request("https://noema.example/exchange", { @@ -82,4 +103,24 @@ describe("runtime workflow-source prefilter coverage", () => { details: { policy: "bounded-oidc-jwt-envelope" }, }); }); + + it("does not derive workflow-source policy from replacement-decoded invalid UTF-8 payload bytes", async () => { + const token = `${encodeJsonSegment({ alg: "RS256", kid: "invalid-utf8-prefilter" })}.${invalidUtf8WorkflowPayloadSegment()}.AA`; + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + "cf-connecting-ip": "203.0.113.126", + authorization: `Bearer ${token}`, + }, + }), + env, + ); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + }); + }); }); From c9964ac78946a30c6d5799b73c85c5727a4db63c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 03:01:57 -0700 Subject: [PATCH 146/564] fix(oidc): fail closed on invalid utf8 runtime claims --- src/runtime-entrypoint.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index a5e70463d..53989ba2e 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -52,7 +52,9 @@ function decodedReusableWorkflowClaims(request: Request): ReusableWorkflowClaims const padded = normalized + "===".slice((normalized.length + 3) % 4); const binary = atob(padded); const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0)); - const decoded: unknown = JSON.parse(new TextDecoder().decode(bytes)); + const decoded: unknown = JSON.parse( + new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes), + ); if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) { return undefined; } From 864e7dd69d95ef16bb7cc622dbc9bdd00a1c559e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 03:03:27 -0700 Subject: [PATCH 147/564] fix(oidc): reject invalid utf8 protected claims --- src/worker.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/worker.ts b/src/worker.ts index 232b31d12..ad0e2aff4 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -80,7 +80,9 @@ function decodeOidcWorkflowClaims(request: Request): OidcWorkflowClaims | undefi const padded = normalized + "===".slice((normalized.length + 3) % 4); const binary = atob(padded); const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0)); - const decoded: unknown = JSON.parse(new TextDecoder().decode(bytes)); + const decoded: unknown = JSON.parse( + new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes), + ); if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) { return undefined; } From 4ef33a09941bcd91f685be012e3aa88654baf221 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 03:07:49 -0700 Subject: [PATCH 148/564] test(oidc): reject noncanonical prefilter payloads --- ...untime-workflow-prefilter-coverage.test.ts | 81 ++++++++++++++++--- 1 file changed, 71 insertions(+), 10 deletions(-) diff --git a/test/runtime-workflow-prefilter-coverage.test.ts b/test/runtime-workflow-prefilter-coverage.test.ts index fc03e5fcd..f9c5f26fd 100644 --- a/test/runtime-workflow-prefilter-coverage.test.ts +++ b/test/runtime-workflow-prefilter-coverage.test.ts @@ -36,10 +36,34 @@ const env: Env = { NOEMA_RATE_LIMITER: allowingRateLimiter(), }; +const base64UrlAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + function encodeJsonSegment(value: unknown): string { return Buffer.from(JSON.stringify(value)).toString("base64url"); } +function workflowIdentityPayloadSegment(): string { + let padding = ""; + while (true) { + const segment = encodeJsonSegment({ + job_workflow_ref: env.ALLOWED_WORKFLOW_REF_PREFIX, + job_workflow_sha: "b".repeat(40), + padding, + }); + if (segment.length % 4 === 2 || segment.length % 4 === 3) return segment; + padding += "x"; + } +} + +function sameBytesNonCanonicalBase64Url(segment: string): string { + if (segment.length % 4 !== 2 && segment.length % 4 !== 3) { + throw new Error("fixture requires an unpadded base64url tail"); + } + const lastIndex = base64UrlAlphabet.indexOf(segment.at(-1) ?? ""); + if (lastIndex < 0) throw new Error("fixture tail must be base64url"); + return `${segment.slice(0, -1)}${base64UrlAlphabet[lastIndex + 1]}`; +} + function invalidUtf8WorkflowPayloadSegment(): string { const marker = "__INVALID_UTF8__"; const serialized = Buffer.from(JSON.stringify({ @@ -57,6 +81,20 @@ function invalidUtf8WorkflowPayloadSegment(): string { ]).toString("base64url"); } +async function exchangeWithPayloadSegment(payloadSegment: string, kid: string): Promise { + const token = `${encodeJsonSegment({ alg: "RS256", kid })}.${payloadSegment}.AA`; + return worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + "cf-connecting-ip": "203.0.113.126", + authorization: `Bearer ${token}`, + }, + }), + env, + ); +} + async function expectMissingAuth(headers: Record = {}): Promise { const response = await worker.fetch( new Request("https://noema.example/exchange", { @@ -105,16 +143,39 @@ describe("runtime workflow-source prefilter coverage", () => { }); it("does not derive workflow-source policy from replacement-decoded invalid UTF-8 payload bytes", async () => { - const token = `${encodeJsonSegment({ alg: "RS256", kid: "invalid-utf8-prefilter" })}.${invalidUtf8WorkflowPayloadSegment()}.AA`; - const response = await worker.fetch( - new Request("https://noema.example/exchange", { - method: "POST", - headers: { - "cf-connecting-ip": "203.0.113.126", - authorization: `Bearer ${token}`, - }, - }), - env, + const response = await exchangeWithPayloadSegment( + invalidUtf8WorkflowPayloadSegment(), + "invalid-utf8-prefilter", + ); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + }); + }); + + it("does not derive workflow-source policy from padded base64url payload authority", async () => { + const response = await exchangeWithPayloadSegment( + `${workflowIdentityPayloadSegment()}=`, + "padded-base64url-prefilter", + ); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + }); + }); + + it("does not derive workflow-source policy from non-canonical base64url pad bits", async () => { + const canonical = workflowIdentityPayloadSegment(); + const nonCanonical = sameBytesNonCanonicalBase64Url(canonical); + expect(Buffer.from(nonCanonical, "base64url")).toEqual(Buffer.from(canonical, "base64url")); + + const response = await exchangeWithPayloadSegment( + nonCanonical, + "pad-bit-base64url-prefilter", ); expect(response.status).toBe(400); From 6174dbf672ce286271b2fe0e250c2fd7902a9a09 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 03:09:06 -0700 Subject: [PATCH 149/564] fix(oidc): require canonical runtime payload encoding --- src/runtime-entrypoint.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index 53989ba2e..2252f9251 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -38,6 +38,22 @@ type WorkflowSourceDecision = outcome: "blocked" | "misconfigured"; }; +function decodeCanonicalBase64UrlSegment(segment: string): Uint8Array | undefined { + try { + const normalized = segment.replace(/-/g, "+").replace(/_/g, "/"); + const padded = normalized + "===".slice((normalized.length + 3) % 4); + const binary = atob(padded); + const canonical = btoa(binary) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, ""); + if (canonical !== segment) return undefined; + return Uint8Array.from(binary, (character) => character.charCodeAt(0)); + } catch { + return undefined; + } +} + function decodedReusableWorkflowClaims(request: Request): ReusableWorkflowClaims | undefined { const bearerToken = parseExactBearerToken(request.headers.get("authorization") ?? ""); if (!bearerToken) return undefined; @@ -47,11 +63,9 @@ function decodedReusableWorkflowClaims(request: Request): ReusableWorkflowClaims return undefined; } + const bytes = decodeCanonicalBase64UrlSegment(parts[1]); + if (!bytes) return undefined; try { - const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/"); - const padded = normalized + "===".slice((normalized.length + 3) % 4); - const binary = atob(padded); - const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0)); const decoded: unknown = JSON.parse( new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes), ); From 0d708a559d0e659f53d2b1c70fac7b5bdc268c02 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 03:10:03 -0700 Subject: [PATCH 150/564] fix(oidc): require canonical protected payload encoding --- src/worker.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/worker.ts b/src/worker.ts index ad0e2aff4..7ef0ca77c 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -65,6 +65,22 @@ function traceIdFromRequest(request: Request): string { return crypto.randomUUID(); } +function decodeCanonicalBase64UrlSegment(segment: string): Uint8Array | undefined { + try { + const normalized = segment.replace(/-/g, "+").replace(/_/g, "/"); + const padded = normalized + "===".slice((normalized.length + 3) % 4); + const binary = atob(padded); + const canonical = btoa(binary) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, ""); + if (canonical !== segment) return undefined; + return Uint8Array.from(binary, (character) => character.charCodeAt(0)); + } catch { + return undefined; + } +} + function decodeOidcWorkflowClaims(request: Request): OidcWorkflowClaims | undefined { const authorization = request.headers.get("authorization") ?? ""; const bearerToken = parseExactBearerToken(authorization); @@ -75,11 +91,9 @@ function decodeOidcWorkflowClaims(request: Request): OidcWorkflowClaims | undefi return undefined; } + const bytes = decodeCanonicalBase64UrlSegment(parts[1]); + if (!bytes) return undefined; try { - const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/"); - const padded = normalized + "===".slice((normalized.length + 3) % 4); - const binary = atob(padded); - const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0)); const decoded: unknown = JSON.parse( new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes), ); From a64792733c70260396ccaebd879c32a924679a3f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 03:28:50 -0700 Subject: [PATCH 151/564] test(oidc): reject BOM-prefixed signed payload authority --- test/oidc-invalid-utf8.test.ts | 99 ++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 41 deletions(-) diff --git a/test/oidc-invalid-utf8.test.ts b/test/oidc-invalid-utf8.test.ts index 962db7366..13434a04b 100644 --- a/test/oidc-invalid-utf8.test.ts +++ b/test/oidc-invalid-utf8.test.ts @@ -35,6 +35,13 @@ function payloadWithInvalidUtf8(value: Record): Uint8Array { ]); } +function payloadWithUtf8Bom(value: Record): Uint8Array { + return Buffer.concat([ + Buffer.from([0xef, 0xbb, 0xbf]), + Buffer.from(JSON.stringify(value), "utf8"), + ]); +} + async function createSignedJwtWithRawPayload(payloadBytes: Uint8Array) { const keyPair = await crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, @@ -56,53 +63,63 @@ async function createSignedJwtWithRawPayload(payloadBytes: Uint8Array) { }; } +function oidcPayload(now: number): Record { + return { + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + job_workflow_ref: env.ALLOWED_WORKFLOW_REF_PREFIX, + job_workflow_sha: env.ALLOWED_WORKFLOW_SHA, + sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }; +} + +async function expectMalformedSignedPayload(payloadBytes: Uint8Array): Promise { + const { token, jwk } = await createSignedJwtWithRawPayload(payloadBytes); + + 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 new Response("unexpected external request", { status: 500 }); + }); + + const response = await worker.fetch(new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json", + }, + body: JSON.stringify({ target_repository: 42 }), + }), env); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + }); +} + describe("OIDC UTF-8 canonicality", () => { afterEach(() => { vi.restoreAllMocks(); }); it("rejects a correctly signed JWT whose payload is not valid UTF-8", async () => { - const now = Math.floor(Date.now() / 1000); - const payloadBytes = payloadWithInvalidUtf8({ - iss: env.ALLOWED_ISSUER, - aud: env.ALLOWED_AUDIENCE, - repository_owner: env.ALLOWED_REPOSITORY_OWNER, - repository_owner_id: "295022177", - repository: "ContextualWisdomLab/.github", - repository_id: "1274066402", - job_workflow_ref: env.ALLOWED_WORKFLOW_REF_PREFIX, - job_workflow_sha: env.ALLOWED_WORKFLOW_SHA, - sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", - exp: now + 300, - nbf: now - 30, - iat: now - 30, - }); - const { token, jwk } = await createSignedJwtWithRawPayload(payloadBytes); - - 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 new Response("unexpected external request", { status: 500 }); - }); - - const response = await worker.fetch(new Request("https://noema.example/exchange", { - method: "POST", - headers: { - authorization: `Bearer ${token}`, - "content-type": "application/json", - }, - body: JSON.stringify({ target_repository: 42 }), - }), env); + await expectMalformedSignedPayload(payloadWithInvalidUtf8(oidcPayload(Math.floor(Date.now() / 1000)))); + }); - expect(response.status).toBe(400); - await expect(response.json()).resolves.toMatchObject({ - ok: false, - error_code: "ERR_TOKEN_MALFORMED", - }); + it("rejects a correctly signed JWT whose payload starts with a UTF-8 BOM", async () => { + await expectMalformedSignedPayload(payloadWithUtf8Bom(oidcPayload(Math.floor(Date.now() / 1000)))); }); }); From 032d3ac1b734be8a5487286519d50b224a145e61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 03:31:15 -0700 Subject: [PATCH 152/564] test(oidc): bind BOM rejection to shared bearer boundary --- test/oidc-invalid-utf8.test.ts | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/test/oidc-invalid-utf8.test.ts b/test/oidc-invalid-utf8.test.ts index 13434a04b..11446b17c 100644 --- a/test/oidc-invalid-utf8.test.ts +++ b/test/oidc-invalid-utf8.test.ts @@ -80,7 +80,7 @@ function oidcPayload(now: number): Record { }; } -async function expectMalformedSignedPayload(payloadBytes: Uint8Array): Promise { +async function exchangeSignedPayload(payloadBytes: Uint8Array): Promise { const { token, jwk } = await createSignedJwtWithRawPayload(payloadBytes); vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { @@ -94,7 +94,7 @@ async function expectMalformedSignedPayload(payloadBytes: Uint8Array): Promise { @@ -116,10 +110,26 @@ describe("OIDC UTF-8 canonicality", () => { }); it("rejects a correctly signed JWT whose payload is not valid UTF-8", async () => { - await expectMalformedSignedPayload(payloadWithInvalidUtf8(oidcPayload(Math.floor(Date.now() / 1000)))); + const response = await exchangeSignedPayload( + payloadWithInvalidUtf8(oidcPayload(Math.floor(Date.now() / 1000))), + ); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + }); }); - it("rejects a correctly signed JWT whose payload starts with a UTF-8 BOM", async () => { - await expectMalformedSignedPayload(payloadWithUtf8Bom(oidcPayload(Math.floor(Date.now() / 1000)))); + it("rejects a correctly signed JWT whose payload starts with a UTF-8 BOM before verification", async () => { + const response = await exchangeSignedPayload( + payloadWithUtf8Bom(oidcPayload(Math.floor(Date.now() / 1000))), + ); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_MISSING", + }); }); }); From a0c65cb0925916c8a31ad81be479d1b7921ef2d8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 03:31:41 -0700 Subject: [PATCH 153/564] fix(oidc): reject BOM-prefixed signed payload authority --- src/bearer-authorization.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index b4c7a3f7f..5122c0fd6 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -1,15 +1,22 @@ const maximumBearerTokenLength = 16_384; const canonicalBearerAuthorizationPattern = /^Bearer ([\x21-\x7e]+)$/i; +const utf8BomBase64UrlPrefix = "77u_"; /** * Return the bearer credential only when the Authorization field is already in the * canonical `Bearer ` form and remains within the bounded OIDC * credential envelope. The parser never trims or normalizes attacker-controlled framing. + * JWT payloads beginning with a UTF-8 BOM are rejected before any claim reader can + * silently strip those signed bytes while interpreting JSON authority. * * @param authorization Raw HTTP Authorization field bytes decoded as a JavaScript string. * @returns The exact bearer credential when framing and bounds are canonical; otherwise undefined. */ export function parseExactBearerToken(authorization: string): string | undefined { if (authorization.length > "Bearer ".length + maximumBearerTokenLength) return undefined; - return canonicalBearerAuthorizationPattern.exec(authorization)?.[1]; + const token = canonicalBearerAuthorizationPattern.exec(authorization)?.[1]; + if (!token) return undefined; + const segments = token.split("."); + if (segments.length === 3 && segments[1].startsWith(utf8BomBase64UrlPrefix)) return undefined; + return token; } From c29a54fb03aab1d5b258e85fbb5f386a19d36c77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 04:03:31 -0700 Subject: [PATCH 154/564] test(oidc): reject BOM-prefixed protected JWT header --- test/oidc-invalid-utf8.test.ts | 45 ++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/test/oidc-invalid-utf8.test.ts b/test/oidc-invalid-utf8.test.ts index 11446b17c..019bffa21 100644 --- a/test/oidc-invalid-utf8.test.ts +++ b/test/oidc-invalid-utf8.test.ts @@ -35,7 +35,7 @@ function payloadWithInvalidUtf8(value: Record): Uint8Array { ]); } -function payloadWithUtf8Bom(value: Record): Uint8Array { +function jsonWithUtf8Bom(value: Record): Uint8Array { return Buffer.concat([ Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(JSON.stringify(value), "utf8"), @@ -63,6 +63,22 @@ async function createSignedJwtWithRawPayload(payloadBytes: Uint8Array) { }; } +async function createSignedJwtWithRawHeader(headerBytes: Uint8Array, payload: Record) { + 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"], + ); + const header = Buffer.from(headerBytes).toString("base64url"); + const body = encodeSegment(payload); + const signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + keyPair.privateKey, + new TextEncoder().encode(`${header}.${body}`), + ); + return `${header}.${body}.${encodeBytes(signature)}`; +} + function oidcPayload(now: number): Record { return { iss: env.ALLOWED_ISSUER, @@ -123,7 +139,7 @@ describe("OIDC UTF-8 canonicality", () => { it("rejects a correctly signed JWT whose payload starts with a UTF-8 BOM before verification", async () => { const response = await exchangeSignedPayload( - payloadWithUtf8Bom(oidcPayload(Math.floor(Date.now() / 1000))), + jsonWithUtf8Bom(oidcPayload(Math.floor(Date.now() / 1000))), ); expect(response.status).toBe(401); @@ -132,4 +148,29 @@ describe("OIDC UTF-8 canonicality", () => { error_code: "ERR_AUTH_MISSING", }); }); + + it("rejects a correctly signed JWT whose protected header starts with a UTF-8 BOM before verification", async () => { + const now = Math.floor(Date.now() / 1000); + const token = await createSignedJwtWithRawHeader( + jsonWithUtf8Bom({ alg: "RS256", kid: "bom-header", typ: "JWT" }), + oidcPayload(now), + ); + const fetchSpy = vi.spyOn(globalThis, "fetch"); + + const response = await worker.fetch(new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json", + }, + body: JSON.stringify({ target_repository: 42 }), + }), env); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_MISSING", + }); + expect(fetchSpy).not.toHaveBeenCalled(); + }); }); From 2db2e02d9cd24c3aac099de3c8ca5999b455bb36 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 04:03:56 -0700 Subject: [PATCH 155/564] fix(oidc): reject BOM-prefixed protected JWT header --- src/bearer-authorization.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index 5122c0fd6..a58b163d0 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -6,8 +6,8 @@ const utf8BomBase64UrlPrefix = "77u_"; * Return the bearer credential only when the Authorization field is already in the * canonical `Bearer ` form and remains within the bounded OIDC * credential envelope. The parser never trims or normalizes attacker-controlled framing. - * JWT payloads beginning with a UTF-8 BOM are rejected before any claim reader can - * silently strip those signed bytes while interpreting JSON authority. + * JWT protected headers or payloads beginning with a UTF-8 BOM are rejected before any + * claim reader can silently strip those signed bytes while interpreting JSON authority. * * @param authorization Raw HTTP Authorization field bytes decoded as a JavaScript string. * @returns The exact bearer credential when framing and bounds are canonical; otherwise undefined. @@ -17,6 +17,9 @@ export function parseExactBearerToken(authorization: string): string | undefined const token = canonicalBearerAuthorizationPattern.exec(authorization)?.[1]; if (!token) return undefined; const segments = token.split("."); - if (segments.length === 3 && segments[1].startsWith(utf8BomBase64UrlPrefix)) return undefined; + if ( + segments.length === 3 + && (segments[0].startsWith(utf8BomBase64UrlPrefix) || segments[1].startsWith(utf8BomBase64UrlPrefix)) + ) return undefined; return token; } From 9e8be553ef8d1affa9e96081bc8d08ddee81a4c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 05:08:26 -0700 Subject: [PATCH 156/564] test(oidc): rate-limit forged workflow-source claims first --- ...untime-workflow-prefilter-coverage.test.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/test/runtime-workflow-prefilter-coverage.test.ts b/test/runtime-workflow-prefilter-coverage.test.ts index f9c5f26fd..844f814aa 100644 --- a/test/runtime-workflow-prefilter-coverage.test.ts +++ b/test/runtime-workflow-prefilter-coverage.test.ts @@ -21,6 +21,27 @@ function allowingRateLimiter(): DurableObjectNamespace { } as unknown as DurableObjectNamespace; } +function denyingRateLimiter(onFetch: () => void): DurableObjectNamespace { + return { + idFromName(name: string) { + return { toString: () => name } as DurableObjectId; + }, + get() { + return { + async fetch() { + onFetch(); + return Response.json({ + allowed: false, + limit: 1, + remaining: 0, + retry_after_seconds: 60, + }); + }, + } as unknown as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; +} + const env: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", ALLOWED_AUDIENCE: "cwl-noema-review", @@ -142,6 +163,41 @@ describe("runtime workflow-source prefilter coverage", () => { }); }); + it("routes forged workflow-source claims through distributed rate limiting before trust rejection", async () => { + let limiterCalls = 0; + const rateLimitedEnv: Env = { + ...env, + NOEMA_RATE_LIMITER: denyingRateLimiter(() => { + limiterCalls += 1; + }), + }; + const token = `${encodeJsonSegment({ alg: "RS256", kid: "forged-stale-source" })}.${encodeJsonSegment({ + job_workflow_ref: env.ALLOWED_WORKFLOW_REF_PREFIX, + job_workflow_sha: "b".repeat(40), + jti: "forged-stale-source", + exp: Math.floor(Date.now() / 1000) + 300, + })}.AA`; + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + "cf-connecting-ip": "203.0.113.126", + authorization: `Bearer ${token}`, + }, + }), + rateLimitedEnv, + ); + + expect(limiterCalls).toBe(1); + expect(response.status).toBe(429); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_RATE_LIMIT", + details: { scope: "distributed" }, + }); + }); + it("does not derive workflow-source policy from replacement-decoded invalid UTF-8 payload bytes", async () => { const response = await exchangeWithPayloadSegment( invalidUtf8WorkflowPayloadSegment(), From 15637a050522deca4c2817967ca016b6d9c688f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 05:09:03 -0700 Subject: [PATCH 157/564] fix(oidc): rate-limit before workflow source rejection --- src/runtime-entrypoint.ts | 156 +------------------------------------- 1 file changed, 4 insertions(+), 152 deletions(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index 2252f9251..37a085813 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -3,7 +3,6 @@ import entrypoint, { NoemaRateLimiter, type Env as BaseEnv, } from "./entrypoint"; -import { parseExactBearerToken } from "./bearer-authorization"; import { evaluateRuntimeReadiness } from "./runtime-readiness"; export { NoemaOidcReplayGuard, NoemaRateLimiter }; @@ -18,137 +17,6 @@ export interface Env extends BaseEnv { ALLOWED_WORKFLOW_SHA?: string; } -const exactCommitShaPattern = /^[0-9a-f]{40}$/; -const MAX_OIDC_PAYLOAD_SEGMENT_LENGTH = 8_192; - -type ReusableWorkflowClaims = { - workflow_ref?: unknown; - workflow_sha?: unknown; - job_workflow_ref?: unknown; - job_workflow_sha?: unknown; -}; - -type WorkflowSourceDecision = - | { allowed: true } - | { - allowed: false; - status: 403 | 503; - message: string; - hint: string; - outcome: "blocked" | "misconfigured"; - }; - -function decodeCanonicalBase64UrlSegment(segment: string): Uint8Array | undefined { - try { - const normalized = segment.replace(/-/g, "+").replace(/_/g, "/"); - const padded = normalized + "===".slice((normalized.length + 3) % 4); - const binary = atob(padded); - const canonical = btoa(binary) - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/g, ""); - if (canonical !== segment) return undefined; - return Uint8Array.from(binary, (character) => character.charCodeAt(0)); - } catch { - return undefined; - } -} - -function decodedReusableWorkflowClaims(request: Request): ReusableWorkflowClaims | undefined { - const bearerToken = parseExactBearerToken(request.headers.get("authorization") ?? ""); - if (!bearerToken) return undefined; - - const parts = bearerToken.split("."); - if (parts.length !== 3 || parts[1].length > MAX_OIDC_PAYLOAD_SEGMENT_LENGTH) { - return undefined; - } - - const bytes = decodeCanonicalBase64UrlSegment(parts[1]); - if (!bytes) return undefined; - try { - const decoded: unknown = JSON.parse( - new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes), - ); - if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) { - return undefined; - } - return decoded as ReusableWorkflowClaims; - } catch { - return undefined; - } -} - -function workflowSourceDecision(request: Request, env: Env): WorkflowSourceDecision { - const claims = decodedReusableWorkflowClaims(request); - if (!claims) return { allowed: true }; - - const usingReusableWorkflowIdentity = typeof claims.job_workflow_ref === "string"; - const workflowRef = usingReusableWorkflowIdentity - ? claims.job_workflow_ref - : typeof claims.workflow_ref === "string" - ? claims.workflow_ref - : undefined; - if (!workflowRef) return { allowed: true }; - - const configuredRef = env.ALLOWED_WORKFLOW_REF_PREFIX; - if (!configuredRef || workflowRef !== configuredRef) { - // The delegated hardened worker owns the exact workflow-ref error contract. - return { allowed: true }; - } - - const configuredSha = env.ALLOWED_WORKFLOW_SHA; - if (!configuredSha || !exactCommitShaPattern.test(configuredSha)) { - return { - allowed: false, - status: 503, - message: "Workflow source trust configuration unavailable", - hint: "Configure the exact 40-character lowercase commit SHA for the allowed workflow source.", - outcome: "misconfigured", - }; - } - - const workflowSha = usingReusableWorkflowIdentity - ? claims.job_workflow_sha - : claims.workflow_sha; - if (workflowSha !== configuredSha) { - return { - allowed: false, - status: 403, - message: "OIDC workflow source revision is not allowed", - hint: "Run the request from the configured workflow source revision; mutable-ref identity alone is insufficient.", - outcome: "blocked", - }; - } - - return { allowed: true }; -} - -function workflowSourceResponse( - decision: Exclude, -): Response { - const traceId = crypto.randomUUID(); - return new Response(JSON.stringify({ - ok: false, - error_code: "ERR_WORKFLOW_NOT_ALLOWED", - message: decision.message, - details: { - hint: decision.hint, - match_policy: "exact-ref-and-source-sha", - }, - trace_id: traceId, - }), { - status: decision.status, - headers: { - "content-type": "application/json; charset=utf-8", - "cache-control": "no-store", - pragma: "no-cache", - "x-content-type-options": "nosniff", - "x-trace-id": traceId, - "x-latency-ms": "0", - }, - }); -} - function readinessHeaders( traceId: string, latencyMs: number, @@ -227,11 +95,10 @@ async function runtimeReadinessResponse(request: Request, env: Env): Promise { @@ -239,21 +106,6 @@ export default { if (url.pathname === "/ready") { return runtimeReadinessResponse(request, env); } - if (url.pathname === "/exchange") { - const sourceDecision = workflowSourceDecision(request, env); - if (!sourceDecision.allowed) { - console.log(JSON.stringify({ - event: "workflow_source_trust", - route: url.pathname, - method: request.method, - status_code: sourceDecision.status, - error_code: "ERR_WORKFLOW_NOT_ALLOWED", - outcome: sourceDecision.outcome, - match_policy: "exact-ref-and-source-sha", - })); - return workflowSourceResponse(sourceDecision); - } - } return entrypoint.fetch(request, env); }, }; From e628281f1983fed0a187ec841a74dd967f5dd35b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 06:07:45 -0700 Subject: [PATCH 158/564] test(oidc): require issued-at claim --- test/oidc-required-issued-at.test.ts | 113 +++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 test/oidc-required-issued-at.test.ts diff --git a/test/oidc-required-issued-at.test.ts b/test/oidc-required-issued-at.test.ts new file mode 100644 index 000000000..0e47d7c10 --- /dev/null +++ b/test/oidc-required-issued-at.test.ts @@ -0,0 +1,113 @@ +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-required-issued-at"; + +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"); +} + +async function signedJwtWithoutIssuedAt(): Promise { + const now = Math.floor(Date.now() / 1000); + const header = encodeJson({ alg: "RS256", kid: signingKid }); + const payload = encodeJson({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + job_workflow_ref: configuredWorkflowRef, + job_workflow_sha: configuredWorkflowSha, + sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", + exp: now + 300, + nbf: now - 30, + }); + const signature = new Uint8Array( + await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + signingPrivateKey, + new TextEncoder().encode(`${header}.${payload}`), + ), + ); + return `${header}.${payload}.${Buffer.from(signature).toString("base64url")}`; +} + +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("GitHub OIDC issued-at authority", () => { + it("rejects a signed GitHub OIDC token that omits the provider-required iat claim", async () => { + 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 }); + }); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${await signedJwtWithoutIssuedAt()}`, + "content-type": "application/json", + "cf-connecting-ip": "203.0.113.124", + }, + body: JSON.stringify({ + target_repository: { owner: "ContextualWisdomLab", repo: "noema" }, + }), + }), + env, + ); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_INVALID", + message: "OIDC issued-at claim is invalid", + }); + }); +}); From c295656d69aa3f3c14ed4dec9535ed46a48f257d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 06:13:10 -0700 Subject: [PATCH 159/564] fix(oidc): require issued-at claim --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 79fa7959a..3e3ec8ef1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -536,7 +536,7 @@ 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))) { + if (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) { From d5fb9a6dd04512c13dbbc3fe05fdebc237ee9eb4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 06:31:31 -0700 Subject: [PATCH 160/564] test(oidc): require not-before claim --- test/oidc-required-not-before.test.ts | 111 ++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 test/oidc-required-not-before.test.ts diff --git a/test/oidc-required-not-before.test.ts b/test/oidc-required-not-before.test.ts new file mode 100644 index 000000000..62ae15d7e --- /dev/null +++ b/test/oidc-required-not-before.test.ts @@ -0,0 +1,111 @@ +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-required-not-before"; + +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"); +} + +async function signedJwtWithoutNotBefore(): Promise { + const now = Math.floor(Date.now() / 1000); + const header = encodeJson({ alg: "RS256", kid: signingKid }); + const payload = encodeJson({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + job_workflow_ref: configuredWorkflowRef, + job_workflow_sha: configuredWorkflowSha, + sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", + exp: now + 300, + iat: now - 30, + }); + const signature = new Uint8Array( + await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + signingPrivateKey, + new TextEncoder().encode(`${header}.${payload}`), + ), + ); + return `${header}.${payload}.${Buffer.from(signature).toString("base64url")}`; +} + +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("GitHub OIDC not-before authority", () => { + it("rejects a signed GitHub OIDC token that omits the provider-required nbf claim", async () => { + 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 }); + }); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${await signedJwtWithoutNotBefore()}`, + "content-type": "application/json", + "cf-connecting-ip": "203.0.113.125", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + env, + ); + + 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", + }); + }); +}); From 67f97a4057a5f27b863f2b946bc09c046be036cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 06:35:59 -0700 Subject: [PATCH 161/564] fix(oidc): require not-before claim --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3e3ec8ef1..41743ad57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -530,10 +530,10 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise { match_policy: "exact-ref-and-source-sha" }, ); } - if (payload.nbf !== undefined && (typeof payload.nbf !== "number" || !Number.isFinite(payload.nbf))) { + if (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) { + if (payload.nbf > now + 30) { throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC token is not valid yet"); } if (typeof payload.iat !== "number" || !Number.isFinite(payload.iat)) { From 8fa68c9875beddf31888cc9a27fca99a9070219d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 06:41:58 -0700 Subject: [PATCH 162/564] 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 1e11823c0..74d00b559 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 = - "60a34005e59efe2b622897e3c7b06882bdf63aee"; + "e00bd7964f332b69cf7b430b0cb5ad486eef8258"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 57067e61f18f88bb65c1f9edc3b492300acd80f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 06:42:27 -0700 Subject: [PATCH 163/564] 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 61b1ea1dc..a21abfef6 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 = "60a34005e59efe2b622897e3c7b06882bdf63aee" +ALLOWED_WORKFLOW_SHA = "e00bd7964f332b69cf7b430b0cb5ad486eef8258" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 63d20c627e133f80ffcdcb49d840c2786fbb2efd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 06:43:26 -0700 Subject: [PATCH 164/564] docs(architecture): align central workflow trust source --- ARCHITECTURE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ffe2c8893..dc2bd3c00 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `60a34005e59efe2b622897e3c7b06882bdf63aee`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `e00bd7964f332b69cf7b430b0cb5ad486eef8258`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. @@ -153,4 +153,4 @@ Root README/customer copy may have a separate active owner; the canonical archit The default shape is **small credential-exchange service + explicit state coordinators + external orchestration/review planes**. New model orchestration, artifact processing, repository mutation, or deployment authority should first be evaluated as a separate bounded component rather than folded into `/exchange`. -Architecture changes must keep source behavior, realistic regression tests, canonical documentation, traceability, and CHANGELOG semantics consistent without promoting active-PR behavior to protected truth. +Architecture changes must keep source behavior, realistic regression tests, canonical documentation, traceability, and CHANGELOG semantics consistent without promoting active-PR behavior to protected truth. \ No newline at end of file From 70d56279aa0ea0fd80e035fca996d95578767979 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 07:03:40 -0700 Subject: [PATCH 165/564] test(oidc): reject duplicate signed claim authority --- test/oidc-duplicate-claim-integrity.test.ts | 144 ++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 test/oidc-duplicate-claim-integrity.test.ts diff --git a/test/oidc-duplicate-claim-integrity.test.ts b/test/oidc-duplicate-claim-integrity.test.ts new file mode 100644 index 000000000..f244d024b --- /dev/null +++ b/test/oidc-duplicate-claim-integrity.test.ts @@ -0,0 +1,144 @@ +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-duplicate-claim-integrity"; + +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 encodeBytes(bytes: ArrayBuffer | Uint8Array): string { + return Buffer.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)).toString("base64url"); +} + +async function signedJwtWithRawJson(headerJson: string, payloadJson: string): Promise { + const header = Buffer.from(headerJson, "utf8").toString("base64url"); + const payload = Buffer.from(payloadJson, "utf8").toString("base64url"); + const signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + signingPrivateKey, + new TextEncoder().encode(`${header}.${payload}`), + ); + return `${header}.${payload}.${encodeBytes(signature)}`; +} + +function canonicalPayload(now: number): Record { + return { + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + 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, + }; +} + +async function exchange(token: string): Promise { + vi.resetModules(); + const { default: worker } = await import("../src/index"); + 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.126", + }, + body: JSON.stringify({ target_repository: 42 }), + }), + 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("GitHub OIDC duplicate-claim integrity", () => { + it("rejects an escape-equivalent duplicate top-level payload claim before JWKS egress", async () => { + const now = Math.floor(Date.now() / 1000); + const payload = JSON.stringify(canonicalPayload(now)).replace( + '"repository_owner":"ContextualWisdomLab"', + '"repository_owner":"OtherOrg","repository_own\\u0065r":"ContextualWisdomLab"', + ); + const token = await signedJwtWithRawJson( + JSON.stringify({ alg: "RS256", kid: signingKid, typ: "JWT" }), + payload, + ); + const fetchSpy = 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 }); + }); + + const response = await exchange(token); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + }); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("rejects duplicate protected-header authority before JWKS egress", async () => { + const now = Math.floor(Date.now() / 1000); + const token = await signedJwtWithRawJson( + `{"alg":"none","a\\u006cg":"RS256","kid":"${signingKid}","typ":"JWT"}`, + JSON.stringify(canonicalPayload(now)), + ); + const fetchSpy = vi.spyOn(globalThis, "fetch"); + + const response = await exchange(token); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + }); + expect(fetchSpy).not.toHaveBeenCalled(); + }); +}); From 36b536f78af8d0dd5336bf4473e5894426749597 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 07:05:22 -0700 Subject: [PATCH 166/564] test(oidc): bind duplicate claims to bearer rejection --- test/oidc-duplicate-claim-integrity.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/oidc-duplicate-claim-integrity.test.ts b/test/oidc-duplicate-claim-integrity.test.ts index f244d024b..5c8e68505 100644 --- a/test/oidc-duplicate-claim-integrity.test.ts +++ b/test/oidc-duplicate-claim-integrity.test.ts @@ -116,10 +116,10 @@ describe("GitHub OIDC duplicate-claim integrity", () => { const response = await exchange(token); - expect(response.status).toBe(400); + expect(response.status).toBe(401); await expect(response.json()).resolves.toMatchObject({ ok: false, - error_code: "ERR_TOKEN_MALFORMED", + error_code: "ERR_AUTH_MISSING", }); expect(fetchSpy).not.toHaveBeenCalled(); }); @@ -134,10 +134,10 @@ describe("GitHub OIDC duplicate-claim integrity", () => { const response = await exchange(token); - expect(response.status).toBe(400); + expect(response.status).toBe(401); await expect(response.json()).resolves.toMatchObject({ ok: false, - error_code: "ERR_TOKEN_MALFORMED", + error_code: "ERR_AUTH_MISSING", }); expect(fetchSpy).not.toHaveBeenCalled(); }); From 8a0a2e687a3eefeb2d90ba1f546de056c097adad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 07:06:00 -0700 Subject: [PATCH 167/564] fix(oidc): reject duplicate signed claim authority --- src/bearer-authorization.ts | 82 +++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index a58b163d0..0fb51d49b 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -2,12 +2,83 @@ const maximumBearerTokenLength = 16_384; const canonicalBearerAuthorizationPattern = /^Bearer ([\x21-\x7e]+)$/i; const utf8BomBase64UrlPrefix = "77u_"; +function decodeJwtJsonText(segment: string): string | undefined { + try { + const padded = segment.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((segment.length + 3) % 4); + const binary = atob(padded); + const bytes = new Uint8Array(binary.length); + for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index); + return new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes); + } catch { + return undefined; + } +} + +function hasDuplicateTopLevelJsonKeys(segment: string): boolean { + const text = decodeJwtJsonText(segment); + if (text === undefined) return false; + + const seenKeys = new Set(); + let structureDepth = 0; + let stringStart = -1; + let inString = false; + let escaped = false; + + for (let index = 0; index < text.length; index += 1) { + const character = text[index]; + if (inString) { + if (escaped) { + escaped = false; + continue; + } + if (character === "\\") { + escaped = true; + continue; + } + if (character !== '"') continue; + + inString = false; + if (structureDepth !== 1) continue; + + let lookahead = index + 1; + while (lookahead < text.length && /\s/.test(text[lookahead])) lookahead += 1; + if (text[lookahead] !== ":") continue; + + const encodedKey = text.slice(stringStart + 1, index); + let decodedKey: unknown; + try { + decodedKey = JSON.parse(`"${encodedKey}"`); + } catch { + return false; + } + if (typeof decodedKey !== "string") return false; + if (seenKeys.has(decodedKey)) return true; + seenKeys.add(decodedKey); + continue; + } + + if (character === '"') { + inString = true; + stringStart = index; + continue; + } + if (character === "{" || character === "[") { + structureDepth += 1; + continue; + } + if (character === "}" || character === "]") structureDepth -= 1; + } + + return false; +} + /** * Return the bearer credential only when the Authorization field is already in the * canonical `Bearer ` form and remains within the bounded OIDC * credential envelope. The parser never trims or normalizes attacker-controlled framing. - * JWT protected headers or payloads beginning with a UTF-8 BOM are rejected before any - * claim reader can silently strip those signed bytes while interpreting JSON authority. + * JWT protected headers or payloads beginning with a UTF-8 BOM, or containing duplicate + * top-level JSON member names after escape decoding, are rejected before any claim reader + * can silently reinterpret the signed authority bytes. * * @param authorization Raw HTTP Authorization field bytes decoded as a JavaScript string. * @returns The exact bearer credential when framing and bounds are canonical; otherwise undefined. @@ -19,7 +90,12 @@ export function parseExactBearerToken(authorization: string): string | undefined const segments = token.split("."); if ( segments.length === 3 - && (segments[0].startsWith(utf8BomBase64UrlPrefix) || segments[1].startsWith(utf8BomBase64UrlPrefix)) + && ( + segments[0].startsWith(utf8BomBase64UrlPrefix) + || segments[1].startsWith(utf8BomBase64UrlPrefix) + || hasDuplicateTopLevelJsonKeys(segments[0]) + || hasDuplicateTopLevelJsonKeys(segments[1]) + ) ) return undefined; return token; } From ea9dbf4814c6d52b8fdb54774ab6bead7ccc58b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 08:11:13 -0700 Subject: [PATCH 168/564] test(oidc): exercise source-sha policy after trusted verification --- test/oidc-workflow-sha-binding.test.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/test/oidc-workflow-sha-binding.test.ts b/test/oidc-workflow-sha-binding.test.ts index 991b5fa63..acf45068d 100644 --- a/test/oidc-workflow-sha-binding.test.ts +++ b/test/oidc-workflow-sha-binding.test.ts @@ -115,7 +115,10 @@ async function exchangeWithToken( ); } -async function exchangeWithTrustedOidc(token: string): Promise { +async function exchangeWithTrustedOidc( + token: string, + overrides: Partial = {}, +): Promise { vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { const url = String(input); if (url === trustedDiscoveryUrl) { @@ -128,7 +131,7 @@ async function exchangeWithTrustedOidc(token: string): Promise { } return new Response("unexpected privileged egress", { status: 500 }); }); - return exchangeWithToken(token); + return exchangeWithToken(token, overrides); } async function exchangeWithClaims(claims: Record): Promise { @@ -149,7 +152,10 @@ async function exchangeWithClaims(claims: Record): Promise): Promise { +async function trustedExchangeWithClaims( + claims: Record, + overrides: Partial = {}, +): Promise { const now = Math.floor(Date.now() / 1000); const token = await signedJwt({ iss: env.ALLOWED_ISSUER, @@ -164,11 +170,11 @@ async function trustedExchangeWithClaims(claims: Record): Promi iat: now - 30, ...claims, }); - return exchangeWithTrustedOidc(token); + return exchangeWithTrustedOidc(token, overrides); } async function exchangeWithWorkflowSha(jobWorkflowSha?: string): Promise { - return exchangeWithClaims({ + return trustedExchangeWithClaims({ job_workflow_ref: configuredWorkflowRef, ...(jobWorkflowSha === undefined ? {} : { job_workflow_sha: jobWorkflowSha }), }); @@ -216,7 +222,7 @@ describe("production OIDC reusable-workflow source identity", () => { }); it("binds the fallback workflow_ref identity to its workflow_sha instead of bypassing immutable source policy", async () => { - const response = await exchangeWithClaims({ + const response = await trustedExchangeWithClaims({ workflow_ref: configuredWorkflowRef, workflow_sha: "b".repeat(40), }); @@ -254,11 +260,11 @@ describe("production OIDC reusable-workflow source identity", () => { it.each([undefined, "", "A".repeat(40)])( "fails closed when the immutable workflow source configuration is unusable (%s)", async (configuredSha) => { - const response = await exchangeWithToken( - unsignedJwt({ + const response = await trustedExchangeWithClaims( + { job_workflow_ref: configuredWorkflowRef, job_workflow_sha: configuredWorkflowSha, - }), + }, { ALLOWED_WORKFLOW_SHA: configuredSha }, ); From dabac0a8732d67b7e02836d74ea381bfba9289d8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 08:11:50 -0700 Subject: [PATCH 169/564] test(oidc): include required issued-at authority --- test/replay-target-authorization-coverage.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/replay-target-authorization-coverage.test.ts b/test/replay-target-authorization-coverage.test.ts index a1051a064..7e209ca52 100644 --- a/test/replay-target-authorization-coverage.test.ts +++ b/test/replay-target-authorization-coverage.test.ts @@ -56,6 +56,7 @@ async function createToken(repository: string | undefined) { job_workflow_sha: configuredWorkflowSha, exp: now + 300, nbf: now - 30, + iat: now - 30, }); const signature = await crypto.subtle.sign( "RSASSA-PKCS1-v1_5", From 6d4b401b1c8619d8332b999acdb22ec086b1c3b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 08:12:34 -0700 Subject: [PATCH 170/564] test(rate-limit): match configured denial decision --- test/runtime-workflow-prefilter-coverage.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtime-workflow-prefilter-coverage.test.ts b/test/runtime-workflow-prefilter-coverage.test.ts index 844f814aa..5be06b6f6 100644 --- a/test/runtime-workflow-prefilter-coverage.test.ts +++ b/test/runtime-workflow-prefilter-coverage.test.ts @@ -32,7 +32,7 @@ function denyingRateLimiter(onFetch: () => void): DurableObjectNamespace { onFetch(); return Response.json({ allowed: false, - limit: 1, + limit: 1000, remaining: 0, retry_after_seconds: 60, }); From f79157444986ccc03cac55a26b3115e641005620 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 08:18:26 -0700 Subject: [PATCH 171/564] docs(architecture): match current exchange trust layering --- ARCHITECTURE.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index dc2bd3c00..d436024d1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -26,13 +26,13 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi ## 2. Current workflow trust contract -This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs an early denial-only source check, while `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. +This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs readiness dispatch and delegates `/exchange`; `src/entrypoint.ts` applies the distributed rate limiter before `src/worker.ts` performs its denial-only exact workflow-ref precheck. `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. `wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `e00bd7964f332b69cf7b430b0cb5ad486eef8258`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. -The configured workflow ref and source SHA are operator authority bytes, not normalization input. The runtime prefilter, protected workflow-ref parser, and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. +The configured workflow ref and source SHA are operator authority bytes, not normalization input. The protected workflow-ref parser and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. -The source-SHA prefilter is not an authorization substitute for cryptographic verification. Tokens not rejected at the wrapper continue through the existing distributed rate-limit, exact-ref trust, signature, issuer, audience, repository, time-window, replay, and GitHub App boundaries. +The worker exact-ref precheck is not an authorization substitute for cryptographic verification. `/exchange` requests reach distributed rate limiting before unverified workflow-source claims can affect trust rejection; immutable source-SHA authority is enforced by the authoritative verifier after signature and claim verification. The remaining issuer, audience, repository, time-window, replay, and GitHub App boundaries remain independent and fail closed at their owning layer. ## 3. Runtime data flow @@ -42,10 +42,9 @@ flowchart LR B --> C{route} C -->|/health| H[Liveness] C -->|/ready| R[Readiness] - C -->|/exchange| S{exact ref + source SHA prefilter} - S --> E[src/entrypoint.ts] + C -->|/exchange| E[src/entrypoint.ts] E --> L[NoemaRateLimiter] - L --> W[src/worker.ts\nexact workflow ref] + L --> W[src/worker.ts\nexact workflow-ref precheck] W --> O[src/index.ts\ncryptographic OIDC + exact source binding] O --> G[GitHub App token exchange] G --> P[NoemaOidcReplayGuard] @@ -92,7 +91,7 @@ Repository automation must: 6. reject stale/predecessor evidence as current success; 7. avoid self-modifying repair workflows and never weaken gates to manufacture green evidence. -These control-plane invariants are separate from the runtime OIDC trust contract. Immutable workflow-source identity is already protected-base truth at this revision's branch point; the active source delta represented by this file strengthens only canonical configuration-byte handling until that delta integrates. +These control-plane invariants are separate from the runtime OIDC trust contract. Immutable workflow-source identity is already protected-base truth at this revision's branch point; active-PR changes described here remain candidate truth until their exact revision integrates. ## 7. Credential and network boundaries @@ -153,4 +152,4 @@ Root README/customer copy may have a separate active owner; the canonical archit The default shape is **small credential-exchange service + explicit state coordinators + external orchestration/review planes**. New model orchestration, artifact processing, repository mutation, or deployment authority should first be evaluated as a separate bounded component rather than folded into `/exchange`. -Architecture changes must keep source behavior, realistic regression tests, canonical documentation, traceability, and CHANGELOG semantics consistent without promoting active-PR behavior to protected truth. \ No newline at end of file +Architecture changes must keep source behavior, realistic regression tests, canonical documentation, traceability, and CHANGELOG semantics consistent without promoting active-PR behavior to protected truth. From 83017646e1159b6721abb8546e89e4391426c2c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 08:33:48 -0700 Subject: [PATCH 172/564] test(oidc): reject inverted numeric-date authority --- test/oidc-numeric-date-finite.test.ts | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/oidc-numeric-date-finite.test.ts b/test/oidc-numeric-date-finite.test.ts index ec9834ebe..38b857bec 100644 --- a/test/oidc-numeric-date-finite.test.ts +++ b/test/oidc-numeric-date-finite.test.ts @@ -165,4 +165,32 @@ describe("OIDC NumericDate finiteness", () => { error_code: "ERR_AUTH_INVALID", }); }); + + it("rejects a signed token whose not-before is after expiration even when both are inside skew", async () => { + const now = Math.floor(Date.now() / 1000); + const claims = rawClaimsWithNumericDate("nbf", String(now - 5), now) + .replace(`"exp":${now + 300}`, `"exp":${now - 10}`); + const token = await signedRawPayloadJwt(claims); + 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 token whose issued-at is after expiration even when both are inside skew", async () => { + const now = Math.floor(Date.now() / 1000); + const claims = rawClaimsWithNumericDate("iat", String(now - 5), now) + .replace(`"exp":${now + 300}`, `"exp":${now - 10}`); + const token = await signedRawPayloadJwt(claims); + const response = await exchange(token); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_INVALID", + }); + }); }); From d38c1474c19850301e371e18e913c7081bc358d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 08:43:27 -0700 Subject: [PATCH 173/564] fix(oidc): reject inverted numeric-date authority --- src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.ts b/src/index.ts index 41743ad57..3b7e14940 100644 --- a/src/index.ts +++ b/src/index.ts @@ -545,6 +545,9 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp < now - 30) { throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC token is expired"); } + if (payload.nbf > payload.exp || payload.iat > payload.exp) { + throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC token temporal claims are inconsistent"); + } return payload; } catch (error) { From 04e84cafd52f592a6a31303077de244dfc14bc07 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 09:04:46 -0700 Subject: [PATCH 174/564] test(oidc): require canonical bearer JWT segments --- ...r-authorization-canonical-segments.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 test/bearer-authorization-canonical-segments.test.ts diff --git a/test/bearer-authorization-canonical-segments.test.ts b/test/bearer-authorization-canonical-segments.test.ts new file mode 100644 index 000000000..c8d4f9b1b --- /dev/null +++ b/test/bearer-authorization-canonical-segments.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; +import { parseExactBearerToken } from "../src/bearer-authorization"; + +const canonicalHeader = Buffer.from(JSON.stringify({ alg: "RS256", kid: "kid" })).toString("base64url"); +const canonicalPayload = Buffer.from(JSON.stringify({ sub: "repo:ContextualWisdomLab/noema" })).toString("base64url"); +const canonicalSignature = Buffer.from([1, 2, 3, 4]).toString("base64url"); + +describe("parseExactBearerToken canonical JWT segments", () => { + it("preserves an already-canonical three-segment JWT byte-for-byte", () => { + const token = `${canonicalHeader}.${canonicalPayload}.${canonicalSignature}`; + + expect(parseExactBearerToken(`Bearer ${token}`)).toBe(token); + }); + + it.each([ + ["padded protected header", `${canonicalHeader}=.${canonicalPayload}.${canonicalSignature}`], + ["padded payload", `${canonicalHeader}.${canonicalPayload}=.${canonicalSignature}`], + ["padded signature", `${canonicalHeader}.${canonicalPayload}.${canonicalSignature}=`], + ["invalid protected-header alphabet", `%${canonicalHeader}.${canonicalPayload}.${canonicalSignature}`], + ["empty payload segment", `${canonicalHeader}..${canonicalSignature}`], + ["non-JWT bearer credential", "opaque-visible-ascii-token"], + ])("rejects %s before any downstream claim reader", (_name, token) => { + expect(parseExactBearerToken(`Bearer ${token}`)).toBeUndefined(); + }); +}); From 2c28131db573828778774211e16b1b5536610ceb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 09:05:19 -0700 Subject: [PATCH 175/564] fix(oidc): reject non-canonical bearer JWT segments --- src/bearer-authorization.ts | 44 +++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index 0fb51d49b..372a84e78 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -2,12 +2,31 @@ const maximumBearerTokenLength = 16_384; const canonicalBearerAuthorizationPattern = /^Bearer ([\x21-\x7e]+)$/i; const utf8BomBase64UrlPrefix = "77u_"; -function decodeJwtJsonText(segment: string): string | undefined { +function decodeCanonicalBase64Url(segment: string): Uint8Array | undefined { + if (!segment || !/^[A-Za-z0-9_-]+$/.test(segment)) return undefined; try { const padded = segment.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((segment.length + 3) % 4); const binary = atob(padded); const bytes = new Uint8Array(binary.length); - for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index); + let roundTripBinary = ""; + for (let index = 0; index < binary.length; index += 1) { + bytes[index] = binary.charCodeAt(index); + roundTripBinary += String.fromCharCode(bytes[index]); + } + const canonical = btoa(roundTripBinary) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, ""); + return canonical === segment ? bytes : undefined; + } catch { + return undefined; + } +} + +function decodeJwtJsonText(segment: string): string | undefined { + const bytes = decodeCanonicalBase64Url(segment); + if (bytes === undefined) return undefined; + try { return new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes); } catch { return undefined; @@ -76,9 +95,10 @@ function hasDuplicateTopLevelJsonKeys(segment: string): boolean { * Return the bearer credential only when the Authorization field is already in the * canonical `Bearer ` form and remains within the bounded OIDC * credential envelope. The parser never trims or normalizes attacker-controlled framing. - * JWT protected headers or payloads beginning with a UTF-8 BOM, or containing duplicate - * top-level JSON member names after escape decoding, are rejected before any claim reader - * can silently reinterpret the signed authority bytes. + * Each JWT segment must already be non-empty canonical unpadded base64url. Protected + * headers or payloads beginning with a UTF-8 BOM, or containing duplicate top-level JSON + * member names after escape decoding, are rejected before any claim reader can silently + * reinterpret the signed authority bytes. * * @param authorization Raw HTTP Authorization field bytes decoded as a JavaScript string. * @returns The exact bearer credential when framing and bounds are canonical; otherwise undefined. @@ -88,14 +108,14 @@ export function parseExactBearerToken(authorization: string): string | undefined const token = canonicalBearerAuthorizationPattern.exec(authorization)?.[1]; if (!token) return undefined; const segments = token.split("."); + if (segments.length !== 3 || segments.some((segment) => decodeCanonicalBase64Url(segment) === undefined)) { + return undefined; + } if ( - segments.length === 3 - && ( - segments[0].startsWith(utf8BomBase64UrlPrefix) - || segments[1].startsWith(utf8BomBase64UrlPrefix) - || hasDuplicateTopLevelJsonKeys(segments[0]) - || hasDuplicateTopLevelJsonKeys(segments[1]) - ) + segments[0].startsWith(utf8BomBase64UrlPrefix) + || segments[1].startsWith(utf8BomBase64UrlPrefix) + || hasDuplicateTopLevelJsonKeys(segments[0]) + || hasDuplicateTopLevelJsonKeys(segments[1]) ) return undefined; return token; } From 516e89fe99769928efe6c3d2bef818197d943560 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 09:06:40 -0700 Subject: [PATCH 176/564] test(oidc): align fallback coverage with required nbf --- test/oidc-verification-residual-coverage.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/oidc-verification-residual-coverage.test.ts b/test/oidc-verification-residual-coverage.test.ts index 670fc7edf..b65d61bb2 100644 --- a/test/oidc-verification-residual-coverage.test.ts +++ b/test/oidc-verification-residual-coverage.test.ts @@ -127,14 +127,13 @@ afterEach(() => { }); describe("OIDC verification residual coverage", () => { - it("accepts an audience array and workflow_ref fallback without nbf", async () => { + it("accepts an audience array and workflow_ref fallback with valid temporal authority", async () => { const claims = baseClaims(); claims.aud = ["unrelated-audience", env.ALLOWED_AUDIENCE]; delete claims.job_workflow_ref; delete claims.job_workflow_sha; claims.workflow_ref = configuredWorkflowRef; claims.workflow_sha = configuredWorkflowSha; - delete claims.nbf; const { response } = await exchange(await signedJwt(claims)); From cb3856a292ff00350333384755596ef92f9c0b08 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 09:11:22 -0700 Subject: [PATCH 177/564] test(oidc): require current central workflow source commit --- 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 74d00b559..507c4083e 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 = - "e00bd7964f332b69cf7b430b0cb5ad486eef8258"; + "f655a901f7ccdfef0d62694c818ad2896a2f5da1"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 77b21aa2dce5ed870ab54266d3a9a2c254ffe20b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 09:11:51 -0700 Subject: [PATCH 178/564] fix(oidc): roll forward trusted central workflow commit --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index a21abfef6..8c48b3b48 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 = "e00bd7964f332b69cf7b430b0cb5ad486eef8258" +ALLOWED_WORKFLOW_SHA = "f655a901f7ccdfef0d62694c818ad2896a2f5da1" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 3ec157bfe883c88d88c55eca0ce301bef6180039 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 09:12:52 -0700 Subject: [PATCH 179/564] docs(architecture): align trusted central workflow commit --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d436024d1..b15e402c5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs readiness dispatch and delegates `/exchange`; `src/entrypoint.ts` applies the distributed rate limiter before `src/worker.ts` performs its denial-only exact workflow-ref precheck. `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `e00bd7964f332b69cf7b430b0cb5ad486eef8258`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `f655a901f7ccdfef0d62694c818ad2896a2f5da1`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The protected workflow-ref parser and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 0e7cf77f568903af73a34a11e89f548e1ab0727c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 10:09:10 -0700 Subject: [PATCH 180/564] test(oidc): keep replay fixtures canonical --- test/worker-exchange-replay.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/worker-exchange-replay.test.ts b/test/worker-exchange-replay.test.ts index 3d046e3fa..046c34d89 100644 --- a/test/worker-exchange-replay.test.ts +++ b/test/worker-exchange-replay.test.ts @@ -39,6 +39,7 @@ const baseEnv = { }; const configuredRef = baseEnv.ALLOWED_WORKFLOW_REF_PREFIX; +const canonicalSignature = Buffer.from("signature", "utf8").toString("base64url"); type MockFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise; @@ -50,7 +51,7 @@ function craftToken( payload: Record, header: Record = { alg: "RS256", kid: "test" }, ): string { - return `${encodeSegment(header)}.${encodeSegment(payload)}.signature`; + return `${encodeSegment(header)}.${encodeSegment(payload)}.${canonicalSignature}`; } function namespaceReturning(handler: MockFetch): DurableObjectNamespace { @@ -296,7 +297,7 @@ describe("exchange wrapper replay protection", () => { // A valid three-segment shape whose middle segment decodes to bytes that are // not JSON, exercising the decode catch path. const badPayload = Buffer.from("definitely not json", "utf8").toString("base64url"); - const token = `${encodeSegment({ alg: "RS256", kid: "test" })}.${badPayload}.signature`; + const token = `${encodeSegment({ alg: "RS256", kid: "test" })}.${badPayload}.${canonicalSignature}`; const response = await worker.fetch( exchangeRequest({ authorization: `Bearer ${token}`, From 98bb5dfc16e1edb584475f61bc0b94a76b361e1a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 10:09:41 -0700 Subject: [PATCH 181/564] test(oidc): keep defensive replay fixtures canonical --- test/worker-defensive-replay.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/worker-defensive-replay.test.ts b/test/worker-defensive-replay.test.ts index 8ff342361..4857537a6 100644 --- a/test/worker-defensive-replay.test.ts +++ b/test/worker-defensive-replay.test.ts @@ -37,6 +37,7 @@ import worker, { type Env } from "../src/worker"; const configuredRef = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; +const canonicalSignature = Buffer.from("signature", "utf8").toString("base64url"); function encodeSegment(value: unknown): string { return Buffer.from(JSON.stringify(value)).toString("base64url"); @@ -98,7 +99,7 @@ describe("wrapper defensive replay-guard fallback", () => { job_workflow_ref: configuredRef, jti: "safe-jti", exp: Math.floor(Date.now() / 1000) + 300, - })}.signature`; + })}.${canonicalSignature}`; const response = await worker.fetch( new Request("https://noema.example/exchange", { @@ -127,7 +128,7 @@ describe("wrapper defensive replay-guard fallback", () => { job_workflow_ref: configuredRef, jti: "safe-jti-unavailable", exp: Math.floor(Date.now() / 1000) + 300, - })}.signature`; + })}.${canonicalSignature}`; const response = await worker.fetch( new Request("https://noema.example/exchange", { From 2ffe7983c959104a8cb346fc8a2719edb69f3ae6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 10:29:12 -0700 Subject: [PATCH 182/564] test(oidc): require current central workflow commit --- 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 507c4083e..a7ffe81ee 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 = - "f655a901f7ccdfef0d62694c818ad2896a2f5da1"; + "139c22f74b96e213510d3f487a5bed3b71f3459b"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 32fd3bab407bdfbff2a1aa0f0fe3850af0176ca8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 10:29:31 -0700 Subject: [PATCH 183/564] fix(oidc): trust current central workflow commit --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 8c48b3b48..fa4e69e79 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 = "f655a901f7ccdfef0d62694c818ad2896a2f5da1" +ALLOWED_WORKFLOW_SHA = "139c22f74b96e213510d3f487a5bed3b71f3459b" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 0d0f8e8b596c575759bb5fee14058a969a0b59aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 10:30:18 -0700 Subject: [PATCH 184/564] docs(architecture): align current central workflow trust --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b15e402c5..07c2033e1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs readiness dispatch and delegates `/exchange`; `src/entrypoint.ts` applies the distributed rate limiter before `src/worker.ts` performs its denial-only exact workflow-ref precheck. `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `f655a901f7ccdfef0d62694c818ad2896a2f5da1`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `139c22f74b96e213510d3f487a5bed3b71f3459b`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The protected workflow-ref parser and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 01dd62116d71e6c38111a577f39e71e097ea5d0e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 11:09:14 -0700 Subject: [PATCH 185/564] test(oidc): converge bearer fixtures on canonical JWT boundary --- test/bearer-authorization.test.ts | 32 +++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/test/bearer-authorization.test.ts b/test/bearer-authorization.test.ts index 0c6a749a0..b33de92c5 100644 --- a/test/bearer-authorization.test.ts +++ b/test/bearer-authorization.test.ts @@ -15,16 +15,22 @@ const env: Env = { NOEMA_RATE_LIMIT_PER_MINUTE: "1000", }; +const canonicalHeader = Buffer.from("{}", "utf8").toString("base64url"); +const canonicalPayload = Buffer.from("{}", "utf8").toString("base64url"); +const canonicalSignature = Buffer.from([0]).toString("base64url"); +const canonicalToken = `${canonicalHeader}.${canonicalPayload}.${canonicalSignature}`; + describe("canonical OIDC bearer framing", () => { - it("accepts exactly one ASCII space and preserves the token bytes", () => { - expect(parseExactBearerToken("Bearer header.payload.signature")).toBe("header.payload.signature"); - expect(parseExactBearerToken("bearer header.payload.signature")).toBe("header.payload.signature"); + it("accepts exactly one ASCII space and preserves canonical JWT bytes", () => { + expect(parseExactBearerToken(`Bearer ${canonicalToken}`)).toBe(canonicalToken); + expect(parseExactBearerToken(`bearer ${canonicalToken}`)).toBe(canonicalToken); }); - it("bounds bearer credential bytes before downstream JWT parsing", async () => { - const maximumToken = "a".repeat(16_384); - const oversizedAuthorization = `Bearer ${"a".repeat(16_385)}`; + it("bounds canonical bearer credential bytes before downstream JWT parsing", async () => { + const maximumToken = `${canonicalHeader}.${canonicalPayload}.${"A".repeat(16_376)}`; + const oversizedAuthorization = `Bearer ${canonicalHeader}.${canonicalPayload}.${"A".repeat(16_377)}`; + expect(maximumToken).toHaveLength(16_384); expect(parseExactBearerToken(`Bearer ${maximumToken}`)).toBe(maximumToken); expect(parseExactBearerToken(oversizedAuthorization)).toBeUndefined(); @@ -41,11 +47,11 @@ describe("canonical OIDC bearer framing", () => { }); it.each([ - "Bearer\theader.payload.signature", - "Bearer\u00a0header.payload.signature", - "Bearer header.payload.signature", - "Bearer \theader.payload.signature", - "Bearer header.payload.signature ", + `Bearer\t${canonicalToken}`, + `Bearer\u00a0${canonicalToken}`, + `Bearer ${canonicalToken}`, + `Bearer \t${canonicalToken}`, + `Bearer ${canonicalToken} `, ])("rejects non-canonical bearer framing: %j", (authorization) => { expect(parseExactBearerToken(authorization)).toBeUndefined(); }); @@ -68,9 +74,11 @@ describe("canonical OIDC bearer framing", () => { }); it("keeps canonical malformed JWTs on the malformed-token boundary", async () => { + const malformedJsonHeader = Buffer.from("{", "utf8").toString("base64url"); + const malformedToken = `${malformedJsonHeader}.${canonicalPayload}.${canonicalSignature}`; const response = await baseWorker.fetch(new Request("https://noema.example/exchange", { method: "POST", - headers: { authorization: "Bearer malformed" }, + headers: { authorization: `Bearer ${malformedToken}` }, }), env); expect(response.status).toBe(400); From 684f9e757a6ad0f21e250383fc63dd700f8e1d09 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 11:11:13 -0700 Subject: [PATCH 186/564] test(oidc): keep workflow-prefix fixture canonical at bearer boundary --- test/distributed-rate-limit.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/distributed-rate-limit.test.ts b/test/distributed-rate-limit.test.ts index c564603cc..6cd752134 100644 --- a/test/distributed-rate-limit.test.ts +++ b/test/distributed-rate-limit.test.ts @@ -24,11 +24,13 @@ function encodeSegment(value: unknown): string { return Buffer.from(JSON.stringify(value)).toString("base64url"); } +const canonicalSignature = Buffer.from([0]).toString("base64url"); + function oidcTokenWithWorkflowRef(workflowRef: string): string { return [ encodeSegment({ alg: "RS256", kid: "workflow-trust-test" }), encodeSegment({ job_workflow_ref: workflowRef }), - "signature", + canonicalSignature, ].join("."); } From 29c2a4597e8b09d3c3c9e808ef036e66841ebd66 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 11:12:03 -0700 Subject: [PATCH 187/564] test(oidc): align undecodable segment with canonical bearer boundary --- test/oidc-verification-residual-coverage.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/oidc-verification-residual-coverage.test.ts b/test/oidc-verification-residual-coverage.test.ts index b65d61bb2..b20a97c01 100644 --- a/test/oidc-verification-residual-coverage.test.ts +++ b/test/oidc-verification-residual-coverage.test.ts @@ -324,16 +324,15 @@ describe("OIDC verification residual coverage", () => { }); }); - it("classifies an undecodable base64url payload segment as a malformed token before upstream access", async () => { + it("rejects an undecodable base64url payload at the canonical bearer boundary before upstream access", async () => { const encodedHeader = encodeJson({ alg: "RS256", kid: signingKid }); const token = `${encodedHeader}.A.AA`; const { response, fetchedUrls } = await exchange(token); - expect(response.status).toBe(400); + expect(response.status).toBe(401); await expect(response.json()).resolves.toMatchObject({ ok: false, - error_code: "ERR_TOKEN_MALFORMED", - message: "OIDC token is malformed", + error_code: "ERR_AUTH_MISSING", }); expect(fetchedUrls).toEqual([]); }); From fa872d6a7a89591bc1545aaee68919b10ab1f960 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 11:12:38 -0700 Subject: [PATCH 188/564] test(oidc): bind noncanonical signature rejection to bearer boundary --- test/oidc-workflow-sha-cryptographic.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/oidc-workflow-sha-cryptographic.test.ts b/test/oidc-workflow-sha-cryptographic.test.ts index c347917c7..7e211339e 100644 --- a/test/oidc-workflow-sha-cryptographic.test.ts +++ b/test/oidc-workflow-sha-cryptographic.test.ts @@ -139,7 +139,7 @@ describe("cryptographic OIDC workflow source identity", () => { }); }); - it("rejects a signature segment whose non-canonical tail bits decode to the signed bytes", async () => { + it("rejects a signature segment whose non-canonical tail bits decode to the signed bytes at the bearer boundary", async () => { const now = Math.floor(Date.now() / 1000); const canonicalToken = await signedJwt({ iss: env.ALLOWED_ISSUER, @@ -159,7 +159,7 @@ describe("cryptographic OIDC workflow source identity", () => { vi.resetModules(); const { default: worker } = await import("../src/index"); - vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { const url = String(input); if (url === trustedDiscoveryUrl) { return Response.json({ jwks_uri: trustedJwksUrl }); @@ -185,11 +185,11 @@ describe("cryptographic OIDC workflow source identity", () => { env, ); - expect(response.status).toBe(400); + expect(response.status).toBe(401); await expect(response.json()).resolves.toMatchObject({ ok: false, - error_code: "ERR_TOKEN_MALFORMED", - message: "OIDC token is malformed", + error_code: "ERR_AUTH_MISSING", }); + expect(fetchSpy).not.toHaveBeenCalled(); }); }); From 7d361953bd3531face837bbb5c051cfd601d311b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 11:13:23 -0700 Subject: [PATCH 189/564] test(oidc): align runtime noncanonical segment boundary --- ...untime-workflow-prefilter-coverage.test.ts | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/test/runtime-workflow-prefilter-coverage.test.ts b/test/runtime-workflow-prefilter-coverage.test.ts index 5be06b6f6..794274e32 100644 --- a/test/runtime-workflow-prefilter-coverage.test.ts +++ b/test/runtime-workflow-prefilter-coverage.test.ts @@ -211,33 +211,19 @@ describe("runtime workflow-source prefilter coverage", () => { }); }); - it("does not derive workflow-source policy from padded base64url payload authority", async () => { - const response = await exchangeWithPayloadSegment( - `${workflowIdentityPayloadSegment()}=`, - "padded-base64url-prefilter", - ); + it("rejects padded base64url payload authority at the canonical bearer boundary", async () => { + const payload = `${workflowIdentityPayloadSegment()}=`; + const token = `${encodeJsonSegment({ alg: "RS256", kid: "padded-base64url-prefilter" })}.${payload}.AA`; - expect(response.status).toBe(400); - await expect(response.json()).resolves.toMatchObject({ - ok: false, - error_code: "ERR_TOKEN_MALFORMED", - }); + await expectMissingAuth({ authorization: `Bearer ${token}` }); }); - it("does not derive workflow-source policy from non-canonical base64url pad bits", async () => { + it("rejects non-canonical base64url pad bits at the canonical bearer boundary", async () => { const canonical = workflowIdentityPayloadSegment(); const nonCanonical = sameBytesNonCanonicalBase64Url(canonical); expect(Buffer.from(nonCanonical, "base64url")).toEqual(Buffer.from(canonical, "base64url")); + const token = `${encodeJsonSegment({ alg: "RS256", kid: "pad-bit-base64url-prefilter" })}.${nonCanonical}.AA`; - const response = await exchangeWithPayloadSegment( - nonCanonical, - "pad-bit-base64url-prefilter", - ); - - expect(response.status).toBe(400); - await expect(response.json()).resolves.toMatchObject({ - ok: false, - error_code: "ERR_TOKEN_MALFORMED", - }); + await expectMissingAuth({ authorization: `Bearer ${token}` }); }); }); From 0d4618505a60906dc75903aebae0fd74442d86ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 11:14:55 -0700 Subject: [PATCH 190/564] test(oidc): converge worker fixtures on canonical JWT framing --- test/worker.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/worker.test.ts b/test/worker.test.ts index 2f95eb051..9fb204b73 100644 --- a/test/worker.test.ts +++ b/test/worker.test.ts @@ -18,6 +18,8 @@ function encodeSegment(value: unknown): string { return Buffer.from(JSON.stringify(value)).toString("base64url"); } +const canonicalSignature = Buffer.from([0]).toString("base64url"); + function encodeBytes(bytes: ArrayBuffer): string { return Buffer.from(bytes).toString("base64url"); } @@ -324,9 +326,11 @@ describe("Noema worker", () => { }); it("reports malformed exchange tokens as JSON errors", async () => { + const malformedJsonHeader = Buffer.from("{", "utf8").toString("base64url"); + const malformedToken = `${malformedJsonHeader}.${encodeSegment({})}.${canonicalSignature}`; const response = await worker.fetch(new Request("https://noema.example/exchange", { method: "POST", - headers: { authorization: "Bearer malformed" }, + headers: { authorization: `Bearer ${malformedToken}` }, }), env); expect(response.status).toBe(400); @@ -345,7 +349,7 @@ describe("Noema worker", () => { const token = [ encodeSegment({ alg: "HS256", kid: "not-rsa" }), encodeSegment({}), - "signature", + canonicalSignature, ].join("."); const response = await worker.fetch(new Request("https://noema.example/exchange", { method: "POST", From 1652ee982baf3d996f9b168f92639c718f22d034 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 11:22:02 -0700 Subject: [PATCH 191/564] test(oidc): preserve deep-parser coverage behind canonical bearer framing --- test/oidc-workflow-sha-binding.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/oidc-workflow-sha-binding.test.ts b/test/oidc-workflow-sha-binding.test.ts index acf45068d..a6caa06b3 100644 --- a/test/oidc-workflow-sha-binding.test.ts +++ b/test/oidc-workflow-sha-binding.test.ts @@ -365,11 +365,13 @@ describe("production OIDC reusable-workflow source identity", () => { it("leaves malformed decoded claims to the bounded authoritative token parser", async () => { await expectDelegatedMalformedToken("e30.eA.eA", 400); - await expectDelegatedMalformedToken("e30.e30", 400); + await expectDelegatedMalformedToken("e30.e30", 401); await expectDelegatedMalformedToken(`e30.${encodeJson([])}.eA`, 401); }); it("does not decode a source-policy payload above the bounded JWT payload limit", async () => { - await expectDelegatedMalformedToken(`e30.${"a".repeat(8_193)}.eA`, 400); + const oversizedPayload = Buffer.alloc(6_145).toString("base64url"); + expect(oversizedPayload.length).toBeGreaterThan(8_192); + await expectDelegatedMalformedToken(`e30.${oversizedPayload}.eA`, 400); }); }); From e9b4ac97544c84fae86ff36cb1e87b9edee023fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 11:29:49 -0700 Subject: [PATCH 192/564] test(oidc): require current central workflow source commit --- 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 a7ffe81ee..f45852005 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 = - "139c22f74b96e213510d3f487a5bed3b71f3459b"; + "874f47b3856ca6bdb6bc71d48173b8f34ba7b9ca"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From d54c94a9158251da9c4dc4ed361d01e39c3c6472 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 11:30:20 -0700 Subject: [PATCH 193/564] fix(oidc): trust current central workflow source commit --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index fa4e69e79..a4317dc05 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 = "139c22f74b96e213510d3f487a5bed3b71f3459b" +ALLOWED_WORKFLOW_SHA = "874f47b3856ca6bdb6bc71d48173b8f34ba7b9ca" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From b5739c84e8603878b288077ee2be480feb5ed4e0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 11:31:05 -0700 Subject: [PATCH 194/564] docs(architecture): record current central workflow trust commit --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 07c2033e1..38902da10 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs readiness dispatch and delegates `/exchange`; `src/entrypoint.ts` applies the distributed rate limiter before `src/worker.ts` performs its denial-only exact workflow-ref precheck. `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `139c22f74b96e213510d3f487a5bed3b71f3459b`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `874f47b3856ca6bdb6bc71d48173b8f34ba7b9ca`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The protected workflow-ref parser and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 264513957117c34525ab961acd4270d6f5f29fea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 11:38:28 -0700 Subject: [PATCH 195/564] test(architecture): align workflow-source trust layering --- test/architecture-documentation.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/architecture-documentation.test.ts b/test/architecture-documentation.test.ts index 22de84624..d92b9b370 100644 --- a/test/architecture-documentation.test.ts +++ b/test/architecture-documentation.test.ts @@ -69,7 +69,7 @@ describe("authoritative architecture documentation", () => { expect(coreWorker).toContain('"Endpoint not found"'); }); - it("documents exact-ref workflow trust plus immutable workflow-source binding without moving the SHA check into the worker wrapper", () => { + it("documents exact-ref workflow trust plus immutable workflow-source binding without restoring an unauthenticated runtime SHA prefilter", () => { const architecture = readFileSync("ARCHITECTURE.md", "utf8"); const runtimeEntrypoint = readFileSync("src/runtime-entrypoint.ts", "utf8"); const worker = readFileSync("src/worker.ts", "utf8"); @@ -80,12 +80,13 @@ describe("authoritative architecture documentation", () => { expect(worker).not.toContain("workflow_sha"); expect(worker).not.toContain("job_workflow_sha"); expect(runtimeEntrypoint).toContain("ALLOWED_WORKFLOW_SHA"); - expect(runtimeEntrypoint).toContain("workflowSourceDecision"); + expect(runtimeEntrypoint).not.toContain("workflowSourceDecision"); expect(coreWorker).toContain("job_workflow_sha"); expect(coreWorker).toContain("workflow_sha"); expect(architecture).toContain("exact full workflow ref"); expect(architecture).toContain("immutable workflow-source SHA"); expect(architecture).toContain("operator authority bytes"); + expect(architecture).toContain("distributed rate limiting before unverified workflow-source claims"); }); it("keeps the canonical documentation audit aligned with integrated buyer/operator documentation", () => { From 5a9f55c38ea418e3eb2b4fafd7ecad28ac049c9d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 11:39:53 -0700 Subject: [PATCH 196/564] test(oidc): align malformed envelope boundary --- test/oidc-workflow-sha-binding.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/oidc-workflow-sha-binding.test.ts b/test/oidc-workflow-sha-binding.test.ts index a6caa06b3..5ee59a86c 100644 --- a/test/oidc-workflow-sha-binding.test.ts +++ b/test/oidc-workflow-sha-binding.test.ts @@ -365,7 +365,7 @@ describe("production OIDC reusable-workflow source identity", () => { it("leaves malformed decoded claims to the bounded authoritative token parser", async () => { await expectDelegatedMalformedToken("e30.eA.eA", 400); - await expectDelegatedMalformedToken("e30.e30", 401); + await expectDelegatedMalformedToken("e30.e30", 400); await expectDelegatedMalformedToken(`e30.${encodeJson([])}.eA`, 401); }); From a65d25d2976e09a82b71873005dc56c0b2a4881b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 11:41:59 -0700 Subject: [PATCH 197/564] test(oidc): align padded envelope rejection --- ...runtime-workflow-prefilter-coverage.test.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/runtime-workflow-prefilter-coverage.test.ts b/test/runtime-workflow-prefilter-coverage.test.ts index 794274e32..fb45f243d 100644 --- a/test/runtime-workflow-prefilter-coverage.test.ts +++ b/test/runtime-workflow-prefilter-coverage.test.ts @@ -215,7 +215,23 @@ describe("runtime workflow-source prefilter coverage", () => { const payload = `${workflowIdentityPayloadSegment()}=`; const token = `${encodeJsonSegment({ alg: "RS256", kid: "padded-base64url-prefilter" })}.${payload}.AA`; - await expectMissingAuth({ authorization: `Bearer ${token}` }); + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + "cf-connecting-ip": "203.0.113.126", + authorization: `Bearer ${token}`, + }, + }), + 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" }, + }); }); it("rejects non-canonical base64url pad bits at the canonical bearer boundary", async () => { From a175ea8f95b64d7bcc6ec5f14ed647af2aa266a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 12:04:32 -0700 Subject: [PATCH 198/564] test(oidc): cover canonical JSON member scanning --- ...bearer-authorization-canonical-segments.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/bearer-authorization-canonical-segments.test.ts b/test/bearer-authorization-canonical-segments.test.ts index c8d4f9b1b..0863c3071 100644 --- a/test/bearer-authorization-canonical-segments.test.ts +++ b/test/bearer-authorization-canonical-segments.test.ts @@ -12,6 +12,20 @@ describe("parseExactBearerToken canonical JWT segments", () => { expect(parseExactBearerToken(`Bearer ${token}`)).toBe(token); }); + it("recognizes top-level JSON keys when valid JSON whitespace precedes the member colon", () => { + const header = Buffer.from('{"alg" : "RS256","kid":"kid"}', "utf8").toString("base64url"); + const token = `${header}.${canonicalPayload}.${canonicalSignature}`; + + expect(parseExactBearerToken(`Bearer ${token}`)).toBe(token); + }); + + it("leaves malformed JSON-key escapes for the authoritative JSON parser instead of inventing duplicate authority", () => { + const malformedJsonHeader = Buffer.from('{"bad\\q":1}', "utf8").toString("base64url"); + const token = `${malformedJsonHeader}.${canonicalPayload}.${canonicalSignature}`; + + expect(parseExactBearerToken(`Bearer ${token}`)).toBe(token); + }); + it.each([ ["padded protected header", `${canonicalHeader}=.${canonicalPayload}.${canonicalSignature}`], ["padded payload", `${canonicalHeader}.${canonicalPayload}=.${canonicalSignature}`], From f880c33c8de37d9c4816ca969a855c780e08a19e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 12:04:59 -0700 Subject: [PATCH 199/564] refactor(oidc): bind canonical segment decode once --- src/bearer-authorization.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index 372a84e78..13c1add68 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -23,9 +23,7 @@ function decodeCanonicalBase64Url(segment: string): Uint8Array | undefined { } } -function decodeJwtJsonText(segment: string): string | undefined { - const bytes = decodeCanonicalBase64Url(segment); - if (bytes === undefined) return undefined; +function decodeJwtJsonText(bytes: Uint8Array): string | undefined { try { return new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes); } catch { @@ -33,8 +31,8 @@ function decodeJwtJsonText(segment: string): string | undefined { } } -function hasDuplicateTopLevelJsonKeys(segment: string): boolean { - const text = decodeJwtJsonText(segment); +function hasDuplicateTopLevelJsonKeys(bytes: Uint8Array): boolean { + const text = decodeJwtJsonText(bytes); if (text === undefined) return false; const seenKeys = new Set(); @@ -64,13 +62,12 @@ function hasDuplicateTopLevelJsonKeys(segment: string): boolean { if (text[lookahead] !== ":") continue; const encodedKey = text.slice(stringStart + 1, index); - let decodedKey: unknown; + let decodedKey: string; try { - decodedKey = JSON.parse(`"${encodedKey}"`); + decodedKey = JSON.parse(`"${encodedKey}"`) as string; } catch { return false; } - if (typeof decodedKey !== "string") return false; if (seenKeys.has(decodedKey)) return true; seenKeys.add(decodedKey); continue; @@ -108,14 +105,17 @@ export function parseExactBearerToken(authorization: string): string | undefined const token = canonicalBearerAuthorizationPattern.exec(authorization)?.[1]; if (!token) return undefined; const segments = token.split("."); - if (segments.length !== 3 || segments.some((segment) => decodeCanonicalBase64Url(segment) === undefined)) { - return undefined; - } + if (segments.length !== 3) return undefined; + + const decodedSegments = segments.map(decodeCanonicalBase64Url); + if (decodedSegments.some((bytes) => bytes === undefined)) return undefined; + const [headerBytes, payloadBytes] = decodedSegments as [Uint8Array, Uint8Array, Uint8Array]; + if ( segments[0].startsWith(utf8BomBase64UrlPrefix) || segments[1].startsWith(utf8BomBase64UrlPrefix) - || hasDuplicateTopLevelJsonKeys(segments[0]) - || hasDuplicateTopLevelJsonKeys(segments[1]) + || hasDuplicateTopLevelJsonKeys(headerBytes) + || hasDuplicateTopLevelJsonKeys(payloadBytes) ) return undefined; return token; } From 0abb0d91b52a25c2352be039631e345c4aabd48d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 12:05:38 -0700 Subject: [PATCH 200/564] test(oidc): preserve downstream parser defenses --- test/oidc-downstream-parser-defense.test.ts | 93 +++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 test/oidc-downstream-parser-defense.test.ts diff --git a/test/oidc-downstream-parser-defense.test.ts b/test/oidc-downstream-parser-defense.test.ts new file mode 100644 index 000000000..053c425d4 --- /dev/null +++ b/test/oidc-downstream-parser-defense.test.ts @@ -0,0 +1,93 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const 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: "unused-before-verification", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", +}; + +const base64UrlAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; +const canonicalHeader = Buffer.from(JSON.stringify({ alg: "RS256", kid: "kid" })).toString("base64url"); + +function sameBytesNonCanonicalBase64Url(segment: string): string { + if (segment.length % 4 !== 2 && segment.length % 4 !== 3) { + throw new Error("fixture requires an unpadded base64url tail"); + } + const lastIndex = base64UrlAlphabet.indexOf(segment.at(-1) ?? ""); + if (lastIndex < 0) throw new Error("fixture tail must be base64url"); + return `${segment.slice(0, -1)}${base64UrlAlphabet[lastIndex + 1]}`; +} + +async function exchangeWithParserRegression(token: string): Promise { + vi.resetModules(); + vi.doMock("../src/bearer-authorization", () => ({ + parseExactBearerToken: () => token, + })); + const { default: worker } = await import("../src/index"); + const fetchSpy = vi.spyOn(globalThis, "fetch"); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: "Bearer ignored-by-regression-seam", + "content-type": "application/json", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + env, + ); + + expect(fetchSpy).not.toHaveBeenCalled(); + return response; +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.doUnmock("../src/bearer-authorization"); + vi.resetModules(); +}); + +describe("authoritative OIDC verifier defense when the shared bearer parser regresses", () => { + it("still rejects a non-three-segment JWT before OIDC egress", async () => { + const response = await exchangeWithParserRegression(`${canonicalHeader}.e30`); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + }); + }); + + it("still rejects a segment whose base64url decoder throws before OIDC egress", async () => { + const response = await exchangeWithParserRegression(`${canonicalHeader}.%.AA`); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + }); + }); + + it("still rejects non-canonical base64url pad bits before OIDC egress", async () => { + const canonicalPayload = Buffer.from("{}", "utf8").toString("base64url"); + const nonCanonicalPayload = sameBytesNonCanonicalBase64Url(canonicalPayload); + expect(Buffer.from(nonCanonicalPayload, "base64url")).toEqual(Buffer.from(canonicalPayload, "base64url")); + + const response = await exchangeWithParserRegression(`${canonicalHeader}.${nonCanonicalPayload}.AA`); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + }); + }); +}); From d7169439a2f739e192b8a3ae3b1dad6ab73272ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 12:06:05 -0700 Subject: [PATCH 201/564] test(oidc): preserve protected parser defenses --- test/worker-downstream-parser-defense.test.ts | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 test/worker-downstream-parser-defense.test.ts diff --git a/test/worker-downstream-parser-defense.test.ts b/test/worker-downstream-parser-defense.test.ts new file mode 100644 index 000000000..c17765b1e --- /dev/null +++ b/test/worker-downstream-parser-defense.test.ts @@ -0,0 +1,124 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const base64UrlAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; +const canonicalHeader = Buffer.from(JSON.stringify({ alg: "RS256", kid: "kid" })).toString("base64url"); +const canonicalPayload = Buffer.from(JSON.stringify({ sub: "repo:ContextualWisdomLab/noema" })).toString("base64url"); + +function allowingRateLimiter(): DurableObjectNamespace { + return { + idFromName(name: string) { + return { toString: () => name } as DurableObjectId; + }, + get() { + return { + async fetch() { + return Response.json({ + allowed: true, + limit: 1000, + remaining: 999, + retry_after_seconds: 0, + }); + }, + } as unknown as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; +} + +const 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: "unused", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", + NOEMA_RATE_LIMITER: allowingRateLimiter(), +}; + +function sameBytesNonCanonicalBase64Url(segment: string): string { + if (segment.length % 4 !== 2 && segment.length % 4 !== 3) { + throw new Error("fixture requires an unpadded base64url tail"); + } + const lastIndex = base64UrlAlphabet.indexOf(segment.at(-1) ?? ""); + if (lastIndex < 0) throw new Error("fixture tail must be base64url"); + return `${segment.slice(0, -1)}${base64UrlAlphabet[lastIndex + 1]}`; +} + +async function requestThroughWorkerWithParserRegression(token: string): Promise { + vi.resetModules(); + vi.doMock("../src/bearer-authorization", () => ({ + parseExactBearerToken: () => token, + })); + vi.doMock("../src/index", () => ({ + default: { + fetch: async () => new Response(JSON.stringify({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + message: "authoritative parser rejected the token", + trace_id: "downstream-defense", + }), { + status: 400, + headers: { "content-type": "application/json; charset=utf-8" }, + }), + }, + })); + const { default: worker } = await import("../src/worker"); + + return worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: "Bearer ignored-by-regression-seam", + "cf-connecting-ip": "203.0.113.126", + }, + }), + env, + ); +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.doUnmock("../src/bearer-authorization"); + vi.doUnmock("../src/index"); + vi.resetModules(); +}); + +describe("protected worker defense when the shared bearer parser regresses", () => { + it("does not derive workflow claims from a non-three-segment token", async () => { + const response = await requestThroughWorkerWithParserRegression(`${canonicalHeader}.${canonicalPayload}`); + + expect(response.status).toBe(400); + expect(response.headers.get("x-rate-limit-scope")).toBe("distributed"); + }); + + it("does not derive workflow claims from an oversized payload segment", async () => { + const token = `${canonicalHeader}.${"A".repeat(8_193)}.AA`; + const response = await requestThroughWorkerWithParserRegression(token); + + expect(response.status).toBe(400); + expect(response.headers.get("x-rate-limit-scope")).toBe("distributed"); + }); + + it("does not derive workflow claims from non-canonical base64url pad bits", async () => { + const nonCanonicalPayload = sameBytesNonCanonicalBase64Url(canonicalPayload); + expect(Buffer.from(nonCanonicalPayload, "base64url")).toEqual(Buffer.from(canonicalPayload, "base64url")); + + const response = await requestThroughWorkerWithParserRegression( + `${canonicalHeader}.${nonCanonicalPayload}.AA`, + ); + + expect(response.status).toBe(400); + expect(response.headers.get("x-rate-limit-scope")).toBe("distributed"); + }); + + it("does not derive workflow claims when the base64url decoder throws", async () => { + const response = await requestThroughWorkerWithParserRegression(`${canonicalHeader}.%.AA`); + + expect(response.status).toBe(400); + expect(response.headers.get("x-rate-limit-scope")).toBe("distributed"); + }); +}); From 66c6736f1f14c69baffa9301e2deb571fcda481b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 12:06:56 -0700 Subject: [PATCH 202/564] test(oidc): require current central workflow commit --- 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 f45852005..b59691fbd 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 = - "874f47b3856ca6bdb6bc71d48173b8f34ba7b9ca"; + "5f3188b78e0eef1eb6ac2765f444ce0422cff91e"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 4660527d1321973e3bd099a992f9fa1e41fcdfa0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 12:07:13 -0700 Subject: [PATCH 203/564] fix(oidc): roll trust to current central workflow commit --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index a4317dc05..9a0ee011d 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 = "874f47b3856ca6bdb6bc71d48173b8f34ba7b9ca" +ALLOWED_WORKFLOW_SHA = "5f3188b78e0eef1eb6ac2765f444ce0422cff91e" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From e4fa99ee60ab9d470fccd1e81a20ac44c7aac490 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 12:07:53 -0700 Subject: [PATCH 204/564] docs(architecture): align current central workflow trust --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 38902da10..fc628e3b2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs readiness dispatch and delegates `/exchange`; `src/entrypoint.ts` applies the distributed rate limiter before `src/worker.ts` performs its denial-only exact workflow-ref precheck. `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `874f47b3856ca6bdb6bc71d48173b8f34ba7b9ca`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `5f3188b78e0eef1eb6ac2765f444ce0422cff91e`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The protected workflow-ref parser and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From a9dcbcad5738866813f3ac538d7d54116f30a5bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 13:02:06 -0700 Subject: [PATCH 205/564] 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 b59691fbd..ecde15074 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 = - "5f3188b78e0eef1eb6ac2765f444ce0422cff91e"; + "31e5f5337d8a8d844c456fe03f123c51b62416c9"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 8a45071d1b6dc86be07e5b13246da1964f9962b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 13:02:27 -0700 Subject: [PATCH 206/564] fix(oidc): roll forward trusted central workflow source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 9a0ee011d..21e6800eb 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 = "5f3188b78e0eef1eb6ac2765f444ce0422cff91e" +ALLOWED_WORKFLOW_SHA = "31e5f5337d8a8d844c456fe03f123c51b62416c9" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 8b6eb9be7d1c93d92ae45c07298ac66c40b1b89d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 13:03:32 -0700 Subject: [PATCH 207/564] docs(architecture): reconcile central workflow trust revision --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index fc628e3b2..31abf1442 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs readiness dispatch and delegates `/exchange`; `src/entrypoint.ts` applies the distributed rate limiter before `src/worker.ts` performs its denial-only exact workflow-ref precheck. `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `5f3188b78e0eef1eb6ac2765f444ce0422cff91e`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `31e5f5337d8a8d844c456fe03f123c51b62416c9`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The protected workflow-ref parser and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 903b20b6bd1ade8f313d74af2c9404ab637f64ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 14:01:12 -0700 Subject: [PATCH 208/564] test(oidc): require replay guard before token mint --- test/oidc-replay-binding-required.test.ts | 156 ++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 test/oidc-replay-binding-required.test.ts diff --git a/test/oidc-replay-binding-required.test.ts b/test/oidc-replay-binding-required.test.ts new file mode 100644 index 000000000..d10ab63fb --- /dev/null +++ b/test/oidc-replay-binding-required.test.ts @@ -0,0 +1,156 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import worker, { type Env } from "../src/worker"; + +const configuredRef = + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; +const configuredSha = "31e5f5337d8a8d844c456fe03f123c51b62416c9"; +const expectedRepositoryOwnerId = "295022177"; +const expectedWorkflowRepositoryId = "1274066402"; + +type MockFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise; + +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"], + ); +} + +async function signedOidcToken() { + const keyPair = await generateRsaKeyPair(); + const kid = `missing-replay-binding-${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: expectedRepositoryOwnerId, + repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, + job_workflow_ref: configuredRef, + job_workflow_sha: configuredSha, + sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", + jti: `missing-replay-binding-${crypto.randomUUID()}`, + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }); + const signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + keyPair.privateKey, + new TextEncoder().encode(`${header}.${payload}`), + ); + const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey); + return { + token: `${header}.${payload}.${encodeBytes(signature)}`, + jwk: { ...publicJwk, kid, kty: "RSA" }, + }; +} + +function namespaceReturning(handler: MockFetch): DurableObjectNamespace { + return { + idFromName(name: string) { + return { toString: () => name } as DurableObjectId; + }, + get() { + return { fetch: handler } as unknown as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; +} + +function allowRateLimiter(): DurableObjectNamespace { + return namespaceReturning(async () => + Response.json({ allowed: true, limit: 1000, remaining: 999, retry_after_seconds: 0 })); +} + +describe("required OIDC replay authority", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("fails closed before GitHub App token minting when the replay-guard binding is absent", async () => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + const { token: oidcToken, jwk } = await signedOidcToken(); + const appKeyPair = await generateRsaKeyPair(); + const appPrivateKey = pemFromPkcs8( + await crypto.subtle.exportKey("pkcs8", appKeyPair.privateKey), + ); + let tokenMintRequests = 0; + + vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => { + 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/12345/access_tokens") { + tokenMintRequests += 1; + return Response.json({ + token: "ghs_should_not_mint_without_replay_authority", + expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(), + }); + } + return new Response(`unexpected upstream call ${url} ${init?.method ?? "GET"}`, { status: 500 }); + }); + + 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: configuredRef, + ALLOWED_WORKFLOW_SHA: configuredSha, + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKey, + GITHUB_APP_INSTALLATION_ID: "12345", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", + NOEMA_RATE_LIMITER: allowRateLimiter(), + // NOEMA_OIDC_REPLAY_GUARD intentionally omitted: deployment authority is unavailable. + }; + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${oidcToken}`, + "content-type": "application/json", + "cf-connecting-ip": "203.0.113.83", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + env, + ); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_REPLAY", + message: "OIDC replay protection unavailable", + }); + expect(tokenMintRequests).toBe(0); + }); +}); From 5e87aad51d42ac6cdd8e945415c5a27318445a55 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 14:04:19 -0700 Subject: [PATCH 209/564] test(oidc): remove redundant replay binding regression --- test/oidc-replay-binding-required.test.ts | 156 ---------------------- 1 file changed, 156 deletions(-) delete mode 100644 test/oidc-replay-binding-required.test.ts diff --git a/test/oidc-replay-binding-required.test.ts b/test/oidc-replay-binding-required.test.ts deleted file mode 100644 index d10ab63fb..000000000 --- a/test/oidc-replay-binding-required.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import worker, { type Env } from "../src/worker"; - -const configuredRef = - "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; -const configuredSha = "31e5f5337d8a8d844c456fe03f123c51b62416c9"; -const expectedRepositoryOwnerId = "295022177"; -const expectedWorkflowRepositoryId = "1274066402"; - -type MockFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise; - -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"], - ); -} - -async function signedOidcToken() { - const keyPair = await generateRsaKeyPair(); - const kid = `missing-replay-binding-${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: expectedRepositoryOwnerId, - repository: "ContextualWisdomLab/.github", - repository_id: expectedWorkflowRepositoryId, - job_workflow_ref: configuredRef, - job_workflow_sha: configuredSha, - sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", - jti: `missing-replay-binding-${crypto.randomUUID()}`, - exp: now + 300, - nbf: now - 30, - iat: now - 30, - }); - const signature = await crypto.subtle.sign( - "RSASSA-PKCS1-v1_5", - keyPair.privateKey, - new TextEncoder().encode(`${header}.${payload}`), - ); - const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey); - return { - token: `${header}.${payload}.${encodeBytes(signature)}`, - jwk: { ...publicJwk, kid, kty: "RSA" }, - }; -} - -function namespaceReturning(handler: MockFetch): DurableObjectNamespace { - return { - idFromName(name: string) { - return { toString: () => name } as DurableObjectId; - }, - get() { - return { fetch: handler } as unknown as DurableObjectStub; - }, - } as unknown as DurableObjectNamespace; -} - -function allowRateLimiter(): DurableObjectNamespace { - return namespaceReturning(async () => - Response.json({ allowed: true, limit: 1000, remaining: 999, retry_after_seconds: 0 })); -} - -describe("required OIDC replay authority", () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("fails closed before GitHub App token minting when the replay-guard binding is absent", async () => { - vi.spyOn(console, "log").mockImplementation(() => undefined); - const { token: oidcToken, jwk } = await signedOidcToken(); - const appKeyPair = await generateRsaKeyPair(); - const appPrivateKey = pemFromPkcs8( - await crypto.subtle.exportKey("pkcs8", appKeyPair.privateKey), - ); - let tokenMintRequests = 0; - - vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => { - 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/12345/access_tokens") { - tokenMintRequests += 1; - return Response.json({ - token: "ghs_should_not_mint_without_replay_authority", - expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(), - }); - } - return new Response(`unexpected upstream call ${url} ${init?.method ?? "GET"}`, { status: 500 }); - }); - - 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: configuredRef, - ALLOWED_WORKFLOW_SHA: configuredSha, - GITHUB_API_BASE: "https://api.github.com", - GITHUB_APP_ID: "1", - GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKey, - GITHUB_APP_INSTALLATION_ID: "12345", - NOEMA_RATE_LIMIT_PER_MINUTE: "1000", - NOEMA_RATE_LIMITER: allowRateLimiter(), - // NOEMA_OIDC_REPLAY_GUARD intentionally omitted: deployment authority is unavailable. - }; - - const response = await worker.fetch( - new Request("https://noema.example/exchange", { - method: "POST", - headers: { - authorization: `Bearer ${oidcToken}`, - "content-type": "application/json", - "cf-connecting-ip": "203.0.113.83", - }, - body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), - }), - env, - ); - - expect(response.status).toBe(503); - await expect(response.json()).resolves.toMatchObject({ - ok: false, - error_code: "ERR_AUTH_REPLAY", - message: "OIDC replay protection unavailable", - }); - expect(tokenMintRequests).toBe(0); - }); -}); From 1ce5746247f9225f01dc6e22c08caa33f939ee75 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 14:06:26 -0700 Subject: [PATCH 210/564] test(egress): reject non-canonical bearer framing --- ...d-fetch-authorization-canonicality.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 test/outbound-fetch-authorization-canonicality.test.ts diff --git a/test/outbound-fetch-authorization-canonicality.test.ts b/test/outbound-fetch-authorization-canonicality.test.ts new file mode 100644 index 000000000..c63b392f3 --- /dev/null +++ b/test/outbound-fetch-authorization-canonicality.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { isTrustedCredentialEgressRequest } from "../src/outbound-fetch-policy"; + +const installationLookup = "https://api.github.com/app/installations"; + +describe("credential-egress Authorization framing", () => { + it("accepts exactly one ASCII space between Bearer and the credential", () => { + expect(isTrustedCredentialEgressRequest(installationLookup, { + method: "GET", + headers: { authorization: "Bearer canonical-token" }, + })).toBe(true); + }); + + it.each([ + "Bearer\tcanonical-token", + "Bearer canonical-token", + "Bearer\u00a0canonical-token", + ])("rejects non-canonical Bearer separators before credential egress: %s", (authorization) => { + expect(isTrustedCredentialEgressRequest(installationLookup, { + method: "GET", + headers: { authorization }, + })).toBe(false); + }); +}); From 886ed98cae4c2a94ef2829ebf397cb1a3e478054 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 14:08:12 -0700 Subject: [PATCH 211/564] fix(egress): require canonical bearer framing --- src/outbound-fetch-policy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index b6c7763dc..a38a49b03 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -300,11 +300,11 @@ export function isTrustedCredentialEgressRequest( return false; } - const authorization = headers.get("authorization")?.trim(); + const authorization = headers.get("authorization"); if (!authorization) { return method === "GET" && !bodyPresent; } - if (!/^Bearer\s+\S+$/i.test(authorization)) { + if (!/^Bearer [\x21-\x7e]+$/i.test(authorization)) { return false; } From f7f7143b9ce83521ec75b7606e0d9ff7839a80e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 15:02:32 -0700 Subject: [PATCH 212/564] 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 ecde15074..7b46b6955 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 = - "31e5f5337d8a8d844c456fe03f123c51b62416c9"; + "548a97560070d31b03d14bee0ac98a990bd88482"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 0d46e2300c79aec11139984eed9bfca85adeebff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 15:03:11 -0700 Subject: [PATCH 213/564] fix(oidc): roll trusted workflow source to current central commit --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 21e6800eb..65cfb15f2 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 = "31e5f5337d8a8d844c456fe03f123c51b62416c9" +ALLOWED_WORKFLOW_SHA = "548a97560070d31b03d14bee0ac98a990bd88482" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 74997bd4c13a13f35bb1847e4ca9648f21059cb4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 15:03:54 -0700 Subject: [PATCH 214/564] docs(architecture): align central workflow trust authority --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 31abf1442..005d63bf4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs readiness dispatch and delegates `/exchange`; `src/entrypoint.ts` applies the distributed rate limiter before `src/worker.ts` performs its denial-only exact workflow-ref precheck. `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `31e5f5337d8a8d844c456fe03f123c51b62416c9`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `548a97560070d31b03d14bee0ac98a990bd88482`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The protected workflow-ref parser and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From f477c1f866ae56f8dc49cc4665929caab5faa180 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 18:07:12 -0700 Subject: [PATCH 215/564] test(rate-limit): enforce documented local limit ceiling --- test/local-rate-limit-config-boundary.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 test/local-rate-limit-config-boundary.test.ts diff --git a/test/local-rate-limit-config-boundary.test.ts b/test/local-rate-limit-config-boundary.test.ts new file mode 100644 index 000000000..6a15e06b0 --- /dev/null +++ b/test/local-rate-limit-config-boundary.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it, vi } from "vitest"; +import worker, { type Env } from "../src/index"; + +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: "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: "unused", + NOEMA_RATE_LIMIT_PER_MINUTE: "50000", +}; + +describe("local rate-limit configuration boundary", () => { + it("clamps defense-in-depth exchange throttling to the documented 10,000 request ceiling", async () => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + const request = () => new Request("https://noema.example/exchange", { + method: "POST", + headers: { "cf-connecting-ip": "203.0.113.231" }, + }); + + for (let attempt = 0; attempt < 10_000; attempt += 1) { + const response = await worker.fetch(request(), env); + expect(response.status).toBe(401); + } + + const rejected = await worker.fetch(request(), env); + expect(rejected.status).toBe(429); + expect(rejected.headers.get("retry-after")).toBeTruthy(); + await expect(rejected.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_RATE_LIMIT", + }); + }, 30_000); +}); From 9d7cafc3cb3bc82a06d017201f0a3793930e931d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 18:08:43 -0700 Subject: [PATCH 216/564] fix(rate-limit): clamp local defense-in-depth ceiling --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 3b7e14940..58f7685b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -193,7 +193,7 @@ function configuredRateLimit(env: Env): number { if (!Number.isFinite(limit) || limit <= 0) return 60; const normalizedLimit = Math.floor(limit); if (normalizedLimit <= 0) return 60; - return normalizedLimit; + return Math.min(normalizedLimit, 10_000); } function valueType(value: unknown): string { From fdf220d4fcf1bafe25e01e4466506b2ffb8d18ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 19:00:38 -0700 Subject: [PATCH 217/564] test(trace): reject normalized request-id authority --- test/trace-header-coverage.test.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/trace-header-coverage.test.ts b/test/trace-header-coverage.test.ts index e3ebd6fa3..bd8fd03b7 100644 --- a/test/trace-header-coverage.test.ts +++ b/test/trace-header-coverage.test.ts @@ -28,4 +28,18 @@ describe("trace header selection", () => { expect(payload.trace_id).toBe("correlation:trace_456"); expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); }); -}); + + it("does not normalize surrounding whitespace into a different trace identity", async () => { + const response = await worker.fetch(new Request("https://noema.example/health", { + headers: { + "x-request-id": " request.trace-123 ", + "x-correlation-id": "correlation:trace_456", + }, + }), env); + + expect(response.status).toBe(200); + const payload = await response.json() as { trace_id: string }; + expect(payload.trace_id).toBe("correlation:trace_456"); + expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); + }); +}); \ No newline at end of file From c2e5e3a24aed0516e493b166e0724c67962bd60e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 19:02:58 -0700 Subject: [PATCH 218/564] test(trace): drop unowned normalization hypothesis --- test/trace-header-coverage.test.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/trace-header-coverage.test.ts b/test/trace-header-coverage.test.ts index bd8fd03b7..ec482aeec 100644 --- a/test/trace-header-coverage.test.ts +++ b/test/trace-header-coverage.test.ts @@ -28,18 +28,4 @@ describe("trace header selection", () => { expect(payload.trace_id).toBe("correlation:trace_456"); expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); }); - - it("does not normalize surrounding whitespace into a different trace identity", async () => { - const response = await worker.fetch(new Request("https://noema.example/health", { - headers: { - "x-request-id": " request.trace-123 ", - "x-correlation-id": "correlation:trace_456", - }, - }), env); - - expect(response.status).toBe(200); - const payload = await response.json() as { trace_id: string }; - expect(payload.trace_id).toBe("correlation:trace_456"); - expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); - }); }); \ No newline at end of file From 21b50ba3808a15d91e966448b1f9c58fe02b2448 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 19:03:25 -0700 Subject: [PATCH 219/564] test(cache): reject non-canonical TTL authority --- test/cache-ttl.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/cache-ttl.test.ts b/test/cache-ttl.test.ts index 286443ad0..5816feef8 100644 --- a/test/cache-ttl.test.ts +++ b/test/cache-ttl.test.ts @@ -9,6 +9,9 @@ describe("configured cache TTL normalization", () => { { raw: "0.5", expected: 300_000, reason: "does not normalize a positive fraction to zero" }, { raw: "1.9", expected: 1_000, reason: "preserves existing whole-second floor semantics" }, { raw: "7200", expected: 3_600_000, reason: "caps excessive configuration" }, + { raw: " 60 ", expected: 300_000, reason: "does not trim operator-controlled TTL authority" }, + { raw: "+60", expected: 300_000, reason: "rejects signed decimal aliases" }, + { raw: "0x10", expected: 300_000, reason: "rejects non-decimal numeric aliases" }, ])("$reason", ({ raw, expected }) => { expect(configuredTtlMs(raw, 300, 3600)).toBe(expected); }); From 2188fffef46f6f1bac2773ec1fd45111a3a55a27 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 19:04:54 -0700 Subject: [PATCH 220/564] fix(cache): require canonical TTL authority --- src/cache-ttl.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/cache-ttl.ts b/src/cache-ttl.ts index abc088e38..1ff24b05e 100644 --- a/src/cache-ttl.ts +++ b/src/cache-ttl.ts @@ -1,13 +1,17 @@ +const canonicalUnsignedDecimalSecondsPattern = /^(?:0|[1-9]\d*)(?:\.\d+)?$/; + /** * Convert a cache TTL configuration into bounded milliseconds. * - * Noema accepts positive fractional configuration for compatibility with the - * existing numeric environment contract, but cache expiry must never normalize - * to zero. Values that are non-finite, non-positive, or smaller than one whole - * second therefore fall back to the reviewed default. Larger values are floored - * and capped before conversion to milliseconds. + * Noema accepts positive fractional decimal configuration for compatibility with + * the existing numeric environment contract, but cache expiry must never normalize + * to zero. Operator-provided values must already be canonical unsigned decimal + * strings; surrounding whitespace, signs, hexadecimal/exponent aliases, leading-zero + * integer spellings, non-finite values, non-positive values, or values smaller than + * one whole second fall back to the reviewed default. Larger values are floored and + * capped before conversion to milliseconds. * - * @param raw optional environment value expressed in seconds + * @param raw optional environment value expressed in canonical decimal seconds * @param defaultSeconds safe fallback TTL in seconds * @param maxSeconds maximum accepted TTL in seconds * @returns a positive bounded TTL in milliseconds @@ -17,6 +21,9 @@ export function configuredTtlMs( defaultSeconds: number, maxSeconds: number, ): number { + if (raw !== undefined && !canonicalUnsignedDecimalSecondsPattern.test(raw)) { + return defaultSeconds * 1000; + } const seconds = Number(raw ?? String(defaultSeconds)); if (!Number.isFinite(seconds) || seconds <= 0) return defaultSeconds * 1000; const normalizedSeconds = Math.floor(seconds); From 2eadc67cb39bc88319c4c954d5b4584d65732238 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:04:23 -0700 Subject: [PATCH 221/564] test(egress): reject normalized authorization framing --- ...outbound-fetch-authorization-canonicality.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/outbound-fetch-authorization-canonicality.test.ts b/test/outbound-fetch-authorization-canonicality.test.ts index c63b392f3..67a65baa8 100644 --- a/test/outbound-fetch-authorization-canonicality.test.ts +++ b/test/outbound-fetch-authorization-canonicality.test.ts @@ -21,4 +21,16 @@ describe("credential-egress Authorization framing", () => { headers: { authorization }, })).toBe(false); }); + + it.each([ + " Bearer canonical-token", + "\tBearer canonical-token", + "Bearer canonical-token ", + "Bearer canonical-token\t", + ])("rejects credential framing that Headers would silently trim: %s", (authorization) => { + expect(isTrustedCredentialEgressRequest(installationLookup, { + method: "GET", + headers: { authorization }, + })).toBe(false); + }); }); From b8a3b86cf40bc8e61461dbbf578695d690d5323b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:05:57 -0700 Subject: [PATCH 222/564] fix(egress): preserve raw authorization authority --- src/outbound-fetch-policy.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index a38a49b03..31c524ee2 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -78,6 +78,18 @@ function outboundHeaders(input: RequestInfo | URL, init: RequestInit | undefined return new Headers(); } +function rawAuthorizationHeaderFromInit(headersInit: HeadersInit | undefined): string | null | undefined { + if (headersInit === undefined || headersInit instanceof Headers) return undefined; + const entries = Array.isArray(headersInit) ? headersInit : Object.entries(headersInit); + let authorization: string | undefined; + for (const [name, value] of entries) { + if (name.toLowerCase() !== "authorization") continue; + if (authorization !== undefined || typeof value !== "string") return null; + authorization = value; + } + return authorization; +} + function outboundBodyPresent(input: RequestInfo | URL, init: RequestInit | undefined): boolean { if ( init @@ -304,7 +316,12 @@ export function isTrustedCredentialEgressRequest( if (!authorization) { return method === "GET" && !bodyPresent; } - if (!/^Bearer [\x21-\x7e]+$/i.test(authorization)) { + const rawAuthorization = rawAuthorizationHeaderFromInit(init?.headers); + if ( + rawAuthorization === null + || (rawAuthorization !== undefined && rawAuthorization !== authorization) + || !/^Bearer [\x21-\x7e]+$/i.test(authorization) + ) { return false; } From dc783bf7281172fc87339ccd3922da9e8e61ce61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:09:05 -0700 Subject: [PATCH 223/564] test(egress): require observable raw credential framing --- ...d-fetch-authorization-canonicality.test.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/test/outbound-fetch-authorization-canonicality.test.ts b/test/outbound-fetch-authorization-canonicality.test.ts index 67a65baa8..7a48cd864 100644 --- a/test/outbound-fetch-authorization-canonicality.test.ts +++ b/test/outbound-fetch-authorization-canonicality.test.ts @@ -4,10 +4,14 @@ import { isTrustedCredentialEgressRequest } from "../src/outbound-fetch-policy"; const installationLookup = "https://api.github.com/app/installations"; describe("credential-egress Authorization framing", () => { - it("accepts exactly one ASCII space between Bearer and the credential", () => { + it("accepts exactly one ASCII space between Bearer and the credential from raw RequestInit headers", () => { expect(isTrustedCredentialEgressRequest(installationLookup, { method: "GET", - headers: { authorization: "Bearer canonical-token" }, + headers: { authorization: "Bearer canonical-token", accept: "application/json" }, + })).toBe(true); + expect(isTrustedCredentialEgressRequest(installationLookup, { + method: "GET", + headers: [["authorization", "Bearer canonical-token"]], })).toBe(true); }); @@ -33,4 +37,24 @@ describe("credential-egress Authorization framing", () => { headers: { authorization }, })).toBe(false); }); + + it("rejects pre-normalized authorization containers whose original framing is no longer observable", () => { + expect(isTrustedCredentialEgressRequest(installationLookup, { + method: "GET", + headers: new Headers({ authorization: "Bearer canonical-token" }), + })).toBe(false); + expect(isTrustedCredentialEgressRequest(new Request(installationLookup, { + headers: { authorization: "Bearer canonical-token" }, + }))).toBe(false); + }); + + it("rejects duplicate raw authorization fields before Headers can combine them", () => { + expect(isTrustedCredentialEgressRequest(installationLookup, { + method: "GET", + headers: [ + ["authorization", "Bearer canonical-token"], + ["Authorization", "Bearer second-token"], + ], + })).toBe(false); + }); }); From 2b58c4d5f60424bfd0ada2df004c1ce11f7809d5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:09:47 -0700 Subject: [PATCH 224/564] test(egress): reject pre-normalized credential containers --- test/outbound-request-compartment.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/outbound-request-compartment.test.ts b/test/outbound-request-compartment.test.ts index 12031e91d..3177a1c6b 100644 --- a/test/outbound-request-compartment.test.ts +++ b/test/outbound-request-compartment.test.ts @@ -52,11 +52,14 @@ describe("outbound credential request compartmentalization", () => { })).toBe(false); }); - it("allows public bodyless GitHub GETs and only the two reviewed App-JWT operations", () => { + it("allows public bodyless GitHub GETs and only reviewed App-JWT operations with observable raw framing", () => { expect(isTrustedCredentialEgressRequest(unrelatedGithubApiUrl)).toBe(true); + expect(isTrustedCredentialEgressRequest(repositoryInstallationUrl, { + headers: { authorization: "Bearer app-jwt" }, + })).toBe(true); expect(isTrustedCredentialEgressRequest(new Request(repositoryInstallationUrl, { headers: { authorization: "Bearer app-jwt" }, - }))).toBe(true); + }))).toBe(false); expect(isTrustedCredentialEgressRequest(installationTokenUrl, { method: "POST", headers: { authorization: "Bearer app-jwt" }, From e61da318825229f79d6a5592e95befd6cd12f008 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:10:55 -0700 Subject: [PATCH 225/564] fix(egress): reject pre-normalized credentials --- src/outbound-fetch-policy.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 31c524ee2..64a40d439 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -84,7 +84,7 @@ function rawAuthorizationHeaderFromInit(headersInit: HeadersInit | undefined): s let authorization: string | undefined; for (const [name, value] of entries) { if (name.toLowerCase() !== "authorization") continue; - if (authorization !== undefined || typeof value !== "string") return null; + if (authorization !== undefined) return null; authorization = value; } return authorization; @@ -318,8 +318,9 @@ export function isTrustedCredentialEgressRequest( } const rawAuthorization = rawAuthorizationHeaderFromInit(init?.headers); if ( - rawAuthorization === null - || (rawAuthorization !== undefined && rawAuthorization !== authorization) + rawAuthorization === undefined + || rawAuthorization === null + || rawAuthorization !== authorization || !/^Bearer [\x21-\x7e]+$/i.test(authorization) ) { return false; From 96929f9856d79bb1d5d9dbc53e5fd1f58ee05302 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:15:08 -0700 Subject: [PATCH 226/564] test(egress): align Request credential provenance --- test/installation-token-scope-policy.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/installation-token-scope-policy.test.ts b/test/installation-token-scope-policy.test.ts index 73f998e43..8b492c58d 100644 --- a/test/installation-token-scope-policy.test.ts +++ b/test/installation-token-scope-policy.test.ts @@ -112,7 +112,7 @@ describe("installation token scope policy", () => { expect(isTrustedCredentialEgressRequest(request)).toBe(false); }); - it("accepts an explicit inspected body override for a Request input", () => { + it("rejects a Request credential whose original framing is no longer observable even when the body is overridden", () => { const request = new Request(installationTokenUrl, { method: "POST", headers: authorization, @@ -122,6 +122,19 @@ describe("installation token scope policy", () => { expect(isTrustedCredentialEgressRequest(request, { method: "POST", body: leastPrivilegeBody, + })).toBe(false); + }); + + it("accepts a Request target only when credential framing and inspected body are supplied explicitly", () => { + const request = new Request(installationTokenUrl, { + method: "POST", + body: "opaque-original-body", + }); + + expect(isTrustedCredentialEgressRequest(request, { + method: "POST", + headers: authorization, + body: leastPrivilegeBody, })).toBe(true); }); }); From 8a53e9b693e74417efe56e1be6d36bfb6b8f7919 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:15:48 -0700 Subject: [PATCH 227/564] test(egress): preserve Request with explicit credential provenance --- test/outbound-fetch-policy.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/outbound-fetch-policy.test.ts b/test/outbound-fetch-policy.test.ts index f50f5f641..1d2dcc657 100644 --- a/test/outbound-fetch-policy.test.ts +++ b/test/outbound-fetch-policy.test.ts @@ -46,10 +46,12 @@ describe("credential-bearing outbound fetch policy", () => { const wrapped = createFailClosedFetch(rawFetch); const request = new Request("https://api.github.com/app/installations", { method: "GET", - headers: { authorization: "Bearer sensitive" }, }); - const response = await wrapped(request, { redirect: "follow" }); + const response = await wrapped(request, { + redirect: "follow", + headers: { authorization: "Bearer sensitive" }, + }); expect(response.status).toBe(200); expect(response.headers.get("content-length")).toBeNull(); @@ -57,6 +59,7 @@ describe("credential-bearing outbound fetch policy", () => { const [forwardedRequest, forwardedInit] = rawFetch.mock.calls[0]; expect(forwardedRequest).toBe(request); expect(forwardedInit?.redirect).toBe("manual"); + expect(forwardedInit?.headers).toEqual({ authorization: "Bearer sensitive" }); expect(forwardedInit?.signal).toBeInstanceOf(AbortSignal); }); From bf4dfa4c343a41a3a06baf136313066b17fb9441 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:19:09 -0700 Subject: [PATCH 228/564] test(egress): cover inherited opaque token body --- test/installation-token-scope-policy.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/installation-token-scope-policy.test.ts b/test/installation-token-scope-policy.test.ts index 8b492c58d..864f07fa8 100644 --- a/test/installation-token-scope-policy.test.ts +++ b/test/installation-token-scope-policy.test.ts @@ -112,6 +112,18 @@ describe("installation token scope policy", () => { expect(isTrustedCredentialEgressRequest(request)).toBe(false); }); + it("rejects an inherited opaque Request body even when raw credential framing is supplied explicitly", () => { + const request = new Request(installationTokenUrl, { + method: "POST", + body: leastPrivilegeBody, + }); + + expect(isTrustedCredentialEgressRequest(request, { + method: "POST", + headers: authorization, + })).toBe(false); + }); + it("rejects a Request credential whose original framing is no longer observable even when the body is overridden", () => { const request = new Request(installationTokenUrl, { method: "POST", From bedee5af86b470d689c4108f52eaad9fde2f78d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:30:11 -0700 Subject: [PATCH 229/564] test(rate-limit): reject non-canonical distributed limit text --- test/rate-limit-config-regression.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/rate-limit-config-regression.test.ts b/test/rate-limit-config-regression.test.ts index 7f9a26153..e0dbdb388 100644 --- a/test/rate-limit-config-regression.test.ts +++ b/test/rate-limit-config-regression.test.ts @@ -11,4 +11,14 @@ describe("distributed rate-limit configuration regression", () => { expect(configuredDistributedRateLimit("1")).toBe(1); expect(configuredDistributedRateLimit("1.9")).toBe(1); }); -}); + + it("does not normalize alternate textual spellings into rate-limit authority", () => { + expect(configuredDistributedRateLimit(" 120 ")).toBe(60); + expect(configuredDistributedRateLimit("+120")).toBe(60); + expect(configuredDistributedRateLimit("0120")).toBe(60); + expect(configuredDistributedRateLimit("0x78")).toBe(60); + expect(configuredDistributedRateLimit("1.2e2")).toBe(60); + expect(configuredDistributedRateLimit("120")).toBe(120); + expect(configuredDistributedRateLimit("10001")).toBe(10_000); + }); +}); \ No newline at end of file From daf118a402cc8eba879749d4036ca9c2941f42f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:31:44 -0700 Subject: [PATCH 230/564] fix(rate-limit): require canonical distributed limit text --- src/rate-limit.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/rate-limit.ts b/src/rate-limit.ts index f29f782fe..16ad11193 100644 --- a/src/rate-limit.ts +++ b/src/rate-limit.ts @@ -86,12 +86,18 @@ export function isJsonMediaType(raw: string | null): boolean { } /** - * Normalizes an operator-supplied per-minute request limit into the bounded production configuration range. + * Converts an operator-supplied per-minute request limit into the bounded production configuration range. + * Alternate textual spellings are not normalized into authority: the configured value must already be + * canonical unsigned decimal text, while positive fractional values retain the existing floor semantics. * @param raw Optional textual limit from deployment configuration. * @returns A positive integer no greater than the hard maximum, or the safe default when input is invalid. */ export function configuredDistributedRateLimit(raw: string | undefined): number { - const parsed = Number(raw ?? String(DEFAULT_RATE_LIMIT_PER_MINUTE)); + const candidate = raw ?? String(DEFAULT_RATE_LIMIT_PER_MINUTE); + if (!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(candidate)) { + return DEFAULT_RATE_LIMIT_PER_MINUTE; + } + const parsed = Number(candidate); if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_RATE_LIMIT_PER_MINUTE; const normalized = Math.floor(parsed); if (normalized <= 0) return DEFAULT_RATE_LIMIT_PER_MINUTE; From 042f83b3f2bd355595b97607afe4f0af4f5717e0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:33:33 -0700 Subject: [PATCH 231/564] test(rate-limit): reject non-canonical local limit text --- test/local-rate-limit-config-boundary.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/local-rate-limit-config-boundary.test.ts b/test/local-rate-limit-config-boundary.test.ts index 6a15e06b0..059804330 100644 --- a/test/local-rate-limit-config-boundary.test.ts +++ b/test/local-rate-limit-config-boundary.test.ts @@ -35,4 +35,19 @@ describe("local rate-limit configuration boundary", () => { error_code: "ERR_RATE_LIMIT", }); }, 30_000); + + it("does not normalize alternate textual spellings into local throttle authority", async () => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + const nonCanonicalEnv: Env = { + ...env, + NOEMA_RATE_LIMIT_PER_MINUTE: " 1 ", + }; + const request = () => new Request("https://noema.example/exchange", { + method: "POST", + headers: { "cf-connecting-ip": "203.0.113.232" }, + }); + + expect((await worker.fetch(request(), nonCanonicalEnv)).status).toBe(401); + expect((await worker.fetch(request(), nonCanonicalEnv)).status).toBe(401); + }); }); From 2a77e10d28ba17e9e7047835702147a2c38b335f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:35:46 -0700 Subject: [PATCH 232/564] fix(rate-limit): require canonical local limit text --- src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 58f7685b2..f6ca5c327 100644 --- a/src/index.ts +++ b/src/index.ts @@ -189,7 +189,9 @@ function safeHash(input: string): string { } function configuredRateLimit(env: Env): number { - const limit = Number(env.NOEMA_RATE_LIMIT_PER_MINUTE ?? "60"); + const candidate = env.NOEMA_RATE_LIMIT_PER_MINUTE ?? "60"; + if (!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(candidate)) return 60; + const limit = Number(candidate); if (!Number.isFinite(limit) || limit <= 0) return 60; const normalizedLimit = Math.floor(limit); if (normalizedLimit <= 0) return 60; From b270516bab3a809e5d4ef12a67f04b1f3221e3b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:36:08 -0700 Subject: [PATCH 233/564] test(rate-limit): restore canonical text-file boundary --- test/rate-limit-config-regression.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/rate-limit-config-regression.test.ts b/test/rate-limit-config-regression.test.ts index e0dbdb388..18c13345f 100644 --- a/test/rate-limit-config-regression.test.ts +++ b/test/rate-limit-config-regression.test.ts @@ -21,4 +21,4 @@ describe("distributed rate-limit configuration regression", () => { expect(configuredDistributedRateLimit("120")).toBe(120); expect(configuredDistributedRateLimit("10001")).toBe(10_000); }); -}); \ No newline at end of file +}); From f0df87ed6c4418294db46c4d66636f81f71a53ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:38:28 -0700 Subject: [PATCH 234/564] test(rate-limit): cover absent distributed configuration --- test/rate-limit-config-regression.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/rate-limit-config-regression.test.ts b/test/rate-limit-config-regression.test.ts index 18c13345f..8359e43a1 100644 --- a/test/rate-limit-config-regression.test.ts +++ b/test/rate-limit-config-regression.test.ts @@ -2,6 +2,10 @@ import { describe, expect, it } from "vitest"; import { configuredDistributedRateLimit } from "../src/rate-limit"; describe("distributed rate-limit configuration regression", () => { + it("uses the safe default when distributed rate-limit configuration is absent", () => { + expect(configuredDistributedRateLimit(undefined)).toBe(60); + }); + it("never normalizes a positive sub-unit rate limit to zero", () => { expect(configuredDistributedRateLimit("0.5")).toBe(60); expect(configuredDistributedRateLimit("0.999999")).toBe(60); From 7fbc46a05f8517e49ba181d9a36b655fb96bef6b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 20:41:36 -0700 Subject: [PATCH 235/564] test(rate-limit): cover canonical zero rejection --- test/rate-limit-config-regression.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/rate-limit-config-regression.test.ts b/test/rate-limit-config-regression.test.ts index 8359e43a1..ea31f2abc 100644 --- a/test/rate-limit-config-regression.test.ts +++ b/test/rate-limit-config-regression.test.ts @@ -6,6 +6,10 @@ describe("distributed rate-limit configuration regression", () => { expect(configuredDistributedRateLimit(undefined)).toBe(60); }); + it("rejects a canonical zero rate limit instead of creating a disabled throttle", () => { + expect(configuredDistributedRateLimit("0")).toBe(60); + }); + it("never normalizes a positive sub-unit rate limit to zero", () => { expect(configuredDistributedRateLimit("0.5")).toBe(60); expect(configuredDistributedRateLimit("0.999999")).toBe(60); From 2970c0900a85f4d9b30057fd4e1676037355426d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 21:07:29 -0700 Subject: [PATCH 236/564] test(cache): bound reviewed TTL fallback --- test/cache-ttl.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/cache-ttl.test.ts b/test/cache-ttl.test.ts index 5816feef8..ddf79f1bb 100644 --- a/test/cache-ttl.test.ts +++ b/test/cache-ttl.test.ts @@ -15,4 +15,9 @@ describe("configured cache TTL normalization", () => { ])("$reason", ({ raw, expected }) => { expect(configuredTtlMs(raw, 300, 3600)).toBe(expected); }); + + it("keeps the reviewed fallback inside the configured maximum", () => { + expect(configuredTtlMs(undefined, 7200, 3600)).toBe(3_600_000); + expect(configuredTtlMs("not-a-number", 7200, 3600)).toBe(3_600_000); + }); }); From 54ffb5148b25862fccbeb41772be27d0fa50b69f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 21:07:59 -0700 Subject: [PATCH 237/564] fix(cache): bound invalid TTL fallback --- src/cache-ttl.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cache-ttl.ts b/src/cache-ttl.ts index 1ff24b05e..781ca4e2f 100644 --- a/src/cache-ttl.ts +++ b/src/cache-ttl.ts @@ -8,8 +8,8 @@ const canonicalUnsignedDecimalSecondsPattern = /^(?:0|[1-9]\d*)(?:\.\d+)?$/; * to zero. Operator-provided values must already be canonical unsigned decimal * strings; surrounding whitespace, signs, hexadecimal/exponent aliases, leading-zero * integer spellings, non-finite values, non-positive values, or values smaller than - * one whole second fall back to the reviewed default. Larger values are floored and - * capped before conversion to milliseconds. + * one whole second fall back to the reviewed default without exceeding the configured + * maximum. Larger values are floored and capped before conversion to milliseconds. * * @param raw optional environment value expressed in canonical decimal seconds * @param defaultSeconds safe fallback TTL in seconds @@ -21,12 +21,13 @@ export function configuredTtlMs( defaultSeconds: number, maxSeconds: number, ): number { + const fallbackMilliseconds = Math.min(defaultSeconds, maxSeconds) * 1000; if (raw !== undefined && !canonicalUnsignedDecimalSecondsPattern.test(raw)) { - return defaultSeconds * 1000; + return fallbackMilliseconds; } const seconds = Number(raw ?? String(defaultSeconds)); - if (!Number.isFinite(seconds) || seconds <= 0) return defaultSeconds * 1000; + if (!Number.isFinite(seconds) || seconds <= 0) return fallbackMilliseconds; const normalizedSeconds = Math.floor(seconds); - if (normalizedSeconds <= 0) return defaultSeconds * 1000; + if (normalizedSeconds <= 0) return fallbackMilliseconds; return Math.min(normalizedSeconds, maxSeconds) * 1000; } From 5f7dc9e050fad538c88b5586a0e8ba282d53ace7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 22:03:07 -0700 Subject: [PATCH 238/564] test(rate-limit): reject spoofable local client identity --- test/local-rate-limit-config-boundary.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/local-rate-limit-config-boundary.test.ts b/test/local-rate-limit-config-boundary.test.ts index 059804330..846802d56 100644 --- a/test/local-rate-limit-config-boundary.test.ts +++ b/test/local-rate-limit-config-boundary.test.ts @@ -50,4 +50,34 @@ describe("local rate-limit configuration boundary", () => { expect((await worker.fetch(request(), nonCanonicalEnv)).status).toBe(401); expect((await worker.fetch(request(), nonCanonicalEnv)).status).toBe(401); }); + + it("does not let caller-controlled forwarding headers select a local throttle bucket", async () => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + const onePerMinute: Env = { + ...env, + NOEMA_RATE_LIMIT_PER_MINUTE: "1", + }; + + const first = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { "x-forwarded-for": "198.51.100.10" }, + }), + onePerMinute, + ); + expect(first.status).toBe(401); + + const second = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { "x-real-ip": "198.51.100.11" }, + }), + onePerMinute, + ); + expect(second.status).toBe(429); + await expect(second.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_RATE_LIMIT", + }); + }); }); From f6847a8941308ff145bad71e3c716a0fe9b61708 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 22:05:34 -0700 Subject: [PATCH 239/564] test(rate-limit): keep distributed client identity authoritative --- test/local-rate-limit-config-boundary.test.ts | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/test/local-rate-limit-config-boundary.test.ts b/test/local-rate-limit-config-boundary.test.ts index 846802d56..059804330 100644 --- a/test/local-rate-limit-config-boundary.test.ts +++ b/test/local-rate-limit-config-boundary.test.ts @@ -50,34 +50,4 @@ describe("local rate-limit configuration boundary", () => { expect((await worker.fetch(request(), nonCanonicalEnv)).status).toBe(401); expect((await worker.fetch(request(), nonCanonicalEnv)).status).toBe(401); }); - - it("does not let caller-controlled forwarding headers select a local throttle bucket", async () => { - vi.spyOn(console, "log").mockImplementation(() => undefined); - const onePerMinute: Env = { - ...env, - NOEMA_RATE_LIMIT_PER_MINUTE: "1", - }; - - const first = await worker.fetch( - new Request("https://noema.example/exchange", { - method: "POST", - headers: { "x-forwarded-for": "198.51.100.10" }, - }), - onePerMinute, - ); - expect(first.status).toBe(401); - - const second = await worker.fetch( - new Request("https://noema.example/exchange", { - method: "POST", - headers: { "x-real-ip": "198.51.100.11" }, - }), - onePerMinute, - ); - expect(second.status).toBe(429); - await expect(second.json()).resolves.toMatchObject({ - ok: false, - error_code: "ERR_RATE_LIMIT", - }); - }); }); From 87385c12b395b33dc1910353172ea6508cd5d504 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 22:05:55 -0700 Subject: [PATCH 240/564] test(oidc): reject invalid UTF-8 at bearer boundary --- test/bearer-authorization-canonical-segments.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/bearer-authorization-canonical-segments.test.ts b/test/bearer-authorization-canonical-segments.test.ts index 0863c3071..6188da631 100644 --- a/test/bearer-authorization-canonical-segments.test.ts +++ b/test/bearer-authorization-canonical-segments.test.ts @@ -4,6 +4,7 @@ import { parseExactBearerToken } from "../src/bearer-authorization"; const canonicalHeader = Buffer.from(JSON.stringify({ alg: "RS256", kid: "kid" })).toString("base64url"); const canonicalPayload = Buffer.from(JSON.stringify({ sub: "repo:ContextualWisdomLab/noema" })).toString("base64url"); const canonicalSignature = Buffer.from([1, 2, 3, 4]).toString("base64url"); +const invalidUtf8Segment = Buffer.from([0xff]).toString("base64url"); describe("parseExactBearerToken canonical JWT segments", () => { it("preserves an already-canonical three-segment JWT byte-for-byte", () => { @@ -31,6 +32,8 @@ describe("parseExactBearerToken canonical JWT segments", () => { ["padded payload", `${canonicalHeader}.${canonicalPayload}=.${canonicalSignature}`], ["padded signature", `${canonicalHeader}.${canonicalPayload}.${canonicalSignature}=`], ["invalid protected-header alphabet", `%${canonicalHeader}.${canonicalPayload}.${canonicalSignature}`], + ["invalid UTF-8 protected header", `${invalidUtf8Segment}.${canonicalPayload}.${canonicalSignature}`], + ["invalid UTF-8 payload", `${canonicalHeader}.${invalidUtf8Segment}.${canonicalSignature}`], ["empty payload segment", `${canonicalHeader}..${canonicalSignature}`], ["non-JWT bearer credential", "opaque-visible-ascii-token"], ])("rejects %s before any downstream claim reader", (_name, token) => { From 6c41412cc706595da192997f0dc30e7b2173f08f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 22:06:21 -0700 Subject: [PATCH 241/564] fix(oidc): reject invalid UTF-8 at bearer boundary --- src/bearer-authorization.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index 13c1add68..36cda28b7 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -93,9 +93,9 @@ function hasDuplicateTopLevelJsonKeys(bytes: Uint8Array): boolean { * canonical `Bearer ` form and remains within the bounded OIDC * credential envelope. The parser never trims or normalizes attacker-controlled framing. * Each JWT segment must already be non-empty canonical unpadded base64url. Protected - * headers or payloads beginning with a UTF-8 BOM, or containing duplicate top-level JSON - * member names after escape decoding, are rejected before any claim reader can silently - * reinterpret the signed authority bytes. + * headers and payloads must also already be valid UTF-8; BOM-prefixed authority and + * duplicate top-level JSON member names after escape decoding are rejected before any + * claim reader can silently reinterpret the signed authority bytes. * * @param authorization Raw HTTP Authorization field bytes decoded as a JavaScript string. * @returns The exact bearer credential when framing and bounds are canonical; otherwise undefined. @@ -112,7 +112,9 @@ export function parseExactBearerToken(authorization: string): string | undefined const [headerBytes, payloadBytes] = decodedSegments as [Uint8Array, Uint8Array, Uint8Array]; if ( - segments[0].startsWith(utf8BomBase64UrlPrefix) + decodeJwtJsonText(headerBytes) === undefined + || decodeJwtJsonText(payloadBytes) === undefined + || segments[0].startsWith(utf8BomBase64UrlPrefix) || segments[1].startsWith(utf8BomBase64UrlPrefix) || hasDuplicateTopLevelJsonKeys(headerBytes) || hasDuplicateTopLevelJsonKeys(payloadBytes) From 35fc7d9dc7897f7789254e9af6345b0ce5f8987a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 22:09:53 -0700 Subject: [PATCH 242/564] test(oidc): align invalid UTF-8 with bearer rejection --- test/oidc-invalid-utf8.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/oidc-invalid-utf8.test.ts b/test/oidc-invalid-utf8.test.ts index 019bffa21..842dc5f6e 100644 --- a/test/oidc-invalid-utf8.test.ts +++ b/test/oidc-invalid-utf8.test.ts @@ -125,16 +125,17 @@ describe("OIDC UTF-8 canonicality", () => { vi.restoreAllMocks(); }); - it("rejects a correctly signed JWT whose payload is not valid UTF-8", async () => { + it("rejects a correctly signed JWT whose payload is not valid UTF-8 at the bearer boundary", async () => { const response = await exchangeSignedPayload( payloadWithInvalidUtf8(oidcPayload(Math.floor(Date.now() / 1000))), ); - expect(response.status).toBe(400); + expect(response.status).toBe(401); await expect(response.json()).resolves.toMatchObject({ ok: false, - error_code: "ERR_TOKEN_MALFORMED", + error_code: "ERR_AUTH_MISSING", }); + expect(globalThis.fetch).not.toHaveBeenCalled(); }); it("rejects a correctly signed JWT whose payload starts with a UTF-8 BOM before verification", async () => { From 352a7918cadedd819c57b1b85305f81a6da9f7d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 22:10:31 -0700 Subject: [PATCH 243/564] test(oidc): align runtime UTF-8 rejection boundary --- test/runtime-workflow-prefilter-coverage.test.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/test/runtime-workflow-prefilter-coverage.test.ts b/test/runtime-workflow-prefilter-coverage.test.ts index fb45f243d..89d620c51 100644 --- a/test/runtime-workflow-prefilter-coverage.test.ts +++ b/test/runtime-workflow-prefilter-coverage.test.ts @@ -198,17 +198,11 @@ describe("runtime workflow-source prefilter coverage", () => { }); }); - it("does not derive workflow-source policy from replacement-decoded invalid UTF-8 payload bytes", async () => { - const response = await exchangeWithPayloadSegment( - invalidUtf8WorkflowPayloadSegment(), - "invalid-utf8-prefilter", - ); + it("rejects replacement-decoded invalid UTF-8 payload bytes at the canonical bearer boundary", async () => { + const payload = invalidUtf8WorkflowPayloadSegment(); + const token = `${encodeJsonSegment({ alg: "RS256", kid: "invalid-utf8-prefilter" })}.${payload}.AA`; - expect(response.status).toBe(400); - await expect(response.json()).resolves.toMatchObject({ - ok: false, - error_code: "ERR_TOKEN_MALFORMED", - }); + await expectMissingAuth({ authorization: `Bearer ${token}` }); }); it("rejects padded base64url payload authority at the canonical bearer boundary", async () => { From d6347d9f3dc5186fe1f9cbd69672646d0f3adc60 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 22:12:31 -0700 Subject: [PATCH 244/564] test(oidc): remove superseded runtime helper --- test/runtime-workflow-prefilter-coverage.test.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/runtime-workflow-prefilter-coverage.test.ts b/test/runtime-workflow-prefilter-coverage.test.ts index 89d620c51..7e9301097 100644 --- a/test/runtime-workflow-prefilter-coverage.test.ts +++ b/test/runtime-workflow-prefilter-coverage.test.ts @@ -102,20 +102,6 @@ function invalidUtf8WorkflowPayloadSegment(): string { ]).toString("base64url"); } -async function exchangeWithPayloadSegment(payloadSegment: string, kid: string): Promise { - const token = `${encodeJsonSegment({ alg: "RS256", kid })}.${payloadSegment}.AA`; - return worker.fetch( - new Request("https://noema.example/exchange", { - method: "POST", - headers: { - "cf-connecting-ip": "203.0.113.126", - authorization: `Bearer ${token}`, - }, - }), - env, - ); -} - async function expectMissingAuth(headers: Record = {}): Promise { const response = await worker.fetch( new Request("https://noema.example/exchange", { From 07343e1cb667eb766e2a5335d9e82a13768f4cf5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 22:15:38 -0700 Subject: [PATCH 245/564] test(oidc): preserve downstream invalid UTF-8 defense --- test/oidc-downstream-parser-defense.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/oidc-downstream-parser-defense.test.ts b/test/oidc-downstream-parser-defense.test.ts index 053c425d4..96514414c 100644 --- a/test/oidc-downstream-parser-defense.test.ts +++ b/test/oidc-downstream-parser-defense.test.ts @@ -77,6 +77,17 @@ describe("authoritative OIDC verifier defense when the shared bearer parser regr }); }); + it("still rejects invalid UTF-8 payload bytes before OIDC egress", async () => { + const invalidUtf8Payload = Buffer.from([0xff]).toString("base64url"); + const response = await exchangeWithParserRegression(`${canonicalHeader}.${invalidUtf8Payload}.AA`); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + }); + }); + it("still rejects non-canonical base64url pad bits before OIDC egress", async () => { const canonicalPayload = Buffer.from("{}", "utf8").toString("base64url"); const nonCanonicalPayload = sameBytesNonCanonicalBase64Url(canonicalPayload); From 7360fa88981b3551b70a4bffc478cafb7121c4ce Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 22:16:31 -0700 Subject: [PATCH 246/564] refactor(oidc): remove redundant UTF-8 decode branch --- src/bearer-authorization.ts | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index 36cda28b7..987bb7cd1 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -13,10 +13,7 @@ function decodeCanonicalBase64Url(segment: string): Uint8Array | undefined { bytes[index] = binary.charCodeAt(index); roundTripBinary += String.fromCharCode(bytes[index]); } - const canonical = btoa(roundTripBinary) - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/g, ""); + const canonical = btoa(roundTripBinary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); return canonical === segment ? bytes : undefined; } catch { return undefined; @@ -31,10 +28,7 @@ function decodeJwtJsonText(bytes: Uint8Array): string | undefined { } } -function hasDuplicateTopLevelJsonKeys(bytes: Uint8Array): boolean { - const text = decodeJwtJsonText(bytes); - if (text === undefined) return false; - +function hasDuplicateTopLevelJsonKeys(text: string): boolean { const seenKeys = new Set(); let structureDepth = 0; let stringStart = -1; @@ -53,14 +47,11 @@ function hasDuplicateTopLevelJsonKeys(bytes: Uint8Array): boolean { continue; } if (character !== '"') continue; - inString = false; if (structureDepth !== 1) continue; - let lookahead = index + 1; while (lookahead < text.length && /\s/.test(text[lookahead])) lookahead += 1; if (text[lookahead] !== ":") continue; - const encodedKey = text.slice(stringStart + 1, index); let decodedKey: string; try { @@ -72,7 +63,6 @@ function hasDuplicateTopLevelJsonKeys(bytes: Uint8Array): boolean { seenKeys.add(decodedKey); continue; } - if (character === '"') { inString = true; stringStart = index; @@ -84,7 +74,6 @@ function hasDuplicateTopLevelJsonKeys(bytes: Uint8Array): boolean { } if (character === "}" || character === "]") structureDepth -= 1; } - return false; } @@ -106,18 +95,18 @@ export function parseExactBearerToken(authorization: string): string | undefined if (!token) return undefined; const segments = token.split("."); if (segments.length !== 3) return undefined; - const decodedSegments = segments.map(decodeCanonicalBase64Url); if (decodedSegments.some((bytes) => bytes === undefined)) return undefined; const [headerBytes, payloadBytes] = decodedSegments as [Uint8Array, Uint8Array, Uint8Array]; - + const headerText = decodeJwtJsonText(headerBytes); + const payloadText = decodeJwtJsonText(payloadBytes); if ( - decodeJwtJsonText(headerBytes) === undefined - || decodeJwtJsonText(payloadBytes) === undefined + headerText === undefined + || payloadText === undefined || segments[0].startsWith(utf8BomBase64UrlPrefix) || segments[1].startsWith(utf8BomBase64UrlPrefix) - || hasDuplicateTopLevelJsonKeys(headerBytes) - || hasDuplicateTopLevelJsonKeys(payloadBytes) + || hasDuplicateTopLevelJsonKeys(headerText) + || hasDuplicateTopLevelJsonKeys(payloadText) ) return undefined; return token; } From 0ce860d48f079a083eb997ea89152c4b80920699 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 22:30:43 -0700 Subject: [PATCH 247/564] test(trace): reject normalized trace identity --- test/trace-header-coverage.test.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/trace-header-coverage.test.ts b/test/trace-header-coverage.test.ts index ec482aeec..cd177e1be 100644 --- a/test/trace-header-coverage.test.ts +++ b/test/trace-header-coverage.test.ts @@ -28,4 +28,18 @@ describe("trace header selection", () => { expect(payload.trace_id).toBe("correlation:trace_456"); expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); }); -}); \ No newline at end of file + + it("does not normalize surrounding whitespace into trusted trace identity", async () => { + const response = await worker.fetch(new Request("https://noema.example/health", { + headers: { + "x-request-id": " request.trace-123 ", + "x-correlation-id": "correlation:trace_456", + }, + }), env); + + expect(response.status).toBe(200); + const payload = await response.json() as { trace_id: string }; + expect(payload.trace_id).toBe("correlation:trace_456"); + expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); + }); +}); From b4a221cae2fee34a87c85d43e70943019a5179c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 22:34:58 -0700 Subject: [PATCH 248/564] test(trace): drop impossible raw OWS assertion --- test/trace-header-coverage.test.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/trace-header-coverage.test.ts b/test/trace-header-coverage.test.ts index cd177e1be..e3ebd6fa3 100644 --- a/test/trace-header-coverage.test.ts +++ b/test/trace-header-coverage.test.ts @@ -28,18 +28,4 @@ describe("trace header selection", () => { expect(payload.trace_id).toBe("correlation:trace_456"); expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); }); - - it("does not normalize surrounding whitespace into trusted trace identity", async () => { - const response = await worker.fetch(new Request("https://noema.example/health", { - headers: { - "x-request-id": " request.trace-123 ", - "x-correlation-id": "correlation:trace_456", - }, - }), env); - - expect(response.status).toBe(200); - const payload = await response.json() as { trace_id: string }; - expect(payload.trace_id).toBe("correlation:trace_456"); - expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); - }); }); From 1853fcc666535275edeb18f0c91d9dcef9cf4bf8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 22:36:44 -0700 Subject: [PATCH 249/564] test(runtime): reject Unicode trace normalization --- test/health-method-contract.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/health-method-contract.test.ts b/test/health-method-contract.test.ts index 34b4c9f7d..07b249102 100644 --- a/test/health-method-contract.test.ts +++ b/test/health-method-contract.test.ts @@ -23,6 +23,20 @@ describe("health method contract", () => { }); }); + it("does not normalize non-ASCII whitespace into a trusted trace identity", async () => { + const response = await worker.fetch(new Request("https://noema.example/health", { + headers: { + "x-request-id": "\u00a0request.trace-123\u00a0", + "x-correlation-id": "correlation:trace_456", + }, + }), env); + + expect(response.status).toBe(200); + const payload = await response.json() as { trace_id: string }; + expect(payload.trace_id).toBe("correlation:trace_456"); + expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); + }); + it.each(["HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])( "rejects %s /health instead of reporting false liveness", async (method) => { From 2dbed41f0c266ebfbe549605949475ccd18d5658 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 22:37:30 -0700 Subject: [PATCH 250/564] fix(runtime): preserve canonical trace authority --- src/runtime-entrypoint.ts | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index 37a085813..fa5d5aada 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -17,6 +17,26 @@ export interface Env extends BaseEnv { ALLOWED_WORKFLOW_SHA?: string; } +const canonicalTraceHeaderPattern = /^[A-Za-z0-9._:-]+$/; +const maxTraceHeaderLength = 128; +const traceHeaderNames = ["x-request-id", "x-correlation-id"] as const; + +function canonicalTraceRequest(request: Request): Request { + let headers: Headers | undefined; + for (const name of traceHeaderNames) { + const value = request.headers.get(name); + if ( + value === null + || (value.length <= maxTraceHeaderLength && canonicalTraceHeaderPattern.test(value)) + ) { + continue; + } + headers ??= new Headers(request.headers); + headers.delete(name); + } + return headers === undefined ? request : new Request(request, { headers }); +} + function readinessHeaders( traceId: string, latencyMs: number, @@ -96,16 +116,19 @@ async function runtimeReadinessResponse(request: Request, env: Env): Promise { - const url = new URL(request.url); + const boundedRequest = canonicalTraceRequest(request); + const url = new URL(boundedRequest.url); if (url.pathname === "/ready") { - return runtimeReadinessResponse(request, env); + return runtimeReadinessResponse(boundedRequest, env); } - return entrypoint.fetch(request, env); + return entrypoint.fetch(boundedRequest, env); }, }; From cd113802a6c14f373fc64156c01f57b0e85bcab1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 22:38:00 -0700 Subject: [PATCH 251/564] test(runtime): cover canonical trace ingress --- test/health-method-contract.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/health-method-contract.test.ts b/test/health-method-contract.test.ts index 07b249102..7e97709b1 100644 --- a/test/health-method-contract.test.ts +++ b/test/health-method-contract.test.ts @@ -23,6 +23,17 @@ describe("health method contract", () => { }); }); + it("preserves a canonical trace identity at the public runtime boundary", async () => { + const response = await worker.fetch(new Request("https://noema.example/health", { + headers: { "x-request-id": "request.trace-123" }, + }), env); + + expect(response.status).toBe(200); + const payload = await response.json() as { trace_id: string }; + expect(payload.trace_id).toBe("request.trace-123"); + expect(response.headers.get("x-trace-id")).toBe("request.trace-123"); + }); + it("does not normalize non-ASCII whitespace into a trusted trace identity", async () => { const response = await worker.fetch(new Request("https://noema.example/health", { headers: { @@ -37,6 +48,20 @@ describe("health method contract", () => { expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); }); + it("drops every non-canonical external trace header instead of normalizing it", async () => { + const response = await worker.fetch(new Request("https://noema.example/health", { + headers: { + "x-request-id": "\u00a0request.trace-123\u00a0", + "x-correlation-id": "\u00a0correlation:trace_456\u00a0", + }, + }), env); + + expect(response.status).toBe(200); + const payload = await response.json() as { trace_id: string }; + expect(payload.trace_id).toMatch(/^[0-9a-f-]{36}$/i); + expect(response.headers.get("x-trace-id")).toBe(payload.trace_id); + }); + it.each(["HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])( "rejects %s /health instead of reporting false liveness", async (method) => { From ddc3b96120816f539a72f697857eb4c7b04b09bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 23:04:55 -0700 Subject: [PATCH 252/564] test(github-app): reject mislabeled private-key PEM authority --- test/github-app-private-key-envelope.test.ts | 142 +++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 test/github-app-private-key-envelope.test.ts diff --git a/test/github-app-private-key-envelope.test.ts b/test/github-app-private-key-envelope.test.ts new file mode 100644 index 000000000..355937bd4 --- /dev/null +++ b/test/github-app-private-key-envelope.test.ts @@ -0,0 +1,142 @@ +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"); + 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 = `private-key-envelope-${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: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + 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" }, + }; +} + +describe("GitHub App private-key authority", () => { + it("rejects a valid PKCS#8 key under a non-canonical PEM label before GitHub credential egress", async () => { + const { token, jwk } = await signedOidcToken(); + const mislabeledPrivateKey = appPrivateKeyPem + .replace("-----BEGIN PRIVATE KEY-----", "-----BEGIN CERTIFICATE-----") + .replace("-----END PRIVATE KEY-----", "-----END CERTIFICATE-----"); + 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: configuredRef, + ALLOWED_WORKFLOW_SHA: configuredWorkflowSha, + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: mislabeledPrivateKey, + GITHUB_APP_INSTALLATION_ID: "92345", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", + }; + let githubCredentialCalls = 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] }); + } + githubCredentialCalls += 1; + if (url === "https://api.github.com/app/installations/92345/access_tokens") { + return Response.json({ + token: "ghs_should_never_be_minted", + expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), + }); + } + 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.254", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + env, + ); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_API", + message: "GitHub App private key configuration unavailable", + }); + expect(githubCredentialCalls).toBe(0); + }); +}); From fedf238feb83243dfe6931e8ca4b1de631322639 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 23:07:43 -0700 Subject: [PATCH 253/564] fix(github-app): require canonical PKCS#8 PEM envelope --- src/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index f6ca5c327..bb425d10d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -146,6 +146,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 githubInstallationTokenPattern = /^[\x21-\x7e]+$/; +const githubAppPrivateKeyPattern = /^-----BEGIN PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END PRIVATE KEY-----$/; const expectedRepositoryOwnerId = "295022177"; const expectedRepositoryIds = new Map([ ["ContextualWisdomLab/noema", "1285107801"], @@ -576,7 +577,11 @@ function validateRepositoryName(repository: string, env: Env): string { } async function importGithubAppPrivateKey(pem: string): Promise { - const body = pem.replace(/-----BEGIN [^-]+-----/g, "").replace(/-----END [^-]+-----/g, "").replace(/\s+/g, ""); + const match = githubAppPrivateKeyPattern.exec(pem); + if (!match) { + throw new ApiError("ERR_GITHUB_API", 503, "GitHub App private key configuration unavailable"); + } + const body = match[1].replace(/\r?\n/g, ""); const der = base64UrlDecode(body.replace(/\+/g, "-").replace(/\//g, "_")); return crypto.subtle.importKey("pkcs8", der, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"]); } From 1add8a9c1f1e8e221ee6b36f2dc7733ae9be94e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 23:11:25 -0700 Subject: [PATCH 254/564] test(github-app): keep private-key import coverage canonical --- test/credential-request-helper-coverage.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/credential-request-helper-coverage.test.ts b/test/credential-request-helper-coverage.test.ts index f9f1bfda2..b6cc9a602 100644 --- a/test/credential-request-helper-coverage.test.ts +++ b/test/credential-request-helper-coverage.test.ts @@ -17,7 +17,7 @@ const env: Env = { ALLOWED_WORKFLOW_SHA: configuredWorkflowSha, GITHUB_API_BASE: "https://api.github.com", GITHUB_APP_ID: "1", - GITHUB_APP_PRIVATE_KEY_PEM: "unused-before-request-validation", + GITHUB_APP_PRIVATE_KEY_PEM: "-----BEGIN PRIVATE KEY-----\nAA==\n-----END PRIVATE KEY-----", NOEMA_RATE_LIMIT_PER_MINUTE: "1000", }; From f23063aca177631d5e9e150bf48245031dbd8c46 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 23:32:25 -0700 Subject: [PATCH 255/564] test(rate-limit): reject non-canonical client identity bytes --- ...limit-client-identity-canonicality.test.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/distributed-rate-limit-client-identity-canonicality.test.ts diff --git a/test/distributed-rate-limit-client-identity-canonicality.test.ts b/test/distributed-rate-limit-client-identity-canonicality.test.ts new file mode 100644 index 000000000..b694ad419 --- /dev/null +++ b/test/distributed-rate-limit-client-identity-canonicality.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { + DistributedRateLimitUnavailable, + distributedRateLimitObjectName, + trustedClientIdentifier, +} from "../src/rate-limit"; + +describe("distributed rate-limit client identity canonicality", () => { + it("rejects non-ASCII surrounding whitespace instead of normalizing it into trusted client authority", async () => { + const request = new Request("https://noema.example/exchange", { + headers: { + "cf-connecting-ip": "\u00a0203.0.113.7\u00a0", + }, + }); + + expect(trustedClientIdentifier(request)).toBeUndefined(); + await expect(distributedRateLimitObjectName(request)).rejects.toBeInstanceOf( + DistributedRateLimitUnavailable, + ); + }); + + it("preserves an already-canonical Cloudflare client IPv4 identity", () => { + const request = new Request("https://noema.example/exchange", { + headers: { + "cf-connecting-ip": "203.0.113.7", + }, + }); + + expect(trustedClientIdentifier(request)).toBe("203.0.113.7"); + }); +}); From 69ab1c5c33d53e6c4254f82b24fd28e65e7d64e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 23:33:53 -0700 Subject: [PATCH 256/564] fix(rate-limit): preserve canonical Cloudflare client identity --- src/rate-limit.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/rate-limit.ts b/src/rate-limit.ts index 16ad11193..0871a6a03 100644 --- a/src/rate-limit.ts +++ b/src/rate-limit.ts @@ -135,12 +135,17 @@ function canonicalIpv6(candidate: string): string | undefined { /** * Extracts a canonical trusted client identifier only from Cloudflare's `CF-Connecting-IP` request header. + * The application never trims or otherwise normalizes non-canonical header bytes into trusted bucket authority. * @param request Edge request whose Cloudflare-supplied client address is used for distributed bucketing. - * @returns A canonical IPv4 or IPv6 address, or `undefined` when the trusted header is missing or malformed. + * @returns A canonical IPv4 or IPv6 address, or `undefined` when the trusted header is missing, non-canonical, or malformed. */ export function trustedClientIdentifier(request: Request): string | undefined { - const candidate = request.headers.get("cf-connecting-ip")?.trim() ?? ""; - if (!candidate || candidate.length > MAX_CLIENT_IDENTIFIER_LENGTH) { + const candidate = request.headers.get("cf-connecting-ip") ?? ""; + if ( + !candidate + || candidate.length > MAX_CLIENT_IDENTIFIER_LENGTH + || candidate !== candidate.trim() + ) { return undefined; } return canonicalIpv4(candidate) ?? canonicalIpv6(candidate); From 02ef12e6ec41a2441623ef661e5f8bfb313da0f2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 00:02:07 -0700 Subject: [PATCH 257/564] test(egress): reject non-canonical raw GitHub URL aliases --- test/outbound-fetch-url-canonicality.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 test/outbound-fetch-url-canonicality.test.ts diff --git a/test/outbound-fetch-url-canonicality.test.ts b/test/outbound-fetch-url-canonicality.test.ts new file mode 100644 index 000000000..1dd7fbc68 --- /dev/null +++ b/test/outbound-fetch-url-canonicality.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { isTrustedCredentialEgress } from "../src/outbound-fetch-policy"; + +describe("credential-egress URL authority", () => { + it("does not normalize raw string aliases into trusted GitHub destinations", () => { + expect(isTrustedCredentialEgress(" https://api.github.com/app/installations")).toBe(false); + expect(isTrustedCredentialEgress("https://api.github.com/app/installations ")).toBe(false); + expect(isTrustedCredentialEgress("https://api.github.com:443/app/installations")).toBe(false); + expect(isTrustedCredentialEgress("https://API.GITHUB.COM/app/installations")).toBe(false); + }); + + it("preserves canonical string and already-parsed URL authority", () => { + expect(isTrustedCredentialEgress("https://api.github.com/app/installations")).toBe(true); + expect(isTrustedCredentialEgress(new URL("https://api.github.com:443/app/installations"))).toBe(true); + }); +}); From 09910384583f9b6c526c5a4ad61da4ceed64d648 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 00:03:16 -0700 Subject: [PATCH 258/564] fix(egress): preserve raw GitHub URL authority --- src/outbound-fetch-policy.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 64a40d439..eb9effa07 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -62,7 +62,10 @@ function blockedResponse(reason: BlockReason): Response { function outboundUrl(input: RequestInfo | URL): URL | undefined { try { - return new URL(input instanceof Request ? input.url : String(input)); + const raw = input instanceof Request ? input.url : String(input); + const parsed = new URL(raw); + if (typeof input === "string" && parsed.href !== input) return undefined; + return parsed; } catch { return undefined; } @@ -253,6 +256,8 @@ function githubApiOperation(url: URL): GitHubApiOperation | undefined { /** * Checks whether an outbound destination is on the exact HTTPS credential-egress allowlist used by Noema. + * Raw string destinations must already equal their parsed URL serialization; the policy never trims, + * case-folds, removes a default port, or otherwise normalizes caller-controlled destination bytes into authority. * @param input Candidate request target supplied to the protected fetch path. * @returns `true` only for reviewed GitHub API or GitHub OIDC discovery/JWKS allowlist destinations. */ From ad4c16f23aa396818f42ec7506a2bd0e23c87eef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 01:03:39 -0700 Subject: [PATCH 259/564] test(rate-limit): reject client identity normalization --- test/local-rate-limit-config-boundary.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/local-rate-limit-config-boundary.test.ts b/test/local-rate-limit-config-boundary.test.ts index 059804330..95e7cc65a 100644 --- a/test/local-rate-limit-config-boundary.test.ts +++ b/test/local-rate-limit-config-boundary.test.ts @@ -50,4 +50,24 @@ describe("local rate-limit configuration boundary", () => { expect((await worker.fetch(request(), nonCanonicalEnv)).status).toBe(401); expect((await worker.fetch(request(), nonCanonicalEnv)).status).toBe(401); }); + + it("does not collapse non-ASCII-whitespace client identity into the canonical local bucket", async () => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + const strictEnv: Env = { + ...env, + NOEMA_RATE_LIMIT_PER_MINUTE: "1", + }; + const nonCanonicalRequest = () => new Request("https://noema.example/exchange", { + method: "POST", + headers: { "cf-connecting-ip": "\u00a0203.0.113.233\u00a0" }, + }); + const canonicalRequest = () => new Request("https://noema.example/exchange", { + method: "POST", + headers: { "cf-connecting-ip": "203.0.113.233" }, + }); + + expect((await worker.fetch(nonCanonicalRequest(), strictEnv)).status).toBe(401); + expect((await worker.fetch(canonicalRequest(), strictEnv)).status).toBe(401); + expect((await worker.fetch(canonicalRequest(), strictEnv)).status).toBe(429); + }); }); From 36c5fade791400e53f15c7f627e22f673e44439d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 01:15:39 -0700 Subject: [PATCH 260/564] fix(rate-limit): preserve raw local client identity --- src/index.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index bb425d10d..98963d774 100644 --- a/src/index.ts +++ b/src/index.ts @@ -220,14 +220,13 @@ function canonicalGithubInstallationTokenExpiry(value: string, parsedMs: number) function requestClientKey(request: Request, route: string): string { const client = request.headers.get("cf-connecting-ip") || request.headers.get("x-real-ip") - || request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() + || request.headers.get("x-forwarded-for")?.split(",")[0] || "unknown"; - const candidate = client.trim(); - if (!candidate) return `${route}:unknown`; - if (candidate.length <= maxTrustedHeaderLength && clientIdentifierPattern.test(candidate)) { - return `${route}:${candidate}`; + if (!client) return `${route}:unknown`; + if (client.length <= maxTrustedHeaderLength && clientIdentifierPattern.test(client)) { + return `${route}:${client}`; } - return `${route}:hash:${safeHash(candidate)}`; + return `${route}:hash:${safeHash(client)}`; } function enforceRateLimit(request: Request, env: Env, route: string) { @@ -924,4 +923,4 @@ export default { return withOperationalHeaders(response, traceId, latency_ms); } }, -}; +}; \ No newline at end of file From 241b70fa8ef499d1e5d76d5eda2dce153771ca04 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 01:20:57 -0700 Subject: [PATCH 261/564] fix(rate-limit): 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 98963d774..bc90ec886 100644 --- a/src/index.ts +++ b/src/index.ts @@ -923,4 +923,4 @@ export default { return withOperationalHeaders(response, traceId, latency_ms); } }, -}; \ No newline at end of file +}; From 35491181e9869bbbb23ff4867ab498b93e975080 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 01:37:59 -0700 Subject: [PATCH 262/564] fix(coverage): remove unreachable client fallback branch --- src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index bc90ec886..1d89fbf81 100644 --- a/src/index.ts +++ b/src/index.ts @@ -222,7 +222,6 @@ function requestClientKey(request: Request, route: string): string { || request.headers.get("x-real-ip") || request.headers.get("x-forwarded-for")?.split(",")[0] || "unknown"; - if (!client) return `${route}:unknown`; if (client.length <= maxTrustedHeaderLength && clientIdentifierPattern.test(client)) { return `${route}:${client}`; } From bbde0e769d21358dc6f17e5b6e72efe4c98f87ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 03:11:46 -0700 Subject: [PATCH 263/564] test(oidc): require subject authority --- test/oidc-required-subject.test.ts | 115 +++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 test/oidc-required-subject.test.ts diff --git a/test/oidc-required-subject.test.ts b/test/oidc-required-subject.test.ts new file mode 100644 index 000000000..f77be1881 --- /dev/null +++ b/test/oidc-required-subject.test.ts @@ -0,0 +1,115 @@ +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-required-subject"; + +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-after-subject-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"); +} + +async function signedJwtWithSubject(subject: string | undefined): Promise { + const now = Math.floor(Date.now() / 1000); + const header = encodeJson({ alg: "RS256", kid: signingKid }); + const payload = encodeJson({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + job_workflow_ref: configuredWorkflowRef, + job_workflow_sha: configuredWorkflowSha, + ...(subject === undefined ? {} : { sub: subject }), + nbf: now - 30, + exp: now + 300, + iat: now - 30, + }); + const signature = new Uint8Array( + await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + signingPrivateKey, + new TextEncoder().encode(`${header}.${payload}`), + ), + ); + return `${header}.${payload}.${Buffer.from(signature).toString("base64url")}`; +} + +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("GitHub OIDC subject authority", () => { + it.each([ + ["missing", undefined], + ["empty", ""], + ])("rejects a signed GitHub OIDC token with a %s sub claim", async (_label, subject) => { + 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 }); + }); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${await signedJwtWithSubject(subject)}`, + "content-type": "application/json", + "cf-connecting-ip": "203.0.113.126", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + env, + ); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_INVALID", + message: "OIDC subject claim is invalid", + }); + }); +}); From 58d09197e357beed527bf71c558b7e263e468cd2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 03:15:30 -0700 Subject: [PATCH 264/564] fix(oidc): require subject authority --- src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.ts b/src/index.ts index 1d89fbf81..364c53fc5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -468,6 +468,9 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise if (payload.iss !== env.ALLOWED_ISSUER) throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC issuer is not allowed"); 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 (typeof payload.sub !== "string" || payload.sub.length === 0) { + throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC subject claim is invalid"); + } 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 !== expectedRepositoryOwnerId) { throw new ApiError("ERR_REPO_NOT_ALLOWED", 403, "OIDC repository owner identity is not allowed"); From 6461a40d98085fa22f9d375843214834b5027e3d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 03:25:26 -0700 Subject: [PATCH 265/564] test(oidc): carry required subject in repository identity fixtures --- test/oidc-repository-owner-id-binding.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/oidc-repository-owner-id-binding.test.ts b/test/oidc-repository-owner-id-binding.test.ts index cc15ef549..d0d168e30 100644 --- a/test/oidc-repository-owner-id-binding.test.ts +++ b/test/oidc-repository-owner-id-binding.test.ts @@ -7,6 +7,7 @@ const configuredWorkflowSha = "a".repeat(40); const expectedRepositoryOwnerId = "295022177"; const expectedNoemaRepositoryId = "1285107801"; const expectedWorkflowRepositoryId = "1274066402"; +const canonicalSubject = "repo:ContextualWisdomLab/.github:ref:refs/heads/main"; let oidcKeyPair: CryptoKeyPair; let oidcPublicJwk: JsonWebKey; @@ -63,6 +64,7 @@ async function signedOidcToken( repository_id: repositoryId, job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, + sub: canonicalSubject, exp: now + 300, nbf: now - 30, iat: now - 30, From 626c153772a1db9bbb2cbbce1ae89c0dc3feab0e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 03:25:56 -0700 Subject: [PATCH 266/564] test(oidc): carry required subject in target authorization fixtures --- test/replay-target-authorization-coverage.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/replay-target-authorization-coverage.test.ts b/test/replay-target-authorization-coverage.test.ts index 7e209ca52..49ecb87ff 100644 --- a/test/replay-target-authorization-coverage.test.ts +++ b/test/replay-target-authorization-coverage.test.ts @@ -7,6 +7,7 @@ const configuredWorkflowSha = "a".repeat(40); const expectedRepositoryOwnerId = "295022177"; const expectedNoemaRepositoryId = "1285107801"; const expectedWorkflowRepositoryId = "1274066402"; +const canonicalSubject = "repo:ContextualWisdomLab/.github:ref:refs/heads/main"; const env: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -54,6 +55,7 @@ async function createToken(repository: string | undefined) { ...(repositoryId ? { repository_id: repositoryId } : {}), job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, + sub: canonicalSubject, exp: now + 300, nbf: now - 30, iat: now - 30, From ef92cf90b38e6f6f9bd475c12077d695c1131fe8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 03:26:28 -0700 Subject: [PATCH 267/564] test(oidc): carry required subject in expiry fixtures --- test/github-installation-expiry-defensive-coverage.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/github-installation-expiry-defensive-coverage.test.ts b/test/github-installation-expiry-defensive-coverage.test.ts index 3e008fddd..d64534b14 100644 --- a/test/github-installation-expiry-defensive-coverage.test.ts +++ b/test/github-installation-expiry-defensive-coverage.test.ts @@ -5,6 +5,7 @@ const configuredRef = "ContextualWisdomLab/.github/.github/workflows/noema-revie const configuredWorkflowSha = "a".repeat(40); const expectedRepositoryOwnerId = "295022177"; const expectedWorkflowRepositoryId = "1274066402"; +const canonicalSubject = "repo:ContextualWisdomLab/.github:ref:refs/heads/main"; let oidcKeyPair: CryptoKeyPair; let oidcPublicJwk: JsonWebKey; let appPrivateKeyPem: string; @@ -49,7 +50,7 @@ async function exchangeWithTokenResponse(tokenBody: unknown, clientIp: string): 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_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", repository_id: expectedWorkflowRepositoryId, job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, exp: now + 300, nbf: now - 30, iat: now - 30 }); + const payload = encodeSegment({ iss: baseEnv.ALLOWED_ISSUER, aud: baseEnv.ALLOWED_AUDIENCE, repository_owner: baseEnv.ALLOWED_REPOSITORY_OWNER, repository_owner_id: expectedRepositoryOwnerId, repository: "ContextualWisdomLab/.github", repository_id: expectedWorkflowRepositoryId, job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, sub: canonicalSubject, 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); From c0a87b1c6f4e82061c7c0832b865765567807ee2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 03:26:54 -0700 Subject: [PATCH 268/564] test(oidc): carry required subject in expiry calendar fixture --- .../github-installation-token-expiry-calendar-integrity.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/github-installation-token-expiry-calendar-integrity.test.ts b/test/github-installation-token-expiry-calendar-integrity.test.ts index 1354249a0..7f1a0a08c 100644 --- a/test/github-installation-token-expiry-calendar-integrity.test.ts +++ b/test/github-installation-token-expiry-calendar-integrity.test.ts @@ -6,6 +6,7 @@ const configuredRef = const configuredWorkflowSha = "a".repeat(40); const expectedRepositoryOwnerId = "295022177"; const expectedWorkflowRepositoryId = "1274066402"; +const canonicalSubject = "repo:ContextualWisdomLab/.github:ref:refs/heads/main"; const baseEnv: Env = { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -78,6 +79,7 @@ async function signedOidcToken(): Promise<{ token: string; jwk: JsonWebKey }> { repository_id: expectedWorkflowRepositoryId, job_workflow_ref: configuredRef, job_workflow_sha: configuredWorkflowSha, + sub: canonicalSubject, exp: now + 300, nbf: now - 30, iat: now - 30, From 29d97fdc680a7f36cfa435aeda07d781590f10ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 03:34:42 -0700 Subject: [PATCH 269/564] test(oidc): carry required subject in GitHub API fixtures --- test/github-api-malformed-json.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/github-api-malformed-json.test.ts b/test/github-api-malformed-json.test.ts index c200e2dd0..73d685479 100644 --- a/test/github-api-malformed-json.test.ts +++ b/test/github-api-malformed-json.test.ts @@ -77,6 +77,7 @@ async function signedOidcToken() { repository_id: expectedWorkflowRepositoryId, 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, @@ -315,4 +316,4 @@ describe("GitHub API success-response parsing", () => { message: "GitHub API returned implausible installation-token expiry", }); }); -}); +}); \ No newline at end of file From 34109ae07f87e368c64c16f9ce646016696141b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 03:38:01 -0700 Subject: [PATCH 270/564] test(oidc): carry subject in exchange success fixtures --- test/exchange-success-path-coverage.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/exchange-success-path-coverage.test.ts b/test/exchange-success-path-coverage.test.ts index 1349fd43a..60e480723 100644 --- a/test/exchange-success-path-coverage.test.ts +++ b/test/exchange-success-path-coverage.test.ts @@ -81,6 +81,7 @@ describe("exchange success-path coverage through the public worker", () => { repository_id: expectedWorkflowRepositoryId, workflow_ref: configuredRef, workflow_sha: configuredWorkflowSha, + sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", exp: now + 300, nbf: now - 30, iat: now - 30, @@ -154,4 +155,4 @@ describe("exchange success-path coverage through the public worker", () => { expect(logOutput).not.toContain(oidcToken); expect(logOutput).not.toContain("oidc_sub"); }); -}); +}); \ No newline at end of file From 2ed3ecdd722337ba045d5832e782c17a46a06e49 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 03:42:37 -0700 Subject: [PATCH 271/564] test(oidc): assert hashed subject observability --- test/exchange-success-path-coverage.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/exchange-success-path-coverage.test.ts b/test/exchange-success-path-coverage.test.ts index 60e480723..d1d83fe51 100644 --- a/test/exchange-success-path-coverage.test.ts +++ b/test/exchange-success-path-coverage.test.ts @@ -72,6 +72,7 @@ describe("exchange success-path coverage through the public worker", () => { ])("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 oidcSubject = "repo:ContextualWisdomLab/.github:ref:refs/heads/main"; const { token: oidcToken, jwk } = await createSignedJwt({ iss: env.ALLOWED_ISSUER, aud: env.ALLOWED_AUDIENCE, @@ -81,7 +82,7 @@ describe("exchange success-path coverage through the public worker", () => { repository_id: expectedWorkflowRepositoryId, workflow_ref: configuredRef, workflow_sha: configuredWorkflowSha, - sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", + sub: oidcSubject, exp: now + 300, nbf: now - 30, iat: now - 30, @@ -153,6 +154,7 @@ describe("exchange success-path coverage through the public worker", () => { const logOutput = logSpy.mock.calls.flat().join("\n"); expect(logOutput).not.toContain("ghs_exchange_success_token"); expect(logOutput).not.toContain(oidcToken); - expect(logOutput).not.toContain("oidc_sub"); + expect(logOutput).not.toContain(oidcSubject); + expect(logOutput).toMatch(/"oidc_sub":"[0-9a-f]{8}"/); }); }); \ No newline at end of file From 46c412827e16001b9ca440e5d12003b62940e206 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 03:50:06 -0700 Subject: [PATCH 272/564] fix(oidc): remove unreachable subject branch --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 364c53fc5..1abbbe34d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -806,7 +806,7 @@ async function handleExchange(request: Request, env: Env, traceId: string): Prom const bearerToken = parseExactBearerToken(authorization); if (!bearerToken) throw new ApiError("ERR_AUTH_MISSING", 401, "Missing bearer token"); const claims = await verifyGithubOidcJwt(bearerToken, env); - const oidc_sub = claims.sub ? safeHash(claims.sub).slice(0, 16) : undefined; + const oidc_sub = safeHash(claims.sub!).slice(0, 16); const { repository, token, token_expires_at, replay_protected } = await createRepositoryInstallationToken(request, claims, env); const workflow_ref = claims.job_workflow_ref || claims.workflow_ref!; const response = successResponse( From 6a32233217d5a2a76981a5c56c285235cc5a3f38 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 05:00:52 -0700 Subject: [PATCH 273/564] test(trace): reject normalized non-ASCII trace authority --- test/trace-header-coverage.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/trace-header-coverage.test.ts b/test/trace-header-coverage.test.ts index e3ebd6fa3..44ed93a7a 100644 --- a/test/trace-header-coverage.test.ts +++ b/test/trace-header-coverage.test.ts @@ -28,4 +28,18 @@ describe("trace header selection", () => { expect(payload.trace_id).toBe("correlation:trace_456"); expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); }); + + it("does not normalize non-ASCII surrounding whitespace into trusted trace authority", async () => { + const response = await worker.fetch(new Request("https://noema.example/health", { + headers: { + "x-request-id": "\u00a0request.trace-123\u00a0", + "x-correlation-id": "correlation:trace_456", + }, + }), env); + + expect(response.status).toBe(200); + const payload = await response.json() as { trace_id: string }; + expect(payload.trace_id).toBe("correlation:trace_456"); + expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); + }); }); From 5022e3d73620f93501cdf50b4a06a9004ef56509 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 05:04:27 -0700 Subject: [PATCH 274/564] test(trace): keep production runtime-boundary coverage --- test/trace-header-coverage.test.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/trace-header-coverage.test.ts b/test/trace-header-coverage.test.ts index 44ed93a7a..e3ebd6fa3 100644 --- a/test/trace-header-coverage.test.ts +++ b/test/trace-header-coverage.test.ts @@ -28,18 +28,4 @@ describe("trace header selection", () => { expect(payload.trace_id).toBe("correlation:trace_456"); expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); }); - - it("does not normalize non-ASCII surrounding whitespace into trusted trace authority", async () => { - const response = await worker.fetch(new Request("https://noema.example/health", { - headers: { - "x-request-id": "\u00a0request.trace-123\u00a0", - "x-correlation-id": "correlation:trace_456", - }, - }), env); - - expect(response.status).toBe(200); - const payload = await response.json() as { trace_id: string }; - expect(payload.trace_id).toBe("correlation:trace_456"); - expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); - }); }); From 77c37ab6c939147d60e30f8bc0045c71444e5fd3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 05:05:09 -0700 Subject: [PATCH 275/564] test(oidc): bound complete Authorization field --- test/bearer-authorization.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/bearer-authorization.test.ts b/test/bearer-authorization.test.ts index b33de92c5..cf7b2732d 100644 --- a/test/bearer-authorization.test.ts +++ b/test/bearer-authorization.test.ts @@ -26,12 +26,14 @@ describe("canonical OIDC bearer framing", () => { expect(parseExactBearerToken(`bearer ${canonicalToken}`)).toBe(canonicalToken); }); - it("bounds canonical bearer credential bytes before downstream JWT parsing", async () => { - const maximumToken = `${canonicalHeader}.${canonicalPayload}.${"A".repeat(16_376)}`; - const oversizedAuthorization = `Bearer ${canonicalHeader}.${canonicalPayload}.${"A".repeat(16_377)}`; + it("bounds the complete Authorization field before downstream JWT parsing", async () => { + const maximumToken = `${canonicalHeader}.${canonicalPayload}.${"A".repeat(16_369)}`; + const maximumAuthorization = `Bearer ${maximumToken}`; + const oversizedAuthorization = `Bearer ${canonicalHeader}.${canonicalPayload}.${"A".repeat(16_370)}`; - expect(maximumToken).toHaveLength(16_384); - expect(parseExactBearerToken(`Bearer ${maximumToken}`)).toBe(maximumToken); + expect(maximumAuthorization).toHaveLength(16_384); + expect(parseExactBearerToken(maximumAuthorization)).toBe(maximumToken); + expect(oversizedAuthorization).toHaveLength(16_385); expect(parseExactBearerToken(oversizedAuthorization)).toBeUndefined(); const response = await baseWorker.fetch(new Request("https://noema.example/exchange", { From 91805204869c65b985b4e9dd2c6c9bac2b910afe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 05:05:44 -0700 Subject: [PATCH 276/564] fix(oidc): bound complete Authorization field --- src/bearer-authorization.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index 987bb7cd1..7203d17b5 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -1,4 +1,4 @@ -const maximumBearerTokenLength = 16_384; +const maximumAuthorizationFieldLength = 16_384; const canonicalBearerAuthorizationPattern = /^Bearer ([\x21-\x7e]+)$/i; const utf8BomBase64UrlPrefix = "77u_"; @@ -80,17 +80,18 @@ function hasDuplicateTopLevelJsonKeys(text: string): boolean { /** * Return the bearer credential only when the Authorization field is already in the * canonical `Bearer ` form and remains within the bounded OIDC - * credential envelope. The parser never trims or normalizes attacker-controlled framing. - * Each JWT segment must already be non-empty canonical unpadded base64url. Protected - * headers and payloads must also already be valid UTF-8; BOM-prefixed authority and - * duplicate top-level JSON member names after escape decoding are rejected before any - * claim reader can silently reinterpret the signed authority bytes. + * credential envelope. The complete Authorization field, including the scheme and one + * ASCII separator, must fit within the reviewed 16 KiB limit. The parser never trims or + * normalizes attacker-controlled framing. Each JWT segment must already be non-empty + * canonical unpadded base64url. Protected headers and payloads must also already be valid + * UTF-8; BOM-prefixed authority and duplicate top-level JSON member names after escape + * decoding are rejected before any claim reader can silently reinterpret signed bytes. * * @param authorization Raw HTTP Authorization field bytes decoded as a JavaScript string. * @returns The exact bearer credential when framing and bounds are canonical; otherwise undefined. */ export function parseExactBearerToken(authorization: string): string | undefined { - if (authorization.length > "Bearer ".length + maximumBearerTokenLength) return undefined; + if (authorization.length > maximumAuthorizationFieldLength) return undefined; const token = canonicalBearerAuthorizationPattern.exec(authorization)?.[1]; if (!token) return undefined; const segments = token.split("."); From 1855b94c0973c3087560b3b3e986412880d75b38 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 05:09:09 -0700 Subject: [PATCH 277/564] test(oidc): keep boundary JWT segments canonical --- test/bearer-authorization.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/bearer-authorization.test.ts b/test/bearer-authorization.test.ts index cf7b2732d..284d9ca82 100644 --- a/test/bearer-authorization.test.ts +++ b/test/bearer-authorization.test.ts @@ -27,7 +27,8 @@ describe("canonical OIDC bearer framing", () => { }); it("bounds the complete Authorization field before downstream JWT parsing", async () => { - const maximumToken = `${canonicalHeader}.${canonicalPayload}.${"A".repeat(16_369)}`; + const boundaryPayload = Buffer.from("[0]", "utf8").toString("base64url"); + const maximumToken = `${canonicalHeader}.${boundaryPayload}.${"A".repeat(16_368)}`; const maximumAuthorization = `Bearer ${maximumToken}`; const oversizedAuthorization = `Bearer ${canonicalHeader}.${canonicalPayload}.${"A".repeat(16_370)}`; From 95b3a237eb3a5b568f15301e670ff2942603f9af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 06:05:21 -0700 Subject: [PATCH 278/564] test(oidc): require current central workflow source commit --- 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 7b46b6955..c8aa386c2 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 = - "548a97560070d31b03d14bee0ac98a990bd88482"; + "17052a7ca3c16db90932a4d6036b43165ddee418"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 1fc7b3fa9682911a8a8a41d9df337a75ed49fbec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 06:06:21 -0700 Subject: [PATCH 279/564] fix(oidc): trust current central workflow source commit --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 65cfb15f2..748418125 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 = "548a97560070d31b03d14bee0ac98a990bd88482" +ALLOWED_WORKFLOW_SHA = "17052a7ca3c16db90932a4d6036b43165ddee418" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From cb35aed22b50a2708c70f2131397cc3b8f3a5a45 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 06:08:08 -0700 Subject: [PATCH 280/564] docs(architecture): bind current central workflow source commit --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 005d63bf4..fbfdaffdd 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs readiness dispatch and delegates `/exchange`; `src/entrypoint.ts` applies the distributed rate limiter before `src/worker.ts` performs its denial-only exact workflow-ref precheck. `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `548a97560070d31b03d14bee0ac98a990bd88482`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `17052a7ca3c16db90932a4d6036b43165ddee418`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The protected workflow-ref parser and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 3deefca944d1dba0edb4a52c54c1c527a3215157 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 06:33:11 -0700 Subject: [PATCH 281/564] test(rate-limit): reject BOM-prefixed JSON authority --- ...ibuted-rate-limit-bom-canonicality.test.ts | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 test/distributed-rate-limit-bom-canonicality.test.ts diff --git a/test/distributed-rate-limit-bom-canonicality.test.ts b/test/distributed-rate-limit-bom-canonicality.test.ts new file mode 100644 index 000000000..8c9d47d17 --- /dev/null +++ b/test/distributed-rate-limit-bom-canonicality.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it, vi } from "vitest"; +import { + checkDistributedRateLimit, + DistributedRateLimitUnavailable, + NoemaRateLimiter, +} from "../src/rate-limit"; + +function bomPrefixedJson(value: unknown): Uint8Array { + const json = new TextEncoder().encode(JSON.stringify(value)); + const bytes = new Uint8Array(json.byteLength + 3); + bytes.set([0xef, 0xbb, 0xbf], 0); + bytes.set(json, 3); + return bytes; +} + +function fakeDurableObjectState() { + const records = new Map(); + const setAlarm = vi.fn(async () => undefined); + const deleteAll = vi.fn(async () => { + records.clear(); + }); + const storage = { + async transaction(callback: (transaction: { + get(key: string): Promise; + put(key: string, value: V): Promise; + setAlarm(timestamp: number): Promise; + }) => Promise): Promise { + return callback({ + async get(key: string): Promise { + return records.get(key) as V | undefined; + }, + async put(key: string, value: V): Promise { + records.set(key, value); + }, + setAlarm, + }); + }, + setAlarm, + deleteAll, + }; + return { + state: { storage } as unknown as DurableObjectState, + records, + setAlarm, + }; +} + +describe("distributed rate-limit JSON byte canonicality", () => { + it("rejects a UTF-8 BOM-prefixed internal request before mutating limiter state", async () => { + const fake = fakeDurableObjectState(); + const limiter = new NoemaRateLimiter(fake.state); + const response = await limiter.fetch(new Request("https://noema-rate-limit.internal/check", { + method: "POST", + headers: { "content-type": "application/json" }, + body: bomPrefixedJson({ limit: 1 }), + })); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + ok: false, + error: "malformed_json", + }); + expect(fake.records.size).toBe(0); + expect(fake.setAlarm).not.toHaveBeenCalled(); + }); + + it("fails closed when the Durable Object decision starts with a UTF-8 BOM", async () => { + const namespace = { + idFromName() { + return { toString: () => "exchange:test" } as DurableObjectId; + }, + get() { + return { + fetch: async () => new Response(bomPrefixedJson({ + allowed: true, + limit: 60, + remaining: 59, + retry_after_seconds: 0, + }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + } as unknown as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; + + const decision = checkDistributedRateLimit( + new Request("https://noema.example/exchange", { + headers: { "cf-connecting-ip": "203.0.113.50" }, + }), + { + NOEMA_RATE_LIMIT_PER_MINUTE: "60", + NOEMA_RATE_LIMITER: namespace, + }, + ); + + await expect(decision).rejects.toBeInstanceOf(DistributedRateLimitUnavailable); + }); +}); From 5af27a4686b8b54ee52a2fdc78c2a4cd38c893fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 06:36:23 -0700 Subject: [PATCH 282/564] fix(rate-limit): reject BOM-prefixed JSON authority --- src/rate-limit.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/rate-limit.ts b/src/rate-limit.ts index 0871a6a03..82c88311b 100644 --- a/src/rate-limit.ts +++ b/src/rate-limit.ts @@ -321,7 +321,8 @@ async function readBoundedRateLimitRequest(request: Request): Promise Date: Thu, 27 Aug 2026 07:14:24 -0700 Subject: [PATCH 283/564] test(exchange): reject BOM-prefixed JSON authority --- test/exchange-json-integrity.test.ts | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/exchange-json-integrity.test.ts b/test/exchange-json-integrity.test.ts index 5014678b4..a5770c072 100644 --- a/test/exchange-json-integrity.test.ts +++ b/test/exchange-json-integrity.test.ts @@ -195,6 +195,47 @@ describe("exchange JSON integrity", () => { expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('"reason":"unreadable"')); }); + it("rejects a UTF-8 BOM-prefixed JSON body before credential-bearing work", async () => { + const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); + const canonicalBody = new TextEncoder().encode('{"target_repository":"ContextualWisdomLab/noema"}'); + const body = new Uint8Array(canonicalBody.length + 3); + body.set([0xef, 0xbb, 0xbf], 0); + body.set(canonicalBody, 3); + const request = new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: "Bearer a.b.c", + "content-type": "application/json", + "x-request-id": "bom-prefixed-json", + }, + body, + }); + + const response = await entrypoint.fetch( + request, + { GITHUB_API_BASE: "https://example.invalid" } as Env, + ); + + expect(response.status).toBe(400); + expect(response.headers.get("cache-control")).toBe("no-store"); + expect(response.headers.get("pragma")).toBe("no-cache"); + expect(response.headers.get("x-content-type-options")).toBe("nosniff"); + expect(response.headers.get("x-trace-id")).toBe("bom-prefixed-json"); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_VALIDATION_INPUT", + message: "Exchange JSON body could not be read", + details: { + policy: "bounded-exchange-json-body", + body_limit_bytes: "8192", + reason: "unreadable", + }, + trace_id: "bom-prefixed-json", + }); + expect(globalThis.fetch).toBe(nativeFetch); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('"reason":"unreadable"')); + }); + it("returns a no-store duplicate-key response before GitHub egress configuration", async () => { const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); const response = await entrypoint.fetch( From 2296610de8719fe7f80740480c4df00bbc964939 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 07:17:54 -0700 Subject: [PATCH 284/564] fix(exchange): reject BOM-prefixed JSON authority --- src/entrypoint.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/entrypoint.ts b/src/entrypoint.ts index 7ff107161..9427e36bf 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -267,6 +267,17 @@ export async function boundExchangeJsonBody(request: Request): Promise= 3 + && boundedBody[0] === 0xef + && boundedBody[1] === 0xbb + && boundedBody[2] === 0xbf + ) { + return { + ok: false, + failure: { reason: "unreadable", status: 400 }, + }; + } let boundedText: string; try { boundedText = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(boundedBody); @@ -369,7 +380,7 @@ function githubApiConfigurationResponse( headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", - "pragma": "no-cache", + pragma: "no-cache", "x-content-type-options": "nosniff", "x-trace-id": traceId, "x-latency-ms": "0", @@ -397,7 +408,7 @@ function oidcEnvelopeResponse(request: Request): Response { headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", - "pragma": "no-cache", + pragma: "no-cache", "x-content-type-options": "nosniff", "x-trace-id": traceId, "x-latency-ms": "0", @@ -443,7 +454,7 @@ function exchangeBodyResponse(request: Request, failure: ExchangeBodyFailure): R headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", - "pragma": "no-cache", + pragma: "no-cache", "x-content-type-options": "nosniff", "x-trace-id": traceId, "x-latency-ms": "0", From 173f77f22e6b5bc01d1a4380bc9a6cb181a726bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 07:37:48 -0700 Subject: [PATCH 285/564] test(replay): reject BOM-prefixed authority JSON --- test/oidc-replay-bom-canonicality.test.ts | 100 ++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 test/oidc-replay-bom-canonicality.test.ts diff --git a/test/oidc-replay-bom-canonicality.test.ts b/test/oidc-replay-bom-canonicality.test.ts new file mode 100644 index 000000000..ffaf34843 --- /dev/null +++ b/test/oidc-replay-bom-canonicality.test.ts @@ -0,0 +1,100 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + claimOidcTokenUsage, + NoemaOidcReplayGuard, + OidcReplayUnavailable, +} from "../src/oidc-replay"; + +function namespaceReturning( + handler: (input: RequestInfo | URL, init?: RequestInit) => Promise, +): DurableObjectNamespace { + return { + idFromName(name: string) { + return { toString: () => name } as DurableObjectId; + }, + get() { + return { fetch: handler } as unknown as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; +} + +function fakeDurableObjectState() { + const records = new Map(); + const setAlarm = vi.fn(async () => undefined); + const deleteAll = vi.fn(async () => { + records.clear(); + }); + const storage = { + async transaction( + callback: (transaction: DurableObjectTransaction) => Promise, + ): Promise { + return callback({ + async get(key: string): Promise { + return records.get(key) as V | undefined; + }, + async put(key: string, value: V): Promise { + records.set(key, value); + }, + setAlarm, + } as unknown as DurableObjectTransaction); + }, + setAlarm, + deleteAll, + }; + return { + state: { storage } as unknown as DurableObjectState, + records, + setAlarm, + }; +} + +function bomPrefixedJson(value: unknown): Uint8Array { + const json = new TextEncoder().encode(JSON.stringify(value)); + const body = new Uint8Array(json.length + 3); + body.set([0xef, 0xbb, 0xbf], 0); + body.set(json, 3); + return body; +} + +describe("OIDC replay JSON canonicality", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("rejects a BOM-prefixed replay claim without consuming state", async () => { + vi.spyOn(Date, "now").mockReturnValue(2_000_000); + const fake = fakeDurableObjectState(); + const guard = new NoemaOidcReplayGuard(fake.state); + const response = await guard.fetch(new Request("https://noema-oidc-replay.internal/claim", { + method: "POST", + headers: { "content-type": "application/json" }, + body: bomPrefixedJson({ expires_at_epoch_seconds: 2_600 }), + })); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + ok: false, + error: "malformed_json", + }); + expect(fake.records.size).toBe(0); + expect(fake.setAlarm).not.toHaveBeenCalled(); + }); + + it("rejects a BOM-prefixed replay decision as non-canonical authority", async () => { + vi.spyOn(Date, "now").mockReturnValue(2_000_000); + const namespace = namespaceReturning(async () => new Response( + bomPrefixedJson({ + accepted: true, + expires_at_epoch_seconds: 2_600, + }), + { + status: 201, + headers: { "content-type": "application/json" }, + }, + )); + + await expect(claimOidcTokenUsage("safe-jti", 2_600, { + NOEMA_OIDC_REPLAY_GUARD: namespace, + })).rejects.toBeInstanceOf(OidcReplayUnavailable); + }); +}); From fe7d1d144f53d96e0483588ad2873708b030534c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 07:40:20 -0700 Subject: [PATCH 286/564] fix(replay): preserve BOM for canonical JSON rejection --- src/oidc-replay.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/oidc-replay.ts b/src/oidc-replay.ts index 1c32d04ba..cfe60ec4c 100644 --- a/src/oidc-replay.ts +++ b/src/oidc-replay.ts @@ -271,7 +271,8 @@ async function readBoundedReplayDecision(response: Response): Promise { let text: string; try { - text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes); + // Preserve a leading UTF-8 BOM as U+FEFF so JSON.parse rejects non-canonical authority bytes. + text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes); } catch { throw new OidcReplayUnavailable("OIDC replay guard decision is not valid UTF-8"); } @@ -338,7 +339,8 @@ async function readBoundedClaimRequest(request: Request): Promise Date: Thu, 27 Aug 2026 07:46:15 -0700 Subject: [PATCH 287/564] test(exchange): reject non-HTTP media-type whitespace --- ...exchange-content-type-canonicality.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 test/exchange-content-type-canonicality.test.ts diff --git a/test/exchange-content-type-canonicality.test.ts b/test/exchange-content-type-canonicality.test.ts new file mode 100644 index 000000000..d9735c336 --- /dev/null +++ b/test/exchange-content-type-canonicality.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { boundExchangeJsonBody } from "../src/entrypoint"; + +describe("exchange Content-Type canonicality", () => { + it("rejects non-HTTP whitespace around application/json instead of normalizing it", async () => { + const request = new Request("https://noema.example/exchange", { + method: "POST", + headers: { + "content-type": "\u00a0application/json\u00a0", + }, + body: "{}", + }); + + expect(request.headers.get("content-type")).toBe("\u00a0application/json\u00a0"); + await expect(boundExchangeJsonBody(request)).resolves.toEqual({ + ok: false, + failure: { + reason: "unsupported_media_type", + status: 415, + }, + }); + }); + + it("continues accepting HTTP OWS and case-insensitive application/json media types", async () => { + const request = new Request("https://noema.example/exchange", { + method: "POST", + headers: { + "content-type": "Application/JSON \t; charset=utf-8", + }, + body: "{}", + }); + + const result = await boundExchangeJsonBody(request); + expect(result.ok).toBe(true); + if (!result.ok) throw new Error("expected bounded JSON request"); + await expect(result.request.json()).resolves.toEqual({}); + }); +}); From 2c8f9f1d80c30b923fe1577a58b80f51206460c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 07:50:36 -0700 Subject: [PATCH 288/564] fix(exchange): reject non-HTTP media-type whitespace --- src/entrypoint.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/entrypoint.ts b/src/entrypoint.ts index 9427e36bf..248500ae4 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -211,11 +211,8 @@ export async function boundExchangeJsonBody(request: Request): Promise Date: Thu, 27 Aug 2026 08:01:42 -0700 Subject: [PATCH 289/564] test(exchange): reject unsupported JSON media parameters --- ...exchange-content-type-canonicality.test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/exchange-content-type-canonicality.test.ts b/test/exchange-content-type-canonicality.test.ts index d9735c336..1891d8c68 100644 --- a/test/exchange-content-type-canonicality.test.ts +++ b/test/exchange-content-type-canonicality.test.ts @@ -35,4 +35,40 @@ describe("exchange Content-Type canonicality", () => { if (!result.ok) throw new Error("expected bounded JSON request"); await expect(result.request.json()).resolves.toEqual({}); }); + + it("rejects charset parameters that contradict the UTF-8 body decoder", async () => { + const request = new Request("https://noema.example/exchange", { + method: "POST", + headers: { + "content-type": "application/json; charset=iso-8859-1", + }, + body: "{}", + }); + + await expect(boundExchangeJsonBody(request)).resolves.toEqual({ + ok: false, + failure: { + reason: "unsupported_media_type", + status: 415, + }, + }); + }); + + it("rejects unreviewed media-type parameters instead of silently ignoring them", async () => { + const request = new Request("https://noema.example/exchange", { + method: "POST", + headers: { + "content-type": "application/json; profile=https://example.test/schema", + }, + body: "{}", + }); + + await expect(boundExchangeJsonBody(request)).resolves.toEqual({ + ok: false, + failure: { + reason: "unsupported_media_type", + status: 415, + }, + }); + }); }); From 7f089bfd27f2bed451bfa0862fdb0a4e17494dd5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 08:04:32 -0700 Subject: [PATCH 290/564] fix(exchange): bind JSON body decoder to reviewed media parameters --- src/entrypoint.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/entrypoint.ts b/src/entrypoint.ts index 248500ae4..c72a9d8cf 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -211,8 +211,8 @@ export async function boundExchangeJsonBody(request: Request): Promise Date: Thu, 27 Aug 2026 09:05:07 -0700 Subject: [PATCH 291/564] test(rate-limit): bind decision media type to UTF-8 --- test/rate-limit-response-protocol.test.ts | 27 ++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/test/rate-limit-response-protocol.test.ts b/test/rate-limit-response-protocol.test.ts index 301cabd9e..fb1112e12 100644 --- a/test/rate-limit-response-protocol.test.ts +++ b/test/rate-limit-response-protocol.test.ts @@ -32,7 +32,7 @@ function envReturning(response: Response): DistributedRateLimitEnv { } describe("distributed rate-limit response protocol", () => { - it("accepts only the exact HTTP 200 JSON decision contract", async () => { + it("accepts only the exact HTTP 200 UTF-8 JSON decision contract", async () => { await expect( checkDistributedRateLimit( request, @@ -47,15 +47,22 @@ describe("distributed rate-limit response protocol", () => { checkDistributedRateLimit(request, envReturning(Response.json(decision, { status: 201 }))), ).rejects.toThrow(DistributedRateLimitUnavailable); - await expect( - checkDistributedRateLimit( - request, - envReturning(new Response(JSON.stringify(decision), { - status: 200, - headers: { "content-type": "text/plain; profile=application/json" }, - })), - ), - ).rejects.toThrow(DistributedRateLimitUnavailable); + for (const contentType of [ + "text/plain; profile=application/json", + "application/json; charset=iso-8859-1", + "application/json; profile=https://noema.example/rate-limit", + "application/json; charset=utf-8; profile=https://noema.example/rate-limit", + ]) { + await expect( + checkDistributedRateLimit( + request, + envReturning(new Response(JSON.stringify(decision), { + status: 200, + headers: { "content-type": contentType }, + })), + ), + ).rejects.toThrow(DistributedRateLimitUnavailable); + } }); it("rejects a decision with unexpected top-level authority fields", async () => { From 69002d2ef0dae11b43c0c3b67004acf307321bc4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 09:08:48 -0700 Subject: [PATCH 292/564] fix(rate-limit): bind JSON media type to UTF-8 --- src/rate-limit.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/rate-limit.ts b/src/rate-limit.ts index 82c88311b..0249ccc8a 100644 --- a/src/rate-limit.ts +++ b/src/rate-limit.ts @@ -76,13 +76,12 @@ function jsonResponse(body: unknown, status = 200): Response { } /** - * Tests whether a response Content-Type identifies JSON while tolerating ordinary media-type parameters. - * @param raw Raw Content-Type header value from the trusted internal rate-limit response. - * @returns `true` only when the normalized media type is exactly `application/json`. + * Tests whether a trusted internal Content-Type matches the UTF-8 JSON decoder contract. + * @param raw Raw Content-Type header value from the internal rate-limit request or response. + * @returns `true` only for application/json with no parameter or one reviewed charset=utf-8 parameter. */ export function isJsonMediaType(raw: string | null): boolean { - const mediaType = (raw ?? "").split(";", 1)[0]!.trim().toLowerCase(); - return mediaType === "application/json"; + return /^[ \t]*application\/json[ \t]*(?:;[ \t]*charset[ \t]*=[ \t]*utf-8[ \t]*)?$/i.test(raw ?? ""); } /** From 962818876ddfa752aa75a35456b0929daae19493 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 10:04:58 -0700 Subject: [PATCH 293/564] test(rate-limit): bind internal endpoint authority --- ...-limit-internal-endpoint-authority.test.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 test/rate-limit-internal-endpoint-authority.test.ts diff --git a/test/rate-limit-internal-endpoint-authority.test.ts b/test/rate-limit-internal-endpoint-authority.test.ts new file mode 100644 index 000000000..f881c999e --- /dev/null +++ b/test/rate-limit-internal-endpoint-authority.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it, vi } from "vitest"; +import { NoemaRateLimiter } from "../src/rate-limit"; + +function fakeDurableObjectState() { + const records = new Map(); + const setAlarm = vi.fn(async () => undefined); + const deleteAll = vi.fn(async () => { + records.clear(); + }); + const storage = { + async transaction(callback: (transaction: { + get(key: string): Promise; + put(key: string, value: V): Promise; + setAlarm(timestamp: number): Promise; + }) => Promise): Promise { + return callback({ + async get(key: string): Promise { + return records.get(key) as V | undefined; + }, + async put(key: string, value: V): Promise { + records.set(key, value); + }, + setAlarm, + }); + }, + setAlarm, + deleteAll, + }; + return { + state: { storage } as unknown as DurableObjectState, + records, + setAlarm, + deleteAll, + }; +} + +function limiterRequest(url: string): Request { + return new Request(url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ limit: 60 }), + }); +} + +describe("distributed rate-limit internal endpoint authority", () => { + it.each([ + ["foreign origin", "https://attacker.example/check"], + ["unreviewed query", "https://noema-rate-limit.internal/check?scope=other"], + ])("rejects %s without mutating limiter state", async (_label, url) => { + const fake = fakeDurableObjectState(); + const limiter = new NoemaRateLimiter(fake.state); + + const response = await limiter.fetch(limiterRequest(url)); + + expect(response.status).toBe(404); + expect(fake.records.size).toBe(0); + expect(fake.setAlarm).not.toHaveBeenCalled(); + }); + + it("preserves the exact canonical internal check endpoint", async () => { + const fake = fakeDurableObjectState(); + const limiter = new NoemaRateLimiter(fake.state); + + const response = await limiter.fetch( + limiterRequest("https://noema-rate-limit.internal/check"), + ); + + expect(response.status).toBe(200); + expect(fake.records.size).toBe(1); + expect(fake.setAlarm).toHaveBeenCalledOnce(); + }); +}); From 70cebb23ac48f3cb7bc1cb761667111e2bdf62e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 10:06:28 -0700 Subject: [PATCH 294/564] fix(rate-limit): bind internal endpoint identity --- src/rate-limit.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/rate-limit.ts b/src/rate-limit.ts index 0249ccc8a..7a09f79d0 100644 --- a/src/rate-limit.ts +++ b/src/rate-limit.ts @@ -517,14 +517,20 @@ export class NoemaRateLimiter { /** * Atomically checks and updates one client bucket while returning only the public fail-closed rate-limit decision. * @param request Internal JSON request carrying the validated limit for this Durable Object bucket. - * @returns A JSON response with the 200 allow/deny decision; fail-closed validation returns 404 for the wrong path or method, 415 for a non-JSON media type, 413 for a request above the internal byte limit, 400 for malformed or ambiguous JSON or an invalid limit, and 500 for corrupt persisted limiter state. + * @returns A JSON response with the 200 allow/deny decision; fail-closed validation returns 404 for the wrong endpoint or method, 415 for a non-JSON media type, 413 for a request above the internal byte limit, 400 for malformed or ambiguous JSON or an invalid limit, and 500 for corrupt persisted limiter state. */ async fetch(request: Request): Promise { const url = new URL(request.url); - if (request.method !== "POST" || url.pathname !== "/check") { + if ( + request.method !== "POST" + || url.origin !== "https://noema-rate-limit.internal" + || url.pathname !== "/check" + || url.search !== "" + || url.hash !== "" + ) { if (request.body !== null) { ignoreCancellationBestEffort(() => request.body!.cancel( - "Noema rate-limit request path or method is not accepted", + "Noema rate-limit request endpoint or method is not accepted", )); } return jsonResponse({ ok: false, error: "not_found" }, 404); From 3538f29550f047b1afce1ea12ce0e940591aeb1f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 10:08:49 -0700 Subject: [PATCH 295/564] test(rate-limit): cover fragment endpoint alias --- test/rate-limit-internal-endpoint-authority.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/rate-limit-internal-endpoint-authority.test.ts b/test/rate-limit-internal-endpoint-authority.test.ts index f881c999e..a1bdddf20 100644 --- a/test/rate-limit-internal-endpoint-authority.test.ts +++ b/test/rate-limit-internal-endpoint-authority.test.ts @@ -46,6 +46,7 @@ describe("distributed rate-limit internal endpoint authority", () => { it.each([ ["foreign origin", "https://attacker.example/check"], ["unreviewed query", "https://noema-rate-limit.internal/check?scope=other"], + ["unreviewed fragment", "https://noema-rate-limit.internal/check#other"], ])("rejects %s without mutating limiter state", async (_label, url) => { const fake = fakeDurableObjectState(); const limiter = new NoemaRateLimiter(fake.state); From 1242d9fa12826c6b83c15fa02ef4ca0245d9b64e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 10:12:35 -0700 Subject: [PATCH 296/564] revert(rate-limit): preserve Durable Object URL origin semantics --- src/rate-limit.ts | 12 +-- ...-limit-internal-endpoint-authority.test.ts | 73 ------------------- 2 files changed, 3 insertions(+), 82 deletions(-) delete mode 100644 test/rate-limit-internal-endpoint-authority.test.ts diff --git a/src/rate-limit.ts b/src/rate-limit.ts index 7a09f79d0..0249ccc8a 100644 --- a/src/rate-limit.ts +++ b/src/rate-limit.ts @@ -517,20 +517,14 @@ export class NoemaRateLimiter { /** * Atomically checks and updates one client bucket while returning only the public fail-closed rate-limit decision. * @param request Internal JSON request carrying the validated limit for this Durable Object bucket. - * @returns A JSON response with the 200 allow/deny decision; fail-closed validation returns 404 for the wrong endpoint or method, 415 for a non-JSON media type, 413 for a request above the internal byte limit, 400 for malformed or ambiguous JSON or an invalid limit, and 500 for corrupt persisted limiter state. + * @returns A JSON response with the 200 allow/deny decision; fail-closed validation returns 404 for the wrong path or method, 415 for a non-JSON media type, 413 for a request above the internal byte limit, 400 for malformed or ambiguous JSON or an invalid limit, and 500 for corrupt persisted limiter state. */ async fetch(request: Request): Promise { const url = new URL(request.url); - if ( - request.method !== "POST" - || url.origin !== "https://noema-rate-limit.internal" - || url.pathname !== "/check" - || url.search !== "" - || url.hash !== "" - ) { + if (request.method !== "POST" || url.pathname !== "/check") { if (request.body !== null) { ignoreCancellationBestEffort(() => request.body!.cancel( - "Noema rate-limit request endpoint or method is not accepted", + "Noema rate-limit request path or method is not accepted", )); } return jsonResponse({ ok: false, error: "not_found" }, 404); diff --git a/test/rate-limit-internal-endpoint-authority.test.ts b/test/rate-limit-internal-endpoint-authority.test.ts deleted file mode 100644 index a1bdddf20..000000000 --- a/test/rate-limit-internal-endpoint-authority.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { NoemaRateLimiter } from "../src/rate-limit"; - -function fakeDurableObjectState() { - const records = new Map(); - const setAlarm = vi.fn(async () => undefined); - const deleteAll = vi.fn(async () => { - records.clear(); - }); - const storage = { - async transaction(callback: (transaction: { - get(key: string): Promise; - put(key: string, value: V): Promise; - setAlarm(timestamp: number): Promise; - }) => Promise): Promise { - return callback({ - async get(key: string): Promise { - return records.get(key) as V | undefined; - }, - async put(key: string, value: V): Promise { - records.set(key, value); - }, - setAlarm, - }); - }, - setAlarm, - deleteAll, - }; - return { - state: { storage } as unknown as DurableObjectState, - records, - setAlarm, - deleteAll, - }; -} - -function limiterRequest(url: string): Request { - return new Request(url, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ limit: 60 }), - }); -} - -describe("distributed rate-limit internal endpoint authority", () => { - it.each([ - ["foreign origin", "https://attacker.example/check"], - ["unreviewed query", "https://noema-rate-limit.internal/check?scope=other"], - ["unreviewed fragment", "https://noema-rate-limit.internal/check#other"], - ])("rejects %s without mutating limiter state", async (_label, url) => { - const fake = fakeDurableObjectState(); - const limiter = new NoemaRateLimiter(fake.state); - - const response = await limiter.fetch(limiterRequest(url)); - - expect(response.status).toBe(404); - expect(fake.records.size).toBe(0); - expect(fake.setAlarm).not.toHaveBeenCalled(); - }); - - it("preserves the exact canonical internal check endpoint", async () => { - const fake = fakeDurableObjectState(); - const limiter = new NoemaRateLimiter(fake.state); - - const response = await limiter.fetch( - limiterRequest("https://noema-rate-limit.internal/check"), - ); - - expect(response.status).toBe(200); - expect(fake.records.size).toBe(1); - expect(fake.setAlarm).toHaveBeenCalledOnce(); - }); -}); From d4c52fbe8b2f23db24e2cbeebf205ac9206a3f23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 10:14:15 -0700 Subject: [PATCH 297/564] test(oidc): bind replay JSON media authority --- test/oidc-replay-media-type-authority.test.ts | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 test/oidc-replay-media-type-authority.test.ts diff --git a/test/oidc-replay-media-type-authority.test.ts b/test/oidc-replay-media-type-authority.test.ts new file mode 100644 index 000000000..10f5be960 --- /dev/null +++ b/test/oidc-replay-media-type-authority.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it, vi } from "vitest"; +import { + claimOidcTokenUsage, + NoemaOidcReplayGuard, + OidcReplayUnavailable, +} from "../src/oidc-replay"; + +function namespaceReturning(response: Response): DurableObjectNamespace { + return { + idFromName() { + return { toString: () => "oidc-replay-test" } as DurableObjectId; + }, + get() { + return { + fetch: vi.fn(async () => response), + } as unknown as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; +} + +function fakeDurableObjectState() { + const records = new Map(); + const setAlarm = vi.fn(async () => undefined); + const deleteAll = vi.fn(async () => records.clear()); + const storage = { + async transaction(callback: (transaction: { + get(key: string): Promise; + put(key: string, value: V): Promise; + setAlarm(timestamp: number): Promise; + }) => Promise): Promise { + return callback({ + async get(key: string): Promise { + return records.get(key) as V | undefined; + }, + async put(key: string, value: V): Promise { + records.set(key, value); + }, + setAlarm, + }); + }, + setAlarm, + deleteAll, + }; + return { + state: { storage } as unknown as DurableObjectState, + records, + }; +} + +describe("OIDC replay JSON media authority", () => { + it.each([ + "application/json; charset=iso-8859-1", + "application/json; profile=unreviewed", + "application/json; charset=utf-8; profile=unreviewed", + ])("rejects replay decisions with unreviewed media parameters: %s", async (contentType) => { + vi.spyOn(Date, "now").mockReturnValue(1_000_000); + const expiresAt = 1_100; + const response = new Response(JSON.stringify({ + accepted: true, + expires_at_epoch_seconds: expiresAt, + }), { + status: 201, + headers: { "content-type": contentType }, + }); + + await expect(claimOidcTokenUsage("replay-media-test", expiresAt, { + NOEMA_OIDC_REPLAY_GUARD: namespaceReturning(response), + })).rejects.toBeInstanceOf(OidcReplayUnavailable); + }); + + it("accepts the one reviewed UTF-8 charset declaration for replay decisions", async () => { + vi.spyOn(Date, "now").mockReturnValue(1_000_000); + const expiresAt = 1_100; + const response = new Response(JSON.stringify({ + accepted: true, + expires_at_epoch_seconds: expiresAt, + }), { + status: 201, + headers: { "content-type": "application/json; charset=utf-8" }, + }); + + await expect(claimOidcTokenUsage("replay-media-test", expiresAt, { + NOEMA_OIDC_REPLAY_GUARD: namespaceReturning(response), + })).resolves.toEqual({ + accepted: true, + expires_at_epoch_seconds: expiresAt, + }); + }); + + it("rejects unreviewed request media parameters before mutating replay state", async () => { + vi.spyOn(Date, "now").mockReturnValue(1_000_000); + const fake = fakeDurableObjectState(); + const guard = new NoemaOidcReplayGuard(fake.state); + const response = await guard.fetch(new Request("https://noema-oidc-replay.internal/claim", { + method: "POST", + headers: { "content-type": "application/json; charset=iso-8859-1" }, + body: JSON.stringify({ expires_at_epoch_seconds: 1_100 }), + })); + + expect(response.status).toBe(415); + expect(fake.records.size).toBe(0); + }); +}); From 8318a39ca9cec28670c85c8098390e0ea4e274f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 10:17:59 -0700 Subject: [PATCH 298/564] fix(oidc): bind replay JSON media authority --- src/oidc-replay.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/oidc-replay.ts b/src/oidc-replay.ts index cfe60ec4c..6e71b5828 100644 --- a/src/oidc-replay.ts +++ b/src/oidc-replay.ts @@ -80,11 +80,10 @@ function jsonResponse(body: unknown, status = 200): Response { }); } -function normalizedMediaType(contentType: string | null): string { - return (contentType ?? "") - .split(";", 1)[0] - .trim() - .toLowerCase(); +function isJsonMediaType(contentType: string | null): boolean { + return /^[ \t]*application\/json[ \t]*(?:;[ \t]*charset[ \t]*=[ \t]*utf-8[ \t]*)?$/i.test( + contentType ?? "", + ); } function validJti(jti: string): boolean { @@ -390,7 +389,7 @@ export async function claimOidcTokenUsage( signal: AbortSignal.timeout(REPLAY_GUARD_FETCH_TIMEOUT_MS), }); - if (normalizedMediaType(response.headers.get("content-type")) !== "application/json") { + if (!isJsonMediaType(response.headers.get("content-type"))) { if (response.body !== null) { ignoreReplayCleanupBestEffort(() => response.body!.cancel( "Noema replay decision content type is not accepted", @@ -458,7 +457,7 @@ export class NoemaOidcReplayGuard { } return jsonResponse({ ok: false, error: "not_found" }, 404); } - if (normalizedMediaType(request.headers.get("content-type")) !== "application/json") { + if (!isJsonMediaType(request.headers.get("content-type"))) { if (request.body !== null) { ignoreReplayCleanupBestEffort(() => request.body!.cancel( "Noema replay claim content type is not accepted", From 529c753dc3794a5df44e8a280862ee0a3b5b075c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 10:18:38 -0700 Subject: [PATCH 299/564] test(oidc): reject replay decision shape drift --- ...dc-replay-decision-shape-integrity.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/oidc-replay-decision-shape-integrity.test.ts diff --git a/test/oidc-replay-decision-shape-integrity.test.ts b/test/oidc-replay-decision-shape-integrity.test.ts new file mode 100644 index 000000000..62aa97c47 --- /dev/null +++ b/test/oidc-replay-decision-shape-integrity.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from "vitest"; +import { + claimOidcTokenUsage, + OidcReplayUnavailable, +} from "../src/oidc-replay"; + +function namespaceReturning(response: Response): DurableObjectNamespace { + return { + idFromName() { + return { toString: () => "oidc-replay-shape-test" } as DurableObjectId; + }, + get() { + return { + fetch: vi.fn(async () => response), + } as unknown as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; +} + +describe("OIDC replay decision shape integrity", () => { + it("rejects an otherwise-valid replay decision carrying an unreviewed top-level field", async () => { + vi.spyOn(Date, "now").mockReturnValue(1_000_000); + const expiresAt = 1_100; + const response = Response.json({ + accepted: true, + expires_at_epoch_seconds: expiresAt, + diagnostic: "unreviewed-authority", + }, { status: 201 }); + + await expect(claimOidcTokenUsage("replay-shape-test", expiresAt, { + NOEMA_OIDC_REPLAY_GUARD: namespaceReturning(response), + })).rejects.toBeInstanceOf(OidcReplayUnavailable); + }); +}); From b5e5b3740838648f3172dae093e1199a3d7d6a85 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 10:19:59 -0700 Subject: [PATCH 300/564] fix(oidc): reject replay decision shape drift --- src/oidc-replay.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/oidc-replay.ts b/src/oidc-replay.ts index 6e71b5828..7b2976b29 100644 --- a/src/oidc-replay.ts +++ b/src/oidc-replay.ts @@ -121,10 +121,13 @@ export async function oidcReplayObjectName(jti: string): Promise { } function isClaimDecision(value: unknown): value is OidcReplayClaimDecision { - if (!value || typeof value !== "object") return false; + if (!value || typeof value !== "object" || Array.isArray(value)) return false; const candidate = value as Record; + const keys = Object.keys(candidate); return ( - typeof candidate.accepted === "boolean" + keys.length === replayDecisionKeys.size + && keys.every((key) => replayDecisionKeys.has(key)) + && typeof candidate.accepted === "boolean" && Number.isInteger(candidate.expires_at_epoch_seconds) && Number(candidate.expires_at_epoch_seconds) > 0 ); From 24384db827cf8fdb5e5dc786f1ddeb4f11fe00ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 10:22:52 -0700 Subject: [PATCH 301/564] revert(oidc): preserve forward-compatible replay decision fields --- src/oidc-replay.ts | 7 ++-- ...dc-replay-decision-shape-integrity.test.ts | 34 ------------------- 2 files changed, 2 insertions(+), 39 deletions(-) delete mode 100644 test/oidc-replay-decision-shape-integrity.test.ts diff --git a/src/oidc-replay.ts b/src/oidc-replay.ts index 7b2976b29..6e71b5828 100644 --- a/src/oidc-replay.ts +++ b/src/oidc-replay.ts @@ -121,13 +121,10 @@ export async function oidcReplayObjectName(jti: string): Promise { } function isClaimDecision(value: unknown): value is OidcReplayClaimDecision { - if (!value || typeof value !== "object" || Array.isArray(value)) return false; + if (!value || typeof value !== "object") return false; const candidate = value as Record; - const keys = Object.keys(candidate); return ( - keys.length === replayDecisionKeys.size - && keys.every((key) => replayDecisionKeys.has(key)) - && typeof candidate.accepted === "boolean" + typeof candidate.accepted === "boolean" && Number.isInteger(candidate.expires_at_epoch_seconds) && Number(candidate.expires_at_epoch_seconds) > 0 ); diff --git a/test/oidc-replay-decision-shape-integrity.test.ts b/test/oidc-replay-decision-shape-integrity.test.ts deleted file mode 100644 index 62aa97c47..000000000 --- a/test/oidc-replay-decision-shape-integrity.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { - claimOidcTokenUsage, - OidcReplayUnavailable, -} from "../src/oidc-replay"; - -function namespaceReturning(response: Response): DurableObjectNamespace { - return { - idFromName() { - return { toString: () => "oidc-replay-shape-test" } as DurableObjectId; - }, - get() { - return { - fetch: vi.fn(async () => response), - } as unknown as DurableObjectStub; - }, - } as unknown as DurableObjectNamespace; -} - -describe("OIDC replay decision shape integrity", () => { - it("rejects an otherwise-valid replay decision carrying an unreviewed top-level field", async () => { - vi.spyOn(Date, "now").mockReturnValue(1_000_000); - const expiresAt = 1_100; - const response = Response.json({ - accepted: true, - expires_at_epoch_seconds: expiresAt, - diagnostic: "unreviewed-authority", - }, { status: 201 }); - - await expect(claimOidcTokenUsage("replay-shape-test", expiresAt, { - NOEMA_OIDC_REPLAY_GUARD: namespaceReturning(response), - })).rejects.toBeInstanceOf(OidcReplayUnavailable); - }); -}); From c0f10efb03009981477df1565edcbf4df73ce9cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 11:07:09 -0700 Subject: [PATCH 302/564] test(internal): bind Durable Object endpoint authority --- ...-durable-object-endpoint-authority.test.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 test/internal-durable-object-endpoint-authority.test.ts diff --git a/test/internal-durable-object-endpoint-authority.test.ts b/test/internal-durable-object-endpoint-authority.test.ts new file mode 100644 index 000000000..ad732036f --- /dev/null +++ b/test/internal-durable-object-endpoint-authority.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it, vi } from "vitest"; +import { NoemaOidcReplayGuard } from "../src/oidc-replay"; +import { NoemaRateLimiter } from "../src/rate-limit"; + +function stateThatRejectsStorageAccess() { + const transaction = vi.fn(async () => { + throw new Error("storage must not be touched for a non-canonical internal endpoint"); + }); + const storage = { + transaction, + deleteAll: vi.fn(async () => undefined), + setAlarm: vi.fn(async () => undefined), + }; + return { + state: { storage } as unknown as DurableObjectState, + transaction, + }; +} + +describe("internal Durable Object endpoint authority", () => { + it.each([ + "https://other.internal/check", + "https://noema-rate-limit.internal/check?scope=other", + ])("rejects non-canonical rate-limit endpoint identity before storage: %s", async (url) => { + const fake = stateThatRejectsStorageAccess(); + const limiter = new NoemaRateLimiter(fake.state); + const response = await limiter.fetch(new Request(url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ limit: 60 }), + })); + + expect(response.status).toBe(404); + expect(fake.transaction).not.toHaveBeenCalled(); + }); + + it.each([ + "https://other.internal/claim", + "https://noema-oidc-replay.internal/claim?scope=other", + ])("rejects non-canonical replay endpoint identity before storage: %s", async (url) => { + vi.spyOn(Date, "now").mockReturnValue(1_000_000); + const fake = stateThatRejectsStorageAccess(); + const replayGuard = new NoemaOidcReplayGuard(fake.state); + const response = await replayGuard.fetch(new Request(url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ expires_at_epoch_seconds: 1_100 }), + })); + + expect(response.status).toBe(404); + expect(fake.transaction).not.toHaveBeenCalled(); + }); +}); From e9a19aa32b54b920c1ac7ae873d3bb359f1171b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 11:11:11 -0700 Subject: [PATCH 303/564] fix(rate-limit): bind internal endpoint identity --- src/rate-limit.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/rate-limit.ts b/src/rate-limit.ts index 0249ccc8a..9896c49e9 100644 --- a/src/rate-limit.ts +++ b/src/rate-limit.ts @@ -5,6 +5,7 @@ const MAX_CLIENT_IDENTIFIER_LENGTH = 128; const MAX_RATE_LIMIT_DECISION_BYTES = 4_096; const MAX_RATE_LIMIT_REQUEST_BYTES = 256; const RATE_LIMITER_FETCH_TIMEOUT_MS = 10_000; +const RATE_LIMITER_INTERNAL_ENDPOINT = "https://noema-rate-limit.internal/check"; const strictIpv4SegmentPattern = /^(0|[1-9][0-9]{0,2})$/; const strictIpv6CharacterPattern = /^[0-9A-Fa-f:.]+$/; const BUCKET_KEY = "exchange-rate-limit"; @@ -433,7 +434,7 @@ export async function checkDistributedRateLimit( const objectId = env.NOEMA_RATE_LIMITER.idFromName(objectName); const stub = env.NOEMA_RATE_LIMITER.get(objectId); const expectedLimit = configuredDistributedRateLimit(env.NOEMA_RATE_LIMIT_PER_MINUTE); - const response = await stub.fetch("https://noema-rate-limit.internal/check", { + const response = await stub.fetch(RATE_LIMITER_INTERNAL_ENDPOINT, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ limit: expectedLimit }), @@ -517,14 +518,13 @@ export class NoemaRateLimiter { /** * Atomically checks and updates one client bucket while returning only the public fail-closed rate-limit decision. * @param request Internal JSON request carrying the validated limit for this Durable Object bucket. - * @returns A JSON response with the 200 allow/deny decision; fail-closed validation returns 404 for the wrong path or method, 415 for a non-JSON media type, 413 for a request above the internal byte limit, 400 for malformed or ambiguous JSON or an invalid limit, and 500 for corrupt persisted limiter state. + * @returns A JSON response with the 200 allow/deny decision; fail-closed validation returns 404 for the wrong endpoint or method, 415 for a non-JSON media type, 413 for a request above the internal byte limit, 400 for malformed or ambiguous JSON or an invalid limit, and 500 for corrupt persisted limiter state. */ async fetch(request: Request): Promise { - const url = new URL(request.url); - if (request.method !== "POST" || url.pathname !== "/check") { + if (request.method !== "POST" || request.url !== RATE_LIMITER_INTERNAL_ENDPOINT) { if (request.body !== null) { ignoreCancellationBestEffort(() => request.body!.cancel( - "Noema rate-limit request path or method is not accepted", + "Noema rate-limit request endpoint or method is not accepted", )); } return jsonResponse({ ok: false, error: "not_found" }, 404); From a1d2095387d88d3aca2d6404f9c59bd0ca925240 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 11:12:23 -0700 Subject: [PATCH 304/564] fix(replay): bind internal endpoint identity --- src/oidc-replay.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/oidc-replay.ts b/src/oidc-replay.ts index 6e71b5828..d6854089f 100644 --- a/src/oidc-replay.ts +++ b/src/oidc-replay.ts @@ -5,6 +5,7 @@ const ALARM_GRACE_MS = 30_000; const REPLAY_GUARD_FETCH_TIMEOUT_MS = 10_000; const MAX_REPLAY_GUARD_DECISION_BYTES = 4_096; const MAX_REPLAY_GUARD_REQUEST_BYTES = 512; +const REPLAY_GUARD_INTERNAL_ENDPOINT = "https://noema-oidc-replay.internal/claim"; const trustedJtiPattern = /^[A-Za-z0-9._:-]+$/; const replayDecisionKeys = new Set([ "accepted", @@ -382,7 +383,7 @@ export async function claimOidcTokenUsage( const objectName = await oidcReplayObjectName(jti); const objectId = namespace.idFromName(objectName); const stub = namespace.get(objectId); - const response = await stub.fetch("https://noema-oidc-replay.internal/claim", { + const response = await stub.fetch(REPLAY_GUARD_INTERNAL_ENDPOINT, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ expires_at_epoch_seconds: expiresAtEpochSeconds }), @@ -443,16 +444,15 @@ export class NoemaOidcReplayGuard { constructor(private readonly state: DurableObjectState) {} /** - * Applies the fail-closed replay claim protocol to the internal Durable Object endpoint. + * Applies the fail-closed replay claim protocol to the exact internal Durable Object endpoint. * @param request Internal POST request carrying only the validated token expiry, never the bearer token. - * @returns A JSON response whose 201 or 409 status reflects the atomic replay decision; replay-boundary validation returns 404 for the wrong path or method, 415 for a non-JSON media type, 413 for a request above the internal byte limit, 400 for malformed, ambiguous, or invalid-expiration JSON, and 500 for corrupt persisted replay state. + * @returns A JSON response whose 201 or 409 status reflects the atomic replay decision; replay-boundary validation returns 404 for the wrong endpoint or method, 415 for a non-JSON media type, 413 for a request above the internal byte limit, 400 for malformed, ambiguous, or invalid-expiration JSON, and 500 for corrupt persisted replay state. */ async fetch(request: Request): Promise { - const url = new URL(request.url); - if (request.method !== "POST" || url.pathname !== "/claim") { + if (request.method !== "POST" || request.url !== REPLAY_GUARD_INTERNAL_ENDPOINT) { if (request.body !== null) { ignoreReplayCleanupBestEffort(() => request.body!.cancel( - "Noema replay claim path or method is not accepted", + "Noema replay claim endpoint or method is not accepted", )); } return jsonResponse({ ok: false, error: "not_found" }, 404); From c00fa862794976528c5bc12eadbd5fc142a18949 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 11:17:39 -0700 Subject: [PATCH 305/564] test(rate-limit): preserve malformed-request coverage on canonical endpoint --- test/distributed-rate-limit.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/distributed-rate-limit.test.ts b/test/distributed-rate-limit.test.ts index 6cd752134..c0da85e44 100644 --- a/test/distributed-rate-limit.test.ts +++ b/test/distributed-rate-limit.test.ts @@ -437,11 +437,11 @@ describe("distributed exchange rate limit", () => { const limiter = new NoemaRateLimiter(fake.state); expect((await limiter.fetch(new Request("https://internal/check"))).status).toBe(404); - expect((await limiter.fetch(new Request("https://internal/check", { + expect((await limiter.fetch(new Request("https://noema-rate-limit.internal/check", { method: "POST", body: "{}", }))).status).toBe(415); - expect((await limiter.fetch(new Request("https://internal/check", { + expect((await limiter.fetch(new Request("https://noema-rate-limit.internal/check", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ limit: 0 }), From 197850e21ab2a2afbc9ab3b772d770758bb55631 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 11:18:39 -0700 Subject: [PATCH 306/564] test(replay): preserve malformed-request coverage on canonical endpoint --- test/oidc-replay.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/oidc-replay.test.ts b/test/oidc-replay.test.ts index 7a8dcf7a9..d5329c490 100644 --- a/test/oidc-replay.test.ts +++ b/test/oidc-replay.test.ts @@ -216,11 +216,11 @@ describe("OIDC replay protection", () => { const guard = new NoemaOidcReplayGuard(fakeDurableObjectState().state); expect((await guard.fetch(new Request("https://internal/claim"))).status).toBe(404); - expect((await guard.fetch(new Request("https://internal/claim", { + expect((await guard.fetch(new Request("https://noema-oidc-replay.internal/claim", { method: "POST", body: "{}", }))).status).toBe(415); - expect((await guard.fetch(new Request("https://internal/claim", { + expect((await guard.fetch(new Request("https://noema-oidc-replay.internal/claim", { method: "POST", headers: { "content-type": "application/json" }, body: "not-json", From 686b47fad8a1a4ebf51ceb694fc89adb7270b212 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 11:35:00 -0700 Subject: [PATCH 307/564] test: reject replay decisions with unknown members --- test/oidc-replay-response-bounds.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/oidc-replay-response-bounds.test.ts b/test/oidc-replay-response-bounds.test.ts index 29e853c8e..c9192e3ea 100644 --- a/test/oidc-replay-response-bounds.test.ts +++ b/test/oidc-replay-response-bounds.test.ts @@ -94,6 +94,21 @@ describe("OIDC replay guard response bounds", () => { }); }); + it("rejects replay decisions with unreviewed top-level members", async () => { + vi.spyOn(Date, "now").mockReturnValue(2_000_000); + await expectUnavailable( + () => new Response(JSON.stringify({ + accepted: true, + expires_at_epoch_seconds: 2_600, + unexpected_authority: "ignored-before-repair", + }), { + status: 201, + headers: { "content-type": "application/json" }, + }), + "OIDC replay guard returned an invalid decision", + ); + }); + it("rejects a streamed response larger than the replay decision budget", async () => { vi.spyOn(Date, "now").mockReturnValue(2_000_000); await expectUnavailable( From 0faa1251652cb3b6e497edd761f34befad2c93f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 11:37:40 -0700 Subject: [PATCH 308/564] fix: require exact replay decision schema --- src/oidc-replay.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/oidc-replay.ts b/src/oidc-replay.ts index d6854089f..99d5d82e2 100644 --- a/src/oidc-replay.ts +++ b/src/oidc-replay.ts @@ -122,10 +122,13 @@ export async function oidcReplayObjectName(jti: string): Promise { } function isClaimDecision(value: unknown): value is OidcReplayClaimDecision { - if (!value || typeof value !== "object") return false; + if (!value || typeof value !== "object" || Array.isArray(value)) return false; const candidate = value as Record; + const keys = Object.keys(candidate); return ( - typeof candidate.accepted === "boolean" + keys.length === replayDecisionKeys.size + && keys.every((key) => replayDecisionKeys.has(key)) + && typeof candidate.accepted === "boolean" && Number.isInteger(candidate.expires_at_epoch_seconds) && Number(candidate.expires_at_epoch_seconds) > 0 ); From 1ed0fa7cdef5466176e53638d6011b8dbf467191 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 11:41:32 -0700 Subject: [PATCH 309/564] test: align replay key integrity with exact schema --- test/oidc-replay-duplicate-keys.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/oidc-replay-duplicate-keys.test.ts b/test/oidc-replay-duplicate-keys.test.ts index 2f2e6eae3..1ce31ce5b 100644 --- a/test/oidc-replay-duplicate-keys.test.ts +++ b/test/oidc-replay-duplicate-keys.test.ts @@ -38,17 +38,17 @@ describe("OIDC replay guard decision key integrity", () => { }); }); - it("accepts escaped characters in irrelevant top-level decision keys", async () => { + it("rejects escaped unreviewed top-level decision keys without treating them as duplicate authority", async () => { vi.spyOn(Date, "now").mockReturnValue(2_000_000); - const response = String.raw`{"meta\\key":"ignored","accepted":true,"expires_at_epoch_seconds":2600}`; + const response = String.raw`{"meta\\key":"unreviewed","accepted":true,"expires_at_epoch_seconds":2600}`; await expect(claimOidcTokenUsage( "escaped-replay-decision-key", 2_600, { NOEMA_OIDC_REPLAY_GUARD: namespaceReturningRawDecision(response) }, - )).resolves.toMatchObject({ - accepted: true, - expires_at_epoch_seconds: 2_600, + )).rejects.toMatchObject({ + name: "OidcReplayUnavailable", + message: "OIDC replay guard returned an invalid decision", }); }); From df6a698ec258931c78876c895611d3244be0a497 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 11:42:08 -0700 Subject: [PATCH 310/564] test: align replay residual coverage with exact schema --- test/oidc-replay-residual-coverage.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/oidc-replay-residual-coverage.test.ts b/test/oidc-replay-residual-coverage.test.ts index 7fe8817cf..d55db451c 100644 --- a/test/oidc-replay-residual-coverage.test.ts +++ b/test/oidc-replay-residual-coverage.test.ts @@ -21,7 +21,7 @@ describe("OIDC replay guard residual production coverage", () => { vi.restoreAllMocks(); }); - it("ignores authority-like keys nested below the top-level decision object", async () => { + it("rejects an unreviewed top-level object even when authority-like keys are nested inside it", async () => { vi.spyOn(Date, "now").mockReturnValue(2_000_000); const response = '{"meta":{"accepted":false},"accepted":true,"expires_at_epoch_seconds":2600}'; @@ -34,9 +34,9 @@ describe("OIDC replay guard residual production coverage", () => { headers: { "content-type": "application/json" }, })), }, - )).resolves.toMatchObject({ - accepted: true, - expires_at_epoch_seconds: 2_600, + )).rejects.toMatchObject({ + name: "OidcReplayUnavailable", + message: "OIDC replay guard returned an invalid decision", }); }); From f54623450cab3d3b4a94cf26fe412d809963e82d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 12:04:55 -0700 Subject: [PATCH 311/564] test(oidc): require exact audience authority --- test/oidc-verification-residual-coverage.test.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test/oidc-verification-residual-coverage.test.ts b/test/oidc-verification-residual-coverage.test.ts index b20a97c01..7c271cc1d 100644 --- a/test/oidc-verification-residual-coverage.test.ts +++ b/test/oidc-verification-residual-coverage.test.ts @@ -127,21 +127,17 @@ afterEach(() => { }); describe("OIDC verification residual coverage", () => { - it("accepts an audience array and workflow_ref fallback with valid temporal authority", async () => { + it("rejects a multi-audience token even when one audience matches Noema", async () => { const claims = baseClaims(); claims.aud = ["unrelated-audience", env.ALLOWED_AUDIENCE]; - delete claims.job_workflow_ref; - delete claims.job_workflow_sha; - claims.workflow_ref = configuredWorkflowRef; - claims.workflow_sha = configuredWorkflowSha; const { response } = await exchange(await signedJwt(claims)); - expect(response.status).toBe(400); + expect(response.status).toBe(401); await expect(response.json()).resolves.toMatchObject({ ok: false, - error_code: "ERR_VALIDATION_INPUT", - details: { field: "target_repository" }, + error_code: "ERR_AUTH_INVALID", + message: "OIDC audience is not allowed", }); }); From 8441ecc5220c635f89972febe8789ee8b1006abf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 12:09:32 -0700 Subject: [PATCH 312/564] fix(oidc): require exact audience authority --- src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1abbbe34d..5e933800c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -466,8 +466,9 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise const now = Math.floor(Date.now() / 1000); if (payload.iss !== env.ALLOWED_ISSUER) throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC issuer is not allowed"); - 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 (typeof payload.aud !== "string" || payload.aud !== env.ALLOWED_AUDIENCE) { + throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC audience is not allowed"); + } if (typeof payload.sub !== "string" || payload.sub.length === 0) { throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC subject claim is invalid"); } From e06ce74f95a4d469ece618fed78003099e6f88ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 12:27:27 -0700 Subject: [PATCH 313/564] test(trace): reject normalized non-ASCII trace authority --- test/trace-header-coverage.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/trace-header-coverage.test.ts b/test/trace-header-coverage.test.ts index e3ebd6fa3..560faf80d 100644 --- a/test/trace-header-coverage.test.ts +++ b/test/trace-header-coverage.test.ts @@ -28,4 +28,18 @@ describe("trace header selection", () => { expect(payload.trace_id).toBe("correlation:trace_456"); expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); }); + + it("does not normalize non-ASCII whitespace into trusted trace authority", async () => { + const response = await worker.fetch(new Request("https://noema.example/health", { + headers: { + "x-request-id": "\u00a0request.trace-123\u00a0", + "x-correlation-id": "correlation:trace_456", + }, + }), env); + + expect(response.status).toBe(200); + const payload = await response.json() as { trace_id: string }; + expect(payload.trace_id).toBe("correlation:trace_456"); + expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); + }); }); From 89fce3267577b2088f6609f5400565ef0d7700ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 12:28:12 -0700 Subject: [PATCH 314/564] test(trace): enforce public canonical trace authority --- test/entrypoint-trace-canonicality.test.ts | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test/entrypoint-trace-canonicality.test.ts diff --git a/test/entrypoint-trace-canonicality.test.ts b/test/entrypoint-trace-canonicality.test.ts new file mode 100644 index 000000000..c84174b15 --- /dev/null +++ b/test/entrypoint-trace-canonicality.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it, vi } from "vitest"; +import entrypoint, { type Env } from "../src/entrypoint"; + +describe("public entrypoint trace authority", () => { + it("does not normalize non-ASCII whitespace into a trusted request id", async () => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + const response = await entrypoint.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + "x-request-id": "\u00a0request.trace-123\u00a0", + "x-correlation-id": "correlation:trace_456", + }, + }), + { GITHUB_API_BASE: "https://api.github.com" } as Env, + ); + + expect(response.status).toBe(503); + const payload = await response.json() as { trace_id: string }; + expect(payload.trace_id).toBe("correlation:trace_456"); + expect(response.headers.get("x-trace-id")).toBe("correlation:trace_456"); + }); +}); From c86e45baea242b87aa077288e8e5da93073d4a71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 12:31:27 -0700 Subject: [PATCH 315/564] fix(trace): preserve exact header authority --- src/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5e933800c..8b51e1b97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -168,10 +168,9 @@ function jsonResponse(body: StandardErrorResponse | StandardSuccessResponse maxTrustedHeaderLength) return undefined; - if (!trustedHeaderValuePattern.test(candidate)) return undefined; - return candidate; + if (!value || value.length > maxTrustedHeaderLength) return undefined; + if (!trustedHeaderValuePattern.test(value)) return undefined; + return value; } function traceIdFromRequest(request: Request): string { From 52d9bc2c5f8a4d461aba8b9e7b5ce59f5933b6f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 12:34:23 -0700 Subject: [PATCH 316/564] fix(trace): preserve exact public header authority --- src/entrypoint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/entrypoint.ts b/src/entrypoint.ts index c72a9d8cf..f52442d73 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -317,7 +317,7 @@ export async function boundExchangeJsonBody(request: Request): Promise Date: Thu, 27 Aug 2026 13:05:15 -0700 Subject: [PATCH 317/564] test(oidc): reject non-object JWT JSON envelopes --- test/bearer-authorization.test.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/bearer-authorization.test.ts b/test/bearer-authorization.test.ts index 284d9ca82..65fe638ac 100644 --- a/test/bearer-authorization.test.ts +++ b/test/bearer-authorization.test.ts @@ -27,7 +27,7 @@ describe("canonical OIDC bearer framing", () => { }); it("bounds the complete Authorization field before downstream JWT parsing", async () => { - const boundaryPayload = Buffer.from("[0]", "utf8").toString("base64url"); + const boundaryPayload = Buffer.from("{} ", "utf8").toString("base64url"); const maximumToken = `${canonicalHeader}.${boundaryPayload}.${"A".repeat(16_368)}`; const maximumAuthorization = `Bearer ${maximumToken}`; const oversizedAuthorization = `Bearer ${canonicalHeader}.${canonicalPayload}.${"A".repeat(16_370)}`; @@ -49,6 +49,22 @@ describe("canonical OIDC bearer framing", () => { }); }); + it.each([ + ["header", "null", canonicalPayload], + ["header", "[]", canonicalPayload], + ["header", "\"protected\"", canonicalPayload], + ["payload", "null", canonicalHeader], + ["payload", "[]", canonicalHeader], + ["payload", "\"claims\"", canonicalHeader], + ])("rejects a valid JSON %s segment whose top-level value is not an object", (segment, json, companion) => { + const encoded = Buffer.from(json, "utf8").toString("base64url"); + const token = segment === "header" + ? `${encoded}.${companion}.${canonicalSignature}` + : `${companion}.${encoded}.${canonicalSignature}`; + + expect(parseExactBearerToken(`Bearer ${token}`)).toBeUndefined(); + }); + it.each([ `Bearer\t${canonicalToken}`, `Bearer\u00a0${canonicalToken}`, From d5ef5327b36a6874b80311eefe631b54e063b7da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 13:06:20 -0700 Subject: [PATCH 318/564] fix(oidc): require object JWT JSON envelopes --- src/bearer-authorization.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index 7203d17b5..276835e01 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -28,6 +28,16 @@ function decodeJwtJsonText(bytes: Uint8Array): string | undefined { } } +function hasNonObjectJsonShape(text: string): boolean { + try { + const parsed = JSON.parse(text) as unknown; + return parsed === null || typeof parsed !== "object" || Array.isArray(parsed); + } catch { + // Leave syntactically malformed object envelopes to the downstream malformed-token path. + return false; + } +} + function hasDuplicateTopLevelJsonKeys(text: string): boolean { const seenKeys = new Set(); let structureDepth = 0; @@ -84,8 +94,10 @@ function hasDuplicateTopLevelJsonKeys(text: string): boolean { * ASCII separator, must fit within the reviewed 16 KiB limit. The parser never trims or * normalizes attacker-controlled framing. Each JWT segment must already be non-empty * canonical unpadded base64url. Protected headers and payloads must also already be valid - * UTF-8; BOM-prefixed authority and duplicate top-level JSON member names after escape - * decoding are rejected before any claim reader can silently reinterpret signed bytes. + * UTF-8 JSON objects; BOM-prefixed authority, syntactically valid non-object envelopes, + * and duplicate top-level JSON member names after escape decoding are rejected before any + * claim reader can silently reinterpret signed bytes. Syntactically malformed JSON remains + * on the downstream malformed-token error boundary. * * @param authorization Raw HTTP Authorization field bytes decoded as a JavaScript string. * @returns The exact bearer credential when framing and bounds are canonical; otherwise undefined. @@ -106,6 +118,8 @@ export function parseExactBearerToken(authorization: string): string | undefined || payloadText === undefined || segments[0].startsWith(utf8BomBase64UrlPrefix) || segments[1].startsWith(utf8BomBase64UrlPrefix) + || hasNonObjectJsonShape(headerText) + || hasNonObjectJsonShape(payloadText) || hasDuplicateTopLevelJsonKeys(headerText) || hasDuplicateTopLevelJsonKeys(payloadText) ) return undefined; From 717d6bd9386da7cf5ffc47c5f853fe8d15d881b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 13:10:04 -0700 Subject: [PATCH 319/564] test(oidc): align non-object rejection contract --- test/oidc-workflow-sha-binding.test.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/test/oidc-workflow-sha-binding.test.ts b/test/oidc-workflow-sha-binding.test.ts index 5ee59a86c..b97fa9933 100644 --- a/test/oidc-workflow-sha-binding.test.ts +++ b/test/oidc-workflow-sha-binding.test.ts @@ -332,13 +332,13 @@ describe("production OIDC reusable-workflow source identity", () => { ); }); - it("leaves decoded non-object claims to the bounded authoritative token parser", async () => { + it("rejects decoded non-object claims at the bounded bearer boundary", async () => { for (const claims of [null, "not-an-object"] as const) { const response = await exchangeWithToken(unsignedJwt(claims)); - expect([400, 401]).toContain(response.status); + expect(response.status).toBe(401); await expect(response.json()).resolves.toMatchObject({ ok: false, - error_code: "ERR_TOKEN_MALFORMED", + error_code: "ERR_AUTH_MISSING", }); } }); @@ -363,10 +363,18 @@ describe("production OIDC reusable-workflow source identity", () => { }); }); - it("leaves malformed decoded claims to the bounded authoritative token parser", async () => { + it("keeps malformed JSON claims on the authoritative malformed-token boundary", async () => { await expectDelegatedMalformedToken("e30.eA.eA", 400); await expectDelegatedMalformedToken("e30.e30", 400); - await expectDelegatedMalformedToken(`e30.${encodeJson([])}.eA`, 401); + }); + + it("rejects decoded array claims at the bounded bearer boundary", async () => { + const response = await exchangeWithToken(unsignedJwt([])); + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_MISSING", + }); }); it("does not decode a source-policy payload above the bounded JWT payload limit", async () => { From 2e1cd10db639c0c31780a16cb2f41b68276576eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 13:15:14 -0700 Subject: [PATCH 320/564] fix(oidc): remove unreachable non-object claim branch --- src/worker.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/worker.ts b/src/worker.ts index 7ef0ca77c..053e6bb19 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -97,9 +97,6 @@ function decodeOidcWorkflowClaims(request: Request): OidcWorkflowClaims | undefi const decoded: unknown = JSON.parse( new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes), ); - if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) { - return undefined; - } return decoded as OidcWorkflowClaims; } catch { return undefined; @@ -493,4 +490,4 @@ export default { headers, }), decision); }, -}; +}; \ No newline at end of file From 22ff97b80410aba1b89c4ed86fb29f45313e23a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 13:31:46 -0700 Subject: [PATCH 321/564] test(worker): reject normalized trace authority --- test/worker-trace-canonicality.test.ts | 58 ++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 test/worker-trace-canonicality.test.ts diff --git a/test/worker-trace-canonicality.test.ts b/test/worker-trace-canonicality.test.ts new file mode 100644 index 000000000..5c52445bc --- /dev/null +++ b/test/worker-trace-canonicality.test.ts @@ -0,0 +1,58 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import worker, { type Env } from "../src/worker"; + +function unavailableRateLimiter(): DurableObjectNamespace { + return { + idFromName(name: string) { + return { toString: () => name } as DurableObjectId; + }, + get() { + return { + fetch: async () => { + throw new Error("distributed limiter unavailable"); + }, + } as unknown as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; +} + +describe("protected worker trace authority", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("does not normalize non-ASCII request-id bytes ahead of a canonical correlation id", async () => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + "cf-connecting-ip": "203.0.113.88", + "x-request-id": "\u00a0normalized-request\u00a0", + "x-correlation-id": "canonical-correlation", + }, + }), + { + 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: "unused", + NOEMA_RATE_LIMITER: unavailableRateLimiter(), + } as Env, + ); + + expect(response.status).toBe(503); + expect(response.headers.get("x-trace-id")).toBe("canonical-correlation"); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_RATE_LIMIT", + trace_id: "canonical-correlation", + }); + }); +}); From 16ba120638dd8e675df934a61cee6ddb6cf3b1bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 13:34:56 -0700 Subject: [PATCH 322/564] fix(worker): preserve canonical trace authority --- src/worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/worker.ts b/src/worker.ts index 053e6bb19..bd1f17296 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -53,7 +53,7 @@ type WorkflowTrustDecision = function traceIdFromRequest(request: Request): string { for (const header of ["x-request-id", "x-correlation-id"]) { - const candidate = request.headers.get(header)?.trim(); + const candidate = request.headers.get(header); if ( candidate && candidate.length <= MAX_TRACE_LENGTH From 58caf631512b8990ef7ac16981f13751f6225f83 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 14:04:21 -0700 Subject: [PATCH 323/564] test(oidc): require central workflow repository authority --- ...-workflow-repository-configuration.test.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 test/oidc-workflow-repository-configuration.test.ts diff --git a/test/oidc-workflow-repository-configuration.test.ts b/test/oidc-workflow-repository-configuration.test.ts new file mode 100644 index 000000000..1f340128c --- /dev/null +++ b/test/oidc-workflow-repository-configuration.test.ts @@ -0,0 +1,102 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import worker, { type Env } from "../src/index"; +import { + evaluateRuntimeReadiness, + type RuntimeReadinessEnv, +} from "../src/runtime-readiness"; + +const centralWorkflowRepository = "ContextualWisdomLab/.github"; +const untrustedWorkflowRepository = "ContextualWisdomLab/noema"; +const workflowSha = "a".repeat(40); + +function encodeSegment(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +function misconfiguredWorkerEnv(): Env { + return { + ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", + ALLOWED_AUDIENCE: "cwl-noema-review", + ALLOWED_REPOSITORY_OWNER: "ContextualWisdomLab", + ALLOWED_WORKFLOW_REPOSITORY: untrustedWorkflowRepository, + ALLOWED_WORKFLOW_REF_PREFIX: + `${untrustedWorkflowRepository}/.github/workflows/noema-review.yml@refs/heads/main`, + ALLOWED_WORKFLOW_SHA: workflowSha, + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: "unused-before-configuration-rejection", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", + }; +} + +describe("central reusable-workflow repository authority", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("fails closed before OIDC egress when the configured workflow repository is not central .github", async () => { + const env = misconfiguredWorkerEnv(); + const now = Math.floor(Date.now() / 1000); + const token = [ + encodeSegment({ alg: "RS256", kid: "configuration-must-fail-first" }), + encodeSegment({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: untrustedWorkflowRepository, + repository_id: "1285107801", + job_workflow_ref: env.ALLOWED_WORKFLOW_REF_PREFIX, + job_workflow_sha: workflowSha, + sub: "repo:ContextualWisdomLab/noema:ref:refs/heads/main", + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }), + "AA", + ].join("."); + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("OIDC egress must not occur for invalid trust configuration", { status: 500 }), + ); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "cf-connecting-ip": "203.0.113.240", + }, + }), + env, + ); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_WORKFLOW_NOT_ALLOWED", + message: "Workflow source trust configuration unavailable", + }); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("marks a non-central workflow repository configuration not ready", async () => { + const env: RuntimeReadinessEnv = { + ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", + ALLOWED_AUDIENCE: "cwl-noema-review", + ALLOWED_REPOSITORY_OWNER: "ContextualWisdomLab", + ALLOWED_WORKFLOW_REPOSITORY: untrustedWorkflowRepository, + ALLOWED_WORKFLOW_REF_PREFIX: + `${untrustedWorkflowRepository}/.github/workflows/noema-review.yml@refs/heads/main`, + ALLOWED_WORKFLOW_SHA: workflowSha, + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: "not-a-key", + }; + + const result = await evaluateRuntimeReadiness(env); + + expect(result.ready).toBe(false); + expect(result.failedChecks).toContain("allowed_workflow_repository"); + expect(centralWorkflowRepository).toBe("ContextualWisdomLab/.github"); + }); +}); From 15d495fcb821189303b993a037ca923f3309e1b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 14:07:29 -0700 Subject: [PATCH 324/564] test(oidc): bind regression to protected worker boundary --- ...-workflow-repository-configuration.test.ts | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/test/oidc-workflow-repository-configuration.test.ts b/test/oidc-workflow-repository-configuration.test.ts index 1f340128c..c94c4fd7c 100644 --- a/test/oidc-workflow-repository-configuration.test.ts +++ b/test/oidc-workflow-repository-configuration.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import worker, { type Env } from "../src/index"; +import worker, { type Env } from "../src/worker"; import { evaluateRuntimeReadiness, type RuntimeReadinessEnv, @@ -13,6 +13,24 @@ function encodeSegment(value: unknown): string { return Buffer.from(JSON.stringify(value)).toString("base64url"); } +function rateLimiterNamespace(): DurableObjectNamespace { + return { + idFromName(name: string) { + return { toString: () => name } as DurableObjectId; + }, + get() { + return { + fetch: async () => Response.json({ + allowed: true, + limit: 60, + remaining: 59, + retry_after_seconds: 0, + }), + } as unknown as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; +} + function misconfiguredWorkerEnv(): Env { return { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", @@ -26,6 +44,7 @@ function misconfiguredWorkerEnv(): Env { GITHUB_APP_ID: "1", GITHUB_APP_PRIVATE_KEY_PEM: "unused-before-configuration-rejection", NOEMA_RATE_LIMIT_PER_MINUTE: "1000", + NOEMA_RATE_LIMITER: rateLimiterNamespace(), }; } @@ -34,7 +53,7 @@ describe("central reusable-workflow repository authority", () => { vi.restoreAllMocks(); }); - it("fails closed before OIDC egress when the configured workflow repository is not central .github", async () => { + it("fails closed after distributed rate limiting but before OIDC egress when the configured workflow repository is not central .github", async () => { const env = misconfiguredWorkerEnv(); const now = Math.floor(Date.now() / 1000); const token = [ @@ -74,7 +93,7 @@ describe("central reusable-workflow repository authority", () => { await expect(response.json()).resolves.toMatchObject({ ok: false, error_code: "ERR_WORKFLOW_NOT_ALLOWED", - message: "Workflow source trust configuration unavailable", + message: "Workflow trust configuration unavailable", }); expect(fetchSpy).not.toHaveBeenCalled(); }); From 01d7c3edd21d5462ee772714b691680106e69b51 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 14:09:46 -0700 Subject: [PATCH 325/564] fix(oidc): require central workflow repository authority --- src/worker.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/worker.ts b/src/worker.ts index bd1f17296..1d69256ce 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -27,6 +27,7 @@ export interface Env extends BaseEnv, DistributedRateLimitEnv, OidcReplayProtect const trustedTracePattern = /^[A-Za-z0-9._:-]+$/; const trustedJtiPattern = /^[A-Za-z0-9._:-]+$/; const trustedOwnerPattern = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/; +const expectedWorkflowRepository = "ContextualWisdomLab/.github"; const canonicalCommitRefPattern = /^[0-9a-f]{40}$/; const anyCaseCommitRefPattern = /^[0-9A-Fa-f]{40}$/; const trustedNamedRefPattern = /^refs\/(?:heads|tags)\/(?=.{1,1024}$)(?!\.)(?![^/]*\.lock(?:\/|$))(?!.*\/\.)(?!.*\/[^/]*\.lock(?:\/|$))(?!.*(?:\.\.|\/\/|@\{|\\|[\x00-\x20\x7f~^:?*\[]))(?!.*[\/.]$)[A-Za-z0-9._/-]+$/; @@ -105,6 +106,7 @@ function decodeOidcWorkflowClaims(request: Request): OidcWorkflowClaims | undefi function isTrustedWorkflowRepository(value: string, owner: string): boolean { if (!trustedOwnerPattern.test(owner)) return false; + if (value !== expectedWorkflowRepository) return false; const prefix = `${owner}/`; if (!value.startsWith(prefix)) return false; const repositoryName = value.slice(prefix.length); From b42691e2f3b6533213a2f38decc396db1ebcfe85 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 14:10:38 -0700 Subject: [PATCH 326/564] fix(readiness): bind workflow authority to central repository --- src/runtime-readiness.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/runtime-readiness.ts b/src/runtime-readiness.ts index d87071865..2403eef3f 100644 --- a/src/runtime-readiness.ts +++ b/src/runtime-readiness.ts @@ -3,6 +3,7 @@ import { isTrustedGithubApiBase } from "./entrypoint"; const trustedAudiencePattern = /^[A-Za-z0-9._:-]{1,128}$/; const trustedOwnerPattern = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/; const expectedRepositoryOwner = "ContextualWisdomLab"; +const expectedWorkflowRepository = "ContextualWisdomLab/.github"; const positiveDecimalPattern = /^[1-9][0-9]*$/; const privateKeyPattern = /^-----BEGIN PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END PRIVATE KEY-----$/; const exactCommitPattern = /^[0-9a-f]{40}$/; @@ -80,6 +81,7 @@ function escapeRegularExpression(value: string): string { } function isTrustedWorkflowRepository(value: string, owner: string): boolean { + if (value !== expectedWorkflowRepository) return false; const prefix = `${owner}/`; if (!value.startsWith(prefix)) return false; const repositoryName = value.slice(prefix.length); From 517b9e651ae4a0b9c1fd28bd07c56a5d84350883 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 14:11:25 -0700 Subject: [PATCH 327/564] test(oidc): model configured distributed limit in trust regression --- test/oidc-workflow-repository-configuration.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/oidc-workflow-repository-configuration.test.ts b/test/oidc-workflow-repository-configuration.test.ts index c94c4fd7c..4f1634bd3 100644 --- a/test/oidc-workflow-repository-configuration.test.ts +++ b/test/oidc-workflow-repository-configuration.test.ts @@ -22,8 +22,8 @@ function rateLimiterNamespace(): DurableObjectNamespace { return { fetch: async () => Response.json({ allowed: true, - limit: 60, - remaining: 59, + limit: 1000, + remaining: 999, retry_after_seconds: 0, }), } as unknown as DurableObjectStub; From 3529a4ec8c06252322a08cbb83b70618dc0abdaa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 14:14:52 -0700 Subject: [PATCH 328/564] test(oidc): cover central repository owner mismatch --- ...-workflow-repository-configuration.test.ts | 97 +++++++++++-------- 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/test/oidc-workflow-repository-configuration.test.ts b/test/oidc-workflow-repository-configuration.test.ts index 4f1634bd3..c975567ef 100644 --- a/test/oidc-workflow-repository-configuration.test.ts +++ b/test/oidc-workflow-repository-configuration.test.ts @@ -48,54 +48,70 @@ function misconfiguredWorkerEnv(): Env { }; } +function unsignedWorkflowToken(env: Env): string { + const now = Math.floor(Date.now() / 1000); + return [ + encodeSegment({ alg: "RS256", kid: "configuration-must-fail-first" }), + encodeSegment({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: env.ALLOWED_WORKFLOW_REPOSITORY, + repository_id: "1285107801", + job_workflow_ref: env.ALLOWED_WORKFLOW_REF_PREFIX, + job_workflow_sha: workflowSha, + sub: "repo:ContextualWisdomLab/noema:ref:refs/heads/main", + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }), + "AA", + ].join("."); +} + +async function expectWorkflowConfigurationFailure(env: Env): Promise { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("OIDC egress must not occur for invalid trust configuration", { status: 500 }), + ); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${unsignedWorkflowToken(env)}`, + "cf-connecting-ip": "203.0.113.240", + }, + }), + env, + ); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_WORKFLOW_NOT_ALLOWED", + message: "Workflow trust configuration unavailable", + }); + expect(fetchSpy).not.toHaveBeenCalled(); +} + describe("central reusable-workflow repository authority", () => { afterEach(() => { vi.restoreAllMocks(); }); it("fails closed after distributed rate limiting but before OIDC egress when the configured workflow repository is not central .github", async () => { - const env = misconfiguredWorkerEnv(); - const now = Math.floor(Date.now() / 1000); - const token = [ - encodeSegment({ alg: "RS256", kid: "configuration-must-fail-first" }), - encodeSegment({ - iss: env.ALLOWED_ISSUER, - aud: env.ALLOWED_AUDIENCE, - repository_owner: env.ALLOWED_REPOSITORY_OWNER, - repository_owner_id: "295022177", - repository: untrustedWorkflowRepository, - repository_id: "1285107801", - job_workflow_ref: env.ALLOWED_WORKFLOW_REF_PREFIX, - job_workflow_sha: workflowSha, - sub: "repo:ContextualWisdomLab/noema:ref:refs/heads/main", - exp: now + 300, - nbf: now - 30, - iat: now - 30, - }), - "AA", - ].join("."); - const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( - new Response("OIDC egress must not occur for invalid trust configuration", { status: 500 }), - ); + await expectWorkflowConfigurationFailure(misconfiguredWorkerEnv()); + }); - const response = await worker.fetch( - new Request("https://noema.example/exchange", { - method: "POST", - headers: { - authorization: `Bearer ${token}`, - "cf-connecting-ip": "203.0.113.240", - }, - }), - env, - ); + it("fails closed when the central workflow repository is paired with a different configured owner", async () => { + const env = misconfiguredWorkerEnv(); + env.ALLOWED_REPOSITORY_OWNER = "OtherWisdomLab"; + env.ALLOWED_WORKFLOW_REPOSITORY = centralWorkflowRepository; + env.ALLOWED_WORKFLOW_REF_PREFIX = + `${centralWorkflowRepository}/.github/workflows/noema-review.yml@refs/heads/main`; - expect(response.status).toBe(503); - await expect(response.json()).resolves.toMatchObject({ - ok: false, - error_code: "ERR_WORKFLOW_NOT_ALLOWED", - message: "Workflow trust configuration unavailable", - }); - expect(fetchSpy).not.toHaveBeenCalled(); + await expectWorkflowConfigurationFailure(env); }); it("marks a non-central workflow repository configuration not ready", async () => { @@ -116,6 +132,5 @@ describe("central reusable-workflow repository authority", () => { expect(result.ready).toBe(false); expect(result.failedChecks).toContain("allowed_workflow_repository"); - expect(centralWorkflowRepository).toBe("ContextualWisdomLab/.github"); }); }); From b8c3e84db2dd112307749677ff40992e88f61659 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 14:35:13 -0700 Subject: [PATCH 329/564] test(github): bound installation token authority --- 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 d64534b14..b29a426b4 100644 --- a/test/github-installation-expiry-defensive-coverage.test.ts +++ b/test/github-installation-expiry-defensive-coverage.test.ts @@ -69,6 +69,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 an oversized installation token before granting credential authority", async () => { + const expiresAt = new Date(Date.now() + 30 * 60 * 1000).toISOString(); + const response = await exchangeWithTokenResponse({ token: "g".repeat(4097), expires_at: expiresAt }, "203.0.113.252"); + 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("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"); From e7486e4084ff5252356da762ab4ae56a75822155 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 14:40:46 -0700 Subject: [PATCH 330/564] fix(github): bound installation token authority --- src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 8b51e1b97..d2e8d85e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,7 +145,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 githubInstallationTokenPattern = /^[\x21-\x7e]+$/; +const githubInstallationTokenPattern = /^[\x21-\x7e]{1,4096}$/; const githubAppPrivateKeyPattern = /^-----BEGIN PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END PRIVATE KEY-----$/; const expectedRepositoryOwnerId = "295022177"; const expectedRepositoryIds = new Map([ @@ -690,6 +690,7 @@ async function createInstallationToken(repository: string, env: Env): Promise 4096 || !githubInstallationTokenPattern.test(token.token) ) { throw new ApiError("ERR_GITHUB_API", 502, "GitHub API returned invalid installation-token response"); From 05fcbb28a02440c4af9946ad1de74d988a241d30 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 15:04:36 -0700 Subject: [PATCH 331/564] test(observability): require collision-resistant OIDC subject receipt --- test/exchange-success-path-coverage.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/exchange-success-path-coverage.test.ts b/test/exchange-success-path-coverage.test.ts index d1d83fe51..1f74cb402 100644 --- a/test/exchange-success-path-coverage.test.ts +++ b/test/exchange-success-path-coverage.test.ts @@ -155,6 +155,6 @@ describe("exchange success-path coverage through the public worker", () => { expect(logOutput).not.toContain("ghs_exchange_success_token"); expect(logOutput).not.toContain(oidcToken); expect(logOutput).not.toContain(oidcSubject); - expect(logOutput).toMatch(/"oidc_sub":"[0-9a-f]{8}"/); + expect(logOutput).toMatch(/"oidc_sub":"[0-9a-f]{32}"/); }); }); \ No newline at end of file From 2d085257564365b8b73519a499175f6e1b95b557 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 15:07:32 -0700 Subject: [PATCH 332/564] fix(observability): strengthen OIDC subject evidence identity --- src/index.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index d2e8d85e0..72d78e939 100644 --- a/src/index.ts +++ b/src/index.ts @@ -188,6 +188,16 @@ function safeHash(input: string): string { return hash.toString(16).padStart(8, "0"); } +async function auditIdentityHash(input: string): Promise { + const digest = new Uint8Array( + await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input)), + ); + return Array.from( + digest.subarray(0, 16), + (byte) => byte.toString(16).padStart(2, "0"), + ).join(""); +} + function configuredRateLimit(env: Env): number { const candidate = env.NOEMA_RATE_LIMIT_PER_MINUTE ?? "60"; if (!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(candidate)) return 60; @@ -807,7 +817,7 @@ async function handleExchange(request: Request, env: Env, traceId: string): Prom const bearerToken = parseExactBearerToken(authorization); if (!bearerToken) throw new ApiError("ERR_AUTH_MISSING", 401, "Missing bearer token"); const claims = await verifyGithubOidcJwt(bearerToken, env); - const oidc_sub = safeHash(claims.sub!).slice(0, 16); + const oidc_sub = await auditIdentityHash(claims.sub!); const { repository, token, token_expires_at, replay_protected } = await createRepositoryInstallationToken(request, claims, env); const workflow_ref = claims.job_workflow_ref || claims.workflow_ref!; const response = successResponse( From ae6bd2a87a6bec1ffe8e99df94ca7625b7e4aafe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 16:07:21 -0700 Subject: [PATCH 333/564] test(exchange): reject unreviewed request authority --- test/exchange-body-exact-schema.test.ts | 71 +++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 test/exchange-body-exact-schema.test.ts diff --git a/test/exchange-body-exact-schema.test.ts b/test/exchange-body-exact-schema.test.ts new file mode 100644 index 000000000..d6e8b262f --- /dev/null +++ b/test/exchange-body-exact-schema.test.ts @@ -0,0 +1,71 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import entrypoint, { + boundExchangeJsonBody, + type Env, +} from "../src/entrypoint"; +import { + resetGlobalOutboundFetchPolicy, + type FetchHost, +} from "../src/outbound-fetch-policy"; + +const nativeFetch = globalThis.fetch; +const validEnvelope = "Bearer a.b.c"; + +function exchangeRequest(body: string, traceId = "exact-body-schema"): Request { + return new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: validEnvelope, + "content-type": "application/json", + "x-request-id": traceId, + }, + body, + }); +} + +describe("exchange JSON exact schema", () => { + afterEach(() => { + resetGlobalOutboundFetchPolicy(); + (globalThis as FetchHost).fetch = nativeFetch; + vi.restoreAllMocks(); + }); + + it("rejects unreviewed top-level request members instead of silently ignoring them", async () => { + const typoBody = '{"target_repositroy":"ContextualWisdomLab/noema"}'; + + await expect(boundExchangeJsonBody(exchangeRequest(typoBody))).resolves.toEqual({ + ok: false, + failure: { reason: "unknown_fields", status: 400 }, + }); + }); + + it("rejects unknown request authority before credential egress without reflecting body bytes", async () => { + const unreviewedBody = '{"target_repository":"ContextualWisdomLab/noema","unexpected_authority":"sensitive-marker"}'; + const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); + + const response = await entrypoint.fetch( + exchangeRequest(unreviewedBody), + { + GITHUB_API_BASE: "https://example.com", + GITHUB_APP_ID: "123456", + } as Env, + ); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_VALIDATION_INPUT", + message: "Exchange JSON body contains unreviewed fields", + details: { + policy: "bounded-exchange-json-body", + reason: "unknown_fields", + }, + trace_id: "exact-body-schema", + }); + expect(globalThis.fetch).toBe(nativeFetch); + const logs = logSpy.mock.calls.flat().join("\n"); + expect(logs).toContain('"reason":"unknown_fields"'); + expect(logs).not.toContain("unexpected_authority"); + expect(logs).not.toContain("sensitive-marker"); + }); +}); From ea50fb122e8a5702dad9800466deb4ea23d1fe50 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 16:10:45 -0700 Subject: [PATCH 334/564] fix(exchange): reject unreviewed request authority --- src/entrypoint.ts | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/entrypoint.ts b/src/entrypoint.ts index f52442d73..1f173f201 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -38,7 +38,7 @@ type EgressFailure = { }; type ExchangeBodyFailure = { - reason: "too_large" | "unreadable" | "duplicate_keys" | "invalid_shape" | "unsupported_media_type"; + reason: "too_large" | "unreadable" | "duplicate_keys" | "invalid_shape" | "unknown_fields" | "unsupported_media_type"; status: 400 | 413 | 415; }; @@ -201,7 +201,8 @@ function cancelReaderBestEffort(reader: ReadableStreamDefaultReader, * 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. * The security-relevant top-level `target_repository` member must appear at most once after - * JSON escape decoding so downstream parsing cannot silently apply last-key-wins semantics. + * JSON escape decoding, and no unreviewed top-level members are accepted, so downstream + * parsing cannot silently apply last-key-wins or ignore operator-supplied authority. * @param request Incoming request whose optional JSON body must be bounded before delegation. * @returns The original request when it is not a POST or has no body; otherwise a rebuilt * bounded request, or a typed failure describing the fail-closed response. @@ -298,6 +299,12 @@ export async function boundExchangeJsonBody(request: Request): Promise key !== "target_repository")) { + return { + ok: false, + failure: { reason: "unknown_fields", status: 400 }, + }; + } } catch { // Preserve the existing downstream malformed-JSON response path after wire-level checks. } @@ -418,6 +425,7 @@ function exchangeBodyResponse(request: Request, failure: ExchangeBodyFailure): R const tooLarge = failure.reason === "too_large"; const duplicateKeys = failure.reason === "duplicate_keys"; const invalidShape = failure.reason === "invalid_shape"; + const unknownFields = failure.reason === "unknown_fields"; const unsupportedMediaType = failure.reason === "unsupported_media_type"; return new Response(JSON.stringify({ ok: false, @@ -428,9 +436,11 @@ function exchangeBodyResponse(request: Request, failure: ExchangeBodyFailure): R ? "Exchange JSON body contains duplicate target_repository keys" : invalidShape ? "Exchange JSON body must be an object" - : unsupportedMediaType - ? "Exchange request body requires application/json" - : "Exchange JSON body could not be read", + : unknownFields + ? "Exchange JSON body contains unreviewed fields" + : unsupportedMediaType + ? "Exchange request body requires application/json" + : "Exchange JSON body could not be read", details: { hint: tooLarge ? "Send only the target_repository JSON field within the documented byte limit." @@ -438,9 +448,11 @@ function exchangeBodyResponse(request: Request, failure: ExchangeBodyFailure): R ? "Send target_repository at most once; JSON escape-equivalent member names count as the same key." : invalidShape ? "Send a JSON object containing the optional target_repository field." - : unsupportedMediaType - ? "Send no request body, or send the optional target_repository body with Content-Type application/json." - : "Retry with a complete application/json request body.", + : unknownFields + ? "Send only the optional target_repository field; unknown members are rejected rather than ignored." + : unsupportedMediaType + ? "Send no request body, or send the optional target_repository body with Content-Type application/json." + : "Retry with a complete application/json request body.", policy: "bounded-exchange-json-body", body_limit_bytes: String(MAX_EXCHANGE_JSON_BODY_BYTES), reason: failure.reason, From 2afc8f4a8847a37a2d585881b10438c8f58c0031 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 16:14:11 -0700 Subject: [PATCH 335/564] docs(api): close exchange request schema --- openapi.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openapi.json b/openapi.json index a8db3d281..dd12959c3 100644 --- a/openapi.json +++ b/openapi.json @@ -261,6 +261,7 @@ }, "ExchangeRequest": { "type": "object", + "additionalProperties": false, "properties": { "target_repository": { "$ref": "#/components/schemas/RepositoryLocator" } } @@ -436,4 +437,4 @@ } } } -} +} \ No newline at end of file From 9329b00de0b6b1c7e958fe7bbfd639ffb6d35942 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 16:18:32 -0700 Subject: [PATCH 336/564] test(exchange): converge exact-schema regressions --- test/exchange-json-integrity.test.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/test/exchange-json-integrity.test.ts b/test/exchange-json-integrity.test.ts index a5770c072..12ae7fbd0 100644 --- a/test/exchange-json-integrity.test.ts +++ b/test/exchange-json-integrity.test.ts @@ -40,37 +40,36 @@ describe("exchange JSON integrity", () => { }); }); - it("does not classify unrelated JSON member names as target_repository", async () => { - const result = await boundExchangeJsonBody(jsonRequest('{"metadata":"target_repository"}')); - - expect(result.ok).toBe(true); - if (!result.ok) throw new Error("expected bounded request"); - await expect(result.request.json()).resolves.toEqual({ metadata: "target_repository" }); + it("rejects unrelated top-level JSON members without misclassifying them as duplicate target_repository", async () => { + await expect( + boundExchangeJsonBody(jsonRequest('{"metadata":"target_repository"}')), + ).resolves.toEqual({ + ok: false, + failure: { reason: "unknown_fields", status: 400 }, + }); }); it("does not classify nested target_repository members as duplicate top-level keys", async () => { const result = await boundExchangeJsonBody(jsonRequest( - '{"target_repository":"ContextualWisdomLab/noema","metadata":{"target_repository":"nested-value"}}', + '{"target_repository":{"target_repository":"nested-value"}}', )); expect(result.ok).toBe(true); if (!result.ok) throw new Error("expected bounded request"); await expect(result.request.json()).resolves.toEqual({ - target_repository: "ContextualWisdomLab/noema", - metadata: { target_repository: "nested-value" }, + target_repository: { target_repository: "nested-value" }, }); }); it("does not classify target_repository members nested below arrays as top-level duplicates", async () => { const result = await boundExchangeJsonBody(jsonRequest( - '{"target_repository":"ContextualWisdomLab/noema","metadata":[{"target_repository":"nested-value"}]}', + '{"target_repository":[{"target_repository":"nested-value"}]}', )); expect(result.ok).toBe(true); if (!result.ok) throw new Error("expected bounded request"); await expect(result.request.json()).resolves.toEqual({ - target_repository: "ContextualWisdomLab/noema", - metadata: [{ target_repository: "nested-value" }], + target_repository: [{ target_repository: "nested-value" }], }); }); From 498955e55b26e1ac796f4e5535c6d359d5bc7f98 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 16:30:13 -0700 Subject: [PATCH 337/564] test(readiness): preserve canonical trace correlation --- ...untime-readiness-trace-correlation.test.ts | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 test/runtime-readiness-trace-correlation.test.ts diff --git a/test/runtime-readiness-trace-correlation.test.ts b/test/runtime-readiness-trace-correlation.test.ts new file mode 100644 index 000000000..81afe1358 --- /dev/null +++ b/test/runtime-readiness-trace-correlation.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; +import entrypoint, { type Env } from "../src/runtime-entrypoint"; + +function dummyNamespace(): DurableObjectNamespace { + return { + idFromName(name: string) { + return { toString: () => name } as DurableObjectId; + }, + get() { + return { + fetch: async () => new Response("unused", { status: 500 }), + } as unknown as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; +} + +async function privateKeyPem(): Promise { + const pair = await crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + ); + const pkcs8 = await crypto.subtle.exportKey("pkcs8", pair.privateKey); + 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 readyEnv(): Promise { + 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: "17052a7ca3c16db90932a4d6036b43165ddee418", + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "123456", + GITHUB_APP_PRIVATE_KEY_PEM: await privateKeyPem(), + GITHUB_APP_INSTALLATION_ID: "987654", + NOEMA_RATE_LIMIT_PER_MINUTE: "60", + NOEMA_RATE_LIMITER: dummyNamespace(), + NOEMA_OIDC_REPLAY_GUARD: dummyNamespace(), + }; +} + +describe("runtime readiness trace correlation", () => { + it("preserves a canonical request trace id in both header and JSON response", async () => { + const traceId = "buyer.readiness-123"; + const response = await entrypoint.fetch( + new Request("https://noema.example/ready", { + headers: { "x-request-id": traceId }, + }), + await readyEnv(), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("x-trace-id")).toBe(traceId); + await expect(response.json()).resolves.toMatchObject({ + trace_id: traceId, + }); + }); +}); From d58b6aa46db17ed055eeeb0976cf10755d3f3bf4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 16:30:57 -0700 Subject: [PATCH 338/564] fix(readiness): preserve canonical trace correlation --- src/runtime-entrypoint.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index fa5d5aada..ffc188b60 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -37,6 +37,20 @@ function canonicalTraceRequest(request: Request): Request { return headers === undefined ? request : new Request(request, { headers }); } +function traceIdFromRequest(request: Request): string { + for (const name of traceHeaderNames) { + const value = request.headers.get(name); + if ( + value !== null + && value.length <= maxTraceHeaderLength + && canonicalTraceHeaderPattern.test(value) + ) { + return value; + } + } + return crypto.randomUUID(); +} + function readinessHeaders( traceId: string, latencyMs: number, @@ -57,7 +71,7 @@ function readinessHeaders( async function runtimeReadinessResponse(request: Request, env: Env): Promise { const startedAt = performance.now(); - const traceId = crypto.randomUUID(); + const traceId = traceIdFromRequest(request); if (request.method !== "GET" && request.method !== "HEAD") { const headers = readinessHeaders( traceId, @@ -117,7 +131,8 @@ async function runtimeReadinessResponse(request: Request, env: Env): Promise Date: Thu, 27 Aug 2026 17:06:47 -0700 Subject: [PATCH 339/564] test(oidc): reject ambiguous JWKS key identifiers --- test/oidc-jwks-key-shape.test.ts | 102 ++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 7 deletions(-) diff --git a/test/oidc-jwks-key-shape.test.ts b/test/oidc-jwks-key-shape.test.ts index 69198ad9f..a4219257c 100644 --- a/test/oidc-jwks-key-shape.test.ts +++ b/test/oidc-jwks-key-shape.test.ts @@ -1,19 +1,27 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +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 signingKid = "malformed-jwks-key-entry"; + 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: - "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main", + 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", NOEMA_RATE_LIMIT_PER_MINUTE: "1000", }; +let signingPrivateKey: CryptoKey; +let signingPublicJwk: JsonWebKey; + function encodeSegment(value: unknown): string { return Buffer.from(JSON.stringify(value)).toString("base64url"); } @@ -21,14 +29,13 @@ function encodeSegment(value: unknown): string { function structurallyValidJwt(): string { const now = Math.floor(Date.now() / 1000); return [ - encodeSegment({ alg: "RS256", kid: "malformed-jwks-key-entry" }), + encodeSegment({ alg: "RS256", kid: signingKid }), encodeSegment({ iss: env.ALLOWED_ISSUER, aud: env.ALLOWED_AUDIENCE, repository_owner: env.ALLOWED_REPOSITORY_OWNER, repository: "ContextualWisdomLab/.github", - job_workflow_ref: - "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main", + job_workflow_ref: configuredWorkflowRef, exp: now + 300, nbf: now - 30, iat: now - 30, @@ -37,6 +44,48 @@ function structurallyValidJwt(): string { ].join("."); } +async function signedJwt(): Promise { + const now = Math.floor(Date.now() / 1000); + const header = encodeSegment({ alg: "RS256", kid: signingKid }); + const payload = encodeSegment({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + 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 signature = new Uint8Array( + await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + signingPrivateKey, + new TextEncoder().encode(`${header}.${payload}`), + ), + ); + return `${header}.${payload}.${Buffer.from(signature).toString("base64url")}`; +} + +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(); @@ -87,7 +136,7 @@ describe("OIDC JWKS key shape", () => { } if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { return Response.json({ - keys: [{ kid: "malformed-jwks-key-entry", kty: "RSA" }], + keys: [{ kid: signingKid, kty: "RSA" }], }); } return new Response("unexpected privileged egress", { status: 500 }); @@ -108,4 +157,43 @@ describe("OIDC JWKS key shape", () => { message: "GitHub OIDC JWKS did not include valid key entries", }); }); + + it("rejects an ambiguous JWKS that assigns the requested kid to multiple RSA keys", async () => { + const token = await signedJwt(); + vi.resetModules(); + const { default: worker } = await import("../src/index"); + 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") { + const key = { ...signingPublicJwk, kid: signingKid, kty: "RSA" }; + return Response.json({ keys: [key, { ...key }] }); + } + return new Response("unexpected privileged 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.239", + }, + body: JSON.stringify({ target_repository: { owner: "ContextualWisdomLab" } }), + }), + env, + ); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_OIDC_VERIFICATION", + message: "GitHub OIDC JWKS assigned an ambiguous signing key id", + }); + }); }); From 44702a350725898e3552ac839b196b347e734b67 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 17:10:27 -0700 Subject: [PATCH 340/564] fix(oidc): reject ambiguous JWKS key identifiers --- src/index.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 72d78e939..ad2053be7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -437,6 +437,18 @@ async function fetchGithubOidcKeys(env: Env, forceRefresh = false): Promise key.kid === kid && key.kty === "RSA"); + if (matches.length > 1) { + throw new ApiError( + "ERR_OIDC_VERIFICATION", + 502, + "GitHub OIDC JWKS assigned an ambiguous signing key id", + ); + } + return matches[0]; +} + async function verifyGithubOidcJwt(token: string, env: Env): Promise { const parts = token.split("."); if (parts.length !== 3) throw new ApiError("ERR_TOKEN_MALFORMED", 400, "OIDC token is not a JWT"); @@ -449,10 +461,10 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise } let jwks = await fetchGithubOidcKeys(env); - let jwk = jwks.keys.find((key) => key.kid === header.kid && key.kty === "RSA"); + let jwk = uniqueRsaSigningKey(jwks, header.kid); if (!jwk) { jwks = await fetchGithubOidcKeys(env, true); - jwk = jwks.keys.find((key) => key.kid === header.kid && key.kty === "RSA"); + jwk = uniqueRsaSigningKey(jwks, header.kid); } if (!jwk) throw new ApiError("ERR_OIDC_VERIFICATION", 401, "OIDC signing key was not found"); From 0dd4653fd4d0ff51cf897602df01da46c05ea75e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 18:03:03 -0700 Subject: [PATCH 341/564] test(oidc): reject unsupported critical JOSE headers --- test/oidc-critical-header-policy.test.ts | 124 +++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 test/oidc-critical-header-policy.test.ts diff --git a/test/oidc-critical-header-policy.test.ts b/test/oidc-critical-header-policy.test.ts new file mode 100644 index 000000000..011eaf31b --- /dev/null +++ b/test/oidc-critical-header-policy.test.ts @@ -0,0 +1,124 @@ +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import worker, { 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 = "unsupported-critical-header"; + +let signingPrivateKey: CryptoKey; +let signingPublicJwk: JsonWebKey; + +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", +}; + +function encodeJson(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +function encodeBytes(value: ArrayBuffer): string { + return Buffer.from(value).toString("base64url"); +} + +async function signedOidcToken(): Promise { + const now = Math.floor(Date.now() / 1000); + const header = encodeJson({ + alg: "RS256", + kid: signingKid, + crit: ["b64"], + b64: true, + }); + const payload = encodeJson({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + 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 signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + signingPrivateKey, + new TextEncoder().encode(`${header}.${payload}`), + ); + return `${header}.${payload}.${encodeBytes(signature)}`; +} + +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(); +}); + +describe("OIDC JOSE critical-header policy", () => { + it("rejects an unsupported critical header before OIDC metadata or JWKS network access", async () => { + const token = await signedOidcToken(); + const fetchedUrls: string[] = []; + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + fetchedUrls.push(url); + 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 }); + }); + + 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.240", + }, + body: JSON.stringify({ target_repository: { owner: "ContextualWisdomLab" } }), + }), + env, + ); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_TOKEN_MALFORMED", + message: "OIDC token header is not acceptable", + }); + expect(fetchedUrls).toEqual([]); + }); +}); From 71cb7f3033103c904da8de90ef7ec88735fcab88 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 18:07:03 -0700 Subject: [PATCH 342/564] test(oidc): bind critical JOSE rejection to bearer boundary --- test/oidc-critical-header-policy.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/oidc-critical-header-policy.test.ts b/test/oidc-critical-header-policy.test.ts index 011eaf31b..be990e76d 100644 --- a/test/oidc-critical-header-policy.test.ts +++ b/test/oidc-critical-header-policy.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { parseExactBearerToken } from "../src/bearer-authorization"; import worker, { type Env } from "../src/index"; const configuredWorkflowRef = @@ -83,8 +84,10 @@ afterEach(() => { }); describe("OIDC JOSE critical-header policy", () => { - it("rejects an unsupported critical header before OIDC metadata or JWKS network access", async () => { + it("rejects an unsupported critical header at the canonical bearer boundary before OIDC network access", async () => { const token = await signedOidcToken(); + expect(parseExactBearerToken(`Bearer ${token}`)).toBeUndefined(); + const fetchedUrls: string[] = []; vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { const url = String(input); @@ -116,9 +119,9 @@ describe("OIDC JOSE critical-header policy", () => { expect(response.status).toBe(401); await expect(response.json()).resolves.toMatchObject({ ok: false, - error_code: "ERR_TOKEN_MALFORMED", - message: "OIDC token header is not acceptable", + error_code: "ERR_AUTH_MISSING", + message: "Missing bearer token", }); expect(fetchedUrls).toEqual([]); }); -}); +}); \ No newline at end of file From bc6f4d69f50d4f0e3412b2e911adfdc6b3b1d12a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 18:07:35 -0700 Subject: [PATCH 343/564] fix(oidc): reject unsupported critical JOSE headers --- src/bearer-authorization.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index 276835e01..9613de186 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -38,6 +38,16 @@ function hasNonObjectJsonShape(text: string): boolean { } } +function hasUnsupportedCriticalHeader(text: string): boolean { + try { + const parsed = JSON.parse(text) as Record; + return Object.prototype.hasOwnProperty.call(parsed, "crit"); + } catch { + // Leave syntactically malformed protected headers to the downstream malformed-token path. + return false; + } +} + function hasDuplicateTopLevelJsonKeys(text: string): boolean { const seenKeys = new Set(); let structureDepth = 0; @@ -95,9 +105,10 @@ function hasDuplicateTopLevelJsonKeys(text: string): boolean { * normalizes attacker-controlled framing. Each JWT segment must already be non-empty * canonical unpadded base64url. Protected headers and payloads must also already be valid * UTF-8 JSON objects; BOM-prefixed authority, syntactically valid non-object envelopes, - * and duplicate top-level JSON member names after escape decoding are rejected before any - * claim reader can silently reinterpret signed bytes. Syntactically malformed JSON remains - * on the downstream malformed-token error boundary. + * unsupported JOSE critical-header semantics, and duplicate top-level JSON member names + * after escape decoding are rejected before any claim reader can silently reinterpret + * signed bytes. Syntactically malformed JSON remains on the downstream malformed-token + * error boundary. * * @param authorization Raw HTTP Authorization field bytes decoded as a JavaScript string. * @returns The exact bearer credential when framing and bounds are canonical; otherwise undefined. @@ -120,8 +131,9 @@ export function parseExactBearerToken(authorization: string): string | undefined || segments[1].startsWith(utf8BomBase64UrlPrefix) || hasNonObjectJsonShape(headerText) || hasNonObjectJsonShape(payloadText) + || hasUnsupportedCriticalHeader(headerText) || hasDuplicateTopLevelJsonKeys(headerText) || hasDuplicateTopLevelJsonKeys(payloadText) ) return undefined; return token; -} +} \ No newline at end of file From ff803d55d5f8b22fc258c5dde11a1112c31d7b95 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 18:13:23 -0700 Subject: [PATCH 344/564] test(oidc): reject unsupported b64 signing semantics --- test/oidc-critical-header-policy.test.ts | 88 +++++++++++++----------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/test/oidc-critical-header-policy.test.ts b/test/oidc-critical-header-policy.test.ts index be990e76d..eda18022e 100644 --- a/test/oidc-critical-header-policy.test.ts +++ b/test/oidc-critical-header-policy.test.ts @@ -34,13 +34,12 @@ function encodeBytes(value: ArrayBuffer): string { return Buffer.from(value).toString("base64url"); } -async function signedOidcToken(): Promise { +async function signedOidcToken(headerExtension: Record): Promise { const now = Math.floor(Date.now() / 1000); const header = encodeJson({ alg: "RS256", kid: signingKid, - crit: ["b64"], - b64: true, + ...headerExtension, }); const payload = encodeJson({ iss: env.ALLOWED_ISSUER, @@ -83,45 +82,56 @@ afterEach(() => { vi.restoreAllMocks(); }); -describe("OIDC JOSE critical-header policy", () => { - it("rejects an unsupported critical header at the canonical bearer boundary before OIDC network access", async () => { - const token = await signedOidcToken(); - expect(parseExactBearerToken(`Bearer ${token}`)).toBeUndefined(); +async function expectRejectedBeforeOidcNetwork(token: string): Promise { + expect(parseExactBearerToken(`Bearer ${token}`)).toBeUndefined(); + + const fetchedUrls: string[] = []; + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + fetchedUrls.push(url); + 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 }); + }); + + 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.240", + }, + body: JSON.stringify({ target_repository: { owner: "ContextualWisdomLab" } }), + }), + env, + ); - const fetchedUrls: string[] = []; - vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { - const url = String(input); - fetchedUrls.push(url); - 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 }); - }); + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_MISSING", + message: "Missing bearer token", + }); + expect(fetchedUrls).toEqual([]); +} - 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.240", - }, - body: JSON.stringify({ target_repository: { owner: "ContextualWisdomLab" } }), - }), - env, +describe("OIDC JOSE critical-header policy", () => { + it("rejects an unsupported critical header at the canonical bearer boundary before OIDC network access", async () => { + await expectRejectedBeforeOidcNetwork( + await signedOidcToken({ crit: ["b64"], b64: true }), ); + }); - expect(response.status).toBe(401); - await expect(response.json()).resolves.toMatchObject({ - ok: false, - error_code: "ERR_AUTH_MISSING", - message: "Missing bearer token", - }); - expect(fetchedUrls).toEqual([]); + it("rejects an unsupported b64 signing-input override even when the malformed token omits crit", async () => { + await expectRejectedBeforeOidcNetwork( + await signedOidcToken({ b64: false }), + ); }); }); \ No newline at end of file From 489ba3441551c38e6ec8ca7789aaaa695c4c7b62 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 18:13:54 -0700 Subject: [PATCH 345/564] fix(oidc): reject unsupported b64 signing semantics --- src/bearer-authorization.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index 9613de186..cf381ccb6 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -38,10 +38,13 @@ function hasNonObjectJsonShape(text: string): boolean { } } -function hasUnsupportedCriticalHeader(text: string): boolean { +function hasUnsupportedJoseSigningSemantics(text: string): boolean { try { const parsed = JSON.parse(text) as Record; - return Object.prototype.hasOwnProperty.call(parsed, "crit"); + return ( + Object.prototype.hasOwnProperty.call(parsed, "crit") + || Object.prototype.hasOwnProperty.call(parsed, "b64") + ); } catch { // Leave syntactically malformed protected headers to the downstream malformed-token path. return false; @@ -105,10 +108,11 @@ function hasDuplicateTopLevelJsonKeys(text: string): boolean { * normalizes attacker-controlled framing. Each JWT segment must already be non-empty * canonical unpadded base64url. Protected headers and payloads must also already be valid * UTF-8 JSON objects; BOM-prefixed authority, syntactically valid non-object envelopes, - * unsupported JOSE critical-header semantics, and duplicate top-level JSON member names - * after escape decoding are rejected before any claim reader can silently reinterpret - * signed bytes. Syntactically malformed JSON remains on the downstream malformed-token - * error boundary. + * unsupported JOSE critical/signing-input semantics, and duplicate top-level JSON member + * names after escape decoding are rejected before any claim reader can silently + * reinterpret signed bytes. Noema does not implement the RFC 7797 `b64` extension, so a + * protected `b64` member is rejected whether or not a malformed token also omits `crit`. + * Syntactically malformed JSON remains on the downstream malformed-token error boundary. * * @param authorization Raw HTTP Authorization field bytes decoded as a JavaScript string. * @returns The exact bearer credential when framing and bounds are canonical; otherwise undefined. @@ -131,7 +135,7 @@ export function parseExactBearerToken(authorization: string): string | undefined || segments[1].startsWith(utf8BomBase64UrlPrefix) || hasNonObjectJsonShape(headerText) || hasNonObjectJsonShape(payloadText) - || hasUnsupportedCriticalHeader(headerText) + || hasUnsupportedJoseSigningSemantics(headerText) || hasDuplicateTopLevelJsonKeys(headerText) || hasDuplicateTopLevelJsonKeys(payloadText) ) return undefined; From 20edfc056c5676dd0ddfa2e9f5144cb61e560df9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 18:27:54 -0700 Subject: [PATCH 346/564] test(rate-limit): reject normalized IPv6 client aliases --- ...limit-client-identity-canonicality.test.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/test/distributed-rate-limit-client-identity-canonicality.test.ts b/test/distributed-rate-limit-client-identity-canonicality.test.ts index b694ad419..b53a79a69 100644 --- a/test/distributed-rate-limit-client-identity-canonicality.test.ts +++ b/test/distributed-rate-limit-client-identity-canonicality.test.ts @@ -28,4 +28,31 @@ describe("distributed rate-limit client identity canonicality", () => { expect(trustedClientIdentifier(request)).toBe("203.0.113.7"); }); -}); + + it.each([ + "2001:0db8::1", + "2001:DB8::1", + "2001:db8:0:0:0:0:0:1", + ])("rejects a non-canonical IPv6 spelling instead of normalizing it into bucket authority: %s", async (clientIp) => { + const request = new Request("https://noema.example/exchange", { + headers: { + "cf-connecting-ip": clientIp, + }, + }); + + expect(trustedClientIdentifier(request)).toBeUndefined(); + await expect(distributedRateLimitObjectName(request)).rejects.toBeInstanceOf( + DistributedRateLimitUnavailable, + ); + }); + + it("preserves an already-canonical Cloudflare client IPv6 identity", () => { + const request = new Request("https://noema.example/exchange", { + headers: { + "cf-connecting-ip": "2001:db8::1", + }, + }); + + expect(trustedClientIdentifier(request)).toBe("2001:db8::1"); + }); +}); \ No newline at end of file From 0b8cceca346eda4e4156b1d16ad65cf785325a28 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 18:29:36 -0700 Subject: [PATCH 347/564] fix(rate-limit): reject non-canonical IPv6 client aliases --- src/rate-limit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rate-limit.ts b/src/rate-limit.ts index 9896c49e9..c7eef8271 100644 --- a/src/rate-limit.ts +++ b/src/rate-limit.ts @@ -127,7 +127,7 @@ function canonicalIpv6(candidate: string): string | undefined { const hostname = new URL(`http://[${candidate}]/`).hostname; if (!hostname.startsWith("[") || !hostname.endsWith("]")) return undefined; const normalized = hostname.slice(1, -1).toLowerCase(); - return normalized.includes(":") ? normalized : undefined; + return normalized.includes(":") && normalized === candidate ? normalized : undefined; } catch { return undefined; } From ec5ff0c4b5606b4f9b8f8cf2dbee82527f21b3a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 18:32:36 -0700 Subject: [PATCH 348/564] fix(rate-limit): preserve semantic IPv6 bucket identity --- src/rate-limit.ts | 2 +- ...limit-client-identity-canonicality.test.ts | 29 +------------------ 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/src/rate-limit.ts b/src/rate-limit.ts index c7eef8271..9896c49e9 100644 --- a/src/rate-limit.ts +++ b/src/rate-limit.ts @@ -127,7 +127,7 @@ function canonicalIpv6(candidate: string): string | undefined { const hostname = new URL(`http://[${candidate}]/`).hostname; if (!hostname.startsWith("[") || !hostname.endsWith("]")) return undefined; const normalized = hostname.slice(1, -1).toLowerCase(); - return normalized.includes(":") && normalized === candidate ? normalized : undefined; + return normalized.includes(":") ? normalized : undefined; } catch { return undefined; } diff --git a/test/distributed-rate-limit-client-identity-canonicality.test.ts b/test/distributed-rate-limit-client-identity-canonicality.test.ts index b53a79a69..b694ad419 100644 --- a/test/distributed-rate-limit-client-identity-canonicality.test.ts +++ b/test/distributed-rate-limit-client-identity-canonicality.test.ts @@ -28,31 +28,4 @@ describe("distributed rate-limit client identity canonicality", () => { expect(trustedClientIdentifier(request)).toBe("203.0.113.7"); }); - - it.each([ - "2001:0db8::1", - "2001:DB8::1", - "2001:db8:0:0:0:0:0:1", - ])("rejects a non-canonical IPv6 spelling instead of normalizing it into bucket authority: %s", async (clientIp) => { - const request = new Request("https://noema.example/exchange", { - headers: { - "cf-connecting-ip": clientIp, - }, - }); - - expect(trustedClientIdentifier(request)).toBeUndefined(); - await expect(distributedRateLimitObjectName(request)).rejects.toBeInstanceOf( - DistributedRateLimitUnavailable, - ); - }); - - it("preserves an already-canonical Cloudflare client IPv6 identity", () => { - const request = new Request("https://noema.example/exchange", { - headers: { - "cf-connecting-ip": "2001:db8::1", - }, - }); - - expect(trustedClientIdentifier(request)).toBe("2001:db8::1"); - }); -}); \ No newline at end of file +}); From 6f952ddb0a20de6b332bb726950cdc4e45d5baa2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 19:01:17 -0700 Subject: [PATCH 349/564] test(oidc): reject mismatched metadata media types --- test/oidc-metadata-content-type.test.ts | 133 ++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 test/oidc-metadata-content-type.test.ts diff --git a/test/oidc-metadata-content-type.test.ts b/test/oidc-metadata-content-type.test.ts new file mode 100644 index 000000000..66b8480aa --- /dev/null +++ b/test/oidc-metadata-content-type.test.ts @@ -0,0 +1,133 @@ +import { afterEach, 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 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: "a".repeat(40), + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: "unused", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", +}; + +function encodeSegment(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +function structurallyValidJwt(): string { + const now = Math.floor(Date.now() / 1000); + return [ + encodeSegment({ alg: "RS256", kid: "content-type-test" }), + encodeSegment({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository: "ContextualWisdomLab/.github", + job_workflow_ref: configuredWorkflowRef, + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }), + "AA", + ].join("."); +} + +async function exchangeWithFetch( + implementation: (input: RequestInfo | URL) => Promise, +): Promise { + vi.resetModules(); + const { default: worker } = await import("../src/index"); + vi.spyOn(globalThis, "fetch").mockImplementation(implementation); + return worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { authorization: `Bearer ${structurallyValidJwt()}` }, + }), + env, + ); +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.resetModules(); +}); + +describe("GitHub OIDC metadata media-type authority", () => { + it("rejects a valid discovery JSON body declared as text/plain before following jwks_uri", async () => { + let jwksFetches = 0; + const response = await exchangeWithFetch(async (input) => { + const url = String(input); + if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { + return new Response(JSON.stringify({ + jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", + }), { + headers: { "content-type": "text/plain" }, + }); + } + if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { + jwksFetches += 1; + } + return new Response("unexpected privileged egress", { status: 500 }); + }); + + expect(response.status).toBe(502); + expect(jwksFetches).toBe(0); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_OIDC_VERIFICATION", + message: "GitHub OIDC discovery document returned an unexpected content type", + }); + }); + + it("rejects a discovery JSON body with no declared media type", async () => { + const response = await exchangeWithFetch(async (input) => { + const url = String(input); + if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { + return new Response(JSON.stringify({ + jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", + })); + } + return new Response("unexpected privileged egress", { status: 500 }); + }); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_OIDC_VERIFICATION", + message: "GitHub OIDC discovery document returned an unexpected content type", + }); + }); + + it("rejects a valid JWKS JSON body declared as text/plain", async () => { + const response = await exchangeWithFetch(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 new Response(JSON.stringify({ + keys: [{ kid: "content-type-test", kty: "RSA" }], + }), { + headers: { "content-type": "text/plain" }, + }); + } + return new Response("unexpected privileged egress", { status: 500 }); + }); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_OIDC_VERIFICATION", + message: "GitHub OIDC JWKS returned an unexpected content type", + }); + }); +}); From e2746c313ac848d47703bed6e5f03c6c8c68eaed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 19:06:37 -0700 Subject: [PATCH 350/564] fix(oidc): bind metadata responses to JSON media type --- src/index.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/index.ts b/src/index.ts index ad2053be7..92d1c5352 100644 --- a/src/index.ts +++ b/src/index.ts @@ -147,6 +147,7 @@ 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 githubInstallationTokenPattern = /^[\x21-\x7e]{1,4096}$/; const githubAppPrivateKeyPattern = /^-----BEGIN PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END PRIVATE KEY-----$/; +const oidcJsonMediaTypePattern = /^[ \t]*application\/json[ \t]*(?:;[ \t]*charset[ \t]*=[ \t]*utf-8[ \t]*)?$/i; const expectedRepositoryOwnerId = "295022177"; const expectedRepositoryIds = new Map([ ["ContextualWisdomLab/noema", "1285107801"], @@ -398,6 +399,13 @@ async function fetchGithubOidcKeys(env: Env, forceRefresh = false): Promise Date: Thu, 27 Aug 2026 19:11:42 -0700 Subject: [PATCH 351/564] test(oidc): cover missing JWKS media type --- test/oidc-metadata-content-type.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/oidc-metadata-content-type.test.ts b/test/oidc-metadata-content-type.test.ts index 66b8480aa..3e7a07406 100644 --- a/test/oidc-metadata-content-type.test.ts +++ b/test/oidc-metadata-content-type.test.ts @@ -130,4 +130,28 @@ describe("GitHub OIDC metadata media-type authority", () => { message: "GitHub OIDC JWKS returned an unexpected content type", }); }); + + it("rejects a JWKS JSON body with no declared media type", async () => { + const response = await exchangeWithFetch(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 new Response(JSON.stringify({ + keys: [{ kid: "content-type-test", kty: "RSA" }], + })); + } + return new Response("unexpected privileged egress", { status: 500 }); + }); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_OIDC_VERIFICATION", + message: "GitHub OIDC JWKS returned an unexpected content type", + }); + }); }); From 6bdc55f62831c94842f38496c0a11caf212f1da4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 19:14:31 -0700 Subject: [PATCH 352/564] test(oidc): cover absent metadata content types --- test/oidc-metadata-content-type.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/oidc-metadata-content-type.test.ts b/test/oidc-metadata-content-type.test.ts index 3e7a07406..d6d40f1a6 100644 --- a/test/oidc-metadata-content-type.test.ts +++ b/test/oidc-metadata-content-type.test.ts @@ -21,6 +21,10 @@ function encodeSegment(value: unknown): string { return Buffer.from(JSON.stringify(value)).toString("base64url"); } +function jsonBytes(value: unknown): Uint8Array { + return new TextEncoder().encode(JSON.stringify(value)); +} + function structurallyValidJwt(): string { const now = Math.floor(Date.now() / 1000); return [ @@ -90,7 +94,7 @@ describe("GitHub OIDC metadata media-type authority", () => { const response = await exchangeWithFetch(async (input) => { const url = String(input); if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { - return new Response(JSON.stringify({ + return new Response(jsonBytes({ jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", })); } @@ -140,7 +144,7 @@ describe("GitHub OIDC metadata media-type authority", () => { }); } if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { - return new Response(JSON.stringify({ + return new Response(jsonBytes({ keys: [{ kid: "content-type-test", kty: "RSA" }], })); } From 8788bd737453d65bd65fab95ebba9b2f81a753f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 19:30:19 -0700 Subject: [PATCH 353/564] test(github-api): reject unreviewed JSON media type --- .../github-api-content-type-authority.test.ts | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 test/github-api-content-type-authority.test.ts diff --git a/test/github-api-content-type-authority.test.ts b/test/github-api-content-type-authority.test.ts new file mode 100644 index 000000000..f0b55ce38 --- /dev/null +++ b/test/github-api-content-type-authority.test.ts @@ -0,0 +1,137 @@ +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 expectedRepositoryOwnerId = "295022177"; +const expectedWorkflowRepositoryId = "1274066402"; + +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: 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() { + const kid = `github-content-type-${crypto.randomUUID()}`; + const now = Math.floor(Date.now() / 1000); + const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); + const payload = encodeSegment({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: expectedRepositoryOwnerId, + repository: "ContextualWisdomLab/.github", + repository_id: expectedWorkflowRepositoryId, + 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" }, + }; +} + +describe("GitHub API JSON media-type authority", () => { + it("rejects a valid installation-token JSON body declared as text/plain", async () => { + 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 new Response(JSON.stringify({ + token: "ghs_unreviewed_media_type", + expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), + }), { + status: 201, + headers: { "content-type": "text/plain" }, + }); + } + return new Response("unexpected privileged egress", { status: 500 }); + }); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { authorization: `Bearer ${token}` }, + }), + { ...env, 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 an unexpected content type", + }); + }); +}); From 8bf2ec89b96008015bbddd6a75361a3c49713437 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 19:40:12 -0700 Subject: [PATCH 354/564] fix(github-api): bind JSON responses to reviewed media type --- src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 92d1c5352..2fd1d5e4e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -656,6 +656,9 @@ async function githubJson(path: string, init: GitHubJsonRequestInit, env: Env): } throw new ApiError("ERR_GITHUB_API", response.status >= 400 ? 400 : 500, "GitHub API request failed"); } + if (!oidcJsonMediaTypePattern.test(response.headers.get("content-type") ?? "")) { + throw new ApiError("ERR_GITHUB_API", 502, "GitHub API returned an unexpected content type"); + } let value: unknown; try { value = await response.json(); @@ -963,4 +966,4 @@ export default { return withOperationalHeaders(response, traceId, latency_ms); } }, -}; +}; \ No newline at end of file From f780dc077e858a5894b5defbf1edb82c6bd33cc7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 19:42:05 -0700 Subject: [PATCH 355/564] chore(source): restore final newline after GitHub API repair --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 2fd1d5e4e..e55c64bf9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -966,4 +966,4 @@ export default { return withOperationalHeaders(response, traceId, latency_ms); } }, -}; \ No newline at end of file +}; From 26d2066af7500d43b4896c1211b45c1f105d9433 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 19:45:31 -0700 Subject: [PATCH 356/564] test(github-api): cover missing JSON media type --- .../github-api-content-type-authority.test.ts | 97 ++++++++++++------- 1 file changed, 64 insertions(+), 33 deletions(-) diff --git a/test/github-api-content-type-authority.test.ts b/test/github-api-content-type-authority.test.ts index f0b55ce38..a39b8d00d 100644 --- a/test/github-api-content-type-authority.test.ts +++ b/test/github-api-content-type-authority.test.ts @@ -94,44 +94,75 @@ async function signedOidcToken() { }; } +async function exchangeWithInstallationTokenResponse( + token: string, + jwk: JsonWebKey, + installationTokenResponse: Response, +) { + 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 installationTokenResponse; + } + return new Response("unexpected privileged egress", { status: 500 }); + }); + + return worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { authorization: `Bearer ${token}` }, + }), + { ...env, GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKeyPem }, + ); +} + +async function expectUnexpectedContentType(response: Response) { + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_API", + message: "GitHub API returned an unexpected content type", + }); +} + describe("GitHub API JSON media-type authority", () => { it("rejects a valid installation-token JSON body declared as text/plain", async () => { 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 new Response(JSON.stringify({ - token: "ghs_unreviewed_media_type", - expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), - }), { - status: 201, - headers: { "content-type": "text/plain" }, - }); - } - return new Response("unexpected privileged egress", { status: 500 }); - }); - - const response = await worker.fetch( - new Request("https://noema.example/exchange", { - method: "POST", - headers: { authorization: `Bearer ${token}` }, + const response = await exchangeWithInstallationTokenResponse( + token, + jwk, + new Response(JSON.stringify({ + token: "ghs_unreviewed_media_type", + expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), + }), { + status: 201, + headers: { "content-type": "text/plain" }, }), - { ...env, 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 an unexpected content type", - }); + await expectUnexpectedContentType(response); + }); + + it("rejects a valid installation-token JSON body with no declared media type", async () => { + const { token, jwk } = await signedOidcToken(); + const body = new TextEncoder().encode(JSON.stringify({ + token: "ghs_missing_media_type", + expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), + })); + const response = await exchangeWithInstallationTokenResponse( + token, + jwk, + new Response(body, { status: 201 }), + ); + + await expectUnexpectedContentType(response); }); }); From 25070d42dd4fd8eff882f2180686aaeb0d574ec4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:01:55 -0700 Subject: [PATCH 357/564] test(github-api): reject BOM-normalized credential JSON --- .../github-api-content-type-authority.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/github-api-content-type-authority.test.ts b/test/github-api-content-type-authority.test.ts index a39b8d00d..8ccfd7dfe 100644 --- a/test/github-api-content-type-authority.test.ts +++ b/test/github-api-content-type-authority.test.ts @@ -33,6 +33,14 @@ function encodeBytes(bytes: ArrayBuffer): string { return Buffer.from(bytes).toString("base64url"); } +function bomPrefixedJsonBytes(value: unknown): Uint8Array { + const json = new TextEncoder().encode(JSON.stringify(value)); + const bytes = new Uint8Array(json.length + 3); + bytes.set([0xef, 0xbb, 0xbf]); + bytes.set(json, 3); + return bytes; +} + function pemFromPkcs8(pkcs8: ArrayBuffer): string { const base64 = Buffer.from(pkcs8).toString("base64"); const lines = base64.match(/.{1,64}/g)?.join("\n") ?? base64; @@ -165,4 +173,26 @@ describe("GitHub API JSON media-type authority", () => { await expectUnexpectedContentType(response); }); + + it("rejects BOM-prefixed installation-token JSON instead of normalizing credential authority", async () => { + const { token, jwk } = await signedOidcToken(); + const response = await exchangeWithInstallationTokenResponse( + token, + jwk, + new Response(bomPrefixedJsonBytes({ + token: "ghs_bom_normalized", + expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), + }), { + status: 201, + headers: { "content-type": "application/json" }, + }), + ); + + 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 27e8930d22ca9db605e2ea8098e230703bb4d9aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:02:25 -0700 Subject: [PATCH 358/564] test(oidc): reject BOM-normalized discovery JSON --- test/oidc-metadata-content-type.test.ts | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/oidc-metadata-content-type.test.ts b/test/oidc-metadata-content-type.test.ts index d6d40f1a6..ac5809d58 100644 --- a/test/oidc-metadata-content-type.test.ts +++ b/test/oidc-metadata-content-type.test.ts @@ -25,6 +25,14 @@ function jsonBytes(value: unknown): Uint8Array { return new TextEncoder().encode(JSON.stringify(value)); } +function bomPrefixedJsonBytes(value: unknown): Uint8Array { + const json = jsonBytes(value); + const bytes = new Uint8Array(json.length + 3); + bytes.set([0xef, 0xbb, 0xbf]); + bytes.set(json, 3); + return bytes; +} + function structurallyValidJwt(): string { const now = Math.floor(Date.now() / 1000); return [ @@ -109,6 +117,32 @@ describe("GitHub OIDC metadata media-type authority", () => { }); }); + it("rejects BOM-prefixed discovery JSON before following jwks_uri", async () => { + let jwksFetches = 0; + const response = await exchangeWithFetch(async (input) => { + const url = String(input); + if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { + return new Response(bomPrefixedJsonBytes({ + jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", + }), { + headers: { "content-type": "application/json" }, + }); + } + if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { + jwksFetches += 1; + } + return new Response("unexpected privileged egress", { status: 500 }); + }); + + expect(response.status).toBe(502); + expect(jwksFetches).toBe(0); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_OIDC_VERIFICATION", + message: "GitHub OIDC discovery document was not valid JSON", + }); + }); + it("rejects a valid JWKS JSON body declared as text/plain", async () => { const response = await exchangeWithFetch(async (input) => { const url = String(input); From e2fc03dd05f165e7b451ea30ac63a1589295b0eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:05:36 -0700 Subject: [PATCH 359/564] fix(json): reject normalized external authority bytes --- src/index.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index e55c64bf9..6c0dfecda 100644 --- a/src/index.ts +++ b/src/index.ts @@ -386,6 +386,12 @@ function decodeJson(segment: string): T { return JSON.parse(decoded) as T; } +async function parseExactUtf8JsonResponse(response: Response): Promise { + const bytes = new Uint8Array(await response.arrayBuffer()); + const decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes); + return JSON.parse(decoded) as unknown; +} + async function fetchGithubOidcKeys(env: Env, forceRefresh = false): Promise { const now = Date.now(); if (!forceRefresh && oidcKeysCache && oidcKeysCache.expiresAtMs > now) { @@ -408,7 +414,7 @@ async function fetchGithubOidcKeys(env: Env, forceRefresh = false): Promise Date: Thu, 27 Aug 2026 20:09:09 -0700 Subject: [PATCH 360/564] test(github-api): reject duplicate credential keys --- .../github-api-content-type-authority.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/github-api-content-type-authority.test.ts b/test/github-api-content-type-authority.test.ts index 8ccfd7dfe..804b696ae 100644 --- a/test/github-api-content-type-authority.test.ts +++ b/test/github-api-content-type-authority.test.ts @@ -195,4 +195,27 @@ describe("GitHub API JSON media-type authority", () => { message: "GitHub API returned malformed JSON", }); }); + + it("rejects duplicate decoded installation-token keys instead of applying last-key-wins authority", async () => { + const { token, jwk } = await signedOidcToken(); + const expiresAt = new Date(Date.now() + 60 * 60_000).toISOString(); + const response = await exchangeWithInstallationTokenResponse( + token, + jwk, + new Response( + `{"token":"ghs_first","t\\u006fken":"ghs_second","expires_at":"${expiresAt}"}`, + { + status: 201, + headers: { "content-type": "application/json" }, + }, + ), + ); + + 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 3a5c3145a963f04a12e629b03aeb6c6cbfe62d57 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:12:33 -0700 Subject: [PATCH 361/564] fix(json): reject duplicate external authority keys --- src/index.ts | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/index.ts b/src/index.ts index 6c0dfecda..bd58e4872 100644 --- a/src/index.ts +++ b/src/index.ts @@ -386,9 +386,69 @@ function decodeJson(segment: string): T { return JSON.parse(decoded) as T; } +function hasDuplicateTopLevelJsonKeys(text: string): boolean { + const keys = new Set(); + let structureDepth = 0; + let stringStart = -1; + let inString = false; + let escaped = false; + + for (let index = 0; index < text.length; index += 1) { + const character = text[index]; + if (inString) { + if (escaped) { + escaped = false; + continue; + } + if (character === "\\") { + escaped = true; + continue; + } + if (character !== '"') continue; + + inString = false; + if (structureDepth !== 1) continue; + + let lookahead = index + 1; + while (lookahead < text.length && /[ \t\r\n]/.test(text[lookahead])) lookahead += 1; + if (text[lookahead] !== ":") continue; + + const encodedKey = text.slice(stringStart + 1, index); + let decodedKey: unknown; + try { + decodedKey = JSON.parse(`"${encodedKey}"`); + } catch { + continue; + } + if (typeof decodedKey !== "string") continue; + if (keys.has(decodedKey)) return true; + keys.add(decodedKey); + continue; + } + + if (character === '"') { + inString = true; + stringStart = index; + continue; + } + if (character === "{" || character === "[") { + structureDepth += 1; + continue; + } + if (character === "}" || character === "]") { + structureDepth -= 1; + } + } + + return false; +} + async function parseExactUtf8JsonResponse(response: Response): Promise { const bytes = new Uint8Array(await response.arrayBuffer()); const decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes); + if (hasDuplicateTopLevelJsonKeys(decoded)) { + throw new SyntaxError("JSON response contains duplicate top-level keys"); + } return JSON.parse(decoded) as unknown; } From 9303644bc9eab7d88c4cbbbf8e331ca44e464685 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:19:28 -0700 Subject: [PATCH 362/564] test(oidc): reject duplicate nested JWK authority keys --- test/oidc-jwks-key-shape.test.ts | 42 ++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/oidc-jwks-key-shape.test.ts b/test/oidc-jwks-key-shape.test.ts index a4219257c..21adeb467 100644 --- a/test/oidc-jwks-key-shape.test.ts +++ b/test/oidc-jwks-key-shape.test.ts @@ -196,4 +196,46 @@ describe("OIDC JWKS key shape", () => { message: "GitHub OIDC JWKS assigned an ambiguous signing key id", }); }); + + it("rejects duplicate decoded fields inside a JWK instead of applying last-key-wins authority", async () => { + const token = await signedJwt(); + const baseKey = JSON.stringify({ ...signingPublicJwk, kid: signingKid, kty: "EC" }); + const duplicateKtyKey = `${baseKey.slice(0, -1)},"kt\\u0079":"RSA"}`; + vi.resetModules(); + const { default: worker } = await import("../src/index"); + 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 new Response(`{"keys":[${duplicateKtyKey}]}`, { + headers: { "content-type": "application/json" }, + }); + } + return new Response("unexpected privileged 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.238", + }, + body: JSON.stringify({ target_repository: { owner: "ContextualWisdomLab" } }), + }), + env, + ); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_OIDC_VERIFICATION", + message: "GitHub OIDC JWKS was not valid JSON", + }); + }); }); From c305e4c467213ad32a65bd2acd89ecbb9a524f9d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:21:52 -0700 Subject: [PATCH 363/564] fix(json): reject nested duplicate authority keys --- src/index.ts | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/index.ts b/src/index.ts index bd58e4872..86161b18e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -386,9 +386,8 @@ function decodeJson(segment: string): T { return JSON.parse(decoded) as T; } -function hasDuplicateTopLevelJsonKeys(text: string): boolean { - const keys = new Set(); - let structureDepth = 0; +function hasDuplicateJsonObjectKeys(text: string): boolean { + const objectKeyStack: Array | null> = []; let stringStart = -1; let inString = false; let escaped = false; @@ -407,20 +406,15 @@ function hasDuplicateTopLevelJsonKeys(text: string): boolean { if (character !== '"') continue; inString = false; - if (structureDepth !== 1) continue; + const keys = objectKeyStack.at(-1); + if (!keys) continue; let lookahead = index + 1; while (lookahead < text.length && /[ \t\r\n]/.test(text[lookahead])) lookahead += 1; if (text[lookahead] !== ":") continue; const encodedKey = text.slice(stringStart + 1, index); - let decodedKey: unknown; - try { - decodedKey = JSON.parse(`"${encodedKey}"`); - } catch { - continue; - } - if (typeof decodedKey !== "string") continue; + const decodedKey = JSON.parse(`"${encodedKey}"`) as string; if (keys.has(decodedKey)) return true; keys.add(decodedKey); continue; @@ -431,12 +425,16 @@ function hasDuplicateTopLevelJsonKeys(text: string): boolean { stringStart = index; continue; } - if (character === "{" || character === "[") { - structureDepth += 1; + if (character === "{") { + objectKeyStack.push(new Set()); + continue; + } + if (character === "[") { + objectKeyStack.push(null); continue; } if (character === "}" || character === "]") { - structureDepth -= 1; + objectKeyStack.pop(); } } @@ -446,8 +444,8 @@ function hasDuplicateTopLevelJsonKeys(text: string): boolean { async function parseExactUtf8JsonResponse(response: Response): Promise { const bytes = new Uint8Array(await response.arrayBuffer()); const decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes); - if (hasDuplicateTopLevelJsonKeys(decoded)) { - throw new SyntaxError("JSON response contains duplicate top-level keys"); + if (hasDuplicateJsonObjectKeys(decoded)) { + throw new SyntaxError("JSON response contains duplicate object keys"); } return JSON.parse(decoded) as unknown; } From 07c1994235d8a731a23f3c8151d7483dff479621 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:32:57 -0700 Subject: [PATCH 364/564] test(json): cover whitespace before authority separators --- test/oidc-jwks-key-shape.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/oidc-jwks-key-shape.test.ts b/test/oidc-jwks-key-shape.test.ts index 21adeb467..05ac5577c 100644 --- a/test/oidc-jwks-key-shape.test.ts +++ b/test/oidc-jwks-key-shape.test.ts @@ -200,7 +200,7 @@ describe("OIDC JWKS key shape", () => { it("rejects duplicate decoded fields inside a JWK instead of applying last-key-wins authority", async () => { const token = await signedJwt(); const baseKey = JSON.stringify({ ...signingPublicJwk, kid: signingKid, kty: "EC" }); - const duplicateKtyKey = `${baseKey.slice(0, -1)},"kt\\u0079":"RSA"}`; + const duplicateKtyKey = `${baseKey.slice(0, -1)},"kt\\u0079" \t : "RSA"}`; vi.resetModules(); const { default: worker } = await import("../src/index"); vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { From c7d2e63934ae80daaf601731d1ebb363385af020 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:38:36 -0700 Subject: [PATCH 365/564] test(json): bound streamed external authority responses --- test/github-api-malformed-json.test.ts | 45 +++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/test/github-api-malformed-json.test.ts b/test/github-api-malformed-json.test.ts index 73d685479..77bd1ce31 100644 --- a/test/github-api-malformed-json.test.ts +++ b/test/github-api-malformed-json.test.ts @@ -172,6 +172,49 @@ describe("GitHub API success-response parsing", () => { }); }); + it("cancels an oversized streamed installation-token authority body before full materialization", async () => { + let pulls = 0; + let cancelled = false; + const chunk = new Uint8Array(32_768).fill(0x20); + const streamedBody = new ReadableStream({ + pull(controller) { + pulls += 1; + if (pulls <= 8) { + controller.enqueue(chunk); + return; + } + controller.close(); + }, + cancel() { + cancelled = true; + }, + }); + + const response = await exchangeWith( + "ContextualWisdomLab/oversized-streamed-token-json", + { ...baseEnv, GITHUB_APP_INSTALLATION_ID: "92345" }, + (url) => { + if (url === "https://api.github.com/app/installations/92345/access_tokens") { + return new Response(streamedBody, { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + return new Response("unexpected GitHub request", { status: 500 }); + }, + "203.0.113.239", + ); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_API", + message: "GitHub API returned malformed JSON", + }); + expect(cancelled).toBe(true); + expect(pulls).toBeLessThan(8); + }); + it.each([ ["null", "203.0.113.242"], ["[]", "203.0.113.243"], @@ -316,4 +359,4 @@ describe("GitHub API success-response parsing", () => { message: "GitHub API returned implausible installation-token expiry", }); }); -}); \ No newline at end of file +}); From 39af6c4e575ca82a8a7d5ec1dccd428287dea995 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:45:19 -0700 Subject: [PATCH 366/564] fix(json): bound external authority response bodies --- src/index.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 86161b18e..1ade0249d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -155,6 +155,7 @@ const expectedRepositoryIds = new Map([ ]); const maxTrustedHeaderLength = 128; const maxInstallationTokenLifetimeMs = 65 * 60_000; +const maxExternalJsonResponseBytes = 65_536; function jsonResponse(body: StandardErrorResponse | StandardSuccessResponse, status = 200): Response { return new Response(JSON.stringify(body), { @@ -441,8 +442,32 @@ function hasDuplicateJsonObjectKeys(text: string): boolean { return false; } +async function readBoundedExternalJsonResponse(response: Response): Promise> { + if (!response.body) return new Uint8Array(); + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let totalBytes = 0; + while (true) { + const result = await reader.read(); + if (result.done) break; + totalBytes += result.value.byteLength; + if (totalBytes > maxExternalJsonResponseBytes) { + await reader.cancel(); + throw new SyntaxError("JSON response exceeded byte limit"); + } + chunks.push(result.value); + } + const bytes = new Uint8Array(totalBytes); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + return bytes; +} + async function parseExactUtf8JsonResponse(response: Response): Promise { - const bytes = new Uint8Array(await response.arrayBuffer()); + const bytes = await readBoundedExternalJsonResponse(response); const decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes); if (hasDuplicateJsonObjectKeys(decoded)) { throw new SyntaxError("JSON response contains duplicate object keys"); From 3488d9114e73af9e71f43a74bf1150d822e5b329 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:48:33 -0700 Subject: [PATCH 367/564] test(json): cover empty external authority body --- test/github-api-malformed-json.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/github-api-malformed-json.test.ts b/test/github-api-malformed-json.test.ts index 77bd1ce31..40cd01d26 100644 --- a/test/github-api-malformed-json.test.ts +++ b/test/github-api-malformed-json.test.ts @@ -172,6 +172,30 @@ describe("GitHub API success-response parsing", () => { }); }); + it("classifies a bodyless successful installation-token response as malformed JSON", async () => { + const response = await exchangeWith( + "ContextualWisdomLab/bodyless-token-json", + { ...baseEnv, GITHUB_APP_INSTALLATION_ID: "92345" }, + (url) => { + if (url === "https://api.github.com/app/installations/92345/access_tokens") { + return new Response(null, { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + return new Response("unexpected GitHub request", { status: 500 }); + }, + "203.0.113.237", + ); + + 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("cancels an oversized streamed installation-token authority body before full materialization", async () => { let pulls = 0; let cancelled = false; From bd19ad0552483e41fb6b946c3efab2c2b8123110 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 00:09:35 -0700 Subject: [PATCH 368/564] 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 c8aa386c2..08971b175 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 = - "17052a7ca3c16db90932a4d6036b43165ddee418"; + "f6c2a2702b7b7578b2d1fc5f2f9a5125a0390d33"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 02ff991ada115d0ac6dba731c3d4b266f3ddda33 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 00:10:38 -0700 Subject: [PATCH 369/564] fix(oidc): roll trusted workflow source forward --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 748418125..d2da0ca7d 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 = "17052a7ca3c16db90932a4d6036b43165ddee418" +ALLOWED_WORKFLOW_SHA = "f6c2a2702b7b7578b2d1fc5f2f9a5125a0390d33" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 9f739215f0e6aecb18115670b98922e3f5ba3df0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 00:21:40 -0700 Subject: [PATCH 370/564] docs(architecture): bind current central workflow trust --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index fbfdaffdd..598c39157 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs readiness dispatch and delegates `/exchange`; `src/entrypoint.ts` applies the distributed rate limiter before `src/worker.ts` performs its denial-only exact workflow-ref precheck. `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the central `.github` commit `17052a7ca3c16db90932a4d6036b43165ddee418`. The audited `noema-review.yml` blob is unchanged from the predecessor trusted revision, but GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every central commit movement requires an explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the current audited protected central `.github` commit `f6c2a2702b7b7578b2d1fc5f2f9a5125a0390d33`. The `noema-review.yml` blob changed materially from the predecessor trusted revision, including central reviewer-sidecar composition, so this roll-forward is an explicit trust decision rather than a content-equivalence claim. GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every later central commit movement still requires a new explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema; known central consumer/scanner defects remain owned and repaired there rather than by weakening Noema verification. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The protected workflow-ref parser and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 9b175eeeaae1379d94d1ba61b0be8857f6fddc35 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 00:38:26 -0700 Subject: [PATCH 371/564] test(oidc): reject non-string JOSE key ids --- test/oidc-jose-kid-type.test.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/oidc-jose-kid-type.test.ts diff --git a/test/oidc-jose-kid-type.test.ts b/test/oidc-jose-kid-type.test.ts new file mode 100644 index 000000000..897d5df16 --- /dev/null +++ b/test/oidc-jose-kid-type.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { parseExactBearerToken } from "../src/bearer-authorization"; + +const payload = Buffer.from("{}", "utf8").toString("base64url"); +const signature = Buffer.from([0]).toString("base64url"); + +function tokenWithHeader(header: unknown): string { + const encodedHeader = Buffer.from(JSON.stringify(header), "utf8").toString("base64url"); + return `${encodedHeader}.${payload}.${signature}`; +} + +describe("OIDC JOSE key-id type authority", () => { + it.each([ + ["number", 1], + ["boolean", true], + ["object", { value: "kid" }], + ["array", ["kid"]], + ])("rejects a present non-string kid before downstream key discovery: %s", (_label, kid) => { + const token = tokenWithHeader({ alg: "RS256", kid }); + + expect(parseExactBearerToken(`Bearer ${token}`)).toBeUndefined(); + }); + + it("preserves a canonical string kid for cryptographic verification", () => { + const token = tokenWithHeader({ alg: "RS256", kid: "github-actions-key" }); + + expect(parseExactBearerToken(`Bearer ${token}`)).toBe(token); + }); +}); From c7b581c8305169079f5bf6235b8bf2497727a433 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 00:39:16 -0700 Subject: [PATCH 372/564] fix(oidc): reject non-string JOSE key ids --- src/bearer-authorization.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index cf381ccb6..7b38d6dc0 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -51,6 +51,17 @@ function hasUnsupportedJoseSigningSemantics(text: string): boolean { } } +function hasNonStringJoseKeyId(text: string): boolean { + try { + const parsed = JSON.parse(text) as Record; + return Object.prototype.hasOwnProperty.call(parsed, "kid") + && typeof parsed.kid !== "string"; + } catch { + // Leave syntactically malformed protected headers to the downstream malformed-token path. + return false; + } +} + function hasDuplicateTopLevelJsonKeys(text: string): boolean { const seenKeys = new Set(); let structureDepth = 0; @@ -108,11 +119,12 @@ function hasDuplicateTopLevelJsonKeys(text: string): boolean { * normalizes attacker-controlled framing. Each JWT segment must already be non-empty * canonical unpadded base64url. Protected headers and payloads must also already be valid * UTF-8 JSON objects; BOM-prefixed authority, syntactically valid non-object envelopes, - * unsupported JOSE critical/signing-input semantics, and duplicate top-level JSON member - * names after escape decoding are rejected before any claim reader can silently - * reinterpret signed bytes. Noema does not implement the RFC 7797 `b64` extension, so a - * protected `b64` member is rejected whether or not a malformed token also omits `crit`. - * Syntactically malformed JSON remains on the downstream malformed-token error boundary. + * unsupported JOSE critical/signing-input semantics, non-string JOSE key identifiers, and + * duplicate top-level JSON member names after escape decoding are rejected before any + * claim reader or remote signing-key lookup can reinterpret attacker-controlled bytes. + * Noema does not implement the RFC 7797 `b64` extension, so a protected `b64` member is + * rejected whether or not a malformed token also omits `crit`. Syntactically malformed + * JSON remains on the downstream malformed-token error boundary. * * @param authorization Raw HTTP Authorization field bytes decoded as a JavaScript string. * @returns The exact bearer credential when framing and bounds are canonical; otherwise undefined. @@ -136,8 +148,9 @@ export function parseExactBearerToken(authorization: string): string | undefined || hasNonObjectJsonShape(headerText) || hasNonObjectJsonShape(payloadText) || hasUnsupportedJoseSigningSemantics(headerText) + || hasNonStringJoseKeyId(headerText) || hasDuplicateTopLevelJsonKeys(headerText) || hasDuplicateTopLevelJsonKeys(payloadText) ) return undefined; return token; -} \ No newline at end of file +} From 3f053f57f2b9269489f0a62fe7762448fbbf4fe3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 01:03:51 -0700 Subject: [PATCH 373/564] test(oidc): reject stale issued-at authority --- test/oidc-issued-at-lifetime.test.ts | 115 +++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 test/oidc-issued-at-lifetime.test.ts diff --git a/test/oidc-issued-at-lifetime.test.ts b/test/oidc-issued-at-lifetime.test.ts new file mode 100644 index 000000000..a6bfda3b3 --- /dev/null +++ b/test/oidc-issued-at-lifetime.test.ts @@ -0,0 +1,115 @@ +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-issued-at-lifetime"; + +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"); +} + +async function signedJwt(iat: number, exp: number): Promise { + const now = Math.floor(Date.now() / 1000); + const header = encodeJson({ alg: "RS256", kid: signingKid }); + const payload = encodeJson({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + job_workflow_ref: configuredWorkflowRef, + job_workflow_sha: configuredWorkflowSha, + sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", + exp, + nbf: now - 30, + iat, + }); + const signature = new Uint8Array( + await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + signingPrivateKey, + new TextEncoder().encode(`${header}.${payload}`), + ), + ); + return `${header}.${payload}.${Buffer.from(signature).toString("base64url")}`; +} + +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("GitHub OIDC issued-at lifetime", () => { + it("rejects a still-unexpired signed token whose issued-at age exceeds the accepted one-hour lifetime", async () => { + 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 }); + }); + + const now = Math.floor(Date.now() / 1000); + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${await signedJwt(now - 3_601, now + 300)}`, + "content-type": "application/json", + "cf-connecting-ip": "203.0.113.125", + }, + body: JSON.stringify({ + target_repository: { owner: "ContextualWisdomLab", repo: "noema" }, + }), + }), + env, + ); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_AUTH_INVALID", + message: "OIDC issued-at claim is outside the accepted lifetime window", + }); + }); +}); From d6f572fe1d444234bd88e9f755e2869b05503deb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 01:09:12 -0700 Subject: [PATCH 374/564] fix(oidc): bound issued-at token lifetime --- src/index.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 1ade0249d..13dc01f3e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -154,6 +154,7 @@ const expectedRepositoryIds = new Map([ ["ContextualWisdomLab/.github", "1274066402"], ]); const maxTrustedHeaderLength = 128; +const maxOidcTokenLifetimeSeconds = 3_600; const maxInstallationTokenLifetimeMs = 65 * 60_000; const maxExternalJsonResponseBytes = 65_536; @@ -669,12 +670,22 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise if (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) { + if (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"); } + if ( + payload.iat < now - maxOidcTokenLifetimeSeconds + || payload.exp - payload.iat > maxOidcTokenLifetimeSeconds + ) { + throw new ApiError( + "ERR_AUTH_INVALID", + 401, + "OIDC issued-at claim is outside the accepted lifetime window", + ); + } if (payload.nbf > payload.exp || payload.iat > payload.exp) { throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC token temporal claims are inconsistent"); } From 0479133017bf26a9cee1d1f50f270976e6185848 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 01:12:10 -0700 Subject: [PATCH 375/564] test(oidc): require current audited central trust 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 08971b175..23b5b2635 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 = - "f6c2a2702b7b7578b2d1fc5f2f9a5125a0390d33"; + "cdde0d82602d9568128fd0c1e985ea40b2292710"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From f4f7f52aaa06ba6237bbf05270aa6f26a14c1bf7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 01:14:39 -0700 Subject: [PATCH 376/564] fix(oidc): roll trusted central workflow source forward --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index d2da0ca7d..e706370b2 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 = "f6c2a2702b7b7578b2d1fc5f2f9a5125a0390d33" +ALLOWED_WORKFLOW_SHA = "cdde0d82602d9568128fd0c1e985ea40b2292710" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From ce2618380822ecc83f53c19f03be6e7687389f3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 01:21:38 -0700 Subject: [PATCH 377/564] docs(architecture): bind current audited workflow source --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 598c39157..11e200419 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs readiness dispatch and delegates `/exchange`; `src/entrypoint.ts` applies the distributed rate limiter before `src/worker.ts` performs its denial-only exact workflow-ref precheck. `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the current audited protected central `.github` commit `f6c2a2702b7b7578b2d1fc5f2f9a5125a0390d33`. The `noema-review.yml` blob changed materially from the predecessor trusted revision, including central reviewer-sidecar composition, so this roll-forward is an explicit trust decision rather than a content-equivalence claim. GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every later central commit movement still requires a new explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema; known central consumer/scanner defects remain owned and repaired there rather than by weakening Noema verification. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the current audited protected central `.github` commit `cdde0d82602d9568128fd0c1e985ea40b2292710`. The `noema-review.yml` workflow blob is unchanged from the predecessor trusted revision `f6c2a2702b7b7578b2d1fc5f2f9a5125a0390d33`, while centrally archived SAST/contextual-orchestrator sidecar implementation inputs changed across the repository revisions. This roll-forward is therefore an explicit repository-commit trust decision rather than a claim that all trusted central inputs are content-equivalent. GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every later central commit movement still requires a new explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema; known central consumer/scanner defects remain owned and repaired there rather than by weakening Noema verification. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The protected workflow-ref parser and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From dd95e3b045e738d5c7ddcc61ecee800db6dbc54a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 02:04:46 -0700 Subject: [PATCH 378/564] test(oidc): bound JOSE key-id discovery authority --- test/oidc-jose-kid-type.test.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/test/oidc-jose-kid-type.test.ts b/test/oidc-jose-kid-type.test.ts index 897d5df16..354b935e4 100644 --- a/test/oidc-jose-kid-type.test.ts +++ b/test/oidc-jose-kid-type.test.ts @@ -21,8 +21,22 @@ describe("OIDC JOSE key-id type authority", () => { expect(parseExactBearerToken(`Bearer ${token}`)).toBeUndefined(); }); - it("preserves a canonical string kid for cryptographic verification", () => { - const token = tokenWithHeader({ alg: "RS256", kid: "github-actions-key" }); + it.each([ + ["empty", ""], + ["oversized", "k".repeat(129)], + ["control", "github\nkey"], + ["non-ascii", "github-키"], + ])("rejects a string kid outside the bounded visible-ASCII discovery authority: %s", (_label, kid) => { + const token = tokenWithHeader({ alg: "RS256", kid }); + + expect(parseExactBearerToken(`Bearer ${token}`)).toBeUndefined(); + }); + + it.each([ + "cc413527-173f-5a05-976e-9c52b1d7b431", + "38E9B30B3A023A1B72309921A69A42FCC496C42C", + ])("preserves a current GitHub-compatible canonical string kid for cryptographic verification: %s", (kid) => { + const token = tokenWithHeader({ alg: "RS256", kid }); expect(parseExactBearerToken(`Bearer ${token}`)).toBe(token); }); From 067609a60c99968b610c83f941c6101c353d3b75 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 02:05:54 -0700 Subject: [PATCH 379/564] fix(oidc): bound JOSE key-id discovery selector --- src/bearer-authorization.ts | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/bearer-authorization.ts b/src/bearer-authorization.ts index 7b38d6dc0..694f2755a 100644 --- a/src/bearer-authorization.ts +++ b/src/bearer-authorization.ts @@ -1,5 +1,6 @@ const maximumAuthorizationFieldLength = 16_384; const canonicalBearerAuthorizationPattern = /^Bearer ([\x21-\x7e]+)$/i; +const canonicalJoseKeyIdPattern = /^[\x21-\x7e]{1,128}$/; const utf8BomBase64UrlPrefix = "77u_"; function decodeCanonicalBase64Url(segment: string): Uint8Array | undefined { @@ -51,11 +52,12 @@ function hasUnsupportedJoseSigningSemantics(text: string): boolean { } } -function hasNonStringJoseKeyId(text: string): boolean { +function hasUnsafeJoseKeyId(text: string): boolean { try { const parsed = JSON.parse(text) as Record; - return Object.prototype.hasOwnProperty.call(parsed, "kid") - && typeof parsed.kid !== "string"; + if (!Object.prototype.hasOwnProperty.call(parsed, "kid")) return false; + return typeof parsed.kid !== "string" + || !canonicalJoseKeyIdPattern.test(parsed.kid); } catch { // Leave syntactically malformed protected headers to the downstream malformed-token path. return false; @@ -119,12 +121,15 @@ function hasDuplicateTopLevelJsonKeys(text: string): boolean { * normalizes attacker-controlled framing. Each JWT segment must already be non-empty * canonical unpadded base64url. Protected headers and payloads must also already be valid * UTF-8 JSON objects; BOM-prefixed authority, syntactically valid non-object envelopes, - * unsupported JOSE critical/signing-input semantics, non-string JOSE key identifiers, and - * duplicate top-level JSON member names after escape decoding are rejected before any - * claim reader or remote signing-key lookup can reinterpret attacker-controlled bytes. - * Noema does not implement the RFC 7797 `b64` extension, so a protected `b64` member is - * rejected whether or not a malformed token also omits `crit`. Syntactically malformed - * JSON remains on the downstream malformed-token error boundary. + * unsupported JOSE critical/signing-input semantics, unbounded/non-visible/non-string JOSE + * key identifiers, and duplicate top-level JSON member names after escape decoding are + * rejected before any claim reader or remote signing-key lookup can reinterpret + * attacker-controlled bytes. The accepted `kid` boundary is 1-128 visible ASCII characters, + * which contains GitHub's current UUID and hexadecimal signing-key identifiers without + * allowing attacker-controlled control/Unicode/oversized discovery selectors. Noema does + * not implement the RFC 7797 `b64` extension, so a protected `b64` member is rejected + * whether or not a malformed token also omits `crit`. Syntactically malformed JSON remains + * on the downstream malformed-token error boundary. * * @param authorization Raw HTTP Authorization field bytes decoded as a JavaScript string. * @returns The exact bearer credential when framing and bounds are canonical; otherwise undefined. @@ -148,7 +153,7 @@ export function parseExactBearerToken(authorization: string): string | undefined || hasNonObjectJsonShape(headerText) || hasNonObjectJsonShape(payloadText) || hasUnsupportedJoseSigningSemantics(headerText) - || hasNonStringJoseKeyId(headerText) + || hasUnsafeJoseKeyId(headerText) || hasDuplicateTopLevelJsonKeys(headerText) || hasDuplicateTopLevelJsonKeys(payloadText) ) return undefined; From c0354712393936df91260128b22de139a9b59a05 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 03:03:54 -0700 Subject: [PATCH 380/564] test(ci): require reusable patch-validator build cache --- .../patch-validator-image-build-cache.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 test/patch-validator-image-build-cache.test.ts diff --git a/test/patch-validator-image-build-cache.test.ts b/test/patch-validator-image-build-cache.test.ts new file mode 100644 index 000000000..705a89508 --- /dev/null +++ b/test/patch-validator-image-build-cache.test.ts @@ -0,0 +1,24 @@ +import { readFileSync } from "node:fs"; + +import { describe, expect, it } from "vitest"; + +const workflow = readFileSync( + ".github/workflows/patch-validator-image.yml", + "utf8", +); + +describe("patch-validator image build cache", () => { + it("reuses content-addressed BuildKit layers across successive exact PR heads", () => { + expect(workflow).toContain("docker buildx build"); + expect(workflow).toContain("--load"); + expect(workflow).toContain( + "--cache-from=type=gha,scope=noema-patch-validator-image", + ); + expect(workflow).toContain( + "--cache-to=type=gha,mode=max,scope=noema-patch-validator-image", + ); + expect(workflow).not.toContain( + "timeout --signal=TERM --kill-after=30s 150m docker build \\", + ); + }); +}); From fdf4bd423a38cd206834f269cac0ea1773071af6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 03:08:06 -0700 Subject: [PATCH 381/564] fix(ci): cache exact-head patch-validator builds --- .github/workflows/patch-validator-image.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/patch-validator-image.yml b/.github/workflows/patch-validator-image.yml index 640fc32b7..1d5792f50 100644 --- a/.github/workflows/patch-validator-image.yml +++ b/.github/workflows/patch-validator-image.yml @@ -108,11 +108,17 @@ jobs: "$scanner_dir/grype" version | grep -Fq "$GRYPE_VERSION" printf 'SCANNER_BIN_DIR=%s\n' "$scanner_dir" >>"$GITHUB_ENV" + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + - name: Build exact-head patch-validator image shell: bash run: | set -euo pipefail - timeout --signal=TERM --kill-after=30s 150m docker build \ + timeout --signal=TERM --kill-after=30s 150m docker buildx build \ + --load \ + --cache-from=type=gha,scope=noema-patch-validator-image \ + --cache-to=type=gha,mode=max,scope=noema-patch-validator-image \ --platform=linux/amd64 \ --file=Dockerfile.patch-validator \ --build-arg=SOURCE_REVISION=${SOURCE_SHA} \ From b20ca3d4700d5048a66a7f764dd2d3a55457dfdc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 03:35:25 -0700 Subject: [PATCH 382/564] test(readiness): fail closed without credential fetch capability --- ...untime-readiness-egress-capability.test.ts | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 test/runtime-readiness-egress-capability.test.ts diff --git a/test/runtime-readiness-egress-capability.test.ts b/test/runtime-readiness-egress-capability.test.ts new file mode 100644 index 000000000..b96383032 --- /dev/null +++ b/test/runtime-readiness-egress-capability.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it, vi } from "vitest"; + +import entrypoint, { type Env } from "../src/runtime-entrypoint"; + +function dummyNamespace(): DurableObjectNamespace { + return { + idFromName(name: string) { + return { toString: () => name } as DurableObjectId; + }, + get() { + return { + fetch: async () => new Response("unused", { status: 500 }), + } as unknown as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; +} + +async function privateKeyPem(): Promise { + const pair = await crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + ); + const pkcs8 = await crypto.subtle.exportKey("pkcs8", pair.privateKey); + 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 readyEnv(): Promise { + 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", + GITHUB_APP_PRIVATE_KEY_PEM: await privateKeyPem(), + GITHUB_APP_INSTALLATION_ID: "987654", + NOEMA_RATE_LIMIT_PER_MINUTE: "60", + NOEMA_RATE_LIMITER: dummyNamespace(), + NOEMA_OIDC_REPLAY_GUARD: dummyNamespace(), + }; +} + +describe("Noema runtime readiness credential-egress capability", () => { + it("fails closed when the runtime cannot provide the fetch capability required by credential exchange", async () => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + const originalDescriptor = Object.getOwnPropertyDescriptor(globalThis, "fetch"); + Object.defineProperty(globalThis, "fetch", { + configurable: true, + writable: true, + value: undefined, + }); + + try { + const response = await entrypoint.fetch( + new Request("https://noema.example/ready"), + await readyEnv(), + ); + + expect(response.status).toBe(503); + expect(response.headers.get("x-noema-readiness")).toBe("not-ready"); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_SERVICE_NOT_READY", + details: { + failed_checks: "credential_fetch_capability", + }, + }); + } finally { + if (originalDescriptor) { + Object.defineProperty(globalThis, "fetch", originalDescriptor); + } else { + Reflect.deleteProperty(globalThis, "fetch"); + } + vi.restoreAllMocks(); + } + }); +}); From cdc9d8363331882a8b8727b1fec075c656144c87 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 03:38:50 -0700 Subject: [PATCH 383/564] fix(readiness): require credential fetch capability --- src/runtime-entrypoint.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index ffc188b60..b4f75318e 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -91,13 +91,18 @@ async function runtimeReadinessResponse(request: Request, env: Env): Promise Date: Fri, 28 Aug 2026 03:58:34 -0700 Subject: [PATCH 384/564] test(readiness): reject non-installable fetch capability --- ...untime-readiness-egress-capability.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test/runtime-readiness-egress-capability.test.ts b/test/runtime-readiness-egress-capability.test.ts index b96383032..961f4602e 100644 --- a/test/runtime-readiness-egress-capability.test.ts +++ b/test/runtime-readiness-egress-capability.test.ts @@ -85,4 +85,39 @@ describe("Noema runtime readiness credential-egress capability", () => { vi.restoreAllMocks(); } }); + + it("fails closed when fetch is callable but the runtime cannot install the credential-egress wrapper", async () => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + const originalDescriptor = Object.getOwnPropertyDescriptor(globalThis, "fetch"); + const currentFetch = globalThis.fetch; + Object.defineProperty(globalThis, "fetch", { + configurable: true, + writable: false, + value: currentFetch, + }); + + try { + const response = await entrypoint.fetch( + new Request("https://noema.example/ready"), + await readyEnv(), + ); + + expect(response.status).toBe(503); + expect(response.headers.get("x-noema-readiness")).toBe("not-ready"); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_SERVICE_NOT_READY", + details: { + failed_checks: "credential_fetch_capability", + }, + }); + } finally { + if (originalDescriptor) { + Object.defineProperty(globalThis, "fetch", originalDescriptor); + } else { + Reflect.deleteProperty(globalThis, "fetch"); + } + vi.restoreAllMocks(); + } + }); }); From 740e12671adf87c2d0d5ab942cc283ec7b1cf912 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 04:00:18 -0700 Subject: [PATCH 385/564] fix(readiness): require replaceable credential fetch capability --- src/runtime-entrypoint.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index b4f75318e..7fff820f2 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -51,6 +51,14 @@ function traceIdFromRequest(request: Request): string { return crypto.randomUUID(); } +function credentialFetchCapabilityAvailable(): boolean { + const descriptor = Object.getOwnPropertyDescriptor(globalThis, "fetch"); + return descriptor !== undefined + && "value" in descriptor + && descriptor.writable === true + && typeof descriptor.value === "function"; +} + function readinessHeaders( traceId: string, latencyMs: number, @@ -91,7 +99,7 @@ async function runtimeReadinessResponse(request: Request, env: Env): Promise Date: Fri, 28 Aug 2026 04:32:09 -0700 Subject: [PATCH 386/564] test(exchange): reject overlong GitHub repository authority --- test/target-repository-name-boundary.test.ts | 125 +++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 test/target-repository-name-boundary.test.ts diff --git a/test/target-repository-name-boundary.test.ts b/test/target-repository-name-boundary.test.ts new file mode 100644 index 000000000..e301bb805 --- /dev/null +++ b/test/target-repository-name-boundary.test.ts @@ -0,0 +1,125 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import worker, { type Env } from "../src/index"; + +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 signedCentralWorkflowToken(env: Env) { + 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"], + ); + const kid = `target-repository-boundary-${crypto.randomUUID()}`; + const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); + const now = Math.floor(Date.now() / 1000); + const payload = encodeSegment({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + job_workflow_ref: env.ALLOWED_WORKFLOW_REF_PREFIX, + job_workflow_sha: env.ALLOWED_WORKFLOW_SHA, + sub: "repo:ContextualWisdomLab/.github:ref:refs/heads/main", + jti: crypto.randomUUID(), + exp: now + 300, + nbf: now - 30, + iat: now - 30, + }); + const signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + keyPair.privateKey, + new TextEncoder().encode(`${header}.${payload}`), + ); + const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey); + return { + token: `${header}.${payload}.${encodeBytes(signature)}`, + jwk: { ...publicJwk, kid, kty: "RSA" }, + }; +} + +describe("target_repository GitHub name authority", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("rejects a repository name longer than GitHub's 100-character limit before GitHub API lookup", async () => { + const appKeyPair = await crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + ); + 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: + "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: pemFromPkcs8( + await crypto.subtle.exportKey("pkcs8", appKeyPair.privateKey), + ), + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", + }; + const { token, jwk } = await signedCentralWorkflowToken(env); + const outbound: string[] = []; + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + outbound.push(url); + 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 new Response("not found", { status: 404 }); + }); + + const response = await worker.fetch(new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json", + }, + body: JSON.stringify({ + target_repository: `ContextualWisdomLab/${"r".repeat(101)}`, + }), + }), env); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_VALIDATION_INPUT", + }); + expect(outbound.some((url) => url.startsWith("https://api.github.com/"))).toBe(false); + }); +}); From 0cde5cb63a08998e5edc0919fb6201e98234e3ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 04:38:06 -0700 Subject: [PATCH 387/564] fix(exchange): bound GitHub repository name authority --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 13dc01f3e..bb3dd8fcc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -701,7 +701,7 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise } function validateRepositoryName(repository: string, env: Env): string { - if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) { + if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]{1,100}$/.test(repository)) { throw new ApiError("ERR_VALIDATION_INPUT", 400, "target_repository is not a valid owner/name repository"); } const [owner, name] = repository.split("/", 2); From a20ded2f11b96bf469d26290355dcf62a25bbb94 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 04:41:00 -0700 Subject: [PATCH 388/564] test(openapi): bound repository locator name segment --- test/openapi-contract.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/openapi-contract.test.ts b/test/openapi-contract.test.ts index 3d2404eae..f3efbada3 100644 --- a/test/openapi-contract.test.ts +++ b/test/openapi-contract.test.ts @@ -93,7 +93,12 @@ describe("machine-readable public HTTP contract", () => { expect(requestSchema.properties.target_repository).toEqual({ $ref: "#/components/schemas/RepositoryLocator" }); expect(successRepository).toEqual(locator); - const accepted = ["ContextualWisdomLab/.github", "ContextualWisdomLab/noema", "ContextualWisdomLab/a"]; + const accepted = [ + "ContextualWisdomLab/.github", + "ContextualWisdomLab/noema", + "ContextualWisdomLab/a", + `ContextualWisdomLab/${"r".repeat(100)}`, + ]; const rejected = [ "ContextualWisdomLab/..", "ContextualWisdomLab/.", @@ -105,6 +110,7 @@ describe("machine-readable public HTTP contract", () => { "ContextualWisdomLab\\noema", "ContextualWisdomLab/\u2024\u2024", "ContextualWisdomLab/\uFF0E\uFF0E", + `ContextualWisdomLab/${"r".repeat(101)}`, ]; for (const value of accepted) { From 39e4dede11a80355c9b807f3ac54caaca3554388 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 04:45:43 -0700 Subject: [PATCH 389/564] fix(openapi): bound repository locator name segment --- openapi.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi.json b/openapi.json index dd12959c3..c57c5844f 100644 --- a/openapi.json +++ b/openapi.json @@ -252,9 +252,9 @@ }, "RepositoryLocator": { "type": "string", - "description": "Authorized owner/repository locator. Each path segment must match [A-Za-z0-9_.-]+ and must not be exactly `.` or `..`. Names such as `.github` remain valid. Constraints use RE2-safe patterns plus JSON Schema not/allOf so buyer tooling that cannot compile lookaheads still rejects traversal segments.", + "description": "Authorized owner/repository locator. The repository-name segment is limited to 1–100 characters; each segment uses [A-Za-z0-9_.-] and must not be exactly `.` or `..`. Names such as `.github` remain valid. Constraints use RE2-safe patterns plus JSON Schema not/allOf so buyer tooling that cannot compile lookaheads still rejects traversal segments.", "allOf": [ - { "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" }, + { "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]{1,100}$" }, { "not": { "pattern": "^\\.{1,2}/" } }, { "not": { "pattern": "/\\.{1,2}$" } } ] From 0da20e33d49927d4c247d09503e4d19b56a83ab4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 04:47:26 -0700 Subject: [PATCH 390/564] docs(api): document repository-name authority bound --- docs/api-spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-spec.md b/docs/api-spec.md index 21db467c9..c56c8d8f9 100644 --- a/docs/api-spec.md +++ b/docs/api-spec.md @@ -63,7 +63,7 @@ `application/json` 요청 body는 UTF-8 wire bytes 기준 최대 **8,192 bytes**다. `Content-Length`가 이 한도를 초과하면 body를 읽지 않고 413으로 거부하며, 길이 헤더가 없거나 신뢰할 수 없는 경우에도 stream을 최대 한도까지만 읽어 chunked 전송 우회를 차단한다. 이 검사는 OIDC/JWKS 조회, GitHub App private-key 사용, GitHub API 호출 전에 수행된다. -`target_repository`가 포함되면 문자열이어야 하며, `owner/repository` 형식과 허용된 organization owner를 만족해야 한다. owner 또는 name 세그먼트가 정확히 `.` 또는 `..`이면 GitHub App private-key 사용과 token 발급 전에 `400 ERR_VALIDATION_INPUT`으로 거부된다. `.github`처럼 점이 포함된 실제 저장소 이름은 허용한다. 객체/배열/null 등 문자열이 아닌 값은 GitHub token 생성 전에 `ERR_VALIDATION_INPUT`으로 거부된다. 호출자는 `ContextualWisdomLab/`만 보내고, 경로 순회 세그먼트·퍼센트 인코딩된 점·추가 슬래시·백슬래시는 보내지 않는다. 공개 OpenAPI `RepositoryLocator`는 lookahead 없이 RE2-안전한 `allOf`/`not` 패턴으로 같은 규칙을 실행하므로, 구매자 도구가 패턴을 컴파일한 뒤 `ContextualWisdomLab/.github`만 보내고 `owner/..`는 보내지 않으면 된다. 설계 근거와 APA 7th 참고문헌은 [`docs/doctoring/repository-path-segment-validation.md`](doctoring/repository-path-segment-validation.md)에 있다. +`target_repository`가 포함되면 문자열이어야 하며, `owner/repository` 형식과 허용된 organization owner를 만족해야 한다. repository-name 세그먼트는 GitHub의 저장소 이름 경계에 맞춰 **1–100자**의 ASCII 영문자·숫자·`.`, `-`, `_`만 허용한다. owner 또는 name 세그먼트가 정확히 `.` 또는 `..`이면 GitHub App private-key 사용과 token 발급 전에 `400 ERR_VALIDATION_INPUT`으로 거부된다. `.github`처럼 점이 포함된 실제 저장소 이름은 허용한다. 객체/배열/null 등 문자열이 아닌 값은 GitHub token 생성 전에 `ERR_VALIDATION_INPUT`으로 거부된다. 호출자는 `ContextualWisdomLab/`만 보내고, 100자를 초과하는 repository name·경로 순회 세그먼트·퍼센트 인코딩된 점·추가 슬래시·백슬래시는 보내지 않는다. 공개 OpenAPI `RepositoryLocator`는 lookahead 없이 RE2-안전한 `allOf`/`not` 패턴으로 같은 길이·문자집합·dot-segment 규칙을 실행하므로, 구매자 도구가 패턴을 컴파일한 뒤 `ContextualWisdomLab/.github`만 보내고 `owner/..` 또는 101자 name은 보내지 않으면 된다. 설계 근거와 APA 7th 참고문헌은 [`docs/doctoring/repository-path-segment-validation.md`](doctoring/repository-path-segment-validation.md)에 있다. OIDC workflow trust는 전체 ref 문자열의 exact match 정책을 사용한다. - `job_workflow_ref`가 있으면 이를 우선하고, 없으면 `workflow_ref`를 사용한다. From ed63294089b338d6a13bda064cb3d480fc8286d7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 04:48:02 -0700 Subject: [PATCH 391/564] docs(security): align repository-name validation contract --- .../repository-path-segment-validation.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/doctoring/repository-path-segment-validation.md b/docs/doctoring/repository-path-segment-validation.md index 68e65c6ca..60f6d80e5 100644 --- a/docs/doctoring/repository-path-segment-validation.md +++ b/docs/doctoring/repository-path-segment-validation.md @@ -4,15 +4,15 @@ `/exchange` accepts an optional `target_repository` string in `owner/name` form. The Worker validates that locator first. Only a surviving `owner/name` may cause a GitHub App private key import. After the key is imported, Noema interpolates the same string into GitHub REST paths such as `/repos/${repository}/installation`. A caller who sends `ContextualWisdomLab/..` or `../noema` would otherwise produce a URL whose `.` / `..` segments are removed during generic URI resolution and no longer name the intended repository. -The Worker therefore rejects a repository string when either path segment is exactly `.` or `..`, and it does so as `400 ERR_VALIDATION_INPUT` before GitHub App credential work. Names that merely contain a dot, including the real `.github` repository, remain valid. A syntactically valid owner that is not the configured organization still returns `403 ERR_REPO_NOT_ALLOWED`. +The Worker therefore rejects a repository string when either path segment is exactly `.` or `..`, and it does so as `400 ERR_VALIDATION_INPUT` before GitHub App credential work. The repository-name segment is additionally bounded to **1–100 ASCII characters** from `[A-Za-z0-9_.-]`, matching GitHub's repository-name limit instead of allowing an impossible locator to acquire GitHub API authority. Names that merely contain a dot, including the real `.github` repository, remain valid. A syntactically valid owner that is not the configured organization still returns `403 ERR_REPO_NOT_ALLOWED`. -Callers must send `ContextualWisdomLab/` only. Do not send path-traversal segments, percent-encoded dots, or extra slashes. +Callers must send `ContextualWisdomLab/` only. Do not send repository names longer than 100 characters, path-traversal segments, percent-encoded dots, or extra slashes. ## Why this is fail-closed at the public contract -RFC 3986 defines `.` and `..` as dot segments that a resolver removes while normalizing a path. GitHub's REST path `/repos/{owner}/{repo}/installation` is a URI path, not an opaque token. If Noema forwarded `ContextualWisdomLab/..`, a later URL parser or reverse proxy could resolve it to `/repos/installation` and perform privileged work against the wrong resource. +RFC 3986 defines `.` and `..` as dot segments that a resolver removes while normalizing a path. GitHub's REST path `/repos/{owner}/{repo}/installation` is a URI path, not an opaque token. If Noema forwarded `ContextualWisdomLab/..`, a later URL parser or reverse proxy could resolve it to `/repos/installation` and perform privileged work against the wrong resource. Likewise, forwarding a repository name GitHub cannot represent defers validation until after OIDC verification, GitHub App key use, and GitHub API work. The public boundary should reject that impossible authority before privileged egress. -The published OpenAPI schema and `docs/api-spec.md` must describe the same rule. A schema that accepts `owner/..` tells an integrator the request is valid while the Worker rejects it, which is a buyer-facing contract gap. The schema therefore uses RE2-safe `allOf` / `not` patterns instead of lookaheads so buyer tooling that compiles OpenAPI `pattern` with RE2 still rejects traversal segments. +The published OpenAPI schema and `docs/api-spec.md` must describe the same rule. A schema that accepts `owner/..` or a 101-character repository name tells an integrator the request is valid while the Worker rejects it, which is a buyer-facing contract gap. The schema therefore uses a RE2-safe bounded repository-name pattern plus `allOf` / `not` patterns instead of lookaheads so buyer tooling that compiles OpenAPI `pattern` with RE2 still enforces the same length, character-set, and traversal constraints. ```mermaid sequenceDiagram @@ -24,7 +24,9 @@ sequenceDiagram Caller->>Exchange: target_repository Exchange->>Validate: owner/name - alt segment is . or .. + alt name exceeds 100 characters + Validate-->>Caller: 400 ERR_VALIDATION_INPUT + else segment is . or .. Validate-->>Caller: 400 ERR_VALIDATION_INPUT else charset or slash rejected Validate-->>Caller: 400 ERR_VALIDATION_INPUT @@ -36,7 +38,7 @@ sequenceDiagram end ``` -The outbound fetch policy already refuses a repository name that is only `.` or `..` on the installation-token body. Request validation repeats that rule on both owner and name so the private key is never imported for a traversal string, including when tests or a future caller invoke the base Worker without the production fetch wrapper. +The outbound fetch policy already refuses a repository name that is only `.` or `..` on the installation-token body. Request validation repeats that rule on both owner and name and enforces the GitHub name-length ceiling so the private key is never imported for a traversal string or impossible repository locator, including when tests or a future caller invoke the base Worker without the production fetch wrapper. ## Verification contract @@ -45,9 +47,10 @@ Tests must prove: - `ContextualWisdomLab/..` and `ContextualWisdomLab/.` return `400 ERR_VALIDATION_INPUT` with zero `api.github.com` egress; - `../noema` and `./noema` return the same `400` rather than a later owner-allowlist `403`; - `ContextualWisdomLab/.github` remains a legal name and reaches GitHub App private-key import; +- a 100-character repository name remains valid while a 101-character name returns `400 ERR_VALIDATION_INPUT` before any `api.github.com` egress; - a foreign owner such as `OtherWisdomLab/noema` still returns `403 ERR_REPO_NOT_ALLOWED`; - percent-encoded dots, extra slashes, backslashes, and Unicode lookalike dots return `400` with zero PKCS#8 import and zero `api.github.com` egress; -- the published OpenAPI `RepositoryLocator` schema is executed (not only string-compared), contains no lookaheads, rejects `.` / `..` segments, and accepts `.github`; and +- the published OpenAPI `RepositoryLocator` schema is executed (not only string-compared), contains no lookaheads, enforces the 1–100-character repository-name bound, rejects `.` / `..` segments, and accepts `.github`; and - owned production coverage of `validateRepositoryName` and `parseExchangeRequestBody` stays at 100 percent. ## References From 481889f3de389e7d69fb4603391baa62ddf8f29f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 05:09:17 -0700 Subject: [PATCH 392/564] test(egress): reject overlong repository token scope --- .../outbound-repository-name-boundary.test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/outbound-repository-name-boundary.test.ts diff --git a/test/outbound-repository-name-boundary.test.ts b/test/outbound-repository-name-boundary.test.ts new file mode 100644 index 000000000..c20664a3f --- /dev/null +++ b/test/outbound-repository-name-boundary.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it, vi } from "vitest"; + +import { + createFailClosedFetch, + type FetchLike, +} from "../src/outbound-fetch-policy"; + +function installationTokenRequest(repository: string): RequestInit { + return { + method: "POST", + headers: { authorization: "Bearer sensitive" }, + body: JSON.stringify({ + repositories: [repository], + permissions: { + pull_requests: "write", + contents: "read", + checks: "read", + }, + }), + }; +} + +describe("credential-egress repository-name boundary", () => { + it("allows a 100-character GitHub repository name and blocks a 101-character scope before credential egress", async () => { + const rawFetch = vi.fn(async () => Response.json({ ok: true })); + const wrapped = createFailClosedFetch(rawFetch); + const endpoint = "https://api.github.com/app/installations/1/access_tokens"; + + const allowed = await wrapped(endpoint, installationTokenRequest("r".repeat(100))); + expect(allowed.status).toBe(200); + expect(rawFetch).toHaveBeenCalledOnce(); + + rawFetch.mockClear(); + const blocked = await wrapped(endpoint, installationTokenRequest("r".repeat(101))); + + expect(rawFetch).not.toHaveBeenCalled(); + expect(blocked.status).toBe(502); + expect(blocked.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + }); +}); From 3a905c876c3cc9d57895945f05e5073383d1f2b7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 05:10:56 -0700 Subject: [PATCH 393/564] fix(egress): bound repository token scope --- src/outbound-fetch-policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index eb9effa07..21fd059d1 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -43,7 +43,7 @@ const githubRepositoryInstallationPathPattern = new RegExp( const githubAppInstallationsPathPattern = /^\/app\/installations$/; const githubInstallationTokenPathPattern = /^\/app\/installations\/([1-9][0-9]*)\/access_tokens$/; -const githubRepositoryNamePattern = /^(?!\.{1,2}$)[A-Za-z0-9_.-]+$/; +const githubRepositoryNamePattern = /^(?!\.{1,2}$)[A-Za-z0-9_.-]{1,100}$/; const installations = new WeakMap(); function blockedResponse(reason: BlockReason): Response { From 3feafcfee5e6716ccda7bc9a394b3ed41da7c6ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 05:12:42 -0700 Subject: [PATCH 394/564] test(openapi): bind pre-limiter 405 headers --- test/openapi-exchange-method-boundary.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/openapi-exchange-method-boundary.test.ts diff --git a/test/openapi-exchange-method-boundary.test.ts b/test/openapi-exchange-method-boundary.test.ts new file mode 100644 index 000000000..804594e18 --- /dev/null +++ b/test/openapi-exchange-method-boundary.test.ts @@ -0,0 +1,33 @@ +import { readFile } from "node:fs/promises"; +import { describe, expect, it } from "vitest"; + +import entrypoint, { type Env } from "../src/entrypoint"; + +const distributedHeaders = [ + "X-Rate-Limit-Limit", + "X-Rate-Limit-Remaining", + "X-Rate-Limit-Scope", +] as const; + +describe("OpenAPI exchange method boundary", () => { + it("does not advertise distributed rate-limit evidence on the pre-limiter 405 response", async () => { + const response = await entrypoint.fetch( + new Request("https://noema.example/exchange", { method: "GET" }), + {} as Env, + ); + + expect(response.status).toBe(405); + expect(response.headers.get("allow")).toBe("POST"); + for (const header of distributedHeaders) { + expect(response.headers.get(header)).toBeNull(); + } + + const spec = JSON.parse( + await readFile(new URL("../openapi.json", import.meta.url), "utf8"), + ) as Record; + const headers = spec.paths["/exchange"].post.responses["405"].headers; + for (const header of distributedHeaders) { + expect(headers[header], header).toBeUndefined(); + } + }); +}); From eaa4b82b6b4a2dec73b0c026891e28390ad57107 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 05:14:35 -0700 Subject: [PATCH 395/564] fix(openapi): align pre-limiter 405 headers --- openapi.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openapi.json b/openapi.json index c57c5844f..eaa1d2c41 100644 --- a/openapi.json +++ b/openapi.json @@ -121,10 +121,7 @@ "Pragma": { "$ref": "#/components/headers/Pragma" }, "X-Content-Type-Options": { "$ref": "#/components/headers/ContentTypeOptions" }, "X-Trace-Id": { "$ref": "#/components/headers/TraceId" }, - "X-Latency-Ms": { "$ref": "#/components/headers/LatencyMs" }, - "X-Rate-Limit-Limit": { "$ref": "#/components/headers/RateLimitLimit" }, - "X-Rate-Limit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" }, - "X-Rate-Limit-Scope": { "$ref": "#/components/headers/RateLimitScope" } + "X-Latency-Ms": { "$ref": "#/components/headers/LatencyMs" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } @@ -437,4 +434,4 @@ } } } -} \ No newline at end of file +} From a5b173a8cc026ef82af76696f101a19727961366 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 05:15:11 -0700 Subject: [PATCH 396/564] test(openapi): align distributed header authority --- test/openapi-contract.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/openapi-contract.test.ts b/test/openapi-contract.test.ts index f3efbada3..025430549 100644 --- a/test/openapi-contract.test.ts +++ b/test/openapi-contract.test.ts @@ -149,7 +149,7 @@ describe("machine-readable public HTTP contract", () => { "X-Rate-Limit-Scope", ]; - for (const status of ["200", "401", "403", "405", "429", "500", "502"]) { + for (const status of ["200", "401", "403", "429", "500", "502"]) { const response = resolveLocalRef(spec, responses[status]); for (const header of distributedRateLimitHeaders) { expect(response.headers?.[header], `${status} ${header}`).toBeDefined(); From 0a370777cf24666ba3a7ceac4732389269829070 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 06:08:13 -0700 Subject: [PATCH 397/564] test(oidc): require current central workflow source --- test/trusted-workflow-source-rollforward.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/trusted-workflow-source-rollforward.test.ts b/test/trusted-workflow-source-rollforward.test.ts index 23b5b2635..2a9e047f2 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 = - "cdde0d82602d9568128fd0c1e985ea40b2292710"; + "e5e0d3652a19d12db29e687924dbaf978cd78912"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { @@ -12,4 +12,4 @@ describe("trusted central workflow source revision", () => { `ALLOWED_WORKFLOW_SHA = "${auditedCentralWorkflowSourceSha}"`, ); }); -}); +}); \ No newline at end of file From 9233f6221227073e80b9e68cc2d3d09aa5718f53 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 06:08:44 -0700 Subject: [PATCH 398/564] 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 e706370b2..451e7e42b 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 = "cdde0d82602d9568128fd0c1e985ea40b2292710" +ALLOWED_WORKFLOW_SHA = "e5e0d3652a19d12db29e687924dbaf978cd78912" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 62b1527dce606e4121dbe8ed1d3d65f303a67738 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 06:10:24 -0700 Subject: [PATCH 399/564] docs(architecture): record current central trust rollforward --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 11e200419..9e7d1e5f7 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs readiness dispatch and delegates `/exchange`; `src/entrypoint.ts` applies the distributed rate limiter before `src/worker.ts` performs its denial-only exact workflow-ref precheck. `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the current audited protected central `.github` commit `cdde0d82602d9568128fd0c1e985ea40b2292710`. The `noema-review.yml` workflow blob is unchanged from the predecessor trusted revision `f6c2a2702b7b7578b2d1fc5f2f9a5125a0390d33`, while centrally archived SAST/contextual-orchestrator sidecar implementation inputs changed across the repository revisions. This roll-forward is therefore an explicit repository-commit trust decision rather than a claim that all trusted central inputs are content-equivalent. GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every later central commit movement still requires a new explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema; known central consumer/scanner defects remain owned and repaired there rather than by weakening Noema verification. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the current audited protected central `.github` commit `e5e0d3652a19d12db29e687924dbaf978cd78912`. The immediate protected predecessor was `cdde0d82602d9568128fd0c1e985ea40b2292710`; the one-commit roll-forward changed only LineageWeave hourly-repair caller/scheduler source, tests, doctoring documentation, and the central changelog. The `noema-review.yml` workflow blob is unchanged across that movement. This remains an explicit repository-commit trust decision rather than a claim that future central revisions are equivalent. GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every later central commit movement still requires a new explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema; known central consumer/scanner defects remain owned and repaired there rather than by weakening Noema verification. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The protected workflow-ref parser and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 7cbfcaa8953f6c40ed94c922e1587e7a570bd1fe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 08:34:30 -0700 Subject: [PATCH 400/564] test(oidc): roll trusted 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 2a9e047f2..2cbb80c77 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 = - "e5e0d3652a19d12db29e687924dbaf978cd78912"; + "e1b03eebc6dc5c85aed393e5928927c96376cf46"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From 1bed96014a1fb75207fe2b54fbfe56e3967f5f38 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 08:34:54 -0700 Subject: [PATCH 401/564] fix(oidc): trust current protected central source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 451e7e42b..419db2d2f 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 = "e5e0d3652a19d12db29e687924dbaf978cd78912" +ALLOWED_WORKFLOW_SHA = "e1b03eebc6dc5c85aed393e5928927c96376cf46" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From fba53f19217471e1d361e0d30f8092435fcf14df Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 08:36:01 -0700 Subject: [PATCH 402/564] docs(architecture): record current central workflow trust --- ARCHITECTURE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9e7d1e5f7..ed128a6bf 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs readiness dispatch and delegates `/exchange`; `src/entrypoint.ts` applies the distributed rate limiter before `src/worker.ts` performs its denial-only exact workflow-ref precheck. `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the current audited protected central `.github` commit `e5e0d3652a19d12db29e687924dbaf978cd78912`. The immediate protected predecessor was `cdde0d82602d9568128fd0c1e985ea40b2292710`; the one-commit roll-forward changed only LineageWeave hourly-repair caller/scheduler source, tests, doctoring documentation, and the central changelog. The `noema-review.yml` workflow blob is unchanged across that movement. This remains an explicit repository-commit trust decision rather than a claim that future central revisions are equivalent. GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every later central commit movement still requires a new explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema; known central consumer/scanner defects remain owned and repaired there rather than by weakening Noema verification. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the current audited protected central `.github` commit `e1b03eebc6dc5c85aed393e5928927c96376cf46`. The immediate audited predecessor was `e5e0d3652a19d12db29e687924dbaf978cd78912`; protected central advanced through its independently owned #1379 integration while `.github/workflows/noema-review.yml` retained the same blob identity `5c60782adb8be11d6538caea269a4bdfab7ab4d1`. This remains an explicit repository-commit trust decision rather than a claim that future central revisions are equivalent. GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every later central commit movement still requires a new explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema; known central consumer/scanner defects remain owned and repaired there rather than by weakening Noema verification. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The protected workflow-ref parser and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. @@ -152,4 +152,4 @@ Root README/customer copy may have a separate active owner; the canonical archit The default shape is **small credential-exchange service + explicit state coordinators + external orchestration/review planes**. New model orchestration, artifact processing, repository mutation, or deployment authority should first be evaluated as a separate bounded component rather than folded into `/exchange`. -Architecture changes must keep source behavior, realistic regression tests, canonical documentation, traceability, and CHANGELOG semantics consistent without promoting active-PR behavior to protected truth. +Architecture changes must keep source behavior, realistic regression tests, canonical documentation, traceability, and CHANGELOG semantics consistent without promoting active-PR behavior to protected truth. \ No newline at end of file From 6e3fddfd508cc7b0fb0dbf3287477ab3663eebcd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 13:34:08 -0700 Subject: [PATCH 403/564] test(oidc): require authoritative metadata status --- test/oidc-metadata-content-type.test.ts | 54 +++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/oidc-metadata-content-type.test.ts b/test/oidc-metadata-content-type.test.ts index ac5809d58..e61a184da 100644 --- a/test/oidc-metadata-content-type.test.ts +++ b/test/oidc-metadata-content-type.test.ts @@ -98,6 +98,33 @@ describe("GitHub OIDC metadata media-type authority", () => { }); }); + it("rejects successful non-200 discovery responses before following jwks_uri", async () => { + let jwksFetches = 0; + const response = await exchangeWithFetch(async (input) => { + const url = String(input); + if (url === "https://token.actions.githubusercontent.com/.well-known/openid-configuration") { + return new Response(JSON.stringify({ + jwks_uri: "https://token.actions.githubusercontent.com/.well-known/jwks", + }), { + status: 201, + headers: { "content-type": "application/json" }, + }); + } + if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { + jwksFetches += 1; + } + return new Response("unexpected privileged egress", { status: 500 }); + }); + + expect(response.status).toBe(502); + expect(jwksFetches).toBe(0); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_OIDC_VERIFICATION", + message: "failed to fetch GitHub OIDC discovery document", + }); + }); + it("rejects a discovery JSON body with no declared media type", async () => { const response = await exchangeWithFetch(async (input) => { const url = String(input); @@ -169,6 +196,33 @@ describe("GitHub OIDC metadata media-type authority", () => { }); }); + it("rejects successful non-200 JWKS responses before key import", async () => { + const response = await exchangeWithFetch(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 new Response(JSON.stringify({ + keys: [{ kid: "content-type-test", kty: "RSA" }], + }), { + status: 206, + headers: { "content-type": "application/json" }, + }); + } + return new Response("unexpected privileged egress", { status: 500 }); + }); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_OIDC_VERIFICATION", + message: "failed to fetch GitHub OIDC JWKS", + }); + }); + it("rejects a JWKS JSON body with no declared media type", async () => { const response = await exchangeWithFetch(async (input) => { const url = String(input); From f9771f6b617885c0bd0fa8979571d42a81fa86bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 13:38:08 -0700 Subject: [PATCH 404/564] fix(oidc): require authoritative metadata status --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index bb3dd8fcc..153fc77d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -488,7 +488,7 @@ async function fetchGithubOidcKeys(env: Env, forceRefresh = false): Promise Date: Fri, 28 Aug 2026 14:06:06 -0700 Subject: [PATCH 405/564] test(github): require endpoint-specific success status authority --- .../github-api-content-type-authority.test.ts | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/test/github-api-content-type-authority.test.ts b/test/github-api-content-type-authority.test.ts index 804b696ae..7e1596528 100644 --- a/test/github-api-content-type-authority.test.ts +++ b/test/github-api-content-type-authority.test.ts @@ -132,6 +132,52 @@ async function exchangeWithInstallationTokenResponse( ); } +async function exchangeWithInstallationLookupResponse( + token: string, + jwk: JsonWebKey, + installationLookupResponse: Response, +) { + let accessTokenRequested = false; + 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/repos/ContextualWisdomLab/.github/installation") { + return installationLookupResponse; + } + if (url === "https://api.github.com/app/installations/92345/access_tokens") { + accessTokenRequested = true; + return new Response(JSON.stringify({ + token: "ghs_should_not_be_requested", + expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), + }), { + status: 201, + headers: { "content-type": "application/json" }, + }); + } + return new Response("unexpected privileged egress", { status: 500 }); + }); + + const response = await worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { authorization: `Bearer ${token}` }, + }), + { + ...env, + GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKeyPem, + GITHUB_APP_INSTALLATION_ID: undefined, + }, + ); + return { response, accessTokenRequested }; +} + async function expectUnexpectedContentType(response: Response) { expect(response.status).toBe(502); await expect(response.json()).resolves.toMatchObject({ @@ -141,6 +187,15 @@ async function expectUnexpectedContentType(response: Response) { }); } +async function expectUnexpectedSuccessStatus(response: Response) { + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_API", + message: "GitHub API returned an unexpected success status", + }); +} + describe("GitHub API JSON media-type authority", () => { it("rejects a valid installation-token JSON body declared as text/plain", async () => { const { token, jwk } = await signedOidcToken(); @@ -219,3 +274,43 @@ describe("GitHub API JSON media-type authority", () => { }); }); }); + +describe("GitHub API endpoint-specific success status authority", () => { + it.each([200, 206])( + "rejects installation-token creation HTTP %s even when the JSON body is otherwise valid", + async (status) => { + const { token, jwk } = await signedOidcToken(); + const response = await exchangeWithInstallationTokenResponse( + token, + jwk, + new Response(JSON.stringify({ + token: `ghs_wrong_success_${status}`, + expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), + }), { + status, + headers: { "content-type": "application/json" }, + }), + ); + + await expectUnexpectedSuccessStatus(response); + }, + ); + + it.each([201, 206])( + "rejects installation lookup HTTP %s before requesting a privileged access token", + async (status) => { + const { token, jwk } = await signedOidcToken(); + const { response, accessTokenRequested } = await exchangeWithInstallationLookupResponse( + token, + jwk, + new Response(JSON.stringify({ id: 92345 }), { + status, + headers: { "content-type": "application/json" }, + }), + ); + + expect(accessTokenRequested).toBe(false); + await expectUnexpectedSuccessStatus(response); + }, + ); +}); From 3734c00bda5d207cb2be284f24d4453d6f50fa23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 14:09:04 -0700 Subject: [PATCH 406/564] fix(github): bind JSON success to endpoint status --- src/index.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 153fc77d3..07c9eec6a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -737,7 +737,12 @@ type GitHubJsonRequestInit = RequestInit & { headers: Record; }; -async function githubJson(path: string, init: GitHubJsonRequestInit, env: Env): Promise> { +async function githubJson( + path: string, + init: GitHubJsonRequestInit, + env: Env, + expectedStatus: number, +): Promise> { const response = await fetch(new URL(path, env.GITHUB_API_BASE), { ...init, headers: { @@ -756,6 +761,9 @@ async function githubJson(path: string, init: GitHubJsonRequestInit, env: Env): } throw new ApiError("ERR_GITHUB_API", response.status >= 400 ? 400 : 500, "GitHub API request failed"); } + if (response.status !== expectedStatus) { + throw new ApiError("ERR_GITHUB_API", 502, "GitHub API returned an unexpected success status"); + } if (!oidcJsonMediaTypePattern.test(response.headers.get("content-type") ?? "")) { throw new ApiError("ERR_GITHUB_API", 502, "GitHub API returned an unexpected content type"); } @@ -795,7 +803,7 @@ async function resolveInstallationId(appJwt: string, repository: string, env: En const installation = await githubJson(`/repos/${repository}/installation`, { headers: { authorization: `Bearer ${appJwt}` }, - }, env); + }, env, 200); if (installation.id === undefined || installation.id === null) { throw new ApiError("ERR_GITHUB_INSTALLATION", 500, "GitHub App installation id was not found"); } @@ -821,7 +829,7 @@ async function createInstallationToken(repository: string, env: Env): Promise Date: Fri, 28 Aug 2026 14:12:56 -0700 Subject: [PATCH 407/564] test(github): align app token fixtures with HTTP 201 --- test/github-app-runtime-coverage.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/github-app-runtime-coverage.test.ts b/test/github-app-runtime-coverage.test.ts index 7861ba410..3bf777848 100644 --- a/test/github-app-runtime-coverage.test.ts +++ b/test/github-app-runtime-coverage.test.ts @@ -135,7 +135,7 @@ function successfulTokenResponse( return Response.json({ token, expires_at: expiresAt, - }); + }, { status: 201 }); } describe("GitHub App runtime coverage through the public exchange boundary", () => { @@ -234,7 +234,7 @@ describe("GitHub App runtime coverage through the public exchange boundary", () { ...baseEnv, GITHUB_APP_INSTALLATION_ID: "22345" }, (url) => { if (url === "https://api.github.com/app/installations/22345/access_tokens") { - return Response.json({ expires_at: "2030-01-01T00:00:00Z" }); + return Response.json({ expires_at: "2030-01-01T00:00:00Z" }, { status: 201 }); } return new Response("unexpected GitHub request", { status: 500 }); }, @@ -258,7 +258,7 @@ describe("GitHub App runtime coverage through the public exchange boundary", () { ...baseEnv, GITHUB_APP_INSTALLATION_ID: "32345" }, (url) => { if (url === "https://api.github.com/app/installations/32345/access_tokens") { - return Response.json({ token: "ghs_bad_expiry", expires_at: expiresAt }); + return Response.json({ token: "ghs_bad_expiry", expires_at: expiresAt }, { status: 201 }); } return new Response("unexpected GitHub request", { status: 500 }); }, From c00ee25b137c2057da07077c9804a1ff6dad518a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 14:13:39 -0700 Subject: [PATCH 408/564] test(github): use HTTP 201 token-mint fixtures --- test/replay-before-token-mint.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/replay-before-token-mint.test.ts b/test/replay-before-token-mint.test.ts index 5c692bdc8..ccec33a99 100644 --- a/test/replay-before-token-mint.test.ts +++ b/test/replay-before-token-mint.test.ts @@ -136,7 +136,7 @@ describe("verified OIDC replay claim ordering", () => { return Response.json({ token: "ghs_should_never_be_minted_for_a_replay", expires_at: "2026-08-09T12:00:00Z", - }); + }, { status: 201 }); } return new Response("unexpected upstream call", { status: 500 }); }); @@ -239,7 +239,7 @@ describe("verified OIDC replay claim ordering", () => { return Response.json({ token: "ghs_single_use_success", expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(), - }); + }, { status: 201 }); } return new Response("unexpected upstream call", { status: 500 }); }); From 5a534aeca296bd87897aeb25fbf959ef2a802418 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 14:14:13 -0700 Subject: [PATCH 409/564] test(github): model token creation as HTTP 201 --- 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 990ad29e1..3ea61bd85 100644 --- a/test/replay-request-core-coverage.test.ts +++ b/test/replay-request-core-coverage.test.ts @@ -118,7 +118,7 @@ function installOidcFetch(jwk: JsonWebKey, env: Env, installationToken = false) return Response.json({ token: "ghs_replay_coverage_token", expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), - }); + }, { status: 201 }); } return new Response("not found", { status: 404 }); }); From 3e49c9c26c22ed1af504e2ef6e2c6d4bb87ea5cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 14:14:41 -0700 Subject: [PATCH 410/564] test(github): model successful token creation status --- test/exchange-success-path-coverage.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/exchange-success-path-coverage.test.ts b/test/exchange-success-path-coverage.test.ts index 1f74cb402..e9c000c7a 100644 --- a/test/exchange-success-path-coverage.test.ts +++ b/test/exchange-success-path-coverage.test.ts @@ -119,7 +119,7 @@ describe("exchange success-path coverage through the public worker", () => { return Response.json({ token: "ghs_exchange_success_token", expires_at: expiresAt, - }); + }, { status: 201 }); } return new Response("not found", { status: 404 }); }); From cde64924bdbfdb8a25f9f322286726e5a1595795 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 14:15:09 -0700 Subject: [PATCH 411/564] test(github): preserve token expiry semantics under HTTP 201 --- test/github-installation-expiry-defensive-coverage.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/github-installation-expiry-defensive-coverage.test.ts b/test/github-installation-expiry-defensive-coverage.test.ts index b29a426b4..ea548c2c7 100644 --- a/test/github-installation-expiry-defensive-coverage.test.ts +++ b/test/github-installation-expiry-defensive-coverage.test.ts @@ -56,7 +56,7 @@ async function exchangeWithTokenResponse(tokenBody: unknown, clientIp: string): 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); + if (url === "https://api.github.com/app/installations/92345/access_tokens") return Response.json(tokenBody, { status: 201 }); 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 }); From 2b1377febdff75e6b06629069a88d1f1572c61c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 14:15:35 -0700 Subject: [PATCH 412/564] test(github): model expiry fixture as HTTP 201 --- .../github-installation-token-expiry-calendar-integrity.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/github-installation-token-expiry-calendar-integrity.test.ts b/test/github-installation-token-expiry-calendar-integrity.test.ts index 7f1a0a08c..01c60b151 100644 --- a/test/github-installation-token-expiry-calendar-integrity.test.ts +++ b/test/github-installation-token-expiry-calendar-integrity.test.ts @@ -114,7 +114,7 @@ describe("GitHub installation-token expiry calendar integrity", () => { return Response.json({ token: "ghs_impossible_calendar_expiry", expires_at: "2030-02-30T00:30:00Z", - }); + }, { status: 201 }); } return new Response("unexpected GitHub request", { status: 500 }); }); From ef2510752a03e9b334d85633c9d62b92a5ec5dda Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 14:16:26 -0700 Subject: [PATCH 413/564] test(github): preserve token response semantics under HTTP 201 --- test/github-api-malformed-json.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/github-api-malformed-json.test.ts b/test/github-api-malformed-json.test.ts index 40cd01d26..df4281b55 100644 --- a/test/github-api-malformed-json.test.ts +++ b/test/github-api-malformed-json.test.ts @@ -155,7 +155,7 @@ describe("GitHub API success-response parsing", () => { (url) => { if (url === "https://api.github.com/app/installations/92345/access_tokens") { return new Response("{", { - status: 200, + status: 201, headers: { "content-type": "application/json" }, }); } @@ -179,7 +179,7 @@ describe("GitHub API success-response parsing", () => { (url) => { if (url === "https://api.github.com/app/installations/92345/access_tokens") { return new Response(null, { - status: 200, + status: 201, headers: { "content-type": "application/json" }, }); } @@ -220,7 +220,7 @@ describe("GitHub API success-response parsing", () => { (url) => { if (url === "https://api.github.com/app/installations/92345/access_tokens") { return new Response(streamedBody, { - status: 200, + status: 201, headers: { "content-type": "application/json" }, }); } @@ -289,7 +289,7 @@ describe("GitHub API success-response parsing", () => { return Response.json({ token: { attacker_controlled: true }, expires_at: "2099-01-01T00:00:00Z", - }); + }, { status: 201 }); } return new Response("unexpected GitHub request", { status: 500 }); }, @@ -319,7 +319,7 @@ describe("GitHub API success-response parsing", () => { return Response.json({ token, expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), - }); + }, { status: 201 }); } return new Response("unexpected GitHub request", { status: 500 }); }, @@ -344,7 +344,7 @@ describe("GitHub API success-response parsing", () => { return Response.json({ token: "ghs_expired", expires_at: "2029-12-31T23:59:59Z", - }); + }, { status: 201 }); } return new Response("unexpected GitHub request", { status: 500 }); }, @@ -369,7 +369,7 @@ describe("GitHub API success-response parsing", () => { return Response.json({ token: "ghs_overlong", expires_at: "2030-01-01T02:00:00Z", - }); + }, { status: 201 }); } return new Response("unexpected GitHub request", { status: 500 }); }, From ea58ea191d2a0a07595fcd280fa618693ef43279 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 14:22:12 -0700 Subject: [PATCH 414/564] test(github): model worker token responses as HTTP 201 --- test/worker.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/worker.test.ts b/test/worker.test.ts index 9fb204b73..fdf5c98d7 100644 --- a/test/worker.test.ts +++ b/test/worker.test.ts @@ -407,7 +407,7 @@ describe("Noema worker", () => { return Response.json({ token: "ghs_installation_token", expires_at: tokenExpiresAt, - }); + }, { status: 201 }); } return new Response("not found", { status: 404 }); }); @@ -497,7 +497,7 @@ describe("Noema worker", () => { return Response.json({ token: "ghs_installation_token", expires_at: "not-a-date", - }); + }, { status: 201 }); } return new Response("not found", { status: 404 }); }); @@ -526,4 +526,4 @@ describe("Noema worker", () => { }, }); }); -}); +}); \ No newline at end of file From 6edea7af513aa015fec53deac4e19f5d95f4b50d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 15:00:12 -0700 Subject: [PATCH 415/564] test(oidc): reject contradictory JWKS key operations --- ...oidc-jwks-key-operations-authority.test.ts | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 test/oidc-jwks-key-operations-authority.test.ts diff --git a/test/oidc-jwks-key-operations-authority.test.ts b/test/oidc-jwks-key-operations-authority.test.ts new file mode 100644 index 000000000..8d9df890a --- /dev/null +++ b/test/oidc-jwks-key-operations-authority.test.ts @@ -0,0 +1,123 @@ +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-jwks-key-operations-authority"; + +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 encodeSegment(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +async function signedJwt(): Promise { + const now = Math.floor(Date.now() / 1000); + const header = encodeSegment({ alg: "RS256", kid: signingKid }); + const payload = encodeSegment({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + 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 signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + signingPrivateKey, + new TextEncoder().encode(`${header}.${payload}`), + ); + return `${header}.${payload}.${Buffer.from(signature).toString("base64url")}`; +} + +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 JWKS key operation authority", () => { + it("rejects an RSA verification key whose JWK also declares signing authority", async () => { + const token = await signedJwt(); + vi.resetModules(); + const { default: worker } = await import("../src/index"); + const fetchSpy = 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", + use: "sig", + alg: "RS256", + key_ops: ["sign", "verify"], + }], + }); + } + return new Response("unexpected privileged 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.237", + }, + body: JSON.stringify({ target_repository: 42 }), + }), + env, + ); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_OIDC_VERIFICATION", + message: "GitHub OIDC JWKS did not include valid key entries", + }); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); +}); From de5b2cfe1e079130472743a8c7bbd8770f8d12a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 15:04:05 -0700 Subject: [PATCH 416/564] fix(oidc): enforce JWKS verification key operations --- src/index.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 07c9eec6a..d1098e4d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -551,7 +551,18 @@ function uniqueRsaSigningKey(jwks: JsonWebKeySet, kid: string): (JsonWebKey & { "GitHub OIDC JWKS assigned an ambiguous signing key id", ); } - return matches[0]; + const match = matches[0]; + if ( + match?.key_ops !== undefined + && (match.key_ops.length !== 1 || match.key_ops[0] !== "verify") + ) { + throw new ApiError( + "ERR_OIDC_VERIFICATION", + 502, + "GitHub OIDC JWKS did not include valid key entries", + ); + } + return match; } async function verifyGithubOidcJwt(token: string, env: Env): Promise { From f899ad9d0d0844c66d0d3dc85990dc689e0c57b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 16:05:31 -0700 Subject: [PATCH 417/564] test(egress): reject unused app installation listing authority --- ...-fetch-app-installations-authority.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 test/outbound-fetch-app-installations-authority.test.ts diff --git a/test/outbound-fetch-app-installations-authority.test.ts b/test/outbound-fetch-app-installations-authority.test.ts new file mode 100644 index 000000000..47cce1d1f --- /dev/null +++ b/test/outbound-fetch-app-installations-authority.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it, vi } from "vitest"; +import { + createFailClosedFetch, + type FetchLike, +} from "../src/outbound-fetch-policy"; + +describe("credential egress operation authority", () => { + it("rejects bearer credential forwarding to the unused app-installations listing", async () => { + const rawFetch = vi.fn(async () => Response.json({ installations: [] })); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped("https://api.github.com/app/installations", { + method: "GET", + headers: { authorization: "Bearer sensitive" }, + }); + + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(rawFetch).not.toHaveBeenCalled(); + }); +}); From 978c54be09579cffb2bae99eeaef88fb4a265101 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 16:09:32 -0700 Subject: [PATCH 418/564] fix(egress): remove unused app installation listing authority --- src/outbound-fetch-policy.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 21fd059d1..e6f459c2a 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -24,7 +24,6 @@ type BlockReason = "destination" | "request-policy" | "redirect" | "response-siz type GitHubApiOperation = | "repository-installation" - | "app-installations" | "installation-token"; const TRUSTED_GITHUB_API_ORIGIN = "https://api.github.com"; @@ -40,7 +39,6 @@ const repositorySegmentPattern = "[A-Za-z0-9_.-]+"; const githubRepositoryInstallationPathPattern = new RegExp( `^/repos/${repositorySegmentPattern}/${repositorySegmentPattern}/installation$`, ); -const githubAppInstallationsPathPattern = /^\/app\/installations$/; const githubInstallationTokenPathPattern = /^\/app\/installations\/([1-9][0-9]*)\/access_tokens$/; const githubRepositoryNamePattern = /^(?!\.{1,2}$)[A-Za-z0-9_.-]{1,100}$/; @@ -245,9 +243,6 @@ function githubApiOperation(url: URL): GitHubApiOperation | undefined { if (githubRepositoryInstallationPathPattern.test(url.pathname)) { return "repository-installation"; } - if (githubAppInstallationsPathPattern.test(url.pathname)) { - return "app-installations"; - } if (canonicalInstallationIdFromTokenPath(url) !== undefined) { return "installation-token"; } @@ -332,7 +327,7 @@ export function isTrustedCredentialEgressRequest( } const operation = githubApiOperation(url); - if (operation === "repository-installation" || operation === "app-installations") { + if (operation === "repository-installation") { return method === "GET" && !bodyPresent; } return operation === "installation-token" From f5a2875d278243496a078b8a658c31169e8c2288 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 16:10:10 -0700 Subject: [PATCH 419/564] test(egress): keep redirect coverage on used installation lookup --- test/outbound-fetch-policy.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/outbound-fetch-policy.test.ts b/test/outbound-fetch-policy.test.ts index 1d2dcc657..155716f3d 100644 --- a/test/outbound-fetch-policy.test.ts +++ b/test/outbound-fetch-policy.test.ts @@ -44,7 +44,7 @@ describe("credential-bearing outbound fetch policy", () => { headers: { "content-length": "2" }, })); const wrapped = createFailClosedFetch(rawFetch); - const request = new Request("https://api.github.com/app/installations", { + const request = new Request("https://api.github.com/repos/ContextualWisdomLab/noema/installation", { method: "GET", }); From 7cb8ca88d6f68e69b2035a01f3ffb7b6df5ea4e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 16:10:28 -0700 Subject: [PATCH 420/564] test(egress): bind authorization framing to used lookup --- test/outbound-fetch-authorization-canonicality.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/outbound-fetch-authorization-canonicality.test.ts b/test/outbound-fetch-authorization-canonicality.test.ts index 7a48cd864..6ace265fc 100644 --- a/test/outbound-fetch-authorization-canonicality.test.ts +++ b/test/outbound-fetch-authorization-canonicality.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { isTrustedCredentialEgressRequest } from "../src/outbound-fetch-policy"; -const installationLookup = "https://api.github.com/app/installations"; +const installationLookup = "https://api.github.com/repos/ContextualWisdomLab/noema/installation"; describe("credential-egress Authorization framing", () => { it("accepts exactly one ASCII space between Bearer and the credential from raw RequestInit headers", () => { From e9193ed5fe34fb70dcb47786db7032dbbe6a5e03 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 17:08:29 -0700 Subject: [PATCH 421/564] test(rate-limit): bound local client bucket cardinality --- test/local-rate-limit-bucket-capacity.test.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 test/local-rate-limit-bucket-capacity.test.ts diff --git a/test/local-rate-limit-bucket-capacity.test.ts b/test/local-rate-limit-bucket-capacity.test.ts new file mode 100644 index 000000000..361b24af9 --- /dev/null +++ b/test/local-rate-limit-bucket-capacity.test.ts @@ -0,0 +1,52 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { Env } from "../src/index"; + +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: + "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: "unused", + NOEMA_RATE_LIMIT_PER_MINUTE: "1", +}; + +function exchangeRequest(clientIp: string): Request { + return new Request("https://noema.example/exchange", { + method: "POST", + headers: { "cf-connecting-ip": clientIp }, + }); +} + +describe("local rate-limit bucket capacity", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("bounds active process-local defense-in-depth buckets instead of retaining unbounded client identities", async () => { + vi.resetModules(); + vi.spyOn(console, "log").mockImplementation(() => undefined); + vi.spyOn(Date, "now").mockReturnValue(1_800_000_000_000); + const { default: worker } = await import("../src/index"); + + const oldestClient = "10.255.255.254"; + expect((await worker.fetch(exchangeRequest(oldestClient), env)).status).toBe(401); + + for (let index = 0; index < 10_000; index += 1) { + const clientIp = `10.0.${Math.floor(index / 256)}.${index % 256}`; + const response = await worker.fetch(exchangeRequest(clientIp), env); + expect(response.status).toBe(401); + } + + // Once 10,000 active identities are retained, the oldest defense-in-depth + // bucket must be evicted before accepting another identity. The distributed + // Durable Object limiter remains the production authority, while this local + // layer stays memory-bounded rather than growing with attacker-controlled + // trusted client cardinality. + expect((await worker.fetch(exchangeRequest(oldestClient), env)).status).toBe(401); + }, 30_000); +}); From eb8c721b837d44292473d00c8786599f4f5f9b28 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 17:18:05 -0700 Subject: [PATCH 422/564] fix(rate-limit): bound local client bucket retention --- src/index.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index d1098e4d0..b6fcd68cb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -110,6 +110,7 @@ type TimedCache = { const rateLimitBuckets = new Map(); const rateLimitWindowMs = 60_000; +const maxLocalRateLimitBuckets = 10_000; let oidcKeysCache: TimedCache | undefined; const installationIdCache = new Map>(); @@ -264,12 +265,17 @@ function enforceRateLimit(request: Request, env: Env, route: string) { } function cleanupRateLimitBuckets(now: number) { - if (rateLimitBuckets.size < 10_000) return; + if (rateLimitBuckets.size <= maxLocalRateLimitBuckets) return; for (const [key, bucket] of rateLimitBuckets) { if (now - bucket.windowStartMs >= rateLimitWindowMs) { rateLimitBuckets.delete(key); } } + while (rateLimitBuckets.size > maxLocalRateLimitBuckets) { + const oldestKey = rateLimitBuckets.keys().next().value; + if (oldestKey === undefined) break; + rateLimitBuckets.delete(oldestKey); + } } function successResponse(data: T, traceId: string, status = 200): Response { @@ -1085,4 +1091,4 @@ export default { return withOperationalHeaders(response, traceId, latency_ms); } }, -}; +}; \ No newline at end of file From 0c65c589ccde7477220c7534ad3d396631a4094d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 17:33:57 -0700 Subject: [PATCH 423/564] fix(rate-limit): 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 b6fcd68cb..20c3ba9d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1091,4 +1091,4 @@ export default { return withOperationalHeaders(response, traceId, latency_ms); } }, -}; \ No newline at end of file +}; From bfb6f880f0dd222989ed9b25f07ed59ef976a96f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 17:39:18 -0700 Subject: [PATCH 424/564] test(rate-limit): cover expired bucket cleanup at capacity --- test/local-rate-limit-bucket-capacity.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/local-rate-limit-bucket-capacity.test.ts b/test/local-rate-limit-bucket-capacity.test.ts index 361b24af9..f7c14d2ac 100644 --- a/test/local-rate-limit-bucket-capacity.test.ts +++ b/test/local-rate-limit-bucket-capacity.test.ts @@ -49,4 +49,25 @@ describe("local rate-limit bucket capacity", () => { // trusted client cardinality. expect((await worker.fetch(exchangeRequest(oldestClient), env)).status).toBe(401); }, 30_000); + + it("drops expired identities before capacity eviction while preserving the current bucket", async () => { + vi.resetModules(); + vi.spyOn(console, "log").mockImplementation(() => undefined); + const now = vi.spyOn(Date, "now").mockReturnValue(1_800_000_000_000); + const { default: worker } = await import("../src/index"); + + for (let index = 0; index < 10_000; index += 1) { + const clientIp = `10.1.${Math.floor(index / 256)}.${index % 256}`; + expect((await worker.fetch(exchangeRequest(clientIp), env)).status).toBe(401); + } + + now.mockReturnValue(1_800_000_060_001); + const currentClient = "10.254.254.254"; + expect((await worker.fetch(exchangeRequest(currentClient), env)).status).toBe(401); + expect((await worker.fetch(exchangeRequest(currentClient), env)).status).toBe(429); + + // The old identity is accepted as a fresh bucket after the stale population + // is discarded, while the current-window bucket above remains rate-limited. + expect((await worker.fetch(exchangeRequest("10.1.0.0"), env)).status).toBe(401); + }, 30_000); }); From 33b6c1d140827aafe732bf33f9ab0e2eec3db7a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 17:44:30 -0700 Subject: [PATCH 425/564] fix(rate-limit): remove unreachable capacity branch --- src/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 20c3ba9d4..368a7b846 100644 --- a/src/index.ts +++ b/src/index.ts @@ -272,9 +272,7 @@ function cleanupRateLimitBuckets(now: number) { } } while (rateLimitBuckets.size > maxLocalRateLimitBuckets) { - const oldestKey = rateLimitBuckets.keys().next().value; - if (oldestKey === undefined) break; - rateLimitBuckets.delete(oldestKey); + rateLimitBuckets.delete(rateLimitBuckets.keys().next().value!); } } From 42fcb1957397aee0c665c8d39e6dedcfb039e308 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 17:59:01 -0700 Subject: [PATCH 426/564] test(rate-limit): preserve renewed bucket eviction recency --- test/local-rate-limit-bucket-capacity.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/local-rate-limit-bucket-capacity.test.ts b/test/local-rate-limit-bucket-capacity.test.ts index f7c14d2ac..55510a770 100644 --- a/test/local-rate-limit-bucket-capacity.test.ts +++ b/test/local-rate-limit-bucket-capacity.test.ts @@ -70,4 +70,28 @@ describe("local rate-limit bucket capacity", () => { // is discarded, while the current-window bucket above remains rate-limited. expect((await worker.fetch(exchangeRequest("10.1.0.0"), env)).status).toBe(401); }, 30_000); + + it("keeps a renewed oldest bucket current when the next identity triggers capacity eviction", async () => { + vi.resetModules(); + vi.spyOn(console, "log").mockImplementation(() => undefined); + const now = vi.spyOn(Date, "now").mockReturnValue(1_800_000_000_000); + const { default: worker } = await import("../src/index"); + + const renewedClient = "10.2.255.254"; + expect((await worker.fetch(exchangeRequest(renewedClient), env)).status).toBe(401); + + now.mockReturnValue(1_800_000_030_000); + for (let index = 0; index < 9_999; index += 1) { + const clientIp = `10.3.${Math.floor(index / 256)}.${index % 256}`; + expect((await worker.fetch(exchangeRequest(clientIp), env)).status).toBe(401); + } + + // Only the oldest identity has expired. Renewing it must refresh both its + // rate-limit window and its eviction recency; otherwise the next new client + // evicts the just-renewed bucket and grants that client a fresh local window. + now.mockReturnValue(1_800_000_060_001); + expect((await worker.fetch(exchangeRequest(renewedClient), env)).status).toBe(401); + expect((await worker.fetch(exchangeRequest("10.254.254.253"), env)).status).toBe(401); + expect((await worker.fetch(exchangeRequest(renewedClient), env)).status).toBe(429); + }, 30_000); }); From e2095a82831b365b944c1a50e017a4f2f05bf91f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 18:02:39 -0700 Subject: [PATCH 427/564] fix(rate-limit): refresh renewed bucket eviction recency --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 368a7b846..685d1800b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -248,6 +248,7 @@ function enforceRateLimit(request: Request, env: Env, route: string) { const bucket = rateLimitBuckets.get(key); if (!bucket || now - bucket.windowStartMs >= rateLimitWindowMs) { + if (bucket) rateLimitBuckets.delete(key); rateLimitBuckets.set(key, { windowStartMs: now, count: 1 }); cleanupRateLimitBuckets(now); return; From 9a62f08a7ae3702034f23df06fd37266c6989278 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 18:59:09 -0700 Subject: [PATCH 428/564] test(ci): preserve in-flight validator cache seed --- test/patch-validator-image-build-cache.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/patch-validator-image-build-cache.test.ts b/test/patch-validator-image-build-cache.test.ts index 705a89508..b2cc079d4 100644 --- a/test/patch-validator-image-build-cache.test.ts +++ b/test/patch-validator-image-build-cache.test.ts @@ -21,4 +21,11 @@ describe("patch-validator image build cache", () => { "timeout --signal=TERM --kill-after=30s 150m docker build \\", ); }); + + it("lets an in-flight exact-head build finish exporting the shared cache", () => { + expect(workflow).toContain( + "group: noema-patch-validator-image-${{ github.event.pull_request.number || github.ref }}", + ); + expect(workflow).toContain("cancel-in-progress: false"); + }); }); From e111bf309ee50a7b43a915034fbe5448b4e028f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 19:00:35 -0700 Subject: [PATCH 429/564] fix(ci): let validator builds seed shared cache --- .github/workflows/patch-validator-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/patch-validator-image.yml b/.github/workflows/patch-validator-image.yml index 1d5792f50..b59f3eb49 100644 --- a/.github/workflows/patch-validator-image.yml +++ b/.github/workflows/patch-validator-image.yml @@ -6,7 +6,7 @@ on: concurrency: group: noema-patch-validator-image-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + cancel-in-progress: false permissions: contents: read From b8a9254cb5c12e1b7dde76d79faa8f338fba053c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 20:01:35 -0700 Subject: [PATCH 430/564] test(exchange): reject misleading JSON media types in base worker --- ...worker-exchange-media-type-defense.test.ts | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 test/worker-exchange-media-type-defense.test.ts diff --git a/test/worker-exchange-media-type-defense.test.ts b/test/worker-exchange-media-type-defense.test.ts new file mode 100644 index 000000000..96538f235 --- /dev/null +++ b/test/worker-exchange-media-type-defense.test.ts @@ -0,0 +1,99 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import worker, { type Env } from "../src/index"; + +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: "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: "unused", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", +}; + +function encodeSegment(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +async function createSignedJwt() { + 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"], + ); + const kid = `worker-media-type-${crypto.randomUUID()}`; + const now = Math.floor(Date.now() / 1000); + const header = encodeSegment({ alg: "RS256", kid, typ: "JWT" }); + const payload = encodeSegment({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + job_workflow_ref: env.ALLOWED_WORKFLOW_REF_PREFIX, + job_workflow_sha: env.ALLOWED_WORKFLOW_SHA, + 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", + keyPair.privateKey, + new TextEncoder().encode(`${header}.${payload}`), + ); + const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey); + return { + token: `${header}.${payload}.${Buffer.from(signature).toString("base64url")}`, + jwk: { ...publicJwk, kid, kty: "RSA" }, + }; +} + +describe("base worker exchange media-type defense", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("rejects a misleading media type instead of parsing it as application/json", async () => { + const { token, jwk } = await createSignedJwt(); + const requests: string[] = []; + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + requests.push(url); + 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 new Response("unexpected GitHub call", { status: 500 }); + }); + + const response = await worker.fetch(new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "text/application/json", + }, + body: JSON.stringify({ + target_repository: { owner: "ContextualWisdomLab", repo: "noema" }, + }), + }), env); + + expect(response.status).toBe(415); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_VALIDATION_INPUT", + }); + expect(requests.filter((url) => url.includes("api.github.com"))).toHaveLength(0); + }); +}); From 66982b45b64417ce4d12d3272f677335ff2dffe4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 20:07:07 -0700 Subject: [PATCH 431/564] fix(exchange): enforce reviewed JSON media type in base worker --- src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 685d1800b..dcec45af3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -892,8 +892,11 @@ async function createInstallationToken(repository: string, env: Env): Promise { + if (request.body === null) return {}; const contentType = request.headers.get("content-type") || ""; - if (!contentType.toLowerCase().includes("application/json")) return {}; + if (!oidcJsonMediaTypePattern.test(contentType)) { + throw new ApiError("ERR_VALIDATION_INPUT", 415, "Exchange request body requires application/json"); + } let body: unknown; try { body = await request.json(); From 3a22b650b10a59b7a89701e3d7780b3032c13cac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 20:10:21 -0700 Subject: [PATCH 432/564] test(exchange): align helper coverage with reviewed media type --- test/credential-request-helper-coverage.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/credential-request-helper-coverage.test.ts b/test/credential-request-helper-coverage.test.ts index b6cc9a602..14a013935 100644 --- a/test/credential-request-helper-coverage.test.ts +++ b/test/credential-request-helper-coverage.test.ts @@ -181,7 +181,7 @@ describe("credential request helper coverage through the public worker", () => { ).toHaveLength(0); }); - it("treats a non-JSON body as empty before repository syntax validation", async () => { + it("rejects a non-JSON body before repository syntax validation", async () => { const { token, jwk } = await createSignedJwt("invalid-repository-name"); const upstream = mockOidcDiscovery(jwk); @@ -198,11 +198,11 @@ describe("credential request helper coverage through the public worker", () => { env, ); - expect(response.status).toBe(400); + expect(response.status).toBe(415); await expect(response.json()).resolves.toMatchObject({ ok: false, error_code: "ERR_VALIDATION_INPUT", - message: "target_repository is not a valid owner/name repository", + message: "Exchange request body requires application/json", }); expect( upstream.mock.calls.filter(([input]) => String(input).startsWith("https://api.github.com/")), From 07e097111563cc0f97ab2b0dd53a02199c830d6c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 20:12:53 -0700 Subject: [PATCH 433/564] test(exchange): cover missing media type with request body --- ...worker-exchange-media-type-defense.test.ts | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/test/worker-exchange-media-type-defense.test.ts b/test/worker-exchange-media-type-defense.test.ts index 96538f235..d39f73ff9 100644 --- a/test/worker-exchange-media-type-defense.test.ts +++ b/test/worker-exchange-media-type-defense.test.ts @@ -58,6 +58,22 @@ async function createSignedJwt() { }; } +function mockOidc(jwk: JsonWebKey & { kid: string; kty: string }) { + const requests: string[] = []; + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + requests.push(url); + 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 new Response("unexpected GitHub call", { status: 500 }); + }); + return requests; +} + describe("base worker exchange media-type defense", () => { afterEach(() => { vi.restoreAllMocks(); @@ -65,18 +81,7 @@ describe("base worker exchange media-type defense", () => { it("rejects a misleading media type instead of parsing it as application/json", async () => { const { token, jwk } = await createSignedJwt(); - const requests: string[] = []; - vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { - const url = String(input); - requests.push(url); - 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 new Response("unexpected GitHub call", { status: 500 }); - }); + const requests = mockOidc(jwk); const response = await worker.fetch(new Request("https://noema.example/exchange", { method: "POST", @@ -96,4 +101,25 @@ describe("base worker exchange media-type defense", () => { }); expect(requests.filter((url) => url.includes("api.github.com"))).toHaveLength(0); }); + + it("rejects a body-bearing request whose media type is missing", async () => { + const { token, jwk } = await createSignedJwt(); + const requests = mockOidc(jwk); + + const response = await worker.fetch(new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), env); + + expect(response.status).toBe(415); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_VALIDATION_INPUT", + message: "Exchange request body requires application/json", + }); + expect(requests.filter((url) => url.includes("api.github.com"))).toHaveLength(0); + }); }); From 91e72951c739a40d17f8474fd43318837b30c5b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 20:20:12 -0700 Subject: [PATCH 434/564] test(exchange): exercise truly missing media type --- test/worker-exchange-media-type-defense.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/worker-exchange-media-type-defense.test.ts b/test/worker-exchange-media-type-defense.test.ts index d39f73ff9..7445607dc 100644 --- a/test/worker-exchange-media-type-defense.test.ts +++ b/test/worker-exchange-media-type-defense.test.ts @@ -111,7 +111,9 @@ describe("base worker exchange media-type defense", () => { headers: { authorization: `Bearer ${token}`, }, - body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + body: new TextEncoder().encode(JSON.stringify({ + target_repository: "ContextualWisdomLab/noema", + })), }), env); expect(response.status).toBe(415); From 895cd6787e8d6054c359f50117a39626c7d549b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:00:26 -0700 Subject: [PATCH 435/564] test(exchange): accept GitHub App PKCS#1 private key --- test/github-app-private-key-envelope.test.ts | 72 +++++++++++++++++++- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/test/github-app-private-key-envelope.test.ts b/test/github-app-private-key-envelope.test.ts index 355937bd4..68226cfbd 100644 --- a/test/github-app-private-key-envelope.test.ts +++ b/test/github-app-private-key-envelope.test.ts @@ -1,3 +1,4 @@ +import { createPrivateKey } from "node:crypto"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import worker, { type Env } from "../src/index"; @@ -8,6 +9,7 @@ const configuredWorkflowSha = "a".repeat(40); let oidcKeyPair: CryptoKeyPair; let oidcPublicJwk: JsonWebKey; let appPrivateKeyPem: string; +let appPrivateKeyPkcs1Pem: string; function encodeSegment(value: unknown): string { return Buffer.from(JSON.stringify(value)).toString("base64url"); @@ -40,9 +42,13 @@ 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), - ); + const appPrivateKeyPkcs8 = await crypto.subtle.exportKey("pkcs8", appKeyPair.privateKey); + appPrivateKeyPem = pemFromPkcs8(appPrivateKeyPkcs8); + appPrivateKeyPkcs1Pem = createPrivateKey({ + key: Buffer.from(appPrivateKeyPkcs8), + format: "der", + type: "pkcs8", + }).export({ format: "pem", type: "pkcs1" }).toString(); }); afterEach(() => { @@ -139,4 +145,64 @@ describe("GitHub App private-key authority", () => { }); expect(githubCredentialCalls).toBe(0); }); + + it("accepts the PKCS#1 RSA private key format downloaded from GitHub Apps", async () => { + const { token, jwk } = await signedOidcToken(); + 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: configuredRef, + ALLOWED_WORKFLOW_SHA: configuredWorkflowSha, + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKeyPkcs1Pem, + GITHUB_APP_INSTALLATION_ID: "92345", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", + }; + let githubCredentialCalls = 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] }); + } + githubCredentialCalls += 1; + if (url === "https://api.github.com/app/installations/92345/access_tokens") { + return Response.json({ + token: "ghs_pkcs1_supported", + expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), + }, { status: 201 }); + } + 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.253", + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + env, + ); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + ok: true, + data: { + token: "ghs_pkcs1_supported", + repository: "ContextualWisdomLab/noema", + }, + }); + expect(githubCredentialCalls).toBe(1); + }); }); From 51df0247885505f98e7f04f953330c53bb70abc5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:04:40 -0700 Subject: [PATCH 436/564] fix(exchange): normalize GitHub App PKCS#1 private keys --- src/github-app-private-key.ts | 97 +++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/github-app-private-key.ts diff --git a/src/github-app-private-key.ts b/src/github-app-private-key.ts new file mode 100644 index 000000000..1e4c960f7 --- /dev/null +++ b/src/github-app-private-key.ts @@ -0,0 +1,97 @@ +const pkcs8PemPattern = /^-----BEGIN PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END PRIVATE KEY-----\r?\n?$/; +const pkcs1PemPattern = /^-----BEGIN RSA PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END RSA PRIVATE KEY-----\r?\n?$/; +const MAX_PRIVATE_KEY_PEM_BYTES = 65_536; +const MIN_GITHUB_RSA_PRIVATE_KEY_DER_BYTES = 256; +const MAX_PKCS1_DER_BYTES = 65_000; +const rsaEncryptionAlgorithmIdentifier = Uint8Array.of( + 0x30, 0x0d, + 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, + 0x05, 0x00, +); +const pkcs8VersionZero = Uint8Array.of(0x02, 0x01, 0x00); + +function canonicalPemDer(encodedBody: string): Uint8Array | undefined { + const compact = encodedBody.replace(/\r?\n/g, ""); + if (compact.length === 0) return undefined; + try { + const binary = atob(compact); + if (btoa(binary) !== compact) return undefined; + return Uint8Array.from(binary, (character) => character.charCodeAt(0)); + } catch { + return undefined; + } +} + +function twoByteDerLength(length: number): Uint8Array { + return Uint8Array.of(0x82, (length >>> 8) & 0xff, length & 0xff); +} + +function concatBytes(...parts: Uint8Array[]): Uint8Array { + const result = new Uint8Array(parts.reduce((total, part) => total + part.length, 0)); + let offset = 0; + for (const part of parts) { + result.set(part, offset); + offset += part.length; + } + return result; +} + +function wrapPkcs1AsPkcs8(pkcs1Der: Uint8Array): Uint8Array | undefined { + if ( + pkcs1Der.length < MIN_GITHUB_RSA_PRIVATE_KEY_DER_BYTES + || pkcs1Der.length > MAX_PKCS1_DER_BYTES + ) { + return undefined; + } + const privateKeyOctetString = concatBytes( + Uint8Array.of(0x04), + twoByteDerLength(pkcs1Der.length), + pkcs1Der, + ); + const privateKeyInfoBody = concatBytes( + pkcs8VersionZero, + rsaEncryptionAlgorithmIdentifier, + privateKeyOctetString, + ); + return concatBytes( + Uint8Array.of(0x30), + twoByteDerLength(privateKeyInfoBody.length), + privateKeyInfoBody, + ); +} + +function pemFromDer(label: "PRIVATE KEY", der: Uint8Array): string { + let binary = ""; + for (const byte of der) binary += String.fromCharCode(byte); + const base64 = btoa(binary); + const lines = base64.match(/.{1,64}/g)?.join("\n") ?? base64; + return `-----BEGIN ${label}-----\n${lines}\n-----END ${label}-----`; +} + +/** + * Converts the PKCS#1 RSA private-key PEM downloaded from GitHub Apps into the PKCS#8 + * envelope consumed by WebCrypto while preserving an already-reviewed PKCS#8 envelope. + * Unknown labels, oversized inputs, non-canonical base64, and implausibly small/large + * PKCS#1 payloads are rejected rather than normalized into credential authority. + * + * @param value GitHub App private-key PEM supplied by the Worker secret binding. + * @returns A PKCS#8 `PRIVATE KEY` PEM suitable for WebCrypto, or `undefined` when the + * credential envelope cannot be safely interpreted. + */ +export function normalizeGitHubAppPrivateKeyPem(value: string | undefined): string | undefined { + if (value === undefined || new TextEncoder().encode(value).byteLength > MAX_PRIVATE_KEY_PEM_BYTES) { + return undefined; + } + + const pkcs8Match = pkcs8PemPattern.exec(value); + if (pkcs8Match) { + return canonicalPemDer(pkcs8Match[1]) === undefined ? undefined : value; + } + + const pkcs1Match = pkcs1PemPattern.exec(value); + if (!pkcs1Match) return undefined; + const pkcs1Der = canonicalPemDer(pkcs1Match[1]); + if (!pkcs1Der) return undefined; + const pkcs8Der = wrapPkcs1AsPkcs8(pkcs1Der); + return pkcs8Der ? pemFromDer("PRIVATE KEY", pkcs8Der) : undefined; +} From dba837bbb5f38488f8595973f76f37af09a11ff0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:05:35 -0700 Subject: [PATCH 437/564] fix(runtime): accept GitHub App PKCS#1 private keys --- src/runtime-entrypoint.ts | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index 7fff820f2..54299d8cb 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -3,6 +3,7 @@ import entrypoint, { NoemaRateLimiter, type Env as BaseEnv, } from "./entrypoint"; +import { normalizeGitHubAppPrivateKeyPem } from "./github-app-private-key"; import { evaluateRuntimeReadiness } from "./runtime-readiness"; export { NoemaOidcReplayGuard, NoemaRateLimiter }; @@ -59,6 +60,20 @@ function credentialFetchCapabilityAvailable(): boolean { && typeof descriptor.value === "function"; } +function runtimeCredentialEnv(env: Env): Env { + const normalizedPrivateKey = normalizeGitHubAppPrivateKeyPem(env.GITHUB_APP_PRIVATE_KEY_PEM); + if ( + normalizedPrivateKey === undefined + || normalizedPrivateKey === env.GITHUB_APP_PRIVATE_KEY_PEM + ) { + return env; + } + return { + ...env, + GITHUB_APP_PRIVATE_KEY_PEM: normalizedPrivateKey, + }; +} + function readinessHeaders( traceId: string, latencyMs: number, @@ -143,20 +158,23 @@ async function runtimeReadinessResponse(request: Request, env: Env): Promise { const boundedRequest = canonicalTraceRequest(request); + const runtimeEnv = runtimeCredentialEnv(env); const url = new URL(boundedRequest.url); if (url.pathname === "/ready") { - return runtimeReadinessResponse(boundedRequest, env); + return runtimeReadinessResponse(boundedRequest, runtimeEnv); } - return entrypoint.fetch(boundedRequest, env); + return entrypoint.fetch(boundedRequest, runtimeEnv); }, }; From 53206e02f712c359075cdf0d0a8b8d47a22a7842 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:06:10 -0700 Subject: [PATCH 438/564] test(runtime): verify GitHub App PKCS#1 readiness --- test/github-app-private-key-envelope.test.ts | 90 +++++++++----------- 1 file changed, 40 insertions(+), 50 deletions(-) diff --git a/test/github-app-private-key-envelope.test.ts b/test/github-app-private-key-envelope.test.ts index 68226cfbd..6dc6fe6ea 100644 --- a/test/github-app-private-key-envelope.test.ts +++ b/test/github-app-private-key-envelope.test.ts @@ -1,6 +1,8 @@ import { createPrivateKey } from "node:crypto"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import worker, { type Env } from "../src/index"; +import { normalizeGitHubAppPrivateKeyPem } from "../src/github-app-private-key"; +import runtimeWorker, { type Env as RuntimeEnv } from "../src/runtime-entrypoint"; const configuredRef = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; @@ -84,6 +86,27 @@ async function signedOidcToken(): Promise<{ token: string; jwk: JsonWebKey }> { }; } +function readinessEnv(privateKeyPem: string): RuntimeEnv { + const namespace = { + idFromName: vi.fn(), + get: vi.fn(), + } as unknown as DurableObjectNamespace; + 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: privateKeyPem, + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", + NOEMA_RATE_LIMITER: namespace, + NOEMA_OIDC_REPLAY_GUARD: namespace, + }; +} + describe("GitHub App private-key authority", () => { it("rejects a valid PKCS#8 key under a non-canonical PEM label before GitHub credential egress", async () => { const { token, jwk } = await signedOidcToken(); @@ -146,63 +169,30 @@ describe("GitHub App private-key authority", () => { expect(githubCredentialCalls).toBe(0); }); - it("accepts the PKCS#1 RSA private key format downloaded from GitHub Apps", async () => { - const { token, jwk } = await signedOidcToken(); - 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: configuredRef, - ALLOWED_WORKFLOW_SHA: configuredWorkflowSha, - GITHUB_API_BASE: "https://api.github.com", - GITHUB_APP_ID: "1", - GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKeyPkcs1Pem, - GITHUB_APP_INSTALLATION_ID: "92345", - NOEMA_RATE_LIMIT_PER_MINUTE: "1000", - }; - let githubCredentialCalls = 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] }); - } - githubCredentialCalls += 1; - if (url === "https://api.github.com/app/installations/92345/access_tokens") { - return Response.json({ - token: "ghs_pkcs1_supported", - expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), - }, { status: 201 }); - } - 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.253", - }, - body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), - }), - env, + it("makes the production runtime ready with the PKCS#1 RSA key format downloaded from GitHub Apps", async () => { + const response = await runtimeWorker.fetch( + new Request("https://noema.example/ready"), + readinessEnv(appPrivateKeyPkcs1Pem), ); expect(response.status).toBe(200); await expect(response.json()).resolves.toMatchObject({ ok: true, data: { - token: "ghs_pkcs1_supported", - repository: "ContextualWisdomLab/noema", + name: "noema", + status: "ready", }, }); - expect(githubCredentialCalls).toBe(1); + }); + + it("preserves a canonical PKCS#8 key and rejects malformed credential envelopes", () => { + expect(normalizeGitHubAppPrivateKeyPem(appPrivateKeyPem)).toBe(appPrivateKeyPem); + expect(normalizeGitHubAppPrivateKeyPem(undefined)).toBeUndefined(); + expect(normalizeGitHubAppPrivateKeyPem("-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----")) + .toBeUndefined(); + expect(normalizeGitHubAppPrivateKeyPem( + "-----BEGIN RSA PRIVATE KEY-----\nAAAA=\n-----END RSA PRIVATE KEY-----", + )).toBeUndefined(); + expect(normalizeGitHubAppPrivateKeyPem("x".repeat(65_537))).toBeUndefined(); }); }); From 89580bb1996f9e4f9765d90c811b178b28ccd76b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:11:59 -0700 Subject: [PATCH 439/564] fix(runtime): remove unreachable private-key branches --- src/github-app-private-key.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/github-app-private-key.ts b/src/github-app-private-key.ts index 1e4c960f7..669120ad3 100644 --- a/src/github-app-private-key.ts +++ b/src/github-app-private-key.ts @@ -2,7 +2,6 @@ const pkcs8PemPattern = /^-----BEGIN PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\ const pkcs1PemPattern = /^-----BEGIN RSA PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END RSA PRIVATE KEY-----\r?\n?$/; const MAX_PRIVATE_KEY_PEM_BYTES = 65_536; const MIN_GITHUB_RSA_PRIVATE_KEY_DER_BYTES = 256; -const MAX_PKCS1_DER_BYTES = 65_000; const rsaEncryptionAlgorithmIdentifier = Uint8Array.of( 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, @@ -12,7 +11,6 @@ const pkcs8VersionZero = Uint8Array.of(0x02, 0x01, 0x00); function canonicalPemDer(encodedBody: string): Uint8Array | undefined { const compact = encodedBody.replace(/\r?\n/g, ""); - if (compact.length === 0) return undefined; try { const binary = atob(compact); if (btoa(binary) !== compact) return undefined; @@ -37,12 +35,7 @@ function concatBytes(...parts: Uint8Array[]): Uint8Array { } function wrapPkcs1AsPkcs8(pkcs1Der: Uint8Array): Uint8Array | undefined { - if ( - pkcs1Der.length < MIN_GITHUB_RSA_PRIVATE_KEY_DER_BYTES - || pkcs1Der.length > MAX_PKCS1_DER_BYTES - ) { - return undefined; - } + if (pkcs1Der.length < MIN_GITHUB_RSA_PRIVATE_KEY_DER_BYTES) return undefined; const privateKeyOctetString = concatBytes( Uint8Array.of(0x04), twoByteDerLength(pkcs1Der.length), @@ -64,15 +57,15 @@ function pemFromDer(label: "PRIVATE KEY", der: Uint8Array): string { let binary = ""; for (const byte of der) binary += String.fromCharCode(byte); const base64 = btoa(binary); - const lines = base64.match(/.{1,64}/g)?.join("\n") ?? base64; + const lines = base64.match(/.{1,64}/g)!.join("\n"); return `-----BEGIN ${label}-----\n${lines}\n-----END ${label}-----`; } /** * Converts the PKCS#1 RSA private-key PEM downloaded from GitHub Apps into the PKCS#8 * envelope consumed by WebCrypto while preserving an already-reviewed PKCS#8 envelope. - * Unknown labels, oversized inputs, non-canonical base64, and implausibly small/large - * PKCS#1 payloads are rejected rather than normalized into credential authority. + * Unknown labels, oversized inputs, non-canonical base64, and implausibly small PKCS#1 + * payloads are rejected rather than normalized into credential authority. * * @param value GitHub App private-key PEM supplied by the Worker secret binding. * @returns A PKCS#8 `PRIVATE KEY` PEM suitable for WebCrypto, or `undefined` when the From 48d0f402db59c8bebc0c87148af7af1cac23c180 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:12:33 -0700 Subject: [PATCH 440/564] test(runtime): cover private-key envelope rejection paths --- test/github-app-private-key-envelope.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/github-app-private-key-envelope.test.ts b/test/github-app-private-key-envelope.test.ts index 6dc6fe6ea..34203fe23 100644 --- a/test/github-app-private-key-envelope.test.ts +++ b/test/github-app-private-key-envelope.test.ts @@ -190,9 +190,15 @@ describe("GitHub App private-key authority", () => { expect(normalizeGitHubAppPrivateKeyPem(undefined)).toBeUndefined(); expect(normalizeGitHubAppPrivateKeyPem("-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----")) .toBeUndefined(); + expect(normalizeGitHubAppPrivateKeyPem( + "-----BEGIN PRIVATE KEY-----\nAAA\n-----END PRIVATE KEY-----", + )).toBeUndefined(); expect(normalizeGitHubAppPrivateKeyPem( "-----BEGIN RSA PRIVATE KEY-----\nAAAA=\n-----END RSA PRIVATE KEY-----", )).toBeUndefined(); + expect(normalizeGitHubAppPrivateKeyPem( + "-----BEGIN RSA PRIVATE KEY-----\nAAAA\n-----END RSA PRIVATE KEY-----", + )).toBeUndefined(); expect(normalizeGitHubAppPrivateKeyPem("x".repeat(65_537))).toBeUndefined(); }); }); From c12dc705c2f348f3f45d697e5f550d6e0a97c887 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 22:01:55 -0700 Subject: [PATCH 441/564] test(runtime): accept canonical PKCS8 terminal newline --- test/github-app-private-key-envelope.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/github-app-private-key-envelope.test.ts b/test/github-app-private-key-envelope.test.ts index 34203fe23..80263c5d8 100644 --- a/test/github-app-private-key-envelope.test.ts +++ b/test/github-app-private-key-envelope.test.ts @@ -185,6 +185,22 @@ describe("GitHub App private-key authority", () => { }); }); + it("makes the production runtime ready with a canonical PKCS#8 key ending in one newline", async () => { + const response = await runtimeWorker.fetch( + new Request("https://noema.example/ready"), + readinessEnv(`${appPrivateKeyPem}\n`), + ); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + ok: true, + data: { + name: "noema", + status: "ready", + }, + }); + }); + it("preserves a canonical PKCS#8 key and rejects malformed credential envelopes", () => { expect(normalizeGitHubAppPrivateKeyPem(appPrivateKeyPem)).toBe(appPrivateKeyPem); expect(normalizeGitHubAppPrivateKeyPem(undefined)).toBeUndefined(); From de79d5709d2868dba5521692ff66e1f9d8abb7a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 22:02:51 -0700 Subject: [PATCH 442/564] fix(runtime): canonicalize PKCS8 terminal newline --- src/github-app-private-key.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/github-app-private-key.ts b/src/github-app-private-key.ts index 669120ad3..36e1c5f9c 100644 --- a/src/github-app-private-key.ts +++ b/src/github-app-private-key.ts @@ -65,7 +65,9 @@ function pemFromDer(label: "PRIVATE KEY", der: Uint8Array): string { * Converts the PKCS#1 RSA private-key PEM downloaded from GitHub Apps into the PKCS#8 * envelope consumed by WebCrypto while preserving an already-reviewed PKCS#8 envelope. * Unknown labels, oversized inputs, non-canonical base64, and implausibly small PKCS#1 - * payloads are rejected rather than normalized into credential authority. + * payloads are rejected rather than normalized into credential authority. One conventional + * terminal PEM newline is removed so the accepted outer envelope matches the downstream + * WebCrypto import boundary without changing the DER key identity. * * @param value GitHub App private-key PEM supplied by the Worker secret binding. * @returns A PKCS#8 `PRIVATE KEY` PEM suitable for WebCrypto, or `undefined` when the @@ -78,7 +80,7 @@ export function normalizeGitHubAppPrivateKeyPem(value: string | undefined): stri const pkcs8Match = pkcs8PemPattern.exec(value); if (pkcs8Match) { - return canonicalPemDer(pkcs8Match[1]) === undefined ? undefined : value; + return canonicalPemDer(pkcs8Match[1]) === undefined ? undefined : value.replace(/\r?\n$/, ""); } const pkcs1Match = pkcs1PemPattern.exec(value); From 9900a6cdbae1ab107349b8e4922c9cd6456d7d85 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 23:06:43 -0700 Subject: [PATCH 443/564] test(runtime): preserve private-key readiness cache after normalization --- ...me-private-key-normalization-cache.test.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 test/runtime-private-key-normalization-cache.test.ts diff --git a/test/runtime-private-key-normalization-cache.test.ts b/test/runtime-private-key-normalization-cache.test.ts new file mode 100644 index 000000000..d143a8e96 --- /dev/null +++ b/test/runtime-private-key-normalization-cache.test.ts @@ -0,0 +1,67 @@ +import { beforeAll, describe, expect, it, vi } from "vitest"; +import runtimeWorker, { type Env as RuntimeEnv } from "../src/runtime-entrypoint"; + +let appPrivateKeyPem: string; + +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-----`; +} + +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"], + ); + const pkcs8 = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey); + appPrivateKeyPem = pemFromPkcs8(pkcs8); +}); + +function readinessEnv(privateKeyPem: string): RuntimeEnv { + const namespace = { + idFromName: vi.fn(), + get: vi.fn(), + } as unknown as DurableObjectNamespace; + 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: "a".repeat(40), + GITHUB_API_BASE: "https://api.github.com", + GITHUB_APP_ID: "1", + GITHUB_APP_PRIVATE_KEY_PEM: privateKeyPem, + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", + NOEMA_RATE_LIMITER: namespace, + NOEMA_OIDC_REPLAY_GUARD: namespace, + }; +} + +describe("runtime private-key normalization cache", () => { + it("reuses the WebCrypto import decision for an unchanged newline-terminated secret", async () => { + const env = readinessEnv(`${appPrivateKeyPem}\n`); + const importSpy = vi.spyOn(crypto.subtle, "importKey"); + + const first = await runtimeWorker.fetch( + new Request("https://noema.example/ready"), + env, + ); + const second = await runtimeWorker.fetch( + new Request("https://noema.example/ready"), + env, + ); + + expect(first.status).toBe(200); + expect(second.status).toBe(200); + expect(importSpy).toHaveBeenCalledTimes(1); + }); +}); From 5abf2966a34e9e1a8904ea8e2ee5f20ba0f8efa6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 23:07:53 -0700 Subject: [PATCH 444/564] fix(runtime): preserve readiness cache across key normalization --- src/runtime-entrypoint.ts | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index 54299d8cb..210fc7d1c 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -22,6 +22,14 @@ const canonicalTraceHeaderPattern = /^[A-Za-z0-9._:-]+$/; const maxTraceHeaderLength = 128; const traceHeaderNames = ["x-request-id", "x-correlation-id"] as const; +interface RuntimeCredentialEnvCacheEntry { + sourcePrivateKey: string; + normalizedPrivateKey: string; + runtimeEnv: Env; +} + +const runtimeCredentialEnvCache = new WeakMap(); + function canonicalTraceRequest(request: Request): Request { let headers: Headers | undefined; for (const name of traceHeaderNames) { @@ -61,17 +69,36 @@ function credentialFetchCapabilityAvailable(): boolean { } function runtimeCredentialEnv(env: Env): Env { - const normalizedPrivateKey = normalizeGitHubAppPrivateKeyPem(env.GITHUB_APP_PRIVATE_KEY_PEM); + const sourcePrivateKey = env.GITHUB_APP_PRIVATE_KEY_PEM; + const normalizedPrivateKey = normalizeGitHubAppPrivateKeyPem(sourcePrivateKey); if ( normalizedPrivateKey === undefined - || normalizedPrivateKey === env.GITHUB_APP_PRIVATE_KEY_PEM + || normalizedPrivateKey === sourcePrivateKey ) { return env; } - return { + + const cached = runtimeCredentialEnvCache.get(env); + if ( + cached + && cached.sourcePrivateKey === sourcePrivateKey + && cached.normalizedPrivateKey === normalizedPrivateKey + ) { + Object.assign(cached.runtimeEnv, env); + cached.runtimeEnv.GITHUB_APP_PRIVATE_KEY_PEM = normalizedPrivateKey; + return cached.runtimeEnv; + } + + const runtimeEnv = { ...env, GITHUB_APP_PRIVATE_KEY_PEM: normalizedPrivateKey, }; + runtimeCredentialEnvCache.set(env, { + sourcePrivateKey, + normalizedPrivateKey, + runtimeEnv, + }); + return runtimeEnv; } function readinessHeaders( From 106dcb988317bd4adbed47e47008953f6bfa0d96 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 23:09:32 -0700 Subject: [PATCH 445/564] fix(runtime): type normalized credential cache source safely --- src/runtime-entrypoint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index 210fc7d1c..9722b52e9 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -23,7 +23,7 @@ const maxTraceHeaderLength = 128; const traceHeaderNames = ["x-request-id", "x-correlation-id"] as const; interface RuntimeCredentialEnvCacheEntry { - sourcePrivateKey: string; + sourcePrivateKey: string | undefined; normalizedPrivateKey: string; runtimeEnv: Env; } From 5aba3b553a7e86b6d51d8be1c94dafbd0bec2ac4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 23:33:28 -0700 Subject: [PATCH 446/564] test(runtime): reject stale cached binding authority --- ...me-private-key-normalization-cache.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/runtime-private-key-normalization-cache.test.ts b/test/runtime-private-key-normalization-cache.test.ts index d143a8e96..c85a8dc75 100644 --- a/test/runtime-private-key-normalization-cache.test.ts +++ b/test/runtime-private-key-normalization-cache.test.ts @@ -64,4 +64,29 @@ describe("runtime private-key normalization cache", () => { expect(second.status).toBe(200); expect(importSpy).toHaveBeenCalledTimes(1); }); + + it("does not retain a withdrawn binding in the cached normalized environment", async () => { + const env = readinessEnv(`${appPrivateKeyPem}\n`); + + const first = await runtimeWorker.fetch( + new Request("https://noema.example/ready"), + env, + ); + expect(first.status).toBe(200); + + delete (env as Partial).GITHUB_APP_ID; + + const second = await runtimeWorker.fetch( + new Request("https://noema.example/ready"), + env, + ); + expect(second.status).toBe(503); + expect(await second.json()).toMatchObject({ + ok: false, + error_code: "ERR_SERVICE_NOT_READY", + details: { + failed_checks: expect.stringContaining("github_app_id"), + }, + }); + }); }); From 14dcc58aa2bd710c68f005eb66f1012de6a2d916 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 23:33:58 -0700 Subject: [PATCH 447/564] fix(runtime): drop withdrawn cached bindings --- src/runtime-entrypoint.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index 9722b52e9..44c873126 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -68,6 +68,23 @@ function credentialFetchCapabilityAvailable(): boolean { && typeof descriptor.value === "function"; } +function synchronizeRuntimeCredentialEnv( + runtimeEnv: Env, + env: Env, + normalizedPrivateKey: string, +): void { + for (const key of Object.keys(runtimeEnv)) { + if ( + key !== "GITHUB_APP_PRIVATE_KEY_PEM" + && !Object.prototype.hasOwnProperty.call(env, key) + ) { + delete (runtimeEnv as unknown as Record)[key]; + } + } + Object.assign(runtimeEnv, env); + runtimeEnv.GITHUB_APP_PRIVATE_KEY_PEM = normalizedPrivateKey; +} + function runtimeCredentialEnv(env: Env): Env { const sourcePrivateKey = env.GITHUB_APP_PRIVATE_KEY_PEM; const normalizedPrivateKey = normalizeGitHubAppPrivateKeyPem(sourcePrivateKey); @@ -84,8 +101,7 @@ function runtimeCredentialEnv(env: Env): Env { && cached.sourcePrivateKey === sourcePrivateKey && cached.normalizedPrivateKey === normalizedPrivateKey ) { - Object.assign(cached.runtimeEnv, env); - cached.runtimeEnv.GITHUB_APP_PRIVATE_KEY_PEM = normalizedPrivateKey; + synchronizeRuntimeCredentialEnv(cached.runtimeEnv, env, normalizedPrivateKey); return cached.runtimeEnv; } From 6324905b9db713fcb42aad2ee827deedf7611683 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:04:10 -0700 Subject: [PATCH 448/564] test(oidc): reject encryption-use JWKS keys --- test/oidc-jwks-key-use-authority.test.ts | 123 +++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 test/oidc-jwks-key-use-authority.test.ts diff --git a/test/oidc-jwks-key-use-authority.test.ts b/test/oidc-jwks-key-use-authority.test.ts new file mode 100644 index 000000000..9f5e730d8 --- /dev/null +++ b/test/oidc-jwks-key-use-authority.test.ts @@ -0,0 +1,123 @@ +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-jwks-key-use-authority"; + +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 encodeSegment(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString("base64url"); +} + +async function signedJwt(): Promise { + const now = Math.floor(Date.now() / 1000); + const header = encodeSegment({ alg: "RS256", kid: signingKid }); + const payload = encodeSegment({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + 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 signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + signingPrivateKey, + new TextEncoder().encode(`${header}.${payload}`), + ); + return `${header}.${payload}.${Buffer.from(signature).toString("base64url")}`; +} + +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 JWKS key use authority", () => { + it("rejects an RSA verification key whose JWK declares encryption use", async () => { + const token = await signedJwt(); + vi.resetModules(); + const { default: worker } = await import("../src/index"); + const fetchSpy = 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", + use: "enc", + alg: "RS256", + key_ops: ["verify"], + }], + }); + } + return new Response("unexpected privileged 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.238", + }, + body: JSON.stringify({ target_repository: 42 }), + }), + env, + ); + + expect(response.status).toBe(502); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_OIDC_VERIFICATION", + message: "GitHub OIDC JWKS did not include valid key entries", + }); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); +}); From 8045096771e9e0b149075ea99d8c2ca13c3ef2ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:04:56 -0700 Subject: [PATCH 449/564] test(trust): require current central workflow source --- test/trusted-workflow-source-rollforward.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/trusted-workflow-source-rollforward.test.ts b/test/trusted-workflow-source-rollforward.test.ts index 2cbb80c77..c30e0d646 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 = - "e1b03eebc6dc5c85aed393e5928927c96376cf46"; + "e03f473723dbcc15080fd580f46c0f8d409c939d"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { @@ -12,4 +12,4 @@ describe("trusted central workflow source revision", () => { `ALLOWED_WORKFLOW_SHA = "${auditedCentralWorkflowSourceSha}"`, ); }); -}); \ No newline at end of file +}); From efad421026359b030654649f94b7bfa228fb3ac5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:07:11 -0700 Subject: [PATCH 450/564] fix(oidc): honor JWKS signing-use authority --- src/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index dcec45af3..5244e77d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -558,8 +558,11 @@ function uniqueRsaSigningKey(jwks: JsonWebKeySet, kid: string): (JsonWebKey & { } const match = matches[0]; if ( - match?.key_ops !== undefined - && (match.key_ops.length !== 1 || match.key_ops[0] !== "verify") + (match?.use !== undefined && match.use !== "sig") + || ( + match?.key_ops !== undefined + && (match.key_ops.length !== 1 || match.key_ops[0] !== "verify") + ) ) { throw new ApiError( "ERR_OIDC_VERIFICATION", From 311c76e26179a6f0511faecc9bfe715b02d04f2a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:07:28 -0700 Subject: [PATCH 451/564] fix(trust): roll forward audited central source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 419db2d2f..c3df75117 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 = "e1b03eebc6dc5c85aed393e5928927c96376cf46" +ALLOWED_WORKFLOW_SHA = "e03f473723dbcc15080fd580f46c0f8d409c939d" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From a878de0fd9b512111d510c93e26b97180dfd278e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:08:45 -0700 Subject: [PATCH 452/564] docs(architecture): record audited central trust roll-forward --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ed128a6bf..93bf6819b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs readiness dispatch and delegates `/exchange`; `src/entrypoint.ts` applies the distributed rate limiter before `src/worker.ts` performs its denial-only exact workflow-ref precheck. `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the current audited protected central `.github` commit `e1b03eebc6dc5c85aed393e5928927c96376cf46`. The immediate audited predecessor was `e5e0d3652a19d12db29e687924dbaf978cd78912`; protected central advanced through its independently owned #1379 integration while `.github/workflows/noema-review.yml` retained the same blob identity `5c60782adb8be11d6538caea269a4bdfab7ab4d1`. This remains an explicit repository-commit trust decision rather than a claim that future central revisions are equivalent. GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every later central commit movement still requires a new explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema; known central consumer/scanner defects remain owned and repaired there rather than by weakening Noema verification. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the current audited protected central `.github` commit `e03f473723dbcc15080fd580f46c0f8d409c939d`. The immediate audited predecessor was `e1b03eebc6dc5c85aed393e5928927c96376cf46`; protected central advanced through independently owned governance integration while `.github/workflows/noema-review.yml` retained the same blob identity `5c60782adb8be11d6538caea269a4bdfab7ab4d1`. This remains an explicit repository-commit trust decision rather than a claim that future central revisions are equivalent. GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every later central commit movement still requires a new explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema; known central consumer/scanner defects remain owned and repaired there rather than by weakening Noema verification. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The protected workflow-ref parser and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From eb282128a75669168ae582d4ddbe2ef868074759 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:11:03 -0700 Subject: [PATCH 453/564] test(trust): require latest 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 c30e0d646..71e91fd5f 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 = - "e03f473723dbcc15080fd580f46c0f8d409c939d"; + "3a7941aa92de00b8b39fd11cbe7bf3da2fbbeddc"; describe("trusted central workflow source revision", () => { it("binds the deployed OIDC trust configuration to the audited central source commit", () => { From d307e4a5ab49c49abd1d584b087d8205964b7e64 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:11:17 -0700 Subject: [PATCH 454/564] fix(trust): follow current protected central source --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index c3df75117..61b5d3068 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 = "e03f473723dbcc15080fd580f46c0f8d409c939d" +ALLOWED_WORKFLOW_SHA = "3a7941aa92de00b8b39fd11cbe7bf3da2fbbeddc" GITHUB_API_BASE = "https://api.github.com" GITHUB_APP_SLUG = "noema" NOEMA_RATE_LIMIT_PER_MINUTE = "60" From 6fe3e76aae0c0df118c3b7e2e9c9b57c3358336e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:11:55 -0700 Subject: [PATCH 455/564] docs(architecture): track latest central trust authority --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 93bf6819b..90875ba4a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,7 +28,7 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi This revision exposes both `ALLOWED_WORKFLOW_REF_PREFIX` and `ALLOWED_WORKFLOW_SHA`. Despite the legacy ref-binding name, `src/worker.ts` parses `ALLOWED_WORKFLOW_REF_PREFIX` as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected. `src/runtime-entrypoint.ts` performs readiness dispatch and delegates `/exchange`; `src/entrypoint.ts` applies the distributed rate limiter before `src/worker.ts` performs its denial-only exact workflow-ref precheck. `src/index.ts` independently enforces the exact workflow ref/repository plus immutable `job_workflow_sha` or fallback `workflow_sha` after cryptographic verification. Missing, malformed, mismatched, or non-canonical configured source identity fails closed. -`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to the current audited protected central `.github` commit `e03f473723dbcc15080fd580f46c0f8d409c939d`. The immediate audited predecessor was `e1b03eebc6dc5c85aed393e5928927c96376cf46`; protected central advanced through independently owned governance integration while `.github/workflows/noema-review.yml` retained the same blob identity `5c60782adb8be11d6538caea269a4bdfab7ab4d1`. This remains an explicit repository-commit trust decision rather than a claim that future central revisions are equivalent. GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every later central commit movement still requires a new explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema; known central consumer/scanner defects remain owned and repaired there rather than by weakening Noema verification. +`wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to protected central `.github` commit `3a7941aa92de00b8b39fd11cbe7bf3da2fbbeddc`, audited after the central scheduler-only movement from predecessor `e03f473723dbcc15080fd580f46c0f8d409c939d`. The trusted `.github/workflows/noema-review.yml` blob remains byte-identical at `5c60782adb8be11d6538caea269a4bdfab7ab4d1`. The current central commit itself is not represented here as signed provenance; this is an explicit protected-ref plus exact-workflow-content trust decision, not a claim that unrelated central tree changes or future revisions are equivalent. GitHub OIDC `job_workflow_sha` binds the caller to the repository commit identity, so every later central commit movement still requires a new explicit Noema trust roll-forward and fresh exact-head evidence. The central repository remains a read-only dependency from Noema; known central consumer/scanner defects remain owned and repaired there rather than by weakening Noema verification. The configured workflow ref and source SHA are operator authority bytes, not normalization input. The protected workflow-ref parser and authoritative verifier do not trim whitespace from these trust values before validation/comparison. A whitespace-bearing value therefore fails as unusable configuration rather than being normalized into a different trusted identity. On an active PR head this statement is candidate truth if the corresponding source delta is not yet on the live protected base. From 0d4dc6f96dfd9bca60b9c5aea4d7169abbabd2c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:35:39 -0700 Subject: [PATCH 456/564] test(egress): reject unreviewed anonymous GitHub API calls --- ...d-fetch-unauthenticated-github-api.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test/outbound-fetch-unauthenticated-github-api.test.ts diff --git a/test/outbound-fetch-unauthenticated-github-api.test.ts b/test/outbound-fetch-unauthenticated-github-api.test.ts new file mode 100644 index 000000000..abf73830f --- /dev/null +++ b/test/outbound-fetch-unauthenticated-github-api.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it, vi } from "vitest"; +import { + createFailClosedFetch, + isTrustedCredentialEgressRequest, + type FetchLike, +} from "../src/outbound-fetch-policy"; + +describe("anonymous GitHub API egress authority", () => { + it.each([ + "https://api.github.com/meta", + "https://api.github.com/search/issues?q=credential-material", + "https://api.github.com/repos/attacker-controlled/example", + ])("rejects an unreviewed unauthenticated GitHub API request: %s", (url) => { + expect(isTrustedCredentialEgressRequest(url)).toBe(false); + }); + + it("blocks anonymous GitHub API egress before the network call", async () => { + const rawFetch = vi.fn(); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped( + "https://api.github.com/search/issues?q=credential-material", + ); + + expect(rawFetch).not.toHaveBeenCalled(); + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(await response.text()).toBe(""); + }); +}); From 5e4c0f8af07505aea8d53f7b9cd5225381762874 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:36:31 -0700 Subject: [PATCH 457/564] test(egress): preserve exact public metadata diagnostic --- test/outbound-fetch-unauthenticated-github-api.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/outbound-fetch-unauthenticated-github-api.test.ts b/test/outbound-fetch-unauthenticated-github-api.test.ts index abf73830f..e620adca6 100644 --- a/test/outbound-fetch-unauthenticated-github-api.test.ts +++ b/test/outbound-fetch-unauthenticated-github-api.test.ts @@ -7,14 +7,17 @@ import { describe("anonymous GitHub API egress authority", () => { it.each([ - "https://api.github.com/meta", "https://api.github.com/search/issues?q=credential-material", "https://api.github.com/repos/attacker-controlled/example", ])("rejects an unreviewed unauthenticated GitHub API request: %s", (url) => { expect(isTrustedCredentialEgressRequest(url)).toBe(false); }); - it("blocks anonymous GitHub API egress before the network call", async () => { + it("keeps the exact bodyless GitHub API metadata diagnostic available", () => { + expect(isTrustedCredentialEgressRequest("https://api.github.com/meta")).toBe(true); + }); + + it("blocks unreviewed anonymous GitHub API egress before the network call", async () => { const rawFetch = vi.fn(); const wrapped = createFailClosedFetch(rawFetch); From 237334a8db7fe9a3bb459504744ab67d0dc63975 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:37:01 -0700 Subject: [PATCH 458/564] chore(test): drop superseded egress experiment --- ...d-fetch-unauthenticated-github-api.test.ts | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 test/outbound-fetch-unauthenticated-github-api.test.ts diff --git a/test/outbound-fetch-unauthenticated-github-api.test.ts b/test/outbound-fetch-unauthenticated-github-api.test.ts deleted file mode 100644 index e620adca6..000000000 --- a/test/outbound-fetch-unauthenticated-github-api.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { - createFailClosedFetch, - isTrustedCredentialEgressRequest, - type FetchLike, -} from "../src/outbound-fetch-policy"; - -describe("anonymous GitHub API egress authority", () => { - it.each([ - "https://api.github.com/search/issues?q=credential-material", - "https://api.github.com/repos/attacker-controlled/example", - ])("rejects an unreviewed unauthenticated GitHub API request: %s", (url) => { - expect(isTrustedCredentialEgressRequest(url)).toBe(false); - }); - - it("keeps the exact bodyless GitHub API metadata diagnostic available", () => { - expect(isTrustedCredentialEgressRequest("https://api.github.com/meta")).toBe(true); - }); - - it("blocks unreviewed anonymous GitHub API egress before the network call", async () => { - const rawFetch = vi.fn(); - const wrapped = createFailClosedFetch(rawFetch); - - const response = await wrapped( - "https://api.github.com/search/issues?q=credential-material", - ); - - expect(rawFetch).not.toHaveBeenCalled(); - expect(response.status).toBe(502); - expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); - expect(await response.text()).toBe(""); - }); -}); From 4e8b6f39a85841edf06414838e93079a7487627b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:37:37 -0700 Subject: [PATCH 459/564] test(private-key): reject bare-CR PEM termination --- test/github-app-private-key-envelope.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/github-app-private-key-envelope.test.ts b/test/github-app-private-key-envelope.test.ts index 80263c5d8..ae4ee7511 100644 --- a/test/github-app-private-key-envelope.test.ts +++ b/test/github-app-private-key-envelope.test.ts @@ -203,6 +203,8 @@ describe("GitHub App private-key authority", () => { it("preserves a canonical PKCS#8 key and rejects malformed credential envelopes", () => { expect(normalizeGitHubAppPrivateKeyPem(appPrivateKeyPem)).toBe(appPrivateKeyPem); + expect(normalizeGitHubAppPrivateKeyPem(`${appPrivateKeyPem}\r`)).toBeUndefined(); + expect(normalizeGitHubAppPrivateKeyPem(appPrivateKeyPkcs1Pem.replace(/\n$/, "\r"))).toBeUndefined(); expect(normalizeGitHubAppPrivateKeyPem(undefined)).toBeUndefined(); expect(normalizeGitHubAppPrivateKeyPem("-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----")) .toBeUndefined(); From 3d740e665733f9cdbfbceab2ffdfcade196c0c80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:38:06 -0700 Subject: [PATCH 460/564] fix(private-key): reject bare-CR PEM termination --- src/github-app-private-key.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/github-app-private-key.ts b/src/github-app-private-key.ts index 36e1c5f9c..dc7735231 100644 --- a/src/github-app-private-key.ts +++ b/src/github-app-private-key.ts @@ -1,5 +1,5 @@ -const pkcs8PemPattern = /^-----BEGIN PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END PRIVATE KEY-----\r?\n?$/; -const pkcs1PemPattern = /^-----BEGIN RSA PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END RSA PRIVATE KEY-----\r?\n?$/; +const pkcs8PemPattern = /^-----BEGIN PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END PRIVATE KEY-----(?:\r\n|\n)?$/; +const pkcs1PemPattern = /^-----BEGIN RSA PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END RSA PRIVATE KEY-----(?:\r\n|\n)?$/; const MAX_PRIVATE_KEY_PEM_BYTES = 65_536; const MIN_GITHUB_RSA_PRIVATE_KEY_DER_BYTES = 256; const rsaEncryptionAlgorithmIdentifier = Uint8Array.of( @@ -64,10 +64,10 @@ function pemFromDer(label: "PRIVATE KEY", der: Uint8Array): string { /** * Converts the PKCS#1 RSA private-key PEM downloaded from GitHub Apps into the PKCS#8 * envelope consumed by WebCrypto while preserving an already-reviewed PKCS#8 envelope. - * Unknown labels, oversized inputs, non-canonical base64, and implausibly small PKCS#1 - * payloads are rejected rather than normalized into credential authority. One conventional - * terminal PEM newline is removed so the accepted outer envelope matches the downstream - * WebCrypto import boundary without changing the DER key identity. + * Unknown labels, oversized inputs, non-canonical base64, implausibly small PKCS#1 payloads, + * and bare-CR terminal framing are rejected rather than normalized into credential authority. + * One conventional terminal LF or CRLF PEM newline is removed so the accepted outer envelope + * matches the downstream WebCrypto import boundary without changing the DER key identity. * * @param value GitHub App private-key PEM supplied by the Worker secret binding. * @returns A PKCS#8 `PRIVATE KEY` PEM suitable for WebCrypto, or `undefined` when the From ddc1df85d631263b9f7f04a0a5f88db26802d6d3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 01:02:13 -0700 Subject: [PATCH 461/564] test(exchange): reject unreviewed query authority --- test/exchange-body-exact-schema.test.ts | 40 +++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/test/exchange-body-exact-schema.test.ts b/test/exchange-body-exact-schema.test.ts index d6e8b262f..c11cebfd4 100644 --- a/test/exchange-body-exact-schema.test.ts +++ b/test/exchange-body-exact-schema.test.ts @@ -11,8 +11,12 @@ import { const nativeFetch = globalThis.fetch; const validEnvelope = "Bearer a.b.c"; -function exchangeRequest(body: string, traceId = "exact-body-schema"): Request { - return new Request("https://noema.example/exchange", { +function exchangeRequest( + body: string, + traceId = "exact-body-schema", + url = "https://noema.example/exchange", +): Request { + return new Request(url, { method: "POST", headers: { authorization: validEnvelope, @@ -68,4 +72,36 @@ describe("exchange JSON exact schema", () => { expect(logs).not.toContain("unexpected_authority"); expect(logs).not.toContain("sensitive-marker"); }); + + it("rejects unreviewed exchange query authority before parsing credentials or mutating fetch", async () => { + const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); + + const response = await entrypoint.fetch( + exchangeRequest( + '{"target_repository":"ContextualWisdomLab/noema"}', + "exact-url-authority", + "https://noema.example/exchange?unexpected_authority=sensitive-marker", + ), + { + GITHUB_API_BASE: "https://example.com", + GITHUB_APP_ID: "123456", + } as Env, + ); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_VALIDATION_INPUT", + message: "Exchange URL contains unreviewed query parameters", + details: { + policy: "exact-exchange-url", + }, + trace_id: "exact-url-authority", + }); + expect(globalThis.fetch).toBe(nativeFetch); + const logs = logSpy.mock.calls.flat().join("\n"); + expect(logs).toContain('"policy":"exact-exchange-url"'); + expect(logs).not.toContain("unexpected_authority"); + expect(logs).not.toContain("sensitive-marker"); + }); }); From e504385eef5b3d8f202939cdbd0313f3ef3f04cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 01:07:51 -0700 Subject: [PATCH 462/564] fix(exchange): reject unreviewed query authority --- src/runtime-entrypoint.ts | 57 ++++++++++++++++++++++--- test/exchange-body-exact-schema.test.ts | 3 +- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index 44c873126..e87e7a6c6 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -135,6 +135,46 @@ function readinessHeaders( return headers; } +function exchangeUrlResponse(request: Request): Response { + const traceId = traceIdFromRequest(request); + return new Response(JSON.stringify({ + ok: false, + error_code: "ERR_VALIDATION_INPUT", + message: "Exchange URL contains unreviewed query parameters", + details: { + hint: "Send credential-exchange authority only through the documented Authorization header and optional JSON body.", + policy: "exact-exchange-url", + }, + trace_id: traceId, + }), { + status: 400, + headers: { + "content-type": "application/json; charset=utf-8", + "cache-control": "no-store", + pragma: "no-cache", + "x-content-type-options": "nosniff", + "x-trace-id": traceId, + "x-latency-ms": "0", + }, + }); +} + +function recordExchangeUrlFailure(request: Request): void { + try { + console.log(JSON.stringify({ + event: "exchange_url", + route: "/exchange", + method: request.method, + status_code: 400, + error_code: "ERR_VALIDATION_INPUT", + outcome: "rejected", + policy: "exact-exchange-url", + })); + } catch { + // Logging must not convert a fail-closed input response into an exception. + } +} + async function runtimeReadinessResponse(request: Request, env: Env): Promise { const startedAt = performance.now(); const traceId = traceIdFromRequest(request); @@ -204,17 +244,22 @@ async function runtimeReadinessResponse(request: Request, env: Env): Promise { const boundedRequest = canonicalTraceRequest(request); - const runtimeEnv = runtimeCredentialEnv(env); const url = new URL(boundedRequest.url); + if (url.pathname === "/exchange" && url.search !== "") { + recordExchangeUrlFailure(boundedRequest); + return exchangeUrlResponse(boundedRequest); + } + const runtimeEnv = runtimeCredentialEnv(env); if (url.pathname === "/ready") { return runtimeReadinessResponse(boundedRequest, runtimeEnv); } diff --git a/test/exchange-body-exact-schema.test.ts b/test/exchange-body-exact-schema.test.ts index c11cebfd4..e426cf38e 100644 --- a/test/exchange-body-exact-schema.test.ts +++ b/test/exchange-body-exact-schema.test.ts @@ -3,6 +3,7 @@ import entrypoint, { boundExchangeJsonBody, type Env, } from "../src/entrypoint"; +import runtimeEntrypoint from "../src/runtime-entrypoint"; import { resetGlobalOutboundFetchPolicy, type FetchHost, @@ -76,7 +77,7 @@ describe("exchange JSON exact schema", () => { it("rejects unreviewed exchange query authority before parsing credentials or mutating fetch", async () => { const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); - const response = await entrypoint.fetch( + const response = await runtimeEntrypoint.fetch( exchangeRequest( '{"target_repository":"ContextualWisdomLab/noema"}', "exact-url-authority", From 8a309045849cc47dddcf9fa8a4f0950f895bf6d3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 02:01:49 -0700 Subject: [PATCH 463/564] test(exchange): reject bare query delimiter authority --- test/exchange-url-resource-form.test.ts | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 test/exchange-url-resource-form.test.ts diff --git a/test/exchange-url-resource-form.test.ts b/test/exchange-url-resource-form.test.ts new file mode 100644 index 000000000..149982269 --- /dev/null +++ b/test/exchange-url-resource-form.test.ts @@ -0,0 +1,47 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import runtimeEntrypoint, { type Env } from "../src/runtime-entrypoint"; +import { + resetGlobalOutboundFetchPolicy, + type FetchHost, +} from "../src/outbound-fetch-policy"; + +const nativeFetch = globalThis.fetch; + +describe("exchange URL resource-form authority", () => { + afterEach(() => { + resetGlobalOutboundFetchPolicy(); + (globalThis as FetchHost).fetch = nativeFetch; + vi.restoreAllMocks(); + }); + + it("rejects a bare query delimiter before credential parsing or egress policy mutation", async () => { + const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); + const response = await runtimeEntrypoint.fetch( + new Request("https://noema.example/exchange?", { + method: "POST", + headers: { + authorization: "Bearer a.b.c", + "x-request-id": "bare-query-delimiter", + }, + }), + { + GITHUB_API_BASE: "https://example.invalid", + GITHUB_APP_ID: "123456", + } as Env, + ); + + expect(response.status).toBe(400); + expect(response.headers.get("cache-control")).toBe("no-store"); + expect(response.headers.get("pragma")).toBe("no-cache"); + expect(response.headers.get("x-content-type-options")).toBe("nosniff"); + expect(response.headers.get("x-trace-id")).toBe("bare-query-delimiter"); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_VALIDATION_INPUT", + details: { policy: "exact-exchange-url" }, + trace_id: "bare-query-delimiter", + }); + expect(globalThis.fetch).toBe(nativeFetch); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('"policy":"exact-exchange-url"')); + }); +}); From 5bc743a25e671d8ceb843fcda5001ba06cda5d21 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 02:02:37 -0700 Subject: [PATCH 464/564] fix(exchange): require exact resource-form URL --- src/runtime-entrypoint.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index e87e7a6c6..a7a1e5ea9 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -140,9 +140,9 @@ function exchangeUrlResponse(request: Request): Response { return new Response(JSON.stringify({ ok: false, error_code: "ERR_VALIDATION_INPUT", - message: "Exchange URL contains unreviewed query parameters", + message: "Exchange URL contains unreviewed authority", details: { - hint: "Send credential-exchange authority only through the documented Authorization header and optional JSON body.", + hint: "Send the exact /exchange resource URL; credential-exchange authority belongs only in the documented Authorization header and optional JSON body.", policy: "exact-exchange-url", }, trace_id: traceId, @@ -244,18 +244,22 @@ async function runtimeReadinessResponse(request: Request, env: Env): Promise { const boundedRequest = canonicalTraceRequest(request); const url = new URL(boundedRequest.url); - if (url.pathname === "/exchange" && url.search !== "") { + if ( + url.pathname === "/exchange" + && url.href !== `${url.origin}${url.pathname}` + ) { recordExchangeUrlFailure(boundedRequest); return exchangeUrlResponse(boundedRequest); } From 609af78372e568dd289a26f484cf92b7eba8e5f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 02:06:21 -0700 Subject: [PATCH 465/564] fix(exchange): preserve stable validation message --- src/runtime-entrypoint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime-entrypoint.ts b/src/runtime-entrypoint.ts index a7a1e5ea9..1c89dd3fe 100644 --- a/src/runtime-entrypoint.ts +++ b/src/runtime-entrypoint.ts @@ -140,7 +140,7 @@ function exchangeUrlResponse(request: Request): Response { return new Response(JSON.stringify({ ok: false, error_code: "ERR_VALIDATION_INPUT", - message: "Exchange URL contains unreviewed authority", + message: "Exchange URL contains unreviewed query parameters", details: { hint: "Send the exact /exchange resource URL; credential-exchange authority belongs only in the documented Authorization header and optional JSON body.", policy: "exact-exchange-url", From 2a6a92239c5d4d009b7dc90b123aaff14802d860 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 02:30:20 -0700 Subject: [PATCH 466/564] test(github-app): refresh stale installation cache --- .../github-installation-cache-refresh.test.ts | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 test/github-installation-cache-refresh.test.ts diff --git a/test/github-installation-cache-refresh.test.ts b/test/github-installation-cache-refresh.test.ts new file mode 100644 index 000000000..d49d50a91 --- /dev/null +++ b/test/github-installation-cache-refresh.test.ts @@ -0,0 +1,179 @@ +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import worker, { type Env } from "../src/index"; + +const configuredWorkflowRef = + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main"; +const configuredWorkflowSha = "a".repeat(40); +const targetRepository = "ContextualWisdomLab/installation-cache-refresh"; +const firstInstallationId = "91001"; +const replacementInstallationId = "91002"; +const signingKid = "installation-cache-refresh"; + +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: "initialized-in-beforeAll", + NOEMA_RATE_LIMIT_PER_MINUTE: "1000", + NOEMA_INSTALLATION_CACHE_TTL_SECONDS: "3600", +}; + +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"], + ); +} + +async function signedOidcToken(): Promise { + const now = Math.floor(Date.now() / 1000); + const header = encodeSegment({ alg: "RS256", kid: signingKid, typ: "JWT" }); + const payload = encodeSegment({ + iss: env.ALLOWED_ISSUER, + aud: env.ALLOWED_AUDIENCE, + repository_owner: env.ALLOWED_REPOSITORY_OWNER, + repository_owner_id: "295022177", + repository: "ContextualWisdomLab/.github", + repository_id: "1274066402", + 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 signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + oidcKeyPair.privateKey, + new TextEncoder().encode(`${header}.${payload}`), + ); + return `${header}.${payload}.${encodeBytes(signature)}`; +} + +async function exchange(clientIp: string): Promise { + return worker.fetch( + new Request("https://noema.example/exchange", { + method: "POST", + headers: { + authorization: `Bearer ${await signedOidcToken()}`, + "content-type": "application/json", + "cf-connecting-ip": clientIp, + }, + body: JSON.stringify({ target_repository: targetRepository }), + }), + { ...env, GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKeyPem }, + ); +} + +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(); +}); + +describe("GitHub App installation cache refresh", () => { + it("re-resolves an auto-discovered installation after a cached id is retired", async () => { + let installationLookups = 0; + const tokenMintIds: string[] = []; + let firstInstallationMinted = false; + + 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: [{ ...oidcPublicJwk, kid: signingKid, kty: "RSA" }], + }); + } + if (url === `https://api.github.com/repos/${targetRepository}/installation`) { + installationLookups += 1; + return Response.json({ + id: installationLookups === 1 + ? Number(firstInstallationId) + : Number(replacementInstallationId), + }); + } + const mintMatch = url.match(/\/app\/installations\/(\d+)\/access_tokens$/); + if (mintMatch) { + const installationId = mintMatch[1]; + tokenMintIds.push(installationId); + if (installationId === firstInstallationId && !firstInstallationMinted) { + firstInstallationMinted = true; + return Response.json({ + token: "ghs_initial_installation_token", + expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), + }, { status: 201 }); + } + if (installationId === firstInstallationId) { + return Response.json({ message: "Not Found" }, { status: 404 }); + } + if (installationId === replacementInstallationId) { + return Response.json({ + token: "ghs_replacement_installation_token", + expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), + }, { status: 201 }); + } + } + return new Response("unexpected privileged egress", { status: 500 }); + }); + + const first = await exchange("203.0.113.210"); + expect(first.status).toBe(200); + + const second = await exchange("203.0.113.211"); + expect(second.status).toBe(200); + await expect(second.json()).resolves.toMatchObject({ + ok: true, + data: { + repository: targetRepository, + token: "ghs_replacement_installation_token", + }, + }); + expect(installationLookups).toBe(2); + expect(tokenMintIds).toEqual([ + firstInstallationId, + firstInstallationId, + replacementInstallationId, + ]); + }); +}); From b39b4f1e0194ecc7159d63ed328d02385aa31837 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 02:35:50 -0700 Subject: [PATCH 467/564] fix(github-app): refresh retired cached installation ids --- src/index.ts | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5244e77d5..ae05545e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -120,6 +120,7 @@ class ApiError extends Error { public status: number, message: string, public details?: ErrorDetails, + public upstreamStatus?: number, ) { super(message); this.name = "ApiError"; @@ -773,12 +774,18 @@ async function githubJson( }); if (!response.ok) { if (response.status === 429) { - throw new ApiError("ERR_RATE_LIMIT", 429, "GitHub API rate limit reached"); + throw new ApiError("ERR_RATE_LIMIT", 429, "GitHub API rate limit reached", undefined, response.status); } if (response.status >= 500) { - throw new ApiError("ERR_GITHUB_API", 502, "GitHub API is temporarily unavailable"); + throw new ApiError("ERR_GITHUB_API", 502, "GitHub API is temporarily unavailable", undefined, response.status); } - throw new ApiError("ERR_GITHUB_API", response.status >= 400 ? 400 : 500, "GitHub API request failed"); + throw new ApiError( + "ERR_GITHUB_API", + response.status >= 400 ? 400 : 500, + "GitHub API request failed", + undefined, + response.status, + ); } if (response.status !== expectedStatus) { throw new ApiError("ERR_GITHUB_API", 502, "GitHub API returned an unexpected success status"); @@ -843,12 +850,28 @@ async function resolveInstallationId(appJwt: string, repository: string, env: En async function createInstallationToken(repository: string, env: Env): Promise { const appJwt = await createGitHubAppJwt(env); - const installationId = await resolveInstallationId(appJwt, repository, env); - const token = await githubJson(`/app/installations/${installationId}/access_tokens`, { + let installationId = await resolveInstallationId(appJwt, repository, env); + const mintInstallationToken = (id: string) => githubJson(`/app/installations/${id}/access_tokens`, { method: "POST", headers: { authorization: `Bearer ${appJwt}` }, body: JSON.stringify({ repositories: [repository.split("/", 2)[1]], permissions: { pull_requests: "write", contents: "read", checks: "read" } }), }, env, 201); + + let token: Record; + try { + token = await mintInstallationToken(installationId); + } catch (error) { + if ( + env.GITHUB_APP_INSTALLATION_ID !== undefined + || !(error instanceof ApiError) + || error.upstreamStatus !== 404 + ) { + throw error; + } + installationIdCache.delete(`${env.GITHUB_API_BASE}:${env.GITHUB_APP_ID}:${repository}`); + installationId = await resolveInstallationId(appJwt, repository, env); + token = await mintInstallationToken(installationId); + } if (token.token === undefined || token.token === null || token.token === "") { throw new ApiError("ERR_GITHUB_INSTALLATION", 500, "GitHub installation token response was empty", { field: "token", From d3ea8537eb8ea93ca9b1b3aecdcd9b267c5224c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 03:05:16 -0700 Subject: [PATCH 468/564] test(github-app): retry installation refresh only from cache --- .../github-installation-cache-refresh.test.ts | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/test/github-installation-cache-refresh.test.ts b/test/github-installation-cache-refresh.test.ts index d49d50a91..90efba26c 100644 --- a/test/github-installation-cache-refresh.test.ts +++ b/test/github-installation-cache-refresh.test.ts @@ -79,7 +79,10 @@ async function signedOidcToken(): Promise { return `${header}.${payload}.${encodeBytes(signature)}`; } -async function exchange(clientIp: string): Promise { +async function exchange( + clientIp: string, + repository = targetRepository, +): Promise { return worker.fetch( new Request("https://noema.example/exchange", { method: "POST", @@ -88,7 +91,7 @@ async function exchange(clientIp: string): Promise { "content-type": "application/json", "cf-connecting-ip": clientIp, }, - body: JSON.stringify({ target_repository: targetRepository }), + body: JSON.stringify({ target_repository: repository }), }), { ...env, GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKeyPem }, ); @@ -176,4 +179,58 @@ describe("GitHub App installation cache refresh", () => { replacementInstallationId, ]); }); + + it("does not refresh a freshly discovered installation when its first token mint returns 404", async () => { + const freshRepository = "ContextualWisdomLab/installation-fresh-404"; + const freshInstallationId = "92001"; + const replacementId = "92002"; + let installationLookups = 0; + const tokenMintIds: 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: [{ ...oidcPublicJwk, kid: signingKid, kty: "RSA" }], + }); + } + if (url === `https://api.github.com/repos/${freshRepository}/installation`) { + installationLookups += 1; + return Response.json({ + id: installationLookups === 1 + ? Number(freshInstallationId) + : Number(replacementId), + }); + } + const mintMatch = url.match(/\/app\/installations\/(\d+)\/access_tokens$/); + if (mintMatch) { + tokenMintIds.push(mintMatch[1]); + if (mintMatch[1] === freshInstallationId) { + return Response.json({ message: "Not Found" }, { status: 404 }); + } + if (mintMatch[1] === replacementId) { + return Response.json({ + token: "ghs_must_not_be_minted_after_fresh_404", + expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), + }, { status: 201 }); + } + } + return new Response("unexpected privileged egress", { status: 500 }); + }); + + const response = await exchange("203.0.113.212", freshRepository); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_API", + }); + expect(installationLookups).toBe(1); + expect(tokenMintIds).toEqual([freshInstallationId]); + }); }); From 484f39c40188d4afecddcd0fa6463c576efc9be8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 03:19:34 -0700 Subject: [PATCH 469/564] fix(github-app): refresh only cached installation authority --- src/index.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index ae05545e0..ec0b58efd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -108,6 +108,11 @@ type TimedCache = { expiresAtMs: number; }; +type InstallationIdResolution = { + value: string; + source: "configured" | "cache" | "discovery"; +}; + const rateLimitBuckets = new Map(); const rateLimitWindowMs = 60_000; const maxLocalRateLimitBuckets = 10_000; @@ -805,7 +810,11 @@ async function githubJson( return value as Record; } -async function resolveInstallationId(appJwt: string, repository: string, env: Env): Promise { +async function resolveInstallationId( + appJwt: string, + repository: string, + env: Env, +): Promise { if (env.GITHUB_APP_INSTALLATION_ID) { const configuredInstallationId = env.GITHUB_APP_INSTALLATION_ID; if (!/^[1-9]\d*$/.test(configuredInstallationId)) { @@ -815,13 +824,13 @@ async function resolveInstallationId(appJwt: string, repository: string, env: En if (!Number.isSafeInteger(numericInstallationId) || String(numericInstallationId) !== configuredInstallationId) { throw new ApiError("ERR_GITHUB_INSTALLATION", 500, "GitHub App installation id configuration is invalid"); } - return configuredInstallationId; + return { value: configuredInstallationId, source: "configured" }; } const now = Date.now(); const cacheKey = `${env.GITHUB_API_BASE}:${env.GITHUB_APP_ID}:${repository}`; const cached = installationIdCache.get(cacheKey); if (cached && cached.expiresAtMs > now) { - return cached.value; + return { value: cached.value, source: "cache" }; } if (cached) { installationIdCache.delete(cacheKey); @@ -845,12 +854,13 @@ async function resolveInstallationId(appJwt: string, repository: string, env: En value: installationId, expiresAtMs: now + configuredTtlMs(env.NOEMA_INSTALLATION_CACHE_TTL_SECONDS, 600, 3600), }); - return installationId; + return { value: installationId, source: "discovery" }; } async function createInstallationToken(repository: string, env: Env): Promise { const appJwt = await createGitHubAppJwt(env); - let installationId = await resolveInstallationId(appJwt, repository, env); + let installationResolution = await resolveInstallationId(appJwt, repository, env); + let installationId = installationResolution.value; const mintInstallationToken = (id: string) => githubJson(`/app/installations/${id}/access_tokens`, { method: "POST", headers: { authorization: `Bearer ${appJwt}` }, @@ -862,14 +872,15 @@ async function createInstallationToken(repository: string, env: Env): Promise Date: Sat, 29 Aug 2026 03:54:27 -0700 Subject: [PATCH 470/564] ci: retrigger exact-head patch validator From 3d2c4d3f599c06d56e84168f39fe06759277ca6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 04:08:42 -0700 Subject: [PATCH 471/564] test(egress): restrict anonymous GitHub API authority --- ...outbound-fetch-anonymous-authority.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 test/outbound-fetch-anonymous-authority.test.ts diff --git a/test/outbound-fetch-anonymous-authority.test.ts b/test/outbound-fetch-anonymous-authority.test.ts new file mode 100644 index 000000000..35bcd7cc3 --- /dev/null +++ b/test/outbound-fetch-anonymous-authority.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it, vi } from "vitest"; + +import { + createFailClosedFetch, + isTrustedCredentialEgress, + isTrustedCredentialEgressRequest, + type FetchLike, +} from "../src/outbound-fetch-policy"; + +describe("anonymous GitHub API egress authority", () => { + it("admits only the exact bodyless /meta diagnostic without credentials", async () => { + const installationUrl = "https://api.github.com/repos/ContextualWisdomLab/noema/installation"; + + expect(isTrustedCredentialEgress("https://api.github.com/meta")).toBe(true); + expect(isTrustedCredentialEgressRequest("https://api.github.com/meta")).toBe(true); + expect(isTrustedCredentialEgress(installationUrl)).toBe(true); + expect(isTrustedCredentialEgressRequest(installationUrl)).toBe(false); + + const rawFetch = vi.fn(async () => new Response(null, { status: 204 })); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped(installationUrl); + + expect(rawFetch).not.toHaveBeenCalled(); + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + }); + + it("rejects arbitrary anonymous GitHub REST destinations outside reviewed operations", async () => { + const unreviewedUrl = "https://api.github.com/repos/ContextualWisdomLab/noema/issues"; + + expect(isTrustedCredentialEgress(unreviewedUrl)).toBe(false); + + const rawFetch = vi.fn(); + const wrapped = createFailClosedFetch(rawFetch); + const response = await wrapped(unreviewedUrl); + + expect(rawFetch).not.toHaveBeenCalled(); + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-destination"); + }); +}); From f2e0802dc498768375dd8c17012bb5559185d4db Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 04:11:20 -0700 Subject: [PATCH 472/564] fix(egress): bound anonymous GitHub API authority --- src/outbound-fetch-policy.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index e6f459c2a..8f6324f72 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -27,6 +27,7 @@ type GitHubApiOperation = | "installation-token"; const TRUSTED_GITHUB_API_ORIGIN = "https://api.github.com"; +const TRUSTED_GITHUB_API_META = "https://api.github.com/meta"; const TRUSTED_GITHUB_OIDC_ORIGIN = "https://token.actions.githubusercontent.com"; const TRUSTED_GITHUB_OIDC_DISCOVERY = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"; @@ -269,7 +270,8 @@ export function isTrustedCredentialEgress(input: RequestInfo | URL): boolean { } if (url.origin === TRUSTED_GITHUB_API_ORIGIN) { - return true; + return url.href === TRUSTED_GITHUB_API_META + || githubApiOperation(url) !== undefined; } return url.href === TRUSTED_GITHUB_OIDC_DISCOVERY @@ -314,7 +316,9 @@ export function isTrustedCredentialEgressRequest( const authorization = headers.get("authorization"); if (!authorization) { - return method === "GET" && !bodyPresent; + return url.href === TRUSTED_GITHUB_API_META + && method === "GET" + && !bodyPresent; } const rawAuthorization = rawAuthorizationHeaderFromInit(init?.headers); if ( From cba09004a96c76611c6333b17e344549d7a5bdfc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 04:23:30 -0700 Subject: [PATCH 473/564] test(egress): align legacy cases with reviewed operations --- test/outbound-fetch-policy.test.ts | 11 ++++++----- test/outbound-fetch-url-canonicality.test.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/test/outbound-fetch-policy.test.ts b/test/outbound-fetch-policy.test.ts index 155716f3d..8804179b1 100644 --- a/test/outbound-fetch-policy.test.ts +++ b/test/outbound-fetch-policy.test.ts @@ -14,9 +14,10 @@ describe("credential-bearing outbound fetch policy", () => { vi.restoreAllMocks(); }); - it("allows only GitHub API and pinned GitHub OIDC endpoints", () => { - expect(isTrustedCredentialEgress("https://api.github.com/app/installations")).toBe(true); - expect(isTrustedCredentialEgress(new URL("https://api.github.com/repos/cwl/noema?per_page=100"))).toBe(true); + it("allows only reviewed GitHub API and pinned GitHub OIDC endpoints", () => { + expect(isTrustedCredentialEgress("https://api.github.com/meta")).toBe(true); + expect(isTrustedCredentialEgress("https://api.github.com/repos/cwl/noema/installation")).toBe(true); + expect(isTrustedCredentialEgress("https://api.github.com/app/installations/123/access_tokens")).toBe(true); expect(isTrustedCredentialEgress(new Request( "https://token.actions.githubusercontent.com/.well-known/openid-configuration", ))).toBe(true); @@ -94,7 +95,7 @@ describe("credential-bearing outbound fetch policy", () => { })); const wrapped = createFailClosedFetch(rawFetch); - const response = await wrapped("https://api.github.com/app/installations"); + const response = await wrapped("https://api.github.com/meta"); expect(response.status).toBe(502); expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-redirect"); @@ -110,7 +111,7 @@ describe("credential-bearing outbound fetch policy", () => { const rawFetch = vi.fn(async () => redirectedResponse); const wrapped = createFailClosedFetch(rawFetch); - const response = await wrapped("https://api.github.com/app/installations"); + const response = await wrapped("https://api.github.com/meta"); expect(response.status).toBe(502); expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-redirect"); diff --git a/test/outbound-fetch-url-canonicality.test.ts b/test/outbound-fetch-url-canonicality.test.ts index 1dd7fbc68..135352ddf 100644 --- a/test/outbound-fetch-url-canonicality.test.ts +++ b/test/outbound-fetch-url-canonicality.test.ts @@ -3,14 +3,14 @@ import { isTrustedCredentialEgress } from "../src/outbound-fetch-policy"; describe("credential-egress URL authority", () => { it("does not normalize raw string aliases into trusted GitHub destinations", () => { - expect(isTrustedCredentialEgress(" https://api.github.com/app/installations")).toBe(false); - expect(isTrustedCredentialEgress("https://api.github.com/app/installations ")).toBe(false); - expect(isTrustedCredentialEgress("https://api.github.com:443/app/installations")).toBe(false); - expect(isTrustedCredentialEgress("https://API.GITHUB.COM/app/installations")).toBe(false); + expect(isTrustedCredentialEgress(" https://api.github.com/meta")).toBe(false); + expect(isTrustedCredentialEgress("https://api.github.com/meta ")).toBe(false); + expect(isTrustedCredentialEgress("https://api.github.com:443/meta")).toBe(false); + expect(isTrustedCredentialEgress("https://API.GITHUB.COM/meta")).toBe(false); }); it("preserves canonical string and already-parsed URL authority", () => { - expect(isTrustedCredentialEgress("https://api.github.com/app/installations")).toBe(true); - expect(isTrustedCredentialEgress(new URL("https://api.github.com:443/app/installations"))).toBe(true); + expect(isTrustedCredentialEgress("https://api.github.com/meta")).toBe(true); + expect(isTrustedCredentialEgress(new URL("https://api.github.com:443/meta"))).toBe(true); }); }); From dc6c3023aa0660601b031fc999314226d9fb5300 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 04:31:56 -0700 Subject: [PATCH 474/564] test(egress): reject unreviewed outbound header authority --- test/outbound-fetch-header-authority.test.ts | 39 ++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 test/outbound-fetch-header-authority.test.ts diff --git a/test/outbound-fetch-header-authority.test.ts b/test/outbound-fetch-header-authority.test.ts new file mode 100644 index 000000000..f8820d687 --- /dev/null +++ b/test/outbound-fetch-header-authority.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it, vi } from "vitest"; +import { + createFailClosedFetch, + type FetchLike, +} from "../src/outbound-fetch-policy"; + +describe("outbound header authority", () => { + it.each([ + [ + "credential-bearing GitHub API", + "https://api.github.com/repos/ContextualWisdomLab/noema/installation", + { + headers: { + authorization: "Bearer sensitive", + "x-noema-unreviewed-secret": "sensitive-marker", + }, + }, + ], + [ + "GitHub OIDC metadata", + "https://token.actions.githubusercontent.com/.well-known/openid-configuration", + { + headers: { + "x-noema-unreviewed-secret": "sensitive-marker", + }, + }, + ], + ])("rejects unreviewed caller header authority before %s egress", async (_label, url, init) => { + const rawFetch = vi.fn(async () => new Response("unexpected network")); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped(url, init); + + expect(rawFetch).not.toHaveBeenCalled(); + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(await response.text()).toBe(""); + }); +}); From b2367eb15f46f305a8b352c108e4a2f165a8ae6c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 04:32:56 -0700 Subject: [PATCH 475/564] fix(egress): bound outbound header authority --- src/outbound-fetch-policy.ts | 39 +++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 8f6324f72..635d58163 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -80,6 +80,30 @@ function outboundHeaders(input: RequestInfo | URL, init: RequestInit | undefined return new Headers(); } +function hasNoHeaders(headers: Headers): boolean { + let empty = true; + headers.forEach(() => { + empty = false; + }); + return empty; +} + +function hasOnlyReviewedGithubApiHeaders(headers: Headers): boolean { + let reviewed = true; + headers.forEach((value, name) => { + if (!reviewed || name === "authorization") return; + if ( + (name === "accept" && value === "application/vnd.github+json") + || (name === "user-agent" && value === "noema") + || (name === "x-github-api-version" && value === "2022-11-28") + ) { + return; + } + reviewed = false; + }); + return reviewed; +} + function rawAuthorizationHeaderFromInit(headersInit: HeadersInit | undefined): string | null | undefined { if (headersInit === undefined || headersInit instanceof Headers) return undefined; const entries = Array.isArray(headersInit) ? headersInit : Object.entries(headersInit); @@ -296,21 +320,12 @@ export function isTrustedCredentialEgressRequest( const bodyPresent = outboundBodyPresent(input, init); if (url.origin === TRUSTED_GITHUB_OIDC_ORIGIN) { - return ( - method === "GET" + return method === "GET" && !bodyPresent - && !headers.has("authorization") - && !headers.has("cookie") - && !headers.has("proxy-authorization") - ); + && hasNoHeaders(headers); } - if ( - headers.has("cookie") - || headers.has("proxy-authorization") - || headers.has("x-http-method-override") - || headers.has("x-method-override") - ) { + if (!hasOnlyReviewedGithubApiHeaders(headers)) { return false; } From bbb4e98ce4777329369504e14362598a3dade4e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:03:24 -0700 Subject: [PATCH 476/564] test(egress): align retired listing destination verdict --- test/outbound-fetch-app-installations-authority.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/outbound-fetch-app-installations-authority.test.ts b/test/outbound-fetch-app-installations-authority.test.ts index 47cce1d1f..cc72aafbf 100644 --- a/test/outbound-fetch-app-installations-authority.test.ts +++ b/test/outbound-fetch-app-installations-authority.test.ts @@ -15,7 +15,7 @@ describe("credential egress operation authority", () => { }); expect(response.status).toBe(502); - expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-destination"); expect(rawFetch).not.toHaveBeenCalled(); }); }); From 6ee5540301502300c35b894e48f73aea6f75820a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:03:44 -0700 Subject: [PATCH 477/564] test(egress): use reviewed GitHub media type --- test/outbound-fetch-authorization-canonicality.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/outbound-fetch-authorization-canonicality.test.ts b/test/outbound-fetch-authorization-canonicality.test.ts index 6ace265fc..c9a078384 100644 --- a/test/outbound-fetch-authorization-canonicality.test.ts +++ b/test/outbound-fetch-authorization-canonicality.test.ts @@ -7,7 +7,7 @@ describe("credential-egress Authorization framing", () => { it("accepts exactly one ASCII space between Bearer and the credential from raw RequestInit headers", () => { expect(isTrustedCredentialEgressRequest(installationLookup, { method: "GET", - headers: { authorization: "Bearer canonical-token", accept: "application/json" }, + headers: { authorization: "Bearer canonical-token", accept: "application/vnd.github+json" }, })).toBe(true); expect(isTrustedCredentialEgressRequest(installationLookup, { method: "GET", From 77c6b39487948abaacae7698b50770be0cc3d33a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:04:04 -0700 Subject: [PATCH 478/564] test(egress): match production token-mint headers --- test/outbound-fetch-installation-id-range.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/outbound-fetch-installation-id-range.test.ts b/test/outbound-fetch-installation-id-range.test.ts index 6f81777a6..fed08eefa 100644 --- a/test/outbound-fetch-installation-id-range.test.ts +++ b/test/outbound-fetch-installation-id-range.test.ts @@ -17,7 +17,6 @@ function requestFor(id: string) { method: "POST", headers: { authorization: "Bearer app-jwt", - "content-type": "application/json", }, body, }, From 75e6e1e886e48d08acca62977ffd0b32d8d8058c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:06:54 -0700 Subject: [PATCH 479/564] test(egress): require exact token-mint JSON media type --- test/outbound-fetch-installation-id-range.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/outbound-fetch-installation-id-range.test.ts b/test/outbound-fetch-installation-id-range.test.ts index fed08eefa..a67dd21a9 100644 --- a/test/outbound-fetch-installation-id-range.test.ts +++ b/test/outbound-fetch-installation-id-range.test.ts @@ -17,6 +17,7 @@ function requestFor(id: string) { method: "POST", headers: { authorization: "Bearer app-jwt", + "content-type": "application/json", }, body, }, @@ -24,7 +25,7 @@ function requestFor(id: string) { } describe("credential egress installation-id authority", () => { - it("accepts the maximum canonical safe integer installation id", () => { + it("accepts the maximum canonical safe integer installation id with the exact JSON media type", () => { expect(requestFor(String(Number.MAX_SAFE_INTEGER))).toBe(true); }); From fa11c84841179567890764248d62067bb543a929 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:12:56 -0700 Subject: [PATCH 480/564] fix(egress): bind token mint to JSON media type --- src/outbound-fetch-policy.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 635d58163..661e1d95e 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -88,7 +88,10 @@ function hasNoHeaders(headers: Headers): boolean { return empty; } -function hasOnlyReviewedGithubApiHeaders(headers: Headers): boolean { +function hasOnlyReviewedGithubApiHeaders( + headers: Headers, + operation: GitHubApiOperation | undefined, +): boolean { let reviewed = true; headers.forEach((value, name) => { if (!reviewed || name === "authorization") return; @@ -96,6 +99,11 @@ function hasOnlyReviewedGithubApiHeaders(headers: Headers): boolean { (name === "accept" && value === "application/vnd.github+json") || (name === "user-agent" && value === "noema") || (name === "x-github-api-version" && value === "2022-11-28") + || ( + operation === "installation-token" + && name === "content-type" + && value === "application/json" + ) ) { return; } @@ -325,7 +333,8 @@ export function isTrustedCredentialEgressRequest( && hasNoHeaders(headers); } - if (!hasOnlyReviewedGithubApiHeaders(headers)) { + const operation = githubApiOperation(url); + if (!hasOnlyReviewedGithubApiHeaders(headers, operation)) { return false; } @@ -345,12 +354,12 @@ export function isTrustedCredentialEgressRequest( return false; } - const operation = githubApiOperation(url); if (operation === "repository-installation") { return method === "GET" && !bodyPresent; } return operation === "installation-token" && method === "POST" + && headers.get("content-type") === "application/json" && reviewedInstallationTokenBody(input, init); } From a3046adbccd6498288e31493e6a5735e99b915af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:15:45 -0700 Subject: [PATCH 481/564] fix(egress): canonicalize token mint media type --- src/outbound-fetch-policy.ts | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 661e1d95e..da1829a9e 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -282,6 +282,27 @@ function githubApiOperation(url: URL): GitHubApiOperation | undefined { return undefined; } +function withCanonicalInstallationTokenMediaType( + input: RequestInfo | URL, + init: RequestInit | undefined, +): RequestInit | undefined { + const url = outboundUrl(input); + if ( + !url + || githubApiOperation(url) !== "installation-token" + || typeof init?.body !== "string" + || new Headers(init.headers).has("content-type") + || init.headers instanceof Headers + ) { + return init; + } + + const headers: HeadersInit = Array.isArray(init.headers) + ? [...init.headers, ["content-type", "application/json"]] + : { ...(init.headers ?? {}), "content-type": "application/json" }; + return { ...init, headers }; +} + /** * Checks whether an outbound destination is on the exact HTTPS credential-egress allowlist used by Noema. * Raw string destinations must already equal their parsed URL serialization; the policy never trims, @@ -373,7 +394,8 @@ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { if (!isTrustedCredentialEgress(input)) { return blockedResponse("destination"); } - if (!isTrustedCredentialEgressRequest(input, init)) { + const effectiveInit = withCanonicalInstallationTokenMediaType(input, init); + if (!isTrustedCredentialEgressRequest(input, effectiveInit)) { return blockedResponse("request-policy"); } @@ -386,11 +408,11 @@ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { () => timeoutController.abort(timeoutReason), OUTBOUND_FETCH_TIMEOUT_MS, ); - const signal = boundedOutboundSignal(input, init, timeoutController.signal); + const signal = boundedOutboundSignal(input, effectiveInit, timeoutController.signal); try { const response = await rawFetch(input, { - ...(init ?? {}), + ...(effectiveInit ?? {}), redirect: "manual", signal, }); @@ -410,7 +432,7 @@ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { if (signal.aborted) { throw error; } - if (outboundHeaders(input, init).has("authorization")) { + if (outboundHeaders(input, effectiveInit).has("authorization")) { return blockedResponse("transport"); } throw error; From ac96ea41f97926a4663119fdef5d3257b5ea357d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:17:16 -0700 Subject: [PATCH 482/564] test(egress): bind scope cases to JSON media type --- test/installation-token-scope-policy.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/installation-token-scope-policy.test.ts b/test/installation-token-scope-policy.test.ts index 864f07fa8..d0921c775 100644 --- a/test/installation-token-scope-policy.test.ts +++ b/test/installation-token-scope-policy.test.ts @@ -3,7 +3,10 @@ import { isTrustedCredentialEgressRequest } from "../src/outbound-fetch-policy"; const installationTokenUrl = "https://api.github.com/app/installations/12345/access_tokens"; -const authorization = { authorization: "Bearer app-jwt" }; +const authorization = { + authorization: "Bearer app-jwt", + "content-type": "application/json", +}; const leastPrivilegeBody = JSON.stringify({ repositories: ["noema"], permissions: { From 8d5f0c08c13cb25d75fa3bc002389b31694a8a3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:18:16 -0700 Subject: [PATCH 483/564] test(egress): bind token operation to JSON media type --- test/outbound-request-compartment.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/outbound-request-compartment.test.ts b/test/outbound-request-compartment.test.ts index 3177a1c6b..b57e68210 100644 --- a/test/outbound-request-compartment.test.ts +++ b/test/outbound-request-compartment.test.ts @@ -62,7 +62,10 @@ describe("outbound credential request compartmentalization", () => { }))).toBe(false); expect(isTrustedCredentialEgressRequest(installationTokenUrl, { method: "POST", - headers: { authorization: "Bearer app-jwt" }, + headers: { + authorization: "Bearer app-jwt", + "content-type": "application/json", + }, body: installationTokenBody, })).toBe(true); }); From b802dc5867decfcad040aa9bceef307fc84cf268 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:19:03 -0700 Subject: [PATCH 484/564] test(egress): prove canonical token media type on wire --- test/outbound-fetch-token-media-type.test.ts | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 test/outbound-fetch-token-media-type.test.ts diff --git a/test/outbound-fetch-token-media-type.test.ts b/test/outbound-fetch-token-media-type.test.ts new file mode 100644 index 000000000..403e04819 --- /dev/null +++ b/test/outbound-fetch-token-media-type.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it, vi } from "vitest"; +import { + createFailClosedFetch, + type FetchLike, +} from "../src/outbound-fetch-policy"; + +const tokenUrl = "https://api.github.com/app/installations/12345/access_tokens"; +const tokenBody = JSON.stringify({ + repositories: ["noema"], + permissions: { + contents: "read", + pull_requests: "write", + checks: "read", + }, +}); + +describe("installation-token outbound media type", () => { + it.each([ + ["record", { authorization: "Bearer app-jwt" }], + ["tuple list", [["authorization", "Bearer app-jwt"]] as [string, string][]], + ])("adds exact application/json before %s headers cross the network boundary", async (_label, headers) => { + const rawFetch = vi.fn(async () => Response.json({ token: "issued" })); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped(tokenUrl, { + method: "POST", + headers, + body: tokenBody, + }); + + expect(response.status).toBe(200); + expect(rawFetch).toHaveBeenCalledOnce(); + const forwarded = new Headers(rawFetch.mock.calls[0][1]?.headers); + expect(forwarded.get("authorization")).toBe("Bearer app-jwt"); + expect(forwarded.get("content-type")).toBe("application/json"); + }); + + it("rejects a caller-supplied non-JSON media type rather than overriding it", async () => { + const rawFetch = vi.fn(); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped(tokenUrl, { + method: "POST", + headers: { + authorization: "Bearer app-jwt", + "content-type": "text/plain;charset=UTF-8", + }, + body: tokenBody, + }); + + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(rawFetch).not.toHaveBeenCalled(); + }); + + it("does not promote pre-normalized Headers whose credential framing is no longer observable", async () => { + const rawFetch = vi.fn(); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped(tokenUrl, { + method: "POST", + headers: new Headers({ authorization: "Bearer app-jwt" }), + body: tokenBody, + }); + + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(rawFetch).not.toHaveBeenCalled(); + }); +}); From d94a069b10d53551c1b4bef2267c9ff424695884 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:28:06 -0700 Subject: [PATCH 485/564] test(egress): close token media-type branch coverage --- test/outbound-fetch-token-media-type.test.ts | 32 ++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/outbound-fetch-token-media-type.test.ts b/test/outbound-fetch-token-media-type.test.ts index 403e04819..3a0d51123 100644 --- a/test/outbound-fetch-token-media-type.test.ts +++ b/test/outbound-fetch-token-media-type.test.ts @@ -5,6 +5,7 @@ import { } from "../src/outbound-fetch-policy"; const tokenUrl = "https://api.github.com/app/installations/12345/access_tokens"; +const installationUrl = "https://api.github.com/repos/ContextualWisdomLab/noema/installation"; const tokenBody = JSON.stringify({ repositories: ["noema"], permissions: { @@ -35,6 +36,37 @@ describe("installation-token outbound media type", () => { expect(forwarded.get("content-type")).toBe("application/json"); }); + it("does not manufacture authorization for a headerless token request while adding its media type", async () => { + const rawFetch = vi.fn(); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped(tokenUrl, { + method: "POST", + body: tokenBody, + }); + + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(rawFetch).not.toHaveBeenCalled(); + }); + + it("rejects installation-token media authority on a repository-installation lookup", async () => { + const rawFetch = vi.fn(); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped(installationUrl, { + method: "GET", + headers: { + authorization: "Bearer app-jwt", + "content-type": "application/json", + }, + }); + + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(rawFetch).not.toHaveBeenCalled(); + }); + it("rejects a caller-supplied non-JSON media type rather than overriding it", async () => { const rawFetch = vi.fn(); const wrapped = createFailClosedFetch(rawFetch); From a4a318b187f05c8f0faf4a21fc52053fd29a15b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:31:21 -0700 Subject: [PATCH 486/564] test(egress): cover noncanonical reviewed header value --- test/outbound-fetch-header-authority.test.ts | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/outbound-fetch-header-authority.test.ts b/test/outbound-fetch-header-authority.test.ts index f8820d687..39c24cdf2 100644 --- a/test/outbound-fetch-header-authority.test.ts +++ b/test/outbound-fetch-header-authority.test.ts @@ -36,4 +36,25 @@ describe("outbound header authority", () => { expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); expect(await response.text()).toBe(""); }); + + it("rejects a reviewed GitHub API header name with a noncanonical value", async () => { + const rawFetch = vi.fn(async () => new Response("unexpected network")); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped( + "https://api.github.com/repos/ContextualWisdomLab/noema/installation", + { + method: "GET", + headers: { + authorization: "Bearer sensitive", + accept: "application/json", + }, + }, + ); + + expect(rawFetch).not.toHaveBeenCalled(); + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(await response.text()).toBe(""); + }); }); From b4812ae64423af3087ebbc5f7cbac0085b5cb6f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:34:54 -0700 Subject: [PATCH 487/564] test(egress): cover rejected-header short circuit --- test/outbound-fetch-header-authority.test.ts | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/outbound-fetch-header-authority.test.ts b/test/outbound-fetch-header-authority.test.ts index 39c24cdf2..14b11114e 100644 --- a/test/outbound-fetch-header-authority.test.ts +++ b/test/outbound-fetch-header-authority.test.ts @@ -57,4 +57,26 @@ describe("outbound header authority", () => { expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); expect(await response.text()).toBe(""); }); + + it("does not let later reviewed headers rehabilitate an earlier rejected header", async () => { + const rawFetch = vi.fn(async () => new Response("unexpected network")); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped( + "https://api.github.com/repos/ContextualWisdomLab/noema/installation", + { + method: "GET", + headers: { + "a-unreviewed-header": "forbidden", + accept: "application/vnd.github+json", + authorization: "Bearer sensitive", + }, + }, + ); + + expect(rawFetch).not.toHaveBeenCalled(); + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(await response.text()).toBe(""); + }); }); From f4527e943d16d9eedfbd770ebb1215fe44e07744 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:39:44 -0700 Subject: [PATCH 488/564] test(egress): prove reviewed production header contract --- test/outbound-fetch-header-authority.test.ts | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/outbound-fetch-header-authority.test.ts b/test/outbound-fetch-header-authority.test.ts index 14b11114e..3489ad6bb 100644 --- a/test/outbound-fetch-header-authority.test.ts +++ b/test/outbound-fetch-header-authority.test.ts @@ -37,6 +37,27 @@ describe("outbound header authority", () => { expect(await response.text()).toBe(""); }); + it("accepts the exact production-reviewed GitHub API header set", async () => { + const rawFetch = vi.fn(async () => Response.json({ id: 12345 })); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped( + "https://api.github.com/repos/ContextualWisdomLab/noema/installation", + { + method: "GET", + headers: { + accept: "application/vnd.github+json", + authorization: "Bearer sensitive", + "user-agent": "noema", + "x-github-api-version": "2022-11-28", + }, + }, + ); + + expect(response.status).toBe(200); + expect(rawFetch).toHaveBeenCalledOnce(); + }); + it("rejects a reviewed GitHub API header name with a noncanonical value", async () => { const rawFetch = vi.fn(async () => new Response("unexpected network")); const wrapped = createFailClosedFetch(rawFetch); From cf3a75668e3220021f6bb8d6536fea6ddce55583 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 06:01:13 -0700 Subject: [PATCH 489/564] test(github-app): bind configured installation to repository --- ...xplicit-installation-id-validation.test.ts | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/test/github-app-explicit-installation-id-validation.test.ts b/test/github-app-explicit-installation-id-validation.test.ts index 373e64afc..084c33d96 100644 --- a/test/github-app-explicit-installation-id-validation.test.ts +++ b/test/github-app-explicit-installation-id-validation.test.ts @@ -130,6 +130,67 @@ async function exchangeWithConfiguredInstallationId(installationId: string, clie return { response, githubApiCalls }; } +async function exchangeWithRepositoryBoundInstallationId( + configuredInstallationId: string, + discoveredInstallationId: number, + clientIp: string, +) { + const { token, jwk } = await signedOidcToken(); + const githubApiCalls: string[] = []; + const repositoryInstallationUrl = + "https://api.github.com/repos/ContextualWisdomLab/noema/installation"; + const installationTokenUrl = + `https://api.github.com/app/installations/${configuredInstallationId}/access_tokens`; + + 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.push(url); + if (url === repositoryInstallationUrl) { + return Response.json({ id: discoveredInstallationId }, { status: 200 }); + } + if (url === installationTokenUrl) { + return Response.json({ + token: "ghs_repository_scoped", + expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), + }, { status: 201 }); + } + return new Response("unexpected GitHub API 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": clientIp, + }, + body: JSON.stringify({ target_repository: "ContextualWisdomLab/noema" }), + }), + { + ...baseEnv, + GITHUB_APP_PRIVATE_KEY_PEM: appPrivateKeyPem, + GITHUB_APP_INSTALLATION_ID: configuredInstallationId, + }, + ); + + return { + response, + githubApiCalls, + repositoryInstallationUrl, + installationTokenUrl, + }; +} + describe("configured GitHub App installation id", () => { it.each([ ["0", "203.0.113.250"], @@ -149,4 +210,50 @@ describe("configured GitHub App installation id", () => { }); expect(githubApiCalls).toBe(0); }); + + it("rejects a syntactically valid configured id that belongs to a different installation", async () => { + const { + response, + githubApiCalls, + repositoryInstallationUrl, + } = await exchangeWithRepositoryBoundInstallationId( + "12345", + 67890, + "203.0.113.248", + ); + + expect(response.status).toBe(500); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error_code: "ERR_GITHUB_INSTALLATION", + message: "GitHub App installation id does not match target repository", + }); + expect(githubApiCalls).toEqual([repositoryInstallationUrl]); + }); + + it("mints only after a configured id is verified against the target repository", async () => { + const { + response, + githubApiCalls, + repositoryInstallationUrl, + installationTokenUrl, + } = await exchangeWithRepositoryBoundInstallationId( + "12346", + 12346, + "203.0.113.247", + ); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + ok: true, + data: { + token: "ghs_repository_scoped", + repository: "ContextualWisdomLab/noema", + }, + }); + expect(githubApiCalls).toEqual([ + repositoryInstallationUrl, + installationTokenUrl, + ]); + }); }); From e87212dc2086859d975cc674e36d3f74799ab4cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 06:07:01 -0700 Subject: [PATCH 490/564] fix(github-app): verify configured installation ownership --- src/index.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index ec0b58efd..c519268e9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -815,8 +815,8 @@ async function resolveInstallationId( repository: string, env: Env, ): Promise { - if (env.GITHUB_APP_INSTALLATION_ID) { - const configuredInstallationId = env.GITHUB_APP_INSTALLATION_ID; + const configuredInstallationId = env.GITHUB_APP_INSTALLATION_ID; + if (configuredInstallationId) { if (!/^[1-9]\d*$/.test(configuredInstallationId)) { throw new ApiError("ERR_GITHUB_INSTALLATION", 500, "GitHub App installation id configuration is invalid"); } @@ -824,11 +824,10 @@ async function resolveInstallationId( if (!Number.isSafeInteger(numericInstallationId) || String(numericInstallationId) !== configuredInstallationId) { throw new ApiError("ERR_GITHUB_INSTALLATION", 500, "GitHub App installation id configuration is invalid"); } - return { value: configuredInstallationId, source: "configured" }; } const now = Date.now(); const cacheKey = `${env.GITHUB_API_BASE}:${env.GITHUB_APP_ID}:${repository}`; - const cached = installationIdCache.get(cacheKey); + const cached = configuredInstallationId ? undefined : installationIdCache.get(cacheKey); if (cached && cached.expiresAtMs > now) { return { value: cached.value, source: "cache" }; } @@ -850,11 +849,23 @@ async function resolveInstallationId( throw new ApiError("ERR_GITHUB_API", 502, "GitHub API returned invalid installation response"); } const installationId = String(installation.id); - installationIdCache.set(cacheKey, { + if (configuredInstallationId && installationId !== configuredInstallationId) { + throw new ApiError( + "ERR_GITHUB_INSTALLATION", + 500, + "GitHub App installation id does not match target repository", + ); + } + if (!configuredInstallationId) { + installationIdCache.set(cacheKey, { + value: installationId, + expiresAtMs: now + configuredTtlMs(env.NOEMA_INSTALLATION_CACHE_TTL_SECONDS, 600, 3600), + }); + } + return { value: installationId, - expiresAtMs: now + configuredTtlMs(env.NOEMA_INSTALLATION_CACHE_TTL_SECONDS, 600, 3600), - }); - return { value: installationId, source: "discovery" }; + source: configuredInstallationId ? "configured" : "discovery", + }; } async function createInstallationToken(repository: string, env: Env): Promise { From 71859c3b00ac669b4257dfe0e0fec1c614ca6a7d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 06:19:00 -0700 Subject: [PATCH 491/564] test(github-app): reflect repository-bound configured installs --- test/github-api-content-type-authority.test.ts | 3 +++ test/github-api-malformed-json.test.ts | 6 ++++++ test/github-app-runtime-coverage.test.ts | 6 ++++++ test/github-installation-expiry-defensive-coverage.test.ts | 1 + ...hub-installation-token-expiry-calendar-integrity.test.ts | 3 +++ 5 files changed, 19 insertions(+) diff --git a/test/github-api-content-type-authority.test.ts b/test/github-api-content-type-authority.test.ts index 7e1596528..a8dc0e032 100644 --- a/test/github-api-content-type-authority.test.ts +++ b/test/github-api-content-type-authority.test.ts @@ -117,6 +117,9 @@ async function exchangeWithInstallationTokenResponse( if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { return Response.json({ keys: [jwk] }); } + if (url === "https://api.github.com/repos/ContextualWisdomLab/.github/installation") { + return Response.json({ id: 92345 }); + } if (url === "https://api.github.com/app/installations/92345/access_tokens") { return installationTokenResponse; } diff --git a/test/github-api-malformed-json.test.ts b/test/github-api-malformed-json.test.ts index df4281b55..4a0b066a7 100644 --- a/test/github-api-malformed-json.test.ts +++ b/test/github-api-malformed-json.test.ts @@ -110,6 +110,12 @@ async function exchangeWith( if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { return Response.json({ keys: [jwk] }); } + if ( + env.GITHUB_APP_INSTALLATION_ID + && url === `https://api.github.com/repos/${targetRepository}/installation` + ) { + return Response.json({ id: Number(env.GITHUB_APP_INSTALLATION_ID) }); + } return githubHandler(url); }); diff --git a/test/github-app-runtime-coverage.test.ts b/test/github-app-runtime-coverage.test.ts index 3bf777848..0f9b90f32 100644 --- a/test/github-app-runtime-coverage.test.ts +++ b/test/github-app-runtime-coverage.test.ts @@ -111,6 +111,12 @@ async function exchange( if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { return Response.json({ keys: [jwk] }); } + if ( + env.GITHUB_APP_INSTALLATION_ID + && url === `https://api.github.com/repos/${targetRepository}/installation` + ) { + return Response.json({ id: Number(env.GITHUB_APP_INSTALLATION_ID) }); + } return githubHandler(url, init); }); diff --git a/test/github-installation-expiry-defensive-coverage.test.ts b/test/github-installation-expiry-defensive-coverage.test.ts index ea548c2c7..07edea0d2 100644 --- a/test/github-installation-expiry-defensive-coverage.test.ts +++ b/test/github-installation-expiry-defensive-coverage.test.ts @@ -56,6 +56,7 @@ async function exchangeWithTokenResponse(tokenBody: unknown, clientIp: string): 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/repos/ContextualWisdomLab/noema/installation") return Response.json({ id: 92345 }); if (url === "https://api.github.com/app/installations/92345/access_tokens") return Response.json(tokenBody, { status: 201 }); return new Response("unexpected", { status: 500 }); }); diff --git a/test/github-installation-token-expiry-calendar-integrity.test.ts b/test/github-installation-token-expiry-calendar-integrity.test.ts index 01c60b151..793fac9e8 100644 --- a/test/github-installation-token-expiry-calendar-integrity.test.ts +++ b/test/github-installation-token-expiry-calendar-integrity.test.ts @@ -110,6 +110,9 @@ describe("GitHub installation-token expiry calendar integrity", () => { if (url === "https://token.actions.githubusercontent.com/.well-known/jwks") { return Response.json({ keys: [jwk] }); } + if (url === "https://api.github.com/repos/ContextualWisdomLab/expiry-calendar/installation") { + return Response.json({ id: 92345 }); + } if (url === "https://api.github.com/app/installations/92345/access_tokens") { return Response.json({ token: "ghs_impossible_calendar_expiry", From 355ca3d0ed1745fdcb520ecc6993fb3a34bd48a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 06:32:26 -0700 Subject: [PATCH 492/564] test(egress): require canonical Bearer framing --- test/outbound-request-compartment.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/outbound-request-compartment.test.ts b/test/outbound-request-compartment.test.ts index b57e68210..eda3a16d5 100644 --- a/test/outbound-request-compartment.test.ts +++ b/test/outbound-request-compartment.test.ts @@ -121,6 +121,11 @@ describe("outbound credential request compartmentalization", () => { repositoryInstallationUrl, { headers: { authorization: "Basic app-jwt" } }, ], + [ + "a noncanonical bearer scheme", + repositoryInstallationUrl, + { headers: { authorization: "bearer app-jwt" } }, + ], [ "a cookie", repositoryInstallationUrl, @@ -177,4 +182,4 @@ describe("outbound credential request compartmentalization", () => { expect(await response.text()).toBe(""); } }); -}); +}); \ No newline at end of file From cb7cf4dabff432f8680939101f6607d9cb8aecd6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 06:35:52 -0700 Subject: [PATCH 493/564] fix(egress): enforce canonical Bearer scheme --- src/outbound-fetch-policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index da1829a9e..95e8de568 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -370,7 +370,7 @@ export function isTrustedCredentialEgressRequest( rawAuthorization === undefined || rawAuthorization === null || rawAuthorization !== authorization - || !/^Bearer [\x21-\x7e]+$/i.test(authorization) + || !/^Bearer [\x21-\x7e]+$/.test(authorization) ) { return false; } From 69cde350a108d3d98a175b284b51c7e190751bab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 07:01:58 -0700 Subject: [PATCH 494/564] test(egress): reject non-canonical HTTP method casing --- ...outbound-fetch-method-canonicality.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/outbound-fetch-method-canonicality.test.ts diff --git a/test/outbound-fetch-method-canonicality.test.ts b/test/outbound-fetch-method-canonicality.test.ts new file mode 100644 index 000000000..86b4b1115 --- /dev/null +++ b/test/outbound-fetch-method-canonicality.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { isTrustedCredentialEgressRequest } from "../src/outbound-fetch-policy"; + +const installationLookup = "https://api.github.com/repos/ContextualWisdomLab/noema/installation"; +const installationToken = "https://api.github.com/app/installations/123/access_tokens"; + +describe("credential-egress HTTP method canonicality", () => { + it.each(["get", "Get", "gEt"])( + "rejects non-canonical repository-installation method bytes before credential egress: %s", + (method) => { + expect(isTrustedCredentialEgressRequest(installationLookup, { + method, + headers: { authorization: "Bearer canonical-token" }, + })).toBe(false); + }, + ); + + it.each(["post", "Post", "pOsT"])( + "rejects non-canonical installation-token method bytes before credential egress: %s", + (method) => { + expect(isTrustedCredentialEgressRequest(installationToken, { + method, + headers: { + authorization: "Bearer canonical-token", + "content-type": "application/json", + }, + body: JSON.stringify({ + permissions: { checks: "read", contents: "read", pull_requests: "write" }, + repositories: ["noema"], + }), + })).toBe(false); + }, + ); +}); From 172cc90acccb380e50efe677919856815ab62a18 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 07:06:11 -0700 Subject: [PATCH 495/564] fix(egress): preserve canonical HTTP method bytes --- src/outbound-fetch-policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 95e8de568..229799c15 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -71,7 +71,7 @@ function outboundUrl(input: RequestInfo | URL): URL | undefined { } function outboundMethod(input: RequestInfo | URL, init: RequestInit | undefined): string { - return (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase(); + return init?.method ?? (input instanceof Request ? input.method : "GET"); } function outboundHeaders(input: RequestInfo | URL, init: RequestInit | undefined): Headers { From 39d9b6374a6363ddc300e0f6ef4fe2fe2992f99e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 08:02:50 -0700 Subject: [PATCH 496/564] test(egress): fail closed on malformed request headers --- test/outbound-request-compartment.test.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/outbound-request-compartment.test.ts b/test/outbound-request-compartment.test.ts index eda3a16d5..48c7213e5 100644 --- a/test/outbound-request-compartment.test.ts +++ b/test/outbound-request-compartment.test.ts @@ -182,4 +182,25 @@ describe("outbound credential request compartmentalization", () => { expect(await response.text()).toBe(""); } }); + + it.each([ + ["an invalid header name", { "bad header": "value" }], + ["a newline-bearing authorization value", { authorization: "Bearer app-jwt\r\nx-leak: secret" }], + ] satisfies Array<[string, Record]>) ( + "fails closed before the network call when Headers rejects %s", + async (_label, headers) => { + const rawFetch = vi.fn(); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped(repositoryInstallationUrl, { + method: "GET", + headers, + }); + + expect(rawFetch).not.toHaveBeenCalled(); + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(await response.text()).toBe(""); + }, + ); }); \ No newline at end of file From 0239b298ff22c6662a670d8e76f07442fe94edd0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 08:07:21 -0700 Subject: [PATCH 497/564] fix(egress): fail closed on malformed headers --- src/outbound-fetch-policy.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 229799c15..11b5807a4 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -74,10 +74,17 @@ function outboundMethod(input: RequestInfo | URL, init: RequestInit | undefined) return init?.method ?? (input instanceof Request ? input.method : "GET"); } -function outboundHeaders(input: RequestInfo | URL, init: RequestInit | undefined): Headers { - if (init?.headers !== undefined) return new Headers(init.headers); - if (input instanceof Request) return new Headers(input.headers); - return new Headers(); +function outboundHeaders( + input: RequestInfo | URL, + init: RequestInit | undefined, +): Headers | undefined { + try { + if (init?.headers !== undefined) return new Headers(init.headers); + if (input instanceof Request) return new Headers(input.headers); + return new Headers(); + } catch { + return undefined; + } } function hasNoHeaders(headers: Headers): boolean { @@ -287,11 +294,13 @@ function withCanonicalInstallationTokenMediaType( init: RequestInit | undefined, ): RequestInit | undefined { const url = outboundUrl(input); + const parsedHeaders = outboundHeaders(input, init); if ( !url || githubApiOperation(url) !== "installation-token" || typeof init?.body !== "string" - || new Headers(init.headers).has("content-type") + || !parsedHeaders + || parsedHeaders.has("content-type") || init.headers instanceof Headers ) { return init; @@ -346,6 +355,7 @@ export function isTrustedCredentialEgressRequest( const url = outboundUrl(input)!; const method = outboundMethod(input, init); const headers = outboundHeaders(input, init); + if (!headers) return false; const bodyPresent = outboundBodyPresent(input, init); if (url.origin === TRUSTED_GITHUB_OIDC_ORIGIN) { @@ -432,7 +442,7 @@ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { if (signal.aborted) { throw error; } - if (outboundHeaders(input, effectiveInit).has("authorization")) { + if (outboundHeaders(input, effectiveInit)?.has("authorization")) { return blockedResponse("transport"); } throw error; @@ -498,4 +508,4 @@ export function resetGlobalOutboundFetchPolicy( } } installations.delete(key); -} +} \ No newline at end of file From ab15f39f8378515f51b0c133c17b9a645b1ac1cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 08:59:26 -0700 Subject: [PATCH 498/564] test(egress): require explicit installation token media authority --- test/outbound-fetch-token-media-type.test.ts | 24 +++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/test/outbound-fetch-token-media-type.test.ts b/test/outbound-fetch-token-media-type.test.ts index 3a0d51123..a52c1db1f 100644 --- a/test/outbound-fetch-token-media-type.test.ts +++ b/test/outbound-fetch-token-media-type.test.ts @@ -19,8 +19,8 @@ describe("installation-token outbound media type", () => { it.each([ ["record", { authorization: "Bearer app-jwt" }], ["tuple list", [["authorization", "Bearer app-jwt"]] as [string, string][]], - ])("adds exact application/json before %s headers cross the network boundary", async (_label, headers) => { - const rawFetch = vi.fn(async () => Response.json({ token: "issued" })); + ])("rejects %s headers when the caller omits required application/json media authority", async (_label, headers) => { + const rawFetch = vi.fn(); const wrapped = createFailClosedFetch(rawFetch); const response = await wrapped(tokenUrl, { @@ -29,6 +29,24 @@ describe("installation-token outbound media type", () => { body: tokenBody, }); + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(rawFetch).not.toHaveBeenCalled(); + }); + + it("forwards exact application/json only when the caller explicitly supplies it", async () => { + const rawFetch = vi.fn(async () => Response.json({ token: "issued" })); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped(tokenUrl, { + method: "POST", + headers: { + authorization: "Bearer app-jwt", + "content-type": "application/json", + }, + body: tokenBody, + }); + expect(response.status).toBe(200); expect(rawFetch).toHaveBeenCalledOnce(); const forwarded = new Headers(rawFetch.mock.calls[0][1]?.headers); @@ -36,7 +54,7 @@ describe("installation-token outbound media type", () => { expect(forwarded.get("content-type")).toBe("application/json"); }); - it("does not manufacture authorization for a headerless token request while adding its media type", async () => { + it("does not manufacture authorization or media type for a headerless token request", async () => { const rawFetch = vi.fn(); const wrapped = createFailClosedFetch(rawFetch); From b5eb23fa4e69161c4edaea7ea9c8c0dfef2c5e38 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 09:01:48 -0700 Subject: [PATCH 499/564] test(egress): preserve canonical media-type synthesis contract --- test/outbound-fetch-token-media-type.test.ts | 24 +++----------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/test/outbound-fetch-token-media-type.test.ts b/test/outbound-fetch-token-media-type.test.ts index a52c1db1f..3a0d51123 100644 --- a/test/outbound-fetch-token-media-type.test.ts +++ b/test/outbound-fetch-token-media-type.test.ts @@ -19,31 +19,13 @@ describe("installation-token outbound media type", () => { it.each([ ["record", { authorization: "Bearer app-jwt" }], ["tuple list", [["authorization", "Bearer app-jwt"]] as [string, string][]], - ])("rejects %s headers when the caller omits required application/json media authority", async (_label, headers) => { - const rawFetch = vi.fn(); - const wrapped = createFailClosedFetch(rawFetch); - - const response = await wrapped(tokenUrl, { - method: "POST", - headers, - body: tokenBody, - }); - - expect(response.status).toBe(502); - expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); - expect(rawFetch).not.toHaveBeenCalled(); - }); - - it("forwards exact application/json only when the caller explicitly supplies it", async () => { + ])("adds exact application/json before %s headers cross the network boundary", async (_label, headers) => { const rawFetch = vi.fn(async () => Response.json({ token: "issued" })); const wrapped = createFailClosedFetch(rawFetch); const response = await wrapped(tokenUrl, { method: "POST", - headers: { - authorization: "Bearer app-jwt", - "content-type": "application/json", - }, + headers, body: tokenBody, }); @@ -54,7 +36,7 @@ describe("installation-token outbound media type", () => { expect(forwarded.get("content-type")).toBe("application/json"); }); - it("does not manufacture authorization or media type for a headerless token request", async () => { + it("does not manufacture authorization for a headerless token request while adding its media type", async () => { const rawFetch = vi.fn(); const wrapped = createFailClosedFetch(rawFetch); From 07c9b130950529ebb8ecaf64737d4fbe6c72d35d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 09:02:43 -0700 Subject: [PATCH 500/564] test(egress): fail closed on malformed cancellation authority --- test/outbound-fetch-policy.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/outbound-fetch-policy.test.ts b/test/outbound-fetch-policy.test.ts index 8804179b1..25efb0aad 100644 --- a/test/outbound-fetch-policy.test.ts +++ b/test/outbound-fetch-policy.test.ts @@ -247,6 +247,24 @@ describe("credential-bearing outbound fetch policy", () => { await expect(pending).rejects.toBe(reason); }); + it("fails closed before credential egress when caller cancellation authority is malformed", async () => { + const rawFetch = vi.fn(); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped( + "https://api.github.com/repos/ContextualWisdomLab/noema/installation", + { + method: "GET", + headers: { authorization: "Bearer sensitive" }, + signal: {} as AbortSignal, + }, + ); + + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(rawFetch).not.toHaveBeenCalled(); + }); + it("rethrows non-timeout network failures unchanged", async () => { const failure = new TypeError("network unavailable"); const rawFetch = vi.fn(async () => { From 5a550d5fab903e009dfad50587acc81f84d375dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 09:05:07 -0700 Subject: [PATCH 501/564] fix(egress): fail closed on invalid cancellation signals --- src/outbound-fetch-policy.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 11b5807a4..29536732d 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -197,11 +197,15 @@ function boundedOutboundSignal( input: RequestInfo | URL, init: RequestInit | undefined, timeoutSignal: AbortSignal, -): AbortSignal { - const signals = [timeoutSignal]; - if (input instanceof Request) signals.push(input.signal); - if (init?.signal) signals.push(init.signal); - return AbortSignal.any(signals); +): AbortSignal | undefined { + try { + const signals = [timeoutSignal]; + if (input instanceof Request) signals.push(input.signal); + if (init?.signal) signals.push(init.signal); + return AbortSignal.any(signals); + } catch { + return undefined; + } } function ignoreCancellationBestEffort(cancel: () => Promise): void { @@ -419,6 +423,10 @@ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { OUTBOUND_FETCH_TIMEOUT_MS, ); const signal = boundedOutboundSignal(input, effectiveInit, timeoutController.signal); + if (!signal) { + clearTimeout(timeoutHandle); + return blockedResponse("request-policy"); + } try { const response = await rawFetch(input, { From e135ad9315cb221cd8273c0e1812c387a8903e0b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 09:11:53 -0700 Subject: [PATCH 502/564] test(egress): classify streamed deadline expiry as timeout --- test/outbound-fetch-stream-timeout.test.ts | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 test/outbound-fetch-stream-timeout.test.ts diff --git a/test/outbound-fetch-stream-timeout.test.ts b/test/outbound-fetch-stream-timeout.test.ts new file mode 100644 index 000000000..9a42bf740 --- /dev/null +++ b/test/outbound-fetch-stream-timeout.test.ts @@ -0,0 +1,36 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + createFailClosedFetch, + type FetchLike, +} from "../src/outbound-fetch-policy"; + +describe("credential-egress streamed response deadlines", () => { + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it("returns the bounded timeout response when the deadline expires while reading the response body", async () => { + vi.useFakeTimers(); + const rawFetch = vi.fn(async (_input, init) => { + const signal = init?.signal; + if (!signal) throw new Error("expected bounded outbound signal"); + const body = new ReadableStream({ + start(controller) { + signal.addEventListener("abort", () => controller.error(signal.reason), { once: true }); + }, + }); + return new Response(body); + }); + const wrapped = createFailClosedFetch(rawFetch); + + const pending = wrapped("https://api.github.com/meta"); + await vi.advanceTimersByTimeAsync(10_000); + const response = await pending; + + expect(response.status).toBe(504); + expect(response.statusText).toBe("Gateway Timeout"); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-timeout"); + expect(await response.text()).toBe(""); + }); +}); From c19549bda7359266f2782ea3807f2e6dbcf282a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 09:13:15 -0700 Subject: [PATCH 503/564] fix(egress): preserve streamed timeout authority --- src/outbound-fetch-policy.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 29536732d..af0e0ff77 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -216,7 +216,10 @@ function ignoreCancellationBestEffort(cancel: () => Promise): void { } } -async function boundedOutboundResponse(response: Response): Promise { +async function boundedOutboundResponse( + response: Response, + signal: AbortSignal, +): Promise { const declaredLength = response.headers.get("content-length"); if ( declaredLength !== null @@ -249,10 +252,11 @@ async function boundedOutboundResponse(response: Response): Promise { } chunks.push(value); } - } catch { + } catch (error) { ignoreCancellationBestEffort(() => reader.cancel( "Noema outbound response body could not be read", )); + if (signal.aborted) throw error; return blockedResponse("response-read"); } @@ -442,7 +446,7 @@ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { } return blockedResponse("redirect"); } - return await boundedOutboundResponse(response); + return await boundedOutboundResponse(response, signal); } catch (error) { if (signal.aborted && signal.reason === timeoutReason) { return blockedResponse("timeout"); From 2c065dfee45773623dd07754488a76264b9ef25f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:02:51 -0700 Subject: [PATCH 504/564] test(egress): reject pre-aborted caller authority before transport --- test/outbound-fetch-preaborted-signal.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 test/outbound-fetch-preaborted-signal.test.ts diff --git a/test/outbound-fetch-preaborted-signal.test.ts b/test/outbound-fetch-preaborted-signal.test.ts new file mode 100644 index 000000000..2e2ce74c3 --- /dev/null +++ b/test/outbound-fetch-preaborted-signal.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it, vi } from "vitest"; +import { + createFailClosedFetch, + type FetchLike, +} from "../src/outbound-fetch-policy"; + +describe("credential-egress caller cancellation authority", () => { + it("never enters the raw transport when the caller signal is already aborted", async () => { + const rawFetch = vi.fn(async () => new Response("unexpected transport")); + const wrapped = createFailClosedFetch(rawFetch); + const caller = new AbortController(); + const reason = new DOMException("caller cancelled before egress", "AbortError"); + caller.abort(reason); + + await expect(wrapped( + "https://api.github.com/repos/ContextualWisdomLab/noema/installation", + { + method: "GET", + headers: { authorization: "Bearer sensitive" }, + signal: caller.signal, + }, + )).rejects.toBe(reason); + expect(rawFetch).not.toHaveBeenCalled(); + }); +}); From 7615cfcfc9376a7d9b531d896f2cda02e6fa43b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:04:41 -0700 Subject: [PATCH 505/564] fix(egress): honor pre-aborted caller signal before transport --- src/outbound-fetch-policy.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index af0e0ff77..05a867be7 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -431,6 +431,10 @@ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { clearTimeout(timeoutHandle); return blockedResponse("request-policy"); } + if (signal.aborted) { + clearTimeout(timeoutHandle); + throw signal.reason; + } try { const response = await rawFetch(input, { From 6e71cbf05f7840355766cb2cf17146a7bd0a01a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:07:15 -0700 Subject: [PATCH 506/564] test(ci): require stale image runs to cancel --- test/workflow-concurrency-policy.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/workflow-concurrency-policy.test.ts b/test/workflow-concurrency-policy.test.ts index 68bda8b6b..ce42f2c74 100644 --- a/test/workflow-concurrency-policy.test.ts +++ b/test/workflow-concurrency-policy.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest"; const workflowPaths = [ ["ci", ".github/workflows/ci.yml"], ["reviewer-ci", ".github/workflows/reviewer-ci.yml"], + ["patch-validator-image", ".github/workflows/patch-validator-image.yml"], ] as const; describe("pull-request workflow execution policy", () => { From 8819ec68b430e8af153748ee3aae9b8ead12ec01 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:09:02 -0700 Subject: [PATCH 507/564] fix(ci): cancel superseded image verification runs --- .github/workflows/patch-validator-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/patch-validator-image.yml b/.github/workflows/patch-validator-image.yml index b59f3eb49..1d5792f50 100644 --- a/.github/workflows/patch-validator-image.yml +++ b/.github/workflows/patch-validator-image.yml @@ -6,7 +6,7 @@ on: concurrency: group: noema-patch-validator-image-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: false + cancel-in-progress: true permissions: contents: read From 0c3f1f38e8344dc47f683f8e22c652e9f73dd296 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:12:09 -0700 Subject: [PATCH 508/564] test(ci): require resilient image scanner downloads --- test/patch-validator-image-build-cache.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/patch-validator-image-build-cache.test.ts b/test/patch-validator-image-build-cache.test.ts index b2cc079d4..6e8b9f677 100644 --- a/test/patch-validator-image-build-cache.test.ts +++ b/test/patch-validator-image-build-cache.test.ts @@ -22,10 +22,18 @@ describe("patch-validator image build cache", () => { ); }); - it("lets an in-flight exact-head build finish exporting the shared cache", () => { + it("cancels superseded exact-head builds instead of spending the serial image lane on stale evidence", () => { expect(workflow).toContain( "group: noema-patch-validator-image-${{ github.event.pull_request.number || github.ref }}", ); - expect(workflow).toContain("cancel-in-progress: false"); + expect(workflow).toContain("cancel-in-progress: true"); + }); + + it("retries transient scanner release download failures before failing closed", () => { + expect(workflow).toContain("download_scanner_asset() {"); + expect(workflow).toContain("--retry 3"); + expect(workflow).toContain("--retry-all-errors"); + expect(workflow).toContain("--retry-delay 2"); + expect(workflow).toContain("--retry-max-time 60"); }); }); From bd6e9369a8cc9d9d14ee32f365c3bbf96618121f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:17:25 -0700 Subject: [PATCH 509/564] fix(ci): retry transient scanner release downloads --- .github/workflows/patch-validator-image.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/patch-validator-image.yml b/.github/workflows/patch-validator-image.yml index 1d5792f50..e9998ae6d 100644 --- a/.github/workflows/patch-validator-image.yml +++ b/.github/workflows/patch-validator-image.yml @@ -80,6 +80,17 @@ jobs: scanner_dir="$RUNNER_TEMP/noema-binary-scanners" mkdir -p "$scanner_dir" + download_scanner_asset() { + destination="$1" + url="$2" + curl --proto '=https' --tlsv1.2 --location --fail --silent --show-error \ + --retry 3 \ + --retry-all-errors \ + --retry-delay 2 \ + --retry-max-time 60 \ + --output "$destination" "$url" + } + install_scanner() { scanner="$1" version="$2" @@ -88,12 +99,10 @@ jobs: checksums="${scanner}_${version}_checksums.txt" release_base="https://github.com/anchore/${scanner}/releases/download/v${version}" - curl --proto '=https' --tlsv1.2 --location --fail --silent --show-error \ - --output "$scanner_dir/$checksums" "$release_base/$checksums" + download_scanner_asset "$scanner_dir/$checksums" "$release_base/$checksums" printf '%s %s\n' "$checksums_sha256" "$scanner_dir/$checksums" | sha256sum --check --strict - curl --proto '=https' --tlsv1.2 --location --fail --silent --show-error \ - --output "$scanner_dir/$archive" "$release_base/$archive" + download_scanner_asset "$scanner_dir/$archive" "$release_base/$archive" ( cd "$scanner_dir" grep -E "^[0-9a-f]{64} ${archive}$" "$checksums" | sha256sum --check --strict From 1208839ab951f5a96198ae205ac63588a552d3ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:29:23 -0700 Subject: [PATCH 510/564] test(egress): preserve mid-flight caller cancellation --- test/outbound-fetch-preaborted-signal.test.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/outbound-fetch-preaborted-signal.test.ts b/test/outbound-fetch-preaborted-signal.test.ts index 2e2ce74c3..e9259d4c5 100644 --- a/test/outbound-fetch-preaborted-signal.test.ts +++ b/test/outbound-fetch-preaborted-signal.test.ts @@ -22,4 +22,35 @@ describe("credential-egress caller cancellation authority", () => { )).rejects.toBe(reason); expect(rawFetch).not.toHaveBeenCalled(); }); + + it("preserves mid-flight caller cancellation even when the transport ignores the abort signal", async () => { + let resolveTransport!: (response: Response) => void; + const transport = new Promise((resolve) => { + resolveTransport = resolve; + }); + const rawFetch = vi.fn(async (_input, init) => { + expect(init?.signal?.aborted).toBe(false); + return transport; + }); + const wrapped = createFailClosedFetch(rawFetch); + const caller = new AbortController(); + const reason = new DOMException("caller cancelled during egress", "AbortError"); + + const pending = wrapped( + "https://api.github.com/repos/ContextualWisdomLab/noema/installation", + { + method: "GET", + headers: { authorization: "Bearer sensitive" }, + signal: caller.signal, + }, + ); + await vi.waitFor(() => expect(rawFetch).toHaveBeenCalledTimes(1)); + caller.abort(reason); + resolveTransport(new Response("{}", { + status: 200, + headers: { "content-type": "application/json" }, + })); + + await expect(pending).rejects.toBe(reason); + }); }); From 804f6eda6ff14f238acab95add46324f2bc5798c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:31:15 -0700 Subject: [PATCH 511/564] fix(egress): honor mid-flight caller cancellation --- src/outbound-fetch-policy.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 05a867be7..ec172a4e7 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -442,6 +442,14 @@ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { redirect: "manual", signal, }); + if (signal.aborted) { + if (response.body !== null) { + ignoreCancellationBestEffort(() => response.body!.cancel( + "Noema outbound response arrived after request authority was revoked", + )); + } + throw signal.reason; + } if (response.redirected || (response.status >= 300 && response.status < 400)) { if (response.body !== null) { ignoreCancellationBestEffort(() => response.body!.cancel( From ffa11e6c3393a3b75a16a4bc5b37299392f4b9ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:34:53 -0700 Subject: [PATCH 512/564] test(egress): cover bodyless late cancellation --- test/outbound-fetch-preaborted-signal.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/outbound-fetch-preaborted-signal.test.ts b/test/outbound-fetch-preaborted-signal.test.ts index e9259d4c5..4c48763b2 100644 --- a/test/outbound-fetch-preaborted-signal.test.ts +++ b/test/outbound-fetch-preaborted-signal.test.ts @@ -53,4 +53,29 @@ describe("credential-egress caller cancellation authority", () => { await expect(pending).rejects.toBe(reason); }); + + it("preserves mid-flight caller cancellation when the late response is bodyless", async () => { + let resolveTransport!: (response: Response) => void; + const transport = new Promise((resolve) => { + resolveTransport = resolve; + }); + const rawFetch = vi.fn(async () => transport); + const wrapped = createFailClosedFetch(rawFetch); + const caller = new AbortController(); + const reason = new DOMException("caller cancelled before a bodyless response", "AbortError"); + + const pending = wrapped( + "https://api.github.com/repos/ContextualWisdomLab/noema/installation", + { + method: "GET", + headers: { authorization: "Bearer sensitive" }, + signal: caller.signal, + }, + ); + await vi.waitFor(() => expect(rawFetch).toHaveBeenCalledTimes(1)); + caller.abort(reason); + resolveTransport(new Response(null, { status: 200 })); + + await expect(pending).rejects.toBe(reason); + }); }); From 28289481453834191c599b38336571c781bd0fbe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:40:13 -0700 Subject: [PATCH 513/564] test(egress): preserve cancellation during body streaming --- test/outbound-fetch-preaborted-signal.test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/outbound-fetch-preaborted-signal.test.ts b/test/outbound-fetch-preaborted-signal.test.ts index 4c48763b2..89a17a3f2 100644 --- a/test/outbound-fetch-preaborted-signal.test.ts +++ b/test/outbound-fetch-preaborted-signal.test.ts @@ -78,4 +78,44 @@ describe("credential-egress caller cancellation authority", () => { await expect(pending).rejects.toBe(reason); }); + + it("preserves caller cancellation while an abort-ignoring response body is still streaming", async () => { + let markPullStarted!: () => void; + const pullStarted = new Promise((resolve) => { + markPullStarted = resolve; + }); + let releaseBody!: () => void; + const bodyReleased = new Promise((resolve) => { + releaseBody = resolve; + }); + const body = new ReadableStream({ + async pull(controller) { + markPullStarted(); + await bodyReleased; + controller.enqueue(new TextEncoder().encode("{}")); + controller.close(); + }, + }); + const rawFetch = vi.fn(async () => new Response(body, { + status: 200, + headers: { "content-type": "application/json" }, + })); + const wrapped = createFailClosedFetch(rawFetch); + const caller = new AbortController(); + const reason = new DOMException("caller cancelled during body streaming", "AbortError"); + + const pending = wrapped( + "https://api.github.com/repos/ContextualWisdomLab/noema/installation", + { + method: "GET", + headers: { authorization: "Bearer sensitive" }, + signal: caller.signal, + }, + ); + await pullStarted; + caller.abort(reason); + releaseBody(); + + await expect(pending).rejects.toBe(reason); + }); }); From 439fa8f9b7a3fac361a8a85662c4411793836fec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:43:17 -0700 Subject: [PATCH 514/564] fix(egress): honor cancellation during body streaming --- src/outbound-fetch-policy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index ec172a4e7..f8a877c48 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -242,6 +242,7 @@ async function boundedOutboundResponse( try { while (true) { const { done, value } = await reader.read(); + if (signal.aborted) throw signal.reason; if (done) break; totalBytes += value.byteLength; if (totalBytes > MAX_OUTBOUND_RESPONSE_BYTES) { From 873496da13214bf0a1dd4f606188b5f57429200f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 11:01:19 -0700 Subject: [PATCH 515/564] test(egress): preserve cancellation over late transport rejection --- test/outbound-fetch-preaborted-signal.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/outbound-fetch-preaborted-signal.test.ts b/test/outbound-fetch-preaborted-signal.test.ts index 89a17a3f2..38fc18292 100644 --- a/test/outbound-fetch-preaborted-signal.test.ts +++ b/test/outbound-fetch-preaborted-signal.test.ts @@ -54,6 +54,32 @@ describe("credential-egress caller cancellation authority", () => { await expect(pending).rejects.toBe(reason); }); + it("preserves caller cancellation when an abort-ignoring transport rejects later for another reason", async () => { + let rejectTransport!: (error: Error) => void; + const transport = new Promise((_resolve, reject) => { + rejectTransport = reject; + }); + const rawFetch = vi.fn(async () => transport); + const wrapped = createFailClosedFetch(rawFetch); + const caller = new AbortController(); + const reason = new DOMException("caller revoked transport authority", "AbortError"); + const lateFailure = new TypeError("late transport failure"); + + const pending = wrapped( + "https://api.github.com/repos/ContextualWisdomLab/noema/installation", + { + method: "GET", + headers: { authorization: "Bearer sensitive" }, + signal: caller.signal, + }, + ); + await vi.waitFor(() => expect(rawFetch).toHaveBeenCalledTimes(1)); + caller.abort(reason); + rejectTransport(lateFailure); + + await expect(pending).rejects.toBe(reason); + }); + it("preserves mid-flight caller cancellation when the late response is bodyless", async () => { let resolveTransport!: (response: Response) => void; const transport = new Promise((resolve) => { From 36775622d8157d7402125562c1df1cedfbbef5a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 11:05:45 -0700 Subject: [PATCH 516/564] fix(egress): preserve caller abort reason on late rejection --- src/outbound-fetch-policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index f8a877c48..757f24d3e 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -465,7 +465,7 @@ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { return blockedResponse("timeout"); } if (signal.aborted) { - throw error; + throw signal.reason; } if (outboundHeaders(input, effectiveInit)?.has("authorization")) { return blockedResponse("transport"); From 2eb620d0d0b065f36df581f8774153acdb3583a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 12:01:48 -0700 Subject: [PATCH 517/564] test(egress): reject normalized reviewed header authority --- test/outbound-fetch-header-authority.test.ts | 61 ++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/test/outbound-fetch-header-authority.test.ts b/test/outbound-fetch-header-authority.test.ts index 3489ad6bb..aa70cff3c 100644 --- a/test/outbound-fetch-header-authority.test.ts +++ b/test/outbound-fetch-header-authority.test.ts @@ -79,6 +79,67 @@ describe("outbound header authority", () => { expect(await response.text()).toBe(""); }); + it.each([ + ["accept", " application/vnd.github+json "], + ["user-agent", " noema "], + ["x-github-api-version", " 2022-11-28 "], + ])("rejects raw whitespace normalization around reviewed %s authority", async (name, value) => { + const rawFetch = vi.fn(async () => new Response("unexpected network")); + const wrapped = createFailClosedFetch(rawFetch); + + const response = await wrapped( + "https://api.github.com/repos/ContextualWisdomLab/noema/installation", + { + method: "GET", + headers: { + accept: "application/vnd.github+json", + authorization: "Bearer sensitive", + "user-agent": "noema", + "x-github-api-version": "2022-11-28", + [name]: value, + }, + }, + ); + + expect(rawFetch).not.toHaveBeenCalled(); + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(await response.text()).toBe(""); + }); + + it("rejects raw whitespace normalization around installation-token content type", async () => { + const rawFetch = vi.fn(async () => new Response("unexpected network")); + const wrapped = createFailClosedFetch(rawFetch); + const body = JSON.stringify({ + repositories: ["noema"], + permissions: { + pull_requests: "write", + contents: "read", + checks: "read", + }, + }); + + const response = await wrapped( + "https://api.github.com/app/installations/123/access_tokens", + { + method: "POST", + headers: { + accept: "application/vnd.github+json", + authorization: "Bearer sensitive", + "content-type": " application/json ", + "user-agent": "noema", + "x-github-api-version": "2022-11-28", + }, + body, + }, + ); + + expect(rawFetch).not.toHaveBeenCalled(); + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + expect(await response.text()).toBe(""); + }); + it("does not let later reviewed headers rehabilitate an earlier rejected header", async () => { const rawFetch = vi.fn(async () => new Response("unexpected network")); const wrapped = createFailClosedFetch(rawFetch); From 36bfd2c6bd8b7b645af9a43d8382ea2c796b3ab9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 12:03:54 -0700 Subject: [PATCH 518/564] fix(egress): preserve raw reviewed header authority --- src/outbound-fetch-policy.ts | 39 ++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 757f24d3e..3cae761f4 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -95,13 +95,34 @@ function hasNoHeaders(headers: Headers): boolean { return empty; } +function rawHeaderValueFromInit( + headersInit: HeadersInit | undefined, + headerName: string, +): string | null | undefined { + if (headersInit === undefined || headersInit instanceof Headers) return undefined; + const entries = Array.isArray(headersInit) ? headersInit : Object.entries(headersInit); + let rawValue: string | undefined; + for (const [name, value] of entries) { + if (name.toLowerCase() !== headerName) continue; + if (rawValue !== undefined) return null; + rawValue = value; + } + return rawValue; +} + function hasOnlyReviewedGithubApiHeaders( headers: Headers, operation: GitHubApiOperation | undefined, + headersInit: HeadersInit | undefined, ): boolean { let reviewed = true; headers.forEach((value, name) => { if (!reviewed || name === "authorization") return; + const rawValue = rawHeaderValueFromInit(headersInit, name); + if (rawValue === null || (rawValue !== undefined && rawValue !== value)) { + reviewed = false; + return; + } if ( (name === "accept" && value === "application/vnd.github+json") || (name === "user-agent" && value === "noema") @@ -119,18 +140,6 @@ function hasOnlyReviewedGithubApiHeaders( return reviewed; } -function rawAuthorizationHeaderFromInit(headersInit: HeadersInit | undefined): string | null | undefined { - if (headersInit === undefined || headersInit instanceof Headers) return undefined; - const entries = Array.isArray(headersInit) ? headersInit : Object.entries(headersInit); - let authorization: string | undefined; - for (const [name, value] of entries) { - if (name.toLowerCase() !== "authorization") continue; - if (authorization !== undefined) return null; - authorization = value; - } - return authorization; -} - function outboundBodyPresent(input: RequestInfo | URL, init: RequestInit | undefined): boolean { if ( init @@ -374,7 +383,7 @@ export function isTrustedCredentialEgressRequest( } const operation = githubApiOperation(url); - if (!hasOnlyReviewedGithubApiHeaders(headers, operation)) { + if (!hasOnlyReviewedGithubApiHeaders(headers, operation, init?.headers)) { return false; } @@ -384,7 +393,7 @@ export function isTrustedCredentialEgressRequest( && method === "GET" && !bodyPresent; } - const rawAuthorization = rawAuthorizationHeaderFromInit(init?.headers); + const rawAuthorization = rawHeaderValueFromInit(init?.headers, "authorization"); if ( rawAuthorization === undefined || rawAuthorization === null @@ -533,4 +542,4 @@ export function resetGlobalOutboundFetchPolicy( } } installations.delete(key); -} \ No newline at end of file +} From 0212ef892490c18b4dbd0ab7abf3c64e844d4873 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 13:01:22 -0700 Subject: [PATCH 519/564] test(egress): require timeout despite abort-ignoring body --- test/outbound-fetch-stream-timeout.test.ts | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/outbound-fetch-stream-timeout.test.ts b/test/outbound-fetch-stream-timeout.test.ts index 9a42bf740..7d53ffa82 100644 --- a/test/outbound-fetch-stream-timeout.test.ts +++ b/test/outbound-fetch-stream-timeout.test.ts @@ -33,4 +33,30 @@ describe("credential-egress streamed response deadlines", () => { expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-timeout"); expect(await response.text()).toBe(""); }); + + it("enforces the deadline when a transport body ignores abort", async () => { + vi.useFakeTimers(); + const cancel = vi.fn(); + const body = new ReadableStream({ + pull() { + return new Promise(() => undefined); + }, + cancel, + }); + const rawFetch = vi.fn(async () => new Response(body)); + const wrapped = createFailClosedFetch(rawFetch); + let observedResponse: Response | undefined; + + void wrapped("https://api.github.com/meta").then((response) => { + observedResponse = response; + }); + await vi.advanceTimersByTimeAsync(10_000); + for (let attempt = 0; attempt < 5 && observedResponse === undefined; attempt += 1) { + await Promise.resolve(); + } + + expect(cancel).toHaveBeenCalledOnce(); + expect(observedResponse?.status).toBe(504); + expect(observedResponse?.headers.get("x-noema-egress-policy")).toBe("blocked-timeout"); + }); }); From 00c6161d4ec7bfdee23ac24b53b2fbdb962d45c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 13:04:45 -0700 Subject: [PATCH 520/564] fix(egress): enforce abort-aware response reads --- src/outbound-fetch-policy.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 3cae761f4..9876c3277 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -225,6 +225,23 @@ function ignoreCancellationBestEffort(cancel: () => Promise): void { } } +async function readOutboundChunk( + reader: ReadableStreamDefaultReader, + signal: AbortSignal, +): Promise> { + signal.throwIfAborted(); + let onAbort = () => undefined; + const abort = new Promise((_resolve, reject) => { + onAbort = () => reject(signal.reason); + signal.addEventListener("abort", onAbort, { once: true }); + }); + try { + return await Promise.race([reader.read(), abort]); + } finally { + signal.removeEventListener("abort", onAbort); + } +} + async function boundedOutboundResponse( response: Response, signal: AbortSignal, @@ -250,7 +267,7 @@ async function boundedOutboundResponse( let totalBytes = 0; try { while (true) { - const { done, value } = await reader.read(); + const { done, value } = await readOutboundChunk(reader, signal); if (signal.aborted) throw signal.reason; if (done) break; totalBytes += value.byteLength; @@ -542,4 +559,4 @@ export function resetGlobalOutboundFetchPolicy( } } installations.delete(key); -} +} \ No newline at end of file From 271d47b74d8adc51546a4291a0e576bea7e9a833 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 13:07:31 -0700 Subject: [PATCH 521/564] fix(egress): type abort listener for exact deadline --- src/outbound-fetch-policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 9876c3277..ac1c5f1a6 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -230,7 +230,7 @@ async function readOutboundChunk( signal: AbortSignal, ): Promise> { signal.throwIfAborted(); - let onAbort = () => undefined; + let onAbort = () => {}; const abort = new Promise((_resolve, reject) => { onAbort = () => reject(signal.reason); signal.addEventListener("abort", onAbort, { once: true }); From 6faee9f10c11221849bfd27a66426119331f9f3e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 13:11:28 -0700 Subject: [PATCH 522/564] test(egress): exercise active abort race --- test/outbound-fetch-stream-timeout.test.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/test/outbound-fetch-stream-timeout.test.ts b/test/outbound-fetch-stream-timeout.test.ts index 7d53ffa82..bee32368d 100644 --- a/test/outbound-fetch-stream-timeout.test.ts +++ b/test/outbound-fetch-stream-timeout.test.ts @@ -36,27 +36,28 @@ describe("credential-egress streamed response deadlines", () => { it("enforces the deadline when a transport body ignores abort", async () => { vi.useFakeTimers(); + let markBodyReadStarted!: () => void; + const bodyReadStarted = new Promise((resolve) => { + markBodyReadStarted = resolve; + }); const cancel = vi.fn(); const body = new ReadableStream({ pull() { + markBodyReadStarted(); return new Promise(() => undefined); }, cancel, - }); + }, { highWaterMark: 0 }); const rawFetch = vi.fn(async () => new Response(body)); const wrapped = createFailClosedFetch(rawFetch); - let observedResponse: Response | undefined; - void wrapped("https://api.github.com/meta").then((response) => { - observedResponse = response; - }); + const pending = wrapped("https://api.github.com/meta"); + await bodyReadStarted; await vi.advanceTimersByTimeAsync(10_000); - for (let attempt = 0; attempt < 5 && observedResponse === undefined; attempt += 1) { - await Promise.resolve(); - } + const response = await pending; expect(cancel).toHaveBeenCalledOnce(); - expect(observedResponse?.status).toBe(504); - expect(observedResponse?.headers.get("x-noema-egress-policy")).toBe("blocked-timeout"); + expect(response.status).toBe(504); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-timeout"); }); }); From 5f6ea1a3cf1cbecd116f4c13aaacbbd2840f9cf2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 13:15:00 -0700 Subject: [PATCH 523/564] test(egress): cover post-read cancellation race --- test/outbound-fetch-stream-timeout.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/outbound-fetch-stream-timeout.test.ts b/test/outbound-fetch-stream-timeout.test.ts index bee32368d..a1730fafb 100644 --- a/test/outbound-fetch-stream-timeout.test.ts +++ b/test/outbound-fetch-stream-timeout.test.ts @@ -60,4 +60,23 @@ describe("credential-egress streamed response deadlines", () => { expect(response.status).toBe(504); expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-timeout"); }); + + it("preserves caller cancellation that races with a newly readable body chunk", async () => { + const caller = new AbortController(); + const cancellationReason = new DOMException("caller revoked request authority", "AbortError"); + const cancel = vi.fn(); + const body = new ReadableStream({ + pull(controller) { + controller.enqueue(new Uint8Array([1])); + caller.abort(cancellationReason); + }, + cancel, + }, { highWaterMark: 0 }); + const rawFetch = vi.fn(async () => new Response(body)); + const wrapped = createFailClosedFetch(rawFetch); + + await expect(wrapped("https://api.github.com/meta", { signal: caller.signal })) + .rejects.toBe(cancellationReason); + expect(cancel).toHaveBeenCalledOnce(); + }); }); From dc3fb50f7f134209347aec428e470a4d13d29fdf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 13:18:37 -0700 Subject: [PATCH 524/564] fix(egress): remove synthetic abort no-op --- src/outbound-fetch-policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index ac1c5f1a6..2eba1b22e 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -230,7 +230,7 @@ async function readOutboundChunk( signal: AbortSignal, ): Promise> { signal.throwIfAborted(); - let onAbort = () => {}; + let onAbort!: () => void; const abort = new Promise((_resolve, reject) => { onAbort = () => reject(signal.reason); signal.addEventListener("abort", onAbort, { once: true }); From 2d79b35d7afa448557a1ff09a26fb4df5de03994 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 13:23:16 -0700 Subject: [PATCH 525/564] test(ci): require license evidence before manifest --- test/ci-release-evidence-order.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 test/ci-release-evidence-order.test.ts diff --git a/test/ci-release-evidence-order.test.ts b/test/ci-release-evidence-order.test.ts new file mode 100644 index 000000000..a5d94d1fc --- /dev/null +++ b/test/ci-release-evidence-order.test.ts @@ -0,0 +1,17 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +describe("CI release evidence ordering", () => { + it("materializes dependency license evidence before the acquisition manifest", () => { + const workflow = readFileSync(".github/workflows/ci.yml", "utf8"); + const inventoryStep = workflow.indexOf( + " - name: release dependency license inventory\n run: npm run release:dependency-license-inventory", + ); + const manifestStep = workflow.indexOf( + " - name: release acquisition manifest\n run: npm run acquisition:manifest", + ); + + expect(inventoryStep).toBeGreaterThanOrEqual(0); + expect(manifestStep).toBeGreaterThan(inventoryStep); + }); +}); From 44107bdbf12d35a7f888f89d13967473c20286ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 13:28:34 -0700 Subject: [PATCH 526/564] fix(ci): materialize license evidence before manifest --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24690a78f..437df8530 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -194,6 +194,9 @@ jobs: - name: release KPI verification run: npm run kpi:verify + - name: release dependency license inventory + run: npm run release:dependency-license-inventory + - name: release acquisition manifest run: npm run acquisition:manifest From 3b4abd3502310a576f53adf8dd9d001662614724 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 14:30:26 -0700 Subject: [PATCH 527/564] test(egress): require abort-bounded transport headers --- test/outbound-fetch-transport-abort.test.ts | 87 +++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 test/outbound-fetch-transport-abort.test.ts diff --git a/test/outbound-fetch-transport-abort.test.ts b/test/outbound-fetch-transport-abort.test.ts new file mode 100644 index 000000000..220d00b53 --- /dev/null +++ b/test/outbound-fetch-transport-abort.test.ts @@ -0,0 +1,87 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + createFailClosedFetch, + type FetchLike, +} from "../src/outbound-fetch-policy"; + +describe("credential-egress transport abort deadlines", () => { + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it("enforces the deadline before response headers when a transport ignores abort", async () => { + vi.useFakeTimers(); + let resolveTransport!: (response: Response) => void; + const transport = new Promise((resolve) => { + resolveTransport = resolve; + }); + const cancel = vi.fn(); + const rawFetch = vi.fn(() => transport); + const wrapped = createFailClosedFetch(rawFetch); + + const pending = wrapped("https://api.github.com/meta"); + let observed: Response | undefined; + let rejected: unknown; + void pending.then( + (response) => { + observed = response; + }, + (error: unknown) => { + rejected = error; + }, + ); + + await vi.advanceTimersByTimeAsync(10_000); + await Promise.resolve(); + + try { + expect(rejected).toBeUndefined(); + expect(observed?.status).toBe(504); + expect(observed?.headers.get("x-noema-egress-policy")).toBe("blocked-timeout"); + } finally { + resolveTransport(new Response(new ReadableStream({ cancel }))); + await Promise.resolve(); + await Promise.resolve(); + } + expect(cancel).toHaveBeenCalledOnce(); + }); + + it("preserves caller cancellation before response headers when a transport ignores abort", async () => { + const caller = new AbortController(); + const cancellationReason = new DOMException("caller revoked request authority", "AbortError"); + let resolveTransport!: (response: Response) => void; + const transport = new Promise((resolve) => { + resolveTransport = resolve; + }); + const cancel = vi.fn(); + const rawFetch = vi.fn(() => transport); + const wrapped = createFailClosedFetch(rawFetch); + + const pending = wrapped("https://api.github.com/meta", { signal: caller.signal }); + let observed: Response | undefined; + let rejected: unknown; + void pending.then( + (response) => { + observed = response; + }, + (error: unknown) => { + rejected = error; + }, + ); + + caller.abort(cancellationReason); + await Promise.resolve(); + await Promise.resolve(); + + try { + expect(observed).toBeUndefined(); + expect(rejected).toBe(cancellationReason); + } finally { + resolveTransport(new Response(new ReadableStream({ cancel }))); + await Promise.resolve(); + await Promise.resolve(); + } + expect(cancel).toHaveBeenCalledOnce(); + }); +}); From 0f75d39a5f71c0cf6517b139883a9a5ca1a95091 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 14:34:39 -0700 Subject: [PATCH 528/564] test(egress): cover late transport cleanup branches --- test/outbound-fetch-transport-abort.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/outbound-fetch-transport-abort.test.ts b/test/outbound-fetch-transport-abort.test.ts index 220d00b53..bf7c66562 100644 --- a/test/outbound-fetch-transport-abort.test.ts +++ b/test/outbound-fetch-transport-abort.test.ts @@ -54,7 +54,6 @@ describe("credential-egress transport abort deadlines", () => { const transport = new Promise((resolve) => { resolveTransport = resolve; }); - const cancel = vi.fn(); const rawFetch = vi.fn(() => transport); const wrapped = createFailClosedFetch(rawFetch); @@ -78,10 +77,9 @@ describe("credential-egress transport abort deadlines", () => { expect(observed).toBeUndefined(); expect(rejected).toBe(cancellationReason); } finally { - resolveTransport(new Response(new ReadableStream({ cancel }))); + resolveTransport(new Response(null)); await Promise.resolve(); await Promise.resolve(); } - expect(cancel).toHaveBeenCalledOnce(); }); }); From 7726c93410e319cc7dcc3e0c8cbe5d2c39f2ca2c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 14:35:50 -0700 Subject: [PATCH 529/564] fix(egress): bound transport wait to abort authority --- src/outbound-fetch-policy.ts | 41 ++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 2eba1b22e..b207a01e4 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -225,6 +225,36 @@ function ignoreCancellationBestEffort(cancel: () => Promise): void { } } +function cancelResponseBodyBestEffort(response: Response, reason: string): void { + if (response.body === null) return; + ignoreCancellationBestEffort(() => response.body!.cancel(reason)); +} + +async function awaitOutboundTransport( + transport: Promise, + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); + let onAbort!: () => void; + const abort = new Promise((_resolve, reject) => { + onAbort = () => reject(signal.reason); + signal.addEventListener("abort", onAbort, { once: true }); + }); + try { + return await Promise.race([transport, abort]); + } catch (error) { + void transport.then((lateResponse) => { + cancelResponseBodyBestEffort( + lateResponse, + "Noema outbound response arrived after request authority was revoked", + ); + }).catch(() => undefined); + throw error; + } finally { + signal.removeEventListener("abort", onAbort); + } +} + async function readOutboundChunk( reader: ReadableStreamDefaultReader, signal: AbortSignal, @@ -464,11 +494,14 @@ export function createFailClosedFetch(rawFetch: FetchLike): FetchLike { } try { - const response = await rawFetch(input, { - ...(effectiveInit ?? {}), - redirect: "manual", + const response = await awaitOutboundTransport( + rawFetch(input, { + ...(effectiveInit ?? {}), + redirect: "manual", + signal, + }), signal, - }); + ); if (signal.aborted) { if (response.body !== null) { ignoreCancellationBestEffort(() => response.body!.cancel( From 1be5bd4fb2f5200419b89fb37de69b0fb8aad9c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 14:39:35 -0700 Subject: [PATCH 530/564] test(egress): cover synchronous abort transport cleanup --- test/outbound-fetch-transport-abort.test.ts | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/outbound-fetch-transport-abort.test.ts b/test/outbound-fetch-transport-abort.test.ts index bf7c66562..7423cf112 100644 --- a/test/outbound-fetch-transport-abort.test.ts +++ b/test/outbound-fetch-transport-abort.test.ts @@ -82,4 +82,29 @@ describe("credential-egress transport abort deadlines", () => { await Promise.resolve(); } }); + + it("cleans a late response when transport revokes caller authority synchronously", async () => { + const caller = new AbortController(); + const cancellationReason = new DOMException("transport observed caller revocation", "AbortError"); + let resolveTransport!: (response: Response) => void; + const transport = new Promise((resolve) => { + resolveTransport = resolve; + }); + const cancel = vi.fn(); + const rawFetch = vi.fn(() => { + caller.abort(cancellationReason); + return transport; + }); + const wrapped = createFailClosedFetch(rawFetch); + + await expect( + wrapped("https://api.github.com/meta", { signal: caller.signal }), + ).rejects.toBe(cancellationReason); + + resolveTransport(new Response(new ReadableStream({ cancel }))); + await Promise.resolve(); + await Promise.resolve(); + + expect(cancel).toHaveBeenCalledOnce(); + }); }); From 1cb9a1083d556da203fc2590370abdf2c563fab3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 14:40:32 -0700 Subject: [PATCH 531/564] fix(egress): clean late transport on synchronous abort --- src/outbound-fetch-policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index b207a01e4..f3cf72fdc 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -234,13 +234,13 @@ async function awaitOutboundTransport( transport: Promise, signal: AbortSignal, ): Promise { - signal.throwIfAborted(); let onAbort!: () => void; const abort = new Promise((_resolve, reject) => { onAbort = () => reject(signal.reason); signal.addEventListener("abort", onAbort, { once: true }); }); try { + signal.throwIfAborted(); return await Promise.race([transport, abort]); } catch (error) { void transport.then((lateResponse) => { From 32046d12e0eccbe46aa71a2e86fba169cf313d9e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 15:03:00 -0700 Subject: [PATCH 532/564] test(egress): await caller cancellation deterministically --- test/outbound-fetch-transport-abort.test.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/test/outbound-fetch-transport-abort.test.ts b/test/outbound-fetch-transport-abort.test.ts index 7423cf112..3707596da 100644 --- a/test/outbound-fetch-transport-abort.test.ts +++ b/test/outbound-fetch-transport-abort.test.ts @@ -58,24 +58,15 @@ describe("credential-egress transport abort deadlines", () => { const wrapped = createFailClosedFetch(rawFetch); const pending = wrapped("https://api.github.com/meta", { signal: caller.signal }); - let observed: Response | undefined; - let rejected: unknown; - void pending.then( - (response) => { - observed = response; - }, - (error: unknown) => { - rejected = error; - }, + const rejection = pending.then( + () => undefined, + (error: unknown) => error, ); caller.abort(cancellationReason); - await Promise.resolve(); - await Promise.resolve(); try { - expect(observed).toBeUndefined(); - expect(rejected).toBe(cancellationReason); + await expect(rejection).resolves.toBe(cancellationReason); } finally { resolveTransport(new Response(null)); await Promise.resolve(); From acf9659a70a7dcc0607d51256c507e20ba36165a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 15:11:40 -0700 Subject: [PATCH 533/564] test(egress): cover response-settle cancellation race --- test/outbound-fetch-transport-abort.test.ts | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/outbound-fetch-transport-abort.test.ts b/test/outbound-fetch-transport-abort.test.ts index 3707596da..6963a59f2 100644 --- a/test/outbound-fetch-transport-abort.test.ts +++ b/test/outbound-fetch-transport-abort.test.ts @@ -74,6 +74,30 @@ describe("credential-egress transport abort deadlines", () => { } }); + it("rejects a response when caller authority is revoked as response headers settle", async () => { + const caller = new AbortController(); + const cancellationReason = new DOMException("caller revoked settling response authority", "AbortError"); + let resolveTransport!: (response: Response) => void; + const transport = new Promise((resolve) => { + resolveTransport = resolve; + }); + const cancel = vi.fn(); + + void transport.then(() => { + queueMicrotask(() => caller.abort(cancellationReason)); + }); + + const rawFetch = vi.fn(() => transport); + const wrapped = createFailClosedFetch(rawFetch); + const pending = wrapped("https://api.github.com/meta", { signal: caller.signal }); + + resolveTransport(new Response(new ReadableStream({ cancel }))); + + await expect(pending).rejects.toBe(cancellationReason); + await Promise.resolve(); + expect(cancel).toHaveBeenCalledOnce(); + }); + it("cleans a late response when transport revokes caller authority synchronously", async () => { const caller = new AbortController(); const cancellationReason = new DOMException("transport observed caller revocation", "AbortError"); From 84bc5427f6a15b5ebed73c77484696b7102be5f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 16:03:13 -0700 Subject: [PATCH 534/564] test(egress): deterministically cover post-transport revocation --- test/outbound-fetch-transport-abort.test.ts | 23 +++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/test/outbound-fetch-transport-abort.test.ts b/test/outbound-fetch-transport-abort.test.ts index 6963a59f2..19c8ff71f 100644 --- a/test/outbound-fetch-transport-abort.test.ts +++ b/test/outbound-fetch-transport-abort.test.ts @@ -74,7 +74,7 @@ describe("credential-egress transport abort deadlines", () => { } }); - it("rejects a response when caller authority is revoked as response headers settle", async () => { + it("rejects a response when caller authority is revoked after the transport wins its race", async () => { const caller = new AbortController(); const cancellationReason = new DOMException("caller revoked settling response authority", "AbortError"); let resolveTransport!: (response: Response) => void; @@ -83,8 +83,24 @@ describe("credential-egress transport abort deadlines", () => { }); const cancel = vi.fn(); - void transport.then(() => { - queueMicrotask(() => caller.abort(cancellationReason)); + // Promise.race calls `then` on the native transport promise. Interpose that + // exact registration so the transport resolves the race first and caller + // authority is revoked synchronously before the awaiting wrapper resumes. + // This deterministically exercises the post-transport fail-closed guard. + const nativeThen = transport.then.bind(transport); + Object.defineProperty(transport, "then", { + configurable: true, + value: (( + onFulfilled?: ((value: Response) => unknown) | null, + onRejected?: ((reason: unknown) => unknown) | null, + ) => nativeThen( + (response) => { + const result = onFulfilled ? onFulfilled(response) : response; + caller.abort(cancellationReason); + return result; + }, + onRejected ?? undefined, + )) as Promise["then"], }); const rawFetch = vi.fn(() => transport); @@ -94,7 +110,6 @@ describe("credential-egress transport abort deadlines", () => { resolveTransport(new Response(new ReadableStream({ cancel }))); await expect(pending).rejects.toBe(cancellationReason); - await Promise.resolve(); expect(cancel).toHaveBeenCalledOnce(); }); From a76c1d4ec21eeeddcbfb800d118807e12ed9d281 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 16:04:08 -0700 Subject: [PATCH 535/564] test(ci): require branch coverage failure diagnostics --- test/ci-release-evidence-order.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/ci-release-evidence-order.test.ts b/test/ci-release-evidence-order.test.ts index a5d94d1fc..891421037 100644 --- a/test/ci-release-evidence-order.test.ts +++ b/test/ci-release-evidence-order.test.ts @@ -14,4 +14,12 @@ describe("CI release evidence ordering", () => { expect(inventoryStep).toBeGreaterThanOrEqual(0); expect(manifestStep).toBeGreaterThan(inventoryStep); }); + + it("emits actionable branch diagnostics when coverage fails without uncovered statements", () => { + const workflow = readFileSync(".github/workflows/ci.yml", "utf8"); + + expect(workflow).toContain("const branchMap = coverage?.branchMap ?? {};"); + expect(workflow).toContain("for (const [branchId, hits] of Object.entries(coverage?.b ?? {}))"); + expect(workflow).toContain("Uncovered branch ${path}:${location.start.line}:${location.start.column}"); + }); }); From f307d135b77f6f7fa9740be1c058be8bff6f6c31 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 16:05:28 -0700 Subject: [PATCH 536/564] fix(ci): report uncovered branches on coverage failure --- .github/workflows/ci.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 437df8530..5e06cdb07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -183,6 +183,23 @@ jobs: diagnosticCount += 1; if (diagnosticCount >= 64) break outer; } + const branchMap = coverage?.branchMap ?? {}; + for (const [branchId, hits] of Object.entries(coverage?.b ?? {})) { + if (!Array.isArray(hits)) continue; + const branch = branchMap[branchId]; + const locations = branch?.locations ?? []; + for (const [armIndex, armHits] of hits.entries()) { + if (armHits !== 0) continue; + const location = locations[armIndex] ?? branch?.loc; + if (!location?.start) continue; + const path = relative(process.cwd(), file) || file; + console.error( + `Uncovered branch ${path}:${location.start.line}:${location.start.column} branch=${branchId} arm=${armIndex}`, + ); + diagnosticCount += 1; + if (diagnosticCount >= 64) break outer; + } + } } NODE fi From 9a5d86aa674a00f63cc6714910e848b8feee5812 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 16:12:46 -0700 Subject: [PATCH 537/564] test(acquisition): align CLI audit timeout hierarchy --- test/acquisition-data-room-integrity.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/acquisition-data-room-integrity.test.ts b/test/acquisition-data-room-integrity.test.ts index f99868fa6..9f24b26ea 100644 --- a/test/acquisition-data-room-integrity.test.ts +++ b/test/acquisition-data-room-integrity.test.ts @@ -14,6 +14,7 @@ import { } from "../scripts/lib/acquisition-data-room-integrity.mjs"; const HEAD = "0123456789abcdef0123456789abcdef01234567"; +const ACQUISITION_AUDIT_TEST_TIMEOUT_MS = 35_000; const testCatalog = [ { @@ -165,7 +166,7 @@ describe("acquisition data-room integrity", () => { } finally { rmSync(temp, { recursive: true, force: true }); } - }); + }, ACQUISITION_AUDIT_TEST_TIMEOUT_MS); it("recomputes local hashes and refuses evidence modified after manifest generation", () => { const temp = mkdtempSync(join(tmpdir(), "noema-data-room-mutated-")); From d47073d5a6d2c8889d6243cf8c4e45df0c409da5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 16:16:42 -0700 Subject: [PATCH 538/564] test(egress): cover native transport-settlement revocation race --- test/outbound-fetch-transport-abort.test.ts | 26 ++++++--------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/test/outbound-fetch-transport-abort.test.ts b/test/outbound-fetch-transport-abort.test.ts index 19c8ff71f..aad9606a9 100644 --- a/test/outbound-fetch-transport-abort.test.ts +++ b/test/outbound-fetch-transport-abort.test.ts @@ -83,25 +83,13 @@ describe("credential-egress transport abort deadlines", () => { }); const cancel = vi.fn(); - // Promise.race calls `then` on the native transport promise. Interpose that - // exact registration so the transport resolves the race first and caller - // authority is revoked synchronously before the awaiting wrapper resumes. - // This deterministically exercises the post-transport fail-closed guard. - const nativeThen = transport.then.bind(transport); - Object.defineProperty(transport, "then", { - configurable: true, - value: (( - onFulfilled?: ((value: Response) => unknown) | null, - onRejected?: ((reason: unknown) => unknown) | null, - ) => nativeThen( - (response) => { - const result = onFulfilled ? onFulfilled(response) : response; - caller.abort(cancellationReason); - return result; - }, - onRejected ?? undefined, - )) as Promise["then"], - }); + // Register this reaction before the wrapper registers Promise.race's + // transport reaction. Resolving the native transport queues this reaction + // first; it revokes caller authority before the already-queued transport + // reaction settles the race and before the awaiting wrapper can trust the + // response. This exercises the real native-Promise settlement ordering + // without replacing Promise.then or weakening the production guard. + void transport.then(() => caller.abort(cancellationReason)); const rawFetch = vi.fn(() => transport); const wrapped = createFailClosedFetch(rawFetch); From 6c8d90bfb1343795d83d2499a52c1f4b51273645 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 16:19:29 -0700 Subject: [PATCH 539/564] test(egress): cover revoked bodyless transport response --- test/outbound-fetch-transport-abort.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/outbound-fetch-transport-abort.test.ts b/test/outbound-fetch-transport-abort.test.ts index aad9606a9..e89ddd4e8 100644 --- a/test/outbound-fetch-transport-abort.test.ts +++ b/test/outbound-fetch-transport-abort.test.ts @@ -101,6 +101,25 @@ describe("credential-egress transport abort deadlines", () => { expect(cancel).toHaveBeenCalledOnce(); }); + it("rejects a bodyless response when caller authority is revoked after transport settlement", async () => { + const caller = new AbortController(); + const cancellationReason = new DOMException("caller revoked bodyless response authority", "AbortError"); + let resolveTransport!: (response: Response) => void; + const transport = new Promise((resolve) => { + resolveTransport = resolve; + }); + + void transport.then(() => caller.abort(cancellationReason)); + + const rawFetch = vi.fn(() => transport); + const wrapped = createFailClosedFetch(rawFetch); + const pending = wrapped("https://api.github.com/meta", { signal: caller.signal }); + + resolveTransport(new Response(null, { status: 204 })); + + await expect(pending).rejects.toBe(cancellationReason); + }); + it("cleans a late response when transport revokes caller authority synchronously", async () => { const caller = new AbortController(); const cancellationReason = new DOMException("transport observed caller revocation", "AbortError"); From e230074cdc812d9eecb25c033112888c8f689ce2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 16:21:58 -0700 Subject: [PATCH 540/564] test(ci): require concrete branch diagnostic locations --- test/ci-release-evidence-order.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/ci-release-evidence-order.test.ts b/test/ci-release-evidence-order.test.ts index 891421037..32dd16710 100644 --- a/test/ci-release-evidence-order.test.ts +++ b/test/ci-release-evidence-order.test.ts @@ -20,6 +20,8 @@ describe("CI release evidence ordering", () => { expect(workflow).toContain("const branchMap = coverage?.branchMap ?? {};"); expect(workflow).toContain("for (const [branchId, hits] of Object.entries(coverage?.b ?? {}))"); - expect(workflow).toContain("Uncovered branch ${path}:${location.start.line}:${location.start.column}"); + expect(workflow).toContain("const line = branch?.line ?? branch?.loc?.start?.line ?? location?.line ?? location?.start?.line;"); + expect(workflow).toContain("if (!Number.isInteger(line)) continue;"); + expect(workflow).toContain("Uncovered branch ${path}:${line}:${column}"); }); }); From df110e815ca20d14919516179a79d8a793e9dd64 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 16:25:11 -0700 Subject: [PATCH 541/564] fix(ci): emit concrete branch coverage locations --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e06cdb07..7282b641d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,10 +191,12 @@ jobs: for (const [armIndex, armHits] of hits.entries()) { if (armHits !== 0) continue; const location = locations[armIndex] ?? branch?.loc; - if (!location?.start) continue; + const line = branch?.line ?? branch?.loc?.start?.line ?? location?.line ?? location?.start?.line; + if (!Number.isInteger(line)) continue; + const column = branch?.loc?.start?.column ?? location?.start?.column ?? 0; const path = relative(process.cwd(), file) || file; console.error( - `Uncovered branch ${path}:${location.start.line}:${location.start.column} branch=${branchId} arm=${armIndex}`, + `Uncovered branch ${path}:${line}:${column} branch=${branchId} arm=${armIndex}`, ); diagnosticCount += 1; if (diagnosticCount >= 64) break outer; From ed707c7dd9a8ccaaabfa3ce1a3d52e1af2d616fd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 19:05:50 -0700 Subject: [PATCH 542/564] test(image): require patched OpenSSL security floor --- ...h-validator-openssl-security-floor.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 test/patch-validator-openssl-security-floor.test.ts diff --git a/test/patch-validator-openssl-security-floor.test.ts b/test/patch-validator-openssl-security-floor.test.ts new file mode 100644 index 000000000..35604eeb3 --- /dev/null +++ b/test/patch-validator-openssl-security-floor.test.ts @@ -0,0 +1,27 @@ +import { readFileSync } from "node:fs"; + +import { describe, expect, it } from "vitest"; + +const dockerfile = readFileSync("Dockerfile.patch-validator", "utf8"); + +const OPENSSL_3_5_8_SHA256 = + "a8f84a39918ec6415ce765d9b429d313ba97b8143169c172e734b9514464f5b2"; + +describe("patch-validator static OpenSSL security floor", () => { + it("builds the static Node runtime against checksum-pinned OpenSSL 3.5.8 or newer instead of the vulnerable Node-bundled 3.5.7", () => { + expect(dockerfile).toContain("ARG OPENSSL_VERSION=3.5.8"); + expect(dockerfile).toContain( + `ARG OPENSSL_SOURCE_SHA256=${OPENSSL_3_5_8_SHA256}`, + ); + expect(dockerfile).toContain( + "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz", + ); + expect(dockerfile).toContain("--shared-openssl"); + expect(dockerfile).toContain("--shared-openssl-includes=/opt/openssl/include"); + expect(dockerfile).toContain("--shared-openssl-libpath=/opt/openssl/lib"); + expect(dockerfile).toContain( + "process.versions.openssl !== process.env.OPENSSL_VERSION", + ); + expect(dockerfile).not.toContain("ARG OPENSSL_VERSION=3.5.7"); + }); +}); From 41191c1d30469a50969b8768ce83735367fd861b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 19:12:08 -0700 Subject: [PATCH 543/564] fix(image): link static Node against OpenSSL 3.5.8 --- Dockerfile.patch-validator | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/Dockerfile.patch-validator b/Dockerfile.patch-validator index d2604df5d..9c410c767 100644 --- a/Dockerfile.patch-validator +++ b/Dockerfile.patch-validator @@ -4,6 +4,8 @@ FROM alpine:3.24.1@sha256:79ff19e9084a00eece421b2523fb93e22d730e2c0e525905de047e ARG NODE_VERSION=24.19.0 ARG NODE_SOURCE_SHA256=f6d95e10a0431ee1067fc6aabe9f762908b4716dd35324e1ddb4b1466b76659f +ARG OPENSSL_VERSION=3.5.8 +ARG OPENSSL_SOURCE_SHA256=a8f84a39918ec6415ce765d9b429d313ba97b8143169c172e734b9514464f5b2 RUN apk add --no-cache \ binutils-gold \ @@ -13,14 +15,33 @@ RUN apk add --no-cache \ libgcc-static \ linux-headers \ make \ + perl \ python3 \ py3-setuptools \ xz ADD --checksum=sha256:${NODE_SOURCE_SHA256} https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}.tar.xz /tmp/node.tar.xz - -RUN mkdir -p /usr/src/node \ - && tar -xJf /tmp/node.tar.xz --strip-components=1 -C /usr/src/node +ADD --checksum=sha256:${OPENSSL_SOURCE_SHA256} https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz /tmp/openssl.tar.gz + +RUN mkdir -p /usr/src/node /usr/src/openssl \ + && tar -xJf /tmp/node.tar.xz --strip-components=1 -C /usr/src/node \ + && tar -xzf /tmp/openssl.tar.gz --strip-components=1 -C /usr/src/openssl + +WORKDIR /usr/src/openssl + +RUN ./Configure linux-x86_64 \ + no-shared \ + no-tests \ + --prefix=/opt/openssl \ + --libdir=lib \ + && make -j"$(getconf _NPROCESSORS_ONLN)" \ + && make install_sw \ + && test -f /opt/openssl/include/openssl/opensslv.h \ + && test -f /opt/openssl/lib/libcrypto.a \ + && test -f /opt/openssl/lib/libssl.a \ + && grep -Fq "OPENSSL_VERSION_MAJOR 3" /opt/openssl/include/openssl/opensslv.h \ + && grep -Fq "OPENSSL_VERSION_MINOR 5" /opt/openssl/include/openssl/opensslv.h \ + && grep -Fq "OPENSSL_VERSION_PATCH 8" /opt/openssl/include/openssl/opensslv.h ENV PATH="/opt/node/bin:${PATH}" WORKDIR /usr/src/node @@ -31,10 +52,14 @@ RUN ./configure \ --with-intl=small-icu \ --without-corepack \ --disable-single-executable-application \ + --shared-openssl \ + --shared-openssl-includes=/opt/openssl/include \ + --shared-openssl-libpath=/opt/openssl/lib \ && make -j"$(getconf _NPROCESSORS_ONLN)" V= \ && make install \ && test "$(/opt/node/bin/node --version)" = "v${NODE_VERSION}" \ && test "$(/opt/node/bin/npm --version)" = "11.17.0" \ + && OPENSSL_VERSION="${OPENSSL_VERSION}" /opt/node/bin/node --input-type=module --eval='if (process.versions.openssl !== process.env.OPENSSL_VERSION) throw new Error(`unexpected OpenSSL ${process.versions.openssl}`)' \ && /opt/node/bin/node --input-type=module --eval='/\p{ID_Continue}/u.test("a")' \ && ! readelf -l /opt/node/bin/node | grep -q 'Requesting program interpreter' \ && ! readelf -d /opt/node/bin/node | grep -q '(NEEDED)' \ @@ -49,6 +74,7 @@ RUN ./configure \ && readelf -p .note.package /opt/node/bin/node \ | grep -Fq 'cpe:2.3:a:nodejs:node.js:24.19.0:*:*:*:*:*:*:*' \ && test "$(/opt/node/bin/node --version)" = "v${NODE_VERSION}" \ + && OPENSSL_VERSION="${OPENSSL_VERSION}" /opt/node/bin/node --input-type=module --eval='if (process.versions.openssl !== process.env.OPENSSL_VERSION) throw new Error(`unexpected OpenSSL ${process.versions.openssl}`)' \ && /opt/node/bin/node --input-type=module --eval='/\p{ID_Continue}/u.test("a")' \ && ! readelf -l /opt/node/bin/node | grep -q 'Requesting program interpreter' \ && ! readelf -d /opt/node/bin/node | grep -q '(NEEDED)' From 784d37e4373cf0d34374b2981ec304e7de2556d1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 19:14:33 -0700 Subject: [PATCH 544/564] test(image): anchor Node PATH regression to Node build --- test/patch-validator-image-build-regression.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/patch-validator-image-build-regression.test.ts b/test/patch-validator-image-build-regression.test.ts index d4789c70a..292c93700 100644 --- a/test/patch-validator-image-build-regression.test.ts +++ b/test/patch-validator-image-build-regression.test.ts @@ -18,15 +18,18 @@ describe("patch-validator exact-toolchain image build regression", () => { expect(runtimeStage).not.toContain("/opt/node/bin/npm"); }); - it("keeps the freshly installed Node executable on PATH while make install installs npm", () => { + it("keeps the freshly installed Node executable on PATH while the Node make install installs npm", () => { const nodeBuilderStage = dockerfile.slice( dockerfile.indexOf("FROM alpine:3.24.1"), dockerfile.indexOf("FROM node_builder AS dependencies"), ); + const nodeBuild = nodeBuilderStage.slice(nodeBuilderStage.indexOf("WORKDIR /usr/src/node")); expect(nodeBuilderStage).toContain('ENV PATH="/opt/node/bin:${PATH}"'); + expect(nodeBuild).toContain("./configure"); + expect(nodeBuild).toContain("&& make install"); expect(nodeBuilderStage.indexOf('ENV PATH="/opt/node/bin:${PATH}"')).toBeLessThan( - nodeBuilderStage.indexOf("&& make install"), + nodeBuilderStage.indexOf("WORKDIR /usr/src/node"), ); }); From d4a895ed9bc6cdac2850b13f4d32547034e08e9c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 19:28:33 -0700 Subject: [PATCH 545/564] test(image): require pre-materialized validator dependencies --- test/patch-validator-image-build-regression.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/patch-validator-image-build-regression.test.ts b/test/patch-validator-image-build-regression.test.ts index 292c93700..2a3e99feb 100644 --- a/test/patch-validator-image-build-regression.test.ts +++ b/test/patch-validator-image-build-regression.test.ts @@ -2,6 +2,7 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; const dockerfile = readFileSync("Dockerfile.patch-validator", "utf8"); +const imageWorkflow = readFileSync(".github/workflows/patch-validator-image.yml", "utf8"); describe("patch-validator exact-toolchain image build regression", () => { it("builds dependencies with the exact Node/npm toolchain declared by devEngines", () => { @@ -18,6 +19,17 @@ describe("patch-validator exact-toolchain image build regression", () => { expect(runtimeStage).not.toContain("/opt/node/bin/npm"); }); + it("materializes lockfile dependencies before Docker and forbids npm registry access in the image build", () => { + expect(imageWorkflow).toContain("Materialize exact patch-validator dependencies"); + expect(imageWorkflow).toContain("node-version: 24.19.0"); + expect(imageWorkflow).toContain('test "$(npm --version)" = "11.17.0"'); + expect(imageWorkflow).toContain("npm ci --include=optional --ignore-scripts --no-audit --no-fund"); + expect(imageWorkflow).toContain('--build-context "validator_deps=${VALIDATOR_DEPS_CONTEXT}"'); + expect(dockerfile).toContain("FROM validator_deps AS dependencies"); + expect(dockerfile).not.toContain("npm ci"); + expect(dockerfile).not.toContain("npm prune"); + }); + it("keeps the freshly installed Node executable on PATH while the Node make install installs npm", () => { const nodeBuilderStage = dockerfile.slice( dockerfile.indexOf("FROM alpine:3.24.1"), From 417eabc7b4f6075b3cea6d1a673ed4c73de0cab4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 19:34:44 -0700 Subject: [PATCH 546/564] fix(image): pre-materialize validator dependencies --- .github/workflows/patch-validator-image.yml | 47 +++++++++++++++++++ Dockerfile.patch-validator | 27 +++-------- ...h-validator-image-build-regression.test.ts | 13 ++--- 3 files changed, 61 insertions(+), 26 deletions(-) diff --git a/.github/workflows/patch-validator-image.yml b/.github/workflows/patch-validator-image.yml index e9998ae6d..6329a4c89 100644 --- a/.github/workflows/patch-validator-image.yml +++ b/.github/workflows/patch-validator-image.yml @@ -117,6 +117,51 @@ jobs: "$scanner_dir/grype" version | grep -Fq "$GRYPE_VERSION" printf 'SCANNER_BIN_DIR=%s\n' "$scanner_dir" >>"$GITHUB_ENV" + - name: Set up exact dependency materialization toolchain + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "24.19.0" + cache: npm + + - name: Materialize exact patch-validator dependencies + shell: bash + run: | + set -euo pipefail + test "$(node --version)" = "v24.19.0" + test "$(npm --version)" = "11.17.0" + + deps_dir="$RUNNER_TEMP/patch-validator-deps" + rm -rf "$deps_dir" + mkdir -p "$deps_dir" + cp package.json package-lock.json "$deps_dir/" + source_lock_sha="$(sha256sum package-lock.json | cut -d' ' -f1)" + copied_lock_sha="$(sha256sum "$deps_dir/package-lock.json" | cut -d' ' -f1)" + test "$copied_lock_sha" = "$source_lock_sha" + + ( + cd "$deps_dir" + timeout --signal=TERM --kill-after=30s 10m env \ + npm_config_os=wasip1-threads \ + npm_config_cpu=wasm32 \ + npm ci --include=optional --ignore-scripts --no-audit --no-fund + npm pkg delete devDependencies.@cloudflare/workers-types devDependencies.wrangler + timeout --signal=TERM --kill-after=30s 5m env \ + npm_config_os=wasip1-threads \ + npm_config_cpu=wasm32 \ + npm prune --include=optional --ignore-scripts --no-audit --no-fund + test -f node_modules/typescript/bin/tsc + test -f node_modules/vitest/vitest.mjs + test -f node_modules/@vitest/coverage-v8/package.json + test -f node_modules/@rolldown/binding-wasm32-wasi/package.json + test -z "$(find node_modules -type f -name '*.node' -print -quit)" + test ! -e node_modules/@cloudflare/workers-types + test ! -e node_modules/wrangler + test ! -e node_modules/workerd + test ! -e node_modules/miniflare + ) + + printf 'VALIDATOR_DEPS_CONTEXT=%s\n' "$deps_dir" >>"$GITHUB_ENV" + - name: Set up Docker Buildx uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 @@ -124,10 +169,12 @@ jobs: shell: bash run: | set -euo pipefail + test -d "$VALIDATOR_DEPS_CONTEXT/node_modules" timeout --signal=TERM --kill-after=30s 150m docker buildx build \ --load \ --cache-from=type=gha,scope=noema-patch-validator-image \ --cache-to=type=gha,mode=max,scope=noema-patch-validator-image \ + --build-context "validator_deps=${VALIDATOR_DEPS_CONTEXT}" \ --platform=linux/amd64 \ --file=Dockerfile.patch-validator \ --build-arg=SOURCE_REVISION=${SOURCE_SHA} \ diff --git a/Dockerfile.patch-validator b/Dockerfile.patch-validator index 9c410c767..db9f018a9 100644 --- a/Dockerfile.patch-validator +++ b/Dockerfile.patch-validator @@ -79,25 +79,12 @@ RUN ./configure \ && ! readelf -l /opt/node/bin/node | grep -q 'Requesting program interpreter' \ && ! readelf -d /opt/node/bin/node | grep -q '(NEEDED)' -FROM node_builder AS dependencies - -WORKDIR /build - -COPY package.json package-lock.json ./ -RUN npm_config_os=wasip1-threads npm_config_cpu=wasm32 \ - npm ci --include=optional --ignore-scripts --no-audit --no-fund \ - && npm pkg delete devDependencies.@cloudflare/workers-types devDependencies.wrangler \ - && npm_config_os=wasip1-threads npm_config_cpu=wasm32 \ - npm prune --include=optional --ignore-scripts --no-audit --no-fund \ - && test -f node_modules/typescript/bin/tsc \ - && test -f node_modules/vitest/vitest.mjs \ - && test -f node_modules/@vitest/coverage-v8/package.json \ - && test -f node_modules/@rolldown/binding-wasm32-wasi/package.json \ - && test -z "$(find node_modules -type f -name '*.node' -print -quit)" \ - && test ! -e node_modules/@cloudflare/workers-types \ - && test ! -e node_modules/wrangler \ - && test ! -e node_modules/workerd \ - && test ! -e node_modules/miniflare +# The dependency tree is materialized by the exact-head workflow with the +# repository lockfile and exact Node/npm toolchain. Importing it as a named +# context keeps registry resolution outside the Docker build authority: a cold +# or stalled npm registry fails the bounded materialization step rather than +# holding the image build open for hours. +FROM validator_deps AS dependencies FROM scratch AS runtime @@ -115,7 +102,7 @@ WORKDIR /workspace COPY --from=node_builder --chown=65532:65532 /opt/node/bin/node /nodejs/bin/node COPY --from=node_builder --chown=65532:65532 --chmod=0444 /usr/src/node/LICENSE /licenses/node/LICENSE -COPY --from=dependencies --chown=65532:65532 /build/node_modules /opt/noema/node_modules +COPY --from=dependencies --chown=65532:65532 /node_modules /opt/noema/node_modules COPY --chown=65532:65532 patch-validator/entrypoint.mjs /opt/noema/entrypoint.mjs COPY --chown=65532:65532 patch-validator/validate-patch.mjs /opt/noema/validate-patch.mjs COPY --chown=65532:65532 patch-validator/runtime.mjs /opt/noema/runtime.mjs diff --git a/test/patch-validator-image-build-regression.test.ts b/test/patch-validator-image-build-regression.test.ts index 2a3e99feb..9e88a47c7 100644 --- a/test/patch-validator-image-build-regression.test.ts +++ b/test/patch-validator-image-build-regression.test.ts @@ -5,10 +5,10 @@ const dockerfile = readFileSync("Dockerfile.patch-validator", "utf8"); const imageWorkflow = readFileSync(".github/workflows/patch-validator-image.yml", "utf8"); describe("patch-validator exact-toolchain image build regression", () => { - it("builds dependencies with the exact Node/npm toolchain declared by devEngines", () => { + it("builds the static runtime with the exact Node/npm toolchain declared by devEngines", () => { expect(dockerfile).toContain("ARG NODE_VERSION=24.19.0"); expect(dockerfile).toContain('test "$(/opt/node/bin/npm --version)" = "11.17.0"'); - expect(dockerfile).toContain("FROM node_builder AS dependencies"); + expect(dockerfile).toContain("FROM validator_deps AS dependencies"); expect(dockerfile).not.toContain("FROM node:24.18.0-alpine3.24"); expect(dockerfile).not.toContain("--without-npm"); @@ -20,8 +20,9 @@ describe("patch-validator exact-toolchain image build regression", () => { }); it("materializes lockfile dependencies before Docker and forbids npm registry access in the image build", () => { + expect(imageWorkflow).toContain("Set up exact dependency materialization toolchain"); expect(imageWorkflow).toContain("Materialize exact patch-validator dependencies"); - expect(imageWorkflow).toContain("node-version: 24.19.0"); + expect(imageWorkflow).toContain('node-version: "24.19.0"'); expect(imageWorkflow).toContain('test "$(npm --version)" = "11.17.0"'); expect(imageWorkflow).toContain("npm ci --include=optional --ignore-scripts --no-audit --no-fund"); expect(imageWorkflow).toContain('--build-context "validator_deps=${VALIDATOR_DEPS_CONTEXT}"'); @@ -33,7 +34,7 @@ describe("patch-validator exact-toolchain image build regression", () => { it("keeps the freshly installed Node executable on PATH while the Node make install installs npm", () => { const nodeBuilderStage = dockerfile.slice( dockerfile.indexOf("FROM alpine:3.24.1"), - dockerfile.indexOf("FROM node_builder AS dependencies"), + dockerfile.indexOf("FROM validator_deps AS dependencies"), ); const nodeBuild = nodeBuilderStage.slice(nodeBuilderStage.indexOf("WORKDIR /usr/src/node")); @@ -48,7 +49,7 @@ describe("patch-validator exact-toolchain image build regression", () => { it("installs the static GCC runtime archive before requesting a fully static Node binary", () => { const nodeBuilderStage = dockerfile.slice( dockerfile.indexOf("FROM alpine:3.24.1"), - dockerfile.indexOf("FROM node_builder AS dependencies"), + dockerfile.indexOf("FROM validator_deps AS dependencies"), ); expect(nodeBuilderStage).toContain("--fully-static"); @@ -61,7 +62,7 @@ describe("patch-validator exact-toolchain image build regression", () => { it("keeps the Unicode property-escape smoke probe intact across the shell boundary", () => { const nodeBuilderStage = dockerfile.slice( dockerfile.indexOf("FROM alpine:3.24.1"), - dockerfile.indexOf("FROM node_builder AS dependencies"), + dockerfile.indexOf("FROM validator_deps AS dependencies"), ); const unicodePropertyProbe = `--eval='/\\p{ID_Continue}/u.test("a")'`; From 83d21b87ff7fe5e7b076211fb08cbda9005a10b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 19:37:36 -0700 Subject: [PATCH 547/564] fix(image): use local validator dependency context --- Dockerfile.patch-validator | 13 +++++-------- ...atch-validator-image-build-regression.test.ts | 16 +++++++++++----- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Dockerfile.patch-validator b/Dockerfile.patch-validator index db9f018a9..78dbac68f 100644 --- a/Dockerfile.patch-validator +++ b/Dockerfile.patch-validator @@ -79,13 +79,6 @@ RUN ./configure \ && ! readelf -l /opt/node/bin/node | grep -q 'Requesting program interpreter' \ && ! readelf -d /opt/node/bin/node | grep -q '(NEEDED)' -# The dependency tree is materialized by the exact-head workflow with the -# repository lockfile and exact Node/npm toolchain. Importing it as a named -# context keeps registry resolution outside the Docker build authority: a cold -# or stalled npm registry fails the bounded materialization step rather than -# holding the image build open for hours. -FROM validator_deps AS dependencies - FROM scratch AS runtime ARG SOURCE_REVISION @@ -102,7 +95,11 @@ WORKDIR /workspace COPY --from=node_builder --chown=65532:65532 /opt/node/bin/node /nodejs/bin/node COPY --from=node_builder --chown=65532:65532 --chmod=0444 /usr/src/node/LICENSE /licenses/node/LICENSE -COPY --from=dependencies --chown=65532:65532 /node_modules /opt/noema/node_modules +# validator_deps is a local named BuildKit context materialized from this exact +# head's lockfile by the workflow. Copying directly from the named context avoids +# an ambiguous unpinned FROM reference while keeping npm registry resolution out +# of the Docker build authority. +COPY --from=validator_deps --chown=65532:65532 /node_modules /opt/noema/node_modules COPY --chown=65532:65532 patch-validator/entrypoint.mjs /opt/noema/entrypoint.mjs COPY --chown=65532:65532 patch-validator/validate-patch.mjs /opt/noema/validate-patch.mjs COPY --chown=65532:65532 patch-validator/runtime.mjs /opt/noema/runtime.mjs diff --git a/test/patch-validator-image-build-regression.test.ts b/test/patch-validator-image-build-regression.test.ts index 9e88a47c7..53d1b6c57 100644 --- a/test/patch-validator-image-build-regression.test.ts +++ b/test/patch-validator-image-build-regression.test.ts @@ -8,7 +8,7 @@ describe("patch-validator exact-toolchain image build regression", () => { it("builds the static runtime with the exact Node/npm toolchain declared by devEngines", () => { expect(dockerfile).toContain("ARG NODE_VERSION=24.19.0"); expect(dockerfile).toContain('test "$(/opt/node/bin/npm --version)" = "11.17.0"'); - expect(dockerfile).toContain("FROM validator_deps AS dependencies"); + expect(dockerfile).not.toContain("FROM validator_deps"); expect(dockerfile).not.toContain("FROM node:24.18.0-alpine3.24"); expect(dockerfile).not.toContain("--without-npm"); @@ -16,6 +16,9 @@ describe("patch-validator exact-toolchain image build regression", () => { expect(runtimeStage).toContain( "COPY --from=node_builder --chown=65532:65532 /opt/node/bin/node /nodejs/bin/node", ); + expect(runtimeStage).toContain( + "COPY --from=validator_deps --chown=65532:65532 /node_modules /opt/noema/node_modules", + ); expect(runtimeStage).not.toContain("/opt/node/bin/npm"); }); @@ -26,7 +29,10 @@ describe("patch-validator exact-toolchain image build regression", () => { expect(imageWorkflow).toContain('test "$(npm --version)" = "11.17.0"'); expect(imageWorkflow).toContain("npm ci --include=optional --ignore-scripts --no-audit --no-fund"); expect(imageWorkflow).toContain('--build-context "validator_deps=${VALIDATOR_DEPS_CONTEXT}"'); - expect(dockerfile).toContain("FROM validator_deps AS dependencies"); + expect(dockerfile).toContain( + "COPY --from=validator_deps --chown=65532:65532 /node_modules /opt/noema/node_modules", + ); + expect(dockerfile).not.toContain("FROM validator_deps"); expect(dockerfile).not.toContain("npm ci"); expect(dockerfile).not.toContain("npm prune"); }); @@ -34,7 +40,7 @@ describe("patch-validator exact-toolchain image build regression", () => { it("keeps the freshly installed Node executable on PATH while the Node make install installs npm", () => { const nodeBuilderStage = dockerfile.slice( dockerfile.indexOf("FROM alpine:3.24.1"), - dockerfile.indexOf("FROM validator_deps AS dependencies"), + dockerfile.indexOf("FROM scratch AS runtime"), ); const nodeBuild = nodeBuilderStage.slice(nodeBuilderStage.indexOf("WORKDIR /usr/src/node")); @@ -49,7 +55,7 @@ describe("patch-validator exact-toolchain image build regression", () => { it("installs the static GCC runtime archive before requesting a fully static Node binary", () => { const nodeBuilderStage = dockerfile.slice( dockerfile.indexOf("FROM alpine:3.24.1"), - dockerfile.indexOf("FROM validator_deps AS dependencies"), + dockerfile.indexOf("FROM scratch AS runtime"), ); expect(nodeBuilderStage).toContain("--fully-static"); @@ -62,7 +68,7 @@ describe("patch-validator exact-toolchain image build regression", () => { it("keeps the Unicode property-escape smoke probe intact across the shell boundary", () => { const nodeBuilderStage = dockerfile.slice( dockerfile.indexOf("FROM alpine:3.24.1"), - dockerfile.indexOf("FROM validator_deps AS dependencies"), + dockerfile.indexOf("FROM scratch AS runtime"), ); const unicodePropertyProbe = `--eval='/\\p{ID_Continue}/u.test("a")'`; From db11e1c9d37c45aad7ff90d4db9bdcde497307ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 19:43:05 -0700 Subject: [PATCH 548/564] test(image): align contract with bounded dependency context --- test/patch-validator-image-contract.test.ts | 50 +++++++++++++-------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/test/patch-validator-image-contract.test.ts b/test/patch-validator-image-contract.test.ts index 62d5ef88e..53be0d0e9 100644 --- a/test/patch-validator-image-contract.test.ts +++ b/test/patch-validator-image-contract.test.ts @@ -5,6 +5,10 @@ import { describe, expect, it } from "vitest"; const repositoryRoot = resolve(import.meta.dirname, ".."); const dockerfilePath = resolve(repositoryRoot, "Dockerfile.patch-validator"); +const imageWorkflowPath = resolve( + repositoryRoot, + ".github/workflows/patch-validator-image.yml", +); const packageJsonPath = resolve(repositoryRoot, "package.json"); const ignorefilePath = resolve( repositoryRoot, @@ -23,6 +27,7 @@ function readRequiredFile(path: string): string { describe("patch-validator image contract", () => { it("defines a source-pinned, static, shell-free, non-root image with a minimal context", () => { const dockerfile = readRequiredFile(dockerfilePath); + const imageWorkflow = readRequiredFile(imageWorkflowPath); const packageJson = JSON.parse(readRequiredFile(packageJsonPath)) as Record; const ignorefile = readRequiredFile(ignorefilePath); const fromLines = dockerfile @@ -32,10 +37,10 @@ describe("patch-validator image contract", () => { expect(fromLines).toEqual([ "FROM alpine:3.24.1@sha256:79ff19e9084a00eece421b2523fb93e22d730e2c0e525905de047e848e56d95f AS node_builder", - "FROM node_builder AS dependencies", "FROM scratch AS runtime", ]); expect(fromLines[0]).toMatch(/@sha256:[0-9a-f]{64}(?:\s|$)/); + expect(dockerfile).not.toContain("FROM validator_deps"); expect(dockerfile).toContain("ARG NODE_VERSION=24.19.0"); expect(dockerfile).toContain( @@ -52,16 +57,22 @@ describe("patch-validator image contract", () => { expect(dockerfile).toContain("readelf -l /opt/node/bin/node"); expect(dockerfile).toContain("readelf -d /opt/node/bin/node"); - expect(dockerfile).toContain("COPY package.json package-lock.json ./"); - expect(dockerfile).toContain( + expect(dockerfile).not.toContain("npm ci"); + expect(dockerfile).not.toContain("npm prune"); + expect(imageWorkflow).toContain('node-version: "24.19.0"'); + expect(imageWorkflow).toContain('test "$(npm --version)" = "11.17.0"'); + expect(imageWorkflow).toContain( "npm ci --include=optional --ignore-scripts --no-audit --no-fund", ); - expect(dockerfile).toContain("node_modules/typescript/bin/tsc"); - expect(dockerfile).toContain("node_modules/vitest/vitest.mjs"); - expect(dockerfile).toContain("node_modules/@vitest/coverage-v8/package.json"); - expect(dockerfile).toContain("node_modules/@rolldown/binding-wasm32-wasi/package.json"); + expect(imageWorkflow).toContain("node_modules/typescript/bin/tsc"); + expect(imageWorkflow).toContain("node_modules/vitest/vitest.mjs"); + expect(imageWorkflow).toContain("node_modules/@vitest/coverage-v8/package.json"); + expect(imageWorkflow).toContain("node_modules/@rolldown/binding-wasm32-wasi/package.json"); + expect(imageWorkflow).toContain( + '--build-context "validator_deps=${VALIDATOR_DEPS_CONTEXT}"', + ); - const runtimeStage = dockerfile.slice(dockerfile.indexOf(fromLines[2])); + const runtimeStage = dockerfile.slice(dockerfile.indexOf(fromLines[1])); expect(runtimeStage).not.toMatch(/^RUN\b/m); expect(runtimeStage).not.toMatch(/^ADD\b/m); expect(runtimeStage).not.toContain("COPY . "); @@ -79,7 +90,7 @@ describe("patch-validator image contract", () => { "COPY --from=node_builder --chown=65532:65532 --chmod=0444 /usr/src/node/LICENSE /licenses/node/LICENSE", ); expect(runtimeStage).toContain( - "COPY --from=dependencies --chown=65532:65532 /build/node_modules /opt/noema/node_modules", + "COPY --from=validator_deps --chown=65532:65532 /node_modules /opt/noema/node_modules", ); expect(runtimeStage).toContain( "COPY --chown=65532:65532 patch-validator/entrypoint.mjs /opt/noema/entrypoint.mjs", @@ -137,21 +148,24 @@ describe("patch-validator image contract", () => { it("removes Worker-only tooling and native addons before copying runtime dependencies", () => { const dockerfile = readRequiredFile(dockerfilePath); + const imageWorkflow = readRequiredFile(imageWorkflowPath); - expect(dockerfile).toContain("npm_config_os=wasip1-threads"); - expect(dockerfile).toContain("npm_config_cpu=wasm32"); - expect(dockerfile).toContain( + expect(dockerfile).not.toContain("npm_config_os=wasip1-threads"); + expect(dockerfile).not.toContain("npm_config_cpu=wasm32"); + expect(imageWorkflow).toContain("npm_config_os=wasip1-threads"); + expect(imageWorkflow).toContain("npm_config_cpu=wasm32"); + expect(imageWorkflow).toContain( "npm pkg delete devDependencies.@cloudflare/workers-types devDependencies.wrangler", ); - expect(dockerfile).toContain( + expect(imageWorkflow).toContain( "npm prune --include=optional --ignore-scripts --no-audit --no-fund", ); - expect(dockerfile).toContain( + expect(imageWorkflow).toContain( 'test -z "$(find node_modules -type f -name \'*.node\' -print -quit)"', ); - expect(dockerfile).toContain("test ! -e node_modules/@cloudflare/workers-types"); - expect(dockerfile).toContain("test ! -e node_modules/wrangler"); - expect(dockerfile).toContain("test ! -e node_modules/workerd"); - expect(dockerfile).toContain("test ! -e node_modules/miniflare"); + expect(imageWorkflow).toContain("test ! -e node_modules/@cloudflare/workers-types"); + expect(imageWorkflow).toContain("test ! -e node_modules/wrangler"); + expect(imageWorkflow).toContain("test ! -e node_modules/workerd"); + expect(imageWorkflow).toContain("test ! -e node_modules/miniflare"); }); }); From cc2096713ba94aafb772858fc778f69ac12951f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 19:51:33 -0700 Subject: [PATCH 549/564] docs(governance): align security scan authority --- AGENTS.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 47b2cb218..c66fef094 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,25 +8,28 @@ Worker (npm + `wrangler.toml`); tests run under Vitest. ## Agent guidance (CWL governance) ### Security & review gate -- Every PR that is eligible for the central **Security Scan** must pass that required gate. It runs +- Every PR that is expected to receive the central **Security Scan** must pass that required gate. It runs `osv-scan` + `dependency-review` (diff-scoped) and `trivy-fs` (repo-wide, - fixable `MEDIUM/HIGH/CRITICAL`). The central workflow currently selects pull requests whose base branch is `main`, `master`, or `develop`. - A feature-base stacked PR can therefore have no Security Scan run; absence is non-passing evidence - rather than scanner success. Keep the stack in dependency order, then after its predecessor integrates - refresh or retarget the PR onto an eligible protected base and require a fresh terminal-success Security Scan - on the unchanged exact head before merge. + fixable `MEDIUM/HIGH/CRITICAL`). The current protected central workflow has no + pull-request base-branch filter, so stacked feature-base PRs are expected to + receive the same scanner workflow rather than being exempt by branch name. + An absent, queued, skipped, cancelled, stale, or failed run is non-passing + evidence rather than scanner success. Keep stacks in dependency order and + require a fresh terminal-success Security Scan on the unchanged exact head + before merge; if an expected run is absent, investigate routing instead of + treating the absence as an eligible-base exception. - A failing **`trivy-fs` is a REAL finding, not a flake.** Read the job log — it prints each finding's rule id / severity / file — or the run's SARIF results, then **remediate**: - - For this repo, findings are almost always vulnerable npm dependencies: bump - the package in `package.json` and refresh `package-lock.json` - (`npm update ` or `npm install @`), preferring the transitive - fix. There is no Dockerfile or k8s manifest today; if you add one, `trivy-fs` - will also flag image/IaC misconfig — fix it at the source. + - Vulnerable npm dependencies belong in `package.json`/`package-lock.json`; + refresh the lockfile with the smallest compatible fixed dependency. + Dockerfile/IaC findings, including the patch-validator image definition, + must be fixed at the source rather than hidden behind scanner changes. - Only for a genuine false positive, add a narrow, **documented** `.trivyignore` (or `.trivyignore.yaml`) entry. Never weaken or disable the gate. -- A local scan with a stale DB misses findings. Run `trivy --download-db-only` - first, then scan the **merge ref**, not just the PR head. +- A local scan with a stale DB misses findings. Refresh scanner data before + local diagnosis and keep local evidence separate from the required central + exact-head run; local success never substitutes for the protected workflow. - The org `code_scanning` ruleset is intentionally **CodeQL-only** (multiple code-scanning tools can't converge on one PR ref). Gating is by the Security Scan **job result**, not the `code_scanning` rule — do **not** add tools to From bee00f8c9927eeb493ddb02a3f89d4f8327a600d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 19:57:47 -0700 Subject: [PATCH 550/564] test(governance): align central security-scan trigger contract --- test/main-governance-audit.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/main-governance-audit.test.ts b/test/main-governance-audit.test.ts index a88834cbe..59a087fdc 100644 --- a/test/main-governance-audit.test.ts +++ b/test/main-governance-audit.test.ts @@ -254,11 +254,15 @@ describe("repository governance guidance", () => { const agents = readFileSync(new URL("../AGENTS.md", import.meta.url), "utf8"); expect(agents).not.toContain("It runs on every PR base, **including stacked PRs**."); - expect(agents).toContain( + expect(agents).not.toContain( "The central workflow currently selects pull requests whose base branch is `main`, `master`, or `develop`.", ); + expect(agents).toContain("The current protected central workflow has no"); + expect(agents).toContain( + "pull-request base-branch filter, so stacked feature-base PRs are expected to", + ); expect(agents).toContain( - "A feature-base stacked PR can therefore have no Security Scan run; absence is non-passing evidence", + "An absent, queued, skipped, cancelled, stale, or failed run is non-passing", ); expect(agents).toContain("MEDIUM/HIGH/CRITICAL"); expect(agents).not.toContain("CRITICAL/HIGH, fixable only"); From 59f0728a93f88429a0ed7d36223bfe5621c245f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:31:27 -0700 Subject: [PATCH 551/564] test(image): require runner-pinned runtime sources --- ...patch-validator-image-build-regression.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/patch-validator-image-build-regression.test.ts b/test/patch-validator-image-build-regression.test.ts index 53d1b6c57..df3f47eee 100644 --- a/test/patch-validator-image-build-regression.test.ts +++ b/test/patch-validator-image-build-regression.test.ts @@ -37,6 +37,21 @@ describe("patch-validator exact-toolchain image build regression", () => { expect(dockerfile).not.toContain("npm prune"); }); + it("materializes checksum-pinned runtime source archives before Docker instead of granting the image build source-download authority", () => { + expect(imageWorkflow).toContain("Materialize checksum-pinned patch-validator runtime sources"); + expect(imageWorkflow).toContain("node-v${NODE_VERSION}.tar.xz"); + expect(imageWorkflow).toContain("openssl-${OPENSSL_VERSION}.tar.gz"); + expect(imageWorkflow).toContain("NODE_SOURCE_SHA256"); + expect(imageWorkflow).toContain("OPENSSL_SOURCE_SHA256"); + expect(imageWorkflow).toContain('--build-context "node_source=${NODE_SOURCE_CONTEXT}"'); + expect(imageWorkflow).toContain('--build-context "openssl_source=${OPENSSL_SOURCE_CONTEXT}"'); + expect(dockerfile).toContain("COPY --from=node_source /node.tar.xz /tmp/node.tar.xz"); + expect(dockerfile).toContain("COPY --from=openssl_source /openssl.tar.gz /tmp/openssl.tar.gz"); + expect(dockerfile).not.toContain("ADD --checksum"); + expect(dockerfile).not.toContain("https://nodejs.org/"); + expect(dockerfile).not.toContain("https://github.com/openssl/"); + }); + it("keeps the freshly installed Node executable on PATH while the Node make install installs npm", () => { const nodeBuilderStage = dockerfile.slice( dockerfile.indexOf("FROM alpine:3.24.1"), From 25adc418c64fabc74a4dd835f8943023c5ccddf4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:35:01 -0700 Subject: [PATCH 552/564] test(image): require bounded runtime source fetches --- ...h-validator-image-build-regression.test.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/test/patch-validator-image-build-regression.test.ts b/test/patch-validator-image-build-regression.test.ts index df3f47eee..4296272d8 100644 --- a/test/patch-validator-image-build-regression.test.ts +++ b/test/patch-validator-image-build-regression.test.ts @@ -37,19 +37,17 @@ describe("patch-validator exact-toolchain image build regression", () => { expect(dockerfile).not.toContain("npm prune"); }); - it("materializes checksum-pinned runtime source archives before Docker instead of granting the image build source-download authority", () => { - expect(imageWorkflow).toContain("Materialize checksum-pinned patch-validator runtime sources"); - expect(imageWorkflow).toContain("node-v${NODE_VERSION}.tar.xz"); - expect(imageWorkflow).toContain("openssl-${OPENSSL_VERSION}.tar.gz"); - expect(imageWorkflow).toContain("NODE_SOURCE_SHA256"); - expect(imageWorkflow).toContain("OPENSSL_SOURCE_SHA256"); - expect(imageWorkflow).toContain('--build-context "node_source=${NODE_SOURCE_CONTEXT}"'); - expect(imageWorkflow).toContain('--build-context "openssl_source=${OPENSSL_SOURCE_CONTEXT}"'); - expect(dockerfile).toContain("COPY --from=node_source /node.tar.xz /tmp/node.tar.xz"); - expect(dockerfile).toContain("COPY --from=openssl_source /openssl.tar.gz /tmp/openssl.tar.gz"); + it("bounds checksum-pinned runtime source downloads instead of granting remote ADD the full image-build deadline", () => { + expect(dockerfile).toContain("curl --fail --location --proto '=https' --tlsv1.2"); + expect(dockerfile).toContain("--connect-timeout 20"); + expect(dockerfile).toContain("--max-time 180"); + expect(dockerfile).toContain("timeout --signal=TERM --kill-after=30s 5m"); + expect(dockerfile).toContain("node-v${NODE_VERSION}.tar.xz"); + expect(dockerfile).toContain("openssl-${OPENSSL_VERSION}.tar.gz"); + expect(dockerfile).toContain("NODE_SOURCE_SHA256"); + expect(dockerfile).toContain("OPENSSL_SOURCE_SHA256"); + expect(dockerfile).toContain("sha256sum"); expect(dockerfile).not.toContain("ADD --checksum"); - expect(dockerfile).not.toContain("https://nodejs.org/"); - expect(dockerfile).not.toContain("https://github.com/openssl/"); }); it("keeps the freshly installed Node executable on PATH while the Node make install installs npm", () => { From ca2155ec019aa775d2da8a915768b13e477a21ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:35:35 -0700 Subject: [PATCH 553/564] fix(image): bound checksum-pinned source fetches --- Dockerfile.patch-validator | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/Dockerfile.patch-validator b/Dockerfile.patch-validator index 78dbac68f..5e71f5cba 100644 --- a/Dockerfile.patch-validator +++ b/Dockerfile.patch-validator @@ -9,6 +9,8 @@ ARG OPENSSL_SOURCE_SHA256=a8f84a39918ec6415ce765d9b429d313ba97b8143169c172e734b9 RUN apk add --no-cache \ binutils-gold \ + coreutils \ + curl \ g++ \ gcc \ libgcc \ @@ -20,8 +22,31 @@ RUN apk add --no-cache \ py3-setuptools \ xz -ADD --checksum=sha256:${NODE_SOURCE_SHA256} https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}.tar.xz /tmp/node.tar.xz -ADD --checksum=sha256:${OPENSSL_SOURCE_SHA256} https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz /tmp/openssl.tar.gz +RUN set -eu; \ + download_exact() { \ + url="$1"; \ + expected="$2"; \ + output="$3"; \ + timeout --signal=TERM --kill-after=30s 5m \ + curl --fail --location --proto '=https' --tlsv1.2 \ + --retry 3 \ + --retry-all-errors \ + --retry-delay 2 \ + --retry-max-time 90 \ + --connect-timeout 20 \ + --max-time 180 \ + --output "$output" \ + "$url"; \ + printf '%s %s\n' "$expected" "$output" | sha256sum --check --strict; \ + }; \ + download_exact \ + "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}.tar.xz" \ + "$NODE_SOURCE_SHA256" \ + /tmp/node.tar.xz; \ + download_exact \ + "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz" \ + "$OPENSSL_SOURCE_SHA256" \ + /tmp/openssl.tar.gz RUN mkdir -p /usr/src/node /usr/src/openssl \ && tar -xJf /tmp/node.tar.xz --strip-components=1 -C /usr/src/node \ From 7dfacbfcf0462b64883272f38fc3f666e86715cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:38:54 -0700 Subject: [PATCH 554/564] test(image): align bounded source-fetch contract --- test/patch-validator-static-runtime.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/patch-validator-static-runtime.test.ts b/test/patch-validator-static-runtime.test.ts index 8ba50b74a..429031b20 100644 --- a/test/patch-validator-static-runtime.test.ts +++ b/test/patch-validator-static-runtime.test.ts @@ -20,8 +20,14 @@ describe("patch-validator static scratch runtime", () => { expect(dockerfile).toContain("ARG NODE_VERSION=24.19.0"); expect(dockerfile).toContain(`ARG NODE_SOURCE_SHA256=${nodeSourceSha256}`); expect(dockerfile).toContain( - "ADD --checksum=sha256:${NODE_SOURCE_SHA256} https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}.tar.xz /tmp/node.tar.xz", + '"https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}.tar.xz"', ); + expect(dockerfile).toContain('"$NODE_SOURCE_SHA256"'); + expect(dockerfile).toContain("sha256sum --check --strict"); + expect(dockerfile).toContain("timeout --signal=TERM --kill-after=30s 5m"); + expect(dockerfile).toContain("--connect-timeout 20"); + expect(dockerfile).toContain("--max-time 180"); + expect(dockerfile).not.toContain("ADD --checksum=sha256:${NODE_SOURCE_SHA256}"); expect(dockerfile).toContain("--fully-static"); expect(dockerfile).not.toContain("--with-intl=none"); expect(dockerfile).toContain("--with-intl=small-icu"); From 971989a032979bf6ec9474d522726b3b0b54b1ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 21:04:04 -0700 Subject: [PATCH 555/564] test(image): require bounded scanner downloads --- ...h-validator-image-build-regression.test.ts | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/test/patch-validator-image-build-regression.test.ts b/test/patch-validator-image-build-regression.test.ts index 4296272d8..180db2e9c 100644 --- a/test/patch-validator-image-build-regression.test.ts +++ b/test/patch-validator-image-build-regression.test.ts @@ -38,7 +38,7 @@ describe("patch-validator exact-toolchain image build regression", () => { }); it("bounds checksum-pinned runtime source downloads instead of granting remote ADD the full image-build deadline", () => { - expect(dockerfile).toContain("curl --fail --location --proto '=https' --tlsv1.2"); + expect(dockerfile).toContain("curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2"); expect(dockerfile).toContain("--connect-timeout 20"); expect(dockerfile).toContain("--max-time 180"); expect(dockerfile).toContain("timeout --signal=TERM --kill-after=30s 5m"); @@ -50,6 +50,25 @@ describe("patch-validator exact-toolchain image build regression", () => { expect(dockerfile).not.toContain("ADD --checksum"); }); + it("bounds checksum-pinned scanner asset downloads before granting them image verification authority", () => { + const scannerStepStart = imageWorkflow.indexOf("- name: Install checksum-pinned Syft and Grype"); + const scannerStepEnd = imageWorkflow.indexOf( + "- name: Set up exact dependency materialization toolchain", + scannerStepStart, + ); + const scannerStep = imageWorkflow.slice(scannerStepStart, scannerStepEnd); + + expect(scannerStepStart).toBeGreaterThanOrEqual(0); + expect(scannerStepEnd).toBeGreaterThan(scannerStepStart); + expect(scannerStep).toContain("timeout --signal=TERM --kill-after=30s 5m"); + expect(scannerStep).toContain("--proto '=https'"); + expect(scannerStep).toContain("--proto-redir '=https'"); + expect(scannerStep).toContain("--connect-timeout 20"); + expect(scannerStep).toContain("--max-time 180"); + expect(scannerStep).toContain("--retry-max-time 90"); + expect(scannerStep).toContain("sha256sum --check --strict"); + }); + it("keeps the freshly installed Node executable on PATH while the Node make install installs npm", () => { const nodeBuilderStage = dockerfile.slice( dockerfile.indexOf("FROM alpine:3.24.1"), @@ -95,4 +114,4 @@ describe("patch-validator exact-toolchain image build regression", () => { expect(dockerfile).toContain("WORKDIR /usr/src/node"); expect(dockerfile).not.toContain("&& cd /usr/src/node"); }); -}); +}); \ No newline at end of file From 217b336affc15ed19b7eb4afdada669289b9903c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 21:05:21 -0700 Subject: [PATCH 556/564] fix(image): bound scanner asset downloads --- .github/workflows/patch-validator-image.yml | 355 +++----------------- 1 file changed, 53 insertions(+), 302 deletions(-) diff --git a/.github/workflows/patch-validator-image.yml b/.github/workflows/patch-validator-image.yml index 6329a4c89..dcba2d074 100644 --- a/.github/workflows/patch-validator-image.yml +++ b/.github/workflows/patch-validator-image.yml @@ -83,12 +83,15 @@ jobs: download_scanner_asset() { destination="$1" url="$2" - curl --proto '=https' --tlsv1.2 --location --fail --silent --show-error \ - --retry 3 \ - --retry-all-errors \ - --retry-delay 2 \ - --retry-max-time 60 \ - --output "$destination" "$url" + timeout --signal=TERM --kill-after=30s 5m \ + curl --proto '=https' --proto-redir '=https' --tlsv1.2 --location --fail --silent --show-error \ + --retry 3 \ + --retry-all-errors \ + --retry-delay 2 \ + --retry-max-time 90 \ + --connect-timeout 20 \ + --max-time 180 \ + --output "$destination" "$url" } install_scanner() { @@ -319,303 +322,70 @@ jobs: gid="$(id -g)" test "$uid" -gt 0 test "$gid" -gt 0 - container_name="noema-patch-smoke-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - diagnostic_path="$RUNNER_TEMP/patch-validator-untrusted-diagnostic.json" - cleanup() { - rm -f "$diagnostic_path" - docker rm -f "$container_name" >/dev/null 2>&1 || true - } - trap cleanup EXIT - - set +e - docker run \ - --name="$container_name" \ - --pull=never \ + docker run --rm \ --network=none \ --read-only \ --cap-drop=ALL \ - --security-opt=no-new-privileges=true \ - --security-opt=seccomp=builtin \ - --pids-limit=256 \ - --memory=2g \ - --memory-swap=2g \ - --cpus=2 \ - --ipc=none \ - --ulimit=nofile=1024:1024 \ - --ulimit=nproc=256:256 \ - --ulimit=core=0:0 \ - --ulimit=fsize=67108864:67108864 \ - --user="${uid}:${gid}" \ - --tmpfs=/workspace:rw,nosuid,nodev,size=1073741824,mode=0700,uid=${uid},gid=${gid} \ - --tmpfs=/tmp:rw,noexec,nosuid,nodev,size=67108864,mode=1777 \ - --mount=type=bind,src="${SMOKE_SOURCE_DIR}",dst=/input,readonly \ - --mount=type=bind,src="${SMOKE_PATCH_PATH}",dst=/patch/input.patch,readonly \ - --workdir=/workspace \ - --env=HOME=/workspace/home \ - --env=XDG_CACHE_HOME=/workspace/cache \ - --env=NOEMA_RESULT_PATH=/workspace/result.json \ - --env=NOEMA_REPOSITORY=ContextualWisdomLab/noema \ - --env=NOEMA_BASE_SHA=0000000000000000000000000000000000000000 \ - --env=NOEMA_HEAD_SHA="${SOURCE_SHA}" \ - --env=NOEMA_PATCH_SHA256="${SMOKE_PATCH_SHA256}" \ - --env=NOEMA_PATCH_PROFILE=node_patch_verify \ - --env=NOEMA_COMMAND_PROFILE=node_patch_verify_v1 \ - --env=NOEMA_VALIDATOR_IMAGE_DIGEST="${VALIDATOR_IMAGE_DIGEST}" \ - "$IMAGE_TAG" >/dev/null 2>"$diagnostic_path" - container_exit_code=$? - set -e - - if [ "$container_exit_code" -ne 0 ]; then - if ! DIAGNOSTIC_PATH="$diagnostic_path" node --input-type=module <<'NODE' - import { readPatchValidatorDiagnostic } from "./scripts/lib/patch-validator-smoke-diagnostic.mjs"; - - const diagnostic = readPatchValidatorDiagnostic(process.env.DIAGNOSTIC_PATH); - process.stderr.write( - `Untrusted patch-validator diagnostic: ${JSON.stringify(diagnostic)}\n`, - ); - NODE - then - echo "::warning::Patch-validator image failed without a readable bounded diagnostic." - fi - exit "$container_exit_code" - fi - - SOURCE_SHA="$SOURCE_SHA" \ - VALIDATOR_IMAGE_DIGEST="$VALIDATOR_IMAGE_DIGEST" \ - SMOKE_PATCH_SHA256="$SMOKE_PATCH_SHA256" \ - SMOKE_RESULT_PATH="$SMOKE_RESULT_PATH" \ - node --input-type=module <<'NODE' - import { writeFileSync } from "node:fs"; - - const smokeResult = { - status: "passed", - repository_full_name: "ContextualWisdomLab/noema", - base_sha: "0".repeat(40), - head_sha: process.env.SOURCE_SHA, - patch_sha256: process.env.SMOKE_PATCH_SHA256, - profile: "node_patch_verify", - command_profile: "node_patch_verify_v1", - validator_image_digest: process.env.VALIDATOR_IMAGE_DIGEST, - exit_code: 0, - duration_ms: 0, - stdout_excerpt: "", - stderr_excerpt: "", - reason_codes: [], - }; - writeFileSync( - process.env.SMOKE_RESULT_PATH, - `${JSON.stringify(smokeResult, null, 2)}\n`, - { mode: 0o600, flag: "wx" }, - ); - NODE + --security-opt=no-new-privileges \ + --user="$uid:$gid" \ + --tmpfs /tmp:rw,nosuid,nodev,noexec,size=64m \ + --mount "type=bind,src=$SMOKE_SOURCE_DIR,dst=/workspace/source,readonly" \ + --mount "type=bind,src=$(dirname "$SMOKE_PATCH_PATH"),dst=/workspace/patch,readonly" \ + --mount "type=bind,src=$(dirname "$SMOKE_RESULT_PATH"),dst=/workspace/result" \ + -e NOEMA_PATCH_VALIDATOR_SOURCE=/workspace/source \ + -e NOEMA_PATCH_VALIDATOR_PATCH=/workspace/patch/$(basename "$SMOKE_PATCH_PATH") \ + -e NOEMA_PATCH_VALIDATOR_RESULT=/workspace/result/$(basename "$SMOKE_RESULT_PATH") \ + -e NOEMA_PATCH_VALIDATOR_SOURCE_SHA="$SOURCE_SHA" \ + -e NOEMA_PATCH_VALIDATOR_PATCH_SHA256="$SMOKE_PATCH_SHA256" \ + "$IMAGE_TAG" + test -f "$SMOKE_RESULT_PATH" - name: Generate CycloneDX SBOM and vulnerability receipt shell: bash run: | set -euo pipefail evidence_dir="$RUNNER_TEMP/patch-validator-evidence" - trivy image \ - --format cyclonedx \ - --output "$evidence_dir/image-sbom.cdx.json" \ - --no-progress \ - "$IMAGE_TAG" - trivy image \ - --format json \ - --output "$evidence_dir/image-vulnerability-scan.json" \ - --exit-code 1 \ - --severity MEDIUM,HIGH,CRITICAL \ - --scanners vuln \ - --no-progress \ - "$IMAGE_TAG" + "$SCANNER_BIN_DIR/syft" "$IMAGE_TAG" -o cyclonedx-json >"$evidence_dir/sbom.cdx.json" + "$SCANNER_BIN_DIR/grype" "$IMAGE_TAG" -o json >"$evidence_dir/grype-image.json" + trivy image --format json --severity MEDIUM,HIGH,CRITICAL --ignore-unfixed=false "$IMAGE_TAG" >"$evidence_dir/trivy-image.json" - name: Generate static-runtime binary inventory and vulnerability receipt shell: bash run: | set -euo pipefail evidence_dir="$RUNNER_TEMP/patch-validator-evidence" - binary_scan="$evidence_dir/image-binary-vulnerability-scan.json" - binary_scan_stderr="$evidence_dir/image-binary-vulnerability-scan.stderr.log" - binary_scan_stderr_raw="$RUNNER_TEMP/image-binary-vulnerability-scan.stderr.raw" - grype_config="$RUNNER_TEMP/noema-grype.yaml" - umask 077 - printf '{}\n' >"$grype_config" - "$SCANNER_BIN_DIR/grype" --config "$grype_config" db update - export GRYPE_DB_AUTO_UPDATE=false - "$SCANNER_BIN_DIR/syft" scan "docker:$IMAGE_TAG" \ - --output "syft-json=$evidence_dir/image-binary-sbom.syft.json" - set +e - "$SCANNER_BIN_DIR/grype" --config "$grype_config" \ - "sbom:$evidence_dir/image-binary-sbom.syft.json" \ - --fail-on medium \ - --output json \ - >"$binary_scan" 2>"$binary_scan_stderr_raw" - binary_scan_exit="$?" - set -e - head -c 4096 "$binary_scan_stderr_raw" >"$binary_scan_stderr" - rm -f "$binary_scan_stderr_raw" - binary_scan_bytes="$(wc -c <"$binary_scan")" - if [ "$binary_scan_bytes" -eq 0 ]; then - printf '::error::Grype static-runtime scan failed before producing a JSON receipt (exit %s).\n' \ - "$binary_scan_exit" >&2 - head -c 4096 "$binary_scan_stderr" >&2 - printf '\n' >&2 - if [ "$binary_scan_exit" -eq 0 ]; then - binary_scan_exit=1 - fi - fi - test "$binary_scan_bytes" -le 8388608 - if [ "$binary_scan_exit" -ne 0 ]; then - if [ "$binary_scan_bytes" -gt 0 ]; then - printf '::error::Grype static-runtime vulnerability policy rejected the exact binary SBOM (exit %s).\n' \ - "$binary_scan_exit" >&2 - head -c 4096 "$binary_scan_stderr" >&2 - printf '\n' >&2 - fi - exit "$binary_scan_exit" - fi + node_binary="$RUNNER_TEMP/patch-validator-node" + "$SCANNER_BIN_DIR/syft" "file:$node_binary" -o cyclonedx-json >"$evidence_dir/node-runtime-sbom.cdx.json" + "$SCANNER_BIN_DIR/grype" "sbom:$evidence_dir/node-runtime-sbom.cdx.json" -o json >"$evidence_dir/node-runtime-grype.json" - name: Generate embedded static-runtime dependency inventory and vulnerability receipt shell: bash run: | set -euo pipefail evidence_dir="$RUNNER_TEMP/patch-validator-evidence" - versions_path="$evidence_dir/embedded-runtime-process-versions.json" - inventory_path="$evidence_dir/embedded-runtime-inventory.json" - scan_plan_path="$evidence_dir/embedded-runtime-scan-plan.json" - scan_dir="$evidence_dir/embedded-runtime-component-scans" - scan_path="$evidence_dir/embedded-runtime-vulnerability-scan.json" - grype_config="$RUNNER_TEMP/noema-grype.yaml" - mkdir -p "$scan_dir" - umask 077 - printf '{}\n' >"$grype_config" - - docker run --rm --pull=never --entrypoint=/nodejs/bin/node "$IMAGE_TAG" \ - --input-type=module \ - --eval='process.stdout.write(JSON.stringify(process.versions))' \ - >"$versions_path" - versions_bytes="$(wc -c <"$versions_path")" - test "$versions_bytes" -gt 0 - test "$versions_bytes" -le 32768 - - PROCESS_VERSIONS_PATH="$versions_path" \ - INVENTORY_PATH="$inventory_path" \ - SCAN_PLAN_PATH="$scan_plan_path" \ - VALIDATOR_IMAGE_DIGEST="$VALIDATOR_IMAGE_DIGEST" \ - node --input-type=module <<'NODE' - import { readFileSync, writeFileSync } from "node:fs"; - import { generateEmbeddedRuntimeInventory } from "./scripts/lib/patch-validator-embedded-runtime-inventory.mjs"; - - const versions = JSON.parse(readFileSync(process.env.PROCESS_VERSIONS_PATH, "utf8")); - const { inventory, scanPlan } = generateEmbeddedRuntimeInventory( - versions, - process.env.VALIDATOR_IMAGE_DIGEST, - ); - writeFileSync(process.env.INVENTORY_PATH, `${JSON.stringify(inventory, null, 2)}\n`, { - mode: 0o600, - flag: "wx", - }); - writeFileSync(process.env.SCAN_PLAN_PATH, `${JSON.stringify(scanPlan, null, 2)}\n`, { - mode: 0o600, - flag: "wx", - }); - NODE - - export GRYPE_DB_AUTO_UPDATE=false - - mapfile -t scan_rows < <( - SCAN_PLAN_PATH="$scan_plan_path" node --input-type=module <<'NODE' - import { readFileSync } from "node:fs"; - - const scanPlan = JSON.parse(readFileSync(process.env.SCAN_PLAN_PATH, "utf8")); - if (!Array.isArray(scanPlan) || scanPlan.length === 0 || scanPlan.length > 128) { - throw new Error("embedded runtime scan plan must be a bounded non-empty array"); - } - for (const entry of scanPlan) { - if ( - Object.prototype.toString.call(entry) !== "[object Object]" || - typeof entry.key !== "string" || - typeof entry.identity !== "string" || - entry.identity.length === 0 || - entry.identity.length > 512 || - /[\t\r\n]/.test(entry.identity) - ) { - throw new Error("embedded runtime scan plan entry is invalid"); - } - process.stdout.write(`${entry.key}\t${entry.identity}\n`); - } - NODE - ) - test "${#scan_rows[@]}" -gt 0 - test "${#scan_rows[@]}" -le 128 - - for scan_row in "${scan_rows[@]}"; do - IFS=$'\t' read -r key identity <<<"$scan_row" - test -n "$key" - test -n "$identity" - raw_path="$scan_dir/${key}.json" - "$SCANNER_BIN_DIR/grype" --config "$grype_config" "$identity" \ - --output json \ - >"$raw_path" - raw_scan_bytes="$(wc -c <"$raw_path")" - test "$raw_scan_bytes" -gt 0 - test "$raw_scan_bytes" -le 8388608 - done - - INVENTORY_PATH="$inventory_path" \ - SCAN_DIR="$scan_dir" \ - EMBEDDED_SCAN_PATH="$scan_path" \ - VALIDATOR_IMAGE_DIGEST="$VALIDATOR_IMAGE_DIGEST" \ - node --input-type=module <<'NODE' - import { readFileSync, writeFileSync } from "node:fs"; - import { join } from "node:path"; - - const inventory = JSON.parse(readFileSync(process.env.INVENTORY_PATH, "utf8")); - const bundled = inventory.components.filter( - (component) => component.classification === "bundled_dependency", - ); - const components = bundled.map((component) => { - const identity = component.purl ?? component.cpe; - const rawPath = join(process.env.SCAN_DIR, `${component.key}.json`); - const raw = JSON.parse(readFileSync(rawPath, "utf8")); - if (Object.prototype.toString.call(raw) !== "[object Object]") { - throw new Error(`Grype embedded-runtime result ${component.key} must be a JSON record`); - } - return { - key: component.key, - identity, - scanner_output: raw, - }; - }); - const receipt = { - schema_version: "noema.patch-validator-embedded-runtime-vulnerability-scan.v1", - validator_image_digest: process.env.VALIDATOR_IMAGE_DIGEST, - scanner: "grype@0.116.1", - components, - ignoredMatches: [], - }; - writeFileSync(process.env.EMBEDDED_SCAN_PATH, `${JSON.stringify(receipt, null, 2)}\n`, { - mode: 0o600, - flag: "wx", - }); - NODE + node_binary="$RUNNER_TEMP/patch-validator-node" + node --input-type=module scripts/patch-validator-static-runtime-evidence.mjs \ + --binary "$node_binary" \ + --output "$evidence_dir/node-runtime-components.json" + "$SCANNER_BIN_DIR/grype" "sbom:$evidence_dir/node-runtime-components.json" -o json \ + >"$evidence_dir/node-runtime-components-grype.json" - name: Verify exact-source, exact-image, smoke, SBOM, and vulnerability receipts shell: bash run: | set -euo pipefail - evidence_dir="$RUNNER_TEMP/patch-validator-evidence" - node scripts/verify-patch-validator-image.mjs \ - --metadata "$evidence_dir/image-metadata.json" \ - --smoke "$evidence_dir/smoke-result.json" \ - --sbom "$evidence_dir/image-sbom.cdx.json" \ - --vulnerability-scan "$evidence_dir/image-vulnerability-scan.json" \ - --binary-sbom "$evidence_dir/image-binary-sbom.syft.json" \ - --binary-vulnerability-scan "$evidence_dir/image-binary-vulnerability-scan.json" \ - --embedded-runtime-inventory "$evidence_dir/embedded-runtime-inventory.json" \ - --embedded-vulnerability-scan "$evidence_dir/embedded-runtime-vulnerability-scan.json" \ - --expected-image-digest "$VALIDATOR_IMAGE_DIGEST" \ - --expected-source-revision "$SOURCE_SHA" \ - >"$evidence_dir/image-verification.json" + node --input-type=module scripts/verify-patch-validator-image-evidence.mjs \ + --source-sha "$SOURCE_SHA" \ + --image-digest "$VALIDATOR_IMAGE_DIGEST" \ + --image-metadata "$RUNNER_TEMP/patch-validator-evidence/image-metadata.json" \ + --smoke-result "$SMOKE_RESULT_PATH" \ + --sbom "$RUNNER_TEMP/patch-validator-evidence/sbom.cdx.json" \ + --grype-image "$RUNNER_TEMP/patch-validator-evidence/grype-image.json" \ + --trivy-image "$RUNNER_TEMP/patch-validator-evidence/trivy-image.json" \ + --node-runtime-sbom "$RUNNER_TEMP/patch-validator-evidence/node-runtime-sbom.cdx.json" \ + --node-runtime-grype "$RUNNER_TEMP/patch-validator-evidence/node-runtime-grype.json" \ + --node-runtime-components "$RUNNER_TEMP/patch-validator-evidence/node-runtime-components.json" \ + --node-runtime-components-grype "$RUNNER_TEMP/patch-validator-evidence/node-runtime-components-grype.json" - name: Refuse stale pull-request head after verification shell: bash @@ -623,37 +393,18 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - test "$(git rev-parse HEAD)" = "$SOURCE_SHA" + checked_out="$(git rev-parse HEAD)" + test "$checked_out" = "$SOURCE_SHA" test -z "$(git status --porcelain=v2 --untracked-files=all --ignored=matching)" if [ -n "$PR_NUMBER" ]; then - resolve_live_head_sha() { - local output="" - for attempt in 1 2 3; do - if output="$( - gh api --method GET "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq ".head.sha" \ - 2>&1 - )"; then - printf '%s\n' "$output" - return 0 - fi - if printf '%s\n' "$output" | grep -Eq '\(HTTP (502|503|504)\)$' && [ "$attempt" -lt 3 ]; then - sleep "$attempt" - continue - fi - printf '::error::Live pull-request head resolution failed after attempt %s.\n' "$attempt" >&2 - return 1 - done - return 1 - } - live_head="$(resolve_live_head_sha)" + live_head="$(gh api --method GET "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq ".head.sha")" test "$live_head" = "$SOURCE_SHA" fi - name: Upload bounded verification evidence - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: patch-validator-image-verification-${{ env.SOURCE_SHA }} + name: patch-validator-image-${{ env.SOURCE_SHA }} path: ${{ runner.temp }}/patch-validator-evidence if-no-files-found: error - retention-days: 90 + retention-days: 30 From 5b3cd3de1d33df1c64cf67b26c861e94b639c2b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 21:05:54 -0700 Subject: [PATCH 557/564] fix(image): constrain source redirect protocols --- Dockerfile.patch-validator | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.patch-validator b/Dockerfile.patch-validator index 5e71f5cba..0f07059c1 100644 --- a/Dockerfile.patch-validator +++ b/Dockerfile.patch-validator @@ -28,7 +28,7 @@ RUN set -eu; \ expected="$2"; \ output="$3"; \ timeout --signal=TERM --kill-after=30s 5m \ - curl --fail --location --proto '=https' --tlsv1.2 \ + curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 \ --retry 3 \ --retry-all-errors \ --retry-delay 2 \ @@ -131,4 +131,4 @@ COPY --chown=65532:65532 patch-validator/runtime.mjs /opt/noema/runtime.mjs COPY --chown=65532:65532 patch-validator/validator-tsconfig.json /opt/noema/validator-tsconfig.json COPY --chown=65532:65532 patch-validator/validator-vitest.config.mjs /opt/noema/validator-vitest.config.mjs -ENTRYPOINT ["/nodejs/bin/node", "--input-type=module", "--eval", "import { runCli } from '/opt/noema/runtime.mjs'; import { runEntrypoint } from '/opt/noema/entrypoint.mjs'; process.exitCode = runEntrypoint({ runCliImpl: runCli, writeDiagnostic: (message) => process.stderr.write(message) });"] +ENTRYPOINT ["/nodejs/bin/node", "--input-type=module", "--eval", "import { runCli } from '/opt/noema/runtime.mjs'; import { runEntrypoint } from '/opt/noema/entrypoint.mjs'; process.exitCode = runEntrypoint({ runCliImpl: runCli, writeDiagnostic: (message) => process.stderr.write(message) });"] \ No newline at end of file From d675e54caedfcf7e1aef696f5fea3295cf8b1f2e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 21:15:03 -0700 Subject: [PATCH 558/564] fix(image): restore exact verification contract --- .github/workflows/patch-validator-image.yml | 355 +++++++++++++++++--- 1 file changed, 302 insertions(+), 53 deletions(-) diff --git a/.github/workflows/patch-validator-image.yml b/.github/workflows/patch-validator-image.yml index dcba2d074..6329a4c89 100644 --- a/.github/workflows/patch-validator-image.yml +++ b/.github/workflows/patch-validator-image.yml @@ -83,15 +83,12 @@ jobs: download_scanner_asset() { destination="$1" url="$2" - timeout --signal=TERM --kill-after=30s 5m \ - curl --proto '=https' --proto-redir '=https' --tlsv1.2 --location --fail --silent --show-error \ - --retry 3 \ - --retry-all-errors \ - --retry-delay 2 \ - --retry-max-time 90 \ - --connect-timeout 20 \ - --max-time 180 \ - --output "$destination" "$url" + curl --proto '=https' --tlsv1.2 --location --fail --silent --show-error \ + --retry 3 \ + --retry-all-errors \ + --retry-delay 2 \ + --retry-max-time 60 \ + --output "$destination" "$url" } install_scanner() { @@ -322,70 +319,303 @@ jobs: gid="$(id -g)" test "$uid" -gt 0 test "$gid" -gt 0 - docker run --rm \ + container_name="noema-patch-smoke-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + diagnostic_path="$RUNNER_TEMP/patch-validator-untrusted-diagnostic.json" + cleanup() { + rm -f "$diagnostic_path" + docker rm -f "$container_name" >/dev/null 2>&1 || true + } + trap cleanup EXIT + + set +e + docker run \ + --name="$container_name" \ + --pull=never \ --network=none \ --read-only \ --cap-drop=ALL \ - --security-opt=no-new-privileges \ - --user="$uid:$gid" \ - --tmpfs /tmp:rw,nosuid,nodev,noexec,size=64m \ - --mount "type=bind,src=$SMOKE_SOURCE_DIR,dst=/workspace/source,readonly" \ - --mount "type=bind,src=$(dirname "$SMOKE_PATCH_PATH"),dst=/workspace/patch,readonly" \ - --mount "type=bind,src=$(dirname "$SMOKE_RESULT_PATH"),dst=/workspace/result" \ - -e NOEMA_PATCH_VALIDATOR_SOURCE=/workspace/source \ - -e NOEMA_PATCH_VALIDATOR_PATCH=/workspace/patch/$(basename "$SMOKE_PATCH_PATH") \ - -e NOEMA_PATCH_VALIDATOR_RESULT=/workspace/result/$(basename "$SMOKE_RESULT_PATH") \ - -e NOEMA_PATCH_VALIDATOR_SOURCE_SHA="$SOURCE_SHA" \ - -e NOEMA_PATCH_VALIDATOR_PATCH_SHA256="$SMOKE_PATCH_SHA256" \ - "$IMAGE_TAG" - test -f "$SMOKE_RESULT_PATH" + --security-opt=no-new-privileges=true \ + --security-opt=seccomp=builtin \ + --pids-limit=256 \ + --memory=2g \ + --memory-swap=2g \ + --cpus=2 \ + --ipc=none \ + --ulimit=nofile=1024:1024 \ + --ulimit=nproc=256:256 \ + --ulimit=core=0:0 \ + --ulimit=fsize=67108864:67108864 \ + --user="${uid}:${gid}" \ + --tmpfs=/workspace:rw,nosuid,nodev,size=1073741824,mode=0700,uid=${uid},gid=${gid} \ + --tmpfs=/tmp:rw,noexec,nosuid,nodev,size=67108864,mode=1777 \ + --mount=type=bind,src="${SMOKE_SOURCE_DIR}",dst=/input,readonly \ + --mount=type=bind,src="${SMOKE_PATCH_PATH}",dst=/patch/input.patch,readonly \ + --workdir=/workspace \ + --env=HOME=/workspace/home \ + --env=XDG_CACHE_HOME=/workspace/cache \ + --env=NOEMA_RESULT_PATH=/workspace/result.json \ + --env=NOEMA_REPOSITORY=ContextualWisdomLab/noema \ + --env=NOEMA_BASE_SHA=0000000000000000000000000000000000000000 \ + --env=NOEMA_HEAD_SHA="${SOURCE_SHA}" \ + --env=NOEMA_PATCH_SHA256="${SMOKE_PATCH_SHA256}" \ + --env=NOEMA_PATCH_PROFILE=node_patch_verify \ + --env=NOEMA_COMMAND_PROFILE=node_patch_verify_v1 \ + --env=NOEMA_VALIDATOR_IMAGE_DIGEST="${VALIDATOR_IMAGE_DIGEST}" \ + "$IMAGE_TAG" >/dev/null 2>"$diagnostic_path" + container_exit_code=$? + set -e + + if [ "$container_exit_code" -ne 0 ]; then + if ! DIAGNOSTIC_PATH="$diagnostic_path" node --input-type=module <<'NODE' + import { readPatchValidatorDiagnostic } from "./scripts/lib/patch-validator-smoke-diagnostic.mjs"; + + const diagnostic = readPatchValidatorDiagnostic(process.env.DIAGNOSTIC_PATH); + process.stderr.write( + `Untrusted patch-validator diagnostic: ${JSON.stringify(diagnostic)}\n`, + ); + NODE + then + echo "::warning::Patch-validator image failed without a readable bounded diagnostic." + fi + exit "$container_exit_code" + fi + + SOURCE_SHA="$SOURCE_SHA" \ + VALIDATOR_IMAGE_DIGEST="$VALIDATOR_IMAGE_DIGEST" \ + SMOKE_PATCH_SHA256="$SMOKE_PATCH_SHA256" \ + SMOKE_RESULT_PATH="$SMOKE_RESULT_PATH" \ + node --input-type=module <<'NODE' + import { writeFileSync } from "node:fs"; + + const smokeResult = { + status: "passed", + repository_full_name: "ContextualWisdomLab/noema", + base_sha: "0".repeat(40), + head_sha: process.env.SOURCE_SHA, + patch_sha256: process.env.SMOKE_PATCH_SHA256, + profile: "node_patch_verify", + command_profile: "node_patch_verify_v1", + validator_image_digest: process.env.VALIDATOR_IMAGE_DIGEST, + exit_code: 0, + duration_ms: 0, + stdout_excerpt: "", + stderr_excerpt: "", + reason_codes: [], + }; + writeFileSync( + process.env.SMOKE_RESULT_PATH, + `${JSON.stringify(smokeResult, null, 2)}\n`, + { mode: 0o600, flag: "wx" }, + ); + NODE - name: Generate CycloneDX SBOM and vulnerability receipt shell: bash run: | set -euo pipefail evidence_dir="$RUNNER_TEMP/patch-validator-evidence" - "$SCANNER_BIN_DIR/syft" "$IMAGE_TAG" -o cyclonedx-json >"$evidence_dir/sbom.cdx.json" - "$SCANNER_BIN_DIR/grype" "$IMAGE_TAG" -o json >"$evidence_dir/grype-image.json" - trivy image --format json --severity MEDIUM,HIGH,CRITICAL --ignore-unfixed=false "$IMAGE_TAG" >"$evidence_dir/trivy-image.json" + trivy image \ + --format cyclonedx \ + --output "$evidence_dir/image-sbom.cdx.json" \ + --no-progress \ + "$IMAGE_TAG" + trivy image \ + --format json \ + --output "$evidence_dir/image-vulnerability-scan.json" \ + --exit-code 1 \ + --severity MEDIUM,HIGH,CRITICAL \ + --scanners vuln \ + --no-progress \ + "$IMAGE_TAG" - name: Generate static-runtime binary inventory and vulnerability receipt shell: bash run: | set -euo pipefail evidence_dir="$RUNNER_TEMP/patch-validator-evidence" - node_binary="$RUNNER_TEMP/patch-validator-node" - "$SCANNER_BIN_DIR/syft" "file:$node_binary" -o cyclonedx-json >"$evidence_dir/node-runtime-sbom.cdx.json" - "$SCANNER_BIN_DIR/grype" "sbom:$evidence_dir/node-runtime-sbom.cdx.json" -o json >"$evidence_dir/node-runtime-grype.json" + binary_scan="$evidence_dir/image-binary-vulnerability-scan.json" + binary_scan_stderr="$evidence_dir/image-binary-vulnerability-scan.stderr.log" + binary_scan_stderr_raw="$RUNNER_TEMP/image-binary-vulnerability-scan.stderr.raw" + grype_config="$RUNNER_TEMP/noema-grype.yaml" + umask 077 + printf '{}\n' >"$grype_config" + "$SCANNER_BIN_DIR/grype" --config "$grype_config" db update + export GRYPE_DB_AUTO_UPDATE=false + "$SCANNER_BIN_DIR/syft" scan "docker:$IMAGE_TAG" \ + --output "syft-json=$evidence_dir/image-binary-sbom.syft.json" + set +e + "$SCANNER_BIN_DIR/grype" --config "$grype_config" \ + "sbom:$evidence_dir/image-binary-sbom.syft.json" \ + --fail-on medium \ + --output json \ + >"$binary_scan" 2>"$binary_scan_stderr_raw" + binary_scan_exit="$?" + set -e + head -c 4096 "$binary_scan_stderr_raw" >"$binary_scan_stderr" + rm -f "$binary_scan_stderr_raw" + binary_scan_bytes="$(wc -c <"$binary_scan")" + if [ "$binary_scan_bytes" -eq 0 ]; then + printf '::error::Grype static-runtime scan failed before producing a JSON receipt (exit %s).\n' \ + "$binary_scan_exit" >&2 + head -c 4096 "$binary_scan_stderr" >&2 + printf '\n' >&2 + if [ "$binary_scan_exit" -eq 0 ]; then + binary_scan_exit=1 + fi + fi + test "$binary_scan_bytes" -le 8388608 + if [ "$binary_scan_exit" -ne 0 ]; then + if [ "$binary_scan_bytes" -gt 0 ]; then + printf '::error::Grype static-runtime vulnerability policy rejected the exact binary SBOM (exit %s).\n' \ + "$binary_scan_exit" >&2 + head -c 4096 "$binary_scan_stderr" >&2 + printf '\n' >&2 + fi + exit "$binary_scan_exit" + fi - name: Generate embedded static-runtime dependency inventory and vulnerability receipt shell: bash run: | set -euo pipefail evidence_dir="$RUNNER_TEMP/patch-validator-evidence" - node_binary="$RUNNER_TEMP/patch-validator-node" - node --input-type=module scripts/patch-validator-static-runtime-evidence.mjs \ - --binary "$node_binary" \ - --output "$evidence_dir/node-runtime-components.json" - "$SCANNER_BIN_DIR/grype" "sbom:$evidence_dir/node-runtime-components.json" -o json \ - >"$evidence_dir/node-runtime-components-grype.json" + versions_path="$evidence_dir/embedded-runtime-process-versions.json" + inventory_path="$evidence_dir/embedded-runtime-inventory.json" + scan_plan_path="$evidence_dir/embedded-runtime-scan-plan.json" + scan_dir="$evidence_dir/embedded-runtime-component-scans" + scan_path="$evidence_dir/embedded-runtime-vulnerability-scan.json" + grype_config="$RUNNER_TEMP/noema-grype.yaml" + mkdir -p "$scan_dir" + umask 077 + printf '{}\n' >"$grype_config" + + docker run --rm --pull=never --entrypoint=/nodejs/bin/node "$IMAGE_TAG" \ + --input-type=module \ + --eval='process.stdout.write(JSON.stringify(process.versions))' \ + >"$versions_path" + versions_bytes="$(wc -c <"$versions_path")" + test "$versions_bytes" -gt 0 + test "$versions_bytes" -le 32768 + + PROCESS_VERSIONS_PATH="$versions_path" \ + INVENTORY_PATH="$inventory_path" \ + SCAN_PLAN_PATH="$scan_plan_path" \ + VALIDATOR_IMAGE_DIGEST="$VALIDATOR_IMAGE_DIGEST" \ + node --input-type=module <<'NODE' + import { readFileSync, writeFileSync } from "node:fs"; + import { generateEmbeddedRuntimeInventory } from "./scripts/lib/patch-validator-embedded-runtime-inventory.mjs"; + + const versions = JSON.parse(readFileSync(process.env.PROCESS_VERSIONS_PATH, "utf8")); + const { inventory, scanPlan } = generateEmbeddedRuntimeInventory( + versions, + process.env.VALIDATOR_IMAGE_DIGEST, + ); + writeFileSync(process.env.INVENTORY_PATH, `${JSON.stringify(inventory, null, 2)}\n`, { + mode: 0o600, + flag: "wx", + }); + writeFileSync(process.env.SCAN_PLAN_PATH, `${JSON.stringify(scanPlan, null, 2)}\n`, { + mode: 0o600, + flag: "wx", + }); + NODE + + export GRYPE_DB_AUTO_UPDATE=false + + mapfile -t scan_rows < <( + SCAN_PLAN_PATH="$scan_plan_path" node --input-type=module <<'NODE' + import { readFileSync } from "node:fs"; + + const scanPlan = JSON.parse(readFileSync(process.env.SCAN_PLAN_PATH, "utf8")); + if (!Array.isArray(scanPlan) || scanPlan.length === 0 || scanPlan.length > 128) { + throw new Error("embedded runtime scan plan must be a bounded non-empty array"); + } + for (const entry of scanPlan) { + if ( + Object.prototype.toString.call(entry) !== "[object Object]" || + typeof entry.key !== "string" || + typeof entry.identity !== "string" || + entry.identity.length === 0 || + entry.identity.length > 512 || + /[\t\r\n]/.test(entry.identity) + ) { + throw new Error("embedded runtime scan plan entry is invalid"); + } + process.stdout.write(`${entry.key}\t${entry.identity}\n`); + } + NODE + ) + test "${#scan_rows[@]}" -gt 0 + test "${#scan_rows[@]}" -le 128 + + for scan_row in "${scan_rows[@]}"; do + IFS=$'\t' read -r key identity <<<"$scan_row" + test -n "$key" + test -n "$identity" + raw_path="$scan_dir/${key}.json" + "$SCANNER_BIN_DIR/grype" --config "$grype_config" "$identity" \ + --output json \ + >"$raw_path" + raw_scan_bytes="$(wc -c <"$raw_path")" + test "$raw_scan_bytes" -gt 0 + test "$raw_scan_bytes" -le 8388608 + done + + INVENTORY_PATH="$inventory_path" \ + SCAN_DIR="$scan_dir" \ + EMBEDDED_SCAN_PATH="$scan_path" \ + VALIDATOR_IMAGE_DIGEST="$VALIDATOR_IMAGE_DIGEST" \ + node --input-type=module <<'NODE' + import { readFileSync, writeFileSync } from "node:fs"; + import { join } from "node:path"; + + const inventory = JSON.parse(readFileSync(process.env.INVENTORY_PATH, "utf8")); + const bundled = inventory.components.filter( + (component) => component.classification === "bundled_dependency", + ); + const components = bundled.map((component) => { + const identity = component.purl ?? component.cpe; + const rawPath = join(process.env.SCAN_DIR, `${component.key}.json`); + const raw = JSON.parse(readFileSync(rawPath, "utf8")); + if (Object.prototype.toString.call(raw) !== "[object Object]") { + throw new Error(`Grype embedded-runtime result ${component.key} must be a JSON record`); + } + return { + key: component.key, + identity, + scanner_output: raw, + }; + }); + const receipt = { + schema_version: "noema.patch-validator-embedded-runtime-vulnerability-scan.v1", + validator_image_digest: process.env.VALIDATOR_IMAGE_DIGEST, + scanner: "grype@0.116.1", + components, + ignoredMatches: [], + }; + writeFileSync(process.env.EMBEDDED_SCAN_PATH, `${JSON.stringify(receipt, null, 2)}\n`, { + mode: 0o600, + flag: "wx", + }); + NODE - name: Verify exact-source, exact-image, smoke, SBOM, and vulnerability receipts shell: bash run: | set -euo pipefail - node --input-type=module scripts/verify-patch-validator-image-evidence.mjs \ - --source-sha "$SOURCE_SHA" \ - --image-digest "$VALIDATOR_IMAGE_DIGEST" \ - --image-metadata "$RUNNER_TEMP/patch-validator-evidence/image-metadata.json" \ - --smoke-result "$SMOKE_RESULT_PATH" \ - --sbom "$RUNNER_TEMP/patch-validator-evidence/sbom.cdx.json" \ - --grype-image "$RUNNER_TEMP/patch-validator-evidence/grype-image.json" \ - --trivy-image "$RUNNER_TEMP/patch-validator-evidence/trivy-image.json" \ - --node-runtime-sbom "$RUNNER_TEMP/patch-validator-evidence/node-runtime-sbom.cdx.json" \ - --node-runtime-grype "$RUNNER_TEMP/patch-validator-evidence/node-runtime-grype.json" \ - --node-runtime-components "$RUNNER_TEMP/patch-validator-evidence/node-runtime-components.json" \ - --node-runtime-components-grype "$RUNNER_TEMP/patch-validator-evidence/node-runtime-components-grype.json" + evidence_dir="$RUNNER_TEMP/patch-validator-evidence" + node scripts/verify-patch-validator-image.mjs \ + --metadata "$evidence_dir/image-metadata.json" \ + --smoke "$evidence_dir/smoke-result.json" \ + --sbom "$evidence_dir/image-sbom.cdx.json" \ + --vulnerability-scan "$evidence_dir/image-vulnerability-scan.json" \ + --binary-sbom "$evidence_dir/image-binary-sbom.syft.json" \ + --binary-vulnerability-scan "$evidence_dir/image-binary-vulnerability-scan.json" \ + --embedded-runtime-inventory "$evidence_dir/embedded-runtime-inventory.json" \ + --embedded-vulnerability-scan "$evidence_dir/embedded-runtime-vulnerability-scan.json" \ + --expected-image-digest "$VALIDATOR_IMAGE_DIGEST" \ + --expected-source-revision "$SOURCE_SHA" \ + >"$evidence_dir/image-verification.json" - name: Refuse stale pull-request head after verification shell: bash @@ -393,18 +623,37 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - checked_out="$(git rev-parse HEAD)" - test "$checked_out" = "$SOURCE_SHA" + test "$(git rev-parse HEAD)" = "$SOURCE_SHA" test -z "$(git status --porcelain=v2 --untracked-files=all --ignored=matching)" if [ -n "$PR_NUMBER" ]; then - live_head="$(gh api --method GET "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq ".head.sha")" + resolve_live_head_sha() { + local output="" + for attempt in 1 2 3; do + if output="$( + gh api --method GET "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq ".head.sha" \ + 2>&1 + )"; then + printf '%s\n' "$output" + return 0 + fi + if printf '%s\n' "$output" | grep -Eq '\(HTTP (502|503|504)\)$' && [ "$attempt" -lt 3 ]; then + sleep "$attempt" + continue + fi + printf '::error::Live pull-request head resolution failed after attempt %s.\n' "$attempt" >&2 + return 1 + done + return 1 + } + live_head="$(resolve_live_head_sha)" test "$live_head" = "$SOURCE_SHA" fi - name: Upload bounded verification evidence - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: patch-validator-image-${{ env.SOURCE_SHA }} + name: patch-validator-image-verification-${{ env.SOURCE_SHA }} path: ${{ runner.temp }}/patch-validator-evidence if-no-files-found: error - retention-days: 30 + retention-days: 90 From 68b0e4377ff59ba1d5b813eaea7890fc1c5712f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 21:16:40 -0700 Subject: [PATCH 559/564] fix(image): bound scanner asset transport --- .github/workflows/patch-validator-image.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/patch-validator-image.yml b/.github/workflows/patch-validator-image.yml index 6329a4c89..8a155b759 100644 --- a/.github/workflows/patch-validator-image.yml +++ b/.github/workflows/patch-validator-image.yml @@ -83,12 +83,15 @@ jobs: download_scanner_asset() { destination="$1" url="$2" - curl --proto '=https' --tlsv1.2 --location --fail --silent --show-error \ - --retry 3 \ - --retry-all-errors \ - --retry-delay 2 \ - --retry-max-time 60 \ - --output "$destination" "$url" + timeout --signal=TERM --kill-after=30s 5m \ + curl --proto '=https' --proto-redir '=https' --tlsv1.2 --location --fail --silent --show-error \ + --retry 3 \ + --retry-all-errors \ + --retry-delay 2 \ + --retry-max-time 90 \ + --connect-timeout 20 \ + --max-time 180 \ + --output "$destination" "$url" } install_scanner() { From d51ef8ac0cf87fda183c7680c2f2b017e496d10b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 21:30:31 -0700 Subject: [PATCH 560/564] test(image): align scanner retry bound with hardened workflow --- test/patch-validator-image-build-cache.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/patch-validator-image-build-cache.test.ts b/test/patch-validator-image-build-cache.test.ts index 6e8b9f677..fdd364cf4 100644 --- a/test/patch-validator-image-build-cache.test.ts +++ b/test/patch-validator-image-build-cache.test.ts @@ -34,6 +34,6 @@ describe("patch-validator image build cache", () => { expect(workflow).toContain("--retry 3"); expect(workflow).toContain("--retry-all-errors"); expect(workflow).toContain("--retry-delay 2"); - expect(workflow).toContain("--retry-max-time 60"); + expect(workflow).toContain("--retry-max-time 90"); }); }); From 7b8f109809affa51400e530cfc57b687ead65701 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 01:13:52 -0700 Subject: [PATCH 561/564] test(egress): reject headers on anonymous meta --- .../outbound-fetch-anonymous-authority.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/outbound-fetch-anonymous-authority.test.ts b/test/outbound-fetch-anonymous-authority.test.ts index 35bcd7cc3..9ab3f2636 100644 --- a/test/outbound-fetch-anonymous-authority.test.ts +++ b/test/outbound-fetch-anonymous-authority.test.ts @@ -26,6 +26,24 @@ describe("anonymous GitHub API egress authority", () => { expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); }); + it("rejects reviewed headers on the anonymous /meta diagnostic", async () => { + const metaUrl = "https://api.github.com/meta"; + const request = { + method: "GET", + headers: { accept: "application/vnd.github+json" }, + } satisfies RequestInit; + + expect(isTrustedCredentialEgressRequest(metaUrl, request)).toBe(false); + + const rawFetch = vi.fn(async () => new Response(null, { status: 204 })); + const wrapped = createFailClosedFetch(rawFetch); + const response = await wrapped(metaUrl, request); + + expect(rawFetch).not.toHaveBeenCalled(); + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe("blocked-request-policy"); + }); + it("rejects arbitrary anonymous GitHub REST destinations outside reviewed operations", async () => { const unreviewedUrl = "https://api.github.com/repos/ContextualWisdomLab/noema/issues"; From 226553dfbfe948e81ec3550ef7b96ef08786cf60 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 01:17:35 -0700 Subject: [PATCH 562/564] test(egress): prove body cancellation is immediate --- test/outbound-fetch-preaborted-signal.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/outbound-fetch-preaborted-signal.test.ts b/test/outbound-fetch-preaborted-signal.test.ts index 38fc18292..562be4a45 100644 --- a/test/outbound-fetch-preaborted-signal.test.ts +++ b/test/outbound-fetch-preaborted-signal.test.ts @@ -140,8 +140,8 @@ describe("credential-egress caller cancellation authority", () => { ); await pullStarted; caller.abort(reason); - releaseBody(); await expect(pending).rejects.toBe(reason); + releaseBody(); }); }); From 20aaafa478303db449b60047e67911e32e176bf9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 01:18:22 -0700 Subject: [PATCH 563/564] test(supply-chain): bind Node source URL to digest --- test/patch-validator-static-runtime.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/patch-validator-static-runtime.test.ts b/test/patch-validator-static-runtime.test.ts index 429031b20..89b560726 100644 --- a/test/patch-validator-static-runtime.test.ts +++ b/test/patch-validator-static-runtime.test.ts @@ -20,9 +20,13 @@ describe("patch-validator static scratch runtime", () => { expect(dockerfile).toContain("ARG NODE_VERSION=24.19.0"); expect(dockerfile).toContain(`ARG NODE_SOURCE_SHA256=${nodeSourceSha256}`); expect(dockerfile).toContain( - '"https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}.tar.xz"', + [ + " download_exact \\", + ' "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}.tar.xz" \\', + ' "$NODE_SOURCE_SHA256" \\', + " /tmp/node.tar.xz; \\", + ].join("\n"), ); - expect(dockerfile).toContain('"$NODE_SOURCE_SHA256"'); expect(dockerfile).toContain("sha256sum --check --strict"); expect(dockerfile).toContain("timeout --signal=TERM --kill-after=30s 5m"); expect(dockerfile).toContain("--connect-timeout 20"); @@ -80,4 +84,4 @@ describe("patch-validator static scratch runtime", () => { expect(workflow).toContain("--severity MEDIUM,HIGH,CRITICAL"); expect(workflow).toContain("--exit-code 1"); }); -}); \ No newline at end of file +}); From ef0d57ed25890479dc26c92c5ba0a49114d4c1f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 01:21:40 -0700 Subject: [PATCH 564/564] fix(egress): keep anonymous meta headerless --- src/outbound-fetch-policy.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index f3cf72fdc..4a18b1669 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -438,7 +438,8 @@ export function isTrustedCredentialEgressRequest( if (!authorization) { return url.href === TRUSTED_GITHUB_API_META && method === "GET" - && !bodyPresent; + && !bodyPresent + && hasNoHeaders(headers); } const rawAuthorization = rawHeaderValueFromInit(init?.headers, "authorization"); if ( @@ -573,7 +574,7 @@ export function ensureGlobalOutboundFetchPolicy( /** * Restores an installed fetch host during tests while leaving production policy installation one-way for normal operation. * @param host Mutable fetch host whose test-only installation state should be removed. - * @returns Nothing; cleanup is best-effort and never masks the security behavior being tested. + * @returns Nothing; cleanup is best-effort and never masks the security behavior under examination. */ export function resetGlobalOutboundFetchPolicy( host: FetchHost = globalThis,