@objectstack/platform-objects 14.3.0 → 14.6.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.
@@ -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,7 +107,7 @@ 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",
@@ -130,7 +141,12 @@ var SysUser = data.ObjectSchema.create({
130
141
  label: "Phone Number",
131
142
  type: "text",
132
143
  required: false,
133
- helpText: "Sign-in phone number (E.164, e.g. +8613800000000). Required when no email is given."
144
+ helpText: "Sign-in phone number (E.164, e.g. +8613800000000). Required when no email is given.",
145
+ // Only offer phone when the opt-in phoneNumber auth plugin is loaded —
146
+ // otherwise the create-user endpoint rejects a phone with
147
+ // "Phone numbers require the phoneNumber auth plugin". `features.phoneNumber`
148
+ // is served in /api/v1/auth/config (getPublicConfig).
149
+ visible: "features.phoneNumber == true"
134
150
  },
135
151
  { field: "name", required: false },
136
152
  {
@@ -164,7 +180,7 @@ var SysUser = data.ObjectSchema.create({
164
180
  label: "Set Password",
165
181
  icon: "key-round",
166
182
  variant: "secondary",
167
- locations: ["list_item"],
183
+ locations: ["list_item", "record_header"],
168
184
  type: "api",
169
185
  // #2766 V1 — same path as better-auth's stock route, but served by the
170
186
  // plugin-auth wrapper registered ahead of the catch-all: it accepts the
@@ -206,7 +222,7 @@ var SysUser = data.ObjectSchema.create({
206
222
  label: "Set Platform Role",
207
223
  icon: "shield-check",
208
224
  variant: "secondary",
209
- locations: ["list_item"],
225
+ locations: ["list_item", "record_header"],
210
226
  type: "api",
211
227
  target: "/api/v1/auth/admin/set-role",
212
228
  recordIdParam: "userId",
@@ -221,7 +237,7 @@ var SysUser = data.ObjectSchema.create({
221
237
  label: "Impersonate User",
222
238
  icon: "user-cog",
223
239
  variant: "secondary",
224
- locations: ["list_item"],
240
+ locations: ["list_item", "record_header"],
225
241
  type: "api",
226
242
  target: "/api/v1/auth/admin/impersonate-user",
227
243
  recordIdParam: "userId",
@@ -445,20 +461,28 @@ var SysUser = data.ObjectSchema.create({
445
461
  maxLength: 255,
446
462
  group: "Identity"
447
463
  }),
464
+ // ADR-0092 D4 — with the generic edit affordance open, every non-profile
465
+ // field is readonly so the standard edit form renders it non-editable.
466
+ // This is UX only; the server boundary is the plugin-auth identity write
467
+ // guard (ADR-0092 D2), which strips/rejects these regardless.
448
468
  email: data.Field.email({
449
469
  label: "Email",
450
470
  required: true,
471
+ readonly: true,
472
+ // login identity — change flows through better-auth change-email verification
451
473
  searchable: true,
452
474
  group: "Identity"
453
475
  }),
454
476
  email_verified: data.Field.boolean({
455
477
  label: "Email Verified",
456
478
  defaultValue: false,
479
+ readonly: true,
457
480
  group: "Identity"
458
481
  }),
459
482
  two_factor_enabled: data.Field.boolean({
460
483
  label: "Two-Factor Enabled",
461
484
  defaultValue: false,
485
+ readonly: true,
462
486
  group: "Identity",
463
487
  description: "Whether two-factor authentication is enabled for this user. Maintained by the better-auth `twoFactor` plugin."
464
488
  }),
@@ -466,6 +490,8 @@ var SysUser = data.ObjectSchema.create({
466
490
  role: data.Field.text({
467
491
  label: "Platform Role",
468
492
  required: false,
493
+ readonly: true,
494
+ // ADR-0092 — set via the Set Platform Role action, never the edit form
469
495
  maxLength: 64,
470
496
  group: "Admin",
471
497
  description: "Platform-level role (admin, user, \u2026). Set via the Set Platform Role action."
@@ -473,18 +499,24 @@ var SysUser = data.ObjectSchema.create({
473
499
  banned: data.Field.boolean({
474
500
  label: "Banned",
475
501
  defaultValue: false,
502
+ readonly: true,
503
+ // ADR-0092 — toggled via Ban/Unban actions (session side effects)
476
504
  group: "Admin",
477
505
  description: "When true, the user cannot sign in. Toggle via Ban User / Unban User actions."
478
506
  }),
479
507
  ban_reason: data.Field.text({
480
508
  label: "Ban Reason",
481
509
  required: false,
510
+ readonly: true,
511
+ // ADR-0092 — written by the Ban User action
482
512
  maxLength: 255,
483
513
  group: "Admin"
484
514
  }),
485
515
  ban_expires: data.Field.datetime({
486
516
  label: "Ban Expires",
487
517
  required: false,
518
+ readonly: true,
519
+ // ADR-0092 — written by the Ban User action
488
520
  group: "Admin",
489
521
  description: "When set, the ban auto-clears at this time."
490
522
  }),
@@ -580,6 +612,8 @@ var SysUser = data.ObjectSchema.create({
580
612
  ai_access: data.Field.boolean({
581
613
  label: "AI Access",
582
614
  defaultValue: false,
615
+ readonly: true,
616
+ // ADR-0092 — a licensed-seat grant; flows through the AiSeatPlugin enforcement path
583
617
  group: "Admin",
584
618
  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)."
585
619
  }),
@@ -593,12 +627,16 @@ var SysUser = data.ObjectSchema.create({
593
627
  manager_id: data.Field.lookup("sys_user", {
594
628
  label: "Manager",
595
629
  required: false,
630
+ readonly: true,
631
+ // ADR-0092 — drives own_and_reports RLS scope; org-structure maintenance is its own surface
596
632
  group: "Organization",
597
633
  description: "This user's direct manager. Forms the reporting chain the `own_and_reports` hierarchy scope walks (ADR-0057 / @objectstack/security-enterprise)."
598
634
  }),
599
635
  primary_business_unit_id: data.Field.lookup("sys_business_unit", {
600
636
  label: "Primary Business Unit",
601
637
  required: false,
638
+ readonly: true,
639
+ // ADR-0092 — denormalised projection maintained by plugin-sharing; never hand-edited
602
640
  group: "Organization",
603
641
  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."
604
642
  }),
@@ -2618,6 +2656,12 @@ var SysDeviceCode = data.ObjectSchema.create({
2618
2656
  icon: "key-round",
2619
2657
  isSystem: true,
2620
2658
  managedBy: "better-auth",
2659
+ // ADR-0057: device codes are dead the moment `expires_at` passes — keep a
2660
+ // 1d grace for post-mortem, then reap.
2661
+ lifecycle: {
2662
+ class: "transient",
2663
+ ttl: { field: "expires_at", expireAfter: "1d" }
2664
+ },
2621
2665
  // [ADR-0066 D2/④] Secure-by-default: rows are LIVE pending device-grant
2622
2666
  // codes — reading `user_code`/`device_code` lets an attacker hijack a
2623
2667
  // pending CLI login. Not covered by the wildcard `'*'` grant; admins retain
@@ -3636,9 +3680,13 @@ var SysSsoProvider = data.ObjectSchema.create({
3636
3680
  // for a one-time DNS-TXT challenge and reveals the ready-to-paste record
3637
3681
  // ONCE via `resultDialog`. Routed through the env bridge (plugin-auth /
3638
3682
  // cloud AuthProxyPlugin) which reshapes the `{domainVerificationToken}`
3639
- // response into the `{ data: { dnsRecordName, dnsRecordValue } }` envelope
3640
- // the dialog reads. When the feature is OFF the bridge returns a clear
3641
- // "not enabled for this environment" error instead of a bare 404.
3683
+ // response into a `{ success, data: { dnsRecordName, dnsRecordValue } }`
3684
+ // envelope. The console action runtime unwraps that envelope, so the
3685
+ // `resultDialog` field paths are relative to the inner `data` payload
3686
+ // (`dnsRecordName`, not `data.dnsRecordName`) — consistent with every
3687
+ // other object's resultDialog (create_user, two-factor, OAuth). When the
3688
+ // feature is OFF the bridge returns a clear "not enabled for this
3689
+ // environment" error instead of a bare 404.
3642
3690
  target: "/api/v1/auth/admin/sso/request-domain-verification",
3643
3691
  params: [
3644
3692
  { name: "providerId", field: "provider_id", defaultFromRow: true, required: true },
@@ -3649,9 +3697,9 @@ var SysSsoProvider = data.ObjectSchema.create({
3649
3697
  description: "Add the DNS TXT record below at your domain\u2019s DNS provider, then run \u201CVerify Domain\u201D. The token is shown once.",
3650
3698
  acknowledge: "Done",
3651
3699
  fields: [
3652
- { path: "data.dnsRecordType", label: "Record type", format: "text" },
3653
- { path: "data.dnsRecordName", label: "Name / Host", format: "secret" },
3654
- { path: "data.dnsRecordValue", label: "Value", format: "secret" }
3700
+ { path: "dnsRecordType", label: "Record type", format: "text" },
3701
+ { path: "dnsRecordName", label: "Name / Host", format: "secret" },
3702
+ { path: "dnsRecordValue", label: "Value", format: "secret" }
3655
3703
  ]
3656
3704
  }
3657
3705
  },