Commit 2a71f48
* feat(auth): admin direct user management + phone sign-in (#2766 V1/V1.5)
V1 — platform admins can create login-capable accounts and (re)set
passwords without the email-dependent invite flow:
- POST /api/v1/auth/admin/create-user: ADR-0068 admin gate, then a trusted
header-less auth.api.createUser call (better-auth pipeline: scrypt hash +
credential sys_account). Optional generated temporary password, returned
ONCE in the response (never logged / persisted / audited).
- POST /api/v1/auth/admin/set-user-password: shadows the stock better-auth
route, which only honors the legacy role scalar; mirrors its hash +
credential-account core through $context under the ADR-0068 gate, adds
generate-password and must-change stamping.
- sys_user.must_change_password + computeAuthGate branch (reuses
PASSWORD_EXPIRED so the existing 403 seam and Console redirect apply);
cleared by stampPasswordChangedAt on any completed password change.
Gate activation via a lazy-TTL "any flagged user" cache mirroring
_orgMfaCache, primed synchronously on the issuing node.
- create_user / upgraded set_user_password actions on sys_user (pure
schema; resultDialog reveals the temporary password one time).
- features.admin now mirrors enabled.admin (SCIM forces admin on,
ADR-0071) instead of advertising false in SCIM deployments.
- Explicit best-effort sys_audit_log rows (better-auth writes bypass the
ObjectQL hooks plugin-audit subscribes to).
V1.5 — better-auth phoneNumber plugin (opt-in auth.plugins.phoneNumber):
- Phone+password sign-in only (POST /sign-in/phone-number); sendOTP throws
NOT_SUPPORTED until SMS infrastructure exists (tracked separately).
- sys_user.phone_number (unique) / phone_number_verified, snake_case via
buildPhoneNumberPluginSchema().
- Phone-only accounts get a placeholder email (u-<random>@placeholder.invalid,
RFC 2606 — never deliverable, never derived from the phone number so
exports/logs can't leak it). Every mail callback (reset / invitation /
magic link) refuses placeholder recipients with PLACEHOLDER_EMAIL.
- Invitation/magic-link warn logs no longer print bearer URLs outside dev.
Closes nothing yet — V2 (bulk import) follows on this branch. Ref #2766 #2758.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016r6eiJzivw1CkwTDSGho1o
* refactor(rest): extract prepareImportRequest + CSV/xlsx parsers to import-prepare.ts (#2766 V2a)
Verbatim move out of rest-server.ts (module-private → exported) so the
identity import endpoint in plugin-auth can accept payloads byte-identical
to the generic /data/:object/import routes without re-implementing the
parsers. rest-server.ts imports the extracted module; the two call sites
and behavior are unchanged (import-integration tests as the regression
gate). Also exports the import building blocks (runImport, coerceRow,
buildFieldMetaMap, types) from the package index for cross-package reuse.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016r6eiJzivw1CkwTDSGho1o
* feat(auth): identity bulk import endpoint POST /admin/import-users (#2766 V2)
Dedicated sys_user import (re-scoped #2758) — the generic /data import
path writes through ObjectQL, bypassing better-auth hashing and never
creating the sys_account credential row, so it stays closed for identity.
This endpoint reuses the generic import FRAMEWORK (prepareImportRequest
payload parsing: rows[]/csv/xlsxBase64 + mapping; runImport row engine:
dryRun, upsert matching, row results) with an identity write protocol:
- createData drives auth.api.createUser per row (scrypt + credential
account); no bulk primitive on purpose.
- passwordPolicy 'invite': rows must carry a real email (row-level
INVITE_REQUIRES_EMAIL at validation/dryRun time; request-level 400
when no EmailService is wired); accounts are created with a throwaway
password and a set-your-password reset email is requested per created
row. Email failure marks the row INVITE_EMAIL_FAILED — created, not
rolled back.
- passwordPolicy 'temporary': generated password per row, returned ONCE
in the response; must_change_password stamped (403 PASSWORD_EXPIRED
gate from V1). Red-line tests assert no password reaches audit rows,
logs, or any persisted surface.
- mode insert|upsert, matchBy email|phone (phone requires the V1.5
phoneNumber plugin); upsert updates touch profile fields only
(name/phone_number/role) — never email, never credentials, so a
re-imported CSV can't silently reset existing users' passwords; blank
match keys skip instead of creating duplicates.
- Phone-only rows (temporary policy) get placeholder emails; imported
password columns are ignored (pre-hashed migration is out of scope).
- Sync only, <=500 rows/request (scrypt ~100ms/row); async:true returns
ASYNC_NOT_SUPPORTED with batching guidance — an async job is a
persistence surface and cannot carry temporary passwords; tracked as
a follow-up. No undo by design (undo == bulk-deleting users).
- Run-level sys_audit_log row (better-auth writes bypass the ObjectQL
hooks plugin-audit subscribes to), password-free.
plugin-auth now depends on @objectstack/rest (no cycle; rest does not
depend on plugin-auth).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016r6eiJzivw1CkwTDSGho1o
* chore: add changeset for #2766 V1/V1.5/V2
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016r6eiJzivw1CkwTDSGho1o
* fix(auth): address CodeQL alerts — linear-time email check, drop unused import
Replaces the /^\S+@\S+\.\S+$/ plausibility regex (polynomially
backtrackable on adversarial input — js/polynomial-redos, alert 843) with
a linear indexOf-based isLikelyEmail() shared by the create-user and
import-users endpoints, with an adversarial-input regression test.
Removes the unused beforeEach import (alert 844).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016r6eiJzivw1CkwTDSGho1o
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent f517341 commit 2a71f48
19 files changed
Lines changed: 2698 additions & 332 deletions
File tree
- .changeset
- packages
- platform-objects/src/identity
- plugins/plugin-auth
- src
- rest/src
- spec/src/system
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
Lines changed: 129 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
114 | 114 | | |
115 | 115 | | |
116 | 116 | | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
117 | 174 | | |
118 | 175 | | |
119 | 176 | | |
120 | 177 | | |
121 | 178 | | |
122 | 179 | | |
123 | 180 | | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
124 | 186 | | |
125 | 187 | | |
126 | 188 | | |
127 | 189 | | |
128 | 190 | | |
129 | | - | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
130 | 206 | | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
131 | 216 | | |
132 | 217 | | |
133 | 218 | | |
| |||
460 | 545 | | |
461 | 546 | | |
462 | 547 | | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
463 | 588 | | |
464 | 589 | | |
465 | 590 | | |
| |||
574 | 699 | | |
575 | 700 | | |
576 | 701 | | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
577 | 705 | | |
578 | 706 | | |
579 | 707 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| 28 | + | |
28 | 29 | | |
29 | 30 | | |
30 | 31 | | |
| |||
0 commit comments