Skip to content

fix(plugin-auth): the organization, invitation-placement and phone-OTP hook refusals carry their ADR-0112 codes - #24350

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-24243-plugin-auth-refusal-codes
Oct 18, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-24243-plugin-auth-refusal-codes

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24243

Clause-②: no

Eight APIError throws from plugin-auth's better-auth hooks answered with message alone, so a client could only show or pattern-match prose. Each now carries the ADR-0112 code in the spec seat's table (card comment 6248819342, the codes landed on main with PR #24294), typed at the throw: RegisteredErrorCode for the four registered codes and StandardErrorCode for the four standard ones, as the invitation role cap's constants are typed, so a removed or misspelled ledger row fails tsc. Every refusal keeps its HTTP status.

position at ec749fbb4 door status code
:1343 daily SMS quota /auth/phone-number/send-otp, /auth/phone-number/request-password-reset 429 RATE_LIMIT_EXCEEDED (standard)
:5761 per-number OTP limit the same two 429 RATE_LIMIT_EXCEEDED (the same constant)
:1358 slug guard read fault /auth/organization/update 503 SERVICE_UNAVAILABLE (standard)
:3464 reserved __global__ id or slug /auth/organization/create 400 VALIDATION_ERROR (standard)
:3472 no organization wall /auth/organization/create 403 ORGANIZATION_CREATION_DISABLED
:3540 no placement runtime /auth/organization/invite-member 403 INVITE_PLACEMENT_UNSUPPORTED
:3558 placement outside the issuer's scope /auth/organization/invite-member 403 INVITE_PLACEMENT_OUTSIDE_ISSUER_SCOPE
:3668 slug change with an active environment /auth/organization/update 403 ORGANIZATION_SLUG_LOCKED_BY_ACTIVE_ENVIRONMENTS

The three implementer notes

  1. The placement catch is split. It used to answer every throw out of assertIssuable with 403 and the thrown message. Now only the gate's own refusal (plugin-security's PermissionDeniedError, read structurally by its PERMISSION_DENIED code, because plugin-auth has no runtime dependency on plugin-security) answers 403 INVITE_PLACEMENT_OUTSIDE_ISSUER_SCOPE with the gate's message. Any other throw, such as the store fault resolveUserAuthzGrants raises as AuthzStoreUnavailableError, answers 503 SERVICE_UNAVAILABLE with its code. The failure rides cause and its message is not relayed. This is the one behaviour change: a store fault was a 403 scope refusal and is now a 503. No invitation row is written either way. The structural interface InvitationPlacementServiceLike gains a doc line that states this refusal contract.
  2. The :3668 message says only what the guard reads. It no longer says environments "still reference" the organization or tells the caller to rename hostnames. The guard counts sys_environment rows by organization_id and status, and reads no hostname. The new text: "Cannot change the organization slug while the organization has N active environment(s), meaning any environment that is neither archived nor failed. Archive those environments first, or keep the current slug." The read-fault message at :1358 had the same "still references" wording and now says "whether this organization has an active environment was never determined".
  3. The comment above SMS_QUOTA_EXCEEDED_CODE no longer calls 'TOO_MANY_REQUESTS' an ADR-0112 error code. It is the service envelope's token (better-call's status name), only matched on the way in. In the same edit, the quota wall's status is spelled 'TOO_MANY_REQUESTS' directly rather than read off that constant, so both walls now construct the identical status and code. A sentence in the role cap's read-fault helper that this branch made false ("Unlike the slug guard's body, this one carries the code too") now reads "Like".

The console's map row for ORGANIZATION_CREATION_DISABLED is objectui's and is not in this PR; the seat files it at landing.

One docs correction, outside the claim's file surface

content/docs/protocol/kernel/error-handling.mdx had two statements this branch makes false: the QUOTA_EXCEEDED entry said the SMS daily quota answers TOO_MANY_REQUESTS, and the RATE_LIMIT_EXCEEDED entry said that code's envelope is never flat. Both are corrected: the phone-OTP walls answer 429 RATE_LIMIT_EXCEEDED in better-auth's flat code and message body, with no Retry-After header (measured: the constructed APIError carries headers {}) and no details. This page is not in the claim's declared surface; the report names it.

