Skip to content

Commit ade2845

Browse files
committed
feat(orb): add an AMS-facing secret type to the generalized broker (#7674)
Adds ORB_SECRET_TYPE_AMS_GITHUB_TOKEN to src/orb/broker.ts, ratified on #4941: hosted AMS reuses ORB's installation-based broker rather than a parallel identity system. Mechanically identical to ORB_SECRET_TYPE_GITHUB_TOKEN -- a GitHub App installation token's permissions come from the App and what the installer granted, not from anything the broker's caller specifies, so there is no real behavioral difference to build. brokerOrbToken's eligibility check now accepts either value, routing both through the exact same mint/cache/ install-eligibility flow; the distinct value exists purely so an enrollment row records which product's container it was issued for. Deliberately distinct from the self-host session-based GitHub auth packages/loopover-miner/lib/github-token-resolution.ts uses (a human's own OAuth token via /v1/auth/github/token, from a loopover-mcp login) -- that flow authenticates an interactive human tool as themselves; this one authorizes a headless hosted container as the installed App, the same reason ORB's own broker exists at all. The two are not duplicative: one models a human identity, the other a machine service identity. Scope is deliberately narrow, mirroring the #8064/#8066 split: this adds only the broker's capability to mint this type. It does not wire up a way for a real caller to request it at issuance time (POST /v1/internal/orb/enrollments and oauth.ts's self-enrollment landing page both still hardcode github_token) -- that's a follow-up once a real hosted-AMS consumer exists to call it. Tests mirror the existing github_token coverage (mint, cache, install- eligibility re-check) for the new type, plus a regression test confirming a genuinely unrecognized secret type is still rejected.
1 parent 321c192 commit ade2845

2 files changed

Lines changed: 69 additions & 5 deletions

File tree

src/orb/broker.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ export const ORB_SECRET_TYPE_GITHUB_TOKEN = "github_token";
3232
// brokerOrbToken's own secret_type branch below.
3333
export const ORB_SECRET_TYPE_TENANT_DB_CREDENTIAL = "tenant_db_credential";
3434

35+
// A SECOND mint-style type (#7674, ratified on #4941: hosted AMS reuses ORB's installation-based broker rather
36+
// than a parallel identity system), mechanically IDENTICAL to ORB_SECRET_TYPE_GITHUB_TOKEN -- a GitHub App
37+
// installation token's permissions come from the App + what the installer granted, not from anything the
38+
// broker's caller specifies, so there is no real behavioral difference to build here. The distinct value exists
39+
// purely so an enrollment row records WHICH product's container it was issued for (audit/bookkeeping), not
40+
// because AMS needs a different mint mechanism. Deliberately distinct from the self-host session-based GitHub
41+
// auth `packages/loopover-miner/lib/github-token-resolution.ts` uses (a human's own OAuth token via
42+
// `/v1/auth/github/token`) -- that flow exists for an interactive human tool acting as themselves; this one is
43+
// for a headless hosted container acting as the installed App, the same reason ORB's own broker exists at all.
44+
export const ORB_SECRET_TYPE_AMS_GITHUB_TOKEN = "ams_github_token";
45+
3546
export function isOrbBrokerEnabled(env: Env): boolean {
3647
return /^(1|true|yes|on)$/i.test(String(env.ORB_BROKER_ENABLED ?? "").trim());
3748
}
@@ -126,9 +137,10 @@ type OrbEnrollmentRow = {
126137
};
127138

128139
/** The container's token-exchange: a valid enrollment secret → either a short-lived GitHub installation token
129-
* (the original, mint-style flow) or a decrypted stored secret value (#8064's store-style flow), branching on
130-
* the enrollment row's own secret_type. installation_id/eligibility only apply to the GitHub-token flow — a
131-
* stored secret has no GitHub installation to re-check at all (see issueOrbStoredSecret's header comment). */
140+
* (the mint-style flow, shared identically by GITHUB_TOKEN and AMS_GITHUB_TOKEN, #7674) or a decrypted stored
141+
* secret value (#8064's store-style flow), branching on the enrollment row's own secret_type.
142+
* installation_id/eligibility only apply to the mint-style flow — a stored secret has no GitHub installation
143+
* to re-check at all (see issueOrbStoredSecret's header comment). */
132144
export async function brokerOrbToken(env: Env, secret: string, options: { forceRefresh?: boolean } = {}): Promise<BrokerResult> {
133145
// Warn when TOKEN_ENCRYPTION_SECRET is absent — without it, the broker cache is bypassed and every exchange hits
134146
// GitHub's token endpoint, dramatically increasing exposure to throttle-induced failures.
@@ -146,8 +158,12 @@ export async function brokerOrbToken(env: Env, secret: string, options: { forceR
146158
if (!row || row.state !== "enrolled" || row.revoked_at !== null) return { error: "invalid_enrollment" };
147159
if (row.secret_type === ORB_SECRET_TYPE_TENANT_DB_CREDENTIAL) return resolveStoredSecret(env, row);
148160
// Checked once the caller is already proven to hold a valid enrollment (same ordering rationale as the App-
149-
// credential check below, #2710) — anything else here belongs to a mint strategy that doesn't exist yet.
150-
if (row.secret_type !== ORB_SECRET_TYPE_GITHUB_TOKEN) return { error: "unsupported_secret_type" };
161+
// credential check below, #2710) — GITHUB_TOKEN and AMS_GITHUB_TOKEN both mint the SAME kind of GitHub App
162+
// installation token through the identical flow below (#7674): the distinct value is bookkeeping only, not a
163+
// different mint strategy. Anything else here belongs to a strategy that doesn't exist yet.
164+
if (row.secret_type !== ORB_SECRET_TYPE_GITHUB_TOKEN && row.secret_type !== ORB_SECRET_TYPE_AMS_GITHUB_TOKEN) {
165+
return { error: "unsupported_secret_type" };
166+
}
151167
const install = await env.DB
152168
.prepare("SELECT registered, suspended_at, removed_at FROM orb_github_installations WHERE installation_id = ?")
153169
.bind(row.installation_id)

test/integration/orb-broker.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
isOrbBrokerEnabled,
77
issueOrbEnrollment,
88
issueOrbStoredSecret,
9+
ORB_SECRET_TYPE_AMS_GITHUB_TOKEN,
910
ORB_SECRET_TYPE_GITHUB_TOKEN,
1011
ORB_SECRET_TYPE_TENANT_DB_CREDENTIAL,
1112
revokeOrbEnrollment,
@@ -79,6 +80,14 @@ describe("issueOrbEnrollment", () => {
7980
const row = await db(e).prepare("SELECT secret_type FROM orb_enrollments WHERE installation_id=202").first<{ secret_type: string }>();
8081
expect(row?.secret_type).toBe("ai_provider_key");
8182
});
83+
84+
it("#7674: records ORB_SECRET_TYPE_AMS_GITHUB_TOKEN when issued for a hosted AMS container", async () => {
85+
const e = await brokerEnv();
86+
await seedInstall(e, 203, { registered: 1 });
87+
await issueOrbEnrollment(e, 203, undefined, ORB_SECRET_TYPE_AMS_GITHUB_TOKEN);
88+
const row = await db(e).prepare("SELECT secret_type, installation_id FROM orb_enrollments WHERE installation_id=203").first<{ secret_type: string; installation_id: number }>();
89+
expect(row).toMatchObject({ secret_type: ORB_SECRET_TYPE_AMS_GITHUB_TOKEN, installation_id: 203 });
90+
});
8291
});
8392

8493
describe("issueOrbStoredSecret", () => {
@@ -162,6 +171,45 @@ describe("brokerOrbToken", () => {
162171
expect(await brokerOrbToken(e, secret)).toEqual({ error: "unsupported_secret_type" });
163172
});
164173

174+
it("#7674: mints the SAME kind of GitHub installation token for an ams_github_token enrollment as for github_token", async () => {
175+
const e = await brokerEnv();
176+
await seedInstall(e, 320, { registered: 1 });
177+
const { secret } = (await issueOrbEnrollment(e, 320, undefined, ORB_SECRET_TYPE_AMS_GITHUB_TOKEN)) as { secret: string };
178+
tokenFetch("ghs_ams_minted", "2026-06-25T08:00:00Z", { contents: "write" });
179+
180+
expect(await brokerOrbToken(e, secret)).toEqual({ token: "ghs_ams_minted", installationId: 320, expiresAt: "2026-06-25T08:00:00Z", permissions: { contents: "write" } });
181+
});
182+
183+
it("#7674: an ams_github_token enrollment shares the SAME cache as github_token — a second exchange doesn't re-mint", async () => {
184+
vi.useFakeTimers();
185+
vi.setSystemTime(new Date("2026-06-25T07:00:00Z"));
186+
const e = await brokerEnv({ TOKEN_ENCRYPTION_SECRET: "orb-ams-cache-test" });
187+
await seedInstall(e, 321, { registered: 1 });
188+
const { secret } = (await issueOrbEnrollment(e, 321, undefined, ORB_SECRET_TYPE_AMS_GITHUB_TOKEN)) as { secret: string };
189+
const fetchCalls = countingTokenFetch("2026-06-25T08:00:00Z");
190+
191+
expect(await brokerOrbToken(e, secret)).toMatchObject({ token: "ghs_minted_1" });
192+
expect(await brokerOrbToken(e, secret)).toMatchObject({ token: "ghs_minted_1" });
193+
expect(fetchCalls()).toBe(1);
194+
});
195+
196+
it("#7674: an ams_github_token enrollment is re-checked for install eligibility just like github_token", async () => {
197+
const e = await brokerEnv();
198+
await seedInstall(e, 322, { registered: 1 });
199+
const { secret } = (await issueOrbEnrollment(e, 322, undefined, ORB_SECRET_TYPE_AMS_GITHUB_TOKEN)) as { secret: string };
200+
await db(e).prepare("UPDATE orb_github_installations SET suspended_at=CURRENT_TIMESTAMP WHERE installation_id=322").run();
201+
202+
expect(await brokerOrbToken(e, secret)).toEqual({ error: "installation_not_eligible" });
203+
});
204+
205+
it("#7674: a genuinely unrecognized secret type is still rejected (the widened check isn't a blanket allow)", async () => {
206+
const e = await brokerEnvMissingAppCreds("both");
207+
await seedInstall(e, 323, { registered: 1 });
208+
const { secret } = (await issueOrbEnrollment(e, 323, undefined, "some_future_type_not_yet_built")) as { secret: string };
209+
210+
expect(await brokerOrbToken(e, secret)).toEqual({ error: "unsupported_secret_type" });
211+
});
212+
165213
it("caches a freshly minted token and serves repeated exchanges without reminting", async () => {
166214
vi.useFakeTimers();
167215
vi.setSystemTime(new Date("2026-06-25T07:00:00Z"));

0 commit comments

Comments
 (0)