Problem
Two identity/permission defects in the GitHub client layer.
1. In broker mode the brokered token is cached under the CALLER's installationId
src/github/app.ts:241-260:
const brokered = await fetchBrokeredInstallationToken(env, fetch, { forceRefresh });
await writeCachedToken(installationId, { token: brokered.token, expiresAtMs: brokered.expiresAtMs });
if (brokered.installationId === installationId && Object.keys(brokered.permissions).length > 0) { … }
The permissions write is guarded on brokered.installationId === installationId — the author
explicitly anticipated divergence — but the cache write and the return value are not.
fetchBrokeredInstallationToken also defaults a missing installationId to 0
(src/orb/broker-client.ts:87), so a broker response that omits the field always diverges.
Trigger: a stale repositories.installation_id row (the maintainer uninstalled and reinstalled the App —
GitHub issues a new installation id, and installed-repos-sync never rewrites the old rows) drives
createInstallationToken(env, oldId). The brokered token for the current install is cached under
oldId and returned. In a hosted/multi-tenant broker, the same code caches a token minted for tenant B
under tenant A's key.
Today (single-install self-host) this fails benignly; it is a latent cross-tenant token-serving bug that
the surrounding code already treats as possible.
Fix: cache under brokered.installationId, and throw when it is non-zero and differs from the
requested id rather than silently returning another install's token.
2. The "maintain" permission tier is unreachable
getRepositoryCollaboratorPermission returns payload.permission from
GET /repos/{o}/{r}/collaborators/{u}/permission, whose permission field is only ever
admin | write | read | none — Maintain collapses to write, Triage to read. The granular value lives
in role_name, which this code never requests or reads.
So permission === "maintain" is dead in both consumers:
isPerTenantAdmin (src/auth/security.ts:198): return permission === "admin" || permission === "maintain";
→ a repo Maintain-role user is denied per-repo admin trust in hosted mode.
resolveRealRepoPermissionAssociation (src/queue/processors.ts:13512):
if (permission === "admin" || permission === "maintain") return "MEMBER"; → a Maintain-role user is
downgraded to COLLABORATOR, which denies them generate-tests (the one ["maintainer"]-only command).
The union type at src/github/app.ts:431-438 listing "maintain" | "triage" shows the author assumed
role_name semantics.
This fails closed (no privilege escalation), but a documented trust tier silently never activates —
worth fixing before anyone relies on it in hosted per-repo-admin mode.
Fix: request and match on role_name, or delete the dead "maintain"/"triage" arms from the union
and both comparisons so the code stops implying a tier it cannot see.
Test Coverage Requirements
99%+ patch coverage, branch-counted. For (1): both arms of the id-match check including the 0 default.
For (2): a Maintain-role fixture resolving to the intended tier.
Links & Resources
src/github/app.ts ~241-260, ~431-438; src/orb/broker-client.ts ~87; src/auth/security.ts ~198;
src/queue/processors.ts ~13512
maintainer-only — credential and permission correctness.
Problem
Two identity/permission defects in the GitHub client layer.
1. In broker mode the brokered token is cached under the CALLER's installationId
src/github/app.ts:241-260:The permissions write is guarded on
brokered.installationId === installationId— the authorexplicitly anticipated divergence — but the cache write and the return value are not.
fetchBrokeredInstallationTokenalso defaults a missinginstallationIdto0(
src/orb/broker-client.ts:87), so a broker response that omits the field always diverges.Trigger: a stale
repositories.installation_idrow (the maintainer uninstalled and reinstalled the App —GitHub issues a new installation id, and
installed-repos-syncnever rewrites the old rows) drivescreateInstallationToken(env, oldId). The brokered token for the current install is cached underoldIdand returned. In a hosted/multi-tenant broker, the same code caches a token minted for tenant Bunder tenant A's key.
Today (single-install self-host) this fails benignly; it is a latent cross-tenant token-serving bug that
the surrounding code already treats as possible.
Fix: cache under
brokered.installationId, and throw when it is non-zero and differs from therequested id rather than silently returning another install's token.
2. The "maintain" permission tier is unreachable
getRepositoryCollaboratorPermissionreturnspayload.permissionfromGET /repos/{o}/{r}/collaborators/{u}/permission, whosepermissionfield is only everadmin | write | read | none— Maintain collapses towrite, Triage toread. The granular value livesin
role_name, which this code never requests or reads.So
permission === "maintain"is dead in both consumers:isPerTenantAdmin(src/auth/security.ts:198):return permission === "admin" || permission === "maintain";→ a repo Maintain-role user is denied per-repo admin trust in hosted mode.
resolveRealRepoPermissionAssociation(src/queue/processors.ts:13512):if (permission === "admin" || permission === "maintain") return "MEMBER";→ a Maintain-role user isdowngraded to
COLLABORATOR, which denies themgenerate-tests(the one["maintainer"]-only command).The union type at
src/github/app.ts:431-438listing"maintain" | "triage"shows the author assumedrole_namesemantics.This fails closed (no privilege escalation), but a documented trust tier silently never activates —
worth fixing before anyone relies on it in hosted per-repo-admin mode.
Fix: request and match on
role_name, or delete the dead"maintain"/"triage"arms from the unionand both comparisons so the code stops implying a tier it cannot see.
Test Coverage Requirements
99%+ patch coverage, branch-counted. For (1): both arms of the id-match check including the
0default.For (2): a Maintain-role fixture resolving to the intended tier.
Links & Resources
src/github/app.ts~241-260, ~431-438;src/orb/broker-client.ts~87;src/auth/security.ts~198;src/queue/processors.ts~13512maintainer-only — credential and permission correctness.