Repository navigation
refactor(plugin-auth): move the createAuthInstance body into auth-manager-auth-instance.ts (#25224 cut 14) - #25566
Conversation
…ager-auth-instance.ts The body of AuthManager.createAuthInstance() moves unedited into the module function createAuthInstance, re-indented one level: the deferred better-auth imports, the plugin list and password hasher, the whole config literal (including the hooks.before and hooks.after arrows that cuts 12 and 13 left in place), the betterAuth() call and the wait on auth.$context. The method keeps its place, docblock, modifiers and return type; it now builds one host of the forty-seven AuthManager members the body reads (forty-five methods as late-bound closures, config and idpLocale as getters) and returns createAuthInstance.call(host). The async moved from the method to the function. readDisableSignUpEnv, the one top-level helper the body reads, is exported in place and imported by value; src/index.ts names its auth-manager.ts exports one by one, so nothing new is published. Ten imported values leave auth-manager.ts with the body, and the cut 12 and cut 13 hook imports are replaced by the new module's. One reader outside the move follows it: the population comments in auth-email-locale.test.ts name the new module among the files the five sendTemplate sites live in. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LSXBc2GCBwiAL7vV4kWkyr
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 21 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin aee575da058f3045f8f29e8e9725670d2af9099d && git checkout aee575da058f3045f8f29e8e9725670d2af9099d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 041a120b6189e9b174a3f4b8cf6202f22ace2bbd b2bbcd6cacc0c8241d0e5d0ca6af3d0cc21a4e2e && git checkout -B drift-repro 041a120b6189e9b174a3f4b8cf6202f22ace2bbd && git merge --no-ff b2bbcd6cacc0c8241d0e5d0ca6af3d0cc21a4e2e
node scripts/docs-audit/affected-docs.mjs --json 041a120b6189e9b174a3f4b8cf6202f22ace2bbd
|
席位核对 · ACCEPT · 2026-10-24T01:34Z
Generated by Claude Code |
Requested by Sales · project thread
Part of #25224
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6348891160, 6364006498
The basis for the
Clause-②: noline: this is a move plus one comment migration. No accept set, route, refusal, code or answer changes, and the published surface is measured unchanged:@objectstack/plugin-auth'sdist/index.d.tsanddist/index.d.mtsare raw byte-identical to base (0 diff lines, so the union-order normalisation has nothing to do). The positive control is the one ruling6348891160asks for.readDisableSignUpEnv, which this cut exports in place inside the package, is not among them.The
.d.tsbar is ruling6348891160item 1 (on #25222, covering this card). Ruling6364006498is Sales's ruling A on this card. This is the sixth cut it applies to.Cut 14 of the
auth-manager.tssplit: the rest ofcreateAuthInstance, which is its whole body. It is the last of the threecreateAuthInstanceslices the seat set (hooks.after, thenhooks.before, then the rest of the method). #25224 stays open for the later cuts.This follows the amended claim
6377759458, which amends6376168919on the file cut 13 left (#25548, landed note6377004954). The shape is #23050's method move, the one cuts 1 to 11 used; cuts 12 and 13 moved closures inside this method.Branch:
claude/project-thread-lg3wrj, the harness-pinned branch for this session, restarted fromorigin/mainafter #25548 merged and rebased onto45f996231ewhen #25541 merged. It names no issue, so the card relation is this body's first line.auth-manager.tsgoes from 7,807 to 7,122 lines. The cut is +926 / −749 over 3 files: 1,675 changed lines, under the 3,000-line line. One of the three files is a path reader outside the move, socheck-governed-merges.mjsran on the trial file list before the cut: NOT governed (0 of 3 paths hit the register), and--branch HEADagrees. One commit:b2bbcd6cacrefactor(plugin-auth): move the createAuthInstance body into auth-manager-auth-instance.ts.The move
createAuthInstance:1690–:2415The lines are base
45f996231e. The 728 proof lines are the body with its braces (:1689's{to:2416's}).What it is:
createAuthInstance(:1689,private async, annotatedPromise<Auth<any>>) builds the better-auth instance.getOrCreateAuth()calls it once per build (:1669,this.createAuthInstance().then(…)).What the body does, in order (all of it moves, with its comments): the deferred
better-authandbetter-auth/apiimports, the plugin list and the password hasher, then one config literal (secret, origin and base path, app name, database, theuser,accountandverificationmodel configs, the social providers,emailAndPasswordwith its reset-password and sign-up callbacks, the e-mail verification block,session, rate limiting and secondary storage, the plugins, the composed database hooks, the globalhooks.beforeandhooks.afterarrows that cuts 12 and 13 left in place,onAPIError, the trusted origins andadvanced), thebetterAuth()call, and the wait onauth.$contextbefore the instance is returned.Where it goes: a new module,
packages/plugins/plugin-auth/src/auth-manager-auth-instance.ts(861 lines). The body becomes the body ofexport async function createAuthInstance(this: AuthInstanceHost): Promise<Auth<any>>, re-indented two spaces (one level) and otherwise unedited. The module header gives the reasoning, in the form the thirteen earlierauth-manager-*.tsmodules use.What stays:
createAuthInstancekeeps its place, its docblock and itsprivatemodifier. Its body is now:The seat's bars
Body.
Promise<Auth<any>>.asyncmoves from the method to the function. The stub is notasync, and it returnscreateAuthInstance.call(host), the function's own promise, unchanged.getOrCreateAuth()chains.then(…)on that promise exactly as before. The two reader closures, the host literal and the.callcannot throw before the promise exists, and a throw inside the body is still a rejection of it.arguments,new.targetorsuper(0 of each), and no barethis: the body's onlythisuses are member readsthis.X. Every method read is a direct call, and the body writes no member. Nothissits inside a non-arrow function, so every callback the body hands better-auth reads the same receiver the body does.Receiver.
thiswas theAuthManagerinstance. It is now the host.AuthInstanceHostnames exactly the 47 members the body reads, no more, each typed as the class member (AuthManager['x']).(...a) => this.x(...a). The method runs on the manager and is looked up when the body calls it, so a spy or double set on the manager is still the one the body reaches, including from the callbacks better-auth runs long after the function returns.readonlyon the interface; the body writes neither:config, which the class reassigns after construction (setRuntimeBaseUrl,:3975, writes it at:3987;applyConfigPatch,:3998, at:4068);idpLocale(:4167,private readonly, anIdpLocaleSync), which is not reassigned, as every field this series carries goes (the same paircomposeDatabaseHooks' stub hands its body as getters,:7030).resolveKernelService): the body callsthis.idpLocale.stage(data, ctx)once (:1839), andthis.config.logger?.info?.(m, meta)once (:1843, inside thelogInfoarrow it hands on). Each receiver is the object the getter returns, the manager's current one, so each is looked up and called on the same object, at the same moment, with the samethis, as before.hasExistingUserForandisBootstrapCreationread private statics. They stay in the class, per ruling A, and the body reaches them as members. The body reads noAuthManager.X, so the module imports the class as a type only.The two hook arrows.
hooks.beforeandhooks.aftermove with the literal, unedited. Each builds its own host fromthis, which is now this host: each of their closures calls this host's closure, which calls the manager's method, and theirconfiggetters read this host's getter, which reads the manager's field. So they reach the same member, at the same moment, as before. Their imports (globalBeforeHook,globalAfterHookand the two host types) move to the new module with them.One top-level helper, exported in place. The body reads one top-level declaration of
auth-manager.ts:readDisableSignUpEnv(:384, once). Per ruling6364006498(A), it getsexportin place, in this cut, the first whose body reads it, and the module imports it by value from./auth-manager.js. The binding proof checks that the head declaration is the base one withexportin front and nothing else changed, and that the base and head reads resolve to it.src/index.tsnames itsauth-manager.tsexports one by one and does not name it, so nothing is published.Host lifetime. The host is built once per call of
createAuthInstance, whichgetOrCreateAuth()makes once per build. This is what every method stub in this series does. Nothing the body reads is captured early: the closures look each method up on the manager at call time, and the getters read the fields when the body, or a callback it handed better-auth, reads them.Readers. The decision letters are read on the merge base and on the head below: 8 drop, as expected, and one entry judgment is disclosed. Tenant census and symbol-anchor output are byte-identical at both ends, and the full plugin-auth suite passes at both ends. The one path reader whose text names where the moved lines live is migrated in this commit.
Imports and evaluation order
Ten imported values leave
auth-manager.ts, because the body was their last reader there:PLATFORM_IDP_PROVIDER_IDandrefuseImplicitAccountLink(theimplicit-account-linking.jsblock keepstype LinkingAuthContext);readEnvWithDeprecation(the@objectstack/typesblock keeps its other names);account-invitation-marker.jsline (readAccountInvitation,settleImportResetDelivery) and the wholeclient-address-vendor.jsline (vendorClientAddressOptions);AUTH_USER_CONFIG,AUTH_SESSION_CONFIG,AUTH_ACCOUNT_CONFIGandAUTH_VERIFICATION_CONFIG(theauth-schema-config.jsblock keeps its other names).The cut 12 and cut 13 import lines (
globalAfterHook,globalBeforeHookand the two host types) and their comments are replaced by one comment and one line:import { createAuthInstance, type AuthInstanceHost } from './auth-manager-auth-instance.js'.audiencePermitsSelfRegistration,resolveEmailVerificationRequirement,isPlaceholderEmailand thebetter-authtypes have other readers, so both files import them.esbuild drops a type-only import, so a module whose last value import leaves
auth-manager.tscan now be evaluated later. The order was measured rather than argued:dist/index.mjsloaded, at base and at head. 171 modules evaluate at both, and the first 117 are the same set at both. One block moves: the 17 modules@better-auth/core/contextloads (16 files of@better-auth/core'scontext,async_hooks,error,db,envandutilsdirectories, andbetter-call'serror) ran first at base and now run after the 100 others in that set (zod's v4 modules,@objectstack/spec'ssecurity,apianddataentries,pg-connection-stringand@objectstack/types). They follow the bundle's first@better-auth/core/contextimport, which isaccount-invitation-marker.ts's at both, and that module's segment now sits later in the bundle (line 196 at base, 477 at head). From position 118 on, the order is identical. Their top levels are constants, classes and function declarations, plusdefineErrorCodes,createLogger()and theenvproxy; the only load-time reads are ofprocess.env(NODE_ENV,TEST, the default logger's colour depth), and thebetter-auth:globalslot is touched only when a function is called. None of the 100 writesprocess.envor that slot. The bundle's own body runs last, after all 170, at both.Module._loadlog fordist/index.jsgives 127 first loads at both, the same apart from@better-auth/core/dist/context/index.mjs, which now loads last instead of eighth from last.init_*calls finds three that run later:implicit-account-linking.ts(one place),client-address-vendor.tsandaccount-invitation-marker.ts, whose value imports leftauth-manager.tswith the body. Their top levels are string constants, one arrow constant (authContextOf), and twodefineRequestState(() => null)calls, each of which makes its own frozen key object and touches no shared state until it is read inside a request. No importer reads any of their bindings at load time: all 14 references in the 6 importing files are inside functions.auth-manager.ts, forreadDisableSignUpEnv, andauth-manager.ts's import of it form a cycle with nothing read at load time.The decision-letter layer: −8, as expected
check-adr-anchors, measured on the merge base and on the head. The gate exits 0 at both.45f996231eb2bbcd6cac:2000,:2114), ADR-0069 D4 (:2142), ADR-0069 D2 (:2190,:2214), ADR-0093 D2 (:2222) and ADR-0135 D6 (:2347,:2351). They leaveauth-manager.ts, which is anchored, for a module that is not. Their citations still resolve, so the citation count does not move.:2222is in the comment on thedatabaseHooks: this.composeDatabaseHooks(this.config.databaseHooks)wiring, which says D2 makes the database-hook seam the single owner of the user-lifecycle invariants. Theauth-manager.tsentry's D2 paragraph describessettleMembership, which stays, and its two seams (user.create.afterandsession.create.before), which are database hooks that cut 4 moved toauth-manager-database-hooks.tswith their own entry. It does not describe the wiring line, and no other entry names it. The ADR-0108 paragraph (the organization roles map) is not in the body. So, per the seat's bar, no entry is added for the new module. If the seat reads the wiring as part of the D2 mechanism, the remedy is an entry for the new module carrying the D2 paragraph verbatim, cut 4's precedent, in this PR.Path readers
One reader outside the move names where the moved lines live, and it follows the body in this commit:
auth-email-locale.test.ts: the two population comments (the docblock at:861and the in-test note at:1126, head numbering) that name the files the fivesendTemplatesites live in now nameauth-manager-auth-instance.ts(the body ofcreateAuthInstance) among them. No assertion, count or pattern changes.Read, and not edited:
api-error-code.pin.test.ts: the body carries nonew APIError(site, soauthManagerFilesis not owed the new module. Its sum stays 30, as at base.scripts/check-dispatcher-error-vocabulary.mjs: the body has nonew APIError(orAPIError.from(site and no live member, so nothing in the gate moves. It exits 0 at both, 56 sites found and 56 classified at both, and only its scanned-file count moves (3,430 → 3,431).--self-testis among the derived gates below.check-tenant-audit-censusexits 0 at both with no--write, with byte-identical output. Its--jsondocument has 238 sites at both and 0 per-site differences with line numbers ignored. 2 sites are inauth-manager.tsat both (each 685 lines earlier at head, the cut's net), 0 are in the new module, and 0 are unresolved or unledgered. OnlyscannedSources(751 → 752) andmeasuredAtmove, so neither census document is edited.scripts/measure-durability-swallow-family.mjs(a measurement, not a gate): 77 members and 84 quiet rows at both, identical with line numbers ignored (one member's write line, inobserveFirstMemberships, which stays, moves 5,177 → 4,492).filesmoves 3,480 → 3,481, andtryStatements(3,645) is the same. One statistic moves, and I traced it:droppedNotAwaitedgoes 233 → 234 (delete149 → 150). It counts write-named calls that are not awaited inside atryblock and the same-file helpers that block calls, walked by name, each name once, at most four levels deep. ThetryinlinkingContext(base:4355) walksgetOrCreateAuth, thencreateAuthInstance. At base the walk entered the body and reachedisOperatorProvisioningat depth 4, soprunePendingOperatorProvisioningsat at depth 5, past the limit. At head the walk enters the stub, whose host literal lists the members alphabetically, and reachesisOperatorProvisioningat depth 3. SoprunePendingOperatorProvisioning(:4923, which stays) is now walked, and itsthis.pendingOperatorProvisioning.delete(key)is counted. That call is a synchronousMapdelete, not a write, and the code is unchanged. A logged copy of the instrument, run on the base and head trees and then removed, shows this is the only dropped call that differs (233 at base, 234 at head). Membership and the quiet rows, which are what the measurement reports, do not move.check-auth-mount-ledger.mjsreads only thebasePathdeclaration inauth-manager.ts, which stays.auth-manager-base-path.test.tsreadsauth-manager.tsfor code lines carrying'/api/v1/auth'; the body has one only in a comment (:1814).better-auth-schema-parity.test.tsandmanaged-extension-fields.test.tsmatchconst { X } = await import('better-auth/plugins…' | '@better-auth/…'). The body's two dynamic imports arebetter-authandbetter-auth/api, neither a factory. At head the scanned shape matches 16 times inauth-manager.ts, as at base, and 0 times in the module. Both suites pass at head.rate-limit-storage-isolation.test.tswalks the import graph fromrate-limit-storage.ts, which reaches neither file. It passes at head.auth-manager.tsfor code now in the module (for example theNODE_ENV-gated trusted-origins substitution named inpackages/cli/test/serve-node-env-production-default.e2e.test.ts) reads no file and is not edited, as cuts 12 and 13 left the prose that names the before-hook.auth-manager.ts#symbolanchor in the tracked tree outside changelogs was mapped withsymbolSegmentResolutionfromscripts/symbol-anchors.mjs. There are 29, and all 29 aredeclaration-class at base, at base with the body blanked, and at head. Five of their symbols occur as a token on a line of the body:secondaryStorage(ADR-0127:114),composeDatabaseHooks(the access-security checklist area:2419),isBootstrapCreation(identity-auth:1810),readDisableSignUpEnv(identity-auth:1901) andrequireEmailVerification(identity-auth:2004). Each resolves to its declaration, which stays, at all three readings;readDisableSignUpEnv's declaration (:384) gains onlyexport.Proof
Builds and suites ran through
os-verify-lock, and every exit code was captured before any pipe. Base45f996231e. A fullturbo run buildran first. Thenplugin-authwas built and its suite run at base, and built and run again at head, on the same dependency dists.1. The moved text.
Token identity: the base method body and the module 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): 728 lines and 2,616 tokens, each line the base line minus two spaces.
The stub's shape, checked by the same run: not
async, the same docblock, modifiers, parameters and return annotation as the base method, the two reader closures, one host literal whose keys are exactlyAuthInstanceHost's, alphabetical, whose 45 method values are each(...a) => this.<key>(...a)and whoseconfigandidpLocaleare getters over the readers, thenreturn createAuthInstance.call(host), and nothing else. The interface types every memberAuthManager['x'], and exactly the two getter fields arereadonly.Binding identity: a second proof walks all 672 identifiers of the body at base and in the module, in order, and resolves each with the TypeScript checker (the module's
./auth-manager.jsresolved to the head file, where the in-place export lives). All 672 resolve the same way at both ends:AuthManagerat base,AuthInstanceHostat head);readDisableSignUpEnvamong them;auth-manager.ts: sevenAuthManagerOptionsmembers read throughthis.config(databaseHooks,emailService,loggerand itsinfo,rateLimit,rateLimitStorageandsecondaryStorage);None resolves to an unexported top-level declaration of
auth-manager.tsor to a top-level declaration of the module.Negative controls: each proof reads the module text, and each control changes it in memory only; the file is untouched.
betterAuth(changed tobetterAuth?.(fails the token and line checks, and the run exits 1.placeholder-emailimport pointed at another specifier gives 1 difference, and the run exits 1. The same import shadowed by a module-levelconstgives 1 difference and reports the module top-level read, and exits 1. Run without being told about the in-place export, it reports the base read of an unexported helper, and exits 1.An independent reading:
git diff --color-moved=plain --color-moved-ws=allow-indentation-change 45f996231e b2bbcd6cacover the two source files marks 734 of the 744 deleted lines and 768 of the 920 added lines as moved.refuseImplicitAccountLink,,readEnvWithDeprecation,), the six comment lines above the cut 12 and cut 13 import lines,function readDisableSignUpEnv(): boolean | undefined {(now withexport) andprivate async createAuthInstance(): Promise<Auth<any>> {(now withoutasync).return, and the new import comment and line.2.
.d.tsand exports (ruling6348891160items 1 and 2).dist/index.d.tsraw1b3a48357d137155de33104277667a922aab62df73b418b03894c56faaacd0badist/index.d.tsnormalisedcee7303ccdfe72794efd588c85b628419d4253d730b5a0c5a3f1cdb8dddb4443dist/index.d.mtsrawb5e8dc982b634f0a5108b1b533626a68767f2da5a56f72dba801059b149e2872dist/index.d.mtsnormalised562d0a4ce656f97aeb937541482cdf531b61d7c75a005918784bf0a52d032dfcnode -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.dist/index.d.tswith one private member made public (private createAuthInstance;→createAuthInstance;, line 1712, a one-line change) hashes raw35b18682171cd394898c9e56bc8d5272ccf20aeff0d1966fc568cb5e566a08dfand normalisede69c1b0157b45e48bbc16bd64eb939abf9be33ccb31553485df93ef05aac6fc3. Both differ from the readings above.dist/index.mjsand CJSdist/index.jseach have 159 names at base and at head, the same list (the sha256 of the sorted names is3abb126f5babb701…on all four).readDisableSignUpEnv,createAuthInstanceandAuthInstanceHostare not among them, anddist/index.d.tsnames neitherreadDisableSignUpEnvnorAuthInstanceHost.dist/at both, plus the two.build-input-hashstamps the build writes from its inputs. Onlyindex.js,index.mjsand their two.mapfiles differ, as a move of code inside the bundle must.tsupbundles the new module into the root entry (splitting: false).3. Tests and lint.
pnpm exec vitest run --maxWorkers=3inpackages/plugins/plugin-auth: baseTest Files 191 passed (191),Tests 3620 passed | 10 skipped (3630), exit 0. Head: the same counts, exit 0.pnpm --filter @objectstack/plugin-auth typecheck(bothtscprojects andcheck:test-typecheck): exit 0.pnpm --filter @objectstack/runtime typecheck: exit 0.eslintover the 3 files: exit 0.4. The censuses, the measurement and the anchors, at base and at head.
The tenant-audit census, as above: byte-identical gate output, 238 sites at both, 0 per-site differences line-blind, 0 unresolved and 0 unledgered at both.
The durability measurement, as above, with its one moved statistic traced.
These checkers were run by hand at base and at head, because
dispatch-gatesdoes not derive the ADR one. All six exit 0 at both. Five print byte-identical output at both (cmpclean):check-adr-symbol-anchors: 2,384 anchors across 142 records, 466 symbol (440 declaration, 26 literal);check-scripts-symbol-anchors: 4,070 anchors;check-spec-docblock-symbol-anchors: 5,076 anchors;check-system-context-census: 149 elevation read sites;check-platform-checklist: 16 areas, 329 items.The sixth,
check-adr-anchors, differs only in the file count and the decision letters, as in the table above.5. Gates.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 60 commands for the 3 paths. All 60 ran at head after the full build, and all 60 exited 0.--ranover the record with each exit code: 60 derived, 60 run, 0 NOT-MEASURED, a derived zero.--tier: no path-derived mandate.check-governed-merges.mjs: on the trial file list before the cut and--branch HEADafter the commit, both read NOT governed (0 of 3 paths hit the register), 1,675 changed lines.node scripts/docs-audit/affected-docs.mjs --json 45f996231e, exit 0): 8 pages, all name matches of names the moved code carries (theOS_CORS_ORIGINandCORS_ORIGINliterals,isAuthGateActiveandresolveActoras host members, andsys_oauth_resourcein a comment). None namesauth-manager.ts, and the code that reads those names is unchanged, so no doc edit is owed. 3 of them are release notes (17.3, 18.0, 18.1), owned by the release, and not edited.auth-manager.ts, the new module's path,auth-email-locale.test.tsor a reader named above. One opened after the claim touchesplugin-auth: fix(plugin-auth, driver-sql)!: fail closed on a missing auth secret outside development; compile window functions from closed tables #25563 editsauth-manager-database-secret.tsandauth-manager.test.ts, neither a file of this cut, and reads no source by path;resolveAuthSecret, which the body calls, is reached through a late-bound closure, so a change to it lands the same way at both. The Version Packages PR chore: version packages #24876 touchesplugin-auth'spackage.json, itsCHANGELOG.mdand changesets only. feat(security): the external organization role is the EXTERNAL principal — owner-only org-wide default, explicit grants only #25541, which the cut 13 claim named, merged as this base; its hunks in the file are outside the body.origin/mainhas moved 6 merges past this base, to041a120b61; none touchesplugin-auth,scripts/adr-anchors/or a reader named above.Changeset
None:
skip-changeset, per ruling6348891160item 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