Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/orb/broker-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
12 changes: 11 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -374,6 +374,16 @@ async function main(): Promise<void> {
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<void> => {
Expand Down
29 changes: 28 additions & 1 deletion test/unit/orb-broker-client.test.ts
Original file line number Diff line number Diff line change
@@ -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 }[] } {
Expand Down Expand Up @@ -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<string, string>).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");
});
});
Loading