Tests

  • The enumeration pin, packages/plugins/plugin-auth/src/api-error-code.pin.test.ts (a test, not a CI gate). It parses every non-test source file under plugin-auth/src, finds every new APIError(...), and requires a top-level code in the body, or a body destructured from judgeHeldSession(...) in the same function (the held-session judge's envelope, whose code is nested under error). A comment-free textual count must equal the walked count per file, a floor (30 sites in 4 files) stops a walk that reads nothing, and a fixture battery shows the classifier both reports the code-less shapes and accepts the coded ones.
  • Control against the card's table (one-time): with auth-manager.ts swapped to its ec749fbb4 blob, the pin reports exactly auth-manager.ts:1343, :1358, :3464, :3472, :3540, :3558, :3668, :5761, the eight positions of the corrected enumeration (6245494426), and does not flag the two relayed judgeHeldSession bodies. Restored to the HEAD blob, git diff HEAD empty.
  • Ablation, one stamp (scripts/ablation-replace.mjs, WRAP mode): deleting the :3668 stamp turns the pin red at that one site (auth-manager.ts:3769 in HEAD numbering); restored, blob equal to HEAD, git diff HEAD empty.
  • Ablation, the behaviour change: deleting the catch's discrimination turns the store-fault pin red (expected 403 to be 503); restored the same way. auth-manager.test.ts imports ./auth-manager from source, so no build sits between the mutation and the reading.
  • Wire pins, reached on a stock showcase boot (the production single posture):
    • :3472: organization-create-refusal-codes.dogfood.test.ts, a new organization answers 403 ORGANIZATION_CREATION_DISABLED, no row; it first asserts /auth/config publishes multiOrgEnabled: false.
    • :3464: the same file, the __global__ slug answers 400 VALIDATION_ERROR ahead of the posture refusal, no row.
    • :3558: delegated-admin-invite.dogfood.test.ts, a delegated_admin with no adminScope who requests placement is refused by the real gate with 403 INVITE_PLACEMENT_OUTSIDE_ISSUER_SCOPE, no row.
  • Unit pins, each with its reason in the test:
    • :1343 and :5761: bootStack mounts no phone-number plugin (OS_AUTH_PHONE_NUMBER_ENABLED is read only by objectstack serve). The existing "both walls present the SAME outward shape" pin now asserts the one shared RATE_LIMIT_EXCEEDED.
    • :3668 and :1358: sys_environment is cloud-provided and no composition in this repository registers it, so the guard does not apply on a stock boot.
    • :3540: bootStack always boots plugin-security, which registers the placement service.
    • The placement store fault: it needs a store that cannot answer.
    • :3472 is also pinned in-package through the real better-auth route (org-create-posture-gate.test.ts, the single and degraded postures), and :3464 in auth-manager.test.ts.

The gate-refusal unit pin uses a structural stand-in for PermissionDeniedError. Importing plugin-security there measured roughly 1 to 2 s more import time on auth-manager.test.ts, and the real class reaches the catch on the wire in the dogfood pin above.

Local readings

All on 4fc739dfb (this branch merged with main at 51ad8e4fb), the head this PR opens with:

  • pnpm --filter @objectstack/plugin-auth typecheck: exit 0. pnpm --filter @objectstack/dogfood typecheck: exit 0.
  • plugin-auth's suite (vitest run): 161 files passed, 3126 tests passed, 10 skipped.
  • dogfood, the two touched files: 2 files, 9 tests passed.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands derives 99 commands for this diff, and all 99 exited 0 on this head. Two first refused with PREREQUISITE NOT MET (exit 3) because packages outside this diff had no dist/: check:skill-examples (needs client-react) and check:dual-build-cjs-loads (seven unbuilt packages). After building those, both re-ran with exit 0, and so did the four other dist-reading gates, re-run on a quiet tree. --ran reconciliation: 99 derived, 99 run, 0 NOT-MEASURED. Outside the derivation, spec check:error-code-provenance and spec check:generated (spec moved on main's side of the merge) both exit 0.
  • The table control and the stamp ablation ran on cd27c533a, the catch ablation on 00cafa8b2. The merge brought four plugin-auth source files from main, which the enumeration pin also reads; run alone on this head it passes 7 of 7.
  • Not run locally, and left to CI: the repo-wide pnpm lint, test:repo, and the full dogfood suite.

Acceptance notes

  • packages/services/service-sms/src/sms-daily-quota.ts (the comment above its SMS_QUOTA_EXCEEDED_CODE) says the envelope token is "the same code the per-number guard raises in plugin-auth (APIError('TOO_MANY_REQUESTS'))". After this PR that spelling is the shared status name, and the code both walls carry is RATE_LIMIT_EXCEEDED. Comment only, outside this card's surface, not touched. Carrier: none.
  • The comment block above organizationHooks in auth-manager.ts still says "any lookup error is treated as 'no envs to protect'". Since the slug guard fails closed on a read fault, that is not what the code does. Comment only, not touched. Carrier: none.
  • The contract review's item 8 (beforeUpdateOrganization does not reserve __global__ while beforeCreateOrganization does) is unchanged here; the review escalated it to the seat.
  • Exclusions held: no packages/spec edit, none of the four files PR fix(cli,runtime): the 17 → 18 ownership ceremony leaves no row without an organization on a single-posture database, its prescribed boot writes nothing before the guard refuses, and an unverified ceremony is not served #24325 touches, nothing in objectui.

Generated by Claude Code

…P hook refusals carry their ADR-0112 codes

Eight APIError throws in auth-manager.ts answered with a message and no
code. Each now carries the code the spec seat's table names, typed
RegisteredErrorCode or StandardErrorCode at the throw:

- both phone-OTP send walls (daily SMS quota, per-number limit): one
  shared RATE_LIMIT_EXCEEDED, so the two walls stay indistinguishable;
- the slug guard's read fault: SERVICE_UNAVAILABLE;
- the reserved __global__ organization id or slug: VALIDATION_ERROR;
- organization creation on a posture with no wall:
  ORGANIZATION_CREATION_DISABLED;
- placement intent with no placement runtime: INVITE_PLACEMENT_UNSUPPORTED;
- the placement gate's own refusal: INVITE_PLACEMENT_OUTSIDE_ISSUER_SCOPE;
  any other throw out of assertIssuable (a store fault) now answers 503
  SERVICE_UNAVAILABLE instead of a 403 scope refusal;
- a slug change while the organization has an active environment:
  ORGANIZATION_SLUG_LOCKED_BY_ACTIVE_ENVIRONMENTS, with its message
  corrected to what the guard reads (no hostname).

The comment above SMS_QUOTA_EXCEEDED_CODE no longer calls the service's
envelope token an ADR-0112 error code.

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
…rries a code

Parses every non-test source file under src/, finds every APIError
construction and requires a top-level code in the body, or a body relayed
from judgeHeldSession's ADR-0112 envelope. A comment-free textual count and
a fixture battery keep the walk from passing on a shape it cannot see.

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
…e placement catch's two arms

- placement: the gate's PermissionDeniedError answers 403
  INVITE_PLACEMENT_OUTSIDE_ISSUER_SCOPE with its message; a store fault
  (AuthzStoreUnavailableError) answers 503 SERVICE_UNAVAILABLE with the
  failure on cause; no placement runtime answers INVITE_PLACEMENT_UNSUPPORTED;
- the reserved __global__ id or slug answers 400 VALIDATION_ERROR, also on a
  single-organization posture;
- both phone-OTP walls answer the same RATE_LIMIT_EXCEEDED;
- the slug guard's refusal and read fault carry their codes;
- organization/create on a posture with no wall answers
  ORGANIZATION_CREATION_DISABLED through the real better-auth route.

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
…als answer their codes on the wire

- a stock single-posture showcase refuses organization/create with 403
  ORGANIZATION_CREATION_DISABLED, and the reserved __global__ slug with 400
  VALIDATION_ERROR ahead of it, writing no row;
- a delegated_admin with no adminScope requesting placement is refused by the
  real gate with 403 INVITE_PLACEMENT_OUTSIDE_ISSUER_SCOPE, writing no row.

The unit pin of the gate's refusal now uses a structural stand-in for
PermissionDeniedError, which the dogfood pin covers with the real class.
Adds the plugin-auth patch changeset.

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
…EDED, flat

This branch stamps RATE_LIMIT_EXCEEDED on both phone-OTP send walls, which
made two statements on the page false: that the SMS daily quota answers
TOO_MANY_REQUESTS (it is better-call's status name, never the code), and
that RATE_LIMIT_EXCEEDED is never flat. The walls answer better-auth's flat
code-and-message body with no Retry-After header and no details.

The changeset drops the SMS invitation from its list: the import route
catches that send's failure into a per-row INVITE_SMS_FAILED, so the 429
code never reaches its wire.

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 18, 2026
@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 23 documentable anchor(s).

15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via RATE_LIMIT_EXCEEDED (literal, a string literal in PHONE_OTP_SEND_REFUSAL_CODE), SERVICE_UNAVAILABLE (literal, a string literal in INVITE_PLACEMENT_READ_FAULT_CODE; a string literal in SLUG_GUARD_READ_FAULT_CODE; a string literal in invitationPlacementReadFaultApiError), VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))
  • content/docs/api/error-catalog.mdx (via RATE_LIMIT_EXCEEDED (literal, a string literal in PHONE_OTP_SEND_REFUSAL_CODE), SERVICE_UNAVAILABLE (literal, a string literal in INVITE_PLACEMENT_READ_FAULT_CODE; a string literal in SLUG_GUARD_READ_FAULT_CODE; a string literal in invitationPlacementReadFaultApiError), VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))
  • content/docs/api/error-handling-client.mdx (via RATE_LIMIT_EXCEEDED (literal, a string literal in PHONE_OTP_SEND_REFUSAL_CODE), VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))
  • content/docs/api/error-handling-server.mdx (via VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))
  • content/docs/api/index.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in INVITE_PLACEMENT_READ_FAULT_CODE; a string literal in SLUG_GUARD_READ_FAULT_CODE; a string literal in invitationPlacementReadFaultApiError))
  • content/docs/automation/jobs.mdx (via VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))
  • content/docs/automation/webhooks.mdx (via VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))
  • content/docs/data-modeling/drivers.mdx (via VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))
  • content/docs/permissions/authorization.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in INVITE_PLACEMENT_READ_FAULT_CODE; a string literal in SLUG_GUARD_READ_FAULT_CODE; a string literal in invitationPlacementReadFaultApiError))
  • content/docs/protocol/kernel/error-handling.mdx (via RATE_LIMIT_EXCEEDED (literal, a string literal in PHONE_OTP_SEND_REFUSAL_CODE), SERVICE_UNAVAILABLE (literal, a string literal in INVITE_PLACEMENT_READ_FAULT_CODE; a string literal in SLUG_GUARD_READ_FAULT_CODE; a string literal in invitationPlacementReadFaultApiError), VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))
  • content/docs/protocol/kernel/http-protocol.mdx (via RATE_LIMIT_EXCEEDED (literal, a string literal in PHONE_OTP_SEND_REFUSAL_CODE))
  • content/docs/protocol/kernel/realtime-protocol.mdx (via RATE_LIMIT_EXCEEDED (literal, a string literal in PHONE_OTP_SEND_REFUSAL_CODE))
  • content/docs/protocol/objectql/types.mdx (via VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))
  • content/docs/ui/forms.mdx (via VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))
  • content/docs/ui/print-pages.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in INVITE_PLACEMENT_READ_FAULT_CODE; a string literal in SLUG_GUARD_READ_FAULT_CODE; a string literal in invitationPlacementReadFaultApiError), VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))

