Repository navigation
refactor(plugin-auth): move AuthManager's email and SMS delivery into auth-manager-email-sms-delivery.ts (#25224 cut 5) - #25395
Conversation
… auth-manager-email-sms-delivery.ts Seven method bodies move unedited: sendChangeEmailNotice, getOtpSendGuard, assertPhoneOtpSendAllowed, deliverPhoneOtp, sendPhoneInviteSms, storedRecipientLocale and renderPhoneSmsBody. The four module-level declarations only they read move with them: the SMS quota refusal code and its matcher, the quota wall's 429, and the one phone OTP send refusal code. The class keeps all seven methods, their docblocks and signatures, and hands each body the members it reads. The APIError pin's file list, the dispatcher error vocabulary row for deliverPhoneOtp and the five-sends population comments follow the moved text. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LSXBc2GCBwiAL7vV4kWkyr
📓 Docs Drift CheckThis PR changes 2 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 38 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 8a6b1cc6fe65ebd3dc5b56481e09f39891e4048e && git checkout 8a6b1cc6fe65ebd3dc5b56481e09f39891e4048e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5f6c2f1952355504a7db5a956c4a86c11feefd57 b2f9b260de6999cf8df224a525bd484c1594da68 && git checkout -B drift-repro 5f6c2f1952355504a7db5a956c4a86c11feefd57 && git merge --no-ff b2f9b260de6999cf8df224a525bd484c1594da68
node scripts/docs-audit/affected-docs.mjs --json 5f6c2f1952355504a7db5a956c4a86c11feefd57
|
席位核对 · ACCEPT · 2026-10-23T07:40Z
Generated by Claude Code |
Requested by Sales · project thread
Part of #25224
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6348891160
The basis for the
Clause-②: noline: this is a move. No accept set, export, route, refusal, code or answer changes. The.d.tsbar is ruling6348891160item 1 (on #25222, covering this card).@objectstack/plugin-auth's publisheddist/index.d.tsanddist/index.d.mtsare 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 5 of the
auth-manager.tssplit: the email and SMS delivery. SevenAuthManagermethod bodies (230 lines) and the four module-level declarations that only they read (81 lines) move unedited, the bodies re-indented one level, into one new module,packages/plugins/plugin-auth/src/auth-manager-email-sms-delivery.ts. #25224 stays open for the later cuts.This follows the amended claim
6359424739, which amends6357765071on the file cut 4 left (#25383, landed note6358711845). The shape is cuts 1 to 4's, which is #23050's.Branch:
claude/project-thread-lg3wrj, the harness-pinned branch for this session, restarted fromorigin/mainca381a8a19after #25383 merged. It names no issue, so the card relation is this body's first line.auth-manager.tsgoes from 9,212 to 8,968 lines. The cut is +527 / −332 over 5 files: 859 changed lines, under the 3,000-line line. One file is cross-lane (see "Readers re-pointed"). It is not a governed surface (check-governed-merges.mjs --branch HEAD: 0 of the 5 paths hit the register).Why this seam
These are the sends
AuthManagermakes itself, outside better-auth's callbacks:sys_user.locale, and the auth SMS body render.The four module-level declarations are the SMS quota wall's: the code the SMS service reports a quota refusal with and its matcher, the 429 the wall answers with, and the one ADR-0112 code both phone OTP send walls carry. Every reader of each, comments included, is in the moved text. So they move with it rather than being exported:
index.tsre-exportsauth-manager.tswhole, so an export there would widen the published surface, and a value import of that file from the new module would be a cycle.What stays, and why:
emailLocaleArgand thesetDefault*Localesetters stay. The first readsauthEmailLocaleFromRequestandnormalizeAuthEmailLocale, whichauth-manager.tsexports.getEmailService,hasEmailTransport,setSmsService,getSmsService,isSmsServiceAvailableandisPhoneOtpDeliverable. These are one- to four-line bodies that other sections of the class call too, and the moved bodies reach the ones they need through the host.createAuthInstanceandbuildPluginList, and stay for those areas.What moved
sendChangeEmailNotice:6299config,emailLocaleArg,getAppName,getEmailService,storedRecipientLocalegetOtpSendGuard:6359_otpSendGuard(read and written),configassertPhoneOtpSendAllowed:6391getOtpSendGuard,isPhoneOtpDeliverabledeliverPhoneOtp:6420config,getAppName,getSmsService,isPhoneOtpDeliverable,renderPhoneSmsBody,storedRecipientLocalesendPhoneInviteSms:6508getAppName,getCanonicalOrigin,getConsolePageUrl,getSmsService,renderPhoneSmsBody,storedRecipientLocalestoredRecipientLocale:6661getDataEnginerenderPhoneSmsBody:6745config,getDataEngine,smsLocaleSMS_QUOTA_EXCEEDED_CODE:1216–:1247isSmsQuotaRefusal, and the docblock ofsmsQuotaExceededApiErrorisSmsQuotaRefusal:1265–:1275deliverPhoneOtp,sendPhoneInviteSmssmsQuotaExceededApiError:1409–:1435deliverPhoneOtp,sendPhoneInviteSmsPHONE_OTP_SEND_REFUSAL_CODE:1437–:1447assertPhoneOtpSendAllowed,smsQuotaExceededApiErrorLines are base
ca381a8a19. The members come from a walk of everythis.Xin each body with the TypeScript compiler API. No body has a barethis,this[...],argumentsorsuper, or athisinside afunctionor a nested class. No body reads a static (AuthManager.X). The one assignment to athismember isgetOtpSendGuard's to_otpSendGuard.The four declarations keep their order in the module, and
smsQuotaExceededApiErrorreadsPHONE_OTP_SEND_REFUSAL_CODEonly when it is called, as before.SCIM_PROTOCOL_PATH_PREFIXandisScimProtocolPath, which sat between them, stay.Imports. Five imported names are read only by the moved text, so they leave
auth-manager.tsand the new module imports each from the same place:PHONE_SMS_TOPICS,interpolatePhoneSmsandresolvePhoneSmsTemplateBody(./phone-sms-texts.js);RECIPIENT_LOCALE_FIELDandnormalizeRecipientLocale(@objectstack/service-messaging). The comment that explains why that normaliser is reused moves with its import, verbatim.OtpSendGuard(./otp-send-guard.js) is read by the movednew OtpSendGuard(…)and by the field type_otpSendGuard?: OtpSendGuardthat stays, so both files import it. So do both files import theStandardErrorCodetype:SLUG_GUARD_READ_FAULT_CODEstays.Evaluation order.
auth-manager.tsno longer imports./phone-sms-texts.jsor@objectstack/service-messagingitself. Nothing observable moves:@objectstack/service-messagingis first evaluated through./idp-locale-claim.js, whichauth-manager.tsimports at:39, at base and at head alike../phone-sms-texts.jshas no imports, and its top level is declarations with literal initialisers, so the point at which it is evaluated cannot be observed.The shape
.d.tslists each one. Each body is now a host literal of exactly the members that body reads, typed as aPickofEmailSmsDeliveryHost, plus.call.EmailSmsDeliveryHostlives in the new module. It names the 14 members the bodies read, each typed as the class member (AuthManager['storedRecipientLocale'], …). The module importsAuthManageras a type only, so there is no runtime cycle.(...a) => this.storedRecipientLocale(...a)). A test double or spy set on the manager is still the one a body reaches. That includesstoredRecipientLocale,renderPhoneSmsBodyandgetOtpSendGuard, which the moved bodies ask through the manager.configandsmsLocalego as getters. The class reassignsconfigafter construction (setRuntimeBaseUrl,applyConfigPatch) andsmsLocaleon everysetDefaultSmsLocale, so each is read off the manager when a body reads it, as before._otpSendGuardgoes as a get/set pair.getOtpSendGuardcreates the guard once and stores it, so the guard, and the per-number budget it counts, stays the one the manager holds.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.sendPhoneInviteSms's missing-servicethrowis still inside an async function, so it still rejects.getOtpSendGuardwas synchronous and stays so.{@link normalizeRecipientLocale}references stay inauth-manager.ts, in the docblocks ofstoredRecipientLocaleandemailLocaleArg. The name is no longer imported there, so an editor no longer resolves those two links. The base file already carries one such unresolved link,{@link phoneSmsLocaleChain}, which it never imported. The text is unchanged, and neither docblock reaches the.d.ts(both methods are private).Readers re-pointed in the same PR
api-error-code.pin.test.tsauthManagerFilesnew APIError(sites over the filesAuthManager's bodies live inauth-manager.ts15, actor guards 9, password and session policy 2, sign-in lockout 1auth-manager-email-sms-delivery.ts, which holds the 3 sites that moved (assertPhoneOtpSendAllowed,deliverPhoneOtp,smsQuotaExceededApiError). Without it the floor would pass on 24, in silence.packages/runtime/src/dispatcher-error-vocabulary.ts, theUNRESOLVED_CODE_HELPERSrow fordeliverPhoneOtp(cross-lane:domain:cli, as the seat agreed on this card)check-dispatcher-error-vocabulary.mjsfinds theobjlithelperhelperdeliverPhoneOtp(code)in its declaring file, and the row must name that filefile: '…/auth-manager.ts'file: '…/auth-manager-email-sms-delivery.ts'. The row's evidence said "Its two in-file callers are better-authsendOTPhooks". It now says those two hooks are inauth-manager.tsand reach the helper through the class method of the same name. Nothing else in the row or the file changes.auth-email-locale.test.ts, two commentssendTemplatecall sites inauth-manager.ts", and "no other file underplugin-auth/srccalls either"auth-manager.tsauth-manager.tsandauth-manager-email-sms-delivery.ts, where the change-email notice's send now sits. No assertion changes.Each migration is load-bearing. Measured on the draft:
node scripts/check-dispatcher-error-vocabulary.mjsexits 1:stale-helper-row(the row names a file the helper left) andunresolved-constant(the helper is found in the new module with no row). Migrated, it exits 0: 56 sites found, 56 classified, the same as base.authManagerFilesfloor is the silent case: unmigrated, it reads 24 and passes. So the per-file count above is the evidence, not the test's exit code.Measured as reading no moved line:
check-tenant-audit-census.mjsand its two documents): the moved text holds no engine write.storedRecipientLocale'sfindOneandresolvePhoneSmsTemplateBody's template read are reads. Compared per site, base and head read the same 237 sites with the same totals, and no total moves (below). So neither document is regenerated.better-auth-schema-parity.test.tsandmanaged-extension-fields.test.ts: the moved text's threeawait import(…)are ofbetter-auth/api, which their pattern does not match.check-auth-mount-ledger.mjsandauth-manager-base-path.test.ts: the moved text does not spell the base-path default.check-dispatcher-error-vocabulary.mjs'sliveMemberFilerecord namesBASE_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAILinauth-manager.ts, which stays. Its self-test passes (a derived gate below).UNTYPED_RECEIVERSrow forauth-manager.ts: it names the SCIM transaction'sdb, which stays.isSystemread.storedRecipientLocale's{ isSystem: true, … }is a context it builds, not a read. The census reads 148 elevation read sites at base and at head.scripts/adr-anchors/: no entry's invariant describes these sends, so no entry is added. Theauth-manager.tsentry (ADR-0093 and ADR-0108) is unchanged and still holds: the moved text cites neither.phone-otp-no-sms-service-refusal.test.ts(a spy ondeliverPhoneOtp),phone-sms-template-organization.integration.test.ts,auth-email-locale.test.ts,auth-manager.test.ts,auth-plugin.test.tsand the twoadmin-import-users-*suites, call each through the class method. All pass at head (below).auth-manager.ts#symbolanchor in the tracked tree outside changelogs was mapped withsymbolSegmentResolutionfromscripts/symbol-anchors.mjs. There are 29, in ADR-0127, ADR-0134, ADR-0135 and four platform-checklist areas, and all 29 aredeclaration-class at base, with the 7 bodies and 4 declarations blanked, and at head. None names a moved method or declaration. Three name a token the moved text also carries, and each resolves on a declaration that stays:#secondaryStorage(ADR-0127),#AuthManager(ADR-0135) and#isPhoneOtpDeliverable(identity-auth.json:1345).One reading the move does shift:
check-adr-anchors' decision-letter layer. That layer checks eachADR-NNNN Dkcitation in an anchored file's text against the cited record's decisions. It reads anchored files only, by design (the checker's header: "Scope is the anchored surface deliberately").[ADR-0131 D7, Q2 A](a notice to the account holder is identity mail). It moves fromauth-manager.ts, which is anchored, to the new module, which is not.invariantparagraph that no existing entry carries for these sends. Writing one is new governance text, not a move, so this PR does not add it. The modules of cuts 1 to 3 carry no anchor entry either.Proof
Builds and suites ran through
os-verify-lock, and every exit code was captured before any pipe. Baseca381a8a19, headb2f9b260de. A fullturbo run buildran first. Thenplugin-authwas built and its suite run at base, then built and run at head, on the same dependency dists.1. The moved text.
asyncdropped from the six that had it.auth-manager.ts.sendChangeEmailNoticegetOtpSendGuardassertPhoneOtpSendAlloweddeliverPhoneOtpsendPhoneInviteSmsstoredRecipientLocalerenderPhoneSmsBodyotpCfg.expiresIn ?? 300→?? 301indeliverPhoneOtp) makes that method fail (tokens and lines DIFFER), and the run exits 1. Restored afterwards;cmpagainst the saved copy was clean.git diff --color-moved=plain --color-moved-ws=allow-indentation-change ca381a8a19 b2f9b260deover the two source files marks 317 of the 323 deleted lines and 345 of the 513 added lines as moved.asyncsignature lines.EmailSmsDeliveryHost, the module header, the per-function pointer docblocks and the import blocks.2.
.d.tsand exports (ruling6348891160items 1 and 2).dist/index.d.ts8a39f1035b6a64888117bb152de21865289e67bc8111d5a14ea457e0fb084d518a39f1035b6a64888117bb152de21865289e67bc8111d5a14ea457e0fb084d51486138d3045a7ae05205c64d640eb98c77a85e3ac797a0be0e9ee351e8230bca486138d3045a7ae05205c64d640eb98c77a85e3ac797a0be0e9ee351e8230bcadist/index.d.mts074a15f3d7e075752bfb5aa299151d77d013fedfae947a8bcc8d28d8446b15df074a15f3d7e075752bfb5aa299151d77d013fedfae947a8bcc8d28d8446b15df8cb244809d4e1cee802060cfe8c37ff784d6f16d341672b7e2d74700ba2e12298cb244809d4e1cee802060cfe8c37ff784d6f16d341672b7e2d74700ba2e1229node -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 deliverPhoneOtp;→deliverPhoneOtp;, line 2703, a one-line change) hashes raw72873388418fd744bdf706e2f4099a8650e58da20cf49526a1b8aed2b9b31c7eand normalisedb15a8158861a2a047848872c97c420a06660ff583e9fa806bad0766fddd323b4. Both differ from the readings above.dist/index.mjsand CJSdist/index.jseach have 155 names at base and at head, the same list (the sha256 of the sorted names ised3ac8b902031d30…on all four).dist/on both sides, the same list. Onlyindex.js,index.mjs, their two.mapfiles and the two build-input hash stamps differ, as a move of code inside the bundle must.3. Tests and lint.
pnpm exec vitest run --maxWorkers=3inpackages/plugins/plugin-auth: baseTest Files 187 passed (187),Tests 3526 passed | 10 skipped (3536), VERDICT command-exit 0. Head: the same counts, VERDICT command-exit 0.pnpm --filter @objectstack/plugin-auth typecheck(bothtscprojects andcheck:test-typecheck) andpnpm --filter @objectstack/runtime typecheck: exit 0.eslint --no-inline-config(the rootlintscript's binary and flags) over the 5 TypeScript files: exit 0.4. The census and the anchors, at base and at head.
node scripts/tenant-audit-census.mjs --json), compared per site with the new module's file name mapped back toauth-manager.ts: 237 sites on both sides, 0 per-site differences in object, verb, tenancy, context and elevation, 0 unresolved and 0 unledgered on both, and every total the same.check-tenant-audit-censuspasses at head on the committed documents.dispatch-gatesdoes not derive the ADR one. All six exit 0 at both.check-adr-symbol-anchors: 2,383 anchors across 142 records, 466 symbol (440 declaration, 26 literal), the same at both.check-adr-anchors: 80 anchored files at both, and the decision-letter shift above.check-scripts-symbol-anchors(4,058 anchors),check-spec-docblock-symbol-anchors(5,071 anchors, both read in the same built tree),check-system-context-census(148 elevation read sites) andcheck-platform-checklist(16 areas, 329 items): the same reading at both.5. Gates.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 59 commands for the 5 paths. All 59 ran atb2f9b260deafter the full build, and all 59 exited 0.--ran: 59 derived, 59 run, 0 NOT-MEASURED.check-governed-merges.mjs --branch HEAD: NOT governed, 859 changed lines.node scripts/docs-audit/affected-docs.mjs --json ca381a8a19, exit 0): 7 hand-written pages and 1 release-owned page (releases/v17/17-7.mdx). Each is reached through a literal the moved text carries unchanged:RATE_LIMIT_EXCEEDED(the value ofPHONE_OTP_SEND_REFUSAL_CODE) orSMS_SERVICE_REQUIRED(indeliverPhoneOtp). No page restates a rule this move changes, so no page edit is owed.auth-manager.ts, the new module,api-error-code.pin.test.ts,auth-email-locale.test.tsordispatcher-error-vocabulary.ts.origin/mainhas since moved to5f6c2f1952. Of its 12 new commits, only fix(plugin-auth): the last-administrator guard answers a read fault 503 SERVICE_UNAVAILABLE, the code its sibling guards answer with #25388 touchesplugin-auth(auth-manager-actor-guards.ts,objectql-adapter.ts,last-admin-guard.tsand four suites), none of this cut's files.git merge-treeof it with this head is clean, and on that mergecheck-dispatcher-error-vocabulary(56 found, 56 classified) andapi-error-code.pin.test.tspass.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