Repository navigation
feat(spec): register the invitation role cap refusal codes under plugin-auth - #24092
Conversation
…der plugin-auth INVITE_ROLE_ABOVE_ISSUER, INVITE_ISSUER_MEMBER_ONLY and INVITE_ISSUER_UNVERIFIED join the ledger ahead of their producer, so the plugin-auth half can stamp them on the cap's refusals instead of a bare FORBIDDEN. Claude-Session: https://claude.ai/code/session_01GDoMKJFY45jdSGzFmyumij Co-authored-by: Claude <noreply@anthropic.com>
…ole cap codes Claude-Session: https://claude.ai/code/session_01GDoMKJFY45jdSGzFmyumij Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 142 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e40e7dcc04fbfe1f5a203fde74840e830302e653 && git checkout e40e7dcc04fbfe1f5a203fde74840e830302e653
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin be076dad28a0d35cefc7f76cd06950620cb8b81b 44818e2e6df0671bf0b55140423ea2327e6e3943 && git checkout -B drift-repro be076dad28a0d35cefc7f76cd06950620cb8b81b && git merge --no-ff 44818e2e6df0671bf0b55140423ea2327e6e3943
node scripts/docs-audit/affected-docs.mjs --json be076dad28a0d35cefc7f76cd06950620cb8b81b
|
Contract reviewServed-tier: Inputs: card #24082 (body, claim ① Derived judgmentsGate verdicts on this head (40 check-runs). 34 are success, 5 are skipped by their path filters or opt-in ( The accept-set change is exactly three strings under Each name against its producer. The producer is
The three are exactly the set: no fourth path, and none unreachable. I read the paths against the grant table ( Condition 3's wording holds.
The comment's main clause, "the issuer's Synonym gate (#8211). None of the three is an HTTP reason phrase, so the first test in #16404, "door or no door". The ledger is the published face, so registering the codes widens it. The rows land AHEAD of their producer: no The added test is a real pin. The case at The regenerated docs are generated, with no hand edits. Both pages carry the ② Semver level
No other package owes a changeset: ③ Boundary flagsDev deviations, each answered:
open_questions[1]: should a thrown lookup answer 503 Ruled: this card is right to register one code now and leave the split to #23945. Covering a read fault in the comment is not a wrong contract sentence that must change before landing. Four reasons:
What this ruling hands to #23945 (escalated, not blocking):
open_questions[2], the names: A, as landed. Each name states its condition, carries the domain prefix the header asks for, sits in the existing Window before the producer lands (escalated to the seat). Until #23945 stamps these codes, the three rows are what the header's "Retiring a code" section describes: "a registered-but-unemittable row stays green forever". No gate fails on that. Admission checks casing, duplication and shadowing, and the provenance gate only checks that a code which is stamped is also registered, not the reverse. This is acceptable as the recorded split shape. When this PR lands, #23945 ( Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT · PR #24092 @
|
Fixes #24082
Clause-②: yes (widening)
What
Registers three
@objectstack/plugin-authcodes inpackages/spec/src/api/error-code-ledger.zod.ts, one per refusal the invitation role cap (packages/plugins/plugin-auth/src/invitation-role-cap.ts) can return. Todayauth-manager.tsthrows all three as a bareAPIError('FORBIDDEN', { message })frombeforeCreateInvitation(:3416–:3423), so the Invite User dialog cannot branch on them.8a83cebc15plugin-authpairs it withINVITE_ROLE_ABOVE_ISSUERinvitation-role-cap.ts:186–192: the requested role grades above the issuer's ownINVITE_ISSUER_MEMBER_ONLYinvitation-role-cap.ts:194–199: an issuer below admin grade asks for anything but a single plainmemberINVITE_ISSUER_UNVERIFIEDinvitation-role-cap.ts:175–181: the issuer's grade is unresolved (resolveMembershipRoleanswerednull)Registration only, ahead of the producer (the
FLOW_INPUT_SCHEMA_INVALIDsplit shape the ledger test already records). #23945 stamps these codes inplugin-auth; that card is not closed by this PR.Measurements
invitationRoleCapFailurehas three non-null returns (lines 175, 186, 194) and no other refusal; its one caller (auth-manager.ts:3416) turns each into the sameFORBIDDEN. Path 2 is reached only by an issuer below admin grade: an admin or owner passesgrantTableAdmitsfor every role except one that outranks them, which is path 1.standardSynonymOfon the builtdistreturnsundefinedfor all three (INVITEis a token no standard member carries), andErrorCode.safeParseaccepts each. NoSTANDARD_SYNONYM_WAIVERSentry. Named in the existingINVITE_*family, for the condition rather than the status.resolveMembershipRole(auth-manager.ts:7612) answersnullfor four inputs: missing keys, no engine, no membership row, or a lookup that threw. The cap cannot tell them apart, so the code's meaning is "could not be established" and it pairs with 403, fail-closed. Whether plugin-auth: the invitation role-cap refusals (invite as admin / delegated_admin by a delegated issuer) answer 403 with a message and nocode— the sibling vendor refusal on the same endpoint carries one #23945 should split the thrown-lookup case onto the standardSERVICE_UNAVAILABLE(as the slug guard's read fault does,auth-manager.ts:1333) is noted below for that card. It is not decided here.Files
packages/spec/src/api/error-code-ledger.zod.ts: three rows, each with a meaning comment citing error-code ledger: register the invitation role-cap refusal codes plugin-auth throws (grade above the issuer, a non-admin inviting a non-member role, membership unverifiable), so a client can tell them apart #24082.packages/spec/src/api/error-code-ledger.test.ts: one case pinning the three by value (parse, owner, no synonym), mirroring the registered-ahead-of-producer precedent.content/docs/references/api/contract.mdx,content/docs/references/api/error-code-ledger.mdx: regenerated bypnpm --filter @objectstack/spec check:generated --fix(the only stale artifact it found wascheck:docs)..changeset/24082-invite-role-cap-codes.md:minoron@objectstack/spec.Verification (head
44818e2e6d)pnpm --filter @objectstack/spec build: exit 0. Thenexec vitest run src/api/error-code-ledger.test.ts src/api/api-error-code-type.test.ts: 2 files, 24 tests passed.typecheck: exit 0.pnpm --filter @objectstack/spec check:generated: exit 1 before regeneration (check:docsstale), exit 0 after--fix.node scripts/pm/dispatch-gates.mjs --commands: 111 families derived, all run and reconciled with--ran(111 derived famil(ies) accounted for — 109 run, 2 NOT-MEASURED). Among themcheck:error-code-casing,check:dispatcher-error-vocabulary,check:error-code-provenance,check:api-surfaceandcheck:docsall exit 0.check:dual-build-cjs-loadsandcheck:lean-entry-closure(exit 3, PREREQUISITE NOT MET: they need a whole-workspace build). Both are left to CI'sLint & Repo Gates.--no-inline-config) over both changed.tsfiles: exit 0.Acceptance notes
code— the sibling vendor refusal on the same endpoint carries one #23945 (not a finding filed):resolveMembershipRoleanswersnullboth when the issuer has no membership row and when the lookup threw. Under this registration both areINVITE_ISSUER_UNVERIFIEDwith 403. If plugin-auth: the invitation role-cap refusals (invite as admin / delegated_admin by a delegated issuer) answer 403 with a message and nocode— the sibling vendor refusal on the same endpoint carries one #23945 separates the thrown lookup, the existing standardSERVICE_UNAVAILABLEis the code that fits it, so no fourth code is registered here.Generated by Claude Code