@objectstack/platform-objects 10.3.0 → 11.0.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.
@@ -35,6 +35,13 @@ var SysUser = data.ObjectSchema.create({
35
35
  locations: ["list_toolbar"],
36
36
  type: "api",
37
37
  target: "/api/v1/auth/organization/invite-member",
38
+ // Org invitations are a multi-org-only flow (the endpoint resolves
39
+ // an active org that does not exist in single-org mode). Hide the
40
+ // affordance unless multi-org is enabled — matching the
41
+ // `create_organization` gate on sys_organization. This action is the
42
+ // most exposed of the set because the Users list is always reachable
43
+ // in single-org, unlike the org/membership lists.
44
+ visible: "features.multiOrgEnabled != false",
38
45
  successMessage: "Invitation sent",
39
46
  refreshAfter: true,
40
47
  params: [
@@ -78,6 +85,21 @@ var SysUser = data.ObjectSchema.create({
78
85
  successMessage: "User unbanned",
79
86
  refreshAfter: true
80
87
  },
88
+ {
89
+ // ADR-0069 D2 — clear a brute-force lockout early (locked_until auto-
90
+ // expires, but an admin can release a user immediately). Hits the
91
+ // plugin-auth custom route, which is admin-guarded server-side.
92
+ name: "unlock_user",
93
+ label: "Unlock Account",
94
+ icon: "lock-open",
95
+ variant: "secondary",
96
+ locations: ["list_item"],
97
+ type: "api",
98
+ target: "/api/v1/auth/admin/unlock-user",
99
+ recordIdParam: "userId",
100
+ successMessage: "Account unlocked",
101
+ refreshAfter: true
102
+ },
81
103
  {
82
104
  name: "set_user_password",
83
105
  label: "Set Password",
@@ -154,7 +176,11 @@ var SysUser = data.ObjectSchema.create({
154
176
  locations: ["record_header", "record_more", "record_section"],
155
177
  type: "api",
156
178
  target: "/api/v1/auth/change-password",
157
- visible: "record.id == ctx.user.id",
179
+ // Managed (IdP-provisioned) users hold no local credential — hide the
180
+ // password form so they can't self-mint a password that bypasses
181
+ // enforced SSO. The break-glass owner (env-native, or flipped back when
182
+ // their break-glass password is set) keeps it. ADR-0024 D4/D5.2.
183
+ visible: 'record.id == ctx.user.id && record.source != "idp_provisioned"',
158
184
  successMessage: "Password changed",
159
185
  refreshAfter: false,
160
186
  params: [
@@ -171,7 +197,9 @@ var SysUser = data.ObjectSchema.create({
171
197
  locations: ["record_header", "record_more", "record_section"],
172
198
  type: "api",
173
199
  target: "/api/v1/auth/change-email",
174
- visible: "record.id == ctx.user.id",
200
+ // A managed user's email is owned by the IdP — a local change would
201
+ // desync. Hide for IdP-provisioned; env-native users keep it.
202
+ visible: 'record.id == ctx.user.id && record.source != "idp_provisioned"',
175
203
  successMessage: "Verification email sent \u2014 check the new address to confirm.",
176
204
  refreshAfter: false,
177
205
  params: [
@@ -202,7 +230,9 @@ var SysUser = data.ObjectSchema.create({
202
230
  locations: ["record_more", "record_section"],
203
231
  type: "api",
204
232
  target: "/api/v1/auth/delete-user",
205
- visible: "record.id == ctx.user.id",
233
+ // Self-delete needs a local password; managed users are deprovisioned
234
+ // via the IdP (org-removal / SCIM), not local self-service. Hide for them.
235
+ visible: 'record.id == ctx.user.id && record.source != "idp_provisioned"',
206
236
  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.",
207
237
  successMessage: "Account deleted",
208
238
  refreshAfter: false,
@@ -285,7 +315,7 @@ var SysUser = data.ObjectSchema.create({
285
315
  name: "all_users",
286
316
  label: "All Users",
287
317
  data: { provider: "object", object: "sys_user" },
288
- columns: ["name", "email", "email_verified", "two_factor_enabled", "created_at"],
318
+ columns: ["name", "email", "email_verified", "source", "two_factor_enabled", "created_at"],
289
319
  sort: [{ field: "name", order: "asc" }],
290
320
  pagination: { pageSize: 50 }
291
321
  },
@@ -372,6 +402,32 @@ var SysUser = data.ObjectSchema.create({
372
402
  group: "Admin",
373
403
  description: "When set, the ban auto-clears at this time."
374
404
  }),
405
+ // ── Anti-brute-force (ADR-0069 D2) — owned by objectql, better-auth is
406
+ // oblivious. The auth manager's sign-in hooks maintain these: failures
407
+ // increment the counter; crossing `lockout_threshold` stamps
408
+ // `locked_until`; a successful sign-in resets both. Admins can clear
409
+ // them early via the Unlock action.
410
+ failed_login_count: data.Field.number({
411
+ label: "Failed Login Count",
412
+ required: false,
413
+ defaultValue: 0,
414
+ readonly: true,
415
+ group: "Admin",
416
+ description: "Consecutive failed sign-in attempts; reset to 0 on success. Maintained by the auth manager."
417
+ }),
418
+ locked_until: data.Field.datetime({
419
+ label: "Locked Until",
420
+ required: false,
421
+ readonly: true,
422
+ group: "Admin",
423
+ 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."
424
+ }),
425
+ ai_access: data.Field.boolean({
426
+ label: "AI Access",
427
+ defaultValue: false,
428
+ group: "Admin",
429
+ 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)."
430
+ }),
375
431
  // ── Profile ──────────────────────────────────────────────────
376
432
  image: data.Field.url({
377
433
  label: "Profile Image",
@@ -392,6 +448,28 @@ var SysUser = data.ObjectSchema.create({
392
448
  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."
393
449
  }),
394
450
  // ── System (auto-managed, hidden from create/edit forms) ─────
451
+ // Identity provenance (ADR-0024 D4). `idp_provisioned` users were
452
+ // JIT-created on first federated login (a `sys_account` exists for an
453
+ // external/OIDC provider — e.g. the cloud-as-IdP `objectstack-cloud`
454
+ // provider, or a customer's own IdP); `env_native` users registered
455
+ // locally (email/password) or are app end-users. Stamped automatically by
456
+ // the AuthManager `account.create.after` hook — never edited by hand.
457
+ // Drives the managed-vs-native user-mgmt UI gating (the password /
458
+ // identity-edit actions hide for managed users, who hold no local
459
+ // credential) and is the marker SCIM lifecycle keys off. Owned by objectql
460
+ // (better-auth is oblivious to this column — like `ai_access`).
461
+ source: data.Field.select({
462
+ label: "Identity Source",
463
+ required: false,
464
+ readonly: true,
465
+ group: "System",
466
+ defaultValue: "env_native",
467
+ options: [
468
+ { label: "IdP-Provisioned", value: "idp_provisioned" },
469
+ { label: "Env-Native", value: "env_native" }
470
+ ],
471
+ 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."
472
+ }),
395
473
  id: data.Field.text({
396
474
  label: "User ID",
397
475
  required: true,
@@ -757,6 +835,16 @@ var SysAccount = data.ObjectSchema.create({
757
835
  label: "Password Hash",
758
836
  required: false,
759
837
  description: "Hashed password for email/password provider"
838
+ }),
839
+ // ADR-0069 D1 — bounded ring of previous password hashes (JSON array of
840
+ // strings), used to reject password reuse on change/reset. Maintained by
841
+ // the auth manager; never exposed in UI.
842
+ previous_password_hashes: data.Field.textarea({
843
+ label: "Previous Password Hashes",
844
+ required: false,
845
+ readonly: true,
846
+ hidden: true,
847
+ description: "JSON array of prior password hashes (bounded by password_history_count); reuse-prevention only. System-managed."
760
848
  })
761
849
  },
762
850
  indexes: [
@@ -822,7 +910,13 @@ var SysVerification = data.ObjectSchema.create({
822
910
  })
823
911
  },
824
912
  indexes: [
825
- { fields: ["value"], unique: true },
913
+ // `value` must NOT be unique. better-auth's oauth-provider stores OIDC
914
+ // authorization codes in this table with `value` = a JSON blob keyed by
915
+ // user+client+state, which can legitimately repeat. A UNIQUE constraint
916
+ // makes `/api/v1/auth/oauth2/authorize` fail (`UNIQUE constraint failed:
917
+ // sys_verification.value`) → 503, breaking cloud-as-IdP SSO entirely.
918
+ // better-auth keys verification lookups on `identifier`, not `value`.
919
+ { fields: ["value"], unique: false },
826
920
  { fields: ["identifier"], unique: false },
827
921
  { fields: ["expires_at"], unique: false }
828
922
  ],
@@ -890,6 +984,10 @@ var SysOrganization = data.ObjectSchema.create({
890
984
  recordIdParam: "organizationId",
891
985
  // better-auth `organization/update` nests editable fields under `data`.
892
986
  bodyShape: { wrap: "data" },
987
+ // Org-admin actions are multi-org-only; hide them in single-org for
988
+ // consistency with `create_organization` (the org list is empty there,
989
+ // but this also guards direct record-URL access).
990
+ visible: "features.multiOrgEnabled != false",
893
991
  successMessage: "Organization updated",
894
992
  refreshAfter: true,
895
993
  params: [
@@ -908,6 +1006,7 @@ var SysOrganization = data.ObjectSchema.create({
908
1006
  type: "api",
909
1007
  target: "/api/v1/auth/organization/delete",
910
1008
  recordIdParam: "organizationId",
1009
+ visible: "features.multiOrgEnabled != false",
911
1010
  confirmText: "Delete this organization? All members will lose access immediately. This cannot be undone.",
912
1011
  successMessage: "Organization deleted",
913
1012
  refreshAfter: true
@@ -926,6 +1025,7 @@ var SysOrganization = data.ObjectSchema.create({
926
1025
  type: "api",
927
1026
  target: "/api/v1/auth/organization/set-active",
928
1027
  recordIdParam: "organizationId",
1028
+ visible: "features.multiOrgEnabled != false",
929
1029
  successMessage: "Active organization switched",
930
1030
  refreshAfter: true
931
1031
  },
@@ -942,6 +1042,7 @@ var SysOrganization = data.ObjectSchema.create({
942
1042
  type: "api",
943
1043
  target: "/api/v1/auth/organization/leave",
944
1044
  recordIdParam: "organizationId",
1045
+ visible: "features.multiOrgEnabled != false",
945
1046
  confirmText: "Leave this organization? You will lose access to all of its resources.",
946
1047
  successMessage: "You have left the organization",
947
1048
  refreshAfter: true
@@ -963,6 +1064,7 @@ var SysOrganization = data.ObjectSchema.create({
963
1064
  type: "api",
964
1065
  target: "/api/v1/cloud/organizations/{id}/change-slug",
965
1066
  method: "POST",
1067
+ visible: "features.multiOrgEnabled != false",
966
1068
  confirmText: "Renaming the slug rewrites every platform subdomain for this org and parks the old slug for 90 days. Continue?",
967
1069
  successMessage: "Organization slug changed",
968
1070
  refreshAfter: true,
@@ -1061,7 +1163,10 @@ var SysMember = data.ObjectSchema.create({
1061
1163
  docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
1062
1164
  },
1063
1165
  description: "Organization membership records",
1064
- titleFormat: "{user_id} in {organization_id}",
1166
+ // Org-independent title: organization_id is null in single-org mode, so a
1167
+ // '{user_id} in {organization_id}' format renders "… in null". User + role
1168
+ // identifies the membership in both single- and multi-org deployments.
1169
+ titleFormat: "{user_id} ({role})",
1065
1170
  compactLayout: ["user_id", "organization_id", "role"],
1066
1171
  // Row-level actions: better-auth `organization/update-member-role` and
1067
1172
  // `organization/remove-member`. Generic CRUD is suppressed on better-auth
@@ -1082,6 +1187,11 @@ var SysMember = data.ObjectSchema.create({
1082
1187
  locations: ["list_toolbar"],
1083
1188
  type: "api",
1084
1189
  target: "/api/v1/auth/organization/add-member",
1190
+ // Org-membership mutations are multi-org-only: the better-auth
1191
+ // endpoints resolve an active org that does not exist in single-org
1192
+ // mode, so these actions would fail at the API. Gate every one on
1193
+ // the multi-org flag (mirrors sys_organization.create_organization).
1194
+ visible: "features.multiOrgEnabled != false",
1085
1195
  successMessage: "Member added",
1086
1196
  refreshAfter: true,
1087
1197
  params: [
@@ -1099,6 +1209,7 @@ var SysMember = data.ObjectSchema.create({
1099
1209
  type: "api",
1100
1210
  target: "/api/v1/auth/organization/update-member-role",
1101
1211
  recordIdParam: "memberId",
1212
+ visible: "features.multiOrgEnabled != false",
1102
1213
  successMessage: "Member role updated",
1103
1214
  refreshAfter: true,
1104
1215
  params: [
@@ -1115,6 +1226,7 @@ var SysMember = data.ObjectSchema.create({
1115
1226
  type: "api",
1116
1227
  target: "/api/v1/auth/organization/remove-member",
1117
1228
  recordIdParam: "memberIdOrEmail",
1229
+ visible: "features.multiOrgEnabled != false",
1118
1230
  confirmText: "Remove this member from the organization? They will lose access to all org resources.",
1119
1231
  successMessage: "Member removed",
1120
1232
  refreshAfter: true
@@ -1136,7 +1248,7 @@ var SysMember = data.ObjectSchema.create({
1136
1248
  target: "/api/v1/auth/organization/update-member-role",
1137
1249
  recordIdParam: "memberId",
1138
1250
  bodyExtra: { role: "owner" },
1139
- visible: "record.role != 'owner'",
1251
+ visible: "record.role != 'owner' && features.multiOrgEnabled != false",
1140
1252
  confirmText: "Transfer ownership of this organization to the selected member? You will be demoted to admin and lose owner-only privileges.",
1141
1253
  successMessage: "Ownership transferred",
1142
1254
  refreshAfter: true
@@ -1221,7 +1333,10 @@ var SysInvitation = data.ObjectSchema.create({
1221
1333
  docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
1222
1334
  },
1223
1335
  description: "Organization invitations for user onboarding",
1224
- titleFormat: "Invitation to {organization_id}",
1336
+ // Title by invitee email rather than organization_id: the latter is null in
1337
+ // single-org mode (renders "Invitation to null"), and the recipient email is
1338
+ // the more useful identifier in both modes anyway.
1339
+ titleFormat: "Invitation for {email}",
1225
1340
  compactLayout: ["email", "organization_id", "status"],
1226
1341
  // Custom actions — generic CRUD is suppressed (better-auth-managed).
1227
1342
  // Mirror the `invite_user` toolbar action from sys_user here so admins
@@ -1235,6 +1350,13 @@ var SysInvitation = data.ObjectSchema.create({
1235
1350
  locations: ["list_toolbar"],
1236
1351
  type: "api",
1237
1352
  target: "/api/v1/auth/organization/invite-member",
1353
+ // Inviting/managing invitations is a multi-org-only flow (the
1354
+ // endpoint resolves an active org absent in single-org mode). Gate
1355
+ // the admin-side actions on the multi-org flag (mirrors
1356
+ // sys_organization.create_organization). The recipient-side
1357
+ // accept/reject actions below stay record-gated — they are
1358
+ // unreachable in single-org anyway (no invitation rows exist).
1359
+ visible: "features.multiOrgEnabled != false",
1238
1360
  successMessage: "Invitation sent",
1239
1361
  refreshAfter: true,
1240
1362
  params: [
@@ -1252,6 +1374,7 @@ var SysInvitation = data.ObjectSchema.create({
1252
1374
  type: "api",
1253
1375
  target: "/api/v1/auth/organization/cancel-invitation",
1254
1376
  recordIdParam: "invitationId",
1377
+ visible: "features.multiOrgEnabled != false",
1255
1378
  confirmText: "Cancel this invitation? The recipient will no longer be able to accept it.",
1256
1379
  successMessage: "Invitation canceled",
1257
1380
  refreshAfter: true
@@ -1265,6 +1388,7 @@ var SysInvitation = data.ObjectSchema.create({
1265
1388
  type: "api",
1266
1389
  target: "/api/v1/auth/organization/invite-member",
1267
1390
  bodyExtra: { resend: true },
1391
+ visible: "features.multiOrgEnabled != false",
1268
1392
  successMessage: "Invitation resent",
1269
1393
  refreshAfter: true,
1270
1394
  params: [
@@ -1449,6 +1573,10 @@ var SysTeam = data.ObjectSchema.create({
1449
1573
  locations: ["list_toolbar"],
1450
1574
  type: "api",
1451
1575
  target: "/api/v1/auth/organization/create-team",
1576
+ // Teams are nested inside organizations — a multi-org-only concept.
1577
+ // Gate every team mutation on the multi-org flag so the affordances
1578
+ // disappear in single-org (mirrors sys_organization.create_organization).
1579
+ visible: "features.multiOrgEnabled != false",
1452
1580
  successMessage: "Team created",
1453
1581
  refreshAfter: true,
1454
1582
  params: [
@@ -1469,6 +1597,7 @@ var SysTeam = data.ObjectSchema.create({
1469
1597
  target: "/api/v1/auth/organization/update-team",
1470
1598
  recordIdParam: "teamId",
1471
1599
  bodyShape: { wrap: "data" },
1600
+ visible: "features.multiOrgEnabled != false",
1472
1601
  successMessage: "Team updated",
1473
1602
  refreshAfter: true,
1474
1603
  params: [
@@ -1487,6 +1616,7 @@ var SysTeam = data.ObjectSchema.create({
1487
1616
  type: "api",
1488
1617
  target: "/api/v1/auth/organization/remove-team",
1489
1618
  recordIdParam: "teamId",
1619
+ visible: "features.multiOrgEnabled != false",
1490
1620
  confirmText: "Delete this team? Members will lose any team-scoped access. This cannot be undone.",
1491
1621
  successMessage: "Team deleted",
1492
1622
  refreshAfter: true
@@ -1597,6 +1727,10 @@ var SysTeamMember = data.ObjectSchema.create({
1597
1727
  locations: ["list_toolbar"],
1598
1728
  type: "api",
1599
1729
  target: "/api/v1/auth/organization/add-team-member",
1730
+ // Team membership lives under organizations — multi-org-only. Gate
1731
+ // both mutations so they vanish in single-org (mirrors
1732
+ // sys_organization.create_organization).
1733
+ visible: "features.multiOrgEnabled != false",
1600
1734
  successMessage: "Team member added",
1601
1735
  refreshAfter: true,
1602
1736
  params: [
@@ -1617,6 +1751,7 @@ var SysTeamMember = data.ObjectSchema.create({
1617
1751
  locations: ["list_item"],
1618
1752
  type: "api",
1619
1753
  target: "/api/v1/auth/organization/remove-team-member",
1754
+ visible: "features.multiOrgEnabled != false",
1620
1755
  confirmText: "Remove this user from the team? They will lose any team-scoped access.",
1621
1756
  successMessage: "Team member removed",
1622
1757
  refreshAfter: true,
@@ -3156,6 +3291,224 @@ var SysJwks = data.ObjectSchema.create({
3156
3291
  mru: false
3157
3292
  }
3158
3293
  });
3294
+ var SysSsoProvider = data.ObjectSchema.create({
3295
+ name: "sys_sso_provider",
3296
+ label: "SSO Provider",
3297
+ pluralLabel: "SSO Providers",
3298
+ icon: "shield-check",
3299
+ isSystem: true,
3300
+ managedBy: "better-auth",
3301
+ // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
3302
+ protection: {
3303
+ lock: "full",
3304
+ reason: "Identity table managed by better-auth (@better-auth/sso) \u2014 see ADR-0024.",
3305
+ docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
3306
+ },
3307
+ description: "External SSO identity providers (OIDC / SAML) this environment federates login to",
3308
+ displayNameField: "provider_id",
3309
+ titleFormat: "{provider_id}",
3310
+ compactLayout: ["provider_id", "issuer", "domain"],
3311
+ // All mutations go through @better-auth/sso's endpoints under
3312
+ // /api/v1/auth/sso/* (register / delete-provider) rather than the generic
3313
+ // data layer, so server-side config validation + secret handling run.
3314
+ actions: [
3315
+ {
3316
+ name: "register_sso_provider",
3317
+ label: "Register SSO Provider",
3318
+ icon: "plus-circle",
3319
+ variant: "primary",
3320
+ mode: "create",
3321
+ locations: ["list_toolbar"],
3322
+ type: "api",
3323
+ method: "POST",
3324
+ target: "/api/v1/auth/sso/register",
3325
+ refreshAfter: true,
3326
+ params: [
3327
+ { name: "providerId", label: "Provider ID", type: "text", required: true, helpText: 'Stable identifier, e.g. "okta" or "acme-entra".' },
3328
+ { name: "issuer", label: "Issuer URL", type: "text", required: true, helpText: "IdP issuer / discovery base, e.g. https://acme.okta.com." },
3329
+ { name: "domain", label: "Email Domain", type: "text", required: true, helpText: "Users with this email domain are routed to this IdP." },
3330
+ { name: "clientId", label: "Client ID", type: "text", required: true },
3331
+ { name: "clientSecret", label: "Client Secret", type: "text", required: true }
3332
+ ]
3333
+ },
3334
+ {
3335
+ name: "delete_sso_provider",
3336
+ label: "Delete SSO Provider",
3337
+ icon: "trash-2",
3338
+ variant: "danger",
3339
+ mode: "delete",
3340
+ locations: ["list_item", "record_header"],
3341
+ type: "api",
3342
+ method: "POST",
3343
+ target: "/api/v1/auth/sso/delete-provider",
3344
+ confirmText: "Delete this SSO provider? Users from its domain will no longer be able to sign in through it.",
3345
+ successMessage: "SSO provider deleted",
3346
+ refreshAfter: true,
3347
+ params: [
3348
+ { name: "providerId", field: "provider_id", defaultFromRow: true, required: true }
3349
+ ]
3350
+ }
3351
+ ],
3352
+ listViews: {
3353
+ all: {
3354
+ type: "grid",
3355
+ name: "all",
3356
+ label: "All",
3357
+ data: { provider: "object", object: "sys_sso_provider" },
3358
+ columns: ["provider_id", "issuer", "domain", "created_at"],
3359
+ sort: [{ field: "provider_id", order: "asc" }],
3360
+ pagination: { pageSize: 50 }
3361
+ }
3362
+ },
3363
+ fields: {
3364
+ id: data.Field.text({ label: "ID", required: true, readonly: true, group: "System" }),
3365
+ provider_id: data.Field.text({
3366
+ label: "Provider ID",
3367
+ required: true,
3368
+ searchable: true,
3369
+ maxLength: 255,
3370
+ description: "Stable provider identifier (unique within the environment)",
3371
+ group: "Identity"
3372
+ }),
3373
+ issuer: data.Field.text({
3374
+ label: "Issuer",
3375
+ required: true,
3376
+ maxLength: 2048,
3377
+ description: "IdP issuer URL",
3378
+ group: "Identity"
3379
+ }),
3380
+ domain: data.Field.text({
3381
+ label: "Email Domain",
3382
+ required: true,
3383
+ maxLength: 255,
3384
+ description: "Email domain routed to this IdP (e.g. acme.com)",
3385
+ group: "Identity"
3386
+ }),
3387
+ oidc_config: data.Field.textarea({
3388
+ label: "OIDC Config",
3389
+ required: false,
3390
+ description: "JSON: clientId, clientSecret, endpoints, scopes, mapping, pkce (managed by better-auth)",
3391
+ group: "Protocol"
3392
+ }),
3393
+ saml_config: data.Field.textarea({
3394
+ label: "SAML Config",
3395
+ required: false,
3396
+ description: "JSON: entryPoint, cert, identifierFormat, mapping (managed by better-auth)",
3397
+ group: "Protocol"
3398
+ }),
3399
+ user_id: data.Field.lookup("sys_user", {
3400
+ label: "Registered By",
3401
+ required: false,
3402
+ description: "User who registered this provider",
3403
+ group: "System"
3404
+ }),
3405
+ organization_id: data.Field.text({
3406
+ label: "Organization",
3407
+ required: false,
3408
+ maxLength: 255,
3409
+ description: "Organization scope (when org-scoped SSO is used)",
3410
+ group: "System"
3411
+ }),
3412
+ created_at: data.Field.datetime({ label: "Created At", defaultValue: "NOW()", readonly: true, group: "System" }),
3413
+ updated_at: data.Field.datetime({ label: "Updated At", defaultValue: "NOW()", readonly: true, group: "System" })
3414
+ },
3415
+ indexes: [
3416
+ { fields: ["provider_id"], unique: true },
3417
+ { fields: ["domain"] },
3418
+ { fields: ["user_id"] }
3419
+ ],
3420
+ enable: {
3421
+ trackHistory: true,
3422
+ searchable: true,
3423
+ apiEnabled: true,
3424
+ // Mutations go through /api/v1/auth/sso/* (register / delete-provider);
3425
+ // the generic data layer is read-only so sysadmins cannot bypass
3426
+ // server-side validation / secret handling.
3427
+ apiMethods: ["get", "list"],
3428
+ trash: false,
3429
+ mru: false
3430
+ }
3431
+ });
3432
+ var SysScimProvider = data.ObjectSchema.create({
3433
+ name: "sys_scim_provider",
3434
+ label: "SCIM Provider",
3435
+ pluralLabel: "SCIM Providers",
3436
+ icon: "users",
3437
+ isSystem: true,
3438
+ managedBy: "better-auth",
3439
+ // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
3440
+ protection: {
3441
+ lock: "full",
3442
+ reason: "Identity table managed by better-auth (@better-auth/scim) \u2014 see ADR-0071.",
3443
+ docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
3444
+ },
3445
+ description: "SCIM 2.0 connections (bearer tokens) external IdPs use to provision/deprovision this environment's users",
3446
+ displayNameField: "provider_id",
3447
+ titleFormat: "{provider_id}",
3448
+ compactLayout: ["provider_id", "organization_id"],
3449
+ listViews: {
3450
+ all: {
3451
+ type: "grid",
3452
+ name: "all",
3453
+ label: "All",
3454
+ data: { provider: "object", object: "sys_scim_provider" },
3455
+ // scim_token is intentionally excluded — never surface the credential.
3456
+ columns: ["provider_id", "organization_id", "created_at"],
3457
+ sort: [{ field: "provider_id", order: "asc" }],
3458
+ pagination: { pageSize: 50 }
3459
+ }
3460
+ },
3461
+ fields: {
3462
+ id: data.Field.text({ label: "ID", required: true, readonly: true, group: "System" }),
3463
+ provider_id: data.Field.text({
3464
+ label: "Provider ID",
3465
+ required: true,
3466
+ searchable: true,
3467
+ maxLength: 255,
3468
+ description: 'Stable SCIM provider identifier (e.g. "okta-scim")',
3469
+ group: "Identity"
3470
+ }),
3471
+ scim_token: data.Field.text({
3472
+ label: "SCIM Token (hash)",
3473
+ required: false,
3474
+ readonly: true,
3475
+ maxLength: 1024,
3476
+ description: "Hashed bearer credential for this SCIM connection \u2014 the plaintext is shown once at generate-token. Sensitive; do not expose.",
3477
+ group: "Secret"
3478
+ }),
3479
+ organization_id: data.Field.text({
3480
+ label: "Organization",
3481
+ required: false,
3482
+ maxLength: 255,
3483
+ description: "Organization scope of this token (org-scoped tokens restrict provisioning to that org)",
3484
+ group: "System"
3485
+ }),
3486
+ user_id: data.Field.lookup("sys_user", {
3487
+ label: "Owned By",
3488
+ required: false,
3489
+ description: "User who generated this token (when provider-ownership is enabled)",
3490
+ group: "System"
3491
+ }),
3492
+ created_at: data.Field.datetime({ label: "Created At", defaultValue: "NOW()", readonly: true, group: "System" }),
3493
+ updated_at: data.Field.datetime({ label: "Updated At", defaultValue: "NOW()", readonly: true, group: "System" })
3494
+ },
3495
+ indexes: [
3496
+ { fields: ["provider_id"], unique: true },
3497
+ { fields: ["organization_id"] },
3498
+ { fields: ["user_id"] }
3499
+ ],
3500
+ enable: {
3501
+ trackHistory: true,
3502
+ searchable: false,
3503
+ apiEnabled: true,
3504
+ // Mutations + token issuance go through @better-auth/scim's endpoints
3505
+ // under /api/v1/auth/scim/*; the generic data layer is read-only so the
3506
+ // credential cannot be written/bypassed through it.
3507
+ apiMethods: ["list"],
3508
+ trash: false,
3509
+ mru: false
3510
+ }
3511
+ });
3159
3512
 
3160
3513
  exports.SysAccount = SysAccount;
3161
3514
  exports.SysApiKey = SysApiKey;
@@ -3170,7 +3523,9 @@ exports.SysOauthApplication = SysOauthApplication;
3170
3523
  exports.SysOauthConsent = SysOauthConsent;
3171
3524
  exports.SysOauthRefreshToken = SysOauthRefreshToken;
3172
3525
  exports.SysOrganization = SysOrganization;
3526
+ exports.SysScimProvider = SysScimProvider;
3173
3527
  exports.SysSession = SysSession;
3528
+ exports.SysSsoProvider = SysSsoProvider;
3174
3529
  exports.SysTeam = SysTeam;
3175
3530
  exports.SysTeamMember = SysTeamMember;
3176
3531
  exports.SysTwoFactor = SysTwoFactor;