Repository navigation
fix(plugin-auth)!: admin import-users sizes a generated temporary password to the deployment's length range - #26018
Conversation
…deployment range (red) Claude-Session: https://claude.ai/code/session_01A5wzhKHa98KsGQNHM9Z3Tq Co-authored-by: Claude <noreply@anthropic.com>
…loyment's length range The identity import minted every temporary password at 16 characters, below a configured minPasswordLength above 16, and the user signed in with it. IdentityImportDeps now carries getAuthContext (the reader create-user holds); the batch reads password.config once before any write and sizes each generated password min(max(16, min), max). The sizing moves to an internal module both doors import. Claude-Session: https://claude.ai/code/session_01A5wzhKHa98KsGQNHM9Z3Tq Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A5wzhKHa98KsGQNHM9Z3Tq Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check8 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. 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 3e74e51a240827a52ac05019472e29dde562f871 && git checkout 3e74e51a240827a52ac05019472e29dde562f871
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a3e5bfa69348cd8dd7067ac3c734f753875d4bc4 425cc7db21b4e5ac643f0ae4bf7994e900b82c4b && git checkout -B drift-repro a3e5bfa69348cd8dd7067ac3c734f753875d4bc4 && git merge --no-ff 425cc7db21b4e5ac643f0ae4bf7994e900b82c4b
node scripts/docs-audit/affected-docs.mjs --json a3e5bfa69348cd8dd7067ac3c734f753875d4bc4 |
Contract reviewServed-tier: Read against ① Derived judgmentsAccept-set and public-surface changes the diff implies, each judged:
② Semver level
③ Boundary flagsDev report (
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | draft, base main, Fixes #25932, Clause-②: no (narrowing) (the claim expected yes; the dev measured the narrowing and the review confirms it), Metadata-protocol: none, patch on @objectstack/plugin-auth with the BREAKING banner, ! on the title and the ADR-0087 marker. |
| Narrowing | the exported IdentityImportDeps gains a REQUIRED getAuthContext(); required rather than optional, because the only fallback for an absent member is the unsized 16 the card reports. Every in-repo constructor (the route and seven test files) supplies it; no package outside plugin-auth builds these deps. The HTTP request shape of POST /admin/import-users is unchanged. |
| Fix | the import door sizes a generated temporary password min(max(16, minPasswordLength), maxPasswordLength) from the same auth context create-user reads (PR #25927); the default range keeps 16. |
| Evidence | red first and ablation; plugin-auth 3726 green; 72/73 derived gates (dual-build-cjs-loads answered by CI's Build Core); every check-run on the head green or an expected skip. |
| Carried | the dogfood authz-probe-blind-spot red was main's own, fixed on main by #26002. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready + auto-merge through the relay. |
Generated by Claude Code
Requested by Leehom · project thread
Fixes #25932
Clause-②: no (narrowing) — the exported
IdentityImportDepsgains a required member,getAuthContext(), so a deps object without it no longer type-checks againstrunAdminImportUsers. No request shape is widened or refused.Metadata-protocol: none
Why
POST /api/v1/auth/admin/import-usersminted every temporary password at 16 characters:generateTemporaryPassword()was called with no length, and the import deps carried no auth context to read the range from. Under a configured minimum above 16 the row got a password below it and the user signed in with it (the card's measurement). This is the import half of what PR #25927 did for create-user.What changed
IdentityImportDeps.getAuthContext()(required): better-auth's$context, the reader create-user already holds.AuthPlugin's import route wires it toAuthManager.getAuthContext(). No second reader of the range exists: the import readspassword.configfrom that context, as create-user does.runAdminImportUsersreads the range once, before any row is written, and sizes each generated temporary passwordmin(max(16, minPasswordLength), maxPasswordLength). If the context cannot be read the request is refused throughmapAuthApiError(import-users failed) with nothing created, the same mapping create-user uses for the same read.temporaryPasswordLengthmoved fromadmin-user-endpoints.tsto the internal moduletemporary-password-length.ts(not re-exported from the package index), and both doors import it.IdentityImportDepsgains the member (real-manager fixtures wiremanager.getAuthContext(), mocks answer better-auth's default 8 to 128 range).Changeset:
.changeset/25932-import-users-temporary-password-length.md(@objectstack/plugin-authpatch, BREAKING banner for a host that callsrunAdminImportUsersitself, ADR-0087not-required (runtime-interface-only packages/plugins/plugin-auth/src/admin-import-users.ts#IdentityImportDeps), before / now / fix).Verification
Code head
71668cb6(the changeset commit after it touches no code).admin-import-users-password-length.test.ts, commit0c9118a4, run before the fix): realAuthManagerover SQLite, the plugin's real mounted route.temporarypolicy,minPasswordLength: 24:expected 'nw33z7aozuB%upS@' to have a length of 24 but got 16.autopolicy, no email service (a row with no deliverable channel):expected '2#jZWgdP#H2oKgGL' to have a length of 24 but got 16. Soautotakes the same path, now measured rather than read from code.maxPasswordLength: 14: the row failed (created: 0), because better-auth refused the 16-character password.sign-in/emailanswers200with each; ④ answers 14 characters and signs in; ③ stays 16.admin-import-users.test.ts: a context read that throws is refused500 INTERNAL_ERRORwithcreateUserand the engine update never called.scripts/ablation-replace.mjs, anchorgenerateTemporaryPassword(temporaryLength)togenerateTemporaryPassword(), 1 hit, bloba7809b32toe056f1db): ①, ② and ④ red, ③ green; restored, blob equals HEAD andgit diff HEADempty. The tests importsrc, so no build leg.pnpm --filter @objectstack/plugin-auth exec vitest run: 196 files, 3726 passed, 10 skipped, lock VERDICT command-exit 0.pnpm --filter @objectstack/plugin-auth typecheck(after building the package, which the examples program reads): exit 0,check:test-typecheckOK.eslint --no-inline-config packages/plugins/plugin-auth: exit 0.dispatch-gates --commandsat425cc7db, 73 commands): 72 exit 0,dispatch-gates --ranreconciles 73 derived, 72 run, 1 NOT MEASURED. The one ischeck:dual-build-cjs-loads, PREREQUISITE NOT MET (exit 3: it reads every package'sdist, and this worktree built only the plugin-auth and dogfood closures); CI'sLint & Repo Gatesruns it. Three first-pass reds were prerequisites, re-run green:check-plugin-teardown-shape --self-test(shallow clone, fixed by fetching its pinned fixture commit),check:dts-closure(a concurrent closure build was rewritingdist) andcheck:type-check-debt(lock queue timeout, then exit 0 under the lock).425cc7db(title carries!): 295 of 296 files pass, 2522 tests pass. The one red isauthz-probe-blind-spot.test.tsonpackages/runtime/src/domains/i18n.ts(controlhandleI18nRequest: expected 3, got 4). This diff does not touchpackages/runtime; the file already holds 4 occurrences at the base05f7758f, since69fa23d6(fix(runtime): GET /i18n/translations/:locale carries an ETag and Cache-Control: private, no-cache, and a matching If-None-Match is answered 304 (#25785) #25956), so it is red onmain's own dogfood battery and is reported to the seat rather than changed here.Acceptance notes
IdentityImportDeps.getAuthContextis required rather than optional. An optional member would need a fallback when absent, and that fallback is the 16-character defect for any host that forgets it; a required one makes the compiler name every such host.#25824(create-user and set-user-password) is landed by PR fix(plugin-auth)!: admin create-user refuses a password outside the deployment's length range #25927; this PR does not touch those doors beyond moving the shared sizing function.Generated by Claude Code