⛔ 7 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in INVITE_PLACEMENT_READ_FAULT_CODE; a string literal in SLUG_GUARD_READ_FAULT_CODE; a string literal in invitationPlacementReadFaultApiError))
  • content/docs/releases/v17/17-1.mdx (via VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))
  • content/docs/releases/v17/17-3.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in INVITE_PLACEMENT_READ_FAULT_CODE; a string literal in SLUG_GUARD_READ_FAULT_CODE; a string literal in invitationPlacementReadFaultApiError))
  • content/docs/releases/v17/17-4.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in INVITE_PLACEMENT_READ_FAULT_CODE; a string literal in SLUG_GUARD_READ_FAULT_CODE; a string literal in invitationPlacementReadFaultApiError))
  • content/docs/releases/v17/17-5.mdx (via VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))
  • content/docs/releases/v17/17-6.mdx (via VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))
  • content/docs/releases/v17/17-7.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in INVITE_PLACEMENT_READ_FAULT_CODE; a string literal in SLUG_GUARD_READ_FAULT_CODE; a string literal in invitationPlacementReadFaultApiError), VALIDATION_ERROR (literal, a string literal in ORGANIZATION_RESERVED_IDENTIFIER_CODE))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: PERMISSION_DENIED (literal, 39 pages)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json dd72a047f83cf8e6b9826183bd4de304d4f16957 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0135bbfecaa297ad0deec697beef73beca176857 — the merge of head 4fc739dfbf28939a54b641025938f075e28d5534 into base dd72a047f83cf8e6b9826183bd4de304d4f16957, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0135bbfecaa297ad0deec697beef73beca176857 && git checkout 0135bbfecaa297ad0deec697beef73beca176857
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dd72a047f83cf8e6b9826183bd4de304d4f16957 4fc739dfbf28939a54b641025938f075e28d5534 && git checkout -B drift-repro dd72a047f83cf8e6b9826183bd4de304d4f16957 && git merge --no-ff 4fc739dfbf28939a54b641025938f075e28d5534

