@objectstack/platform-objects 0.1.0 → 4.1.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 (57) hide show
  1. package/dist/apps/index.d.mts +16 -48
  2. package/dist/apps/index.d.ts +16 -48
  3. package/dist/apps/index.js +139 -215
  4. package/dist/apps/index.js.map +1 -1
  5. package/dist/apps/index.mjs +140 -210
  6. package/dist/apps/index.mjs.map +1 -1
  7. package/dist/audit/index.d.mts +40249 -150
  8. package/dist/audit/index.d.ts +40249 -150
  9. package/dist/audit/index.js +1428 -0
  10. package/dist/audit/index.js.map +1 -1
  11. package/dist/audit/index.mjs +1417 -1
  12. package/dist/audit/index.mjs.map +1 -1
  13. package/dist/identity/index.d.mts +18792 -2520
  14. package/dist/identity/index.d.ts +18792 -2520
  15. package/dist/identity/index.js +1107 -6
  16. package/dist/identity/index.js.map +1 -1
  17. package/dist/identity/index.mjs +1106 -7
  18. package/dist/identity/index.mjs.map +1 -1
  19. package/dist/index.d.mts +9 -7
  20. package/dist/index.d.ts +9 -7
  21. package/dist/index.js +3939 -1504
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +3917 -1487
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/integration/index.d.mts +2905 -0
  26. package/dist/integration/index.d.ts +2905 -0
  27. package/dist/integration/index.js +140 -0
  28. package/dist/integration/index.js.map +1 -0
  29. package/dist/integration/index.mjs +138 -0
  30. package/dist/integration/index.mjs.map +1 -0
  31. package/dist/metadata/index.d.mts +1426 -19092
  32. package/dist/metadata/index.d.ts +1426 -19092
  33. package/dist/metadata/index.js +29 -619
  34. package/dist/metadata/index.js.map +1 -1
  35. package/dist/metadata/index.mjs +30 -615
  36. package/dist/metadata/index.mjs.map +1 -1
  37. package/dist/security/index.d.mts +10759 -60
  38. package/dist/security/index.d.ts +10759 -60
  39. package/dist/security/index.js +786 -0
  40. package/dist/security/index.js.map +1 -1
  41. package/dist/security/index.mjs +782 -1
  42. package/dist/security/index.mjs.map +1 -1
  43. package/dist/system/index.d.mts +8409 -0
  44. package/dist/system/index.d.ts +8409 -0
  45. package/dist/system/index.js +395 -0
  46. package/dist/system/index.js.map +1 -0
  47. package/dist/system/index.mjs +391 -0
  48. package/dist/system/index.mjs.map +1 -0
  49. package/package.json +13 -8
  50. package/dist/tenant/index.d.mts +0 -16454
  51. package/dist/tenant/index.d.ts +0 -16454
  52. package/dist/tenant/index.js +0 -741
  53. package/dist/tenant/index.js.map +0 -1
  54. package/dist/tenant/index.mjs +0 -733
  55. package/dist/tenant/index.mjs.map +0 -1
  56. /package/dist/{state-machine.zod-BFg-VE0M.d-Ek3_yo9P.d.mts → state-machine.zod-BNanU03M.d-Ek3_yo9P.d.mts} +0 -0
  57. /package/dist/{state-machine.zod-BFg-VE0M.d-Ek3_yo9P.d.ts → state-machine.zod-BNanU03M.d-Ek3_yo9P.d.ts} +0 -0
