@objectstack/platform-objects 10.2.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.
@@ -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,32 @@ 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
+ ai_access: Field.boolean({
424
+ label: "AI Access",
425
+ defaultValue: false,
426
+ group: "Admin",
427
+ 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)."
428
+ }),
373
429
  // ── Profile ──────────────────────────────────────────────────
374
430
  image: Field.url({
375
431
  label: "Profile Image",
@@ -390,6 +446,28 @@ var SysUser = ObjectSchema.create({
390
446
  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
447
  }),
392
448
  // ── System (auto-managed, hidden from create/edit forms) ─────
449
+ // Identity provenance (ADR-0024 D4). `idp_provisioned` users were
450
+ // JIT-created on first federated login (a `sys_account` exists for an
451
+ // external/OIDC provider — e.g. the cloud-as-IdP `objectstack-cloud`
452
+ // provider, or a customer's own IdP); `env_native` users registered
453
+ // locally (email/password) or are app end-users. Stamped automatically by
454
+ // the AuthManager `account.create.after` hook — never edited by hand.
455
+ // Drives the managed-vs-native user-mgmt UI gating (the password /
456
+ // identity-edit actions hide for managed users, who hold no local
457
+ // credential) and is the marker SCIM lifecycle keys off. Owned by objectql
458
+ // (better-auth is oblivious to this column — like `ai_access`).
459
+ source: Field.select({
460
+ label: "Identity Source",
461
+ required: false,
462
+ readonly: true,
463
+ group: "System",
464
+ defaultValue: "env_native",
465
+ options: [
466
+ { label: "IdP-Provisioned", value: "idp_provisioned" },
467
+ { label: "Env-Native", value: "env_native" }
468
+ ],
469
+ 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."
470
+ }),
393
471
  id: Field.text({
394
472
  label: "User ID",
395
473
  required: true,
@@ -755,6 +833,16 @@ var SysAccount = ObjectSchema.create({
755
833
  label: "Password Hash",
756
834
  required: false,
757
835
  description: "Hashed password for email/password provider"
836
+ }),
837
+ // ADR-0069 D1 — bounded ring of previous password hashes (JSON array of
838
+ // strings), used to reject password reuse on change/reset. Maintained by
839
+ // the auth manager; never exposed in UI.
840
+ previous_password_hashes: Field.textarea({
841
+ label: "Previous Password Hashes",
842
+ required: false,
843
+ readonly: true,
844
+ hidden: true,
845
+ description: "JSON array of prior password hashes (bounded by password_history_count); reuse-prevention only. System-managed."
758
846
  })
759
847
  },
760
848
  indexes: [
@@ -820,7 +908,13 @@ var SysVerification = ObjectSchema.create({
820
908
  })
821
909
  },
822
910
  indexes: [
823
- { fields: ["value"], unique: true },
911
+ // `value` must NOT be unique. better-auth's oauth-provider stores OIDC
912
+ // authorization codes in this table with `value` = a JSON blob keyed by
913
+ // user+client+state, which can legitimately repeat. A UNIQUE constraint
914
+ // makes `/api/v1/auth/oauth2/authorize` fail (`UNIQUE constraint failed:
915
+ // sys_verification.value`) → 503, breaking cloud-as-IdP SSO entirely.
916
+ // better-auth keys verification lookups on `identifier`, not `value`.
917
+ { fields: ["value"], unique: false },
824
918
  { fields: ["identifier"], unique: false },
825
919
  { fields: ["expires_at"], unique: false }
826
920
  ],
@@ -888,6 +982,10 @@ var SysOrganization = ObjectSchema.create({
888
982
  recordIdParam: "organizationId",
889
983
  // better-auth `organization/update` nests editable fields under `data`.
890
984
  bodyShape: { wrap: "data" },
985
+ // Org-admin actions are multi-org-only; hide them in single-org for
986
+ // consistency with `create_organization` (the org list is empty there,
987
+ // but this also guards direct record-URL access).
988
+ visible: "features.multiOrgEnabled != false",
891
989
  successMessage: "Organization updated",
892
990
  refreshAfter: true,
893
991
  params: [
@@ -906,6 +1004,7 @@ var SysOrganization = ObjectSchema.create({
906
1004
  type: "api",
907
1005
  target: "/api/v1/auth/organization/delete",
908
1006
  recordIdParam: "organizationId",
1007
+ visible: "features.multiOrgEnabled != false",
909
1008
  confirmText: "Delete this organization? All members will lose access immediately. This cannot be undone.",
910
1009
  successMessage: "Organization deleted",
911
1010
  refreshAfter: true
@@ -924,6 +1023,7 @@ var SysOrganization = ObjectSchema.create({
924
1023
  type: "api",
925
1024
  target: "/api/v1/auth/organization/set-active",
926
1025
  recordIdParam: "organizationId",
1026
+ visible: "features.multiOrgEnabled != false",
927
1027
  successMessage: "Active organization switched",
928
1028
  refreshAfter: true
929
1029
  },
@@ -940,6 +1040,7 @@ var SysOrganization = ObjectSchema.create({
940
1040
  type: "api",
941
1041
  target: "/api/v1/auth/organization/leave",
942
1042
  recordIdParam: "organizationId",
1043
+ visible: "features.multiOrgEnabled != false",
943
1044
  confirmText: "Leave this organization? You will lose access to all of its resources.",
944
1045
  successMessage: "You have left the organization",
945
1046
  refreshAfter: true
@@ -961,6 +1062,7 @@ var SysOrganization = ObjectSchema.create({
961
1062
  type: "api",
962
1063
  target: "/api/v1/cloud/organizations/{id}/change-slug",
963
1064
  method: "POST",
1065
+ visible: "features.multiOrgEnabled != false",
964
1066
  confirmText: "Renaming the slug rewrites every platform subdomain for this org and parks the old slug for 90 days. Continue?",
965
1067
  successMessage: "Organization slug changed",
966
1068
  refreshAfter: true,
@@ -1059,7 +1161,10 @@ var SysMember = ObjectSchema.create({
1059
1161
  docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
1060
1162
  },
1061
1163
  description: "Organization membership records",
1062
- titleFormat: "{user_id} in {organization_id}",
1164
+ // Org-independent title: organization_id is null in single-org mode, so a
1165
+ // '{user_id} in {organization_id}' format renders "… in null". User + role
1166
+ // identifies the membership in both single- and multi-org deployments.
1167
+ titleFormat: "{user_id} ({role})",
1063
1168
  compactLayout: ["user_id", "organization_id", "role"],
1064
1169
  // Row-level actions: better-auth `organization/update-member-role` and
1065
1170
  // `organization/remove-member`. Generic CRUD is suppressed on better-auth
@@ -1080,6 +1185,11 @@ var SysMember = ObjectSchema.create({
1080
1185
  locations: ["list_toolbar"],
1081
1186
  type: "api",
1082
1187
  target: "/api/v1/auth/organization/add-member",
1188
+ // Org-membership mutations are multi-org-only: the better-auth
1189
+ // endpoints resolve an active org that does not exist in single-org
1190
+ // mode, so these actions would fail at the API. Gate every one on
1191
+ // the multi-org flag (mirrors sys_organization.create_organization).
1192
+ visible: "features.multiOrgEnabled != false",
1083
1193
  successMessage: "Member added",
1084
1194
  refreshAfter: true,
1085
1195
  params: [
@@ -1097,6 +1207,7 @@ var SysMember = ObjectSchema.create({
1097
1207
  type: "api",
1098
1208
  target: "/api/v1/auth/organization/update-member-role",
1099
1209
  recordIdParam: "memberId",
1210
+ visible: "features.multiOrgEnabled != false",
1100
1211
  successMessage: "Member role updated",
1101
1212
  refreshAfter: true,
1102
1213
  params: [
@@ -1113,6 +1224,7 @@ var SysMember = ObjectSchema.create({
1113
1224
  type: "api",
1114
1225
  target: "/api/v1/auth/organization/remove-member",
1115
1226
  recordIdParam: "memberIdOrEmail",
1227
+ visible: "features.multiOrgEnabled != false",
1116
1228
  confirmText: "Remove this member from the organization? They will lose access to all org resources.",
1117
1229
  successMessage: "Member removed",
1118
1230
  refreshAfter: true
@@ -1134,7 +1246,7 @@ var SysMember = ObjectSchema.create({
1134
1246
  target: "/api/v1/auth/organization/update-member-role",
1135
1247
  recordIdParam: "memberId",
1136
1248
  bodyExtra: { role: "owner" },
1137
- visible: "record.role != 'owner'",
1249
+ visible: "record.role != 'owner' && features.multiOrgEnabled != false",
1138
1250
  confirmText: "Transfer ownership of this organization to the selected member? You will be demoted to admin and lose owner-only privileges.",
1139
1251
  successMessage: "Ownership transferred",
1140
1252
  refreshAfter: true
@@ -1219,7 +1331,10 @@ var SysInvitation = ObjectSchema.create({
1219
1331
  docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
1220
1332
  },
1221
1333
  description: "Organization invitations for user onboarding",
1222
- titleFormat: "Invitation to {organization_id}",
1334
+ // Title by invitee email rather than organization_id: the latter is null in
1335
+ // single-org mode (renders "Invitation to null"), and the recipient email is
1336
+ // the more useful identifier in both modes anyway.
1337
+ titleFormat: "Invitation for {email}",
1223
1338
  compactLayout: ["email", "organization_id", "status"],
1224
1339
  // Custom actions — generic CRUD is suppressed (better-auth-managed).
1225
1340
  // Mirror the `invite_user` toolbar action from sys_user here so admins
@@ -1233,6 +1348,13 @@ var SysInvitation = ObjectSchema.create({
1233
1348
  locations: ["list_toolbar"],
1234
1349
  type: "api",
1235
1350
  target: "/api/v1/auth/organization/invite-member",
1351
+ // Inviting/managing invitations is a multi-org-only flow (the
1352
+ // endpoint resolves an active org absent in single-org mode). Gate
1353
+ // the admin-side actions on the multi-org flag (mirrors
1354
+ // sys_organization.create_organization). The recipient-side
1355
+ // accept/reject actions below stay record-gated — they are
1356
+ // unreachable in single-org anyway (no invitation rows exist).
1357
+ visible: "features.multiOrgEnabled != false",
1236
1358
  successMessage: "Invitation sent",
1237
1359
  refreshAfter: true,
1238
1360
  params: [
@@ -1250,6 +1372,7 @@ var SysInvitation = ObjectSchema.create({
1250
1372
  type: "api",
1251
1373
  target: "/api/v1/auth/organization/cancel-invitation",
1252
1374
  recordIdParam: "invitationId",
1375
+ visible: "features.multiOrgEnabled != false",
1253
1376
  confirmText: "Cancel this invitation? The recipient will no longer be able to accept it.",
1254
1377
  successMessage: "Invitation canceled",
1255
1378
  refreshAfter: true
@@ -1263,6 +1386,7 @@ var SysInvitation = ObjectSchema.create({
1263
1386
  type: "api",
1264
1387
  target: "/api/v1/auth/organization/invite-member",
1265
1388
  bodyExtra: { resend: true },
1389
+ visible: "features.multiOrgEnabled != false",
1266
1390
  successMessage: "Invitation resent",
1267
1391
  refreshAfter: true,
1268
1392
  params: [
@@ -1447,6 +1571,10 @@ var SysTeam = ObjectSchema.create({
1447
1571
  locations: ["list_toolbar"],
1448
1572
  type: "api",
1449
1573
  target: "/api/v1/auth/organization/create-team",
1574
+ // Teams are nested inside organizations — a multi-org-only concept.
1575
+ // Gate every team mutation on the multi-org flag so the affordances
1576
+ // disappear in single-org (mirrors sys_organization.create_organization).
1577
+ visible: "features.multiOrgEnabled != false",
1450
1578
  successMessage: "Team created",
1451
1579
  refreshAfter: true,
1452
1580
  params: [
@@ -1467,6 +1595,7 @@ var SysTeam = ObjectSchema.create({
1467
1595
  target: "/api/v1/auth/organization/update-team",
1468
1596
  recordIdParam: "teamId",
1469
1597
  bodyShape: { wrap: "data" },
1598
+ visible: "features.multiOrgEnabled != false",
1470
1599
  successMessage: "Team updated",
1471
1600
  refreshAfter: true,
1472
1601
  params: [
@@ -1485,6 +1614,7 @@ var SysTeam = ObjectSchema.create({
1485
1614
  type: "api",
1486
1615
  target: "/api/v1/auth/organization/remove-team",
1487
1616
  recordIdParam: "teamId",
1617
+ visible: "features.multiOrgEnabled != false",
1488
1618
  confirmText: "Delete this team? Members will lose any team-scoped access. This cannot be undone.",
1489
1619
  successMessage: "Team deleted",
1490
1620
  refreshAfter: true
@@ -1595,6 +1725,10 @@ var SysTeamMember = ObjectSchema.create({
1595
1725
  locations: ["list_toolbar"],
1596
1726
  type: "api",
1597
1727
  target: "/api/v1/auth/organization/add-team-member",
1728
+ // Team membership lives under organizations — multi-org-only. Gate
1729
+ // both mutations so they vanish in single-org (mirrors
1730
+ // sys_organization.create_organization).
1731
+ visible: "features.multiOrgEnabled != false",
1598
1732
  successMessage: "Team member added",
1599
1733
  refreshAfter: true,
1600
1734
  params: [
@@ -1615,6 +1749,7 @@ var SysTeamMember = ObjectSchema.create({
1615
1749
  locations: ["list_item"],
1616
1750
  type: "api",
1617
1751
  target: "/api/v1/auth/organization/remove-team-member",
1752
+ visible: "features.multiOrgEnabled != false",
1618
1753
  confirmText: "Remove this user from the team? They will lose any team-scoped access.",
1619
1754
  successMessage: "Team member removed",
1620
1755
  refreshAfter: true,
@@ -3154,7 +3289,225 @@ var SysJwks = ObjectSchema.create({
3154
3289
  mru: false
3155
3290
  }
3156
3291
  });
3292
+ var SysSsoProvider = ObjectSchema.create({
3293
+ name: "sys_sso_provider",
3294
+ label: "SSO Provider",
3295
+ pluralLabel: "SSO Providers",
3296
+ icon: "shield-check",
3297
+ isSystem: true,
3298
+ managedBy: "better-auth",
3299
+ // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
3300
+ protection: {
3301
+ lock: "full",
3302
+ reason: "Identity table managed by better-auth (@better-auth/sso) \u2014 see ADR-0024.",
3303
+ docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
3304
+ },
3305
+ description: "External SSO identity providers (OIDC / SAML) this environment federates login to",
3306
+ displayNameField: "provider_id",
3307
+ titleFormat: "{provider_id}",
3308
+ compactLayout: ["provider_id", "issuer", "domain"],
3309
+ // All mutations go through @better-auth/sso's endpoints under
3310
+ // /api/v1/auth/sso/* (register / delete-provider) rather than the generic
3311
+ // data layer, so server-side config validation + secret handling run.
3312
+ actions: [
3313
+ {
3314
+ name: "register_sso_provider",
3315
+ label: "Register SSO Provider",
3316
+ icon: "plus-circle",
3317
+ variant: "primary",
3318
+ mode: "create",
3319
+ locations: ["list_toolbar"],
3320
+ type: "api",
3321
+ method: "POST",
3322
+ target: "/api/v1/auth/sso/register",
3323
+ refreshAfter: true,
3324
+ params: [
3325
+ { name: "providerId", label: "Provider ID", type: "text", required: true, helpText: 'Stable identifier, e.g. "okta" or "acme-entra".' },
3326
+ { name: "issuer", label: "Issuer URL", type: "text", required: true, helpText: "IdP issuer / discovery base, e.g. https://acme.okta.com." },
3327
+ { name: "domain", label: "Email Domain", type: "text", required: true, helpText: "Users with this email domain are routed to this IdP." },
3328
+ { name: "clientId", label: "Client ID", type: "text", required: true },
3329
+ { name: "clientSecret", label: "Client Secret", type: "text", required: true }
3330
+ ]
3331
+ },
3332
+ {
3333
+ name: "delete_sso_provider",
3334
+ label: "Delete SSO Provider",
3335
+ icon: "trash-2",
3336
+ variant: "danger",
3337
+ mode: "delete",
3338
+ locations: ["list_item", "record_header"],
3339
+ type: "api",
3340
+ method: "POST",
3341
+ target: "/api/v1/auth/sso/delete-provider",
3342
+ confirmText: "Delete this SSO provider? Users from its domain will no longer be able to sign in through it.",
3343
+ successMessage: "SSO provider deleted",
3344
+ refreshAfter: true,
3345
+ params: [
3346
+ { name: "providerId", field: "provider_id", defaultFromRow: true, required: true }
3347
+ ]
3348
+ }
3349
+ ],
3350
+ listViews: {
3351
+ all: {
3352
+ type: "grid",
3353
+ name: "all",
3354
+ label: "All",
3355
+ data: { provider: "object", object: "sys_sso_provider" },
3356
+ columns: ["provider_id", "issuer", "domain", "created_at"],
3357
+ sort: [{ field: "provider_id", order: "asc" }],
3358
+ pagination: { pageSize: 50 }
3359
+ }
3360
+ },
3361
+ fields: {
3362
+ id: Field.text({ label: "ID", required: true, readonly: true, group: "System" }),
3363
+ provider_id: Field.text({
3364
+ label: "Provider ID",
3365
+ required: true,
3366
+ searchable: true,
3367
+ maxLength: 255,
3368
+ description: "Stable provider identifier (unique within the environment)",
3369
+ group: "Identity"
3370
+ }),
3371
+ issuer: Field.text({
3372
+ label: "Issuer",
3373
+ required: true,
3374
+ maxLength: 2048,
3375
+ description: "IdP issuer URL",
3376
+ group: "Identity"
3377
+ }),
3378
+ domain: Field.text({
3379
+ label: "Email Domain",
3380
+ required: true,
3381
+ maxLength: 255,
3382
+ description: "Email domain routed to this IdP (e.g. acme.com)",
3383
+ group: "Identity"
3384
+ }),
3385
+ oidc_config: Field.textarea({
3386
+ label: "OIDC Config",
3387
+ required: false,
3388
+ description: "JSON: clientId, clientSecret, endpoints, scopes, mapping, pkce (managed by better-auth)",
3389
+ group: "Protocol"
3390
+ }),
3391
+ saml_config: Field.textarea({
3392
+ label: "SAML Config",
3393
+ required: false,
3394
+ description: "JSON: entryPoint, cert, identifierFormat, mapping (managed by better-auth)",
3395
+ group: "Protocol"
3396
+ }),
3397
+ user_id: Field.lookup("sys_user", {
3398
+ label: "Registered By",
3399
+ required: false,
3400
+ description: "User who registered this provider",
3401
+ group: "System"
3402
+ }),
3403
+ organization_id: Field.text({
3404
+ label: "Organization",
3405
+ required: false,
3406
+ maxLength: 255,
3407
+ description: "Organization scope (when org-scoped SSO is used)",
3408
+ group: "System"
3409
+ }),
3410
+ created_at: Field.datetime({ label: "Created At", defaultValue: "NOW()", readonly: true, group: "System" }),
3411
+ updated_at: Field.datetime({ label: "Updated At", defaultValue: "NOW()", readonly: true, group: "System" })
3412
+ },
3413
+ indexes: [
3414
+ { fields: ["provider_id"], unique: true },
3415
+ { fields: ["domain"] },
3416
+ { fields: ["user_id"] }
3417
+ ],
3418
+ enable: {
3419
+ trackHistory: true,
3420
+ searchable: true,
3421
+ apiEnabled: true,
3422
+ // Mutations go through /api/v1/auth/sso/* (register / delete-provider);
3423
+ // the generic data layer is read-only so sysadmins cannot bypass
3424
+ // server-side validation / secret handling.
3425
+ apiMethods: ["get", "list"],
3426
+ trash: false,
3427
+ mru: false
3428
+ }
3429
+ });
3430
+ var SysScimProvider = ObjectSchema.create({
3431
+ name: "sys_scim_provider",
3432
+ label: "SCIM Provider",
3433
+ pluralLabel: "SCIM Providers",
3434
+ icon: "users",
3435
+ isSystem: true,
3436
+ managedBy: "better-auth",
3437
+ // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
3438
+ protection: {
3439
+ lock: "full",
3440
+ reason: "Identity table managed by better-auth (@better-auth/scim) \u2014 see ADR-0071.",
3441
+ docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
3442
+ },
3443
+ description: "SCIM 2.0 connections (bearer tokens) external IdPs use to provision/deprovision this environment's users",
3444
+ displayNameField: "provider_id",
3445
+ titleFormat: "{provider_id}",
3446
+ compactLayout: ["provider_id", "organization_id"],
3447
+ listViews: {
3448
+ all: {
3449
+ type: "grid",
3450
+ name: "all",
3451
+ label: "All",
3452
+ data: { provider: "object", object: "sys_scim_provider" },
3453
+ // scim_token is intentionally excluded — never surface the credential.
3454
+ columns: ["provider_id", "organization_id", "created_at"],
3455
+ sort: [{ field: "provider_id", order: "asc" }],
3456
+ pagination: { pageSize: 50 }
3457
+ }
3458
+ },
3459
+ fields: {
3460
+ id: Field.text({ label: "ID", required: true, readonly: true, group: "System" }),
3461
+ provider_id: Field.text({
3462
+ label: "Provider ID",
3463
+ required: true,
3464
+ searchable: true,
3465
+ maxLength: 255,
3466
+ description: 'Stable SCIM provider identifier (e.g. "okta-scim")',
3467
+ group: "Identity"
3468
+ }),
3469
+ scim_token: Field.text({
3470
+ label: "SCIM Token (hash)",
3471
+ required: false,
3472
+ readonly: true,
3473
+ maxLength: 1024,
3474
+ description: "Hashed bearer credential for this SCIM connection \u2014 the plaintext is shown once at generate-token. Sensitive; do not expose.",
3475
+ group: "Secret"
3476
+ }),
3477
+ organization_id: Field.text({
3478
+ label: "Organization",
3479
+ required: false,
3480
+ maxLength: 255,
3481
+ description: "Organization scope of this token (org-scoped tokens restrict provisioning to that org)",
3482
+ group: "System"
3483
+ }),
3484
+ user_id: Field.lookup("sys_user", {
3485
+ label: "Owned By",
3486
+ required: false,
3487
+ description: "User who generated this token (when provider-ownership is enabled)",
3488
+ group: "System"
3489
+ }),
3490
+ created_at: Field.datetime({ label: "Created At", defaultValue: "NOW()", readonly: true, group: "System" }),
3491
+ updated_at: Field.datetime({ label: "Updated At", defaultValue: "NOW()", readonly: true, group: "System" })
3492
+ },
3493
+ indexes: [
3494
+ { fields: ["provider_id"], unique: true },
3495
+ { fields: ["organization_id"] },
3496
+ { fields: ["user_id"] }
3497
+ ],
3498
+ enable: {
3499
+ trackHistory: true,
3500
+ searchable: false,
3501
+ apiEnabled: true,
3502
+ // Mutations + token issuance go through @better-auth/scim's endpoints
3503
+ // under /api/v1/auth/scim/*; the generic data layer is read-only so the
3504
+ // credential cannot be written/bypassed through it.
3505
+ apiMethods: ["list"],
3506
+ trash: false,
3507
+ mru: false
3508
+ }
3509
+ });
3157
3510
 
3158
- export { SysAccount, SysApiKey, SysBusinessUnit, SysBusinessUnitMember, SysDeviceCode, SysInvitation, SysJwks, SysMember, SysOauthAccessToken, SysOauthApplication, SysOauthConsent, SysOauthRefreshToken, SysOrganization, SysSession, SysTeam, SysTeamMember, SysTwoFactor, SysUser, SysUserPreference, SysVerification };
3511
+ export { SysAccount, SysApiKey, SysBusinessUnit, SysBusinessUnitMember, SysDeviceCode, SysInvitation, SysJwks, SysMember, SysOauthAccessToken, SysOauthApplication, SysOauthConsent, SysOauthRefreshToken, SysOrganization, SysScimProvider, SysSession, SysSsoProvider, SysTeam, SysTeamMember, SysTwoFactor, SysUser, SysUserPreference, SysVerification };
3159
3512
  //# sourceMappingURL=index.mjs.map
3160
3513
  //# sourceMappingURL=index.mjs.map