From 8cc920fe1d601b5edda61946106fa1c0cfc3ee90 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Thu, 25 Jun 2026 03:33:10 -0700 Subject: [PATCH] feat(orb): auto-register the brokered self-host's relay URL on boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the brokered self-host loop (#1255): the container now self-registers its public relay URL with the central Orb on startup, so the Orb forwards this install's events to it — no manual curl. The container computes its relay URL from PUBLIC_API_ORIGIN + /v1/orb/relay and POSTs it to the broker with its enrollment secret. registerOrbRelayTarget (src/orb/broker-client.ts) is BEST-EFFORT + fire-and-forget: skipped unless broker mode + PUBLIC_API_ORIGIN are set, and any failure (Orb down, install not registered yet, non-public origin rejected by the Orb's SSRF check) just means no relay until the next boot — it never throws or blocks startup. Wired into the selfhost boot alongside the orb-export hook (server.ts, the codecov-ignored process entry). End-to-end now: install Orb App → self-enroll (admin-verified, #1348) → broker tokens (#1341) → boot auto-registers relay (this) → Orb forwards events (#1352) → relay receiver verifies + enqueues (#1354) → review + act. Advances #1255. --- src/orb/broker-client.ts | 25 +++++++++++++++++++++++++ src/server.ts | 12 +++++++++++- test/unit/orb-broker-client.test.ts | 29 ++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/orb/broker-client.ts b/src/orb/broker-client.ts index eaca6aa6d5..c00f7eaf35 100644 --- a/src/orb/broker-client.ts +++ b/src/orb/broker-client.ts @@ -43,3 +43,28 @@ export async function fetchBrokeredInstallationToken( const expiresAtMs = payload.expiresAt ? Date.parse(payload.expiresAt) : Date.now() + 50 * 60_000; return { token: payload.token, installationId: payload.installationId ?? 0, expiresAtMs }; } + +/** Self-register this container's PUBLIC relay URL with the central Orb on boot, so the Orb forwards this install's + * events to us (the event half of brokered review). BEST-EFFORT: skipped unless broker mode + a public origin are + * configured, and any failure (Orb down, install not registered yet, non-public origin rejected) just means no + * relay until the next boot — it never blocks startup or throws. The relay URL is the container's public origin + + * /v1/orb/relay (the receiver); the Orb SSRF-validates it, so PUBLIC_API_ORIGIN must be a real public https host. */ +export async function registerOrbRelayTarget( + env: { ORB_ENROLLMENT_SECRET?: string | undefined; ORB_BROKER_URL?: string | undefined; PUBLIC_API_ORIGIN?: string | undefined }, + fetchImpl: typeof fetch = fetch, +): Promise<"registered" | "skipped" | "failed"> { + if (!isOrbBrokerMode(env) || !env.PUBLIC_API_ORIGIN) return "skipped"; + const base = (env.ORB_BROKER_URL ?? DEFAULT_BROKER_URL).replace(/\/+$/, ""); + const relayUrl = `${env.PUBLIC_API_ORIGIN.replace(/\/+$/, "")}/v1/orb/relay`; + try { + const res = await fetchImpl(`${base}/v1/orb/relay/register`, { + method: "POST", + headers: { authorization: `Bearer ${env.ORB_ENROLLMENT_SECRET}`, "content-type": "application/json" }, // present — isOrbBrokerMode required it + body: JSON.stringify({ relayUrl }), + signal: AbortSignal.timeout(10_000), + }); + return res.ok ? "registered" : "failed"; + } catch { + return "failed"; + } +} diff --git a/src/server.ts b/src/server.ts index 579b1d6380..3b5e16f719 100644 --- a/src/server.ts +++ b/src/server.ts @@ -24,7 +24,7 @@ import { setupAuthCookieValue, timingSafeStrEqual, } from "./selfhost/setup-wizard"; -import { isOrbBrokerMode } from "./orb/broker-client"; +import { isOrbBrokerMode, registerOrbRelayTarget } from "./orb/broker-client"; import { exportOrbBatch } from "./selfhost/orb-collector"; import { createD1Adapter, nodeSqliteDriver } from "./selfhost/d1-adapter"; import { readiness } from "./selfhost/health"; @@ -374,6 +374,16 @@ async function main(): Promise { void runOrbExport(); // flush any pending events at startup setInterval(runOrbExport, 3_600_000); // then hourly + // Brokered self-host: register our public relay URL with the central Orb so it forwards this install's events + // here (best-effort, fire-and-forget — a no-op unless ORB_ENROLLMENT_SECRET + PUBLIC_API_ORIGIN are set). + void registerOrbRelayTarget({ + ORB_ENROLLMENT_SECRET: process.env.ORB_ENROLLMENT_SECRET, + ORB_BROKER_URL: process.env.ORB_BROKER_URL, + PUBLIC_API_ORIGIN: process.env.PUBLIC_API_ORIGIN, + }) + .then((r) => { if (r !== "skipped") console.log(JSON.stringify({ event: "selfhost_orb_relay_register", result: r })); }) + .catch(() => {}); + // Graceful shutdown: stop accepting HTTP, let the queue finish, close the backend. let shuttingDown = false; const shutdown = async (signal: string): Promise => { diff --git a/test/unit/orb-broker-client.test.ts b/test/unit/orb-broker-client.test.ts index 35505c975b..b1141be820 100644 --- a/test/unit/orb-broker-client.test.ts +++ b/test/unit/orb-broker-client.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { fetchBrokeredInstallationToken, isOrbBrokerMode } from "../../src/orb/broker-client"; +import { fetchBrokeredInstallationToken, isOrbBrokerMode, registerOrbRelayTarget } from "../../src/orb/broker-client"; /** A fetch stub that records the URL + init and returns a fixed response. */ function captureFetch(resp: Response): { fetchImpl: typeof fetch; calls: { url: string; init?: RequestInit | undefined }[] } { @@ -53,3 +53,30 @@ describe("fetchBrokeredInstallationToken", () => { await expect(fetchBrokeredInstallationToken({ ORB_ENROLLMENT_SECRET: "s" }, fetchImpl)).rejects.toThrow(/did not include a token/); }); }); + +describe("registerOrbRelayTarget", () => { + it("skips unless broker mode AND a public origin are configured", async () => { + expect(await registerOrbRelayTarget({})).toBe("skipped"); // not broker mode + expect(await registerOrbRelayTarget({ ORB_ENROLLMENT_SECRET: "s" })).toBe("skipped"); // no PUBLIC_API_ORIGIN + }); + + it("POSTs the relay URL (origin + /v1/orb/relay) to the broker with the enrollment secret; trailing slashes stripped", async () => { + const { fetchImpl, calls } = captureFetch(new Response("ok")); + expect(await registerOrbRelayTarget({ ORB_ENROLLMENT_SECRET: "orbsec_x", PUBLIC_API_ORIGIN: "https://me.example/", ORB_BROKER_URL: "https://broker.example/" }, fetchImpl)).toBe("registered"); + expect(calls[0]?.url).toBe("https://broker.example/v1/orb/relay/register"); // ORB_BROKER_URL trailing slash stripped + expect((calls[0]?.init?.headers as Record).authorization).toBe("Bearer orbsec_x"); + expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({ relayUrl: "https://me.example/v1/orb/relay" }); // PUBLIC_API_ORIGIN trailing slash stripped + }); + + it("uses the default broker base when ORB_BROKER_URL is unset", async () => { + const { fetchImpl, calls } = captureFetch(new Response("ok")); + await registerOrbRelayTarget({ ORB_ENROLLMENT_SECRET: "s", PUBLIC_API_ORIGIN: "https://me.example" }, fetchImpl); + expect(calls[0]?.url).toBe("https://gittensory-api.aethereal.dev/v1/orb/relay/register"); + }); + + it("returns failed on a non-ok response or a thrown fetch (never blocks boot)", async () => { + const cfg = { ORB_ENROLLMENT_SECRET: "s", PUBLIC_API_ORIGIN: "https://me.example" }; + expect(await registerOrbRelayTarget(cfg, (async () => new Response("no", { status: 403 })) as typeof fetch)).toBe("failed"); + expect(await registerOrbRelayTarget(cfg, (async () => { throw new Error("down"); }) as typeof fetch)).toBe("failed"); + }); +});