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
10 changes: 10 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ import { requestAprRepoTransfer } from "../orb/apr-repo-transfer";
import { handleOrbIngest, readOrbIngestBody } from "../orb/ingest";
import { handleAmsIngest } from "../ams/ingest";
import { handleOrbWebhook } from "../orb/webhook";
import { backfillOrbInstallations } from "../orb/installations";
import { handleOrbOAuthCallback } from "../orb/oauth";
import {
brokerOrbToken,
Expand Down Expand Up @@ -4512,6 +4513,15 @@ export function createApp() {
return c.json({ installationId, registered: registered === 1 });
});

// Operator-triggered reconciliation of the installation registry against GitHub's authoritative install list —
// recovers installs whose `installation` webhook fired before the receiver's secret was configured (so they were
// never recorded). Upserts each install WITHOUT touching `registered`, so a re-run never re-trusts an opted-out
// install and new rows land at registered=0 (the manual-onboarding gate). Bearer-gated by the `/v1/internal/*`
// middleware (INTERNAL_JOB_TOKEN). Returns { backfilled } — the count of installs GitHub reported.
app.post("/v1/internal/orb/installations/backfill", async (c) => {
return c.json(await backfillOrbInstallations(c.env));
});

// Operator-only: issue a one-time token-broker enrollment secret for a REGISTERED install, to hand to that
// maintainer's self-hosted container. The secret is returned ONCE (stored only hashed). Bearer-gated by the
// /v1/internal/* middleware (INTERNAL_JOB_TOKEN); flag-gated (404 until ORB_BROKER_ENABLED).
Expand Down
35 changes: 34 additions & 1 deletion test/integration/orb-onboarding.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createApp } from "../../src/api/routes";
import { createTestEnv, type TestD1Database } from "../helpers/d1";

async function pkcs8Pem(): Promise<string> {
const key = (await crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, true, ["sign", "verify"])) as CryptoKeyPair;
const b64 = Buffer.from((await crypto.subtle.exportKey("pkcs8", key.privateKey)) as ArrayBuffer).toString("base64").replace(/(.{64})/g, "$1\n");
return ["-----BEGIN", "PRIVATE KEY-----"].join(" ") + `\n${b64}\n` + ["-----END", "PRIVATE KEY-----"].join(" ");
}

describe("Central Orb installation registry routes (/v1/internal/orb/installations)", () => {
const app = createApp();
const auth = { authorization: "Bearer dev-internal-token" };
Expand All @@ -13,6 +19,8 @@ describe("Central Orb installation registry routes (/v1/internal/orb/installatio
const register = (env: Env, body: unknown) =>
app.request("/v1/internal/orb/installations/register", { method: "POST", headers: auth, body: typeof body === "string" ? body : JSON.stringify(body) }, env);

afterEach(() => vi.unstubAllGlobals());

it("lists recorded installations (registered surfaced as a boolean)", async () => {
const env = createTestEnv();
await seed(env, 100);
Expand Down Expand Up @@ -48,4 +56,29 @@ describe("Central Orb installation registry routes (/v1/internal/orb/installatio
const res = await app.request("/v1/internal/orb/installations", { headers: auth }, env);
expect(((await res.json()) as { installations: unknown[] }).installations).toEqual([]);
});

it("backfills the registry from GitHub, recovering a webhook-missed installation", async () => {
const env = createTestEnv({ ORB_GITHUB_APP_ID: "4139483", ORB_GITHUB_APP_PRIVATE_KEY: await pkcs8Pem() });
await seed(env, 200, 1); // already recorded + opted in
vi.stubGlobal("fetch", async () =>
Response.json([
{ id: 200, account: { login: "acme", type: "Organization", id: 20 }, repository_selection: "all" },
{ id: 201, account: { login: "bob", type: "User", id: 21 }, repository_selection: "selected" }, // webhook fired pre-secret → never recorded
]),
);
const res = await app.request("/v1/internal/orb/installations/backfill", { method: "POST", headers: auth }, env);
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ backfilled: 2 });
const rows = await (env.DB as unknown as TestD1Database)
.prepare("SELECT installation_id, account_login, registered FROM orb_github_installations ORDER BY installation_id")
.all<{ installation_id: number; account_login: string; registered: number }>();
expect(rows.results).toEqual([
{ installation_id: 200, account_login: "acme", registered: 1 }, // stayed trusted (backfill never re-trusts/untrusts)
{ installation_id: 201, account_login: "bob", registered: 0 }, // recovered at the onboarding gate
]);
});

it("401 without the internal token on the backfill route", async () => {
expect((await app.request("/v1/internal/orb/installations/backfill", { method: "POST" }, createTestEnv())).status).toBe(401);
});
});