diff --git a/src/orb/broker.ts b/src/orb/broker.ts index 8d07d1e3af..d2acae01ea 100644 --- a/src/orb/broker.ts +++ b/src/orb/broker.ts @@ -54,12 +54,6 @@ export type BrokerResult = { token: string; installationId: number; expiresAt: s * install. installation_id is read from the enrollment row, never the caller; the install must still be * registered=1 and neither suspended nor removed at mint time (the gate is re-checked, not trusted from issue). */ export async function brokerOrbToken(env: Env, secret: string): Promise { - // Validate Orb App credentials up front so a misconfiguration returns a structured error (503) rather than an - // unhandled exception that manifests as a generic 500 to the self-hosted engine. - if (!env.ORB_GITHUB_APP_ID || !env.ORB_GITHUB_APP_PRIVATE_KEY) { - console.error(JSON.stringify({ level: "error", event: "orb_broker_misconfigured", message: "ORB_GITHUB_APP_ID or ORB_GITHUB_APP_PRIVATE_KEY is not set; broker cannot mint tokens." })); - return { error: "broker_misconfigured" }; - } // Warn when TOKEN_ENCRYPTION_SECRET is absent — without it, the broker cache is bypassed and every exchange hits // GitHub's token endpoint, dramatically increasing exposure to throttle-induced failures. if (!env.TOKEN_ENCRYPTION_SECRET) { @@ -84,6 +78,14 @@ export async function brokerOrbToken(env: Env, secret: string): Promise = {}): Promise => createTestEnv({ ORB_BROKER_ENABLED: "true", ORB_GITHUB_APP_ID: "4139483", ORB_GITHUB_APP_PRIVATE_KEY: await pkcs8Pem(), INTERNAL_JOB_TOKEN: "dev-internal-token", ...over }); +// `exactOptionalPropertyTypes` forbids `KEY: undefined` overrides, so a truly-ABSENT App credential means simply +// never setting the key (not spreading `undefined` over it) — hence a dedicated builder rather than `brokerEnv({...})`. +const brokerEnvMissingAppCreds = async (missing: "id" | "key" | "both", over: Partial = {}): Promise => { + const base: Partial = { ORB_BROKER_ENABLED: "true", INTERNAL_JOB_TOKEN: "dev-internal-token", ...over }; + if (missing !== "id") base.ORB_GITHUB_APP_ID = "4139483"; + if (missing !== "key") base.ORB_GITHUB_APP_PRIVATE_KEY = await pkcs8Pem(); + return createTestEnv(base); +}; const tokenFetch = (token = "ghs_broker", expires = "2026-06-25T08:00:00Z") => vi.stubGlobal("fetch", async () => Response.json({ token, expires_at: expires })); const countingTokenFetch = (expires = "2026-06-25T08:00:00Z") => { let calls = 0; @@ -154,6 +162,48 @@ describe("brokerOrbToken", () => { await db(e).prepare("UPDATE orb_github_installations SET suspended_at=CURRENT_TIMESTAMP WHERE installation_id=302").run(); expect(await brokerOrbToken(e, secret)).toEqual({ error: "installation_not_eligible" }); }); + + it("#2710 regression: an invalid secret NEVER reveals broker misconfiguration — invalid_enrollment wins even with no App credentials at all", async () => { + // Before the fix, the ORB_GITHUB_APP_ID/PRIVATE_KEY check ran BEFORE the enrollment lookup, so ANY caller + // (no valid secret required) could learn the server's deployment-config state via the response code. The + // credential check must only ever run once the caller is already proven to hold a valid, eligible enrollment. + const e = await brokerEnvMissingAppCreds("both"); + expect(await brokerOrbToken(e, "orbsec_totally_bogus")).toEqual({ error: "invalid_enrollment" }); + }); + + it("returns broker_misconfigured only once the enrollment is valid+eligible and a real mint is required", async () => { + const missingAppId = await brokerEnvMissingAppCreds("id"); + await seedInstall(missingAppId, 310, { registered: 1 }); + const { secret: secretA } = (await issueOrbEnrollment(missingAppId, 310)) as { secret: string }; + expect(await brokerOrbToken(missingAppId, secretA)).toEqual({ error: "broker_misconfigured" }); + + const missingPrivateKey = await brokerEnvMissingAppCreds("key"); + await seedInstall(missingPrivateKey, 311, { registered: 1 }); + const { secret: secretB } = (await issueOrbEnrollment(missingPrivateKey, 311)) as { secret: string }; + expect(await brokerOrbToken(missingPrivateKey, secretB)).toEqual({ error: "broker_misconfigured" }); + }); + + it("still eligible-checks BEFORE reporting broker_misconfigured (an ineligible install never leaks config state either)", async () => { + const e = await brokerEnvMissingAppCreds("both"); + await seedInstall(e, 312, { registered: 1 }); + const { secret } = (await issueOrbEnrollment(e, 312)) as { secret: string }; + await db(e).prepare("UPDATE orb_github_installations SET removed_at=CURRENT_TIMESTAMP WHERE installation_id=312").run(); + expect(await brokerOrbToken(e, secret)).toEqual({ error: "installation_not_eligible" }); + }); + + it("serves a still-fresh cached token even with no Orb App credentials at all (mint is never reached)", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-06-25T07:00:00Z")); + const e = await brokerEnv({ TOKEN_ENCRYPTION_SECRET: "orb-cache-test-secret" }); + await seedInstall(e, 313, { registered: 1 }); + const { secret } = (await issueOrbEnrollment(e, 313)) as { secret: string }; + tokenFetch("ghs_minted", "2026-06-25T08:00:00Z"); + expect(await brokerOrbToken(e, secret)).toEqual({ token: "ghs_minted", installationId: 313, expiresAt: "2026-06-25T08:00:00Z" }); + + // Drop the App credentials AFTER the initial mint+cache — a still-fresh cache hit must not need them. + const eNoCreds = await brokerEnvMissingAppCreds("both", { TOKEN_ENCRYPTION_SECRET: "orb-cache-test-secret", DB: e.DB }); + expect(await brokerOrbToken(eNoCreds, secret)).toEqual({ token: "ghs_minted", installationId: 313, expiresAt: "2026-06-25T08:00:00Z" }); + }); }); describe("broker endpoints", () => {