@objectstack/platform-objects 13.0.0 → 14.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/apps/index.d.mts +1 -1
  2. package/dist/apps/index.d.ts +1 -1
  3. package/dist/apps/index.js +525 -1200
  4. package/dist/apps/index.js.map +1 -1
  5. package/dist/apps/index.mjs +525 -1200
  6. package/dist/apps/index.mjs.map +1 -1
  7. package/dist/audit/index.d.mts +262 -10
  8. package/dist/audit/index.d.ts +262 -10
  9. package/dist/audit/index.js +14 -0
  10. package/dist/audit/index.js.map +1 -1
  11. package/dist/audit/index.mjs +14 -0
  12. package/dist/audit/index.mjs.map +1 -1
  13. package/dist/identity/index.d.mts +1403 -236
  14. package/dist/identity/index.d.ts +1403 -236
  15. package/dist/identity/index.js +177 -17
  16. package/dist/identity/index.js.map +1 -1
  17. package/dist/identity/index.mjs +177 -17
  18. package/dist/identity/index.mjs.map +1 -1
  19. package/dist/index.d.mts +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1878 -1736
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +1878 -1737
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/metadata-translations/index.js +1020 -488
  26. package/dist/metadata-translations/index.js.map +1 -1
  27. package/dist/metadata-translations/index.mjs +1020 -488
  28. package/dist/metadata-translations/index.mjs.map +1 -1
  29. package/dist/pages/index.d.mts +34 -3
  30. package/dist/pages/index.d.ts +34 -3
  31. package/dist/pages/index.js +111 -0
  32. package/dist/pages/index.js.map +1 -1
  33. package/dist/pages/index.mjs +111 -1
  34. package/dist/pages/index.mjs.map +1 -1
  35. package/dist/plugin.js +1564 -1707
  36. package/dist/plugin.js.map +1 -1
  37. package/dist/plugin.mjs +1564 -1707
  38. package/dist/plugin.mjs.map +1 -1
  39. package/dist/system/index.d.mts +75 -3
  40. package/dist/system/index.d.ts +75 -3
  41. package/package.json +3 -3
