diff --git a/src/api/server.ts b/src/api/server.ts index 8b82a11f..f4b9ba47 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -257,7 +257,7 @@ async function gate( await deps.identity.refresh(); if (deps.identity.classify(actor.p).type !== "internal") actor = null; } - if (!isPublicRoute && requirePortalIdentity) { + if (!isPublicRoute && routeAuth !== "source" && requirePortalIdentity) { const webTurn = method === "POST" && pathname === "/v1/turns" && diff --git a/test/auth-gate.test.ts b/test/auth-gate.test.ts index 06def00d..91020bec 100644 --- a/test/auth-gate.test.ts +++ b/test/auth-gate.test.ts @@ -227,3 +227,23 @@ test("egress-audit ingest requires source auth: unsigned is 401, signed lands await srv.close(); } }); + +test("source-only run reads remain available under portal-identity enforcement", async () => { + const built = buildApp(testConfig({ dataDir: mkdtempSync(join(tmpdir(), "auth-run-read-")) })); + const server = createServer(built.app, { + signingSecret: SECRET, + runs: built.runs, + requireSignedPortalIdentity: true, + capabilitySecret: `${SECRET}-cap`, + portalIdentitySecret: `${SECRET}-portal`, + }); + server.listen(0); + const base = `http://localhost:${(server.address() as AddressInfo).port}`; + try { + const path = "/v1/runs/missing"; + const response = await fetch(`${base}${path}`, { headers: sign("GET", path, "") }); + assert.equal(response.status, 404); + } finally { + await new Promise((resolve) => server.close(() => resolve())); + } +});