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
23 changes: 23 additions & 0 deletions migrations/0068_orb_enrollments.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- Gittensory Orb central GitHub App (#1255) — the token-broker enrollment ledger. A maintainer authorizes the
-- Orb App (OAuth) and is bound, server-side, to a SPECIFIC installation they administer; their self-hosted
-- container is then issued a one-time enrollment secret (stored HASHED, never plaintext) which it exchanges for
-- short-lived installation tokens. installation_id is written here at the OAuth callback after an authority
-- check — the container can never name a different installation at token-exchange time. registered=1 on the
-- referenced install is still required to mint (the das-github-mirror trust gate). The whole broker is gated by
-- ORB_BROKER_ENABLED (default off) so this table is inert until enabled.
CREATE TABLE IF NOT EXISTS orb_enrollments (
enroll_id TEXT PRIMARY KEY NOT NULL, -- opaque id, returned to the container
installation_id INTEGER, -- bound at the OAuth callback; NULL while pending
maintainer_login TEXT,
maintainer_github_id INTEGER,
secret_hash TEXT, -- SHA-256 of the one-time secret; NULL until enrolled
state TEXT NOT NULL DEFAULT 'pending', -- pending | authorized | enrolled | revoked
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
authorized_at TEXT,
enrolled_at TEXT,
last_token_at TEXT,
revoked_at TEXT
);
-- A given secret hashes to exactly one enrollment (NULLs are distinct in SQLite, so many pending rows are fine).
CREATE UNIQUE INDEX IF NOT EXISTS orb_enrollments_secret_hash_idx ON orb_enrollments(secret_hash);
CREATE INDEX IF NOT EXISTS orb_enrollments_installation_idx ON orb_enrollments(installation_id);
3 changes: 3 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ declare global {
ORB_GITHUB_APP_PRIVATE_KEY?: string;
ORB_GITHUB_CLIENT_ID?: string;
ORB_GITHUB_CLIENT_SECRET?: string;
/** Master flag for the Orb token-broker (enrollment OAuth + /v1/orb/token). Default-off: every broker route
* early-404s until this is "true", so the deploy is byte-identical until an operator enables it. */
ORB_BROKER_ENABLED?: string;
GITHUB_APP_PRIVATE_KEY: string;
GITHUB_APP_ID: string;
GITHUB_APP_SLUG: string;
Expand Down
7 changes: 4 additions & 3 deletions src/orb/app-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export async function listOrbAppInstallations(env: Env): Promise<OrbAppInstallat

/** Mints a short-lived GitHub installation access token for one installation — the broker primitive the
* self-hosted container ultimately receives (after enrollment). Not cached: the broker mints on demand. */
export async function createOrbInstallationToken(env: Env, installationId: number): Promise<string> {
export async function createOrbInstallationToken(env: Env, installationId: number): Promise<{ token: string; expiresAt: string }> {
const jwt = await createOrbAppJwt(env);
const response = await timeoutFetch(`https://github.com/ghapi/app/installations/${installationId}/access_tokens`, {
method: "POST",
Expand All @@ -66,7 +66,8 @@ export async function createOrbInstallationToken(env: Env, installationId: numbe
const body = await response.text();
throw new Error(`Failed to create Orb installation token (${response.status}): ${body.slice(0, 200)}`);
}
const payload = (await response.json()) as { token?: string };
const payload = (await response.json()) as { token?: string; expires_at?: string };
if (!payload.token) throw new Error("Orb installation token response did not include a token.");
return payload.token;
// Surface GitHub's real expiry (~1h) so the broker never invents one; absent only on a malformed response.
return { token: payload.token, expiresAt: payload.expires_at ?? "" };
}
8 changes: 5 additions & 3 deletions test/unit/orb-app-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ describe("listOrbAppInstallations", () => {
describe("createOrbInstallationToken", () => {
const env = async (): Promise<Env> => orbEnv({ ORB_GITHUB_APP_PRIVATE_KEY: await pkcs8Pem() });

it("returns the minted token", async () => {
vi.stubGlobal("fetch", async () => Response.json({ token: "ghs_minted" }));
expect(await createOrbInstallationToken(await env(), 42)).toBe("ghs_minted");
it("returns the minted token + GitHub's real expiry (empty only when absent)", async () => {
vi.stubGlobal("fetch", async () => Response.json({ token: "ghs_minted", expires_at: "2026-06-25T07:00:00Z" }));
expect(await createOrbInstallationToken(await env(), 42)).toEqual({ token: "ghs_minted", expiresAt: "2026-06-25T07:00:00Z" });
vi.stubGlobal("fetch", async () => Response.json({ token: "ghs_noexp" }));
expect((await createOrbInstallationToken(await env(), 42)).expiresAt).toBe("");
});

it("throws on a non-ok response or a missing token", async () => {
Expand Down
Loading