Skip to content

fix(auth,services): the services-lane in-process session reads stop renewing a cookie session (#22258) - #22396

Draft
objectstack-fleet[bot] wants to merge 8 commits into
mainfrom
claude/issue-22258-services-session-read
Draft

objectstack-fleet[bot] wants to merge 8 commits into
mainfrom
claude/issue-22258-services-session-read

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #22258
Clause-②: no

What this changes

This is the domain:services half of the split-session card. better-auth's getSession renews a session older than updateAge: it moves sys_session.expires_at to now + expiresIn and 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):

  • A request carrying a session cookie reads with query.disableRefresh, so the session renews only through GET /api/v1/auth/get-session, which re-issues the cookie.
  • A bearer-only request reads exactly as before, renewal included.

Each change is a one-expression substitution. The headers pass through untouched, so every reader resolves the same session it did before.

  • @objectstack/plugin-webhooks gains a workspace dependency on @objectstack/types, per the triage ruling (6072029812). There is no cycle: @objectstack/types depends only on @objectstack/spec, and @objectstack/core already pulled types in transitively. pnpm-lock.yaml was regenerated by pnpm install (+3 lines).
  • check:test-source-alias then required the matching anchored alias in packages/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.
  • No packages/types, rest, runtime, plugin-hono-server, cloud-connection or packages/spec edit.

Enumeration pin: the census over packages/services/** and packages/plugins/**, non-test sources

Measured at the merge base 117d34de and at this head c09841ccf. Excluded: *.test.ts, __tests__/**, *.testkit.ts, *.md.

reader (line at head) spelling before (117d34de) after (c09841ccf)
plugin-auth auth-plugin.ts:2465 (toggle-disabled gate) authApi.getSession( { headers: c.req.raw.headers } inProcessSessionReadInput(c.req.raw.headers)
plugin-auth auth-plugin.ts:2528 (gateAdmin) (authApi as any).getSession( { headers: c.req.raw.headers } inProcessSessionReadInput(c.req.raw.headers)
plugin-auth auth-plugin.ts:2595 (unlock-user gate) authApi.getSession( { headers: c.req.raw.headers } inProcessSessionReadInput(c.req.raw.headers)
plugin-auth auth-plugin.ts:2913 (has-permission branch) (authApi as any).getSession( { headers: c.req.raw.headers } inProcessSessionReadInput(c.req.raw.headers)
plugin-webhooks webhook-outbox-plugin.ts:483 api.getSession( { headers: c.req.raw.headers } inProcessSessionReadInput(c.req.raw.headers)
service-storage storage-service-plugin.ts:844 api.getSession( { headers } inProcessSessionReadInput(headers)
plugin-sharing sharing-plugin.ts:941 api?.getSession?.( { headers: h } inProcessSessionReadInput(h)
service-settings settings-service-plugin.ts:300 api?.getSession?.( { headers: h } inProcessSessionReadInput(h)
service-datasource admin-routes.ts:212 api?.getSession?.( { headers } inProcessSessionReadInput(headers)
plugin-hono-server current-user-endpoints.ts:412 api.getSession( already the helper (PR #22367) unchanged
  • Counted per spelling, at c09841ccf:
    • The fixed string api.getSession( catches 3 calls (webhooks, storage, hono) plus 2 doc comments (anonymous-session-refusal.ts:63 and platform-admin-gate.ts:90).
    • The fixed string api?.getSession?.( catches 3 calls (sharing, settings, datasource).
    • The any-receiver pattern [)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 in service-storage/src/storage-routes.ts and this.getSession( in metadata-store.ts, both the upload-session store.
  • None left. At c09841ccf, the pattern getSession\??\.?\(\{ *headers over the same scope matches only the doc comment at platform-admin-gate.ts:90. No call passes a bare { headers }.
  • The triage spelling misses plugin-auth. As a fixed string, api.getSession( is case-sensitive. It matches neither authApi.getSession( (capital A) 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

  • plugin-auth: real better-auth. src/in-process-session-renewal.pin.test.ts runs the installed better-auth, with expiresIn and updateAge read off the live instance. It uses a real AuthManager and the plugin's real registerAuthRoutes on a Hono app (the admin-remove-user-gate-ordering harness).
    • The session is aged to now + expiresIn − updateAge − 60 s, and sys_session is read back after every request.
    • Precondition: a bare in-process getSession renews.
    • Control: GET /get-session renews and re-issues the cookie with Max-Age = expiresIn.
  • The other five: input pins through each package's real door. They assert what the reader hands better-auth: query: { disableRefresh: true } for a cookie, and for cookie plus bearer; no query at all for bearer-only. What that input does against real better-auth is pinned by the plugin-auth file above and by packages/runtime/src/in-process-session-renewal.pin.test.ts.
  • Ablation, run at c09841ccf through scripts/ablation-replace.mjs in 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 from src/ in its suite (relative imports), so no dist/ leg applies.
reader pin and door by cookie bearer-only control ablation: failing output restored
plugin-auth :2465 real better-auth, POST /admin/oauth2/toggle-disabled 0 s, no cookie renews to now + expiresIn, no cookie 1 failed, 20 passed: exactly "toggle-disabled — by cookie" (the session renewed (+86460 s) but its cookie was not re-issued) blob 32c004a7d80f == HEAD
plugin-auth :2528 gateAdmin real better-auth, POST /admin/set-user-manager 0 s, no cookie renews, no cookie 1 failed, 20 passed: exactly "set-user-manager (gateAdmin) — by cookie" (+86460 s) 32c004a7d80f == HEAD
plugin-auth :2595 real better-auth, POST /admin/unlock-user 0 s, no cookie renews, no cookie 1 failed, 20 passed: exactly "unlock-user — by cookie" (+86460 s) 32c004a7d80f == HEAD
plugin-auth :2913 real better-auth, POST /admin/has-permission 0 s, no cookie renews, no cookie 1 failed, 20 passed: exactly "has-permission — by cookie" (+86460 s) 32c004a7d80f == HEAD
plugin-webhooks :483 input, POST /api/v1/webhooks/redeliver disableRefresh no query 2 failed, 1 passed: the cookie and cookie-plus-bearer cases (expected undefined to deeply equal { disableRefresh: true }) 60dc0b99b985 == HEAD
service-storage :844 input, GET /api/v1/storage/upload/chunked/:uploadId/progress via mountStorageRoutes disableRefresh no query 2 failed, 1 passed: the two cookie cases b85d1c3f6fe1 == HEAD
plugin-sharing :941 input, GET /api/v1/share-links (real plugin boot) disableRefresh no query 2 failed, 1 passed: the two cookie cases 026ac1febf6e == HEAD
service-settings :300 input, the routes' contextFromRequest (real plugin boot, pass-through capture) disableRefresh no query 2 failed, 1 passed: the two cookie cases e9af2764acea == HEAD
service-datasource :212 input, GET /api/v1/datasources/drivers (real registrar on Hono) disableRefresh no query 2 failed, 1 passed: the two cookie cases 0868657715d2 == HEAD

Each 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 with origin/main 191543456)

  • Build: turbo run build --filter=!@objectstack/docs: 72/72 tasks, exit 0.
  • pnpm --filter PKG test, all exit 0:
    • plugin-auth: 133 files, 2693 passed, 10 skipped;
    • plugin-webhooks: 16 files, 168 passed;
    • service-storage: 47 files, 776 passed;
    • plugin-sharing: 41 files, 1005 passed;
    • service-settings: 42 files, 755 passed;
    • service-datasource: 42 files, 763 passed.
  • 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, or tsconfig.test.json through check:test-typecheck.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands derived 82 commands for this diff (the pre-derived 76, plus check:engine-double-contract, check:objectql-double-limit, check:query-options-erasure, check:type-check-coverage, check:type-check-debt and check:where-matcher). All 82 exit 0 at c09841ccf. --ran with the recorded exit codes prints: Run reconciliation — 82 derived, 82 run, 0 NOT-MEASURED, 0 UNRUN.
  • Lint, narrowed and declared (full pnpm lint is CI's run):
    • The population, read from eslint's own config: the 13 changed .ts files. 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 json over the 13: 13 files linted, 0 errors, 0 warnings.
    • Invariance: the config sets no parserOptions.project and no type-aware rule, so this diff cannot move the verdict on an untouched file.

Acceptance notes

  1. Residue measured, out of this census. Four plugin-auth doors still split a cookie session after this change, through in-process reads that do not spell getSession(.
    • Measured on the same real-better-auth harness: each moved expires_at +86460 s by cookie and set no cookie.
      • POST /api/v1/auth/admin/sso/register: the /get-session re-dispatch through the better-auth handler in register-sso-provider.ts:60, behind gateAdmin.
      • POST /api/v1/auth/send-verification-email: the same re-dispatch in send-verification-email.ts:63.
      • POST /api/v1/auth/organization/add-member: authApi.addMember({ ..., headers }) in organization-add-member.ts:175; the vendor's session read inside renews.
      • POST /api/v1/auth/set-initial-password: authApi.setPassword({ ..., headers }) in set-initial-password.ts:65.
    • Same mechanism by source, not measured: authApi.createOAuthClient({ ..., headers }) at auth-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 vendor Set-Cookie there is discarded.
    • These need a rule shape for a handler re-dispatch or a vendor endpoint call, not the one-expression getSession input, so they are reported to the seat rather than changed here.
  2. A correction to PR fix(auth): in-process session reads no longer renew a browser session behind its cookie #22367's H5 table. H5 attributed the POST /admin/sso/register split to gateAdmin alone. With gateAdmin converted, that door still splits through the re-dispatch in note 1. The gateAdmin pin therefore uses POST /admin/set-user-manager, which reads the session once.
  3. The cli half's pending changeset, amended in 8d9dcbb4 (seat's edit of this note, after patch round 1). .changeset/22258-in-process-session-read-no-renewal-behind-cookie.md ended 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, ACCEPT 6073337286), its last paragraph now reads: "The same rule is applied to the in-process auth.api.getSession readers in … by their own changeset." No other sentence and no frontmatter changed. Check Changeset is red by design (the DELIBERATE CORRECTION class); the at-tier record 6073440660 on 8d9dcbb4 confirms it: do not restore the old sentence.
  4. Outside the planned surface: one line. packages/plugins/plugin-webhooks/vitest.config.ts gains the anchored @objectstack/types alias that check:test-source-alias dictates for the new dependency.
  5. Imported fixture. The plugin-auth pin imports createMemoryEngine from impersonation-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.
  6. A doc comment left as is. platform-admin-gate.ts:90 still says the gate's session is what auth.api.getSession({ headers }) returned. It describes the result's shape, which is unchanged.

Generated by Claude Code

claude added 6 commits October 9, 2026 01:17
…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>
@github-actions github-actions Bot added the size/l label Oct 9, 2026
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 6 changed package(s); no hand-written page names any of them. ⚠️ 2 changed file(s) yielded no anchor (packages/plugins/plugin-webhooks/package.json, packages/plugins/plugin-webhooks/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/plugins/plugin-webhooks/package.json, packages/plugins/plugin-webhooks/vitest.config.ts) — pages documenting those are invisible to this run
  • 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 — 32 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 11d119ab1868a658c5f43538f3026a56438b2ed6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from ce577072ffd59120503a9173d18fe7f542b570fa — the merge of head 8d9dcbb4066e5e8d593cdfcf44f2415a8e11b1df into base 11d119ab1868a658c5f43538f3026a56438b2ed6, 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 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

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

claude added 2 commits October 9, 2026 02:30
… 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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI red that is not this PR's: Lint & Repo Gates on 8d9dcbb4 · domain:services seat 2 · session_01WYYhVJ78u7PhwFViWo1EmQ · 2026-10-09T02:56Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8d9dcbb4066e5e8d593cdfcf44f2415a8e11b1df
Local-runs: none

Read: card #22258 (body and all nine comments: triage 6058300952, claim 6069531196, report 6070681067, ACCEPT 6070755027, landed 6071583141, triage answer 6072029812, claim 6072197542, reports 6073006438 and 6073299494); PR #22396 body, its 17-file list, both PR comments (6072979642, 6073315834), and git diff 11d119ab…8d9dcbb4 (the merge base is main's tip, so the net diff is the PR's own: 17 files, +874/−12); the 34 check-runs on 8d9dcbb4 with the annotations and step lists of the two red jobs; main's check-runs on 11d119ab for comparison. The source files are byte-equal between c09841ccf (the head the PR body measured) and this head: the only change since is the merge of main and the one-line changeset amendment, so the PR body's line numbers and blob hashes (32c004a7d, 60dc0b99b, b85d1c3f6, 026ac1feb, e9af2764a, 086865771) hold here.

① Derived judgments

# Change the diff implies Judgment
1 Nine auth.api.getSession inputs change from { headers } to inProcessSessionReadInput(headers): plugin-auth auth-plugin.ts:2465 (toggle-disabled gate), :2528 (gateAdmin), :2595 (unlock-user gate), :2913 (has-permission branch); plugin-webhooks webhook-outbox-plugin.ts:483; service-storage storage-service-plugin.ts:844; plugin-sharing sharing-plugin.ts:941; service-settings settings-service-plugin.ts:300; service-datasource admin-routes.ts:212. Right. The helper (packages/types/src/in-process-session-read.ts at this head) returns { headers } unchanged, plus query: { disableRefresh: true } only when the Cookie header carries a *.session_token= value. The headers object is the same reference, so each door resolves the same session it did; the accept-set of every door is unchanged. What changes is renewal: a cookie request no longer renews at these doors; a bearer-only request renews as before. One-expression substitutions, no other logic moved.
2 Enumeration: the lane's census closes with none left. Right, re-derived at this head. Over packages/services/** and packages/plugins/**, non-test sources: the pattern getSession\??\.?\(\{ *headers matches only the doc comment plugin-auth/src/platform-admin-gate.ts:90; the any-receiver pattern finds the nine above, plugin-hono-server's already-converted reader (current-user-endpoints.ts:412), two doc comments, and five store.getSession(/this.getSession( hits in service-storage that are the upload-session store, not better-auth. At the merge base the same nine passed a bare { headers }. The PR body's note that triage's fixed-string api.getSession( matches none of the four plugin-auth spellings is correct.
3 gateAdmin reach: the new changeset says "every /api/v1/auth/admin/* mount behind the shared platform-admin gate". Right. gateAdmin(c) is called at 12 sites in auth-plugin.ts at this head; one read serves them all.
4 Door claims in the new changeset: webhooks POST /api/v1/webhooks/redeliver; storage upload and download doors; share-links /api/v1/share-links; settings /api/settings; datasource-admin /api/v1/datasources/*. Right. Verified at this head: the redeliver mount at webhook-outbox-plugin.ts:398 calls resolveSession; storage's buildGetSession feeds both consumers (:1052 upload gate, :1202 download authorizer); sharing's base path default is /api/v1/share-links (:973); settings routes mount under /api/settings; datasource's requireDatasourceAdmin (:449) calls buildGetSession (:462) ahead of every handler.
5 @objectstack/plugin-webhooks package.json gains "@objectstack/types": "workspace:*" under dependencies; pnpm-lock.yaml +3 (the link:../../types importer entry). Right. An additive dependency on an internal workspace package in a published manifest, per the triage ruling (6072029812). No cycle: @objectstack/types at this head depends only on @objectstack/spec. Not a public-surface widening (no export changes); patch-compatible.
6 plugin-webhooks/vitest.config.ts gains an anchored ^@objectstack/types$ → ../../types/src/index.ts alias. Right. Test-only, unpublished; the shape the file's core entry already uses, dictated by check:test-source-alias for the new value import.
7 Exports and spec. Right. No export line changes in any shipped source in the diff. No packages/spec, types, rest, runtime, plugin-hono-server, cloud-connection or content/docs path in the file list: the claim's ⛔ boundaries hold. AuthSessionApi.getSession's { headers } declaration drift stays the spec lane's card, as triage ruled.
8 plugin-auth imports the helper on its own line instead of widening the existing @objectstack/types import. Right. platform-owner-email-reader-census.pin.test.ts:105 lists the existing import line verbatim; widening it would have moved that ledger for no behaviour.
9 Pins. plugin-auth in-process-session-renewal.pin.test.ts runs real better-auth behind the real registerAuthRoutes on Hono (expiresIn/updateAge read off the live instance): precondition (a bare read renews), 4 doors × cookie (0 s, no cookie) / bearer (renews to now + expiresIn, no cookie), control (get-session re-issues with Max-Age = expiresIn). Five input pins (webhooks, storage, sharing, settings, datasource) each through the package's real door: cookie and cookie+bearer → query.disableRefresh; bearer-only → no query key. Right. The pin texts assert the exact old-vs-new input, so each would red on the old call shape, which matches the dev's reported ablation (not re-run here). The plugin-auth pin imports createMemoryEngine from a sibling .test.ts (its 10 cases run inside the pin, 21 = 11 + 10); a cost twenty siblings already pay, no new engine double. The doc comment at platform-admin-gate.ts:90 describing the result shape is left as is: correct, since the result shape is unchanged.

② Semver level

  • New changeset .changeset/22258-services-in-process-session-read.md: @objectstack/plugin-auth, plugin-webhooks, plugin-sharing, service-storage, service-settings, service-datasource at patch — right. A bug fix in six released packages; no new or removed export, no authorable key, no accepted-input or wire-shape change (row 1). Each body sentence checks against the diff: the nine-door inventory (rows 3–4), "the rule the REST, dispatcher, current-user and cloud-connection doors already follow" (landed by PR fix(auth): in-process session reads no longer renew a browser session behind its cookie #22367 at a45d5d8ab), the cookie/bearer arms (the helper's source), and "Upgrading: nothing to change; plugin-webhooks now depends on types directly; it already reached it through core" (row 5).
  • Clause-②: no (PR body line 2) — right, and matches the claim's declaration (6072197542). No new export in any package entry; no wider accepted input; no narrowing of an accepted set (every request that resolved a session still resolves the same one). The only behaviour change is where renewal happens. no with no arm is well-formed and takes no minor.
  • The DELIBERATE CORRECTION this record confirms — .changeset/22258-in-process-session-read-no-renewal-behind-cookie.md. Added by PR fix(auth): in-process session reads no longer renew a browser session behind its cookie #22367 (a45d5d8ab), pending on main at 11d119ab, frontmatter unchanged by this PR (@objectstack/types minor; rest, runtime, plugin-hono-server, cloud-connection patch). Exactly one sentence is rewritten (+1/−1), the last paragraph; every other sentence is byte-equal to the base.
    • Old: "Not changed here: the in-process readers in @objectstack/plugin-auth, @objectstack/plugin-webhooks, @objectstack/plugin-sharing, @objectstack/service-storage, @objectstack/service-settings and @objectstack/service-datasource still renew a cookie session without re-issuing its cookie." — FALSE at this head. Every auth.api.getSession reader in those six packages now passes inProcessSessionReadInput, which adds query.disableRefresh on a cookie request; the plugin-auth pin measures, against real better-auth, that four of those doors leave expires_at unchanged by cookie. Both changesets version in the same fixed-group release, so the CHANGELOGs of types, rest, runtime, plugin-hono-server and cloud-connection would tell an upgrader that these six packages still renew a cookie session — a defect that is gone in that version. Restoring the old sentence would put a false statement back. (The four plugin-auth doors that still split do so through a /get-session re-dispatch and vendor endpoint calls, not through the "in-process readers" this paragraph's own preceding sentence defines as auth.api.getSession callers; the old sentence asserts renewal of the whole set, which is false.)
    • New: "The same rule is applied to the in-process auth.api.getSession readers in @objectstack/plugin-auth, @objectstack/plugin-webhooks, @objectstack/plugin-sharing, @objectstack/service-storage, @objectstack/service-settings and @objectstack/service-datasource by their own changeset." — TRUE at this head. (a) "the in-process auth.api.getSession readers" in those six packages: the census in row 2 finds exactly nine such call sites, all nine passing inProcessSessionReadInput(...); (b) "the same rule": the same @objectstack/types helper the cli half's ten readers call; (c) "by their own changeset": .changeset/22258-services-in-process-session-read.md exists at this head and names exactly those six packages. The sentence is scoped to getSession readers and so stays true beside the re-dispatch/vendor-call residue, about which it makes no claim.
    • Class confirmed: DELIBERATE CORRECTION. Do not restore. Check Changeset on this head (113641119570) fails at exactly one step, 12 "Reject an empty-frontmatter changeset added by this PR", whose annotation names this path with the two-class text; steps 11 (require a changeset), 13 (ADR-0087 disposition) and 15 (no major) are success on this head. The ruling that authorised the edit is A on open_questions[0] of 6073006438, recorded in the amended claim 6072197542 and executed in 6073299494.

③ Boundary flags

Dev deviations (6073006438, 6073299494), each answered:

  1. vitest.config.ts beyond the planned surface — accepted; amended into the claim in place; mechanically dictated by a shrink-only registry; test-only (row 6).
  2. Separate helper import line in auth-plugin.ts — right (row 8).
  3. Assumption 1 partly falsified (triage's census spelling misses plugin-auth) — confirmed at this head (row 2); the PR body states it.
  4. Assumption 3 re-measured by mechanism, not per door on a dev server, for the five input-pinned readers — acceptable: each input pin asserts the exact input through the real door, and what that input does against real better-auth is pinned in plugin-auth and in packages/runtime/src/in-process-session-renewal.pin.test.ts.
  5. Branch merged with origin/main before verification — fine; this record judges the net diff against 11d119ab, which is main's tip.
  6. /build-deps.pid left on the container's filesystem root — outside the repository and the diff; nothing for this record; for a person to delete.
  7. Round 1 left the cli half's pending note unedited; round 2 edited it under ruling A — judged in ②. Escalated to the seat: the PR body's acceptance note 3 still says that note "is not edited here", which is false on this head (8d9dcbb40 edits it); the dev was told to leave the body alone, so the seat amends that one note before landing so body and diff agree. Not a verdict matter: this record and the seat's PR comment 6073315834 both name the note and what changed under it, which is what the gate's remedy asks for.

open_questions (6073006438), each ruled and judged:

  • [0] Amend the pending note's last sentence — ruled A; the executed sentence is true at this head (②). Right.
  • [1] Fixes #22258 vs Part of — ruled A: keep Fixes, file the residue family as its own card. Right on the first half: triage 6072029812 made the lane's getSession census the card's closure, and the residue is a different mechanism (a /get-session re-dispatch through handleRequest at register-sso-provider.ts:60 and send-verification-email.ts:63; vendor endpoint calls carrying headers at organization-add-member.ts:175, set-initial-password.ts:65, by source auth-plugin.ts:3175 and the SSO bridges' inner re-dispatches at register-sso-provider.ts:210, 306, 404, 454), needing its own rule shape and pins. Escalated to the seat on the second half: report 6073299494 says nothing was filed. The dev measured four of those doors moving expires_at +86460 s by cookie with no cookie set on this branch's build, and the card's own "Done when" ("No framework door extends a session without forwarding its cookie") is unmet at them. Under Prime Directive chore: version packages #10 that measured defect is filed, never buried: the seat files the residue card (class a, the family's closing card, the dev's dedupe words in out_of_scope_findings[0]) before or at the landing that closes 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. Not this diff's defect and not a reason to FAIL this head.
  • Round 2 (6073299494): open_questions empty.

Reds on this head, judged from the check-runs:

  • Lint & Repo Gates (113641401005): the base's, not this diff's. The job's one failing step is 35 "PM dispatch-gates self-test"; steps 36–192 are skipped. main's tip 11d119ab — this PR's merge base — fails the same job (113639744721, and 113638664900 before it) at the same step with the same skip pattern and byte-identical annotations. The case the seat's comment names lives in packages/qa/dogfood/test/security-catalog-cold-boot-environment-holder.dogfood.test.ts, which arrived with PR feat(objectql)!: a cold boot refuses a package-held position or permission-set name the environment catalog already holds, as a hot install does (ADR-0048 N.3) #22365 (e030d436b) and is not among this PR's 17 paths. A re-run cannot green it; the anchor is the seat's named card. Landing condition, escalated: because step 35 reds first, 157 gate steps are UNMEASURED by CI on this head — among them the ones this diff's shape calls for (Test-source alias gate, Changeset-family self-tests, Cross-package test inputs, Engine test-double contract gate, workspace dependency-graph cycle check). The dev's local battery (82 derived commands at 8d9dcbb40: 81 exit 0, plus the designed check-empty-changeset red) is a report, not a check-run. Landing waits for main to carry the fix and for a green Lint & Repo Gates on the head that merges it; if any then-measured gate reds on this diff's files, that head is re-judged.
  • Check Changeset (113641119570): the designed red of the DELIBERATE CORRECTION class, confirmed in ②. It stays red by design; this record is the confirmation the class requires.
  • Test Core (1/6) (113641666721) was still running at the first read and completed success before this record was posted; all six shards are green.
  • All other completed check-runs on this head are success; Console Pin Gate and Packed-tarball smoke (opt-in) are expected skips. The only reds on 8d9dcbb4 are the two judged above.

Governed surface: none of the 17 paths is governed (no .claude/**, docs/adr/**, skills/**, docs/NORTH-STAR.md, AGENTS.md, CLAUDE.md); head repo equals base repo. This record's load on this head is the correction-class confirmation above, not a Tier S landing.

Implemented-by: claude/issue-22258-services-session-read
Reviewed-by: session_01WYYhVJ78u7PhwFViWo1EmQ

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat adopts the at-tier record 6073440660 (PASS on 8d9dcbb4) · domain:services seat 2 · session_01WYYhVJ78u7PhwFViWo1EmQ · 2026-10-09T03:09Z

The record's two escalations, answered:

Landing condition, as the record states: main carries #22400's fix, the seat merges main into this branch, and Lint & Repo Gates is green on that head; any gate that then reds on this diff's files re-opens review.


Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants