Repository navigation
Role names are validated through the prototype chain: role:"toString" silently demotes a Maintainer and answers changed:true #1838
Description
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 6d3294ad31e94b3bb35def4bde867f49
Branch: dev/1-p247-role-name-own-key
Base: f245520
At: 2026-10-30T13:43:36Z
What goes wrong
changeRole (core/src/host/project-board.js:68-90) validates the requested role with PROJECT_ROLE_RANKS[wanted] === undefined (:74), but PROJECT_ROLE_RANKS (:27) is a plain object literal, so every inherited name passes: toString, __proto__, constructor, valueOf, hasOwnProperty read a function/object instead of undefined. Nothing downstream rejects the name: wanted === 'owner' (:75) is false, and for a self-change the check PROJECT_ROLE_RANKS[wanted] > PROJECT_ROLE_RANKS[actor] (:79) is NaN > n, i.e. false. The write path then falls into else members.push(target) (:88).
Consequence for an Owner re-roling a Maintainer with role:"toString": the answer is {ok:true, roles:{maintainers:[], members:[..., target]}} — the Maintainer is silently demoted to Member and the UI reports "Role changed."
Latent until #1837 lands: on live main f245520c, project-role is missing from TASK_ENDPOINTS (core/src/host/authz.js:60-77), so a signed-in host answers bot/not-found (#1836). #1837 adds the endpoint; from that merge on, any signed-in member of the Project can call it directly and a mistyped role silently demotes someone.
Same family as #1704 (ids resolved through the prototype chain), but a different table: #1704 fixed the state tables in store.js; PROJECT_ROLE_RANKS is a separate literal and that fix does not cover it.
Reproduction
import { changeRole } from 'core/src/host/project-board.js'
const p = { id: 'project-1', name: 'P', ownerUserIds: ['owner'], roles: { maintainers: ['m-3'], members: ['m-2'] } }
changeRole(p, { actorId: 'owner', targetId: 'm-3', role: 'toString' })Observed (probe over an unpacked copy of live f245520c, 2026-10-30): {ok:true, roles:{maintainers:[], members:['m-2','m-3']}}; __proto__ and constructor behave the same. Expected: {ok:false, reason:'unknown-role'} with roles byte-for-byte unchanged. The red-first tests below are the authoritative repro.
Expected
- Any name that is not an own key of
PROJECT_ROLE_RANKSis refused:{ok:false, reason:'unknown-role'}, and the record is untouched. - One-line fix:
if (!Object.hasOwn(PROJECT_ROLE_RANKS, wanted)). - Legitimate switches keep working exactly as today: owner → member/maintainer, maintainer → member for self, and every existing refusal (
owner-not-assignable,last-owner,not-owner,cannot-self-promote).
Red-first contract
On base f245520c, add three cases to the existing core/test/project-roles.test.js:
changeRole(project({roles:{maintainers:['m'],members:['n']}}), {actorId:'owner', targetId:'m', role:'toString'})→{ok:false, reason:'unknown-role'}, roles unchanged.- same with
role:'__proto__'. - same with
role:'constructor'.
All three must fail on base (they currently answer {ok:true, ...} and demote m), and the existing 5 tests must stay green, unmodified.
Scope
core/src/host/project-board.jsand its byte-identical mirrorpatches/ds-bot/files/src/host/project-board.js(both 23561 bytes,cmpclean atf245520c). No.patchregeneration, notopics.jsonchange.core/test/project-roles.test.jsand mirrorpatches/ds-bot/files/test/project-roles.test.js(byte-identical today).changelog.d/<this issue>.fixed.md(repo convention: 1809, 1814).- Out of scope: an endpoint-layer unknown-role test.
project-roleis not callable on live main until fix(host): let the Project role switch reach its handler on a signed-in host (#1836) #1837 merges, so a test reaching it today would be red for an unrelated reason; the end-to-end "Owner re-roles someone" lock is tracked separately and lands after fix(host): let the Project role switch reach its handler on a signed-in host (#1836) #1837.
Verification contract
- Commit first, then run the full
npm run verifyon the committed clean tree (git status --porcelainempty) withARCH_DOCS_BASE=<live main base>; the PR body carries those real numbers only. node scripts/ds-bot-patches.mjs check→ the three green lines;build-client --checkexit 0 as part of verify.- PR:
Fixes #<this issue>,maintainer_can_modify: true, no placeholders, one tip.
Dedupe (2026-10-30)
Grep of live main f245520c: PROJECT_ROLE_RANKS appears only at project-board.js:27,60,74,79; no Object.hasOwn on that map. Closed-issue search: #1704 (prototype lookups on state tables, fixed 2026-10-29) is the same family but a different table and code path; #1836/#1837 cover only the missing project-role authz entry. Neither open nor closed issues cover role-name validation.
Activity
fix(host): a role name is the rank table's own key (ThinkFlowLab#1838)
fix(host): a role name is the rank table's own key (ThinkFlowLab#1838)
Birfy commentedon Oct 30, 2026
Written-by: Chief
状态补记:
agent-pr。本单已有开放 PR #1842(dev/1-p247-role-name-own-key,tip2123e2cc,正文 Fixes #1838),此前本单没有任何状态标签。CI、入队与合并由开该 PR 的那条线负责,本线不接手。读取时间 2026-10-30T16:35Z,来源 api.github.com。