Repository navigation
Description
Filing gate: ① a violation of a declared contract, with a reproduction (pm:queue class).
Reader: the domain:services lane owning packages/plugins/plugin-auth (the organization invite door) — the fix PR there, or a spec/doc PR if the lane decides the docstring is what is wrong.
Dedupe (open AND closed, semantic search on objectstack-ai/objectstack): "invite-member ROLE_NOT_FOUND error code missing" → 10 hits; a vendor-refusal variant of the same query → 11 hits. The nearest, #23945 and #24243 (both closed), cover plugin-auth's own throws, not this vendor refusal. objectstack-ai/objectui → 2 hits, none on point.
reach: measured through the public door in the 18.0 QA wave — identity-auth run #24538 at framework 5b14b7f recorded that an invite naming an unknown role is refused with no code; re-read at framework f7005cb, the code path is unchanged.
Path: identity-auth (invite-member door) · priority of the area's items: P1
QA-source: #24538 · identity-auth · observation recorded in the run's checklist-accuracy section
type: Bug
Filed by the 18.0 QA wave dispatch session session_01CfRDy7SuTfUqaTu2AEH2Hi on the maintainer's instruction there, verbatim: 「立那 2 张漏掉的缺陷卡」 (the run had recorded it as an observation, not a card).
What is wrong
POST /api/v1/auth/organization/invite-member with a role outside the closed vocabulary is refused, but the refusal carries only a message (the text ROLE_NOT_FOUND: followed by the role name) and no machine code. A client written to ADR-0112's rule — branch on error.code, never on prose — can never recognise it.
The contract says it is coded:
packages/spec/src/identity/organization.zod.ts— the invitation schema'sroledocstring: "A name outside it is refused at the door (ROLE_NOT_FOUND) before any invitation row exists."content/docs/permissions/authentication.mdx— the same refusal named asROLE_NOT_FOUND.- ADR-0112 — refusals carry a code from the error-code vocabulary.
Reproduction
- Stock showcase boot with
--seed-admin; sign in as the admin. POST /api/v1/auth/organization/invite-memberwith body{"email":"qa-invitee@example.com","role":"no_such_role"}.- Expected: a 400 whose error carries
code: "ROLE_NOT_FOUND". Actual: a 400 whose error carries the messageROLE_NOT_FOUND: no_such_roleand nocode.
Control: POST /api/v1/auth/organization/update-member-role with an unknown role answers with a code — the same vendor family codes it on that door.
Mechanism
The refusal is thrown by the vendored better-auth organization plugin (its invite route throws a BAD_REQUEST APIError with only a message), and plugin-auth passes it through without mapping it to a code; ROLE_NOT_FOUND is not in the error-code ledger. Expected fix shape: plugin-auth maps this vendor refusal to ROLE_NOT_FOUND (registering it in the ledger), so the door answers what the spec docstring promises — the spec is the baseline when spec and code disagree.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 20, 2026
Still reproduces at 580fd4e · QA-source: #24779 · closed-card / open-card regression section (identity-auth observation carried from #24538)
Admin cookie session on a fresh stock showcase boot (
objectstack dev --seed-admin, framework580fd4ed):POST /api/v1/auth/organization/invite-member{"email":"qa-invitee-1@example.com","role":"no_such_role"}→ 400{"message":"ROLE_NOT_FOUND: no_such_role"}— still nocodekey; reproduced twice.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 7 · 2026-10-20T08:14Z
Session:
session_011TdZ8GjixNZFLz1xJD9zzKAccount:
os-justin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24750-invite-role-not-found-codeWorktree:
objectstack-issue-24750(in the cloud session's own container)Domain:
domain:servicesSeat:
domain:services#1File surface:
packages/plugins/plugin-auth/src/**(where the vendored organization plugin's invite refusal reaches plugin-auth's door, mapped toROLE_NOT_FOUND), its tests, and one new.changeset/24750-*.md. Cross-lane, under the spec lane's pre-approved ledger-append class (lanes/spec.md: the code's existence is already declared, here by the spec's own docstring atidentity/organization.zod.ts:149and:231; the fallback is unchanged; a pure append in this package's own section of the ledger): oneROLE_NOT_FOUNDentry inpackages/spec/src/api/error-code-ledger.zod.ts, plus whatcheck:generatedregenerates from it. ⛔ No otherpackages/spectext, ⛔ no ledger field or mechanism change, ⛔ not the vendored library (stop on breach; explain in the report)Container & model:
M,mode:cloud,model: default (opus), quoting this dispatch's --tier output "no path-derived mandate: the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable"; cloud because the surface names packages/spec/src/** and another package (build-heavy, one container per card); the review owed is CONTRACT_REVIEW_TIER (the published ledger moves), run by an isolated at-tier reviewer before the queueClause-②: yes
Responsibility:
plugin-auth passes the vendored organization plugin's unknown-role refusal through with no code, while the spec's invitation docstring and the authentication docs promise ROLE_NOT_FOUND | no platform path covers it: plugin-auth's door is the only producer of this refusal | every ADR-0112 client of POST /api/v1/auth/organization/invite-memberThread-read: 6284251818
Serial constraints cleared:
PR #24795 (#24739, this seat) landed as 25e16ab00c, which frees plugin-auth's auth-manager.ts and auth-plugin.ts. No open PR touches plugin-auth's auth-manager.ts or auth-plugin.ts, packages/spec/src/api/error-code-ledger.zod.ts, identity/organization.zod.ts or authentication.mdx (all 12 open PRs' files read). The epic's declared territory (#15194) names none of them.Readings taken on
origin/mainc56115bf96.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Dispatch ·
domain:servicesseat 1 · 2026-10-20T08:16Z · claim6286575652.mode:cloudsessionsession_01SKHckexWmsVZ6qZ1sioSkD(title "⚡ services #24750 invite ROLE_NOT_FOUND code"), outcome branchclaude/issue-24750-invite-role-not-found-code, the default tier. The order carries the spec's own promise ofROLE_NOT_FOUND(identity/organization.zod.ts:149,:231) as the acceptance, and confines the ledger change to one append in plugin-auth's section, under the spec lane's pre-approved ledger-append class. ACONTRACT_REVIEW_TIERreview is owed before the queue. The dev delivers a draft PR and anos-dev-reportcomment here.Generated by Claude Code
test(plugin-auth): pin invite-member's unknown-role refusal code (#24750
fix(plugin-auth): invite-member's unknown-role refusal carries code R…
chore(changeset): plugin-auth and spec minor for ROLE_NOT_FOUND (#24750)
docs(references): regenerate for the ROLE_NOT_FOUND ledger entry (#24750
objectstack-fleet commentedon Oct 20, 2026
Still reproduces at 307ce4c · QA-source: #24841 · observed beside the clauses (invite-member with
role:"no_such_role"→ 400{"message":"ROLE_NOT_FOUND: no_such_role"}, nocode, twice)Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{ "issue": 24750, "status": "done", "branch": "claude/issue-24750-invite-role-not-found-code", "pr": "https://github.com/objectstack-ai/objectstack/pull/24846", "session": "session_01SKHckexWmsVZ6qZ1sioSkD", "premise_still_valid": true, "summary": "Reproduced through the real door: invite-member with role no_such_role answered 400 with no code, while update-member-role answered 400 ROLE_NOT_FOUND. New plugin-auth module invite-role-not-found-code.ts is called last in the better-auth after hook. It recognises exactly this vendor refusal (invite path, 400 APIError, no code, message prefix 'ROLE_NOT_FOUND: ') and re-throws it as APIError BAD_REQUEST with code ROLE_NOT_FOUND and the vendor's message verbatim. That is byte-identical to update-member-role's body, and the refusal still comes before any invitation row. One ROLE_NOT_FOUND row was added in plugin-auth's ledger section; check:generated --fix regenerated content/docs/references/api/{contract,error-code-ledger}.mdx. The changeset sets plugin-auth and spec to minor. H1 falsified in part: update-member-role's code comes from better-auth itself (crud-members.mjs sets code), not from a plugin-auth mapping, so there was no shared plugin-auth seam to reuse. The seam used is better-auth's own runAfterHooks, which replaces the returned value with an APIError an after hook throws. The vendored library is untouched and no other refusal changed. Card assignee: not touched. Newest Claim 6286575652 names this branch.", "tests": "head 65cdcf23c (after merging origin/main 08d201a80): pnpm --filter @objectstack/plugin-auth test → Test Files 168 passed, Tests 3227 passed | 10 skipped; pnpm --filter @objectstack/spec test → Test Files 703 passed, Tests 21360 passed | 1 todo; plugin-auth typecheck + spec typecheck → exit 0 (VERDICT command-exit 0). Repro before the fix (3 new pins in src/accept-invitation-adopt-membership.test.ts, -t 24750): 1 failed | 2 passed, 'expected undefined to be ROLE_NOT_FOUND'. Ablation at 925e35e1 via scripts/ablation-replace.mjs: anchor 'await codeInviteRoleNotFound(ctx);' x1→x0, blob 658b4666de2f→44a4a7103dff; unknown-role pin red with the same assertion, valid-role and update-member-role controls green; restored with git checkout HEAD -- PATH; blob after restore 658b4666de2f == HEAD blob, git diff HEAD empty, ABLATED marker count 0, anchor count 1. No dist leg: the test imports ./auth-manager from source.", "gates": "local, head 65cdcf23c: pnpm --filter '@objectstack/plugin-auth^...' build exit 0; pnpm --filter @objectstack/spec build && check:generated exit 0 ('All 14 generated artifacts are up to date'); node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 118 commands from 7 paths, each run with exit captured before any pipe, all 118 exit 0; --ran → '118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). The first pass at 3810c7f1f had 3 exit-3 prerequisites. Two were cured and re-run green at that head: plugin-teardown-shape --self-test after git fetch of fixture 621a4876, and check:skill-examples after building client/client-react. check:dual-build-cjs-loads exited 0 on the second pass. CI at report time, PR check-runs on 65cdcf23c: 25 runs; 'No other open PR may claim the same issue' success; 18.x upgrade harness and Packed-tarball smoke skipped (opt-in); the rest (Type Check ×4, Lint ×9, Check Changeset, Governed Surface Queue Guard, Spec property liveness, Check PR Size, and others) in_progress. Not waited on, per contract.", "mcp_calls": "0", "api_writes": "2 relay strokes (objectstack-fleet[bot], fleet-write): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24846/assignees; read-back identical, 6421 bytes, assignee os-justin; (2) comment → POST /repos/objectstack-ai/objectstack/issues/24750/comments (this report). git push (not REST) ran 6 times to the one branch: the empty-branch probe, then a WIP push per commit as AGENTS.md requires before each long step. All fast-forward, no force.", "open_questions": [ { "question": "Which seam codes the refusal?", "options": ["A after-hook re-throw of the vendor's own uncoded refusal with code ROLE_NOT_FOUND", "B before-hook re-validation of the role in plugin-auth, throwing a coded refusal before the vendor runs"], "recommendation": "A (taken). B gives plugin-auth a second copy of the vendor's vocabulary check (static plus dynamic organizationRole rows), two owners of one decision that drift. A adds only the missing code on the vendor's own verdict, so the contract tightens with no consumer-side tolerance." }, { "question": "Changeset level for @objectstack/spec?", "options": ["A minor: the new ledger row widens the published ErrorCode union", "B patch"], "recommendation": "A (taken). Rule: AGENTS.md Post-Task Checklist step 3, 'yes takes at least minor'; the spec face widens too." }, { "question": "Where do the door pins live?", "options": ["A a new describe block in accept-invitation-adopt-membership.test.ts, reusing its pinned engine double", "B a new test file with its own copied engine double"], "recommendation": "A (taken). B adds a double that moves scripts/engine-double-contract.pinned.json, outside the claimed file surface." } ], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
ACCEPT → PR #24846 comment
6289192939·domain:servicesseat 1 ·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-20T10:49ZGenerated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed ·
domain:servicesseat 1 ·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-20T11:27Zadded_to_merge_queue2026-10-20T10:52Z →merged2026-10-20T11:25Zorigin/mainby contentde9cd1f17f(one parent,7cacad5348).packages/spec/src/api/error-code-ledger.zod.tscarries theROLE_NOT_FOUNDrow under@objectstack/plugin-auth, andplugin-auth/src/invite-role-not-found-code.tsis presentState change in this stroke:
pm:dispatchedoff and the assignee cleared (the card closedcompletedon the merge). The contract review of record is6289177621(CONTRACT_REVIEW_TIER, PASS), and the ACCEPT is6289192939. The cloud sessionsession_01SKHckexWmsVZ6qZ1sioSkDis archived after this note.Noted, not filed: better-auth's other organization codes, which plugin-auth passes through flat (
ORGANIZATION_NOT_FOUND,MEMBER_NOT_FOUND,INVITATION_NOT_FOUND…), are not registered in the ledger either. That gap predates this card, sits outside its class, and has no measured consumer.Generated by Claude Code