Repository navigation
feat(spec): AuthSessionApi.getSession declares the result fields its readers read (session.id, token, impersonatedBy, user.email, authGate) - #24220
Conversation
…readers read The core session arm and the print door's impersonatorOf read them through the declared type instead of any / a cast. Claude-Session: https://claude.ai/code/session_01A6LabtpD5uUXcktHHnReQa Co-authored-by: Claude <noreply@anthropic.com>
…th-session-api-declared-fields
📓 Docs Drift Check6 anchor(s) derived from 3 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 149 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 ad14498fb03b3286e0b9c447087277efc233487a && git checkout ad14498fb03b3286e0b9c447087277efc233487a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b7f29ddfe2390dfa5f2c5fc240be03fb6c4aa761 6d0b827cd7246bdb6f691166778768b59d508a44 && git checkout -B drift-repro b7f29ddfe2390dfa5f2c5fc240be03fb6c4aa761 && git merge --no-ff 6d0b827cd7246bdb6f691166778768b59d508a44
node scripts/docs-audit/affected-docs.mjs --json b7f29ddfe2390dfa5f2c5fc240be03fb6c4aa761 |
Contract reviewServed-tier: PR #24220 for card #24196, isolated at-tier review, read-only. The head is a merge of ① Derived judgmentsPublic surface, one change:
Nothing undeclared is read by the two readers, which is the card's other half ("if a field turns out not to be read by any reader, it is not declared", and its converse). Every Accept sets: none move. No Zod schema, no authorable key, no REST route, no runtime branch is touched. All three source hunks are type-only; Widening only, so every implementer and caller typechecks unchanged: each new member is optional, which plugin-auth's
Generated artifacts: no The docblock's standing claim, "exactly the fields below that they read", now holds for every read this card names. One position outside this diff still contradicts it: a top-level Gate verdicts on the head, read off the check-runs (theirs, not this record's; this record does not wait on them): ② Semver level
③ Boundary flagsEvery dev flag and
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. Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT · PR #24220 @
|
Fixes #24196
Clause-②: yes
What
AuthSessionApi.getSession(packages/spec/src/contracts/auth-service.ts) says its result declares exactly the fields its in-process readers read, but it only declareduser.id,session.userIdandsession.activeOrganizationId. This PR declares the other fields the card's readers read. Each one is optional, carries a TSDoc naming its reader, and is typed the way that reader takes it:user.emailstringresolveAuthzContextsession arm (@objectstack/core) →ctx.emailuser.authGateunknownnormalizeAuthGatefrom the session arm. It validates the value against the closed posture vocabulary, so the contract declares it unvalidated, which is how that reader takes itsession.idstringsession.tokenstringctx.accessTokensession.impersonatedBystringornullimpersonatorOf(@objectstack/service-print). The stored columnsys_session.impersonated_byis a nullable lookup, and better-auth's admin plugin types itstringorundefinedThe two readers now read through the declared type. Both changes are type-only:
packages/core/src/security/resolve-authz-context.ts: the session arm'ssessionDatais annotated with the typeAuthSessionApi.getSessiondeclares it answers (SessionRead, derived locally withAwaited/ReturnType, so no new export). Before, it was theanythatgetSession's input type returns. ⛔ResolveAuthzInput.getSession's own type is left atany. Tightening it would narrow a published input of@objectstack/core, which is a(narrowing)arm and breaking, and this card is a widening. That option goes back to the seat as an open question.packages/services/service-print/src/print-routes.ts:impersonatorOftakes the declaredSessionReadinstead ofunknown. That drops theas { session?: { impersonatedBy?: unknown } }cast. The same branches run on the same values: the optional chain replaces cast-then-chain, and thetypeof by === 'string'narrowing stays.packages/types/src/in-process-session-read.contract.test.ts(H4) pins the declaration's INPUT key for key (DeclaredInput), never the result. It needed no change and passes unchanged.Verification
All of this ran at
7a60e0e5bb. The later merge oforigin/main(b7f29ddfe2) brought only cli / metadata-protocol / rest / runtime / platform-objects files, none of which this diff touches.pnpm --filter @objectstack/spec build→ exit 0.check:generated→ exit 0, "All 14 generated artifacts are up to date". No artifact changed: the result type is inline in an existing export.@objectstack/spec, after building each package's closure:@objectstack/spec,@objectstack/types,@objectstack/core,@objectstack/service-print→ exit 0. Then the four packages that implement or readAuthSessionApi:@objectstack/plugin-auth(itsAuthManager.getApi()answers better-auth's own API, which still fits the widened result),@objectstack/plugin-sharing,@objectstack/service-datasource,@objectstack/plugin-hono-server→ exit 0. plugin-auth's examples leg resolves plugin-auth's owndist, so it was re-run after building it.@objectstack/core109 files / 2617 tests passed.@objectstack/service-print8 / 109 passed.@objectstack/types27 / 759 passed.scripts/ablation-replace.mjs, each leg restored and checked asblob == HEAD,git diff HEADempty:session?.token→session?.tokenRenamed:tscexit 2,TS2339: Property 'tokenRenamed' does not exist on type '{ userId?…; id?…; token?…; impersonatedBy?… }'.session?.impersonatedBy;→session?.impersonatorId;:tscexit 2, TS2339 on the same declared type.dispatch-gates --commands(84 families, change set derived against merge base8259ea03e): 83 exit 0. The one exception ispnpm check:dual-build-cjs-loads, exit 3, PREREQUISITE NOT MET: it reads every package'sdist, and this box built only the closures above. It is NOT MEASURED locally and is CI's reading.node scripts/check-plugin-teardown-shape.mjs --self-testfirst exited 3 on a shallow clone. Aftergit fetch --unshallowit exits 0 (48 cases).--ranreconciliation: 84 derived, 83 run, 1 NOT-MEASURED (derived from the recorded exit 3).Acceptance notes
The other
getSessioncallers were read, not edited (the hold-off). Their reads of the result still go throughanyor a cast. Read on this branch:plugin-auth/src/auth-plugin.ts::2839and:3229call(authApi as any).getSession(…).:3229also casts the result to{ user?: { id?: unknown } }.:2776and:2911are typed by better-auth'sAuth['api'], notAuthSessionApi. All four hand the session tojudgePlatformAdmin/isPlatformAdminUser, which takeunknownand cast. They readuser.isPlatformAdmin,user.role,user.email,user.idanduser.authGate.rest/src/rest-server.ts:3416:gatedSession: any→normalizeAuthGate(gatedSession?.user). The closure at:3202goes toresolveAuthzContext.runtime/src/dispatcher-plugin.ts:214:const session: any→evaluateAuthGate.runtime/src/http-dispatcher.ts:1556(let session: any→evaluateAuthGate) and:1678(implicitlyany; readsuser.id,session.userId,session.activeOrganizationId).runtime/src/security/resolve-session-principal.ts:57:api: any; its callers readuser.id/session.userId.cloud-connection-plugin.ts:317(any→evaluateAuthGate, thenresolveAuthzContext).marketplace-install-local-plugin.ts:3352readssession.activeOrganizationIdor a top-levelactiveOrganizationId.service-storage/src/storage-service-plugin.ts:1176reads the same top-level fallback.plugin-webhooks/src/webhook-outbox-plugin.ts:627andservice-storage:953/:1279:any, thenevaluateAuthGateanduser.id/session.activeOrganizationId.normalizeAuthGate/evaluateAuthGate(packages/core/src/security/auth-gate.ts) keep theiranyparameter. They also validate non-session input ({ authGate: raw }fromresolveUserAuthGates, and deliberately malformed test fixtures). Typing them is a separate change, not this card's.The fields only those callers read (
user.role,user.isPlatformAdmin, the top-levelactiveOrganizationId) are not declared here. The card scopes the declaration to its named readers, and each of those callers would have to drop its cast for the declaration to have a reader.Generated by Claude Code