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