Skip to content

refactor(plugin-auth): move AuthManager's password, auth-gate and session policy into auth-manager-password-session-policy.ts (#25224 cut 3) - #25362

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-lg3wrj
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-lg3wrj

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Part of #25224
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6348891160, 6351993113

The basis for the Clause-②: no line: this is a move. No accept set, export, route, refusal or answer changes. The .d.ts bar is ruling 6348891160 item 1 (on #25222, covering this card). @objectstack/plugin-auth's published dist/index.d.ts and dist/index.d.mts are raw byte-identical to base (0 diff lines, so the union-order normalisation has nothing to do), with the ruling's positive control. Its entry exports the same 155 names under ESM and CJS (measured below).

Cut 3 of the auth-manager.ts split: the password, auth-gate and session policy (ADR-0069). This is the rest of the card's "password policy, lockout, auth gates, session controls" area; cut 2 took its sign-in lockout path. Twelve AuthManager method bodies, 481 lines, move unedited (re-indented one level) into one new module, packages/plugins/plugin-auth/src/auth-manager-password-session-policy.ts. #25224 stays open for the later cuts.

This follows the amended claim 6356007354, which amends 6354594310 on the file the sweep PR left (#25326, landed note 6355882048). The shape is cut 1's and cut 2's, which is #23050's.

Branch: claude/project-thread-lg3wrj, the harness-pinned branch for this session, restarted from origin/main 5f4a3f088c after #25326 merged. It names no issue, so the card relation is this body's first line.

auth-manager.ts goes from 9,927 to 9,587 lines. The cut is +731 / −481 over 5 files: 1,212 changed lines, under the 3,000-line line, and two of the files are the regenerated tenant-audit census. It is not a governed surface (check-governed-merges.mjs --branch HEAD: 0 of the 5 paths hit the register).

Why this seam, and what it leaves

Ruling 6351993113 held back nine methods until their counted query-options erasures were swept. #25326 did that in place, so the bodies can move without carrying a counted site into a file the baseline does not list.

This cut takes eight of those nine, plus the four private bodies of the same area that they lean on: assertPasswordComplexity, stampPasswordChangedAt, parseHashes and resolvePasswordChangeUserId.

What stays in the class:

  • stampIdentitySource, the ninth. It is cloud ADR-0024 D4 provenance (sys_user.source), a different area, and moves with that area's cut. It no longer carries a counted site, so nothing holds it back.
  • isAuthGateActive, computeAuthGate, noteMustChangePasswordIssued and checkPasswordComplexity. Each is a one-expression body no longer than the stub it would leave, and isAuthGateActive is the per-request check its docblock calls cheap.

What moved

method base body (signature to brace) lines members it reads
assertPasswordComplexity (D1) :8895–:8912 18 config
refreshMustChangeCacheIfStale :8998–:9017 20 _mustChangeCache, _mustChangeRefreshing (both written), getDataEngine
refreshOrgMfaCacheIfStale :9025–:9045 21 _orgMfaCache, _orgMfaRefreshing (both written), config, getDataEngine
resolveUserAuthGates :9159–:9251 93 _mustChangeCache, _orgMfaCache, config, getDataEngine, refreshMustChangeCacheIfStale, refreshOrgMfaCacheIfStale
stampPasswordChangedAt (D3) :9258–:9285 28 getDataEngine, logDurabilityDegradation
parseHashes :9291–:9299 9 none
resolvePasswordChangeUserId :9324–:9353 30 resolveActor
assertPasswordNotReused (D3) :9362–:9408 47 config, getDataEngine, parseHashes
recordPasswordHistory (D3) :9414–:9459 46 config, getDataEngine, logDurabilityDegradation, parseHashes
unlockUser (D2) :9685–:9731 47 getDataEngine, logDurabilityDegradation
enforceSessionControls (D4) :9740–:9818 79 config, getDataEngine, logDurabilityDegradation
enforceConcurrentCap (D4) :9825–:9874 50 config, getDataEngine, logDurabilityDegradation

Lines are base 5f4a3f088c. The members come from a walk of every this.X in each body with the TypeScript compiler API. No body has a bare this, this[...], arguments or super. Only the two cache refreshes assign a this member, and only their own cache fields.

No module-level declaration moves. The two imports the moved bodies use (AuthGate from @objectstack/core, recoverInternalFieldsForSystemRead from ./internal-field-readback.js) are also used by code that stays, so auth-manager.ts keeps them. The new module imports each from the same place.

The shape

  • The class keeps every method at its place, with its name, signature and docblock: the published .d.ts lists each one. Each body is now a host literal of exactly the members that body reads, typed as a Pick of PasswordSessionPolicyHost, plus .call. parseHashes reads no member and calls its function directly.
  • PasswordSessionPolicyHost lives in the new module. It names the 11 members the bodies read, each typed as the class member (AuthManager['getDataEngine'], …). The module imports AuthManager as a type only, so there is no runtime cycle.
  • Methods go as late-bound closures ((...a) => this.getDataEngine(...a)), so a test double or spy set on the manager is still the one a body reaches. That includes parseHashes and the two cache refreshes, which bodies here ask through the manager.
  • config goes as a getter. The class reassigns it after construction (setBaseUrl, applyConfigPatch), and the bodies read it after an await.
  • The four cache fields go as accessors. _orgMfaCache, _orgMfaRefreshing, _mustChangeCache and _mustChangeRefreshing go as getters. In the two refreshes that assign them, they go as a get/set pair, whose setter writes the manager's own field through a closure ((v: AuthManager['_mustChangeCache']) => { this._mustChangeCache = v; }). The refreshes write from a background task that settles after the method returned, and the class's isAuthGateActive and noteMustChangePasswordIssued read and write the same fields. So every read and every write lands on the field, never on a copy.
  • Nine of the methods are no longer async. Each returns the moved async function's own promise, so a caller awaits the same promise the body settles. Building the host cannot throw, so no method now throws synchronously where it used to reject. refreshMustChangeCacheIfStale, refreshOrgMfaCacheIfStale and parseHashes were synchronous and stay so.
  • Each call builds one small host object. resolveUserAuthGates, enforceSessionControls and the two refreshes run per request on the session path, so that is one allocation per call there. This is the same per-call cost cut 1 and cut 2 put on their request-path methods.
  • Nothing re-exports the new module, so the published surface does not move.

Readers re-pointed in the same PR

reader what it reads base head
api-error-code.pin.test.ts the sum of new APIError( sites over the files AuthManager's bodies live in (floor 24) 17 + 9 + 1 = 27 15 + 9 + 1 + 2 = 27. The PASSWORD_POLICY_VIOLATION throw of assertPasswordComplexity and the PASSWORD_REUSE throw of assertPasswordNotReused moved, so the module joins the list.
the tenant-audit census (check-tenant-audit-census.mjs, its committed page and counts file) every engine write call site, keyed by file auth-manager.ts update: sys_user 5, sys_session 3, sys_account 1, sys_two_factor 1 auth-manager.ts: sys_user 2. auth-manager-password-session-policy.ts: sys_user 3, sys_session 3, sys_account 1, sys_two_factor 1. Regenerated with node scripts/tenant-audit-census.mjs --write, and the page's hand-written erased-receiver figures follow (see below).

Without the pin edit the sum would read 25 and the floor of 24 would hold in silence, which is the drift the card forbids.

Measured as reading no moved line:

  • Plugin-factory scans in better-auth-schema-parity.test.ts and managed-extension-fields.test.ts: the moved bodies import only better-auth/api, which the scan pattern does not match.
  • pnpm check:dispatcher-error-vocabulary: exit 0. Its auth-manager.ts row names deliverPhoneOtp, which did not move.
  • pnpm check:durability-log-level: exit 0.
  • The query-options erasure ratchet: exit 0, "baseline key set verified against 5f4a3f0: no files added". The moved bodies' engine.update(…, { context: SYSTEM_CTX } as any) casts move as written; the ratchet does not count them.
  • Every symbol anchor into the file, with the same reading at base and at head: check-adr-symbol-anchors (2,383 anchors, 466 symbol), check-adr-anchors, check-scripts-symbol-anchors, check-spec-docblock-symbol-anchors, check-system-context-census and check-platform-checklist, all exit 0. Its one anchor into this region, auth-manager.ts#computeAuthGate, names a method the class keeps. These checkers were run by hand: dispatch-gates does not derive the ADR one.

The census reading

The census reads 237 engine write sites at base and at head. Compared per site, with the new module's file name mapped back to auth-manager.ts, every site keeps its object, verb, tenancy, context and elevation reading: 0 differences, and 0 unresolved and 0 unledgered receivers on both sides.

What changes is how the census reaches the 8 engine.update sites in the moved bodies:

method object sites
resolveUserAuthGates sys_user 1
stampPasswordChangedAt sys_user 1
recordPasswordHistory sys_account 1
unlockUser sys_user, sys_two_factor 2
enforceSessionControls sys_session 2
enforceConcurrentCap sys_session 1
  • In the class, their receiver is const engine = this.getDataEngine(). The census reads it through the method's declared return type in the same file, so the 8 counted as "receiver carried a readable engine type".
  • In the module, the same text runs on a this typed as Pick<PasswordSessionPolicyHost, ...>. The census does not follow a function's this parameter, so it places them by their literal object names, all of them declared objects.
  • The generated table moves readable engine type 193 → 185 and placed by the object NAME 34 → 42, the only total that moves. The page's two hand-written erased-receiver counts (PROSE_COUNTS) are that sum plus the parameter and ledger rows, so they move 44 → 52. 52 of 237 is 21.9%, so the sentence that called 44 "just under a fifth of the population" now says "just over a fifth".
  • The corpus-scale block (not enforced) is re-measured too: 712 sources, one of them the new module, and 172 non-engine subtractions, the same count at base and at head. The page's older 708 and 171 were measured at 75983184c.

The effect is the same as cut 2's (3 sites there). Keeping these 8 typed would mean annotating engine in the moved bodies, which is not a move, or teaching the census to read this parameters, which is a census logic change. So neither belongs in this PR.

Proof

Builds and suites ran through os-verify-lock, and every exit code was captured before any pipe. Base 5f4a3f088c, head 16770e81ae. A full turbo run build ran first. Then plugin-auth was built and its suite run at base, then built and run at head, on the same dependency dists.

1. The moved text.

  • For each of the 12 methods, the base class body and the new module's function body are the same TypeScript token sequence (scanner tokens, kind and text, trivia skipped, each template literal re-scanned at its substitutions as the parser does).
  • Every line is the base line minus exactly two leading spaces, and the parameters and return type are the same text.
  • The class method keeps its docblock and signature, with async dropped.
  • Result: 12 of 12 pass, 481 body lines.
  • Negative control: one token changed in the module (60_000 → 60_001 in refreshMustChangeCacheIfStale) makes that method fail (tokens and lines DIFFER), and the run exits 1. Restored afterwards; git diff HEAD was empty.
  • An independent reading: git diff --color-moved=plain --color-moved-ws=allow-indentation-change 5f4a3f088c 16770e81ae over the two source files marks 457 of the 466 deleted lines and 485 of the 710 added lines as moved. The other added lines are the stubs' host literals, PasswordSessionPolicyHost, the module header, the per-function pointer docblocks and the new import.

2. .d.ts and exports (ruling 6348891160 items 1 and 2).

file raw base raw head raw diff lines normalised base normalised head
dist/index.d.ts 4bc04c66f724c79782493249820b1b7d9bff7c826931ce6b362d325af8797891 4bc04c66f724c79782493249820b1b7d9bff7c826931ce6b362d325af8797891 0 3c8612c88f3f41c963c243e91c2523d2754b2accb4a4c2ba52a5627fd6eaa1e5 3c8612c88f3f41c963c243e91c2523d2754b2accb4a4c2ba52a5627fd6eaa1e5
dist/index.d.mts d70d7d8063053de2d39bd0dbae06423bbe0ef1dcecf5130b867c6f4d79a0c8cd d70d7d8063053de2d39bd0dbae06423bbe0ef1dcecf5130b867c6f4d79a0c8cd 0 51510aad856bbacd24757d1c62dc7273bfd6313ed543c8b1f1e86195fc2c7cd0 51510aad856bbacd24757d1c62dc7273bfd6313ed543c8b1f1e86195fc2c7cd0
  • The normaliser, verbatim (the ruling's one-liner): node -e 'const s=require("fs").readFileSync(process.argv[1],"utf8");process.stdout.write(s.replace(/"[^"\n]*"(?:\s*\|\s*"[^"\n]*")+/g,r=>r.split(/\s*\|\s*/).sort().join(" | ")))' dist/index.d.ts | sha256sum.
  • The raw files are already byte-identical, so no union-order swap occurs in this cut: items 1(b) and 1(d) hold with nothing to account for.
  • Positive control: the head dist/index.d.ts with one private member made public (private assertPasswordNotReused; → assertPasswordNotReused;, line 4001, a one-line change) hashes raw 7d3450b84103f5d80cfbc8a2e4391ee22919d8137744c6a4550a1b237bcc5115 and normalised 56cff0cedafef19e8cf0e559a612c61d84b48aef3134c6c01d977887a8ef061e. Both differ from the readings above.
  • Export lists: ESM dist/index.mjs and CJS dist/index.js each have 155 names at base and at head, the same list (the sha256 of the sorted names is ed3ac8b902031d30… on all four).
  • The bundle: 18 files in dist/ on both sides, the same list. Only index.js, index.mjs and their two .map files differ, as a move of code inside the bundle must.

3. Tests and lint.

  • pnpm exec vitest run --maxWorkers=3 in packages/plugins/plugin-auth: base Test Files 187 passed (187), Tests 3525 passed | 10 skipped (3535), VERDICT command-exit 0. Head: the same counts, VERDICT command-exit 0.
  • pnpm --filter @objectstack/plugin-auth typecheck (both tsc projects and check:test-typecheck): exit 0.
  • eslint --no-inline-config (the root lint script's binary and flags) over the 3 TypeScript files: exit 0.

4. Gates.

Changeset

None: skip-changeset, per ruling 6348891160 item 4. The published declarations and export lists are unchanged, and the bundle's bytes move only because code moved inside it.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01LSXBc2GCBwiAL7vV4kWkyr

…sion policy bodies into auth-manager-password-session-policy.ts

Twelve method bodies (ADR-0069 D1/D2/D3/D4 and the per-user gate
posture) move unedited; the class keeps each method, its docblock and
signature, and hands each body the members it reads.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSXBc2GCBwiAL7vV4kWkyr
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 23, 2026
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests labels Oct 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 48 documentable anchor(s).

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 5c9ba22622f9a1ea473a164421d52f934e095919.

⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 38 pages), sys_user (literal, 42 pages)
  • 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 — 19 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 5c9ba22622f9a1ea473a164421d52f934e095919 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 27a4d449ecebe6a223cf11b2a34e9ff75ec86697 — the merge of head 16770e81ae474713416573d36ebff37cd10feb79 into base 5c9ba22622f9a1ea473a164421d52f934e095919, 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 27a4d449ecebe6a223cf11b2a34e9ff75ec86697 && git checkout 27a4d449ecebe6a223cf11b2a34e9ff75ec86697
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5c9ba22622f9a1ea473a164421d52f934e095919 16770e81ae474713416573d36ebff37cd10feb79 && git checkout -B drift-repro 5c9ba22622f9a1ea473a164421d52f934e095919 && git merge --no-ff 16770e81ae474713416573d36ebff37cd10feb79

node scripts/docs-audit/affected-docs.mjs --json 5c9ba22622f9a1ea473a164421d52f934e095919

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5c9ba22622f9a1ea473a164421d52f934e095919 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-23T03:47Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25224 cut 3 (amended claim 6356007354, Ruling-ref: 6348891160, 6351993113; os-dev-report 6356850475; card session branch claude/project-thread-lg3wrj) · reviewed head 16770e81ae474713416573d36ebff37cd10feb79 · merge base 5f4a3f088c

  • Shape: twelve AuthManager method bodies (481 lines) move unedited into auth-manager-password-session-policy.ts. They cover password policy, the auth-gate caches, admin unlock and session controls (ADR-0069), and the cut keeps cut 1's shape. config and the four cache fields become getters (get/set pairs where the two refreshes assign them). 5 files, +731 / −481.
  • Ruling 6351993113 scope: this follows the sweep PR refactor(plugin-auth): drop AuthManager's 12 counted query-options any assertions and ratchet the erasure baseline (#25224) #25326, which landed first and left 0 counted erasure sites in these bodies. pnpm check:query-options-erasure exits 0 with the baseline key set unchanged (report). The cut moves 8 of the ruling's 9 methods. stampIdentitySource is cloud ADR-0024 D4 provenance and stays for that area's own cut. The ruling sets an order (sweep before move) and does not require one PR, so the seat accepts the split. The card stays open for it.
  • Move proof (report): 12 of 12 bodies are token-identical, with a failing negative control (60_000 → 60_001). --color-moved marks 457 of 466 deleted lines as moved.
  • .d.ts, ruling 6348891160 item 1, re-run by the seat in its own worktree. turbo run build --filter=@objectstack/plugin-auth ran at 5f4a3f088c and at this head, and the hashes are the same at base and head:
    • dist/index.d.ts: raw 4bc04c66f724c79782493249820b1b7d9bff7c826931ce6b362d325af8797891, normalised 3c8612c88f3f41c963c243e91c2523d2754b2accb4a4c2ba52a5627fd6eaa1e5.
    • dist/index.d.mts: raw d70d7d8063053de2d39bd0dbae06423bbe0ef1dcecf5130b867c6f4d79a0c8cd, normalised 51510aad856bbacd24757d1c62dc7273bfd6313ed543c8b1f1e86195fc2c7cd0.
    • The raw diff is 0 lines.
    • The seat's own positive control uses a different member from the report's: head line 4174, private enforceConcurrentCap, made public. Raw 62e4a7312f7072a1… and normalised 81e3dfc857d8abb0… both moved. The report's control (assertPasswordNotReused, line 4001) also moved both.
    • Export lists: 155 names, sorted hash ed3ac8b902031d30 on all four (report).
  • Anchors, re-run by the seat: check-adr-symbol-anchors exits 0 at base and head, both 2383 anchors … 466 symbol (440 declaration, 26 literal). The report also ran check-adr-anchors, check-scripts-symbol-anchors, check-spec-docblock-symbol-anchors, check-system-context-census and check-platform-checklist at both ends, all exit 0.
  • Path readers in this PR:
    • api-error-code.pin.test.ts adds the new module, and the count holds at 27 = 27.
    • The tenant-audit census page and counts file were regenerated with --write. The two hand-written figures go 44 → 52. Per site, 237 = 237 with 0 differences. The only total that moves is placed-by-object-name 34 → 42, from the 8 engine.update sites in the moved bodies.
    • Seat re-run on this head: check-tenant-audit-census exits 0 (237 sites, 23 prose figures held).
  • Tests and gates (report): plugin-auth 187 files, 3525 pass / 10 skipped, the same as base. typecheck and eslint exit 0. dispatch-gates --ran 89 / 89. CI on this head: 40 success and 14 expected skips, none failed or pending. Mergeable clean, and git merge-tree onto today's main is clean.
  • Body: the seat added the attribution header and requested os-sales through the relay (run 39896677623, read back identical).
  • Declarations: refactor(plugin-auth), Clause-②: no, Metadata-protocol: none, skip-changeset. No contract-review record or dogfood battery is owed. Out-of-scope findings: none.
  • Landing: not governed (check-governed-merges --pr 25362: 1212 lines). Ready + auto-merge through the relay. The card stays open and dispatched for the stampIdentitySource area and any later cuts.

Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants