From 5cb5e2a572355fd95313966947098e1f17ffc254 Mon Sep 17 00:00:00 2001 From: jaso0n0818 Date: Mon, 29 Jun 2026 23:06:53 +0000 Subject: [PATCH] fix(orb): order enrollment lookup so relay forwarding targets the current container forwardOrbEvent loaded the brokered self-host relay target with a .first() query on orb_enrollments that has no ORDER BY. issueOrbEnrollment inserts a new 'enrolled' row on every enrollment without revoking the prior rows for the same installation_id, so D1/SQLite could return an arbitrary enrolled row: after a maintainer re-enrolls and registers a new push relay URL, forwardOrbEvent might read an older row (stale relay URL, or none registered) and skip or POST to the wrong container. Order the lookup by relay_registered_at DESC, then enrolled_at DESC, so the most recently registered enrollment wins and re-enrollment always targets the current self-host container. Adds an integration test: double-enroll one installation, register a relay on the second secret only, and assert forwardOrbEvent forwards to the new URL instead of skipping on the stale row. --- src/orb/relay.ts | 8 +++++++- test/integration/orb-relay.test.ts | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/orb/relay.ts b/src/orb/relay.ts index e4c9236f22..b6626be94a 100644 --- a/src/orb/relay.ts +++ b/src/orb/relay.ts @@ -291,8 +291,14 @@ export async function forwardOrbEvent( fetchImpl: typeof fetch = fetch, ): Promise<"forwarded" | "queued" | "skipped" | "failed"> { if (!args.installationId || !RELAY_FORWARD_EVENTS.has(args.eventName)) return "skipped"; + // issueOrbEnrollment inserts a new `enrolled` row per enrollment without revoking the prior one, so one + // installation can have several enrolled rows. Without ORDER BY, D1/SQLite returns an arbitrary match, so a + // stale row (no relay URL, or an old push URL) can win over the current container. Pick the most recently + // registered enrollment — by relay_registered_at, then enrolled_at — so re-enrollment targets the new host (#1783). const row = await env.DB - .prepare("SELECT relay_mode, relay_url, relay_secret_enc, relay_secret_iv, relay_secret_salt FROM orb_enrollments WHERE installation_id = ? AND state = 'enrolled' AND revoked_at IS NULL") + .prepare( + "SELECT relay_mode, relay_url, relay_secret_enc, relay_secret_iv, relay_secret_salt FROM orb_enrollments WHERE installation_id = ? AND state = 'enrolled' AND revoked_at IS NULL ORDER BY relay_registered_at DESC, enrolled_at DESC", + ) .bind(args.installationId) .first<{ relay_mode: string; relay_url: string | null; relay_secret_enc: string | null; relay_secret_iv: string | null; relay_secret_salt: string | null }>(); if (!row) return "skipped"; // not a brokered self-host (or revoked) — nothing to relay to diff --git a/test/integration/orb-relay.test.ts b/test/integration/orb-relay.test.ts index 65b4fa6376..e911d00681 100644 --- a/test/integration/orb-relay.test.ts +++ b/test/integration/orb-relay.test.ts @@ -181,6 +181,19 @@ describe("forwardOrbEvent", () => { expect(calls[0]?.init?.body).toBe(body); }); + it("forwards to the most recently registered enrollment when an installation has multiple enrolled rows (#1783)", async () => { + const e = brokeredEnv(); + // Re-enrollment inserts a second `enrolled` row for the same installation (the install is already seeded, + // so only issueOrbEnrollment runs again); row A stays enrolled with no relay. + await enroll(e, 815); // row A — enrolled, never registers a relay + const secretB = ((await issueOrbEnrollment(e, 815)) as { secret: string }).secret; // row B — the current container + await registerOrbRelay(e, secretB, "https://new-host.example/v1/orb/relay"); // register the relay on row B only + const { fetchImpl, calls } = capture(new Response("ok")); + // Without ordering, the arbitrary first row could be A (no relay) → "skipped"; the fix must pick B. + expect(await forwardOrbEvent(e, { eventName: "pull_request", installationId: 815, deliveryId: "d-1783", rawBody: "{}" }, fetchImpl)).toBe("forwarded"); + expect(calls[0]?.url).toBe("https://new-host.example/v1/orb/relay"); + }); + it("returns FAILED (never throws) on a non-ok response or a thrown fetch — the Orb 202 always stands", async () => { const e = brokeredEnv(); const secret = await enroll(e, 802);