Repository navigation
fix(plugin-auth): the last-administrator guard answers a read fault 503 SERVICE_UNAVAILABLE, the code its sibling guards answer with - #25388
Conversation
…03 SERVICE_UNAVAILABLE Claude-Session: https://claude.ai/code/session_01CuJfNjvts78GKesupEdgbz Co-authored-by: Claude <noreply@anthropic.com>
…doors; changeset Claude-Session: https://claude.ai/code/session_01CuJfNjvts78GKesupEdgbz Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 19 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 505d191e72bd6b5d88ac96eb3d40dc79d62550e5 && git checkout 505d191e72bd6b5d88ac96eb3d40dc79d62550e5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c898bd8ac3f6981c9f45b16dabedea4459095bae 3aee19812a51779419547eff2374614e19c0828f && git checkout -B drift-repro c898bd8ac3f6981c9f45b16dabedea4459095bae && git merge --no-ff 3aee19812a51779419547eff2374614e19c0828f
node scripts/docs-audit/affected-docs.mjs --json c898bd8ac3f6981c9f45b16dabedea4459095bae
|
席位核对ACCEPT ·
Generated by Claude Code |
Requested by Tesla · project thread
Fixes #25364
Clause-②: no
Metadata-protocol: none
Measured first: the shared shape, not one half
The card asked whether to change one half of the last-administrator guard or the shared
failClosedshape. Measured onmainat d12e650:enforcefor the twosys_userhalves,enforceStandingfor the membership, grant, activation-ledger and organization halves, andjudgeOrganizationDelete) runs insidefailClosed, which answered a read fault withrefuse(): 403PERMISSION_DENIED.guardOrganizationDefaultSlug, from fix(plugin-auth)!: under single, no slug change makes another organization the Default Organization #25286) does not usefailClosed. It has its owncatch, which also answered 403PERMISSION_DENIED.organization/updaterequest answered 403. Both now throw one shape.What changes
last-admin-guard.ts: a new module-privatereadFault()besiderefuse():code: 'SERVICE_UNAVAILABLE'(typedStandardErrorCode, an existing ADR-0112 member, no new code),status: 503,object= the table the caller was writing,cause= the read's own failure.failClosedand the slug-capture rule'scatchboth throw it, with their messages unchanged. Both now pass an inner read fault through unchanged as well as an inner refusal (judgeOrganizationDeletenests onefailClosedinside another), so the fault is never wrapped twice.PERMISSION_DENIED, byte for byte. That includes themaxScanoverflow: that read completed and proved too large, so it is a refusal with a remedy, not a read fault.objectql-adapter.ts(the better-auth door):rethrowAsBetterAuthErrorgains an arm for an engine error that declarescode: 'SERVICE_UNAVAILABLE'andstatus: 503, answeringAPIError('SERVICE_UNAVAILABLE')with that code and a fixed message (the driver's text is not relayed, asmapDataError's declared-5xx relay withholds it). Without this arm the 503 reached better-call's router as an unhandled fault: measured by reverting only this file,organization/updateanswered 500 with no code. The arm is keyed on the declared pair, not on the guard, so any engine error declaring that pair gets the answer/dataalready gives it.auth-manager-actor-guards.ts(organization/delete): the Default Organization's standing judge mapped onlyPERMISSION_DENIED. It now maps the guard's read fault toAPIError('SERVICE_UNAVAILABLE')withORGANIZATION_DELETE_READ_FAULT_CODE, the code that admission's own read faults (fix(plugin-auth): an organization-delete door lookup that throws hands the delete to better-auth uncounted, so a driver without transactions keeps a partial delete #25059) already answer with, and a message that does not relay the driver's text.The expected landing was
last-admin-guard.ts. The two other files are in the same package (@objectstack/plugin-auth) and are the doors the guard's refusal crosses: without them, the code moves at the engine and the/datadoor but the better-auth doors answer 500.Security floor
No change turns a refusal into an allow. Every changed site still throws, from the same place, before any write. Only the code and status of a fail-closed refusal move (403 to 503). The pins below assert that nothing was written in each fault case (
bannedFlag,userExists,memberRole,slugOf, and no attempted write onorganization/delete).Pins (all assert
code+status)organization/update(single-reslug-default-anchor.test.ts): a failed read of the Default Organization now answers{ status: 503, code: 'SERVICE_UNAVAILABLE' }(was 403PERMISSION_DENIED), the body does not contain the injected driver text, and the slug is unchanged. That is the codeauth-manager.ts's organization slug guard answers its own read fault with (auth-manager.org-slug-guard-system-context.test.ts).{ code: 'SERVICE_UNAVAILABLE', status: 503 }.last-admin-guard.test.ts: the ban, user-delete and role-downgrade fail-closed cases now assert{ code: 'SERVICE_UNAVAILABLE', status: 503 }. Their message assertions (the reason, "could not be verified") are unchanged.maxScanoverflow keeps{ code: 'PERMISSION_DENIED', status: 403 };judgeOrganizationDeletewith a failed inner read answers the read fault once (oneSERVICE_UNAVAILABLE:prefix); andorganization/deleteof the Default Organization through the real guard answers 503SERVICE_UNAVAILABLEwith nothing attempted. Its CONTROL, with the read back, is the guard's verdict on the same door: 403PERMISSION_DENIED.git grepfor the guard's fail-closed prose and for tests driving the guard through a failing read found pins only inplugin-auth: the 5 flipped above. No doc or ADR states the read-fault code.Verification
main's three source files (git restore --source=d12e650on those files only, then restored from HEAD withgit diff HEADempty): 7 failed / 137 passed across the four touched test files, every new or flipped pin red. Adapter alone reverted: the HTTP pin readsexpected { status: 500, code: null }.@objectstack/plugin-authfull suite at 3aee198: 187/187 files, 3528 passed, 10 skipped.pnpm --filter @objectstack/plugin-auth typecheck(src, examples, test-typecheck): exit 0.dispatch-gates.mjs --commands, reconciled with--ran): 69 derived, 67 run with exit 0, 0 unrun. Two came back NOT MEASURED locally and are left to CI:check-plugin-teardown-shape --self-testneeds history this shallow clone does not have, andcheck:dual-build-cjs-loadsneeds a full monorepo build.Acceptance notes
{ status: 503, code: 'SERVICE_UNAVAILABLE' }the 503 answer at the better-auth door, where it used to be an empty 500. That is the same answermapDataErrorgives on/data. The other in-repo producers of that pair found bygit grep(objectql/src/action-activation.ts,service-automation/src/engine.ts) are action and flow paths, not adapter writes; this was read, not measured.🤖 Generated with Claude Code
https://claude.ai/code/session_01CuJfNjvts78GKesupEdgbz