@objectstack/platform-objects 10.3.0 → 11.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.
@@ -33,6 +33,13 @@ var SysUser = ObjectSchema.create({
33
33
  locations: ["list_toolbar"],
34
34
  type: "api",
35
35
  target: "/api/v1/auth/organization/invite-member",
36
+ // Org invitations are a multi-org-only flow (the endpoint resolves
37
+ // an active org that does not exist in single-org mode). Hide the
38
+ // affordance unless multi-org is enabled — matching the
39
+ // `create_organization` gate on sys_organization. This action is the
40
+ // most exposed of the set because the Users list is always reachable
41
+ // in single-org, unlike the org/membership lists.
42
+ visible: "features.multiOrgEnabled != false",
36
43
  successMessage: "Invitation sent",
37
44
  refreshAfter: true,
38
45
  params: [
@@ -76,6 +83,21 @@ var SysUser = ObjectSchema.create({
76
83
  successMessage: "User unbanned",
77
84
  refreshAfter: true
78
85
  },
86
+ {
87
+ // ADR-0069 D2 — clear a brute-force lockout early (locked_until auto-
88
+ // expires, but an admin can release a user immediately). Hits the
89
+ // plugin-auth custom route, which is admin-guarded server-side.
90
+ name: "unlock_user",
91
+ label: "Unlock Account",
92
+ icon: "lock-open",
93
+ variant: "secondary",
94
+ locations: ["list_item"],
95
+ type: "api",
96
+ target: "/api/v1/auth/admin/unlock-user",
97
+ recordIdParam: "userId",
98
+ successMessage: "Account unlocked",
99
+ refreshAfter: true
100
+ },
79
101
  {
80
102
  name: "set_user_password",
81
103
  label: "Set Password",
@@ -152,7 +174,11 @@ var SysUser = ObjectSchema.create({
152
174
  locations: ["record_header", "record_more", "record_section"],
153
175
  type: "api",
154
176
  target: "/api/v1/auth/change-password",
155
- visible: "record.id == ctx.user.id",
177
+ // Managed (IdP-provisioned) users hold no local credential — hide the
178
+ // password form so they can't self-mint a password that bypasses
179
+ // enforced SSO. The break-glass owner (env-native, or flipped back when
180
+ // their break-glass password is set) keeps it. ADR-0024 D4/D5.2.
181
+ visible: 'record.id == ctx.user.id && record.source != "idp_provisioned"',
156
182
  successMessage: "Password changed",
157
183
  refreshAfter: false,
158
184
  params: [
@@ -169,7 +195,9 @@ var SysUser = ObjectSchema.create({
169
195
  locations: ["record_header", "record_more", "record_section"],
170
196
  type: "api",
171
197
  target: "/api/v1/auth/change-email",
172
- visible: "record.id == ctx.user.id",
198
+ // A managed user's email is owned by the IdP — a local change would
199
+ // desync. Hide for IdP-provisioned; env-native users keep it.
200
+ visible: 'record.id == ctx.user.id && record.source != "idp_provisioned"',
173
201
  successMessage: "Verification email sent \u2014 check the new address to confirm.",
174
202
  refreshAfter: false,
175
203
  params: [
@@ -200,7 +228,9 @@ var SysUser = ObjectSchema.create({
200
228
  locations: ["record_more", "record_section"],
201
229
  type: "api",
202
230
  target: "/api/v1/auth/delete-user",
203
- visible: "record.id == ctx.user.id",
231
+ // Self-delete needs a local password; managed users are deprovisioned
232
+ // via the IdP (org-removal / SCIM), not local self-service. Hide for them.
233
+ visible: 'record.id == ctx.user.id && record.source != "idp_provisioned"',
204
234
  confirmText: "Permanently delete your account? This cannot be undone \u2014 all your sessions will be terminated and all data you own will be removed per the configured retention policy.",
205
235
  successMessage: "Account deleted",
206
236
  refreshAfter: false,
@@ -283,7 +313,7 @@ var SysUser = ObjectSchema.create({
283
313
  name: "all_users",
284
314
  label: "All Users",
285
315
  data: { provider: "object", object: "sys_user" },
286
- columns: ["name", "email", "email_verified", "two_factor_enabled", "created_at"],
316
+ columns: ["name", "email", "email_verified", "source", "two_factor_enabled", "created_at"],
287
317
  sort: [{ field: "name", order: "asc" }],
288
318
  pagination: { pageSize: 50 }
289
319
  },
@@ -370,6 +400,51 @@ var SysUser = ObjectSchema.create({
370
400
  group: "Admin",
371
401
  description: "When set, the ban auto-clears at this time."
372
402
  }),
403
+ // ── Anti-brute-force (ADR-0069 D2) — owned by objectql, better-auth is
404
+ // oblivious. The auth manager's sign-in hooks maintain these: failures
405
+ // increment the counter; crossing `lockout_threshold` stamps
406
+ // `locked_until`; a successful sign-in resets both. Admins can clear
407
+ // them early via the Unlock action.
408
+ failed_login_count: Field.number({
409
+ label: "Failed Login Count",
410
+ required: false,
411
+ defaultValue: 0,
412
+ readonly: true,
413
+ group: "Admin",
414
+ description: "Consecutive failed sign-in attempts; reset to 0 on success. Maintained by the auth manager."
415
+ }),
416
+ locked_until: Field.datetime({
417
+ label: "Locked Until",
418
+ required: false,
419
+ readonly: true,
420
+ group: "Admin",
421
+ description: "When set and in the future, sign-in is rejected (brute-force lockout). Auto-clears past this time; an admin can clear it early via Unlock."
422
+ }),
423
+ // ADR-0069 D1 — last password change; drives password-expiry enforcement.
424
+ // Stamped on sign-up / change-password / reset-password. Null = never
425
+ // expires (until the user next changes their password).
426
+ password_changed_at: Field.datetime({
427
+ label: "Password Changed At",
428
+ required: false,
429
+ readonly: true,
430
+ group: "Admin",
431
+ description: "Timestamp of the last password change. Backs password_expiry_days; system-managed."
432
+ }),
433
+ // ADR-0069 D3 — when enforced MFA first applied to this user; starts the
434
+ // grace clock. Stamped lazily at session validation; system-managed.
435
+ mfa_required_at: Field.datetime({
436
+ label: "MFA Required At",
437
+ required: false,
438
+ readonly: true,
439
+ group: "Admin",
440
+ description: "When enforced MFA first applied to this user (grace-period clock). Backs mfa_required; system-managed."
441
+ }),
442
+ ai_access: Field.boolean({
443
+ label: "AI Access",
444
+ defaultValue: false,
445
+ group: "Admin",
446
+ 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)."
447
+ }),
373
448
  // ── Profile ──────────────────────────────────────────────────
374
449
  image: Field.url({
375
450
  label: "Profile Image",
@@ -390,6 +465,28 @@ var SysUser = ObjectSchema.create({
390
465
  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."
391
466
  }),
392
467
  // ── System (auto-managed, hidden from create/edit forms) ─────
468
+ // Identity provenance (ADR-0024 D4). `idp_provisioned` users were
469
+ // JIT-created on first federated login (a `sys_account` exists for an
470
+ // external/OIDC provider — e.g. the cloud-as-IdP `objectstack-cloud`
471
+ // provider, or a customer's own IdP); `env_native` users registered
472
+ // locally (email/password) or are app end-users. Stamped automatically by
473
+ // the AuthManager `account.create.after` hook — never edited by hand.
474
+ // Drives the managed-vs-native user-mgmt UI gating (the password /
475
+ // identity-edit actions hide for managed users, who hold no local
476
+ // credential) and is the marker SCIM lifecycle keys off. Owned by objectql
477
+ // (better-auth is oblivious to this column — like `ai_access`).
478
+ source: Field.select({
479
+ label: "Identity Source",
480
+ required: false,
481
+ readonly: true,
482
+ group: "System",
483
+ defaultValue: "env_native",
484
+ options: [
485
+ { label: "IdP-Provisioned", value: "idp_provisioned" },
486
+ { label: "Env-Native", value: "env_native" }
487
+ ],
488
+ description: "How this identity was created \u2014 idp_provisioned (federated SSO JIT) or env_native (local signup / app end-user). System-managed; do not edit."
489
+ }),
393
490
  id: Field.text({
394
491
  label: "User ID",
395
492
  required: true,
@@ -514,6 +611,29 @@ var SysSession = ObjectSchema.create({
514
611
  required: true,
515
612
  group: "Session"
516
613
  }),
614
+ // ── ADR-0069 D4 — session controls (idle / absolute / revoke) ──
615
+ last_activity_at: Field.datetime({
616
+ label: "Last Activity At",
617
+ required: false,
618
+ readonly: true,
619
+ group: "Session",
620
+ description: "Timestamp of the last request on this session; drives idle-timeout. System-managed."
621
+ }),
622
+ revoked_at: Field.datetime({
623
+ label: "Revoked At",
624
+ required: false,
625
+ readonly: true,
626
+ group: "Session",
627
+ description: "When set, this session was revoked (idle / absolute-max / concurrent-cap / admin). System-managed."
628
+ }),
629
+ revoke_reason: Field.text({
630
+ label: "Revoke Reason",
631
+ required: false,
632
+ maxLength: 64,
633
+ readonly: true,
634
+ group: "Session",
635
+ description: "Why the session was revoked (idle_timeout, absolute_max, concurrent_cap, \u2026)."
636
+ }),
517
637
  // ── Active context (multi-org/team) ──────────────────────────
518
638
  active_organization_id: Field.lookup("sys_organization", {
519
639
  label: "Active Organization",
@@ -755,6 +875,16 @@ var SysAccount = ObjectSchema.create({
755
875
  label: "Password Hash",
756
876
  required: false,
757
877
  description: "Hashed password for email/password provider"
878
+ }),
879
+ // ADR-0069 D1 — bounded ring of previous password hashes (JSON array of
880
+ // strings), used to reject password reuse on change/reset. Maintained by
881
+ // the auth manager; never exposed in UI.
882
+ previous_password_hashes: Field.textarea({
883
+ label: "Previous Password Hashes",
884
+ required: false,
885
+ readonly: true,
886
+ hidden: true,
887
+ description: "JSON array of prior password hashes (bounded by password_history_count); reuse-prevention only. System-managed."
758
888
  })
759
889
  },
760
890
  indexes: [
@@ -820,7 +950,13 @@ var SysVerification = ObjectSchema.create({
820
950
  })
821
951
  },
822
952
  indexes: [
823
- { fields: ["value"], unique: true },
953
+ // `value` must NOT be unique. better-auth's oauth-provider stores OIDC
954
+ // authorization codes in this table with `value` = a JSON blob keyed by
955
+ // user+client+state, which can legitimately repeat. A UNIQUE constraint
956
+ // makes `/api/v1/auth/oauth2/authorize` fail (`UNIQUE constraint failed:
957
+ // sys_verification.value`) → 503, breaking cloud-as-IdP SSO entirely.
958
+ // better-auth keys verification lookups on `identifier`, not `value`.
959
+ { fields: ["value"], unique: false },
824
960
  { fields: ["identifier"], unique: false },
825
961
  { fields: ["expires_at"], unique: false }
826
962
  ],
@@ -888,6 +1024,10 @@ var SysOrganization = ObjectSchema.create({
888
1024
  recordIdParam: "organizationId",
889
1025
  // better-auth `organization/update` nests editable fields under `data`.
890
1026
  bodyShape: { wrap: "data" },
1027
+ // Org-admin actions are multi-org-only; hide them in single-org for
1028
+ // consistency with `create_organization` (the org list is empty there,
1029
+ // but this also guards direct record-URL access).
1030
+ visible: "features.multiOrgEnabled != false",
891
1031
  successMessage: "Organization updated",
892
1032
  refreshAfter: true,
893
1033
  params: [
@@ -906,6 +1046,7 @@ var SysOrganization = ObjectSchema.create({
906
1046
  type: "api",
907
1047
  target: "/api/v1/auth/organization/delete",
908
1048
  recordIdParam: "organizationId",
1049
+ visible: "features.multiOrgEnabled != false",
909
1050
  confirmText: "Delete this organization? All members will lose access immediately. This cannot be undone.",
910
1051
  successMessage: "Organization deleted",
911
1052
  refreshAfter: true
@@ -924,6 +1065,7 @@ var SysOrganization = ObjectSchema.create({
924
1065
  type: "api",
925
1066
  target: "/api/v1/auth/organization/set-active",
926
1067
  recordIdParam: "organizationId",
1068
+ visible: "features.multiOrgEnabled != false",
927
1069
  successMessage: "Active organization switched",
928
1070
  refreshAfter: true
929
1071
  },
@@ -940,6 +1082,7 @@ var SysOrganization = ObjectSchema.create({
940
1082
  type: "api",
941
1083
  target: "/api/v1/auth/organization/leave",
942
1084
  recordIdParam: "organizationId",
1085
+ visible: "features.multiOrgEnabled != false",
943
1086
  confirmText: "Leave this organization? You will lose access to all of its resources.",
944
1087
  successMessage: "You have left the organization",
945
1088
  refreshAfter: true
@@ -961,6 +1104,7 @@ var SysOrganization = ObjectSchema.create({
961
1104
  type: "api",
962
1105
  target: "/api/v1/cloud/organizations/{id}/change-slug",
963
1106
  method: "POST",
1107
+ visible: "features.multiOrgEnabled != false",
964
1108
  confirmText: "Renaming the slug rewrites every platform subdomain for this org and parks the old slug for 90 days. Continue?",
965
1109
  successMessage: "Organization slug changed",
966
1110
  refreshAfter: true,
@@ -1010,6 +1154,17 @@ var SysOrganization = ObjectSchema.create({
1010
1154
  description: "JSON-serialized organization metadata",
1011
1155
  group: "Configuration"
1012
1156
  }),
1157
+ // ADR-0069 D3 — per-org MFA tightening above the global floor. When true,
1158
+ // members of this org must enrol TOTP to access data (enforced at the
1159
+ // session-validation gate). An org can only tighten, never loosen, the
1160
+ // global `mfa_required` setting.
1161
+ require_mfa: Field.boolean({
1162
+ label: "Require Multi-Factor Auth",
1163
+ required: false,
1164
+ defaultValue: false,
1165
+ group: "Configuration",
1166
+ description: "When true, every member of this organization must enroll an authenticator app to access data."
1167
+ }),
1013
1168
  // ── System ───────────────────────────────────────────────────
1014
1169
  id: Field.text({
1015
1170
  label: "Organization ID",
@@ -1059,7 +1214,10 @@ var SysMember = ObjectSchema.create({
1059
1214
  docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
1060
1215
  },
1061
1216
  description: "Organization membership records",
1062
- titleFormat: "{user_id} in {organization_id}",
1217
+ // Org-independent title: organization_id is null in single-org mode, so a
1218
+ // '{user_id} in {organization_id}' format renders "… in null". User + role
1219
+ // identifies the membership in both single- and multi-org deployments.
1220
+ titleFormat: "{user_id} ({role})",
1063
1221
  compactLayout: ["user_id", "organization_id", "role"],
1064
1222
  // Row-level actions: better-auth `organization/update-member-role` and
1065
1223
  // `organization/remove-member`. Generic CRUD is suppressed on better-auth
@@ -1080,6 +1238,11 @@ var SysMember = ObjectSchema.create({
1080
1238
  locations: ["list_toolbar"],
1081
1239
  type: "api",
1082
1240
  target: "/api/v1/auth/organization/add-member",
1241
+ // Org-membership mutations are multi-org-only: the better-auth
1242
+ // endpoints resolve an active org that does not exist in single-org
1243
+ // mode, so these actions would fail at the API. Gate every one on
1244
+ // the multi-org flag (mirrors sys_organization.create_organization).
1245
+ visible: "features.multiOrgEnabled != false",
1083
1246
  successMessage: "Member added",
1084
1247
  refreshAfter: true,
1085
1248
  params: [
@@ -1097,6 +1260,7 @@ var SysMember = ObjectSchema.create({
1097
1260
  type: "api",
1098
1261
  target: "/api/v1/auth/organization/update-member-role",
1099
1262
  recordIdParam: "memberId",
1263
+ visible: "features.multiOrgEnabled != false",
1100
1264
  successMessage: "Member role updated",
1101
1265
  refreshAfter: true,
1102
1266
  params: [
@@ -1113,6 +1277,7 @@ var SysMember = ObjectSchema.create({
1113
1277
  type: "api",
1114
1278
  target: "/api/v1/auth/organization/remove-member",
1115
1279
  recordIdParam: "memberIdOrEmail",
1280
+ visible: "features.multiOrgEnabled != false",
1116
1281
  confirmText: "Remove this member from the organization? They will lose access to all org resources.",
1117
1282
  successMessage: "Member removed",
1118
1283
  refreshAfter: true
@@ -1134,7 +1299,7 @@ var SysMember = ObjectSchema.create({
1134
1299
  target: "/api/v1/auth/organization/update-member-role",
1135
1300
  recordIdParam: "memberId",
1136
1301
  bodyExtra: { role: "owner" },
1137
- visible: "record.role != 'owner'",
1302
+ visible: "record.role != 'owner' && features.multiOrgEnabled != false",
1138
1303
  confirmText: "Transfer ownership of this organization to the selected member? You will be demoted to admin and lose owner-only privileges.",
1139
1304
  successMessage: "Ownership transferred",
1140
1305
  refreshAfter: true
@@ -1219,7 +1384,10 @@ var SysInvitation = ObjectSchema.create({
1219
1384
  docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
1220
1385
  },
1221
1386
  description: "Organization invitations for user onboarding",
1222
- titleFormat: "Invitation to {organization_id}",
1387
+ // Title by invitee email rather than organization_id: the latter is null in
1388
+ // single-org mode (renders "Invitation to null"), and the recipient email is
1389
+ // the more useful identifier in both modes anyway.
1390
+ titleFormat: "Invitation for {email}",
1223
1391
  compactLayout: ["email", "organization_id", "status"],
1224
1392
  // Custom actions — generic CRUD is suppressed (better-auth-managed).
1225
1393
  // Mirror the `invite_user` toolbar action from sys_user here so admins
@@ -1233,6 +1401,13 @@ var SysInvitation = ObjectSchema.create({
1233
1401
  locations: ["list_toolbar"],
1234
1402
  type: "api",
1235
1403
  target: "/api/v1/auth/organization/invite-member",
1404
+ // Inviting/managing invitations is a multi-org-only flow (the
1405
+ // endpoint resolves an active org absent in single-org mode). Gate
1406
+ // the admin-side actions on the multi-org flag (mirrors
1407
+ // sys_organization.create_organization). The recipient-side
1408
+ // accept/reject actions below stay record-gated — they are
1409
+ // unreachable in single-org anyway (no invitation rows exist).
1410
+ visible: "features.multiOrgEnabled != false",
1236
1411
  successMessage: "Invitation sent",
1237
1412
  refreshAfter: true,
1238
1413
  params: [
@@ -1250,6 +1425,7 @@ var SysInvitation = ObjectSchema.create({
1250
1425
  type: "api",
1251
1426
  target: "/api/v1/auth/organization/cancel-invitation",
1252
1427
  recordIdParam: "invitationId",
1428
+ visible: "features.multiOrgEnabled != false",
1253
1429
  confirmText: "Cancel this invitation? The recipient will no longer be able to accept it.",
1254
1430
  successMessage: "Invitation canceled",
1255
1431
  refreshAfter: true
@@ -1263,6 +1439,7 @@ var SysInvitation = ObjectSchema.create({
1263
1439
  type: "api",
1264
1440
  target: "/api/v1/auth/organization/invite-member",
1265
1441
  bodyExtra: { resend: true },
1442
+ visible: "features.multiOrgEnabled != false",
1266
1443
  successMessage: "Invitation resent",
1267
1444
  refreshAfter: true,
1268
1445
  params: [
@@ -1447,6 +1624,10 @@ var SysTeam = ObjectSchema.create({
1447
1624
  locations: ["list_toolbar"],
1448
1625
  type: "api",
1449
1626
  target: "/api/v1/auth/organization/create-team",
1627
+ // Teams are nested inside organizations — a multi-org-only concept.
1628
+ // Gate every team mutation on the multi-org flag so the affordances
1629
+ // disappear in single-org (mirrors sys_organization.create_organization).
1630
+ visible: "features.multiOrgEnabled != false",
1450
1631
  successMessage: "Team created",
1451
1632
  refreshAfter: true,
1452
1633
  params: [
@@ -1467,6 +1648,7 @@ var SysTeam = ObjectSchema.create({
1467
1648
  target: "/api/v1/auth/organization/update-team",
1468
1649
  recordIdParam: "teamId",
1469
1650
  bodyShape: { wrap: "data" },
1651
+ visible: "features.multiOrgEnabled != false",
1470
1652
  successMessage: "Team updated",
1471
1653
  refreshAfter: true,
1472
1654
  params: [
@@ -1485,6 +1667,7 @@ var SysTeam = ObjectSchema.create({
1485
1667
  type: "api",
1486
1668
  target: "/api/v1/auth/organization/remove-team",
1487
1669
  recordIdParam: "teamId",
1670
+ visible: "features.multiOrgEnabled != false",
1488
1671
  confirmText: "Delete this team? Members will lose any team-scoped access. This cannot be undone.",
1489
1672
  successMessage: "Team deleted",
1490
1673
  refreshAfter: true
@@ -1595,6 +1778,10 @@ var SysTeamMember = ObjectSchema.create({
1595
1778
  locations: ["list_toolbar"],
1596
1779
  type: "api",
1597
1780
  target: "/api/v1/auth/organization/add-team-member",
1781
+ // Team membership lives under organizations — multi-org-only. Gate
1782
+ // both mutations so they vanish in single-org (mirrors
1783
+ // sys_organization.create_organization).
1784
+ visible: "features.multiOrgEnabled != false",
1598
1785
  successMessage: "Team member added",
1599
1786
  refreshAfter: true,
1600
1787
  params: [
@@ -1615,6 +1802,7 @@ var SysTeamMember = ObjectSchema.create({
1615
1802
  locations: ["list_item"],
1616
1803
  type: "api",
1617
1804
  target: "/api/v1/auth/organization/remove-team-member",
1805
+ visible: "features.multiOrgEnabled != false",
1618
1806
  confirmText: "Remove this user from the team? They will lose any team-scoped access.",
1619
1807
  successMessage: "Team member removed",
1620
1808
  refreshAfter: true,
@@ -3154,7 +3342,343 @@ var SysJwks = ObjectSchema.create({
3154
3342
  mru: false
3155
3343
  }
3156
3344
  });
3345
+ var SysSsoProvider = ObjectSchema.create({
3346
+ name: "sys_sso_provider",
3347
+ label: "SSO Provider",
3348
+ pluralLabel: "SSO Providers",
3349
+ icon: "shield-check",
3350
+ isSystem: true,
3351
+ managedBy: "better-auth",
3352
+ // ADR-0024 — env-global, ADMIN-ONLY identity config. Two orthogonal controls:
3353
+ // • `tenancy.enabled: false` — the env IS the tenant; providers are env-wide,
3354
+ // not org-partitioned. Opting out of multi-tenancy lets a platform admin's
3355
+ // `viewAllRecords` superuser bypass see every provider (without it, the
3356
+ // `member_default` wildcard `tenant_isolation` RLS denies every row, since
3357
+ // better-auth writes via its adapter with no tenantId → `organization_id`
3358
+ // is never stamped).
3359
+ // • `requiredPermissions: ['manage_platform_settings']` — object-level
3360
+ // capability gate (ADR-0066 D3) so ordinary members are denied entirely
3361
+ // (without it, tenancy-disabled + `member_default`'s `'*': allowRead` would
3362
+ // leak providers to every authenticated user).
3363
+ // Together: admins see all env providers; non-admins get 403. better-auth's
3364
+ // own endpoints already read via a system context. (Env-only object — no
3365
+ // control-plane cross-tenant risk.)
3366
+ tenancy: { enabled: false, strategy: "shared" },
3367
+ requiredPermissions: ["manage_platform_settings"],
3368
+ // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
3369
+ protection: {
3370
+ lock: "full",
3371
+ reason: "Identity table managed by better-auth (@better-auth/sso) \u2014 see ADR-0024.",
3372
+ docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
3373
+ },
3374
+ description: "External SSO identity providers (OIDC / SAML) this environment federates login to",
3375
+ displayNameField: "provider_id",
3376
+ titleFormat: "{provider_id}",
3377
+ compactLayout: ["provider_id", "issuer", "domain"],
3378
+ // All mutations go through @better-auth/sso's endpoints under
3379
+ // /api/v1/auth/sso/* (register / delete-provider) rather than the generic
3380
+ // data layer, so server-side config validation + secret handling run.
3381
+ actions: [
3382
+ {
3383
+ name: "register_sso_provider",
3384
+ label: "Register SSO Provider",
3385
+ icon: "plus-circle",
3386
+ variant: "primary",
3387
+ mode: "create",
3388
+ locations: ["list_toolbar"],
3389
+ type: "api",
3390
+ method: "POST",
3391
+ // Routed through the env-side bridge (plugin-auth `auth-plugin.ts`), which
3392
+ // reshapes these FLAT form fields into the nested `oidcConfig` body that
3393
+ // `@better-auth/sso`'s /sso/register requires, then re-dispatches to it
3394
+ // (so the admin gate + discovery hydration run). Posting straight to
3395
+ // /sso/register would drop clientId/clientSecret (top-level → Zod-stripped)
3396
+ // and persist an unusable `oidc_config = null` provider.
3397
+ target: "/api/v1/auth/admin/sso/register",
3398
+ refreshAfter: true,
3399
+ params: [
3400
+ { name: "providerId", label: "Provider ID", type: "text", required: true, helpText: 'Stable identifier, e.g. "okta" or "acme-entra".' },
3401
+ { name: "issuer", label: "Issuer URL", type: "text", required: true, helpText: "IdP issuer, e.g. https://acme.okta.com. Discovery is fetched from here unless an explicit URL is given below." },
3402
+ { name: "domain", label: "Email Domain", type: "text", required: true, helpText: "Users with this email domain are routed to this IdP, e.g. acme.com." },
3403
+ { name: "clientId", label: "Client ID", type: "text", required: true, helpText: "OAuth client ID issued by the IdP for this environment." },
3404
+ { name: "clientSecret", label: "Client Secret", type: "text", required: true, helpText: "OAuth client secret (stored encrypted by better-auth)." },
3405
+ { name: "discoveryEndpoint", label: "Discovery URL", type: "text", required: false, helpText: "Optional. OIDC discovery document URL. Leave blank to derive `<issuer>/.well-known/openid-configuration`." },
3406
+ { name: "scopes", label: "Scopes", type: "text", required: false, placeholder: "openid email profile", helpText: 'Optional. Space- or comma-separated OAuth scopes. Defaults to "openid email profile".' },
3407
+ { name: "mapId", label: "Map: User ID claim", type: "text", required: false, placeholder: "sub", helpText: 'Optional. ID-token claim mapped to the user ID. Defaults to "sub".' },
3408
+ { name: "mapEmail", label: "Map: Email claim", type: "text", required: false, placeholder: "email", helpText: 'Optional. Claim mapped to email. Defaults to "email".' },
3409
+ { name: "mapName", label: "Map: Name claim", type: "text", required: false, placeholder: "name", helpText: 'Optional. Claim mapped to display name. Defaults to "name".' }
3410
+ ]
3411
+ },
3412
+ {
3413
+ name: "register_saml_provider",
3414
+ label: "Register SAML Provider",
3415
+ icon: "shield",
3416
+ variant: "primary",
3417
+ mode: "create",
3418
+ locations: ["list_toolbar"],
3419
+ type: "api",
3420
+ method: "POST",
3421
+ // SAML 2.0 via @better-auth/sso (samlify-backed). Routed through the
3422
+ // env-side bridge (plugin-auth `auth-plugin.ts` → register-saml), which
3423
+ // reshapes these FLAT IdP fields into the nested `samlConfig` body that
3424
+ // @better-auth/sso's /sso/register requires (entryPoint/cert/callbackUrl/
3425
+ // spMetadata/identifierFormat), derives the per-provider ACS URL, and
3426
+ // re-dispatches to /sso/register (admin gate runs). The response returns
3427
+ // the SP ACS + metadata URLs to configure on the IdP.
3428
+ target: "/api/v1/auth/admin/sso/register-saml",
3429
+ refreshAfter: true,
3430
+ params: [
3431
+ { name: "providerId", label: "Provider ID", type: "text", required: true, helpText: 'Stable identifier, e.g. "acme-saml".' },
3432
+ { name: "issuer", label: "IdP Entity ID", type: "text", required: true, helpText: "The IdP\u2019s SAML EntityID (issuer), e.g. https://saml.acme.com/entityid." },
3433
+ { name: "domain", label: "Email Domain", type: "text", required: true, helpText: "Users with this email domain are routed to this IdP, e.g. acme.com." },
3434
+ { name: "entryPoint", label: "IdP SSO URL", type: "text", required: true, helpText: "The IdP\u2019s SAML single sign-on (redirect) endpoint that receives the SAMLRequest." },
3435
+ { name: "cert", label: "IdP Signing Certificate", type: "textarea", required: true, helpText: "The IdP\u2019s X.509 signing certificate (PEM body). Used to verify assertion signatures." },
3436
+ { name: "identifierFormat", label: "NameID Format", type: "text", required: false, placeholder: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", helpText: "Optional. Requested SAML NameID format. Defaults to the IdP\u2019s configured format." }
3437
+ ]
3438
+ },
3439
+ {
3440
+ name: "request_domain_verification",
3441
+ label: "Request Domain Verification",
3442
+ icon: "globe",
3443
+ variant: "secondary",
3444
+ locations: ["list_item", "record_header"],
3445
+ type: "api",
3446
+ method: "POST",
3447
+ // ADR-0024 ② (opt-in OS_SSO_DOMAIN_VERIFICATION). Asks @better-auth/sso
3448
+ // for a one-time DNS-TXT challenge and reveals the ready-to-paste record
3449
+ // ONCE via `resultDialog`. Routed through the env bridge (plugin-auth /
3450
+ // cloud AuthProxyPlugin) which reshapes the `{domainVerificationToken}`
3451
+ // response into the `{ data: { dnsRecordName, dnsRecordValue } }` envelope
3452
+ // the dialog reads. When the feature is OFF the bridge returns a clear
3453
+ // "not enabled for this environment" error instead of a bare 404.
3454
+ target: "/api/v1/auth/admin/sso/request-domain-verification",
3455
+ params: [
3456
+ { name: "providerId", field: "provider_id", defaultFromRow: true, required: true },
3457
+ { name: "domain", field: "domain", defaultFromRow: true, required: false }
3458
+ ],
3459
+ resultDialog: {
3460
+ title: "Verify your domain",
3461
+ description: "Add the DNS TXT record below at your domain\u2019s DNS provider, then run \u201CVerify Domain\u201D. The token is shown once.",
3462
+ acknowledge: "Done",
3463
+ fields: [
3464
+ { path: "data.dnsRecordType", label: "Record type", format: "text" },
3465
+ { path: "data.dnsRecordName", label: "Name / Host", format: "secret" },
3466
+ { path: "data.dnsRecordValue", label: "Value", format: "secret" }
3467
+ ]
3468
+ }
3469
+ },
3470
+ {
3471
+ name: "verify_domain",
3472
+ label: "Verify Domain",
3473
+ icon: "shield-check",
3474
+ variant: "secondary",
3475
+ locations: ["list_item", "record_header"],
3476
+ type: "api",
3477
+ method: "POST",
3478
+ // ADR-0024 ②. Re-checks the DNS-TXT record and flips `domain_verified`
3479
+ // on success. Routed through the env bridge, which maps @better-auth/sso's
3480
+ // empty 204 / 502 into a clear success/error toast.
3481
+ target: "/api/v1/auth/admin/sso/verify-domain",
3482
+ successMessage: "Domain ownership verified",
3483
+ refreshAfter: true,
3484
+ params: [
3485
+ { name: "providerId", field: "provider_id", defaultFromRow: true, required: true }
3486
+ ]
3487
+ },
3488
+ {
3489
+ name: "delete_sso_provider",
3490
+ label: "Delete SSO Provider",
3491
+ icon: "trash-2",
3492
+ variant: "danger",
3493
+ mode: "delete",
3494
+ locations: ["list_item", "record_header"],
3495
+ type: "api",
3496
+ method: "POST",
3497
+ target: "/api/v1/auth/sso/delete-provider",
3498
+ confirmText: "Delete this SSO provider? Users from its domain will no longer be able to sign in through it.",
3499
+ successMessage: "SSO provider deleted",
3500
+ refreshAfter: true,
3501
+ params: [
3502
+ { name: "providerId", field: "provider_id", defaultFromRow: true, required: true }
3503
+ ]
3504
+ }
3505
+ ],
3506
+ listViews: {
3507
+ all: {
3508
+ type: "grid",
3509
+ name: "all",
3510
+ label: "All",
3511
+ data: { provider: "object", object: "sys_sso_provider" },
3512
+ columns: ["provider_id", "issuer", "domain", "domain_verified", "created_at"],
3513
+ sort: [{ field: "provider_id", order: "asc" }],
3514
+ pagination: { pageSize: 50 },
3515
+ // Per-object empty state — the shared identity-object copy ("created
3516
+ // automatically … cannot be added here") is wrong for this object, which
3517
+ // HAS a "Register SSO Provider" action. Point admins at it instead.
3518
+ emptyState: {
3519
+ title: "No SSO providers yet",
3520
+ message: "Register your organization\u2019s external IdP \u2014 OIDC (Okta, Entra, Auth0, \u2026) with \u201CRegister SSO Provider\u201D, or SAML 2.0 with \u201CRegister SAML Provider\u201D. Members whose email domain matches can then sign in through it.",
3521
+ icon: "log-in"
3522
+ }
3523
+ }
3524
+ },
3525
+ fields: {
3526
+ id: Field.text({ label: "ID", required: true, readonly: true, group: "System" }),
3527
+ provider_id: Field.text({
3528
+ label: "Provider ID",
3529
+ required: true,
3530
+ searchable: true,
3531
+ maxLength: 255,
3532
+ description: "Stable provider identifier (unique within the environment)",
3533
+ group: "Identity"
3534
+ }),
3535
+ issuer: Field.text({
3536
+ label: "Issuer",
3537
+ required: true,
3538
+ maxLength: 2048,
3539
+ description: "IdP issuer URL",
3540
+ group: "Identity"
3541
+ }),
3542
+ domain: Field.text({
3543
+ label: "Email Domain",
3544
+ required: true,
3545
+ maxLength: 255,
3546
+ description: "Email domain routed to this IdP (e.g. acme.com)",
3547
+ group: "Identity"
3548
+ }),
3549
+ domain_verified: Field.boolean({
3550
+ label: "Domain Verified",
3551
+ defaultValue: false,
3552
+ readonly: true,
3553
+ description: "Whether DNS ownership of the email domain has been proven (ADR-0024 \u2461). Set by \u201CVerify Domain\u201D after the DNS TXT record resolves. Managed by better-auth \u2014 not directly editable. Only enforced when domain verification is enabled for the environment.",
3554
+ group: "Identity"
3555
+ }),
3556
+ oidc_config: Field.textarea({
3557
+ label: "OIDC Config",
3558
+ required: false,
3559
+ description: "JSON: clientId, clientSecret, endpoints, scopes, mapping, pkce (managed by better-auth)",
3560
+ group: "Protocol"
3561
+ }),
3562
+ saml_config: Field.textarea({
3563
+ label: "SAML Config",
3564
+ required: false,
3565
+ description: "JSON: entryPoint, cert, identifierFormat, mapping (managed by better-auth)",
3566
+ group: "Protocol"
3567
+ }),
3568
+ user_id: Field.lookup("sys_user", {
3569
+ label: "Registered By",
3570
+ required: false,
3571
+ description: "User who registered this provider",
3572
+ group: "System"
3573
+ }),
3574
+ organization_id: Field.text({
3575
+ label: "Organization",
3576
+ required: false,
3577
+ maxLength: 255,
3578
+ description: "Organization scope (when org-scoped SSO is used)",
3579
+ group: "System"
3580
+ }),
3581
+ created_at: Field.datetime({ label: "Created At", defaultValue: "NOW()", readonly: true, group: "System" }),
3582
+ updated_at: Field.datetime({ label: "Updated At", defaultValue: "NOW()", readonly: true, group: "System" })
3583
+ },
3584
+ indexes: [
3585
+ { fields: ["provider_id"], unique: true },
3586
+ { fields: ["domain"] },
3587
+ { fields: ["user_id"] }
3588
+ ],
3589
+ enable: {
3590
+ trackHistory: true,
3591
+ searchable: true,
3592
+ apiEnabled: true,
3593
+ // Mutations go through /api/v1/auth/sso/* (register / delete-provider);
3594
+ // the generic data layer is read-only so sysadmins cannot bypass
3595
+ // server-side validation / secret handling.
3596
+ apiMethods: ["get", "list"],
3597
+ trash: false,
3598
+ mru: false
3599
+ }
3600
+ });
3601
+ var SysScimProvider = ObjectSchema.create({
3602
+ name: "sys_scim_provider",
3603
+ label: "SCIM Provider",
3604
+ pluralLabel: "SCIM Providers",
3605
+ icon: "users",
3606
+ isSystem: true,
3607
+ managedBy: "better-auth",
3608
+ // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
3609
+ protection: {
3610
+ lock: "full",
3611
+ reason: "Identity table managed by better-auth (@better-auth/scim) \u2014 see ADR-0071.",
3612
+ docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
3613
+ },
3614
+ description: "SCIM 2.0 connections (bearer tokens) external IdPs use to provision/deprovision this environment's users",
3615
+ displayNameField: "provider_id",
3616
+ titleFormat: "{provider_id}",
3617
+ compactLayout: ["provider_id", "organization_id"],
3618
+ listViews: {
3619
+ all: {
3620
+ type: "grid",
3621
+ name: "all",
3622
+ label: "All",
3623
+ data: { provider: "object", object: "sys_scim_provider" },
3624
+ // scim_token is intentionally excluded — never surface the credential.
3625
+ columns: ["provider_id", "organization_id", "created_at"],
3626
+ sort: [{ field: "provider_id", order: "asc" }],
3627
+ pagination: { pageSize: 50 }
3628
+ }
3629
+ },
3630
+ fields: {
3631
+ id: Field.text({ label: "ID", required: true, readonly: true, group: "System" }),
3632
+ provider_id: Field.text({
3633
+ label: "Provider ID",
3634
+ required: true,
3635
+ searchable: true,
3636
+ maxLength: 255,
3637
+ description: 'Stable SCIM provider identifier (e.g. "okta-scim")',
3638
+ group: "Identity"
3639
+ }),
3640
+ scim_token: Field.text({
3641
+ label: "SCIM Token (hash)",
3642
+ required: false,
3643
+ readonly: true,
3644
+ maxLength: 1024,
3645
+ description: "Hashed bearer credential for this SCIM connection \u2014 the plaintext is shown once at generate-token. Sensitive; do not expose.",
3646
+ group: "Secret"
3647
+ }),
3648
+ organization_id: Field.text({
3649
+ label: "Organization",
3650
+ required: false,
3651
+ maxLength: 255,
3652
+ description: "Organization scope of this token (org-scoped tokens restrict provisioning to that org)",
3653
+ group: "System"
3654
+ }),
3655
+ user_id: Field.lookup("sys_user", {
3656
+ label: "Owned By",
3657
+ required: false,
3658
+ description: "User who generated this token (when provider-ownership is enabled)",
3659
+ group: "System"
3660
+ }),
3661
+ created_at: Field.datetime({ label: "Created At", defaultValue: "NOW()", readonly: true, group: "System" }),
3662
+ updated_at: Field.datetime({ label: "Updated At", defaultValue: "NOW()", readonly: true, group: "System" })
3663
+ },
3664
+ indexes: [
3665
+ { fields: ["provider_id"], unique: true },
3666
+ { fields: ["organization_id"] },
3667
+ { fields: ["user_id"] }
3668
+ ],
3669
+ enable: {
3670
+ trackHistory: true,
3671
+ searchable: false,
3672
+ apiEnabled: true,
3673
+ // Mutations + token issuance go through @better-auth/scim's endpoints
3674
+ // under /api/v1/auth/scim/*; the generic data layer is read-only so the
3675
+ // credential cannot be written/bypassed through it.
3676
+ apiMethods: ["list"],
3677
+ trash: false,
3678
+ mru: false
3679
+ }
3680
+ });
3157
3681
 
3158
- export { SysAccount, SysApiKey, SysBusinessUnit, SysBusinessUnitMember, SysDeviceCode, SysInvitation, SysJwks, SysMember, SysOauthAccessToken, SysOauthApplication, SysOauthConsent, SysOauthRefreshToken, SysOrganization, SysSession, SysTeam, SysTeamMember, SysTwoFactor, SysUser, SysUserPreference, SysVerification };
3682
+ export { SysAccount, SysApiKey, SysBusinessUnit, SysBusinessUnitMember, SysDeviceCode, SysInvitation, SysJwks, SysMember, SysOauthAccessToken, SysOauthApplication, SysOauthConsent, SysOauthRefreshToken, SysOrganization, SysScimProvider, SysSession, SysSsoProvider, SysTeam, SysTeamMember, SysTwoFactor, SysUser, SysUserPreference, SysVerification };
3159
3683
  //# sourceMappingURL=index.mjs.map
3160
3684
  //# sourceMappingURL=index.mjs.map