Repository navigation
feat(spec): the remaining getSession readers read the declared AuthSessionApi result (user.isPlatformAdmin, role, name); the top-level activeOrganizationId fallback is removed - #24232
Conversation
Claude-Session: https://claude.ai/code/session_018fKVdiy1mpvMbYt2ryCsLz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fKVdiy1mpvMbYt2ryCsLz Co-authored-by: Claude <noreply@anthropic.com>
…t-session-readers-declared
📓 Docs Drift CheckThis PR changes 7 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 155 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 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
|
Contract reviewServed-tier: PR #24232 for card #24222, isolated at-tier review, read-only. The head is a merge of ① Derived judgmentsPublic surface, one change:
Nothing undeclared is read by any typed reader. Every read at the head, per site: plugin-auth's four mounts read The docblock's new sentence is universal ("the other in-process readers read the result as declared here too; none reads a top-level The vendor binding at the four plugin-auth mounts:
Accept sets: one runtime behaviour change, the removal of the top-level Every other hunk is type-only, checked branch by branch: Published surfaces of the six consumer packages: unchanged. Generated artifacts: no Zod schema, no One cosmetic note for the lane, not a contract matter: in Gate verdicts on the head, read off the check-runs (theirs, not this record's): all 41 completed. The required five are success: ② Semver level
③ Boundary flagsEvery dev flag and
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 ( Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT · PR #24232 @
|
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 throughanyor a cast, and two of them read a top-levelactiveOrganizationIdthat 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
usermembers, each with its reader nameduser.isPlatformAdminbooleanjudgePlatformAdmin(@objectstack/plugin-auth, viaisPlatformAdminUser)customSessionderives it as a boolean (auth-manager.ts,let platformAdmin = false)user.rolestring | nulladminplugin schema declares ittype: "string", required: false, so a row may hold null (theimpersonatedByprecedent)user.namestringdispatcher-plugin's concrete AI-routereq.userresolver (@objectstack/runtime), the actor's display namebuildActorUser'sdisplayName?: string. The card did not list this read. Removing theanyfromresolveSessionDataturned it into a compile error (TS2339 onuser.name), and that is how it was foundThe docblock gains a
[#24222]paragraph: the remaining readers read the declared result, and none reads a top-levelactiveOrganizationId.Readers: each one reads the declared result (route a). Types only, runtime behaviour unchanged
Each site gets a module-local
SessionReadalias,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 inauth-plugin.ts(toggle-disabled,gateAdmin,unlock-user,has-permission). The two(authApi as any).getSession(…)calls are un-cast, and all four now read intoSessionRead | null.authApiis better-auth's ownAuth['api'], so better-auth's concrete session type now has to be assignable to the declaration. It is: a probe assigning that read tonumberfailed with{ session: { id: string; …; token: string; … }; user: … } | null is not assignable to type 'number', so the type is concrete, notany. Inhas-permission, the{ user?: { id?: unknown } }cast is gone.judgePlatformAdminreads(session as SessionRead | null)?.userinstead of aRecord<string, unknown>cast.unknown. It is a published@objectstack/plugin-authexport, and narrowing it would be a breaking input narrowing inside a widening card. The spec(contracts):AuthSessionApi.getSessionclaims to declare every field its readers read, but its result declares three; readers cast to readsession.id,token,user.email, the auth gate andimpersonatedBy#24196 contract review (6240835611, ③ item 1) made the same call forResolveAuthzInput.getSession.isPlatformAdminUserkeepsunknowntoo, because it also judges raw user rows (admin-impersonate-endpoint.ts: the caller row andfindUserById's target row), the same reasonnormalizeAuthGatekeepsany.@objectstack/rest(rest-server.ts): thegetSessionclosure handed toresolveAuthzContextreturnsPromise<SessionRead | null>, andgatedSessionbecomesSessionRead | nullinstead ofany.@objectstack/runtime:dispatcher-plugin.tsreadGatedSessionUser:session: anyis dropped.apithere is already the contract's handle.security/resolve-session-principal.tsresolveSessionDatareturnsPromise<SessionRead | null>instead ofPromise<any | undefined>. It readsauthService.api/getApi()throughIAuthServiceinstead of threeas anycasts, with the same branches. It is module-internal:index.tsdoes not export it.http-dispatcher.ts: both reads (enforceAuthGate,enforceProjectMembership) are typed.@objectstack/cloud-connection:sessionFromAuthServiceandResolvedSession.readFrom.sessionData(unknownbecomesSessionRead | null) incloud-connection-plugin.ts. Inmarketplace-install-local-plugin.ts,resolveActiveOrgIdand the install-localgetSessionbridge.@objectstack/plugin-webhooks:resolveSessionreturnsPromise<SessionRead | null>.@objectstack/service-storage:buildGetSessionreturnsPromise<SessionRead | null>. Thesession: anyin the upload-stamp resolver and in the posture reader is gone.The top-level
activeOrganizationIdoperand is removed at both sites (Prime Directive #12)Producers measured: the only in-tree
authservice is plugin-auth'sAuthManager(auth-plugin.ts,ctx.registerService('auth', this.authManager)). Its read is better-auth'sgetSession, and itscustomSessionreturns{ user: { ...user, positions, isPlatformAdmin, authGate? }, session }(auth-manager.ts). No production code produces a top-levelactiveOrganizationId. The one test fixture that did wassys-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?.activeOrganizationIdbecomessession?.session?.activeOrganizationId. The docblock that described the fallback is corrected.marketplace-install-local-plugin.tsresolveActiveOrgId:… ?? session?.activeOrganizationId ?? nullbecomessession?.session?.activeOrganizationId ?? null.activeOrganizationId" (no stamp).marketplace-install-local-no-active-organization.test.tsgains atopLevelOrgcase: walled, with only a top-levelactiveOrganizationId, the reseed answers 403PERMISSION_DENIEDnaming the missing active organization, and no seed run starts.The
undefined-direction pin onjudgePlatformAdminplatform-admin-gate.undefined-direction.test.ts: a signed-in user (with anid, so the 401 cannot mask the 403) is refused 403PERMISSION_DENIED, andisPlatformAdminUseranswersfalse, in each of these cases: both fields absent; both explicitlyundefined;role: null;isPlatformAdmin: false;isPlatformAdmin: 'true'(truthy is nottrue);role: 'Admin'. Controls:isPlatformAdmin: trueandrole: 'admin'still admit, and the admitted actor is{ id, email }.Changeset
.changeset/24222-get-session-readers-declared.md:@objectstack/specminor(a published contract widens;Clause-②: yes (widening)in its body).@objectstack/plugin-auth,rest,runtime,cloud-connection,plugin-webhooksandservice-storagearepatch: 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 mergingorigin/main9040945d, which carries PR #24224)Build: the dependency closure of the six reader packages, plus
@objectstack/spec,restandplugin-authagain after the merge:os-verify-lockVERDICT command-exit 0.Typecheck:
pnpm --filter … typecheck, exit 0 forspec,plugin-auth,rest,runtime,cloud-connection,plugin-webhooksandservice-storage.rest,plugin-authandruntimewere re-run after the merge: exit 0.Tests:
spectestspectest:reporesttestresttest:reporuntimetestruntimetest:repoplugin-authtestcloud-connectiontestplugin-webhookstestservice-storagetestEvery row exited 0.
Reverse verification (types): a
user.positionsread added tojudgePlatformAdminfailstscwith TS2339 against{ id?; email?; name?; authGate?; isPlatformAdmin?: boolean; role?: string | null }, so the rebuiltdistdeclaration is the one being read. The organic red was theuser.nameTS2339 above, beforenamewas declared. Both mutations ran underscripts/ablation-replace.mjs, which confirmed the blob was restored to HEAD andgit diff HEADwas empty.Ablation (operand removal): with
?? (session as any)?.activeOrganizationIdput 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:generatedexit 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 --commandsderives 93 commands, and the same list again on the merged head. Run on head05c1551c: 92 exit 0. One is NOT MEASURED:node scripts/check-plugin-teardown-shape.mjs --self-testexits 3 because its fixture needsgit fetch --unshallow(a shallow clone);Lint & Repo Gatescarries it.--ranreconciliation: "93 derived famil(ies) accounted for — 92 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".Acceptance notes
SessionReadalias now exists as a module-local copy in 9 modules: 2 from spec(contracts):AuthSessionApi.getSessionclaims to declare every field its readers read, but its result declares three; readers cast to readsession.id,token,user.email, the auth gate andimpersonatedBy#24196 and 7 here. An exported@objectstack/spec/contractsalias would give it one spelling, at the cost of a public export and an api-surface entry. It is not added here. Each copy derives from the contract, so the copies cannot drift apart.cloud-connection-plugin.tssessionFromAuthServiceresolvesapiasgetApi(), thenauthSvc.api, thenauthSvcitself. The last operand treats the service as its own session handle, which is a shapeIAuthServicedoes not declare. It is not anactiveOrganizationIdoperand, so it is outside this card's Done-when, and it is left as is.content/docs/kernel/contracts/auth-service.mdxnamesAuthSessionApibut does not restate its result shape, so it still reads true and is unchanged.🤖 Generated with Claude Code
https://claude.ai/code/session_018fKVdiy1mpvMbYt2ryCsLz
Generated by Claude Code