node scripts/docs-audit/affected-docs.mjs --json dd72a047f83cf8e6b9826183bd4de304d4f16957

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs dd72a047f83cf8e6b9826183bd4de304d4f16957 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:services seat 1 (#6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-18T15:28Z · head 4fc739dfbf. The seat's own review at TIER_DEFAULT: --tier read no path-derived mandate over the 9 paths, and no published schema or governed text is touched (the eight codes are already on main, 0b44840df). It lands through the merge queue once every check on this head is green.

  • Shape: a draft PR to main. Its first line is Fixes #24243, and that is the only closing keyword in the body. Clause-②: no is at a line start in the body and in the changeset (@objectstack/plugin-auth patch). Card and PR assignee: marchtian. Not governed: check-governed-merges reads 0 of 9 paths, and 686 changed lines ≤ 3000.

  • The spec seat's table (6248819342), each stamp read by the seat in auth-manager.ts:

    • RATE_LIMIT_EXCEEDED on both phone-OTP walls, through ONE constant, PHONE_OTP_SEND_REFUSAL_CODE, so feat(sms): 短信全局/每租户日发送配额(成本总量闸) #2814's two walls stay indistinguishable.
    • SERVICE_UNAVAILABLE on the slug guard's read fault.
    • VALIDATION_ERROR on the reserved __global__ identifier.
    • ORGANIZATION_CREATION_DISABLED, INVITE_PLACEMENT_UNSUPPORTED, INVITE_PLACEMENT_OUTSIDE_ISSUER_SCOPE and ORGANIZATION_SLUG_LOCKED_BY_ACTIVE_ENVIRONMENTS on their throws.

    Each is a module constant: the four registered codes are typed RegisteredErrorCode and the four standard ones StandardErrorCode. A removed ledger row fails tsc. Every status is unchanged.

  • The three implementer notes, met:

    1. The placement catch splits on the gate's own verdict. isInvitationPlacementGateRefusal reads code === 'PERMISSION_DENIED' structurally. The seat checked both sides on main: plugin-security's PermissionDeniedError declares code = 'PERMISSION_DENIED' (errors.ts:42), and the store fault AuthzStoreUnavailableError declares code = 'SERVICE_UNAVAILABLE' (core/src/security/authz-store-unavailable.ts:96, :149). So a store fault cannot pass as a refusal. It now answers 503 with its code and cause, and its message is not relayed. This is the PR's one behaviour change, and it narrows a false refusal.
    2. The :3668 message no longer says environments "still reference" the organization or prescribes renaming hostnames, and the slug guard's read-fault message lost the same wording. The comment above SMS_QUOTA_EXCEEDED_CODE no longer calls it an ADR-0112 code.
    3. The console's map row is objectui's, and the seat files it at landing. It is not in this PR.
  • Pins:

    • api-error-code.pin.test.ts, a test and not a gate, walks every new APIError( in plugin-auth/src and requires a code, reading the two judgeHeldSession relays as coded.
    • Its control: with auth-manager.ts swapped to the base blob, the pin lists exactly the eight positions of 6245494426.
    • Wire pins on a stock showcase boot reach the reserved identifier, creation disabled and the placement scope refusal. The OTP walls, the slug guard's two throws and placement-unsupported are unit-pinned, each with a reason.
    • Two ablations, each restored blob-equal to HEAD: dropping the :3668 stamp turns the pin red at that one site, and dropping the catch's guard turns the store-fault pin from 503 to 403.
  • Gates on this head: 36 checks succeeded and 5 were skipped by design (Console Pin Gate, Dogfood Regression Gate, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance: the roster set; Build Docs ran, since the docs page is in the diff, and passed). There were no failures, and the required contexts are green. Locally the dev derived 99 families on this head, ran all 99, and all exited 0 (--ran: 0 NOT-MEASURED, 0 UNRUN). Two first exited 3 on an unbuilt prerequisite and passed after it was built.

  • Accepted deviations:

  • Out-of-scope findings, with their carriers: the console's ORGANIZATION_CREATION_DISABLED → workspace.multiOrgDisabled map row (the spec seat's note 3). The seat files it in objectstack-ai/objectui at landing.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 18, 2026 15:30
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 18, 2026 15:30
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 18, 2026
Merged via the queue into main with commit 83dfbc0 Oct 18, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24243-plugin-auth-refusal-codes branch October 18, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-auth: seven refusals and one outage thrown from better-auth hooks carry no ADR-0112 code, the same gap #23945 closes for the invitation role cap

2 participants