@@ -8,6 +8,15 @@ var SysUser = ObjectSchema.create({
8
8
  icon: "user",
9
9
  isSystem: true,
10
10
  managedBy: "better-auth",
11
+ // ADR-0092 D4 — the ONE generic affordance opened on an identity table:
12
+ // standard row editing. Safe because the plugin-auth identity write guard
13
+ // (ADR-0092 D2) enforces the profile whitelist server-side — a user-context
14
+ // update may only touch SYS_USER_PROFILE_EDIT_FIELDS ({name, image});
15
+ // everything else is stripped/rejected regardless of what a form submits.
16
+ // The permission layer still decides WHO may edit (platform admins only by
17
+ // default; member/org-admin sets keep allowEdit: false). create / import /
18
+ // delete stay bucket-default (off).
19
+ userActions: { edit: true },
11
20
  // ADR-0010 §3.7 — identity table is managed by better-auth; schema must not drift.
12
21
  protection: {
13
22
  lock: "full",
@@ -55,15 +64,17 @@ var SysUser = ObjectSchema.create({
55
64
  // These actions hit /api/v1/auth/admin/* endpoints that are only
56
65
  // wired when `auth.plugins.admin` is enabled. When the plugin is
57
66
  // disabled the actions still render (schema is static) but server
58
- // returns 404. UI surfaces them under the row menu so platform
59
- // admins can manage accounts without dropping to SQL or
67
+ // returns 404. UI surfaces them under the row menu AND the
68
+ // record-detail header (`record_header`, overflowing into the
69
+ // "More" menu) so platform admins can manage an account from either
70
+ // the Users list or an open user record — without dropping to SQL or
60
71
  // a custom Setup wizard.
61
72
  {
62
73
  name: "ban_user",
63
74
  label: "Ban User",
64
75
  icon: "ban",
65
76
  variant: "danger",
66
- locations: ["list_item"],
77
+ locations: ["list_item", "record_header"],
67
78
  type: "api",
68
79
  target: "/api/v1/auth/admin/ban-user",
69
80
  recordIdParam: "userId",
@@ -79,7 +90,7 @@ var SysUser = ObjectSchema.create({
79
90
  label: "Unban User",
80
91
  icon: "check-circle-2",
81
92
  variant: "secondary",
82
- locations: ["list_item"],
93
+ locations: ["list_item", "record_header"],
83
94
  type: "api",
84
95
  target: "/api/v1/auth/admin/unban-user",
85
96
  recordIdParam: "userId",
@@ -94,34 +105,117 @@ var SysUser = ObjectSchema.create({
94
105
  label: "Unlock Account",
95
106
  icon: "lock-open",
96
107
  variant: "secondary",
97
- locations: ["list_item"],
108
+ locations: ["list_item", "record_header"],
98
109
  type: "api",
99
110
  target: "/api/v1/auth/admin/unlock-user",
100
111
  recordIdParam: "userId",
101
112
  successMessage: "Account unlocked",
102
113
  refreshAfter: true
103
114
  },
115
+ {
116
+ // #2766 V1 — a platform admin can add a login-capable teammate without
117
+ // the email-dependent invite flow. Hits the plugin-auth wrapper route
118
+ // (NOT better-auth's stock /admin/create-user): the wrapper runs the
119
+ // ADR-0068 admin gate, drives the better-auth pipeline (scrypt hash +
120
+ // credential sys_account), stamps must_change_password, and — when
121
+ // "Generate temporary password" is picked — returns the password ONCE
122
+ // in the response for the result dialog. It is never persisted or logged.
123
+ name: "create_user",
124
+ label: "Create User",
125
+ icon: "user-plus",
126
+ variant: "secondary",
127
+ locations: ["list_toolbar"],
128
+ type: "api",
129
+ target: "/api/v1/auth/admin/create-user",
130
+ visible: "features.admin == true",
131
+ successMessage: "User created",
132
+ refreshAfter: true,
133
+ params: [
134
+ // The endpoint requires email OR phone (phone-only users get a
135
+ // placeholder address; requires auth.plugins.phoneNumber).
136
+ { field: "email", required: false },
137
+ {
138
+ name: "phoneNumber",
139
+ label: "Phone Number",
140
+ type: "text",
141
+ required: false,
142
+ helpText: "Sign-in phone number (E.164, e.g. +8613800000000). Required when no email is given."
143
+ },
144
+ { field: "name", required: false },
145
+ {
146
+ name: "generatePassword",
147
+ label: "Generate Temporary Password",
148
+ type: "boolean",
149
+ required: false,
150
+ defaultValue: true
151
+ },
152
+ { name: "password", label: "Password (leave empty to generate)", type: "text", required: false },
153
+ {
154
+ name: "mustChangePassword",
155
+ label: "Require Password Change On First Login",
156
+ type: "boolean",
157
+ required: false,
158
+ defaultValue: true
159
+ }
160
+ ],
161
+ resultDialog: {
162
+ title: "User Created",
163
+ description: "Copy the temporary password now \u2014 it is shown only once and never stored.",
164
+ acknowledge: "I have saved this password",
165
+ fields: [
166
+ { path: "user.email", label: "Email", format: "text" },
167
+ { path: "temporaryPassword", label: "Temporary Password", format: "secret" }
168
+ ]
169
+ }
170
+ },
104
171
  {
105
172
  name: "set_user_password",
106
173
  label: "Set Password",
107
174
  icon: "key-round",
108
175
  variant: "secondary",
109
- locations: ["list_item"],
176
+ locations: ["list_item", "record_header"],
110
177
  type: "api",
178
+ // #2766 V1 — same path as better-auth's stock route, but served by the
179
+ // plugin-auth wrapper registered ahead of the catch-all: it accepts the
180
+ // ADR-0068 platform-admin signals (the stock handler only honors the
181
+ // legacy role scalar), can mint a temporary password, and stamps
182
+ // must_change_password.
111
183
  target: "/api/v1/auth/admin/set-user-password",
112
184
  recordIdParam: "userId",
113
185
  successMessage: "Password updated",
114
186
  refreshAfter: false,
115
187
  params: [
116
- { name: "newPassword", label: "New Password", type: "text", required: true }
117
- ]
188
+ {
189
+ name: "generatePassword",
190
+ label: "Generate Temporary Password",
191
+ type: "boolean",
192
+ required: false,
193
+ defaultValue: false
194
+ },
195
+ { name: "newPassword", label: "New Password (leave empty to generate)", type: "text", required: false },
196
+ {
197
+ name: "mustChangePassword",
198
+ label: "Require Password Change On Next Login",
199
+ type: "boolean",
200
+ required: false,
201
+ defaultValue: true
202
+ }
203
+ ],
204
+ resultDialog: {
205
+ title: "Password Updated",
206
+ description: "If a temporary password was generated, copy it now \u2014 it is shown only once and never stored.",
207
+ acknowledge: "Done",
208
+ fields: [
209
+ { path: "temporaryPassword", label: "Temporary Password", format: "secret" }
210
+ ]
211
+ }
118
212
  },
119
213
  {
120
214
  name: "set_user_role",
121
215
  label: "Set Platform Role",
122
216
  icon: "shield-check",
123
217
  variant: "secondary",
124
- locations: ["list_item"],
218
+ locations: ["list_item", "record_header"],
125
219
  type: "api",
126
220
  target: "/api/v1/auth/admin/set-role",
127
221
  recordIdParam: "userId",
@@ -136,7 +230,7 @@ var SysUser = ObjectSchema.create({
136
230
  label: "Impersonate User",
137
231
  icon: "user-cog",
138
232
  variant: "secondary",
139
- locations: ["list_item"],
233
+ locations: ["list_item", "record_header"],
140
234
  type: "api",
141
235
  target: "/api/v1/auth/admin/impersonate-user",
142
236
  recordIdParam: "userId",
@@ -360,20 +454,28 @@ var SysUser = ObjectSchema.create({
360
454
  maxLength: 255,
361
455
  group: "Identity"
362
456
  }),
457
+ // ADR-0092 D4 — with the generic edit affordance open, every non-profile
458
+ // field is readonly so the standard edit form renders it non-editable.
459
+ // This is UX only; the server boundary is the plugin-auth identity write
460
+ // guard (ADR-0092 D2), which strips/rejects these regardless.
363
461
  email: Field.email({
364
462
  label: "Email",
365
463
  required: true,
464
+ readonly: true,
465
+ // login identity — change flows through better-auth change-email verification
366
466
  searchable: true,
367
467
  group: "Identity"
368
468
  }),
369
469
  email_verified: Field.boolean({
370
470
  label: "Email Verified",
371
471
  defaultValue: false,
472
+ readonly: true,
372
473
  group: "Identity"
373
474
  }),
374
475
  two_factor_enabled: Field.boolean({
375
476
  label: "Two-Factor Enabled",
376
477
  defaultValue: false,
478
+ readonly: true,
377
479
  group: "Identity",
378
480
  description: "Whether two-factor authentication is enabled for this user. Maintained by the better-auth `twoFactor` plugin."
379
481
  }),
@@ -381,6 +483,8 @@ var SysUser = ObjectSchema.create({
381
483
  role: Field.text({
382
484
  label: "Platform Role",
383
485
  required: false,
486
+ readonly: true,
487
+ // ADR-0092 — set via the Set Platform Role action, never the edit form
384
488
  maxLength: 64,
385
489
  group: "Admin",
386
490
  description: "Platform-level role (admin, user, \u2026). Set via the Set Platform Role action."
@@ -388,18 +492,24 @@ var SysUser = ObjectSchema.create({
388
492
  banned: Field.boolean({
389
493
  label: "Banned",
390
494
  defaultValue: false,
495
+ readonly: true,
496
+ // ADR-0092 — toggled via Ban/Unban actions (session side effects)
391
497
  group: "Admin",
392
498
  description: "When true, the user cannot sign in. Toggle via Ban User / Unban User actions."
393
499
  }),
394
500
  ban_reason: Field.text({
395
501
  label: "Ban Reason",
396
502
  required: false,
503
+ readonly: true,
504
+ // ADR-0092 — written by the Ban User action
397
505
  maxLength: 255,
398
506
  group: "Admin"
399
507
  }),
400
508
  ban_expires: Field.datetime({
401
509
  label: "Ban Expires",
402
510
  required: false,
511
+ readonly: true,
512
+ // ADR-0092 — written by the Ban User action
403
513
  group: "Admin",
404
514
  description: "When set, the ban auto-clears at this time."
405
515
  }),
@@ -433,6 +543,37 @@ var SysUser = ObjectSchema.create({
433
543
  group: "Admin",
434
544
  description: "Timestamp of the last password change. Backs password_expiry_days; system-managed."
435
545
  }),
546
+ // #2766 V1.5 — phone-number sign-in identifier (better-auth phoneNumber
547
+ // plugin, mapped via buildPhoneNumberPluginSchema). Unique when present;
548
+ // null for email-only accounts. Written through better-auth, not CRUD.
549
+ phone_number: Field.text({
550
+ label: "Phone Number",
551
+ required: false,
552
+ readonly: true,
553
+ searchable: true,
554
+ maxLength: 32,
555
+ group: "Account",
556
+ description: "Sign-in phone number (E.164 recommended). Unique per user; managed by better-auth when the phoneNumber plugin is enabled."
557
+ }),
558
+ phone_number_verified: Field.boolean({
559
+ label: "Phone Verified",
560
+ defaultValue: false,
561
+ readonly: true,
562
+ group: "Account",
563
+ description: "Whether the phone number has been verified (OTP verification requires SMS infrastructure; false until that ships). System-managed."
564
+ }),
565
+ // #2766 V1 — admin-issued "must change password on next sign-in" flag.
566
+ // Set by the /admin/create-user and /admin/set-user-password routes when
567
+ // a temporary password is issued; enforced through the ADR-0069 authGate
568
+ // (surfaces as PASSWORD_EXPIRED); cleared by stampPasswordChangedAt once
569
+ // the user completes a password change.
570
+ must_change_password: Field.boolean({
571
+ label: "Must Change Password",
572
+ defaultValue: false,
573
+ readonly: true,
574
+ group: "Admin",
575
+ description: "When true, the user is blocked (403 PASSWORD_EXPIRED) until they change their password. Stamped by the admin user-management routes; system-managed."
576
+ }),
436
577
  // ADR-0069 D3 — when enforced MFA first applied to this user; starts the
437
578
  // grace clock. Stamped lazily at session validation; system-managed.
438
579
  mfa_required_at: Field.datetime({
@@ -464,6 +605,8 @@ var SysUser = ObjectSchema.create({
464
605
  ai_access: Field.boolean({
465
606
  label: "AI Access",
466
607
  defaultValue: false,
608
+ readonly: true,
609
+ // ADR-0092 — a licensed-seat grant; flows through the AiSeatPlugin enforcement path
467
610
  group: "Admin",
468
611
  description: "Whether this user holds an AI seat \u2014 grants access to the in-UI AI agents (build / ask). The framework synthesizes the `ai_seat` capability from this flag (plugin-hono-server resolveCtx). Assignment is capped by the licensed / purchased seat count (enforced by @objectstack/security-enterprise AiSeatPlugin). Owned by objectql (better-auth is oblivious to this column)."
469
612
  }),
@@ -477,12 +620,16 @@ var SysUser = ObjectSchema.create({
477
620
  manager_id: Field.lookup("sys_user", {
478
621
  label: "Manager",
479
622
  required: false,
623
+ readonly: true,
624
+ // ADR-0092 — drives own_and_reports RLS scope; org-structure maintenance is its own surface
480
625
  group: "Organization",
481
626
  description: "This user's direct manager. Forms the reporting chain the `own_and_reports` hierarchy scope walks (ADR-0057 / @objectstack/security-enterprise)."
482
627
  }),
483
628
  primary_business_unit_id: Field.lookup("sys_business_unit", {
484
629
  label: "Primary Business Unit",
485
630
  required: false,
631
+ readonly: true,
632
+ // ADR-0092 — denormalised projection maintained by plugin-sharing; never hand-edited
486
633
  group: "Organization",
487
634
  description: "The user's primary business unit \u2014 a denormalised projection of sys_business_unit_member.is_primary, maintained by plugin-sharing (ADR-0057 addendum D12). Lets a user-lookup filter candidates by business unit without traversing the membership junction. Do not edit directly; set it via business-unit membership."
488
635
  }),
@@ -530,7 +677,10 @@ var SysUser = ObjectSchema.create({
530
677
  },
531
678
  indexes: [
532
679
  { fields: ["email"], unique: true },
533
- { fields: ["created_at"], unique: false }
680
+ { fields: ["created_at"], unique: false },
681
+ // #2766 V1.5 — phone sign-in identifier; unique when present (null for
682
+ // email-only accounts), also the upsert match key for identity import.
683
+ { fields: ["phone_number"], unique: true }
534
684
  ],
535
685
  enable: {
536
686
  trackHistory: true,
@@ -2499,6 +2649,12 @@ var SysDeviceCode = ObjectSchema.create({
2499
2649
  icon: "key-round",
2500
2650
  isSystem: true,
2501
2651
  managedBy: "better-auth",
2652
+ // ADR-0057: device codes are dead the moment `expires_at` passes — keep a
2653
+ // 1d grace for post-mortem, then reap.
2654
+ lifecycle: {
2655
+ class: "transient",
2656
+ ttl: { field: "expires_at", expireAfter: "1d" }
2657
+ },
2502
2658
  // [ADR-0066 D2/④] Secure-by-default: rows are LIVE pending device-grant
2503
2659
  // codes — reading `user_code`/`device_code` lets an attacker hijack a
2504
2660
  // pending CLI login. Not covered by the wildcard `'*'` grant; admins retain
@@ -3517,9 +3673,13 @@ var SysSsoProvider = ObjectSchema.create({
3517
3673
  // for a one-time DNS-TXT challenge and reveals the ready-to-paste record
3518
3674
  // ONCE via `resultDialog`. Routed through the env bridge (plugin-auth /
3519
3675
  // cloud AuthProxyPlugin) which reshapes the `{domainVerificationToken}`
3520
- // response into the `{ data: { dnsRecordName, dnsRecordValue } }` envelope
3521
- // the dialog reads. When the feature is OFF the bridge returns a clear
3522
- // "not enabled for this environment" error instead of a bare 404.
3676
+ // response into a `{ success, data: { dnsRecordName, dnsRecordValue } }`
3677
+ // envelope. The console action runtime unwraps that envelope, so the
3678
+ // `resultDialog` field paths are relative to the inner `data` payload
3679
+ // (`dnsRecordName`, not `data.dnsRecordName`) — consistent with every
3680
+ // other object's resultDialog (create_user, two-factor, OAuth). When the
3681
+ // feature is OFF the bridge returns a clear "not enabled for this
3682
+ // environment" error instead of a bare 404.
3523
3683
  target: "/api/v1/auth/admin/sso/request-domain-verification",
3524
3684
  params: [
3525
3685
  { name: "providerId", field: "provider_id", defaultFromRow: true, required: true },
@@ -3530,9 +3690,9 @@ var SysSsoProvider = ObjectSchema.create({
3530
3690
  description: "Add the DNS TXT record below at your domain\u2019s DNS provider, then run \u201CVerify Domain\u201D. The token is shown once.",
3531
3691
  acknowledge: "Done",
3532
3692
  fields: [
3533
- { path: "data.dnsRecordType", label: "Record type", format: "text" },
3534
- { path: "data.dnsRecordName", label: "Name / Host", format: "secret" },
3535
- { path: "data.dnsRecordValue", label: "Value", format: "secret" }
3693
+ { path: "dnsRecordType", label: "Record type", format: "text" },
3694
+ { path: "dnsRecordName", label: "Name / Host", format: "secret" },
3695
+ { path: "dnsRecordValue", label: "Value", format: "secret" }
3536
3696
  ]
3537
3697
  }
3538
3698
  },