Repository navigation
Commit fb63d39
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuMjbPDrsigsyqoKsV4WZQMz)_
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 5f4a3f0 16770e8` 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.**
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derives 89 commands for the 5 paths. All 89
ran at `16770e81ae` after the full build, and all 89 exited 0.
- `--ran`: 89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN.
- `check-governed-merges.mjs --branch HEAD`: NOT governed, 1,212 changed
lines.
- **Docs drift** (`node scripts/docs-audit/affected-docs.mjs --json
5f4a3f0`, exit 0): 31 pages, 12 of them release-owned. Every page is
reached through a literal the moved bodies carry unchanged
(`sys_session`, `sys_account`, `sys_organization`, `sys_two_factor`,
`expires_at`, `revoked_at`, `previous_password_hashes`,
`must_change_password`, `MFA_REQUIRED`, `PASSWORD_EXPIRED`,
`PASSWORD_RESET_REQUIRED`), or through `resolveActor`, which the class
keeps (`system-context.mdx`, whose census reads the same at base and
head). No page restates a rule this move changes, so no page edit is
owed beyond the census's own two documents.
- **Serial constraints** (in the claim): of the open PRs, only #25309
touches `auth-manager.ts`, at the import block and near `:8011`, outside
every moved body. `git merge-tree` of this branch with #25309's head is
clean.
## 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](https://claude.ai/code/session_01LSXBc2GCBwiAL7vV4kWkyr)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01LSXBc2GCBwiAL7vV4kWkyr
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4d7e2b1 commit fb63d39
5 files changed
Lines changed: 731 additions & 481 deletions
File tree
- content/docs/permissions
- docs/audits
- packages/plugins/plugin-auth/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
96 | | - | |
| 96 | + | |
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
| |||
210 | 210 | | |
211 | 211 | | |
212 | 212 | | |
213 | | - | |
| 213 | + | |
214 | 214 | | |
215 | 215 | | |
216 | 216 | | |
| |||
249 | 249 | | |
250 | 250 | | |
251 | 251 | | |
252 | | - | |
253 | | - | |
| 252 | + | |
| 253 | + | |
254 | 254 | | |
255 | 255 | | |
256 | 256 | | |
| |||
307 | 307 | | |
308 | 308 | | |
309 | 309 | | |
310 | | - | |
| 310 | + | |
311 | 311 | | |
312 | 312 | | |
313 | 313 | | |
314 | | - | |
| 314 | + | |
315 | 315 | | |
316 | 316 | | |
317 | | - | |
| 317 | + | |
318 | 318 | | |
319 | 319 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | | - | |
| 93 | + | |
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
97 | | - | |
| 97 | + | |
98 | 98 | | |
99 | 99 | | |
100 | | - | |
| 100 | + | |
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
| |||
130 | 130 | | |
131 | 131 | | |
132 | 132 | | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
133 | 137 | | |
134 | | - | |
135 | | - | |
136 | | - | |
137 | | - | |
| 138 | + | |
138 | 139 | | |
139 | 140 | | |
140 | 141 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
175 | 175 | | |
176 | 176 | | |
177 | 177 | | |
178 | | - | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
179 | 184 | | |
180 | 185 | | |
181 | 186 | | |
| |||
0 commit comments