Skip to content

feat(spec): the remaining getSession readers read the declared AuthSessionApi result (user.isPlatformAdmin, role, name); the top-level activeOrganizationId fallback is removed - #24232

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24222-get-session-readers-declared
Oct 18, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24222-get-session-readers-declared

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24222

Clause-②: yes

What

AuthSessionApi.getSession (packages/spec/src/contracts/auth-service.ts) says its result declares exactly the fields its in-process readers read. After #24196 (PR #24220), the other readers still read the session through any or a cast, and two of them read a top-level activeOrganizationId that the contract does not declare. This PR does both halves in one diff, as triage routed it (6241789224): the contract, and the readers.

Contract: three optional user members, each with its reader named

Declared Type Reader Why this type
user.isPlatformAdmin boolean judgePlatformAdmin (@objectstack/plugin-auth, via isPlatformAdminUser) plugin-auth's own customSession derives it as a boolean (auth-manager.ts, let platformAdmin = false)
user.role string | null same better-auth's admin plugin schema declares it type: "string", required: false, so a row may hold null (the impersonatedBy precedent)
user.name string dispatcher-plugin's concrete AI-route req.user resolver (@objectstack/runtime), the actor's display name buildActorUser's displayName?: string. The card did not list this read. Removing the any from resolveSessionData turned it into a compile error (TS2339 on user.name), and that is how it was found

The docblock gains a [#24222] paragraph: the remaining readers read the declared result, and none reads a top-level activeOrganizationId.

Readers: each one reads the declared result (route a). Types only, runtime behaviour unchanged

Each site gets a module-local SessionRead alias, Awaited<ReturnType<NonNullable<AuthSessionApi['getSession']>>>, the spelling #24196 used. Each read gets a one-line [#24222] comment.

  • @objectstack/plugin-auth: the four platform-admin mounts in auth-plugin.ts (toggle-disabled, gateAdmin, unlock-user, has-permission). The two (authApi as any).getSession(…) calls are un-cast, and all four now read into SessionRead | null. authApi is better-auth's own Auth['api'], so better-auth's concrete session type now has to be assignable to the declaration. It is: a probe assigning that read to number failed with { session: { id: string; …; token: string; … }; user: … } | null is not assignable to type 'number', so the type is concrete, not any. In has-permission, the { user?: { id?: unknown } } cast is gone. judgePlatformAdmin reads (session as SessionRead | null)?.user instead of a Record<string, unknown> cast.
  • @objectstack/rest (rest-server.ts): the getSession closure handed to resolveAuthzContext returns Promise<SessionRead | null>, and gatedSession becomes SessionRead | null instead of any.
  • @objectstack/runtime:
    • dispatcher-plugin.ts readGatedSessionUser: session: any is dropped. api there is already the contract's handle.
    • security/resolve-session-principal.ts resolveSessionData returns Promise<SessionRead | null> instead of Promise<any | undefined>. It reads authService.api / getApi() through IAuthService instead of three as any casts, with the same branches. It is module-internal: index.ts does not export it.
    • http-dispatcher.ts: both reads (enforceAuthGate, enforceProjectMembership) are typed.
  • @objectstack/cloud-connection: sessionFromAuthService and ResolvedSession.readFrom.sessionData (unknown becomes SessionRead | null) in cloud-connection-plugin.ts. In marketplace-install-local-plugin.ts, resolveActiveOrgId and the install-local getSession bridge.
  • @objectstack/plugin-webhooks: resolveSession returns Promise<SessionRead | null>.
  • @objectstack/service-storage: buildGetSession returns Promise<SessionRead | null>. The session: any in the upload-stamp resolver and in the posture reader is gone.

The top-level activeOrganizationId operand is removed at both sites (Prime Directive #12)

Producers measured: the only in-tree auth service is plugin-auth's AuthManager (auth-plugin.ts, ctx.registerService('auth', this.authManager)). Its read is better-auth's getSession, and its customSession returns { user: { ...user, positions, isPlatformAdmin, authGate? }, session } (auth-manager.ts). No production code produces a top-level activeOrganizationId. The one test fixture that did was sys-file-organization-stamping.test.ts, "accepts the flattened shape a host may hand back directly", which pinned the fallback itself and named no producer.

  • storage-service-plugin.ts: session?.session?.activeOrganizationId ?? session?.activeOrganizationId becomes session?.session?.activeOrganizationId. The docblock that described the fallback is corrected.
  • marketplace-install-local-plugin.ts resolveActiveOrgId: … ?? session?.activeOrganizationId ?? null becomes session?.session?.activeOrganizationId ?? null.
  • Pins: the storage fixture is flipped to "does not read a top-level activeOrganizationId" (no stamp). marketplace-install-local-no-active-organization.test.ts gains a topLevelOrg case: walled, with only a top-level activeOrganizationId, the reseed answers 403 PERMISSION_DENIED naming the missing active organization, and no seed run starts.

The undefined-direction pin on judgePlatformAdmin

platform-admin-gate.undefined-direction.test.ts: a signed-in user (with an id, so the 401 cannot mask the 403) is refused 403 PERMISSION_DENIED, and isPlatformAdminUser answers false, in each of these cases: both fields absent; both explicitly undefined; role: null; isPlatformAdmin: false; isPlatformAdmin: 'true' (truthy is not true); role: 'Admin'. Controls: isPlatformAdmin: true and role: 'admin' still admit, and the admitted actor is { id, email }.

Changeset

.changeset/24222-get-session-readers-declared.md: @objectstack/spec minor (a published contract widens; Clause-②: yes (widening) in its body). @objectstack/plugin-auth, rest, runtime, cloud-connection, plugin-webhooks and service-storage are patch: type-only reader edits, plus the fallback removal, which is a fix and carries its one-line remedy for an auth service that answered the flattened shape. Not breaking, so no ADR-0087 marker is owed.

Verification (head 05c1551c, after merging origin/main 9040945d, which carries PR #24224)

  • Build: the dependency closure of the six reader packages, plus @objectstack/spec, rest and plugin-auth again after the merge: os-verify-lock VERDICT command-exit 0.

  • Typecheck: pnpm --filter … typecheck, exit 0 for spec, plugin-auth, rest, runtime, cloud-connection, plugin-webhooks and service-storage. rest, plugin-auth and runtime were re-run after the merge: exit 0.

  • Tests:

    Package Script Result
    spec test 691 files / 20974 passed
    spec test:repo 67 / 1115
    rest test 294 / 6027, post-merge
    rest test:repo 5 / 258
    runtime test 409 / 6280, post-merge
    runtime test:repo 3 / 808
    plugin-auth test 159 / 3105, post-merge
    cloud-connection test 51 / 602
    plugin-webhooks test 19 / 201
    service-storage test 59 / 1029

    Every row exited 0.

  • Reverse verification (types): a user.positions read added to judgePlatformAdmin fails tsc with TS2339 against { id?; email?; name?; authGate?; isPlatformAdmin?: boolean; role?: string | null }, so the rebuilt dist declaration is the one being read. The organic red was the user.name TS2339 above, before name was declared. Both mutations ran under scripts/ablation-replace.mjs, which confirmed the blob was restored to HEAD and git diff HEAD was empty.

  • Ablation (operand removal): with ?? (session as any)?.activeOrganizationId put back, the storage pin fails (1 failed / 11 passed) and the install-local pin fails (1 failed / 7 passed). The tree was restored to HEAD each time, per the tool's blob check.

  • Generated artifacts: pnpm --filter @objectstack/spec check:generated exit 0, before and after the merge. No export is added or removed, and no Zod schema is touched.

  • Gates: node scripts/pm/dispatch-gates.mjs --commands derives 93 commands, and the same list again on the merged head. Run on head 05c1551c: 92 exit 0. One is NOT MEASURED: node scripts/check-plugin-teardown-shape.mjs --self-test exits 3 because its fixture needs git fetch --unshallow (a shallow clone); Lint & Repo Gates carries it. --ran reconciliation: "93 derived famil(ies) accounted for — 92 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".

Acceptance notes

🤖 Generated with Claude Code

https://claude.ai/code/session_018fKVdiy1mpvMbYt2ryCsLz


Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 7 package(s): @objectstack/cloud-connection, @objectstack/plugin-auth, @objectstack/plugin-webhooks, @objectstack/rest, @objectstack/runtime, @objectstack/service-storage, @objectstack/spec, touching 18 documentable anchor(s).

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

  • content/docs/api/environment-routing.mdx (via enforceProjectMembership (symbol, a method of class HttpDispatcher))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 155 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 6e43b7bc3f8cec05e65e200f28c9e6074c585228 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5b3a41f29167da67e9c28ec3fb623d3282fde317 — the merge of head 05c1551ce7b76609b6b9923c5a890a9aa2858afa into base 6e43b7bc3f8cec05e65e200f28c9e6074c585228, 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 5b3a41f29167da67e9c28ec3fb623d3282fde317 && git checkout 5b3a41f29167da67e9c28ec3fb623d3282fde317
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6e43b7bc3f8cec05e65e200f28c9e6074c585228 05c1551ce7b76609b6b9923c5a890a9aa2858afa && git checkout -B drift-repro 6e43b7bc3f8cec05e65e200f28c9e6074c585228 && git merge --no-ff 05c1551ce7b76609b6b9923c5a890a9aa2858afa

node scripts/docs-audit/affected-docs.mjs --json 6e43b7bc3f8cec05e65e200f28c9e6074c585228

⚠️ 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 6e43b7bc3f8cec05e65e200f28c9e6074c585228 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 05c1551ce7b76609b6b9923c5a890a9aa2858afa
Local-runs: none
Scope: packages/spec/src/contracts/auth-service.ts + packages/plugins/plugin-auth/src/platform-admin-gate.ts, packages/plugins/plugin-auth/src/auth-plugin.ts, packages/plugins/plugin-auth/src/platform-admin-gate.undefined-direction.test.ts, packages/rest/src/rest-server.ts, packages/runtime/src/dispatcher-plugin.ts, packages/runtime/src/http-dispatcher.ts, packages/runtime/src/security/resolve-session-principal.ts, packages/cloud-connection/src/cloud-connection-plugin.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/cloud-connection/src/marketplace-install-local-no-active-organization.test.ts, packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts, packages/services/service-storage/src/storage-service-plugin.ts, packages/services/service-storage/src/sys-file-organization-stamping.test.ts, .changeset/24222-get-session-readers-declared.md, packages/plugins/plugin-auth/src/auth-manager.ts, packages/runtime/src/security/actor-user.ts, packages/plugins/plugin-hono-server/src/current-user-endpoints.ts, packages/plugins/plugin-sharing/src/sharing-plugin.ts, packages/services/service-datasource/src/admin-routes.ts, packages/services/service-settings/src/settings-service-plugin.ts, packages/runtime/src/security/resolve-execution-context.ts

PR #24232 for card #24222, isolated at-tier review, read-only. The head is a merge of origin/main 9040945d into the dev branch (two non-merge commits, 3f0e6ed29 and 6f351e9bf); the net diff against that merge base is 15 files, +231/-44, and matches GitHub's PR diff hunk for hunk. The head had not moved when read; origin/main has since advanced one commit (7715d049e, PR #24234, packages/cli only, no overlap). Inputs: the card body and its three comments (triage 6241789224, claim 6242034533, dev report 6243590371), the PR body, file list and diff, the head's 41 check-runs, and the cited code read at the head. The scope above is the contract file (the one reviewFace hit) plus every file its hunks make a claim about: the new docblock paragraph claims every other in-process reader, so the whole diff is in scope, and the last seven paths are read-only producers and readers consulted to judge that claim (the customSession producer, the name reader's consumer, and the five getSession call sites the card never named). No remainder is left to a lane ACCEPT.

① Derived judgments

Public surface, one change: AuthSessionApi.getSession's result gains three optional user members. Each one judged against the reader named on it, read at the head:

  • user.isPlatformAdmin: boolean (optional) — right. The producer is plugin-auth's customSession (auth-manager.ts): let platformAdmin = false, then platformAdmin = grants.posture === 'PLATFORM_ADMIN' inside a try whose catch leaves it false, returned as user: { ...user, positions, isPlatformAdmin: platformAdmin, ... }. A boolean, false when not held, as the docblock says. The reader is judgePlatformAdmin through isPlatformAdminUser (u.isPlatformAdmin === true), which admits on true alone.
  • user.role: string | null (optional) — right. better-auth 1.7.3 (the version plugin-auth pins) declares the admin plugin's column role: { type: 'string', required: false, input: false } and types it role?: string | undefined on UserWithRole; sys_user is managedBy: 'better-auth' and declares no role field of its own, so the vendor schema is the authority on the column and required: false is what makes the null arm real. The contract's string | null is wider than the implementer's own type, the sound direction for an implementer, and the same call the spec(contracts): AuthSessionApi.getSession claims to declare every field its readers read, but its result declares three; readers cast to read session.id, token, user.email, the auth gate and impersonatedBy #24196 review (6240835611) made for impersonatedBy. The reader narrows with u.role === 'admin', so null and undefined both refuse.
  • user.name: string (optional) — right. Read at dispatcher-plugin.ts:2322, displayName: sessionData?.user?.name, into buildActorUser (actor-user.ts:188, displayName?: string), with sessionData from resolveSessionData, which this PR types. The read existed on main under any; removing the any made it TS2339, which is how it surfaced. The card did not name it, but the dispatch's file surface names dispatcher-plugin.ts as a reader, and the hold-off is "any field no reader reads", which this is not. better-auth's core User.name is a required string, so the optional declaration is wider than the implementer. Decided as item 1 of ③.

Nothing undeclared is read by any typed reader. Every read at the head, per site: plugin-auth's four mounts read user.id, user.email, user.authGate (through judgeHeldSession to evaluateAuthGate), user.isPlatformAdmin and user.role; has-permission reads user.id and isPlatformAdminUser(user), its former { user?: { id?: unknown } } cast gone. REST reads gatedSession?.user into normalizeAuthGate, and its closure feeds resolveAuthzContext (declared reads, #24196). Runtime: readGatedSessionUser returns session?.user as unknown; resolveSessionPrincipalId reads user.id and session.userId; the AI-route resolver reads user.id, session.userId, user.name, user.email, session.activeOrganizationId; enforceAuthGate reads session?.user into evaluateAuthGate; enforceProjectMembership reads user.id, session.userId, session.activeOrganizationId. Cloud-connection reads user.id and user (posture), and hands readFrom.sessionData back as a getSession thunk to resolveAuthzContext (:435); install-local's resolveActiveOrgId reads session.activeOrganizationId and its bridge closure feeds resolveAuthzContext. Webhooks reads user, user.id, session.activeOrganizationId (:547, :551, :580). Storage reads user.id and session.activeOrganizationId (upload stamp), session?.user (gate reader), and its closure feeds resolveAuthzContext (file-read authorizer). All declared.

The docblock's new sentence is universal ("the other in-process readers read the result as declared here too; none reads a top-level activeOrganizationId"), so it was judged against the full census of inProcessSessionReadInput call sites at the head (20 non-test sites), not the card's list. Five sites the card never named: plugin-hono-server/current-user-endpoints.ts:477, plugin-sharing/sharing-plugin.ts:948 and service-datasource/admin-routes.ts:223 bind api through IAuthService or AuthSessionApi, so their result is already the declared type (hono reads user.id and hands the session to resolveAuthzContext; the other two read no field). service-settings/settings-service-plugin.ts:308 and runtime/security/resolve-execution-context.ts:183 still build api as any, but each is a pass-through closure that reads no field and hands the result to resolveAuthzContext, whose reads are declared. The sentence therefore holds for what it claims, field reads; the two any handles are a handle-typing matter held out by the dispatch's hold-offs, not a reader outside the contract. The three other spreads of the helper (createOAuthClient, addMember, setPassword) are not session reads. After the two removals, every remaining session?.activeOrganizationId read in the tree is off better-auth's session ROW inside plugin-auth's own hooks and audit (auth-manager.ts:1387, :7971, auth-session-audit.ts:141, :165), never off a getSession result. True as written.

The vendor binding at the four plugin-auth mounts: AuthManager.getApi() answers a Promise of better-auth's own Auth['api'] (options generic any), and each mount now assigns that read to SessionRead | null with no cast. CI's Type Check · workspace leg (success) proves the assignment holds; the dev's probe (assigning the read to number fails naming the vendor's concrete { session, user } | null type) is what shows the binding is not any, reported and consistent with the green legs, not reproduced here. What the type check cannot do, by the card's own design (every member optional, so an absent property is assignable): catch a vendor or customSession rename of isPlatformAdmin or role at compile time. The compensating control triage asked for is present: platform-admin-gate.undefined-direction.test.ts pins six refusing signals (both absent, both explicitly undefined, role: null, isPlatformAdmin: false, the string 'true', 'Admin') through isPlatformAdminUser (false) and judgePlatformAdmin (403 PERMISSION_DENIED, every user carrying an id so the 401 cannot mask the 403), plus two admitting controls. Right.

judgePlatformAdmin(session: unknown) keeps its unknown parameter and reads (session as SessionRead | null)?.user — right: the function is a published @objectstack/plugin-auth export, narrowing it would be an input narrowing inside a widening card, the same call the #24196 review made for ResolveAuthzInput.getSession. The cast is to the contract type, so what the judge reads is checked against the declaration, while the mounts bind the vendor type to it uncast. isPlatformAdminUser(sessionUser: unknown) stays for raw user rows (admin-impersonate-endpoint.ts) — right.

Accept sets: one runtime behaviour change, the removal of the top-level activeOrganizationId operand at storage-service-plugin.ts:1191 and marketplace-install-local-plugin.ts:3370. Producers: the only in-tree auth service is AuthManager; its read is better-auth's getSession under customSession, answering { user, session }; AuthSessionApi on main already declared no top-level member; the only in-tree claims for the flattened shape were a storage code comment and a test fixture naming no producer. Prime Directive #12 wants a bare fallback for an off-contract shape removed or declared as a conversion; the card's Done-when and triage both sanctioned removal. Pins: the storage fixture is flipped to { context: { isSystem: true } }, byte-identical to the declared no-active-organization case at :393 (ADR-0123 D1), so a flattened payload now lands exactly as "no active organization"; install-local gains the topLevelOrg case, 403 PERMISSION_DENIED naming the missing active organization with loadCalls empty. The dev's ablation (operand restored, both pins red) is reported, not reproduced. Right.

Every other hunk is type-only, checked branch by branch: resolveSessionData keeps the same api then getApi() sequence, now through IAuthService's declared members; has-permission keeps user?.id && isPlatformAdminUser(user); REST, both HttpDispatcher reads, webhooks and storage keep their try/catch shapes and the AuthzStoreUnavailableError relays unchanged.

Published surfaces of the six consumer packages: unchanged. SessionRead is module-local in all nine new copies (none exported); resolveSessionData's return type moves from a Promise of any to SessionRead | null, and it is module-internal (runtime/src/index.ts does not export it, the package exports only ., and its one caller is dispatcher-plugin.ts); resolveSession (webhooks) is private, buildGetSession (storage) and ResolvedSession (cloud-connection) are unexported; judgePlatformAdmin's signature is unchanged.

Generated artifacts: no Zod schema, no .describe(), no export added or removed, so no api-surface, json-schema, docs or authorable-surface artifact moves (the api-surface snapshot records that an export exists, not an interface's optional members); the dev reports check:generated exit 0 before and after the merge, and the Type Check · source gates leg that carries its reconcile is success. Docs: the drift check names content/docs/api/environment-routing.mdx through enforceProjectMembership, whose only change is the typed binding, and content/docs/kernel/contracts/auth-service.mdx names AuthSessionApi on its api? and getApi? lines without restating the result shape. Nothing owed.

One cosmetic note for the lane, not a contract matter: in webhook-outbox-plugin.ts the alias sits between two import statements (:20, ahead of import { SysWebhook }); Lint · ESLint is success.

Gate verdicts on the head, read off the check-runs (theirs, not this record's): all 41 completed. The required five are success: Lint & Repo Gates, TypeScript Type Check (all four legs success), Test Core (aggregate and all six shards), Build Core, Governed Surface Queue Guard (no governed path in the file list). Also success: Check Changeset, Check PR Size, Spec property liveness, the card-claim, single-writer and part-of checks, Dogfood Regression Gate (aggregate). The full-battery jobs (Console Pin Gate, Temporal Conformance, the dogfood shards, Build Docs) are skipped on a PR run by design.

② Semver level

.changeset/24222-get-session-readers-declared.md: @objectstack/spec: minor; @objectstack/plugin-auth, rest, runtime, cloud-connection, plugin-webhooks, service-storage: patch. Right. Spec widens a published contract's result type, which is minor, the floor a Clause-②: yes line demands. plugin-auth, rest, runtime and webhooks change no exported surface and no runtime behaviour, type-only reader edits in released packages, so patch, never skip-changeset. service-storage and cloud-connection carry the one behaviour change, and it is not a (narrowing) arm: the set that moved was never published. AuthSessionApi on main already declared no top-level member, IAuthService is the only declared producer handle, and the tolerance lived in a consumer ?? and a fixture naming no producer. Removing a bare fallback for an off-contract shape is Prime Directive #12's prescribed direction and the card's Done-when, so it is a fix in a released package, patch, and the changeset body carries the consumer-facing sentence (what an auth service that answered only the flattened shape now gets, and the one-line remedy: answer it on session.activeOrganizationId). Not breaking, so no ADR-0087 disposition marker is owed; check:adr-0087-registration rides Lint & Repo Gates, success. The Clause-② line reads yes (widening) on the changeset and yes on the PR body, the same declaration said two ways (clause2-line.mjs), both truthful: a declaration widening, no narrowing anywhere in the diff. Check Changeset is success.

③ Boundary flags

Every dev flag and open_questions entry from report 6243590371:

  1. open_question — keep user.name in this PR (A) or split it off and leave the reader on a cast (B)? Answered: A, as shipped. The card's Done-when is "it reads through the declared result, with each field it reads declared for it", and dispatcher-plugin.ts is on the dispatch's file surface; B would leave exactly the cast the card exists to remove. One optional member whose reader is measured (:2322) and whose producer is better-auth's core User.name, the least rename-prone field on the result. The recommendation stands.
  2. open_question — export one SessionRead alias from @objectstack/spec/contracts (B) or keep the module-local copies (A)? Answered: A. No external reader pulls for it, an export is an api-surface entry and a commitment to one public spelling, and every copy is derived from the contract, so none can drift from it. A correction for the record: the head carries eleven copies, not nine — two from spec(contracts): AuthSessionApi.getSession claims to declare every field its readers read, but its result declares three; readers cast to read session.id, token, user.email, the auth gate and impersonatedBy #24196 (core/security/resolve-authz-context.ts, service-print/print-routes.ts) and nine from this diff (platform-admin-gate.ts, auth-plugin.ts, rest-server.ts, http-dispatcher.ts, resolve-session-principal.ts, cloud-connection-plugin.ts, marketplace-install-local-plugin.ts, webhook-outbox-plugin.ts, storage-service-plugin.ts). An exported alias becomes its own card on a named external reader; not this one.
  3. deviation — the harness attribution reminder versus the model-free trailer pair: both non-merge commits (3f0e6ed29, 6f351e9bf) carry the model-free Claude-Session: and Co-authored-by: Claude pair the rule requires. Resolved.
  4. deviation — the PR was not subscribed to CI (os-dev contract): no review consequence; the gate verdicts are read off the head above.
  5. deviation — test:repo run for spec, rest and runtime beyond os-dev's default: no rule broken, no consequence.
  6. deviation — the first build's TS2339 on user.name: the organic red that produced the name declaration; it is in the diff and is item 1. Closed.
  7. node scripts/check-plugin-teardown-shape.mjs --self-test NOT MEASURED locally (exit 3, shallow clone): Lint & Repo Gates carries it and is success on the head. Closed.
  8. out_of_scope_finding, "carrier: none, not filed" — cloud-connection-plugin.ts:316 resolves the handle as getApi(), then authSvc.api, then authSvc itself. Confirmed at the head, and the only operand of that shape in the tree. It is a bare fallback treating the auth service as its own session handle, a shape IAuthService does not declare: the same Prime Directive Add comprehensive test suite for Zod schema validation #12 class as the operand this card removed, one level up (the handle rather than the result). Reach: inert against any IAuthService-conformant service (getApi or api resolves first), and against a bare service with no getSession it answers null, no session, so it admits nothing it should not; it only matters for a host registering a bare session api as auth, which no in-tree producer does. Held out of this card by the dispatch's hold-off ("any runtime behaviour change at a reader beyond the two top-level operands"), so correctly left. Escalated, not blocking: the seat files one follow-up card to remove the operand or name its producer, with a pin beside it. This reviewer's one write channel is this comment.

The card's other route ("typed by better-auth's own handle on purpose, and says so") was taken nowhere; every named reader took the declared result. No governed path in the file list; the PR is a draft with auto-merge unarmed; the head repository is the base repository, so this is a delivery, not a fork proposal. The dev is a cloud session (session_018fKVdiy1mpvMbYt2ryCsLz) distinct from the reviewing session below, so the independence pair holds.

Implemented-by: claude/issue-24222-get-session-readers-declared
Reviewed-by: session_01TyRhWbVbbQpQJTDbW8VahJ

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · PR #24232 @ 05c1551ce7 (#24222) · domain:spec seat 1 · os-project-manager · session session_01TyRhWbVbbQpQJTDbW8VahJ · 2026-10-18T01:15Z

Reviewer of record. Every item below was checked against GitHub and the code, not against the report (6243590371 on #24222).

  • Shape: draft, base main. The first line is Fixes #24222, the body's only closing keyword, then Clause-②: yes, matching claim 6242034533. 15 files, +231/−44, assignee os-project-manager. Not governed: check-governed-merges.mjs --pr 24232 reads 0 of 15 paths on the register, under 3000 lines. The head merges origin/main 9040945d, which carries PR refactor(rest): move the /meta type list, maintenance and per-type list routes into meta-collection-routes.ts (#23050 slice 13a) #24224's /meta move, so the rest-server.ts overlap from the claim is resolved.
  • What it lands:
    • AuthSessionApi.getSession's result gains three optional user members, each naming its reader: isPlatformAdmin and role for judgePlatformAdmin, and name for runtime's AI-route resolver. name was found when the reader's any came off and its read went TS2339.
    • Every named reader reads the declared result through a module-local SessionRead alias: plugin-auth (four mounts; two (authApi as any) casts gone), rest, runtime (three files), cloud-connection, webhooks, storage. judgePlatformAdmin's published unknown parameter is unchanged.
    • The top-level activeOrganizationId fallback operand is removed at both sites, with its pins flipped and added. It is the card's one ordered behaviour change: no in-process producer answers that shape.
    • The judgePlatformAdmin undefined-direction pin: an absent, undefined, null, false, 'true' or 'Admin' signal refuses with 403, beside admitting controls.
    • A changeset of spec minor and six consumer packages patch. Its sentences were read against the diff.
  • Reverse proof (report): a user.positions read in judgePlatformAdmin fails TS2339 against the declaration, and a probe on plugin-auth's binding fails TS2322 naming better-auth's concrete type. Restoring the operand reds the storage and install-local pins. Every leg was restored with the blob equal to HEAD.
  • Contract review: PASS 6244110995 on this head (CONTRACT_REVIEW_TIER). It agrees with the changeset levels and Clause-②: yes (widening). It judged the docblock's universal sentence against all 20 in-process read sites, not only the card's list, and answers both open questions A (keep user.name; keep the aliases module-local).
  • Cross-lane: declared on the domain:services seat post (6243997746) and both domain:cli seat posts (6244002663, 6244007496). No objection was raised.
  • Gates: 41 check-runs on 05c1551ce7, all completed: 35 success, 6 skipped (all in check-expected-skips.mjs's roster, matched by name). The five required contexts are success. Mergeable, state clean.
  • Deviations, accepted: the model-free trailer pair on both non-merge commits; the PR was not subscribed (os-dev contract); test:repo was run beyond the os-dev default, as the dispatch asked.
  • Review ③ item 8, cloud-connection-plugin.ts resolving the handle as getApi() ?? authSvc.api ?? authSvc: no card, below the filing gate. The review measured it inert against any conformant IAuthService, and no in-tree producer registers a bare session api as auth, so it has no reach:. It is recorded on the seat post as a pointer for this lane.
  • Accepted as is: in webhook-outbox-plugin.ts, the new SessionRead alias sits between two import blocks. It is cosmetic, and lint is green on it.

The PR goes to the merge queue in this act. The cloud dev session is archived.


Generated by Claude Code

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/m tests tooling

Projects

None yet

2 participants