Skip to content

Commit ca381a8

Browse files
refactor(plugin-auth): move AuthManager's database hooks and identity-source stamp into auth-manager-database-hooks.ts (#25224 cut 4) (#25383)
<!-- 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, 6353073966 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 4 of the `auth-manager.ts` split: **the better-auth database hooks, and the identity-source stamp they run.** Two `AuthManager` method bodies, 406 lines, move unedited (re-indented one level) into one new module, `packages/plugins/plugin-auth/src/auth-manager-database-hooks.ts`. #25224 stays open for the later cuts. This follows the amended claim `6357765071`, which amends `6356007354` on the file cut 3 left (#25362, landed note `6357231692`). The shape is cuts 1 to 3's, which is #23050's. Branch: `claude/project-thread-lg3wrj`, the harness-pinned branch for this session, restarted from `origin/main` `fb63d396b1` after #25362 merged. It names no issue, so the card relation is this body's first line. `auth-manager.ts` goes from 9,592 to 9,212 lines. The cut is +538 / −418 over 5 files: 956 changed lines, under the 3,000-line line. Two of the files are the regenerated tenant-audit census, and one is the new module's ADR anchor entry. It is not a governed surface (`check-governed-merges.mjs --branch HEAD`: 0 of the 5 paths hit the register). ## Why this seam - **`stampIdentitySource`** (cloud ADR-0024 D4, the `sys_user.source` provenance stamp) is the ninth method of ruling `6351993113`. Cut 3 left it for its own area. Its only caller is `account.create.after`, inside `composeDatabaseHooks`. - **`composeDatabaseHooks`** is where better-auth's database hooks are assembled around a host's own `databaseHooks`. Each hook calls into another section of the class (membership settlement, self-registration grants, owner-verified staging, the IdP locale), but the composition itself is one body with one job. - So the two move together, and the class's own sections keep their bodies. The membership section's methods that the hooks call (`settleMembership`, `settleSelfRegistrationGrant`, `takeOwnerVerifiedStamp`, `isCreatedInThisRequest`) stay for that area's cut. ## What moved | method | base body (signature to brace) | lines | members it reads | |---|---|---:|---| | `composeDatabaseHooks` | `:8431`–`:8792` | 362 | `audienceLogError`, `config`, `idpLocale`, `isCreatedInThisRequest`, `linkingContext`, `settleMembership`, `settleSelfRegistrationGrant`, `stampIdentitySource`, `takeOwnerVerifiedStamp` | | `stampIdentitySource` | `:8865`–`:8910` | 46 | `getDataEngine`, `logDurabilityDegradation` | Lines are base `fb63d396b1`. The members come from a walk of every `this.X` in each body with the TypeScript compiler API. Neither body has a bare `this`, `this[...]`, `arguments` or `super`, a `this` inside a `function` or a nested class, or an assignment to a `this` member. Neither reads a static (`AuthManager.X`). `composeDatabaseHooks` returns an object of arrow functions. Their `this` is the function's own `this`, which is now the host. **Imports.** No module-level declaration moves. Seven imported names are read only by the moved bodies, so they leave `auth-manager.ts`'s import lists and the new module imports each from the same place: - `clearUnlinkTombstone`, `clearUserUnlinkTombstones` and `recordUnlinkTombstone` (`./implicit-account-linking.js`); - `emitAuthSessionAuditEvent`, `loginEventFor` and `logoutEventFor` (`./auth-session-audit.js`); - `holdPrincipalSessionExpiry` (`./principal-session-mint.js`). Each of the three modules stays imported by `auth-manager.ts` for other names, so the order in which modules are evaluated does not change. `withSystemReadContext` (`./objectql-adapter.js`) and the `BetterAuthOptions` type are also read by code that stays, so both files import them. ## The shape - **The class keeps both methods** at their places, with their names, signatures and docblocks: 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 `DatabaseHooksHost`, plus `.call`. - **`DatabaseHooksHost`** lives in the new module. It names the 11 members the bodies read, each typed as the class member (`AuthManager['settleMembership']`, …). The module imports `AuthManager` as a type only, so there is no runtime cycle. - **In `composeDatabaseHooks` the literal is named `hookHost`.** The method's own parameter is already named `host` (the host stack's `databaseHooks`), and the stub passes it on: `composeDatabaseHooks.call(hookHost, host)`. - **Every member is read when a hook runs.** better-auth calls the composed hooks on each account, user and session write, long after `composeDatabaseHooks` returned. So nothing is copied at composition: - Methods go as late-bound closures (`(...a) => this.settleMembership(...a)`). A test double or spy set on the manager is still the one a hook reaches. That includes `stampIdentitySource`, which `account.create.after` asks through the manager. - `config` goes as a getter. The class reassigns it after construction (`setRuntimeBaseUrl`, `applyConfigPatch`), and the hooks read its `dataEngine`, `logger` and `getAuditSink` when they run. Its `autoActiveOrganization` is still read once, at composition, as before. - `idpLocale` goes as a getter too, so `user.delete.after` and `session.create.after` reach the field the manager holds when they run. - **`stampIdentitySource` is no longer `async`.** It returns the moved async function's own promise, so a caller awaits the same promise the body settles. Building the host cannot throw, so the method does not now throw synchronously where it used to reject. `composeDatabaseHooks` was synchronous and stays so. - **The cost:** one small host object per `composeDatabaseHooks` call, which runs once per better-auth instance, and one per `stampIdentitySource` call, which runs on each account link. - **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 | |---|---|---|---| | `check:adr-anchors` (`scripts/adr-anchors/`) | each anchored file must still cite the ADRs listed for it | 78 anchored files; the `auth-manager.ts` entry lists ADR-0093 and ADR-0108 | 79 anchored files. The new entry `packages__plugins__plugin-auth__src__auth-manager-database-hooks.ts.json` lists ADR-0093 and carries the ADR-0093 D2 invariant paragraph of the `auth-manager.ts` entry, verbatim. | | 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`: 2 | `auth-manager-database-hooks.ts` `update` `sys_user`: 2. Regenerated with `node scripts/tenant-audit-census.mjs --write`, and the page's hand-written erased-receiver figures follow (see below). | **Why the ADR anchor entry.** The D2 paragraph names the two seams that must never disagree, `user.create.after` and `session.create.before`, and both are composed in the body that moved. The new entry follows what #25223's cuts 2 to 4 did for their new modules (#25360 the latest). Per ruling `6353073966` item 1, `scripts/adr-anchors/` is not a row of the governed register. The `auth-manager.ts` entry is unchanged and still holds: - `settleMembership`, the "single assembly" its D2 paragraph names, stays in the class, as does ADR-0108's role map. - Of the file's 26 lines citing ADR-0093 or ADR-0108, 22 stay. - The moved body keeps its own ADR-0093 D2, D7 and D9 comments, so the new module cites the ADR it now carries. - `docs/adr/**` is not touched. **Measured as reading no moved line:** - **`api-error-code.pin.test.ts`**: neither body throws an `APIError`. The `authManagerFiles` floor reads the same 27 sites, and the per-file textual control finds 0 in the new module. - **Plugin-factory scans** in `better-auth-schema-parity.test.ts` and `managed-extension-fields.test.ts`: neither body has an `await import(` of a better-auth plugin. - **`pnpm check:dispatcher-error-vocabulary`**: exit 0. Its `auth-manager.ts` row names `deliverPhoneOtp`, which did not move. - **`check-auth-mount-ledger.mjs`** and **`auth-manager-base-path.test.ts`**: they read the base-path default, which neither body spells. - **The census's `UNTYPED_RECEIVERS` row for `auth-manager.ts`**: it names the SCIM transaction's `db`, which did not move. - **The system-context census**: neither body holds an `isSystem` read. `stampIdentitySource`'s `{ isSystem: true, … }` is a context it builds, not a read. - **Callers through the manager**: five suites call `composeDatabaseHooks` and one calls `stampIdentitySource`, each through the class method. All pass at head (below). - **Every symbol anchor into the file.** Before the cut, every `auth-manager.ts#symbol` anchor in the tracked tree outside changelogs was mapped with `symbolSegmentResolution` from `scripts/symbol-anchors.mjs`. There are 29, in ADR-0127, ADR-0134, ADR-0135 and four platform-checklist areas, and all 29 are `declaration`-class at base, with the two bodies blanked, and at head. The one that names a moved method, `access-security.json:2419` `#composeDatabaseHooks`, resolves on the method the class keeps. These checkers were run by hand at base and at head, because `dispatch-gates` does not derive the ADR one: - `check-adr-symbol-anchors`: 2,383 anchors across 142 records, 466 symbol (440 declaration, 26 literal), the same at both. - `check-adr-anchors`: 78 anchored files at base, 79 at head (the new entry). - `check-scripts-symbol-anchors` (4,058 anchors), `check-spec-docblock-symbol-anchors` (5,071 anchors), `check-system-context-census` (148 elevation read sites) and `check-platform-checklist` (16 areas, 329 items): the same reading at both. - All six exit 0 at base and at head. ## 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 2 `engine.update('sys_user', …)` sites of `stampIdentitySource`, the `env_native` and the `idp_provisioned` write: - **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 2 counted as "receiver carried a readable engine type". - **In the module**, the same text runs on a `this` typed as `Pick<DatabaseHooksHost, ...>`. The census does not follow a function's `this` parameter, so it places them by their literal object name, `sys_user`, a declared object. - **The generated table** moves `readable engine type` 185 → 183 and `placed by the object NAME` 42 → 44, 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 52 → 54. 54 of 237 is 22.8%, so "just over a fifth" stays true. - **`composeDatabaseHooks` holds no engine write.** Its `sys_member` lookups are reads. - **The corpus-scale block** (not enforced) is re-measured too: 714 sources at head, 713 at base, the difference being the new module, and 172 non-engine subtractions at both. Its `Measured on` line names the base, `fb63d396b`. The effect is the same as cuts 2 and 3 (3 and 8 sites there). Keeping these 2 typed would mean annotating `engine` in the moved body, 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 `fb63d396b1`, head `8f41da7fd7`. 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 2 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 from `stampIdentitySource`. - Result: 2 of 2 pass, 406 body lines (`composeDatabaseHooks` 360 lines, 1,613 tokens; `stampIdentitySource` 46 lines, 263 tokens). - **Negative control:** one token changed in the module (`role: 'owner'` → `role: 'admin'` in `composeDatabaseHooks`' `selectActiveOrg`) makes that method fail (tokens and lines DIFFER), and the run exits 1. Restored afterwards; `cmp` against the saved copy was clean. - **An independent reading:** `git diff --color-moved=plain --color-moved-ws=allow-indentation-change fb63d39 8f41da7` over the two source files marks 404 of the 409 deleted lines and 418 of the 522 added lines as moved. The other added lines are the stubs' host literals, `DatabaseHooksHost`, the module header, the per-function pointer docblocks and the import blocks; the other 5 deleted lines are four of the import names that left (`emitAuthSessionAuditEvent`, `loginEventFor`, `logoutEventFor`, `holdPrincipalSessionExpiry`; the module imports them on one line or under another module's block) and `stampIdentitySource`'s `async` signature line. **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` | `8a39f1035b6a64888117bb152de21865289e67bc8111d5a14ea457e0fb084d51` | `8a39f1035b6a64888117bb152de21865289e67bc8111d5a14ea457e0fb084d51` | 0 | `486138d3045a7ae05205c64d640eb98c77a85e3ac797a0be0e9ee351e8230bca` | `486138d3045a7ae05205c64d640eb98c77a85e3ac797a0be0e9ee351e8230bca` | | `dist/index.d.mts` | `074a15f3d7e075752bfb5aa299151d77d013fedfae947a8bcc8d28d8446b15df` | `074a15f3d7e075752bfb5aa299151d77d013fedfae947a8bcc8d28d8446b15df` | 0 | `8cb244809d4e1cee802060cfe8c37ff784d6f16d341672b7e2d74700ba2e1229` | `8cb244809d4e1cee802060cfe8c37ff784d6f16d341672b7e2d74700ba2e1229` | - 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 stampIdentitySource;` → `stampIdentitySource;`, line 3821, a one-line change) hashes raw `ae7b5fe254d6dfde9189a1ec5f556eebda8fc8c803859713d430e24f0d9c6503` and normalised `5546200f387e32564d475768d41a8ed51fbb2a6cd2450f8d71babceea0219912`. 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 3526 passed | 10 skipped (3536)`, VERDICT command-exit 0. Head: the same counts, VERDICT command-exit 0. The six suites that call the two methods through the manager are among them: `auth-manager.test.ts`, `auth-session-audit.test.ts`, `first-session-membership-ordering.test.ts`, `membership-policy-setting.test.ts`, `user-created-in-request.test.ts` and `durability-swallow-repair.test.ts`. - `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 2 TypeScript files: exit 0. **4. Gates.** - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 93 commands for the 5 paths. All 93 ran at `8f41da7fd7` after the full build, and all 93 exited 0. - `--ran`: 93 derived, 93 run, 0 NOT-MEASURED. - `check-governed-merges.mjs --branch HEAD`: NOT governed, 956 changed lines. - **Docs drift** (`node scripts/docs-audit/affected-docs.mjs --json fb63d39`, exit 0): 20 hand-written and 9 release-owned pages, each reached through a literal the moved bodies carry unchanged (`sys_member`, `sys_account`, `idp_provisioned`). 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): no open PR touches `auth-manager.ts`, the new module, the anchor entry, the census documents or the six suites above. `origin/main` has since moved to `d12e650ded` (#25363, `packages/spec` only), and `git merge-tree` of it with this 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 bdf762c commit ca381a8

5 files changed

Lines changed: 538 additions & 418 deletions

File tree

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ what moved this page's population from 225 to 227; nothing about the two sites
9393
changed, only whether this instrument could see them.
9494

9595
**The expensive failure direction is a keyword.** Sites whose receiver the author
96-
typed `any` have no type to read, and there are 52 of them — just over a fifth
96+
typed `any` have no type to read, and there are 54 of them — just over a fifth
9797
of the population, concentrated in exactly the seed and bootstrap paths this
9898
control exists for. Scoring an unreadable receiver as "not an engine" would have
9999
dropped every one of them silently, with a clean exit and a smaller number that
@@ -210,7 +210,7 @@ be stated is what this instrument counts, which is written above and re-runnable
210210
at any commit.
211211

212212
Two structural facts do plausibly widen this reading against any hand or regex
213-
one, and both are counted in the generated tables below: the 52 sites reached
213+
one, and both are counted in the generated tables below: the 54 sites reached
214214
through an erased (`any`) receiver, and the 51 that name their object through a
215215
`const` rather than inline. An instrument that read either the way a person does
216216
would report a smaller number and would not say so.
@@ -249,8 +249,8 @@ cannot read, and they are neither in nor out.
249249

250250
| how the instrument reached the site | count |
251251
| :--- | ---: |
252-
| receiver carried a readable engine type | 185 |
253-
| receiver erased, placed by the object NAME | 42 |
252+
| receiver carried a readable engine type | 183 |
253+
| receiver erased, placed by the object NAME | 44 |
254254
| receiver erased, placed by an `object: string` PARAMETER | 5 |
255255
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 5 |
256256

