Repository navigation
Commit ca381a8
<!-- 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
- docs/audits
- packages/plugins/plugin-auth/src
- scripts/adr-anchors
| 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 | | |
| |||
| 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 | | |
| |||
130 | 130 | | |
131 | 131 | | |
132 | 132 | | |
| 133 | + | |
133 | 134 | | |
134 | 135 | | |
135 | 136 | | |
136 | 137 | | |
137 | 138 | | |
138 | | - | |
139 | 139 | | |
140 | 140 | | |
141 | 141 | | |
| |||
0 commit comments