Repository navigation
fix(auth,services): the services-lane in-process session reads stop renewing a cookie session (#22258) - #22396
objectstack-fleet[bot] wants to merge 8 commits into
Conversation
…ing a cookie session The nine in-process getSession readers in plugin-auth (x4), plugin-webhooks, service-storage, plugin-sharing, service-settings and service-datasource now hand better-auth inProcessSessionReadInput(headers) from @objectstack/types: a request carrying a session cookie reads with query.disableRefresh, a bearer-only request reads as before. plugin-webhooks gains a workspace dependency on @objectstack/types. Claude-Session: https://claude.ai/code/session_01WYYhVJ78u7PhwFViWo1EmQ Co-authored-by: Claude <noreply@anthropic.com>
… services-lane readers plugin-auth: the four admin doors against real better-auth (real AuthManager, real route registration), by cookie (aligned, no renewal) and bearer-only (renews), with the bare-read precondition and the get-session control. plugin-webhooks, service-storage, plugin-sharing, service-settings and service-datasource: input pins on the getSession input each reader hands better-auth, through each package's real door. Claude-Session: https://claude.ai/code/session_01WYYhVJ78u7PhwFViWo1EmQ Co-authored-by: Claude <noreply@anthropic.com>
…rocess session reads change Claude-Session: https://claude.ai/code/session_01WYYhVJ78u7PhwFViWo1EmQ Co-authored-by: Claude <noreply@anthropic.com>
…ype the settings pin's captured seam plugin-auth's platform-owner-email-reader-census pin lists the import line that names resolvePlatformOwnerEmail verbatim, so the session-read helper takes its own import line. The settings pin reads its captured seam through a getter so tsc does not narrow the reset slot to undefined. Claude-Session: https://claude.ai/code/session_01WYYhVJ78u7PhwFViWo1EmQ Co-authored-by: Claude <noreply@anthropic.com>
webhook-outbox-plugin.ts now takes a value import on @objectstack/types, so check:test-source-alias asks for an anchored alias rather than a wider KNOWN_UNALIASED_TEST_IMPORTS entry (shrink-only). Claude-Session: https://claude.ai/code/session_01WYYhVJ78u7PhwFViWo1EmQ Co-authored-by: Claude <noreply@anthropic.com>
…rvices-session-read
📓 Docs Drift Check3 anchor(s) derived from 6 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 32 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 ce577072ffd59120503a9173d18fe7f542b570fa && git checkout ce577072ffd59120503a9173d18fe7f542b570fa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 11d119ab1868a658c5f43538f3026a56438b2ed6 8d9dcbb4066e5e8d593cdfcf44f2415a8e11b1df && git checkout -B drift-repro 11d119ab1868a658c5f43538f3026a56438b2ed6 && git merge --no-ff 8d9dcbb4066e5e8d593cdfcf44f2415a8e11b1df
node scripts/docs-audit/affected-docs.mjs --json 11d119ab1868a658c5f43538f3026a56438b2ed6 |
…rvices-session-read
… true at release The pending changeset ended by saying the services-lane in-process readers still renew a cookie session; this branch applies the same rule to their auth.api.getSession readers, so that sentence now names their own changeset instead. Frontmatter and every other sentence unchanged. Claude-Session: https://claude.ai/code/session_01WYYhVJ78u7PhwFViWo1EmQ Co-authored-by: Claude <noreply@anthropic.com>
|
CI red that is not this PR's:
Generated by Claude Code |
Contract reviewServed-tier: Read: card #22258 (body and all nine comments: triage ① Derived judgments
② Semver level
③ Boundary flagsDev deviations (
Reds on this head, judged from the check-runs:
Governed surface: none of the 17 paths is governed (no Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Seat adopts the at-tier record The record's two escalations, answered:
Landing condition, as the record states: Generated by Claude Code |
Fixes #22258
Clause-②: no
What this changes
This is the
domain:serviceshalf of the split-session card. better-auth'sgetSessionrenews a session older thanupdateAge: it movessys_session.expires_attonow + expiresInand stages the renewed cookie on that call's own response. The nine in-process readers below answered with their own response, so the renewal landed in the database and its cookie was discarded, leaving a live bearer beside a dying cookie.Each reader now hands better-auth
inProcessSessionReadInput(headers)from@objectstack/types, the rule PR #22367 landed (a45d5d8ab7):query.disableRefresh, so the session renews only throughGET /api/v1/auth/get-session, which re-issues the cookie.Each change is a one-expression substitution. The headers pass through untouched, so every reader resolves the same session it did before.
@objectstack/plugin-webhooksgains a workspace dependency on@objectstack/types, per the triage ruling (6072029812). There is no cycle:@objectstack/typesdepends only on@objectstack/spec, and@objectstack/corealready pulledtypesin transitively.pnpm-lock.yamlwas regenerated bypnpm install(+3 lines).check:test-source-aliasthen required the matching anchored alias inpackages/plugins/plugin-webhooks/vitest.config.ts. That registry is shrink-only, so the gate's own remedy is the alias, and that line is in this PR.packages/types,rest,runtime,plugin-hono-server,cloud-connectionorpackages/specedit.Enumeration pin: the census over
packages/services/**andpackages/plugins/**, non-test sourcesMeasured at the merge base
117d34deand at this headc09841ccf. Excluded:*.test.ts,__tests__/**,*.testkit.ts,*.md.117d34de)c09841ccf)auth-plugin.ts:2465(toggle-disabled gate)authApi.getSession({ headers: c.req.raw.headers }inProcessSessionReadInput(c.req.raw.headers)auth-plugin.ts:2528(gateAdmin)(authApi as any).getSession({ headers: c.req.raw.headers }inProcessSessionReadInput(c.req.raw.headers)auth-plugin.ts:2595(unlock-user gate)authApi.getSession({ headers: c.req.raw.headers }inProcessSessionReadInput(c.req.raw.headers)auth-plugin.ts:2913(has-permission branch)(authApi as any).getSession({ headers: c.req.raw.headers }inProcessSessionReadInput(c.req.raw.headers)webhook-outbox-plugin.ts:483api.getSession({ headers: c.req.raw.headers }inProcessSessionReadInput(c.req.raw.headers)storage-service-plugin.ts:844api.getSession({ headers }inProcessSessionReadInput(headers)sharing-plugin.ts:941api?.getSession?.({ headers: h }inProcessSessionReadInput(h)settings-service-plugin.ts:300api?.getSession?.({ headers: h }inProcessSessionReadInput(h)admin-routes.ts:212api?.getSession?.({ headers }inProcessSessionReadInput(headers)current-user-endpoints.ts:412api.getSession(c09841ccf:api.getSession(catches 3 calls (webhooks, storage, hono) plus 2 doc comments (anonymous-session-refusal.ts:63andplatform-admin-gate.ts:90).api?.getSession?.(catches 3 calls (sharing, settings, datasource).[)a-zA-Z_]\??\.getSession\??\.?\(catches all 10 calls above, the same 2 doc comments, and 5 hits that are not better-auth reads:store.getSession(×4 inservice-storage/src/storage-routes.tsandthis.getSession(inmetadata-store.ts, both the upload-session store.c09841ccf, the patterngetSession\??\.?\(\{ *headersover the same scope matches only the doc comment atplatform-admin-gate.ts:90. No call passes a bare{ headers }.api.getSession(is case-sensitive. It matches neitherauthApi.getSession((capitalA) nor(authApi as any).getSession(, so it finds none of the four plugin-auth readers. The table rests on the any-receiver pattern.Pins, and the ablation of each
src/in-process-session-renewal.pin.test.tsruns the installed better-auth, withexpiresInandupdateAgeread off the live instance. It uses a realAuthManagerand the plugin's realregisterAuthRouteson a Hono app (theadmin-remove-user-gate-orderingharness).now + expiresIn − updateAge − 60 s, andsys_sessionis read back after every request.getSessionrenews.GET /get-sessionrenews and re-issues the cookie withMax-Age = expiresIn.query: { disableRefresh: true }for a cookie, and for cookie plus bearer; noqueryat all for bearer-only. What that input does against real better-auth is pinned by the plugin-auth file above and bypackages/runtime/src/in-process-session-renewal.pin.test.ts.c09841ccfthroughscripts/ablation-replace.mjsin wrap mode (literal anchor that must hit exactly once, on-disk counts and blob hashes, restore proven against HEAD). Each leg put the old{ headers }call back for one reader and ran that reader's pin. Every mutated reader resolves fromsrc/in its suite (relative imports), so nodist/leg applies.:2465POST /admin/oauth2/toggle-disablednow + expiresIn, no cookiethe session renewed (+86460 s) but its cookie was not re-issued)32c004a7d80f== HEAD:2528gateAdminPOST /admin/set-user-manager32c004a7d80f== HEAD:2595POST /admin/unlock-user32c004a7d80f== HEAD:2913POST /admin/has-permission32c004a7d80f== HEAD:483POST /api/v1/webhooks/redeliverdisableRefreshexpected undefined to deeply equal { disableRefresh: true })60dc0b99b985== HEAD:844GET /api/v1/storage/upload/chunked/:uploadId/progressviamountStorageRoutesdisableRefreshb85d1c3f6fe1== HEAD:941GET /api/v1/share-links(real plugin boot)disableRefresh026ac1febf6e== HEAD:300contextFromRequest(real plugin boot, pass-through capture)disableRefreshe9af2764acea== HEAD:212GET /api/v1/datasources/drivers(real registrar on Hono)disableRefresh0868657715d2== HEADEach plugin-auth leg reddened exactly the one door it ablated, so each door reaches exactly one reader. The bearer control stayed green in every leg.
Verification at
c09841ccf(this branch merged withorigin/main191543456)turbo run build --filter=!@objectstack/docs: 72/72 tasks, exit 0.pnpm --filter PKG test, all exit 0:pnpm --filter PKG typecheck: exit 0 for all six. Each new pin file is listed by a program the typecheck script runs (tsc --listFilesOnly):tsconfig.json, ortsconfig.test.jsonthroughcheck:test-typecheck.node scripts/pm/dispatch-gates.mjs --commandsderived 82 commands for this diff (the pre-derived 76, pluscheck:engine-double-contract,check:objectql-double-limit,check:query-options-erasure,check:type-check-coverage,check:type-check-debtandcheck:where-matcher). All 82 exit 0 atc09841ccf.--ranwith the recorded exit codes prints:Run reconciliation — 82 derived, 82 run, 0 NOT-MEASURED, 0 UNRUN.pnpm lintis CI's run):.tsfiles. The other three changed files (.changeset/*.md,package.json,pnpm-lock.yaml) answer "File ignored because no matching configuration was supplied".eslint --no-inline-config --format jsonover the 13: 13 files linted, 0 errors, 0 warnings.parserOptions.projectand no type-aware rule, so this diff cannot move the verdict on an untouched file.Acceptance notes
getSession(.expires_at+86460 s by cookie and set no cookie.POST /api/v1/auth/admin/sso/register: the/get-sessionre-dispatch through the better-auth handler inregister-sso-provider.ts:60, behindgateAdmin.POST /api/v1/auth/send-verification-email: the same re-dispatch insend-verification-email.ts:63.POST /api/v1/auth/organization/add-member:authApi.addMember({ ..., headers })inorganization-add-member.ts:175; the vendor's session read inside renews.POST /api/v1/auth/set-initial-password:authApi.setPassword({ ..., headers })inset-initial-password.ts:65.authApi.createOAuthClient({ ..., headers })atauth-plugin.ts:3175, and the SSO bridges' inner re-dispatches (register-sso-provider.ts:210, 306, 404, 454). Each bridge returns only status and body, so a vendorSet-Cookiethere is discarded.getSessioninput, so they are reported to the seat rather than changed here.POST /admin/sso/registersplit togateAdminalone. WithgateAdminconverted, that door still splits through the re-dispatch in note 1. ThegateAdminpin therefore usesPOST /admin/set-user-manager, which reads the session once.8d9dcbb4(seat's edit of this note, after patch round 1)..changeset/22258-in-process-session-read-no-renewal-behind-cookie.mdended by saying the services-lane readers "still renew a cookie session without re-issuing its cookie", which this PR makes false in the same release. Under the seat's ruling A (auth: server-side auth.api.getSession reads renew the session without forwarding the renewed cookie, so the browser cookie expires before the session (split session) #22258, ACCEPT6073337286), its last paragraph now reads: "The same rule is applied to the in-processauth.api.getSessionreaders in … by their own changeset." No other sentence and no frontmatter changed.Check Changesetis red by design (the DELIBERATE CORRECTION class); the at-tier record6073440660on8d9dcbb4confirms it: do not restore the old sentence.packages/plugins/plugin-webhooks/vitest.config.tsgains the anchored@objectstack/typesalias thatcheck:test-source-aliasdictates for the new dependency.createMemoryEnginefromimpersonation-bearer-rotation.test.ts, as twenty sibling files do, so that file's ten cases also run inside this pin (21 = 11 + 10). Reusing the pinned double adds no new engine double to the ledger.platform-admin-gate.ts:90still says the gate'ssessionis whatauth.api.getSession({ headers })returned. It describes the result's shape, which is unchanged.Generated by Claude Code