Skip to content

feat(orb): token-broker — operator-issued enrollment + token exchange - #1332

Merged
JSONbored merged 1 commit into
mainfrom
feat/orb-broker-core
Jun 25, 2026
Merged

feat(orb): token-broker — operator-issued enrollment + token exchange#1332
JSONbored merged 1 commit into
mainfrom
feat/orb-broker-core

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

The Orb token-broker: a maintainer's self-hosted container exchanges a one-time enrollment secret for short-lived GitHub installation tokens, so it acts on its own repos without ever holding the Orb App private key (gittensory holds it centrally and mints on demand). Everything flag-gated — 404 until ORB_BROKER_ENABLED, byte-identical deploy.

Trust model (das-github-mirror) — the operator is the authority

Rather than a maintainer-OAuth flow — whose admin-of-installation binding the red-team flagged as a privilege-escalation hole (visibility ≠ authority) — an enrollment is issued only for an install the operator already opted in (registered=1), via the INTERNAL_JOB_TOKEN-gated POST /v1/internal/orb/enrollments. The secret is shown once and stored only as a SHA-256 hash. (Maintainer-OAuth self-enrollment is a later layer on this secure core.)

POST /v1/orb/token (token-exempt; the enrollment secret is the auth)

  • installation_id is read from the enrollment row (bound at issue time) — never the request — so a stolen secret for install X can't mint a token for install Y;
  • the install must still be registered=1 and neither suspended nor removed (the gate is re-checked at mint, not trusted from issue time);
  • createOrbInstallationToken mints on demand (never cached) and surfaces GitHub's real expires_at.

Validation

  • npm run test:ci green
  • 100% branch coverage on broker.ts + the new routes (flag-off→404; issue 404/409/success+hashed; exchange success/unknown/revoked/ineligible; endpoint 401/403/400/404/409 + bad-JSON; server-bound id).

Advances #1255 (the central Orb data layer).

The Orb token-broker: a maintainer's self-hosted container exchanges a one-time enrollment secret for
short-lived GitHub installation tokens, so it acts on its own repos WITHOUT ever holding the Orb App private
key (gittensory holds it centrally, mints on demand). All flag-gated (404 until ORB_BROKER_ENABLED).

Trust model = das-github-mirror: the OPERATOR is the authority. Rather than a maintainer-OAuth flow (whose
admin-of-installation binding the red-team flagged as a privilege-escalation hole), an enrollment is issued only
for an install the operator already opted in (registered=1), via the INTERNAL_JOB_TOKEN-gated
POST /v1/internal/orb/enrollments. The secret is shown ONCE and stored only as a SHA-256 hash.

POST /v1/orb/token (token-exempt; the secret IS the auth): the container presents the secret →
- installation_id is read from the enrollment ROW (bound at issue time) — NEVER the request, so a stolen
  secret for install X can't mint a token for install Y;
- the install must STILL be registered=1 and neither suspended nor removed (the gate is re-checked at mint);
- createOrbInstallationToken mints on demand and is never cached; GitHub's real expiry is surfaced.

Maintainer-OAuth self-enrollment (for external maintainers at scale) is a later layer on this secure core.

Advances #1255 (the central Orb data layer).
@dosubot dosubot Bot added the size:M label Jun 25, 2026
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.24%. Comparing base (06a3a42) to head (e0e0f15).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1332   +/-   ##
=======================================
  Coverage   95.23%   95.24%           
=======================================
  Files         185      186    +1     
  Lines       20259    20292   +33     
  Branches     7286     7301   +15     
=======================================
+ Hits        19294    19327   +33     
  Misses        378      378           
  Partials      587      587           
Files with missing lines Coverage Δ
src/api/routes.ts 94.54% <100.00%> (+0.05%) ⬆️
src/auth/rate-limit.ts 98.79% <100.00%> (+0.01%) ⬆️
src/orb/broker.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JSONbored
JSONbored merged commit 9e88933 into main Jun 25, 2026
18 checks passed
@JSONbored
JSONbored deleted the feat/orb-broker-core branch June 25, 2026 06:22
JSONbored added a commit that referenced this pull request Jun 25, 2026
)

Completes the Orb token broker end-to-end (server: #1330/#1332). A brokered self-host holds no GitHub App private
key — it installs the central Orb App and sets the operator-issued ORB_ENROLLMENT_SECRET. createInstallationToken
now sources tokens from the central Orb (POST /v1/orb/token) when that secret is present, caching them in the
same in-isolate token cache as the App-key path (~1 mint/hour/install). Cloud never sets the secret, so the
branch is inert there → byte-identical.

- src/orb/broker-client.ts: isOrbBrokerMode (secret-presence gate) + fetchBrokeredInstallationToken (exchange
  secret → {token, installationId, expiresAt}; injectable fetch + 10s timeout; throws on non-OK / tokenless body).
- src/github/app.ts: the broker branch slots in at the single token chokepoint, right after the cache check.
- No App-key fallback by design (a brokered self-host has none) — a broker outage fails the request exactly like
  an App-key mint failure, and the queue's retry/dead-letter handling covers a transient blip. The secret is sent
  as a Bearer over the https default and never logged (errors carry only the status).

Advances #1255. (Maintainer-OAuth self-enrollment remains a follow-up; today enrollments are operator-issued.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant