Skip to content

docs(adr-0127): correct three measurably-false statements about the session-cache doors - #12835

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-12734-adr-0127-door-posture-corrections
Aug 28, 2026
Merged

docs(adr-0127): correct three measurably-false statements about the session-cache doors#12835
os-zhuang merged 1 commit into
mainfrom
claude/issue-12734-adr-0127-door-posture-corrections

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes #12734

Draft only — docs/adr/** is governed surface. The maintainer's merge IS the review record. Not to be flipped ready, enqueued, or auto-merged, including by the dispatching seat.

Three statements of fact about the current plugin-auth implementation were false against main. ⭐ No ADR decision moves — see "D7 is byte-identical" below, which is asserted mechanically rather than promised.

All measurements re-taken on the post-merge tree (origin/main at f75a38afa, after PR #12735), not from the card body — the card's own numbers predate that merge.


Statement 1 — :148 "boot-refused and pinned"

Old:

The secondaryStorage door into D7's rejected architecture is boot-refused and pinned

Why it is false: ObjectStack authors no such refusal, and the shipped module now says so in terms.

Evidence — packages/plugins/plugin-auth/src/secondary-storage.ts:47-49:

⛔ Neither door is boot-refused by ObjectStack, and that is the ruled posture, not a gap: opt-in with the cost stated. Adding a refusal to either needs a new maintainer ruling.

There is a boot throw in the default composition — but it is better-auth 1.7's, and it is conditional. packages/plugins/plugin-auth/src/session-of-record.test.ts:528 pins it and attributes it: better-auth hard-refuses secondaryStorage without session.storeSessionInDatabase, reachable only because the OIDC provider is on by default. session-of-record.test.ts:410 reaches the cache-backed session store by passing plugins: { oidcProvider: false } and changing nothing else — so the throw is not a guard this repo owns.

⭐ The likely origin of the error, written into the correction rather than fixed silently

packages/plugins/plugin-auth/src/secondary-storage.ts:87-89 documents a real boot throw:

Fixed-window counter, required by better-auth 1.7 for rateLimit.storage: 'secondary-storage' (it throws at boot without it).

That throw is real, and it contains the words "secondary storage" — but it is a rate-limit configuration requirement, not a refusal of the session-cache door. The correction names both throws and says which is which, so the next reader cannot re-derive the same wrong generalisation from the same evidence.

Statement 2 — :113 "with the same result"

Old:

cookieCache is a different key reaching the same architecture, with the same result.

Why it is wrong: the direction is right; the magnitude is not. ⛔ Deliberately not overcorrected into "harmless" — the new text keeps the failure direction and ends "Smaller is not harmless — a bounded window in which a revoked session still authenticates is a tolerance D4 never granted, so D7's ruling covers this door too."

Evidence — packages/plugins/plugin-auth/src/auth-manager.ts:1403-1421, the measured table PR #12735 landed. Each of its three claims was re-verified first-hand in this branch against the installed better-auth 1.7.1 (read out of node_modules, not off the ^1.7.1 range):

claim verified at reading
window bounded, default 300s better-auth/dist/cookies/index.mjs:50 createCookie("session_data", { maxAge: options.session?.cookieCache?.maxAge || 300 })
same default on the set path better-auth/dist/cookies/index.mjs:99 symmetricEncodeJWT(..., options.maxAge || 300)
stateless refresh force-disabled when a database is configured better-auth/dist/context/create-context.mjs:149-165 if (isStateful && refreshCache) { logger.warn(...); return false; }
sensitive operations already bypass it better-auth/dist/api/routes/session.mjs:277-280 getAuthoritativeSessionFromCtx: if (!isStateful(ctx)) return getSessionFromCtx(ctx); ... getSessionFromCtx(ctx, { disableCookieCache: true })

Statement 3 — :148 "unguarded (measured: zero occurrences repo-wide)"

Old:

the cookieCache door reaches the same architecture and is unguarded (measured: zero occurrences repo-wide, so it is unconfigured rather than protected)

Why both halves are false: this one did not exist when the card was written — PR #12735 created it.

"zero occurrences repo-wide" — re-measured. ⚠️ Counted as lines, and every matched line was read before publishing the number (docblocks quote code, so a raw count is not a fact about occurrences):

git grep -n "cookieCache" -- ':!**/node_modules/**' ':!**/dist/**'

23 matched lines across 5 files — auth-manager.ts (6), session-of-record.test.ts (7), secondary-storage.ts (2), .changeset/plugin-auth-cookiecache-cost-disclosure.md (6), and ADR-0127 itself (2). Not zero.

"unconfigured rather than protected" — also false. The door now carries a measured cost note (auth-manager.ts:1382-1438) and an end-of-chain observation pin (session-of-record.test.ts:479).

⚠️ The correction does not promote it to "guarded" — session-of-record.test.ts:484-486 is explicit that the pin is an observation, not a refusal, so the new text says "tripwire, not a rejection". It also records the sharper truth the old sentence inverted: cookieCache is not reachable through ObjectStack config at all, by construction rather than refusal — AuthConfigSchema.session (packages/spec/src/system/auth-config.zod.ts) declares expiresIn / updateAge only.

Net effect: the asymmetry in the old paragraph was backwards. It presented secondaryStorage as the guarded door and cookieCache as the open one. The measured truth is that neither is guarded, and the reachable, unbounded one is secondaryStorage.


D7 is byte-identical — asserted, not promised

The card's fence is that no ADR decision may move. Checked mechanically rather than by eye:

# D7 section, excluding only the one amended sentence
sed -n '/^### D7 —/,/^### D8 —/p' FILE | grep -v "sibling key\|different key" | sha256sum
base: 089ce7a56be3344fbbd22bba27b4733c3aa976a27bad5bef328c73d09a8fa947
new:  089ce7a56be3344fbbd22bba27b4733c3aa976a27bad5bef328c73d09a8fa947

The whole-file diff also contains no removed decision or ruling line — every difference in the decision/ruling extraction is an addition in Consequences. And the document's ruling marker count is unchanged (6 before, 6 after), so the two new Consequences bullets introduce no new ruling: the summary sentence deliberately starts "That is the ruled posture…" rather than carrying the arrow this document reserves for rulings.

An amendment line was added in the header, following the established convention in this directory (ADR-0059:7, ADR-0045:3): **Amended 2026-08-28 (#12734), measurements only — no decision changes**.

Verification

Gate union re-derived live on the actual changed set, not taken from the dispatch brief:

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
→ change set: 1 path · docs/adr/0127-...md
→ 5 families matched

⚠️ The brief's predicted list diverged from the live derivation and the live one was followed: the brief named check:governed-prose and "the docs-audit families"; the derivation named check:pm-governed-merges and no docs-audit family. Both of the brief's families were run anyway — green — and are reported below marked as extras.

All 7 run at commit ad8b5a186 on a clean tree, exit codes captured before any pipe, each quoted from the gate's own verdict line:

gate exit the gate's own verdict line
check:adr-anchors 0 check-adr-anchors: OK (52 anchored file(s), every governing ADR still referenced; 125 decision number(s) ...; 29690 citation(s) across 3748 file(s) resolve)
check:doc-authoring 0 ✓ doc authoring guard: 391 files clean — no bare metadata literals.
--filter @objectstack/lint check:doc-formula-expressions 0 ✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 424 files / 1453 TS blocks judged clean
check:pm-governed-merges 0 ✓ check-governed-merges --self-test: 206 assertions ... live: the real generator declared 9 output(s) and certified this tree
scripts/check-adr-links.mjs 0 ✅ check-adr-links: 564 relative link destination(s) under docs/adr/ resolve
check:pm-governed-prose (extra) 0 ✓ check-governed-prose: 2 instruction surface(s) name all 5 registered governed surfaces ... and claim no others.
check:docs-audit-scope (extra) 0 ✓ docs-accuracy-audit scope is in sync with content/docs/: 189 hand-written doc(s).

⚠️ check:doc-formula-expressions first returned PREREQUISITE NOT MET twice (@objectstack/formula, then @objectstack/lint not built). That is NOT MEASURED, neither pass nor fail — recorded rather than counted as green. Both prerequisites were built and the gate re-run to a real exit 0.

Positive control — the gates actually read the edited paragraph

A green gate on a prose edit proves nothing unless the gate is shown to read the changed text. Mutation planted in the new paragraph, restore via an EXIT INT TERM trap on an absolute path:

  • mutation confirmed on disk before measuring: injected string present (1), original absent (0), blob 9549067a…d3997abb…
  • gate on the mutated tree: exit 1 (red) — A decision record is only binding if the next reader can reach it.
  • restore proven by bytes, not by exit code: disk blob back to 9549067acb623fcfce9dfd1a168498a078255cb5 = HEAD blob, git diff HEAD empty
  • gate on the restored tree: exit 0

Other

  • check:nul-bytes exit 0 — check-nul-bytes: OK (scanned 7138 text file(s) ... no raw ASCII control bytes); plus a direct control-byte scan of the edited file, no matches.
  • No changeset: docs-only, docs/adr/** alone, publishes nothing. The derivation's own line — "write one unless this card is docs-only". skip-changeset applied accordingly.
  • ⚠️ packages/spec was read for the AuthConfigSchema.session citation and not edited. The diff is one file.

Generated by Claude Code


Generated by Claude Code

…ession-cache doors

Three statements of fact about the CURRENT plugin-auth implementation were
false against main. No ADR decision moves — D7's ruling is byte-identical.

1. Consequences said the `secondaryStorage` door is "boot-refused and pinned".
   ObjectStack authors no such refusal; secondary-storage.ts:47-49 says the
   opposite in terms. The default composition's boot throw is better-auth 1.7's,
   conditional on the OIDC provider being on by default, and evaporates under
   `plugins: { oidcProvider: false }`.

   The likely origin of the error is named rather than silently fixed: the real
   boot throw at secondary-storage.ts:87-89 is a `rateLimit.storage` requirement,
   not a refusal of the session-cache door.

2. D7's closing sentence said `cookieCache` reaches the same architecture "with
   the same result". Direction is right, magnitude is not: the session of record
   stays in sys_session, the window is bounded at cookieCache.maxAge (300s), and
   sensitive operations already bypass it. Corrected without overcorrecting into
   "harmless".

3. Consequences said the `cookieCache` door is "unguarded (measured: zero
   occurrences repo-wide)". Both halves are now false — PR #12735 landed a cost
   note and an observation pin.

Every corrected claim cites the file+line it rests on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194kbQJxUvv2yvsGRtuXpP5
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Aug 28, 2026
@os-litant os-litant added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed documentation Improvements or additions to documentation labels Aug 28, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review August 28, 2026 08:02
@os-zhuang
os-zhuang requested a review from hotlong as a code owner August 28, 2026 08:02
@os-zhuang
os-zhuang merged commit aef1b7e into main Aug 28, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12734-adr-0127-door-posture-corrections branch August 28, 2026 08:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants