You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
src/services/control-panel-roles.ts has two exported builders, buildControlPanelAccessScope (lines
~74-108) and buildControlPanelRoleSummary (lines ~110-142), that independently compute the SAME
access-scope facts from the SAME inputs using byte-identical logic:
accountInstallations — installations for this login that are NOT suspended
accountInstallationIds — the Set of those installations' ids
suspendedAccountInstallationIds — installations for this login that ARE suspended
ownedInstalledRepos — installed repos owned by this login or under one of its non-suspended
installations, explicitly excluding any repo under a suspended installation
maintainerRepos — repos where this login has a maintainer-association PR, restricted to installed repos
Both blocks carry the identical inline comment: // A suspended installation revokes the App's access, so a repo under a suspended account installation must not grant control-panel scope — including via the owner-match branch, which previously ignored suspendedAt (#953). This is the fix for closed issue #953
("suspending the App does not revoke control-panel access to your own repos") — a real, previously-shipped
security bug in exactly this access-scope computation.
Because the fix for #953 was applied identically to two separate, independent copies of the same logic
instead of to one shared function, this file now carries the exact structural precondition for the "fix
applied to one place but not its structurally identical sibling" bug class: any future change to this
access-scope logic (a new eligibility rule, a new suspension state, an additional installation field) has
to be found and applied twice, in two functions in the same file, with nothing enforcing that both stay in
sync. canLoginAccessRepo and canWatchRepo (same file) both depend on buildControlPanelAccessScope/loadControlPanelAccessScope's output for private-repo access decisions, so
a future silent divergence here is not a cosmetic issue — it is the same class of bug #953 already was.
Requirements
This is a pure, behavior-preserving refactor. It must not change what either function returns for any
existing input. The scope is exclusively: extract the identical computation into one shared internal
helper, and have both existing functions call it.
Add a private helper (e.g. resolveAccountRepoScope) in src/services/control-panel-roles.ts that takes
the same inputs the duplicated blocks use (repositories, installations, login) and returns { installedRepos, ownedInstalledRepos, maintainerRepos, accountInstallations, accountInstallationIds, suspendedAccountInstallationIds } (or an equivalent shape covering everything both call sites currently
derive from the duplicated block) — including pullRequests as an input where maintainerRepos
depends on it.
Update buildControlPanelAccessScope to call this helper instead of its own copy of the logic, and use
the returned values to build its existing ControlPanelAccessScope return shape exactly as today.
Update buildControlPanelRoleSummary to call the same helper instead of its own copy, and use the
returned values to build its existing ControlPanelRoleSummary return shape exactly as today.
Keep the #953 explanatory comment (suspended-installation exclusion) attached to the ONE shared
computation, not duplicated.
Do not change canLoginAccessRepo, canWatchRepo, loadControlPanelAccessScope, loadControlPanelRoleSummary, buildStaticControlPanelRoleSummary, buildRoleCards, or any other
function in this file beyond routing the two builders through the new shared helper.
Do not change the exported RoleSummaryInputs, ControlPanelAccessScope, or ControlPanelRoleSummary
types.
Deliverables
One shared internal helper in src/services/control-panel-roles.ts replacing the two duplicated
computation blocks.
buildControlPanelAccessScope and buildControlPanelRoleSummary both call the shared helper; neither
contains its own copy of the installed/owned/maintainer/suspended computation afterward.
Test Coverage Requirements
99%+ Codecov patch coverage, branch-counted, on every changed line in src/**. Because this must be a
zero-behavior-change refactor:
Every existing test in the repo's control-panel-roles test file must continue to pass UNMODIFIED (no
test assertions should need to change — if one does, that indicates a behavior change that is out of
scope for this issue).
Add one explicit new regression test that calls BOTH buildControlPanelAccessScope and buildControlPanelRoleSummary with the SAME input fixture — a login whose installation is suspended but
who still owns the installed repo (the exact [Bug]: suspending the App does not revoke control-panel access to your own repos #953 scenario) — and asserts both functions agree that the
repo is excluded from scope/roles. This pins the shared-helper invariant this issue exists to protect:
the two call sites can no longer silently diverge because they share one implementation.
Confirm branch coverage on the shared helper: suspended vs non-suspended installation, owner-match vs
installation-id-match vs neither, and a maintainer-association PR on a non-installed repo (excluded).
Expected Outcome
src/services/control-panel-roles.ts computes account access scope in exactly one place. A future change to
this logic (a new eligibility rule, an additional suspension state, a new installation field) only needs to
be made once, and buildControlPanelAccessScope and buildControlPanelRoleSummary can never silently
diverge the way two independent copies of the same security-relevant logic otherwise could.
Links & Resources
src/services/control-panel-roles.ts — buildControlPanelAccessScope, buildControlPanelRoleSummary
(the two duplicated blocks).
Context
src/services/control-panel-roles.tshas two exported builders,buildControlPanelAccessScope(lines~74-108) and
buildControlPanelRoleSummary(lines ~110-142), that independently compute the SAMEaccess-scope facts from the SAME inputs using byte-identical logic:
installedRepos—args.repositories.filter((repo) => repo.isInstalled)accountInstallations— installations for this login that are NOT suspendedaccountInstallationIds— theSetof those installations' idssuspendedAccountInstallationIds— installations for this login that ARE suspendedownedInstalledRepos— installed repos owned by this login or under one of its non-suspendedinstallations, explicitly excluding any repo under a suspended installation
maintainerRepos— repos where this login has a maintainer-association PR, restricted to installed reposBoth blocks carry the identical inline comment:
// A suspended installation revokes the App's access, so a repo under a suspended account installation must not grant control-panel scope — including via the owner-match branch, which previously ignored suspendedAt (#953).This is the fix for closed issue #953("suspending the App does not revoke control-panel access to your own repos") — a real, previously-shipped
security bug in exactly this access-scope computation.
Because the fix for #953 was applied identically to two separate, independent copies of the same logic
instead of to one shared function, this file now carries the exact structural precondition for the "fix
applied to one place but not its structurally identical sibling" bug class: any future change to this
access-scope logic (a new eligibility rule, a new suspension state, an additional installation field) has
to be found and applied twice, in two functions in the same file, with nothing enforcing that both stay in
sync.
canLoginAccessRepoandcanWatchRepo(same file) both depend onbuildControlPanelAccessScope/loadControlPanelAccessScope's output for private-repo access decisions, soa future silent divergence here is not a cosmetic issue — it is the same class of bug #953 already was.
Requirements
This is a pure, behavior-preserving refactor. It must not change what either function returns for any
existing input. The scope is exclusively: extract the identical computation into one shared internal
helper, and have both existing functions call it.
resolveAccountRepoScope) insrc/services/control-panel-roles.tsthat takesthe same inputs the duplicated blocks use (
repositories,installations,login) and returns{ installedRepos, ownedInstalledRepos, maintainerRepos, accountInstallations, accountInstallationIds, suspendedAccountInstallationIds }(or an equivalent shape covering everything both call sites currentlyderive from the duplicated block) — including
pullRequestsas an input wheremaintainerReposdepends on it.
buildControlPanelAccessScopeto call this helper instead of its own copy of the logic, and usethe returned values to build its existing
ControlPanelAccessScopereturn shape exactly as today.buildControlPanelRoleSummaryto call the same helper instead of its own copy, and use thereturned values to build its existing
ControlPanelRoleSummaryreturn shape exactly as today.#953explanatory comment (suspended-installation exclusion) attached to the ONE sharedcomputation, not duplicated.
canLoginAccessRepo,canWatchRepo,loadControlPanelAccessScope,loadControlPanelRoleSummary,buildStaticControlPanelRoleSummary,buildRoleCards, or any otherfunction in this file beyond routing the two builders through the new shared helper.
RoleSummaryInputs,ControlPanelAccessScope, orControlPanelRoleSummarytypes.
Deliverables
src/services/control-panel-roles.tsreplacing the two duplicatedcomputation blocks.
buildControlPanelAccessScopeandbuildControlPanelRoleSummaryboth call the shared helper; neithercontains its own copy of the installed/owned/maintainer/suspended computation afterward.
Test Coverage Requirements
99%+ Codecov patch coverage, branch-counted, on every changed line in
src/**. Because this must be azero-behavior-change refactor:
control-panel-rolestest file must continue to pass UNMODIFIED (notest assertions should need to change — if one does, that indicates a behavior change that is out of
scope for this issue).
buildControlPanelAccessScopeandbuildControlPanelRoleSummarywith the SAME input fixture — a login whose installation is suspended butwho still owns the installed repo (the exact [Bug]: suspending the App does not revoke control-panel access to your own repos #953 scenario) — and asserts both functions agree that the
repo is excluded from scope/roles. This pins the shared-helper invariant this issue exists to protect:
the two call sites can no longer silently diverge because they share one implementation.
installation-id-match vs neither, and a maintainer-association PR on a non-installed repo (excluded).
Expected Outcome
src/services/control-panel-roles.tscomputes account access scope in exactly one place. A future change tothis logic (a new eligibility rule, an additional suspension state, a new installation field) only needs to
be made once, and
buildControlPanelAccessScopeandbuildControlPanelRoleSummarycan never silentlydiverge the way two independent copies of the same security-relevant logic otherwise could.
Links & Resources
src/services/control-panel-roles.ts—buildControlPanelAccessScope,buildControlPanelRoleSummary(the two duplicated blocks).
security bug already fixed identically in both places; this issue prevents a future regression of the
same shape by removing the duplication itself.