@@ -9,10 +9,154 @@ var SysUser = data.ObjectSchema.create({
9
9
  pluralLabel: "Users",
10
10
  icon: "user",
11
11
  isSystem: true,
12
+ managedBy: "better-auth",
12
13
  description: "User accounts for authentication",
13
14
  displayNameField: "name",
14
15
  titleFormat: "{name}",
15
16
  compactLayout: ["name", "email", "email_verified"],
17
+ // Custom actions — generic CRUD is suppressed because user accounts are
18
+ // managed by better-auth, but we still need first-class affordances for
19
+ // common operations. Each action delegates to a Console-side named script
20
+ // registered on the ActionRunner (see objectui `AppContent.tsx`). Adding
21
+ // new affordances (reset password, revoke session, …) is now a pure
22
+ // schema + script-registration change — no per-view code.
23
+ actions: [
24
+ {
25
+ name: "invite_user",
26
+ label: "Invite User",
27
+ icon: "user-plus",
28
+ variant: "primary",
29
+ locations: ["list_toolbar"],
30
+ type: "api",
31
+ target: "/api/v1/auth/organization/invite-member",
32
+ successMessage: "Invitation sent",
33
+ refreshAfter: true,
34
+ params: [
35
+ { field: "email", required: true },
36
+ { field: "role", objectOverride: "sys_member", required: true }
37
+ ]
38
+ },
39
+ // ── Platform admin operations (require better-auth `admin` plugin) ─
40
+ //
41
+ // These actions hit /api/v1/auth/admin/* endpoints that are only
42
+ // wired when `auth.plugins.admin` is enabled. When the plugin is
43
+ // disabled the actions still render (schema is static) but server
44
+ // returns 404. UI surfaces them under the row menu so platform
45
+ // admins can manage accounts without dropping to SQL or
46
+ // a custom Setup wizard.
47
+ {
48
+ name: "ban_user",
49
+ label: "Ban User",
50
+ icon: "ban",
51
+ variant: "danger",
52
+ locations: ["list_item"],
53
+ type: "api",
54
+ target: "/api/v1/auth/admin/ban-user",
55
+ recordIdParam: "userId",
56
+ successMessage: "User banned",
57
+ refreshAfter: true,
58
+ confirmText: "Ban this user? They will be signed out and unable to sign in until unbanned.",
59
+ params: [
60
+ { name: "banReason", label: "Ban Reason", type: "text", required: false }
61
+ ]
62
+ },
63
+ {
64
+ name: "unban_user",
65
+ label: "Unban User",
66
+ icon: "check-circle-2",
67
+ variant: "secondary",
68
+ locations: ["list_item"],
69
+ type: "api",
70
+ target: "/api/v1/auth/admin/unban-user",
71
+ recordIdParam: "userId",
72
+ successMessage: "User unbanned",
73
+ refreshAfter: true
74
+ },
75
+ {
76
+ name: "set_user_password",
77
+ label: "Set Password",
78
+ icon: "key-round",
79
+ variant: "secondary",
80
+ locations: ["list_item"],
81
+ type: "api",
82
+ target: "/api/v1/auth/admin/set-user-password",
83
+ recordIdParam: "userId",
84
+ successMessage: "Password updated",
85
+ refreshAfter: false,
86
+ params: [
87
+ { name: "newPassword", label: "New Password", type: "text", required: true }
88
+ ]
89
+ },
90
+ {
91
+ name: "set_user_role",
92
+ label: "Set Platform Role",
93
+ icon: "shield-check",
94
+ variant: "secondary",
95
+ locations: ["list_item"],
96
+ type: "api",
97
+ target: "/api/v1/auth/admin/set-role",
98
+ recordIdParam: "userId",
99
+ successMessage: "Role updated",
100
+ refreshAfter: true,
101
+ params: [
102
+ { name: "role", label: "Platform Role", type: "text", required: true }
103
+ ]
104
+ },
105
+ {
106
+ name: "impersonate_user",
107
+ label: "Impersonate User",
108
+ icon: "user-cog",
109
+ variant: "secondary",
110
+ locations: ["list_item"],
111
+ type: "api",
112
+ target: "/api/v1/auth/admin/impersonate-user",
113
+ recordIdParam: "userId",
114
+ successMessage: "Now impersonating user",
115
+ refreshAfter: true,
116
+ confirmText: "Start an impersonation session for this user? Use only for legitimate support cases \u2014 actions will be logged."
117
+ }
118
+ ],
119
+ listViews: {
120
+ all_users: {
121
+ type: "grid",
122
+ name: "all_users",
123
+ label: "All Users",
124
+ data: { provider: "object", object: "sys_user" },
125
+ columns: ["name", "email", "email_verified", "two_factor_enabled", "created_at"],
126
+ sort: [{ field: "name", order: "asc" }],
127
+ pagination: { pageSize: 50 }
128
+ },
129
+ unverified: {
130
+ type: "grid",
131
+ name: "unverified",
132
+ label: "Unverified",
133
+ data: { provider: "object", object: "sys_user" },
134
+ columns: ["name", "email", "created_at"],
135
+ filter: [{ field: "email_verified", operator: "equals", value: false }],
136
+ sort: [{ field: "created_at", order: "desc" }],
137
+ pagination: { pageSize: 50 }
138
+ },
139
+ two_factor: {
140
+ type: "grid",
141
+ name: "two_factor",
142
+ label: "2FA Enabled",
143
+ data: { provider: "object", object: "sys_user" },
144
+ columns: ["name", "email", "two_factor_enabled", "updated_at"],
145
+ filter: [{ field: "two_factor_enabled", operator: "equals", value: true }],
146
+ sort: [{ field: "name", order: "asc" }],
147
+ pagination: { pageSize: 50 }
148
+ },
149
+ banned: {
150
+ type: "grid",
151
+ name: "banned",
152
+ label: "Banned",
153
+ data: { provider: "object", object: "sys_user" },
154
+ columns: ["name", "email", "banned", "ban_reason", "ban_expires"],
155
+ filter: [{ field: "banned", operator: "equals", value: true }],
156
+ sort: [{ field: "updated_at", order: "desc" }],
157
+ pagination: { pageSize: 50 }
158
+ }
159
+ },
16
160
  fields: {
17
161
  // ── Identity (primary business fields) ───────────────────────
18
162
  name: data.Field.text({
@@ -39,6 +183,32 @@ var SysUser = data.ObjectSchema.create({
39
183
  group: "Identity",
40
184
  description: "Whether two-factor authentication is enabled for this user. Maintained by the better-auth `twoFactor` plugin."
41
185
  }),
186
+ // ── Admin (managed by better-auth `admin` plugin when enabled) ───
187
+ role: data.Field.text({
188
+ label: "Platform Role",
189
+ required: false,
190
+ maxLength: 64,
191
+ group: "Admin",
192
+ description: "Platform-level role (admin, user, \u2026). Set via the Set Platform Role action."
193
+ }),
194
+ banned: data.Field.boolean({
195
+ label: "Banned",
196
+ defaultValue: false,
197
+ group: "Admin",
198
+ description: "When true, the user cannot sign in. Toggle via Ban User / Unban User actions."
199
+ }),
200
+ ban_reason: data.Field.text({
201
+ label: "Ban Reason",
202
+ required: false,
203
+ maxLength: 255,
204
+ group: "Admin"
205
+ }),
206
+ ban_expires: data.Field.datetime({
207
+ label: "Ban Expires",
208
+ required: false,
209
+ group: "Admin",
210
+ description: "When set, the ban auto-clears at this time."
211
+ }),
42
212
  // ── Profile ──────────────────────────────────────────────────
43
213
  image: data.Field.url({
44
214
  label: "Profile Image",
@@ -94,10 +264,67 @@ var SysSession = data.ObjectSchema.create({
94
264
  pluralLabel: "Sessions",
95
265
  icon: "key",
96
266
  isSystem: true,
267
+ managedBy: "better-auth",
97
268
  description: "Active user sessions",
98
269
  displayNameField: "user_id",
99
270
  titleFormat: "Session \u2014 {user_id}",
100
271
  compactLayout: ["user_id", "ip_address", "expires_at"],
272
+ // Custom actions — sessions are managed by better-auth (generic CRUD
273
+ // suppressed). "Sign out other devices" is the high-value self-service
274
+ // affordance every IdP exposes. Maps to better-auth's
275
+ // `revoke-other-sessions` endpoint which terminates every session for
276
+ // the current user except the one making the request.
277
+ actions: [
278
+ {
279
+ name: "revoke_my_other_sessions",
280
+ label: "Sign out other devices",
281
+ icon: "log-out",
282
+ variant: "danger",
283
+ locations: ["list_toolbar"],
284
+ type: "api",
285
+ target: "/api/v1/auth/revoke-other-sessions",
286
+ confirmText: "Sign out of every other device where you're currently logged in? Your current session will remain active.",
287
+ successMessage: "All other sessions revoked",
288
+ refreshAfter: true
289
+ },
290
+ {
291
+ name: "revoke_session",
292
+ label: "Revoke Session",
293
+ icon: "log-out",
294
+ variant: "danger",
295
+ mode: "delete",
296
+ locations: ["list_item"],
297
+ type: "api",
298
+ target: "/api/v1/auth/revoke-session",
299
+ // better-auth `revoke-session` keys off the session token, not the id.
300
+ recordIdParam: "token",
301
+ recordIdField: "token",
302
+ confirmText: "Revoke this session? The user will be signed out from that device.",
303
+ successMessage: "Session revoked",
304
+ refreshAfter: true
305
+ }
306
+ ],
307
+ listViews: {
308
+ mine: {
309
+ type: "grid",
310
+ name: "mine",
311
+ label: "My Sessions",
312
+ data: { provider: "object", object: "sys_session" },
313
+ columns: ["ip_address", "active_organization_id", "created_at", "expires_at"],
314
+ filter: [{ field: "user_id", operator: "equals", value: "{current_user_id}" }],
315
+ sort: [{ field: "created_at", order: "desc" }],
316
+ pagination: { pageSize: 50 }
317
+ },
318
+ all_sessions: {
319
+ type: "grid",
320
+ name: "all_sessions",
321
+ label: "All",
322
+ data: { provider: "object", object: "sys_session" },
323
+ columns: ["user_id", "ip_address", "active_organization_id", "created_at", "expires_at"],
324
+ sort: [{ field: "created_at", order: "desc" }],
325
+ pagination: { pageSize: 50 }
326
+ }
327
+ },
101
328
  fields: {
102
329
  // ── Session owner & expiry ──────────────────────────────────
103
330
  user_id: data.Field.lookup("sys_user", {
@@ -135,6 +362,13 @@ var SysSession = data.ObjectSchema.create({
135
362
  required: false,
136
363
  group: "Client"
137
364
  }),
365
+ // ── Admin (managed by better-auth `admin` plugin) ────────────
366
+ impersonated_by: data.Field.lookup("sys_user", {
367
+ label: "Impersonated By",
368
+ required: false,
369
+ group: "Admin",
370
+ description: "User id of the admin that started this impersonation session, if any."
371
+ }),
138
372
  // ── Secret (hidden by default) ──────────────────────────────
139
373
  token: data.Field.text({
140
374
  label: "Session Token",
@@ -185,9 +419,41 @@ var SysAccount = data.ObjectSchema.create({
185
419
  pluralLabel: "Accounts",
186
420
  icon: "link",
187
421
  isSystem: true,
422
+ managedBy: "better-auth",
188
423
  description: "OAuth and authentication provider accounts",
189
424
  titleFormat: "{provider_id} - {account_id}",
190
425
  compactLayout: ["provider_id", "user_id", "account_id"],
426
+ listViews: {
427
+ mine: {
428
+ type: "grid",
429
+ name: "mine",
430
+ label: "My Links",
431
+ data: { provider: "object", object: "sys_account" },
432
+ columns: ["provider_id", "account_id", "created_at", "updated_at"],
433
+ filter: [{ field: "user_id", operator: "equals", value: "{current_user_id}" }],
434
+ sort: [{ field: "provider_id", order: "asc" }],
435
+ pagination: { pageSize: 50 }
436
+ },
437
+ by_provider: {
438
+ type: "grid",
439
+ name: "by_provider",
440
+ label: "By Provider",
441
+ data: { provider: "object", object: "sys_account" },
442
+ columns: ["provider_id", "user_id", "account_id", "created_at"],
443
+ sort: [{ field: "provider_id", order: "asc" }, { field: "created_at", order: "desc" }],
444
+ grouping: { fields: [{ field: "provider_id", order: "asc", collapsed: false }] },
445
+ pagination: { pageSize: 100 }
446
+ },
447
+ all_links: {
448
+ type: "grid",
449
+ name: "all_links",
450
+ label: "All",
451
+ data: { provider: "object", object: "sys_account" },
452
+ columns: ["provider_id", "user_id", "account_id", "created_at", "updated_at"],
453
+ sort: [{ field: "created_at", order: "desc" }],
454
+ pagination: { pageSize: 100 }
455
+ }
456
+ },
191
457
  fields: {
192
458
  id: data.Field.text({
193
459
  label: "Account ID",
@@ -268,6 +534,7 @@ var SysVerification = data.ObjectSchema.create({
268
534
  pluralLabel: "Verifications",
269
535
  icon: "shield-check",
270
536
  isSystem: true,
537
+ managedBy: "better-auth",
271
538
  description: "Email and phone verification tokens",
272
539
  titleFormat: "Verification for {identifier}",
273
540
  compactLayout: ["identifier", "expires_at", "created_at"],
@@ -322,10 +589,109 @@ var SysOrganization = data.ObjectSchema.create({
322
589
  pluralLabel: "Organizations",
323
590
  icon: "building-2",
324
591
  isSystem: true,
592
+ managedBy: "better-auth",
325
593
  description: "Organizations for multi-tenant grouping",
326
594
  displayNameField: "name",
327
595
  titleFormat: "{name}",
328
596
  compactLayout: ["name", "slug"],
597
+ // Custom actions — generic CRUD is suppressed (better-auth-managed),
598
+ // but admins still need to create new orgs from the Setup app.
599
+ actions: [
600
+ {
601
+ name: "create_organization",
602
+ label: "Create Organization",
603
+ icon: "plus",
604
+ variant: "primary",
605
+ locations: ["list_toolbar"],
606
+ type: "api",
607
+ target: "/api/v1/auth/organization/create",
608
+ successMessage: "Organization created",
609
+ refreshAfter: true,
610
+ params: [
611
+ { field: "name", required: true },
612
+ { field: "slug", required: true },
613
+ { field: "logo" }
614
+ ]
615
+ },
616
+ {
617
+ name: "update_organization",
618
+ label: "Edit Organization",
619
+ icon: "pencil",
620
+ mode: "edit",
621
+ locations: ["list_item"],
622
+ type: "api",
623
+ target: "/api/v1/auth/organization/update",
624
+ recordIdParam: "organizationId",
625
+ // better-auth `organization/update` nests editable fields under `data`.
626
+ bodyShape: { wrap: "data" },
627
+ successMessage: "Organization updated",
628
+ refreshAfter: true,
629
+ params: [
630
+ { field: "name", required: true, defaultFromRow: true },
631
+ { field: "slug", required: true, defaultFromRow: true },
632
+ { field: "logo", defaultFromRow: true }
633
+ ]
634
+ },
635
+ {
636
+ name: "delete_organization",
637
+ label: "Delete Organization",
638
+ icon: "trash-2",
639
+ variant: "danger",
640
+ mode: "delete",
641
+ locations: ["list_item"],
642
+ type: "api",
643
+ target: "/api/v1/auth/organization/delete",
644
+ recordIdParam: "organizationId",
645
+ confirmText: "Delete this organization? All members will lose access immediately. This cannot be undone.",
646
+ successMessage: "Organization deleted",
647
+ refreshAfter: true
648
+ },
649
+ {
650
+ // Switch the caller's active organization context. Standard
651
+ // better-auth endpoint; no extra params needed (org id ships as
652
+ // the row id). Used from the Setup list and the record header so
653
+ // admins can context-switch without leaving the page.
654
+ name: "set_active_organization",
655
+ label: "Set Active",
656
+ icon: "check-circle-2",
657
+ variant: "secondary",
658
+ mode: "custom",
659
+ locations: ["list_item", "record_header"],
660
+ type: "api",
661
+ target: "/api/v1/auth/organization/set-active",
662
+ recordIdParam: "organizationId",
663
+ successMessage: "Active organization switched",
664
+ refreshAfter: true
665
+ },
666
+ {
667
+ // Current user leaves the org. Distinct from `delete_organization`
668
+ // (admin-only, destroys the org) — `leave` only removes the caller's
669
+ // own membership. Better-auth: `organization/leave { organizationId }`.
670
+ name: "leave_organization",
671
+ label: "Leave Organization",
672
+ icon: "log-out",
673
+ variant: "danger",
674
+ mode: "custom",
675
+ locations: ["list_item", "record_header"],
676
+ type: "api",
677
+ target: "/api/v1/auth/organization/leave",
678
+ recordIdParam: "organizationId",
679
+ confirmText: "Leave this organization? You will lose access to all of its resources.",
680
+ successMessage: "You have left the organization",
681
+ refreshAfter: true
682
+ }
683
+ ],
684
+ listViews: {
685
+ all_orgs: {
686
+ type: "grid",
687
+ name: "all_orgs",
688
+ label: "All",
689
+ data: { provider: "object", object: "sys_organization" },
690
+ columns: ["name", "slug", "created_at", "updated_at"],
691
+ sort: [{ field: "name", order: "asc" }],
692
+ pagination: { pageSize: 50 }
693
+ }
694
+ },
329
695
  fields: {
330
696
  // ── Identity ─────────────────────────────────────────────────
331
697
  name: data.Field.text({
@@ -395,9 +761,67 @@ var SysMember = data.ObjectSchema.create({
395
761
  pluralLabel: "Members",
396
762
  icon: "user-check",
397
763
  isSystem: true,
764
+ managedBy: "better-auth",
398
765
  description: "Organization membership records",
399
766
  titleFormat: "{user_id} in {organization_id}",
400
767
  compactLayout: ["user_id", "organization_id", "role"],
768
+ // Row-level actions: better-auth `organization/update-member-role` and
769
+ // `organization/remove-member`. Generic CRUD is suppressed on better-auth
770
+ // managed tables, so these are the canonical edit/delete entry points.
771
+ // The `add_member` toolbar action covers the admin "attach an existing
772
+ // user directly without sending an invitation" flow.
773
+ actions: [
774
+ {
775
+ // Admin-only: directly attach an existing user to the active org,
776
+ // bypassing the invite-accept flow. Better-auth:
777
+ // `organization/add-member { userId, role, organizationId?, teamId? }`.
778
+ // organizationId/teamId default to the caller's active org/team when
779
+ // omitted, so we leave them as optional params.
780
+ name: "add_member",
781
+ label: "Add Member",
782
+ icon: "user-plus",
783
+ variant: "primary",
784
+ locations: ["list_toolbar"],
785
+ type: "api",
786
+ target: "/api/v1/auth/organization/add-member",
787
+ successMessage: "Member added",
788
+ refreshAfter: true,
789
+ params: [
790
+ { name: "userId", field: "user_id", required: true },
791
+ { field: "role", required: true },
792
+ { name: "organizationId", field: "organization_id" }
793
+ ]
794
+ },
795
+ {
796
+ name: "update_member_role",
797
+ label: "Change Role",
798
+ icon: "shield",
799
+ mode: "edit",
800
+ locations: ["list_item"],
801
+ type: "api",
802
+ target: "/api/v1/auth/organization/update-member-role",
803
+ recordIdParam: "memberId",
804
+ successMessage: "Member role updated",
805
+ refreshAfter: true,
806
+ params: [
807
+ { field: "role", required: true, defaultFromRow: true }
808
+ ]
809
+ },
810
+ {
811
+ name: "remove_member",
812
+ label: "Remove Member",
813
+ icon: "user-minus",
814
+ variant: "danger",
815
+ mode: "delete",
816
+ locations: ["list_item"],
817
+ type: "api",
818
+ target: "/api/v1/auth/organization/remove-member",
819
+ recordIdParam: "memberIdOrEmail",
820
+ confirmText: "Remove this member from the organization? They will lose access to all org resources.",
821
+ successMessage: "Member removed",
822
+ refreshAfter: true
823
+ }
824
+ ],
401
825
  fields: {
402
826
  id: data.Field.text({
403
827
  label: "Member ID",
@@ -417,11 +841,16 @@ var SysMember = data.ObjectSchema.create({
417
841
  label: "User",
418
842
  required: true
419
843
  }),
420
- role: data.Field.text({
844
+ role: data.Field.select({
421
845
  label: "Role",
422
846
  required: false,
423
- description: "Member role within the organization (e.g. admin, member)",
424
- maxLength: 100
847
+ description: "Member role within the organization",
848
+ options: [
849
+ { label: "Owner", value: "owner" },
850
+ { label: "Admin", value: "admin" },
851
+ { label: "Member", value: "member" }
852
+ ],
853
+ defaultValue: "member"
425
854
  })
426
855
  },
427
856
  indexes: [
@@ -443,9 +872,101 @@ var SysInvitation = data.ObjectSchema.create({
443
872
  pluralLabel: "Invitations",
444
873
  icon: "mail",
445
874
  isSystem: true,
875
+ managedBy: "better-auth",
446
876
  description: "Organization invitations for user onboarding",
447
877
  titleFormat: "Invitation to {organization_id}",
448
878
  compactLayout: ["email", "organization_id", "status"],
879
+ // Custom actions — generic CRUD is suppressed (better-auth-managed).
880
+ // Mirror the `invite_user` toolbar action from sys_user here so admins
881
+ // landing on the Invitations page get an obvious entry point.
882
+ actions: [
883
+ {
884
+ name: "invite_user",
885
+ label: "Invite User",
886
+ icon: "user-plus",
887
+ variant: "primary",
888
+ locations: ["list_toolbar"],
889
+ type: "api",
890
+ target: "/api/v1/auth/organization/invite-member",
891
+ successMessage: "Invitation sent",
892
+ refreshAfter: true,
893
+ params: [
894
+ { field: "email", required: true },
895
+ { field: "role", required: true }
896
+ ]
897
+ },
898
+ {
899
+ name: "cancel_invitation",
900
+ label: "Cancel Invitation",
901
+ icon: "x-circle",
902
+ variant: "danger",
903
+ mode: "delete",
904
+ locations: ["list_item"],
905
+ type: "api",
906
+ target: "/api/v1/auth/organization/cancel-invitation",
907
+ recordIdParam: "invitationId",
908
+ confirmText: "Cancel this invitation? The recipient will no longer be able to accept it.",
909
+ successMessage: "Invitation canceled",
910
+ refreshAfter: true
911
+ },
912
+ {
913
+ name: "resend_invitation",
914
+ label: "Resend Invitation",
915
+ icon: "send",
916
+ variant: "secondary",
917
+ locations: ["list_item"],
918
+ type: "api",
919
+ target: "/api/v1/auth/organization/invite-member",
920
+ bodyExtra: { resend: true },
921
+ successMessage: "Invitation resent",
922
+ refreshAfter: true,
923
+ params: [
924
+ { field: "email", required: true, defaultFromRow: true },
925
+ { field: "role", required: true, defaultFromRow: true }
926
+ ]
927
+ }
928
+ ],
929
+ listViews: {
930
+ pending: {
931
+ type: "grid",
932
+ name: "pending",
933
+ label: "Pending",
934
+ data: { provider: "object", object: "sys_invitation" },
935
+ columns: ["email", "role", "organization_id", "inviter_id", "expires_at"],
936
+ filter: [{ field: "status", operator: "equals", value: "pending" }],
937
+ sort: [{ field: "expires_at", order: "asc" }],
938
+ pagination: { pageSize: 50 }
939
+ },
940
+ accepted: {
941
+ type: "grid",
942
+ name: "accepted",
943
+ label: "Accepted",
944
+ data: { provider: "object", object: "sys_invitation" },
945
+ columns: ["email", "role", "organization_id", "inviter_id", "created_at"],
946
+ filter: [{ field: "status", operator: "equals", value: "accepted" }],
947
+ sort: [{ field: "created_at", order: "desc" }],
948
+ pagination: { pageSize: 50 }
949
+ },
950
+ expired: {
951
+ type: "grid",
952
+ name: "expired",
953
+ label: "Expired / Canceled",
954
+ data: { provider: "object", object: "sys_invitation" },
955
+ columns: ["email", "status", "organization_id", "expires_at"],
956
+ filter: [{ field: "status", operator: "in", value: ["expired", "rejected", "canceled"] }],
957
+ sort: [{ field: "expires_at", order: "desc" }],
958
+ pagination: { pageSize: 50 }
959
+ },
960
+ all_invitations: {
961
+ type: "grid",
962
+ name: "all_invitations",
963
+ label: "All",
964
+ data: { provider: "object", object: "sys_invitation" },
965
+ columns: ["email", "status", "role", "organization_id", "inviter_id", "created_at"],
966
+ sort: [{ field: "created_at", order: "desc" }],
967
+ pagination: { pageSize: 50 }
968
+ }
969
+ },
449
970
  fields: {
450
971
  id: data.Field.text({
451
972
  label: "Invitation ID",
@@ -466,11 +987,16 @@ var SysInvitation = data.ObjectSchema.create({
466
987
  required: true,
467
988
  description: "Email address of the invited user"
468
989
  }),
469
- role: data.Field.text({
990
+ role: data.Field.select({
470
991
  label: "Role",
471
992
  required: false,
472
- maxLength: 100,
473
- description: "Role to assign upon acceptance"
993
+ description: "Role to assign upon acceptance",
994
+ options: [
995
+ { label: "Owner", value: "owner" },
996
+ { label: "Admin", value: "admin" },
997
+ { label: "Member", value: "member" }
998
+ ],
999
+ defaultValue: "member"
474
1000
  }),
475
1001
  status: data.Field.select(["pending", "accepted", "rejected", "expired", "canceled"], {
476
1002
  label: "Status",
@@ -512,10 +1038,89 @@ var SysTeam = data.ObjectSchema.create({
512
1038
  pluralLabel: "Teams",
513
1039
  icon: "users",
514
1040
  isSystem: true,
1041
+ managedBy: "better-auth",
515
1042
  description: "Teams within organizations for fine-grained grouping",
516
1043
  displayNameField: "name",
517
1044
  titleFormat: "{name}",
518
1045
  compactLayout: ["name", "organization_id"],
1046
+ // Custom actions calling better-auth's team endpoints. Generic CRUD is
1047
+ // suppressed (managedBy: 'better-auth'), so these are the canonical
1048
+ // entry points for create/update/delete.
1049
+ actions: [
1050
+ {
1051
+ // Better-auth: `organization/create-team { name, organizationId? }`.
1052
+ // organizationId defaults to the caller's active org when omitted.
1053
+ name: "create_team",
1054
+ label: "Create Team",
1055
+ icon: "plus",
1056
+ variant: "primary",
1057
+ locations: ["list_toolbar"],
1058
+ type: "api",
1059
+ target: "/api/v1/auth/organization/create-team",
1060
+ successMessage: "Team created",
1061
+ refreshAfter: true,
1062
+ params: [
1063
+ { field: "name", required: true },
1064
+ { name: "organizationId", field: "organization_id" }
1065
+ ]
1066
+ },
1067
+ {
1068
+ // Better-auth: `organization/update-team { teamId, data: { name } }`.
1069
+ // teamId stays flat (top-level); the user-editable params nest under
1070
+ // `data` via bodyShape.
1071
+ name: "update_team",
1072
+ label: "Edit Team",
1073
+ icon: "pencil",
1074
+ mode: "edit",
1075
+ locations: ["list_item"],
1076
+ type: "api",
1077
+ target: "/api/v1/auth/organization/update-team",
1078
+ recordIdParam: "teamId",
1079
+ bodyShape: { wrap: "data" },
1080
+ successMessage: "Team updated",
1081
+ refreshAfter: true,
1082
+ params: [
1083
+ { field: "name", required: true, defaultFromRow: true }
1084
+ ]
1085
+ },
1086
+ {
1087
+ // Better-auth: `organization/remove-team { teamId, organizationId? }`.
1088
+ // organizationId defaults to the caller's active org when omitted.
1089
+ name: "remove_team",
1090
+ label: "Delete Team",
1091
+ icon: "trash-2",
1092
+ variant: "danger",
1093
+ mode: "delete",
1094
+ locations: ["list_item"],
1095
+ type: "api",
1096
+ target: "/api/v1/auth/organization/remove-team",
1097
+ recordIdParam: "teamId",
1098
+ confirmText: "Delete this team? Members will lose any team-scoped access. This cannot be undone.",
1099
+ successMessage: "Team deleted",
1100
+ refreshAfter: true
1101
+ }
1102
+ ],
1103
+ listViews: {
1104
+ by_org: {
1105
+ type: "grid",
1106
+ name: "by_org",
1107
+ label: "By Organization",
1108
+ data: { provider: "object", object: "sys_team" },
1109
+ columns: ["organization_id", "name", "created_at", "updated_at"],
1110
+ sort: [{ field: "organization_id", order: "asc" }, { field: "name", order: "asc" }],
1111
+ grouping: { fields: [{ field: "organization_id", order: "asc", collapsed: false }] },
1112
+ pagination: { pageSize: 100 }
1113
+ },
1114
+ all_teams: {
1115
+ type: "grid",
1116
+ name: "all_teams",
1117
+ label: "All",
1118
+ data: { provider: "object", object: "sys_team" },
1119
+ columns: ["name", "organization_id", "created_at", "updated_at"],
1120
+ sort: [{ field: "name", order: "asc" }],
1121
+ pagination: { pageSize: 50 }
1122
+ }
1123
+ },
519
1124
  fields: {
520
1125
  // ── Identity ─────────────────────────────────────────────────
521
1126
  name: data.Field.text({
@@ -570,9 +1175,52 @@ var SysTeamMember = data.ObjectSchema.create({
570
1175
  pluralLabel: "Team Members",
571
1176
  icon: "user-plus",
572
1177
  isSystem: true,
1178
+ managedBy: "better-auth",
573
1179
  description: "Team membership records linking users to teams",
574
1180
  titleFormat: "{user_id} in {team_id}",
575
1181
  compactLayout: ["user_id", "team_id", "created_at"],
1182
+ // Custom actions calling better-auth's team-member endpoints. Generic
1183
+ // CRUD is suppressed (managedBy: 'better-auth') so these are the
1184
+ // canonical add/remove entry points.
1185
+ actions: [
1186
+ {
1187
+ // Better-auth: `organization/add-team-member { teamId, userId }`.
1188
+ name: "add_team_member",
1189
+ label: "Add Member",
1190
+ icon: "user-plus",
1191
+ variant: "primary",
1192
+ locations: ["list_toolbar"],
1193
+ type: "api",
1194
+ target: "/api/v1/auth/organization/add-team-member",
1195
+ successMessage: "Team member added",
1196
+ refreshAfter: true,
1197
+ params: [
1198
+ { name: "teamId", field: "team_id", required: true },
1199
+ { name: "userId", field: "user_id", required: true }
1200
+ ]
1201
+ },
1202
+ {
1203
+ // Better-auth: `organization/remove-team-member { teamId, userId }`.
1204
+ // The endpoint identifies the membership by the (teamId, userId)
1205
+ // pair rather than the join-row id, so we pull both from the row
1206
+ // via `defaultFromRow` instead of using `recordIdParam`.
1207
+ name: "remove_team_member",
1208
+ label: "Remove from Team",
1209
+ icon: "user-minus",
1210
+ variant: "danger",
1211
+ mode: "delete",
1212
+ locations: ["list_item"],
1213
+ type: "api",
1214
+ target: "/api/v1/auth/organization/remove-team-member",
1215
+ confirmText: "Remove this user from the team? They will lose any team-scoped access.",
1216
+ successMessage: "Team member removed",
1217
+ refreshAfter: true,
1218
+ params: [
1219
+ { name: "teamId", field: "team_id", required: true, defaultFromRow: true },
1220
+ { name: "userId", field: "user_id", required: true, defaultFromRow: true }
1221
+ ]
1222
+ }
1223
+ ],
576
1224
  fields: {
577
1225
  id: data.Field.text({
578
1226
  label: "Team Member ID",
@@ -606,16 +1254,338 @@ var SysTeamMember = data.ObjectSchema.create({
606
1254
  mru: false
607
1255
  }
608
1256
  });
1257
+ var SysDepartment = data.ObjectSchema.create({
1258
+ name: "sys_department",
1259
+ label: "Department",
1260
+ pluralLabel: "Departments",
1261
+ icon: "building",
1262
+ isSystem: true,
1263
+ managedBy: "platform",
1264
+ description: "Hierarchical org-skeleton node (department / division / business unit / office).",
1265
+ displayNameField: "name",
1266
+ titleFormat: "{name}",
1267
+ compactLayout: ["name", "kind", "parent_department_id", "manager_user_id"],
1268
+ listViews: {
1269
+ active: {
1270
+ type: "grid",
1271
+ name: "active",
1272
+ label: "Active",
1273
+ data: { provider: "object", object: "sys_department" },
1274
+ columns: ["name", "code", "kind", "parent_department_id", "manager_user_id", "effective_from"],
1275
+ filter: [{ field: "active", operator: "equals", value: true }],
1276
+ sort: [{ field: "name", order: "asc" }],
1277
+ pagination: { pageSize: 100 }
1278
+ },
1279
+ inactive: {
1280
+ type: "grid",
1281
+ name: "inactive",
1282
+ label: "Inactive",
1283
+ data: { provider: "object", object: "sys_department" },
1284
+ columns: ["name", "code", "kind", "effective_to"],
1285
+ filter: [{ field: "active", operator: "equals", value: false }],
1286
+ sort: [{ field: "effective_to", order: "desc" }],
1287
+ pagination: { pageSize: 50 }
1288
+ },
1289
+ by_kind: {
1290
+ type: "grid",
1291
+ name: "by_kind",
1292
+ label: "By Kind",
1293
+ data: { provider: "object", object: "sys_department" },
1294
+ columns: ["kind", "name", "code", "parent_department_id", "manager_user_id", "active"],
1295
+ sort: [{ field: "kind", order: "asc" }, { field: "name", order: "asc" }],
1296
+ grouping: { fields: [{ field: "kind", order: "asc", collapsed: false }] },
1297
+ pagination: { pageSize: 100 }
1298
+ },
1299
+ all_departments: {
1300
+ type: "grid",
1301
+ name: "all_departments",
1302
+ label: "All",
1303
+ data: { provider: "object", object: "sys_department" },
1304
+ columns: ["name", "code", "kind", "parent_department_id", "manager_user_id", "active"],
1305
+ sort: [{ field: "name", order: "asc" }],
1306
+ pagination: { pageSize: 100 }
1307
+ }
1308
+ },
1309
+ fields: {
1310
+ // ── Identity ─────────────────────────────────────────────────
1311
+ name: data.Field.text({
1312
+ label: "Name",
1313
+ required: true,
1314
+ searchable: true,
1315
+ maxLength: 255,
1316
+ group: "Identity"
1317
+ }),
1318
+ code: data.Field.text({
1319
+ label: "Code",
1320
+ required: false,
1321
+ searchable: true,
1322
+ maxLength: 64,
1323
+ description: "Short stable code (e.g. EMEA-SALES). Unique within tenant.",
1324
+ group: "Identity"
1325
+ }),
1326
+ kind: data.Field.select(
1327
+ ["company", "division", "department", "team", "office", "cost_center"],
1328
+ {
1329
+ label: "Kind",
1330
+ required: true,
1331
+ defaultValue: "department",
1332
+ description: "Categorisation hint \u2014 does not change graph semantics.",
1333
+ group: "Identity"
1334
+ }
1335
+ ),
1336
+ // ── Hierarchy ────────────────────────────────────────────────
1337
+ parent_department_id: data.Field.lookup("sys_department", {
1338
+ label: "Parent Department",
1339
+ required: false,
1340
+ description: "Self-reference for the org tree. Null = root of tenant.",
1341
+ group: "Hierarchy"
1342
+ }),
1343
+ organization_id: data.Field.lookup("sys_organization", {
1344
+ label: "Organization",
1345
+ required: true,
1346
+ description: "Tenant scope.",
1347
+ group: "Hierarchy"
1348
+ }),
1349
+ // ── Leadership ───────────────────────────────────────────────
1350
+ manager_user_id: data.Field.lookup("sys_user", {
1351
+ label: "Department Head",
1352
+ required: false,
1353
+ description: "User responsible for this org unit (department head / lead).",
1354
+ group: "Leadership"
1355
+ }),
1356
+ // ── Lifecycle ────────────────────────────────────────────────
1357
+ active: data.Field.boolean({
1358
+ label: "Active",
1359
+ required: false,
1360
+ defaultValue: true,
1361
+ description: "When false, members are not expanded by graph queries.",
1362
+ group: "Lifecycle"
1363
+ }),
1364
+ effective_from: data.Field.datetime({
1365
+ label: "Effective From",
1366
+ required: false,
1367
+ description: "When this department came into existence (HRIS sync).",
1368
+ group: "Lifecycle"
1369
+ }),
1370
+ effective_to: data.Field.datetime({
1371
+ label: "Effective To",
1372
+ required: false,
1373
+ description: "When this department was retired (HRIS sync).",
1374
+ group: "Lifecycle"
1375
+ }),
1376
+ external_ref: data.Field.text({
1377
+ label: "External Reference",
1378
+ required: false,
1379
+ maxLength: 200,
1380
+ description: "ID in upstream HRIS (Workday / SAP HR / \u5317\u68EE).",
1381
+ group: "Lifecycle"
1382
+ }),
1383
+ // ── System ───────────────────────────────────────────────────
1384
+ id: data.Field.text({
1385
+ label: "Department ID",
1386
+ required: true,
1387
+ readonly: true,
1388
+ group: "System"
1389
+ }),
1390
+ created_at: data.Field.datetime({
1391
+ label: "Created At",
1392
+ defaultValue: "NOW()",
1393
+ readonly: true,
1394
+ group: "System"
1395
+ }),
1396
+ updated_at: data.Field.datetime({
1397
+ label: "Updated At",
1398
+ defaultValue: "NOW()",
1399
+ readonly: true,
1400
+ group: "System"
1401
+ })
1402
+ },
1403
+ indexes: [
1404
+ { fields: ["organization_id"] },
1405
+ { fields: ["parent_department_id"] },
1406
+ { fields: ["code", "organization_id"], unique: true },
1407
+ { fields: ["active"] }
1408
+ ],
1409
+ enable: {
1410
+ trackHistory: true,
1411
+ searchable: true,
1412
+ apiEnabled: true,
1413
+ apiMethods: ["get", "list", "create", "update", "delete"],
1414
+ trash: true,
1415
+ mru: false
1416
+ }
1417
+ });
1418
+ var SysDepartmentMember = data.ObjectSchema.create({
1419
+ name: "sys_department_member",
1420
+ label: "Department Member",
1421
+ pluralLabel: "Department Members",
1422
+ icon: "user-cog",
1423
+ isSystem: true,
1424
+ managedBy: "platform",
1425
+ description: "User assignment to a department (matrix-org friendly, effective-dated).",
1426
+ titleFormat: "{user_id} in {department_id}",
1427
+ compactLayout: ["user_id", "department_id", "role_in_department", "is_primary"],
1428
+ fields: {
1429
+ id: data.Field.text({
1430
+ label: "Member ID",
1431
+ required: true,
1432
+ readonly: true,
1433
+ group: "System"
1434
+ }),
1435
+ department_id: data.Field.lookup("sys_department", {
1436
+ label: "Department",
1437
+ required: true,
1438
+ group: "Assignment"
1439
+ }),
1440
+ user_id: data.Field.lookup("sys_user", {
1441
+ label: "User",
1442
+ required: true,
1443
+ group: "Assignment"
1444
+ }),
1445
+ role_in_department: data.Field.select(
1446
+ ["member", "lead", "deputy"],
1447
+ {
1448
+ label: "Role in Department",
1449
+ required: false,
1450
+ defaultValue: "member",
1451
+ description: "`lead` is the day-to-day head; `deputy` may stand in for the lead in approval routing.",
1452
+ group: "Assignment"
1453
+ }
1454
+ ),
1455
+ is_primary: data.Field.boolean({
1456
+ label: "Primary Assignment",
1457
+ required: false,
1458
+ defaultValue: true,
1459
+ description: "When the user is in multiple departments, this marks the canonical one for reporting.",
1460
+ group: "Assignment"
1461
+ }),
1462
+ effective_from: data.Field.datetime({
1463
+ label: "Effective From",
1464
+ required: false,
1465
+ group: "Lifecycle"
1466
+ }),
1467
+ effective_to: data.Field.datetime({
1468
+ label: "Effective To",
1469
+ required: false,
1470
+ group: "Lifecycle"
1471
+ }),
1472
+ created_at: data.Field.datetime({
1473
+ label: "Created At",
1474
+ defaultValue: "NOW()",
1475
+ readonly: true,
1476
+ group: "System"
1477
+ }),
1478
+ updated_at: data.Field.datetime({
1479
+ label: "Updated At",
1480
+ defaultValue: "NOW()",
1481
+ readonly: true,
1482
+ group: "System"
1483
+ })
1484
+ },
1485
+ indexes: [
1486
+ { fields: ["department_id", "user_id"], unique: true },
1487
+ { fields: ["user_id"] },
1488
+ { fields: ["is_primary"] }
1489
+ ],
1490
+ enable: {
1491
+ trackHistory: true,
1492
+ searchable: true,
1493
+ apiEnabled: true,
1494
+ apiMethods: ["get", "list", "create", "update", "delete"],
1495
+ trash: true,
1496
+ mru: false
1497
+ }
1498
+ });
609
1499
  var SysApiKey = data.ObjectSchema.create({
610
1500
  name: "sys_api_key",
611
1501
  label: "API Key",
612
1502
  pluralLabel: "API Keys",
613
1503
  icon: "key-round",
614
1504
  isSystem: true,
1505
+ managedBy: "better-auth",
615
1506
  description: "API keys for programmatic access",
616
1507
  displayNameField: "name",
617
1508
  titleFormat: "{name}",
618
1509
  compactLayout: ["name", "prefix", "user_id", "expires_at", "revoked"],
1510
+ // Custom actions — sys_api_key is managed-by 'better-auth' but the
1511
+ // `revoked` boolean is a column we control via the data API. These row
1512
+ // actions use the generic PATCH /api/v1/sys_api_key/{id} endpoint with
1513
+ // `bodyExtra` to set the `revoked` flag explicitly.
1514
+ actions: [
1515
+ {
1516
+ name: "revoke_api_key",
1517
+ label: "Revoke API Key",
1518
+ icon: "shield-off",
1519
+ variant: "danger",
1520
+ mode: "custom",
1521
+ locations: ["list_item"],
1522
+ type: "api",
1523
+ method: "PATCH",
1524
+ target: "/api/v1/data/sys_api_key/{id}",
1525
+ bodyExtra: { revoked: true },
1526
+ confirmText: "Revoke this API key? Any clients using it will immediately lose access.",
1527
+ successMessage: "API key revoked",
1528
+ refreshAfter: true
1529
+ },
1530
+ {
1531
+ name: "restore_api_key",
1532
+ label: "Restore API Key",
1533
+ icon: "shield-check",
1534
+ variant: "secondary",
1535
+ mode: "custom",
1536
+ locations: ["list_item"],
1537
+ type: "api",
1538
+ method: "PATCH",
1539
+ target: "/api/v1/data/sys_api_key/{id}",
1540
+ bodyExtra: { revoked: false },
1541
+ confirmText: "Restore this revoked API key? Existing clients holding the key will regain access.",
1542
+ successMessage: "API key restored",
1543
+ refreshAfter: true
1544
+ }
1545
+ ],
1546
+ listViews: {
1547
+ mine: {
1548
+ type: "grid",
1549
+ name: "mine",
1550
+ label: "My Keys",
1551
+ data: { provider: "object", object: "sys_api_key" },
1552
+ columns: ["name", "prefix", "expires_at", "last_used_at", "revoked"],
1553
+ filter: [
1554
+ { field: "user_id", operator: "equals", value: "{current_user_id}" }
1555
+ ],
1556
+ sort: [{ field: "created_at", order: "desc" }],
1557
+ pagination: { pageSize: 50 }
1558
+ },
1559
+ active: {
1560
+ type: "grid",
1561
+ name: "active",
1562
+ label: "Active",
1563
+ data: { provider: "object", object: "sys_api_key" },
1564
+ columns: ["name", "prefix", "user_id", "expires_at", "last_used_at"],
1565
+ filter: [{ field: "revoked", operator: "equals", value: false }],
1566
+ sort: [{ field: "last_used_at", order: "desc" }],
1567
+ pagination: { pageSize: 50 }
1568
+ },
1569
+ revoked: {
1570
+ type: "grid",
1571
+ name: "revoked",
1572
+ label: "Revoked",
1573
+ data: { provider: "object", object: "sys_api_key" },
1574
+ columns: ["name", "prefix", "user_id", "expires_at", "updated_at"],
1575
+ filter: [{ field: "revoked", operator: "equals", value: true }],
1576
+ sort: [{ field: "updated_at", order: "desc" }],
1577
+ pagination: { pageSize: 50 }
1578
+ },
1579
+ all_keys: {
1580
+ type: "grid",
1581
+ name: "all_keys",
1582
+ label: "All",
1583
+ data: { provider: "object", object: "sys_api_key" },
1584
+ columns: ["name", "prefix", "user_id", "expires_at", "last_used_at", "revoked"],
1585
+ sort: [{ field: "created_at", order: "desc" }],
1586
+ pagination: { pageSize: 50 }
1587
+ }
1588
+ },
619
1589
  fields: {
620
1590
  // ── Identity ─────────────────────────────────────────────────
621
1591
  name: data.Field.text({
@@ -714,9 +1684,66 @@ var SysTwoFactor = data.ObjectSchema.create({
714
1684
  pluralLabel: "Two Factor Credentials",
715
1685
  icon: "smartphone",
716
1686
  isSystem: true,
1687
+ managedBy: "better-auth",
717
1688
  description: "Two-factor authentication credentials",
718
1689
  titleFormat: "Two-factor for {user_id}",
719
1690
  compactLayout: ["user_id", "created_at"],
1691
+ listViews: {
1692
+ mine: {
1693
+ type: "grid",
1694
+ name: "mine",
1695
+ label: "My Enrollment",
1696
+ data: { provider: "object", object: "sys_two_factor" },
1697
+ columns: ["created_at", "updated_at"],
1698
+ filter: [{ field: "user_id", operator: "equals", value: "{current_user_id}" }],
1699
+ sort: [{ field: "created_at", order: "desc" }],
1700
+ pagination: { pageSize: 50 }
1701
+ },
1702
+ all_enrollments: {
1703
+ type: "grid",
1704
+ name: "all_enrollments",
1705
+ label: "All",
1706
+ data: { provider: "object", object: "sys_two_factor" },
1707
+ columns: ["user_id", "created_at", "updated_at"],
1708
+ sort: [{ field: "created_at", order: "desc" }],
1709
+ pagination: { pageSize: 50 }
1710
+ }
1711
+ },
1712
+ // Toolbar actions for self-service 2FA enrollment. The actual TOTP secret
1713
+ // and backup codes returned by better-auth must be shown in the response
1714
+ // toast / dialog — the action runner surfaces successMessage; the raw
1715
+ // payload is logged client-side for now (TODO: dedicated 2FA setup wizard).
1716
+ actions: [
1717
+ {
1718
+ name: "enable_two_factor",
1719
+ label: "Enable 2FA",
1720
+ icon: "shield-check",
1721
+ variant: "primary",
1722
+ locations: ["list_toolbar"],
1723
+ type: "api",
1724
+ target: "/api/v1/auth/two-factor/enable",
1725
+ successMessage: "2FA enrollment started \u2014 check response for TOTP URI and backup codes",
1726
+ refreshAfter: true,
1727
+ params: [
1728
+ { name: "password", label: "Current Password", type: "text", required: true }
1729
+ ]
1730
+ },
1731
+ {
1732
+ name: "disable_two_factor",
1733
+ label: "Disable 2FA",
1734
+ icon: "shield-off",
1735
+ variant: "danger",
1736
+ locations: ["list_toolbar"],
1737
+ type: "api",
1738
+ target: "/api/v1/auth/two-factor/disable",
1739
+ confirmText: "Disable two-factor authentication on your account?",
1740
+ successMessage: "2FA disabled",
1741
+ refreshAfter: true,
1742
+ params: [
1743
+ { name: "password", label: "Current Password", type: "text", required: true }
1744
+ ]
1745
+ }
1746
+ ],
720
1747
  fields: {
721
1748
  id: data.Field.text({
722
1749
  label: "Two Factor ID",
@@ -766,6 +1793,7 @@ var SysDeviceCode = data.ObjectSchema.create({
766
1793
  pluralLabel: "Device Codes",
767
1794
  icon: "key-round",
768
1795
  isSystem: true,
1796
+ managedBy: "better-auth",
769
1797
  description: "OAuth 2.0 Device Authorization Grant (RFC 8628) pending requests",
770
1798
  titleFormat: "{user_code}",
771
1799
  compactLayout: ["user_code", "status", "client_id", "expires_at"],
@@ -855,9 +1883,44 @@ var SysUserPreference = data.ObjectSchema.create({
855
1883
  pluralLabel: "User Preferences",
856
1884
  icon: "settings",
857
1885
  isSystem: true,
1886
+ // managedBy: 'system' — preferences are per-user state authored from
1887
+ // the user's own settings page, never created by an admin. The list
1888
+ // surface in Setup is a support/diagnostic view only.
1889
+ managedBy: "system",
858
1890
  description: "Per-user key-value preferences (theme, locale, etc.)",
859
1891
  titleFormat: "{key}",
860
1892
  compactLayout: ["user_id", "key"],
1893
+ listViews: {
1894
+ mine: {
1895
+ type: "grid",
1896
+ name: "mine",
1897
+ label: "My Preferences",
1898
+ data: { provider: "object", object: "sys_user_preference" },
1899
+ columns: ["key", "updated_at"],
1900
+ filter: [{ field: "user_id", operator: "equals", value: "{current_user_id}" }],
1901
+ sort: [{ field: "key", order: "asc" }],
1902
+ pagination: { pageSize: 100 }
1903
+ },
1904
+ by_user: {
1905
+ type: "grid",
1906
+ name: "by_user",
1907
+ label: "By User",
1908
+ data: { provider: "object", object: "sys_user_preference" },
1909
+ columns: ["user_id", "key", "updated_at"],
1910
+ sort: [{ field: "user_id", order: "asc" }, { field: "key", order: "asc" }],
1911
+ grouping: { fields: [{ field: "user_id", order: "asc", collapsed: true }] },
1912
+ pagination: { pageSize: 200 }
1913
+ },
1914
+ all_preferences: {
1915
+ type: "grid",
1916
+ name: "all_preferences",
1917
+ label: "All",
1918
+ data: { provider: "object", object: "sys_user_preference" },
1919
+ columns: ["user_id", "key", "created_at", "updated_at"],
1920
+ sort: [{ field: "updated_at", order: "desc" }],
1921
+ pagination: { pageSize: 100 }
1922
+ }
1923
+ },
861
1924
  fields: {
862
1925
  id: data.Field.text({
863
1926
  label: "Preference ID",
@@ -909,10 +1972,42 @@ var SysOauthApplication = data.ObjectSchema.create({
909
1972
  pluralLabel: "OAuth Applications",
910
1973
  icon: "key-round",
911
1974
  isSystem: true,
1975
+ managedBy: "better-auth",
912
1976
  description: "Registered OAuth/OIDC client applications",
913
1977
  displayNameField: "name",
914
1978
  titleFormat: "{name}",
915
1979
  compactLayout: ["name", "client_id", "type", "disabled"],
1980
+ listViews: {
1981
+ active: {
1982
+ type: "grid",
1983
+ name: "active",
1984
+ label: "Active",
1985
+ data: { provider: "object", object: "sys_oauth_application" },
1986
+ columns: ["name", "client_id", "type", "updated_at"],
1987
+ filter: [{ field: "disabled", operator: "equals", value: false }],
1988
+ sort: [{ field: "name", order: "asc" }],
1989
+ pagination: { pageSize: 50 }
1990
+ },
1991
+ disabled_apps: {
1992
+ type: "grid",
1993
+ name: "disabled_apps",
1994
+ label: "Disabled",
1995
+ data: { provider: "object", object: "sys_oauth_application" },
1996
+ columns: ["name", "client_id", "type", "updated_at"],
1997
+ filter: [{ field: "disabled", operator: "equals", value: true }],
1998
+ sort: [{ field: "updated_at", order: "desc" }],
1999
+ pagination: { pageSize: 50 }
2000
+ },
2001
+ all_apps: {
2002
+ type: "grid",
2003
+ name: "all_apps",
2004
+ label: "All",
2005
+ data: { provider: "object", object: "sys_oauth_application" },
2006
+ columns: ["name", "client_id", "type", "disabled", "created_at"],
2007
+ sort: [{ field: "name", order: "asc" }],
2008
+ pagination: { pageSize: 50 }
2009
+ }
2010
+ },
916
2011
  fields: {
917
2012
  // ── Identity ─────────────────────────────────────────────────
918
2013
  id: data.Field.text({
@@ -1124,6 +2219,7 @@ var SysOauthAccessToken = data.ObjectSchema.create({
1124
2219
  pluralLabel: "OAuth Access Tokens",
1125
2220
  icon: "ticket",
1126
2221
  isSystem: true,
2222
+ managedBy: "better-auth",
1127
2223
  description: "Opaque OAuth access tokens issued to client applications",
1128
2224
  compactLayout: ["client_id", "user_id", "expires_at"],
1129
2225
  fields: {
@@ -1201,6 +2297,7 @@ var SysOauthRefreshToken = data.ObjectSchema.create({
1201
2297
  pluralLabel: "OAuth Refresh Tokens",
1202
2298
  icon: "refresh-cw",
1203
2299
  isSystem: true,
2300
+ managedBy: "better-auth",
1204
2301
  description: "Opaque OAuth refresh tokens (linked to a session)",
1205
2302
  compactLayout: ["client_id", "user_id", "expires_at"],
1206
2303
  fields: {
@@ -1282,6 +2379,7 @@ var SysOauthConsent = data.ObjectSchema.create({
1282
2379
  pluralLabel: "OAuth Consents",
1283
2380
  icon: "shield-check",
1284
2381
  isSystem: true,
2382
+ managedBy: "better-auth",
1285
2383
  description: "User consent records for OAuth client applications",
1286
2384
  compactLayout: ["client_id", "user_id", "scopes"],
1287
2385
  fields: {
@@ -1341,6 +2439,7 @@ var SysJwks = data.ObjectSchema.create({
1341
2439
  pluralLabel: "JWKS Keys",
1342
2440
  icon: "key",
1343
2441
  isSystem: true,
2442
+ managedBy: "better-auth",
1344
2443
  description: "Asymmetric key pairs used to sign and verify issued JWTs",
1345
2444
  compactLayout: ["id", "created_at", "expires_at"],
1346
2445
  fields: {
@@ -1384,6 +2483,8 @@ var SysJwks = data.ObjectSchema.create({
1384
2483
 
1385
2484
  exports.SysAccount = SysAccount;
1386
2485
  exports.SysApiKey = SysApiKey;
2486
+ exports.SysDepartment = SysDepartment;
2487
+ exports.SysDepartmentMember = SysDepartmentMember;
1387
2488
  exports.SysDeviceCode = SysDeviceCode;
1388
2489
  exports.SysInvitation = SysInvitation;
1389
2490
  exports.SysJwks = SysJwks;