From 7dd410a45efbd0bfb0a28ab3c72865a69f0b834e Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:00:34 -0700 Subject: [PATCH] fix(selfhost): require orb webhook secret for export --- src/selfhost/orb-collector.ts | 6 +++++- test/unit/selfhost-orb-collector.test.ts | 23 ++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/selfhost/orb-collector.ts b/src/selfhost/orb-collector.ts index 83fec8bb0f..8040abfa52 100644 --- a/src/selfhost/orb-collector.ts +++ b/src/selfhost/orb-collector.ts @@ -93,7 +93,11 @@ export async function exportOrbBatch( if ((process.env.ORB_AIR_GAP ?? "").toLowerCase() === "true") return 0; const collectorUrl = process.env.ORB_COLLECTOR_URL ?? "https://orb.gittensory.app/v1/ingest"; - const secret = process.env.ORB_WEBHOOK_SECRET ?? ""; + const secret = process.env.ORB_WEBHOOK_SECRET; + if (!secret) { + incr("gittensory_orb_export_errors_total"); + return 0; + } const anonymize = (process.env.ORB_ANONYMIZE ?? "true").toLowerCase() !== "false"; const { results } = await db diff --git a/test/unit/selfhost-orb-collector.test.ts b/test/unit/selfhost-orb-collector.test.ts index 20e0e0dcfa..5ea5d296e9 100644 --- a/test/unit/selfhost-orb-collector.test.ts +++ b/test/unit/selfhost-orb-collector.test.ts @@ -123,7 +123,7 @@ describe("exportOrbBatch()", () => { }); afterEach(() => { delete process.env.ORB_ENABLED; - process.env.ORB_WEBHOOK_SECRET = undefined as unknown as string; + delete process.env.ORB_WEBHOOK_SECRET; delete process.env.ORB_ANONYMIZE; delete process.env.ORB_AIR_GAP; delete process.env.ORB_COLLECTOR_URL; @@ -228,18 +228,19 @@ describe("exportOrbBatch()", () => { expect(sigHeader).toMatch(/^sha256=[a-f0-9]{64}$/); }); - it("uses empty-string HMAC key when ORB_WEBHOOK_SECRET is unset (covers ?? '' branch)", async () => { - delete (process.env as NodeJS.Dict)["ORB_WEBHOOK_SECRET"]; + it("fails closed when ORB_WEBHOOK_SECRET is unset", async () => { + delete process.env.ORB_WEBHOOK_SECRET; const db = makeDb(); await recordOrbEvent(db, { repo: "o/r", pr_number: 1, head_sha: "sha", outcome: "merged" }); - let sigHeader: string | undefined; - const exported = await exportOrbBatch(db, 200, async (_u, init) => { - sigHeader = (init?.headers as Record)?.["x-orb-signature"]; - return new Response(null, { status: 200 }); - }); - expect(exported).toBe(1); - // Signature should still be formed (with empty-string key) - expect(sigHeader).toMatch(/^sha256=[a-f0-9]{64}$/); + const fakeFetch = vi.fn(async () => new Response(null, { status: 200 })); + + const exported = await exportOrbBatch(db, 200, fakeFetch); + + expect(exported).toBe(0); + expect(fakeFetch).not.toHaveBeenCalled(); + expect(await renderMetrics()).toContain("gittensory_orb_export_errors_total"); + const rows = (await allRows(db)) as Array>; + expect(rows[0]?.exported_at).toBeNull(); }); it("defaults ORB_ANONYMIZE to true when unset (covers ?? 'true' branch)", async () => {