@@ -307,11 +307,11 @@ holds still. They are required to be HERE and to say WHEN they were true;
307307
their values are not compared. The reasoning, and the measurement behind it,
308308
are in `scripts/check-tenant-audit-census.mjs`.
309309

310-
Measured on 2026-10-23 at `5f4a3f088`.
310+
Measured on 2026-10-23 at `fb63d396b`.
311311

312312
| corpus scale (not enforced) | count |
313313
| :--- | ---: |
314-
| tracked non-test sources scanned | 712 |
314+
| tracked non-test sources scanned | 714 |
315315
| engine-shaped types recognised | 87 |
316316
| declared objects in the registry | 117 |
317317
| same-named calls subtracted as non-engine | 172 |

‎docs/audits/2026-08-tenant-audit-write-call-sites.counts.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,11 @@ holds still. They are required to be HERE and to say WHEN they were true;
9090
their values are not compared. The reasoning, and the measurement behind it,
9191
are in `scripts/check-tenant-audit-census.mjs`.
9292

93-
Measured on 2026-10-23 at `5f4a3f088`.
93+
Measured on 2026-10-23 at `fb63d396b`.
9494

9595
| corpus scale (not enforced) | count |
9696
| :--- | ---: |
97-
| tracked non-test sources scanned | 712 |
97+
| tracked non-test sources scanned | 714 |
9898
| engine-shaped types recognised | 87 |
9999
| declared objects in the registry | 117 |
100100
| same-named calls subtracted as non-engine | 172 |
@@ -130,12 +130,12 @@ Measured on 2026-10-23 at `5f4a3f088`.
130130
| `packages/plugins/plugin-auth/src/admin-user-endpoints.ts` | `update` | `sys_user` | enabled | elevated | 1 |
131131
| `packages/plugins/plugin-auth/src/adopt-membership.ts` | `update` | `SystemObjectName.MEMBER` | undecidable | PROVABLY NONE | 1 |
132132
| `packages/plugins/plugin-auth/src/audience-gate-test-support.ts` | `insert` | `sys_invitation` | enabled | elevated | 1 |
133+
| `packages/plugins/plugin-auth/src/auth-manager-database-hooks.ts` | `update` | `sys_user` | enabled | options unreadable | 2 |
133134
| `packages/plugins/plugin-auth/src/auth-manager-password-session-policy.ts` | `update` | `sys_account` | enabled | options unreadable | 1 |
134135
| `packages/plugins/plugin-auth/src/auth-manager-password-session-policy.ts` | `update` | `sys_session` | enabled | options unreadable | 3 |
135136
| `packages/plugins/plugin-auth/src/auth-manager-password-session-policy.ts` | `update` | `sys_two_factor` | enabled | options unreadable | 1 |
136137
| `packages/plugins/plugin-auth/src/auth-manager-password-session-policy.ts` | `update` | `sys_user` | enabled | options unreadable | 3 |
137138
| `packages/plugins/plugin-auth/src/auth-manager-sign-in-lockout.ts` | `update` | `sys_user` | enabled | options unreadable | 3 |
138-
| `packages/plugins/plugin-auth/src/auth-manager.ts` | `update` | `sys_user` | enabled | options unreadable | 2 |
139139
| `packages/plugins/plugin-auth/src/auth-manager.ts` | `insert` | `sys_user_permission_set` | enabled | elevated | 1 |
140140
| `packages/plugins/plugin-auth/src/auth-manager.ts` | `delete` | `sys_verification` | enabled | elevated | 1 |
141141
| `packages/plugins/plugin-auth/src/auth-manager.ts` | `insert` | `sys_verification` | enabled | elevated | 1 |

0 commit comments

Comments
 (0)