@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.
package/dist/index.js CHANGED
@@ -37,6 +37,13 @@ var SysUser = data.ObjectSchema.create({
37
37
  locations: ["list_toolbar"],
38
38
  type: "api",
39
39
  target: "/api/v1/auth/organization/invite-member",
40
+ // Org invitations are a multi-org-only flow (the endpoint resolves
41
+ // an active org that does not exist in single-org mode). Hide the
42
+ // affordance unless multi-org is enabled — matching the
43
+ // `create_organization` gate on sys_organization. This action is the
44
+ // most exposed of the set because the Users list is always reachable
45
+ // in single-org, unlike the org/membership lists.
46
+ visible: "features.multiOrgEnabled != false",
40
47
  successMessage: "Invitation sent",
41
48
  refreshAfter: true,
42
49
  params: [
@@ -80,6 +87,21 @@ var SysUser = data.ObjectSchema.create({
80
87
  successMessage: "User unbanned",
81
88
  refreshAfter: true
82
89
  },
90
+ {
91
+ // ADR-0069 D2 — clear a brute-force lockout early (locked_until auto-
92
+ // expires, but an admin can release a user immediately). Hits the
93
+ // plugin-auth custom route, which is admin-guarded server-side.
94
+ name: "unlock_user",
95
+ label: "Unlock Account",
96
+ icon: "lock-open",
97
+ variant: "secondary",
98
+ locations: ["list_item"],
99
+ type: "api",
100
+ target: "/api/v1/auth/admin/unlock-user",
101
+ recordIdParam: "userId",
102
+ successMessage: "Account unlocked",
103
+ refreshAfter: true
104
+ },
83
105
  {
84
106
  name: "set_user_password",
85
107
  label: "Set Password",
@@ -156,7 +178,11 @@ var SysUser = data.ObjectSchema.create({
156
178
  locations: ["record_header", "record_more", "record_section"],
157
179
  type: "api",
158
180
  target: "/api/v1/auth/change-password",
159
- visible: "record.id == ctx.user.id",
181
+ // Managed (IdP-provisioned) users hold no local credential — hide the
182
+ // password form so they can't self-mint a password that bypasses
183
+ // enforced SSO. The break-glass owner (env-native, or flipped back when
184
+ // their break-glass password is set) keeps it. ADR-0024 D4/D5.2.
185
+ visible: 'record.id == ctx.user.id && record.source != "idp_provisioned"',
160
186
  successMessage: "Password changed",
161
187
  refreshAfter: false,
162
188
  params: [
@@ -173,7 +199,9 @@ var SysUser = data.ObjectSchema.create({
173
199
  locations: ["record_header", "record_more", "record_section"],
174
200
  type: "api",
175
201
  target: "/api/v1/auth/change-email",
176
- visible: "record.id == ctx.user.id",
202
+ // A managed user's email is owned by the IdP — a local change would
203
+ // desync. Hide for IdP-provisioned; env-native users keep it.
204
+ visible: 'record.id == ctx.user.id && record.source != "idp_provisioned"',
177
205
  successMessage: "Verification email sent \u2014 check the new address to confirm.",
178
206
  refreshAfter: false,
179
207
  params: [
@@ -204,7 +232,9 @@ var SysUser = data.ObjectSchema.create({
204
232
  locations: ["record_more", "record_section"],
205
233
  type: "api",
206
234
  target: "/api/v1/auth/delete-user",
207
- visible: "record.id == ctx.user.id",
235
+ // Self-delete needs a local password; managed users are deprovisioned
236
+ // via the IdP (org-removal / SCIM), not local self-service. Hide for them.
237
+ visible: 'record.id == ctx.user.id && record.source != "idp_provisioned"',
208
238
  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.",
209
239
  successMessage: "Account deleted",
210
240
  refreshAfter: false,
@@ -287,7 +317,7 @@ var SysUser = data.ObjectSchema.create({
287
317
  name: "all_users",
288
318
  label: "All Users",
289
319
  data: { provider: "object", object: "sys_user" },
290
- columns: ["name", "email", "email_verified", "two_factor_enabled", "created_at"],
320
+ columns: ["name", "email", "email_verified", "source", "two_factor_enabled", "created_at"],
291
321
  sort: [{ field: "name", order: "asc" }],
292
322
  pagination: { pageSize: 50 }
293
323
  },
@@ -374,6 +404,32 @@ var SysUser = data.ObjectSchema.create({
374
404
  group: "Admin",
375
405
  description: "When set, the ban auto-clears at this time."
376
406
  }),
407
+ // ── Anti-brute-force (ADR-0069 D2) — owned by objectql, better-auth is
408
+ // oblivious. The auth manager's sign-in hooks maintain these: failures
409
+ // increment the counter; crossing `lockout_threshold` stamps
410
+ // `locked_until`; a successful sign-in resets both. Admins can clear
411
+ // them early via the Unlock action.
412
+ failed_login_count: data.Field.number({
413
+ label: "Failed Login Count",
414
+ required: false,
415
+ defaultValue: 0,
416
+ readonly: true,
417
+ group: "Admin",
418
+ description: "Consecutive failed sign-in attempts; reset to 0 on success. Maintained by the auth manager."
419
+ }),
420
+ locked_until: data.Field.datetime({
421
+ label: "Locked Until",
422
+ required: false,
423
+ readonly: true,
424
+ group: "Admin",
425
+ 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."
426
+ }),
427
+ ai_access: data.Field.boolean({
428
+ label: "AI Access",
429
+ defaultValue: false,
430
+ group: "Admin",
431
+ 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)."
432
+ }),
377
433
  // ── Profile ──────────────────────────────────────────────────
378
434
  image: data.Field.url({
379
435
  label: "Profile Image",
@@ -394,6 +450,28 @@ var SysUser = data.ObjectSchema.create({
394
450
  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."
395
451
  }),
396
452
  // ── System (auto-managed, hidden from create/edit forms) ─────
453
+ // Identity provenance (ADR-0024 D4). `idp_provisioned` users were
454
+ // JIT-created on first federated login (a `sys_account` exists for an
455
+ // external/OIDC provider — e.g. the cloud-as-IdP `objectstack-cloud`
456
+ // provider, or a customer's own IdP); `env_native` users registered
457
+ // locally (email/password) or are app end-users. Stamped automatically by
458
+ // the AuthManager `account.create.after` hook — never edited by hand.
459
+ // Drives the managed-vs-native user-mgmt UI gating (the password /
460
+ // identity-edit actions hide for managed users, who hold no local
461
+ // credential) and is the marker SCIM lifecycle keys off. Owned by objectql
462
+ // (better-auth is oblivious to this column — like `ai_access`).
463
+ source: data.Field.select({
464
+ label: "Identity Source",
465
+ required: false,
466
+ readonly: true,
467
+ group: "System",
468
+ defaultValue: "env_native",
469
+ options: [
470
+ { label: "IdP-Provisioned", value: "idp_provisioned" },
471
+ { label: "Env-Native", value: "env_native" }
472
+ ],
473
+ 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."
474
+ }),
397
475
  id: data.Field.text({
398
476
  label: "User ID",
399
477
  required: true,
@@ -759,6 +837,16 @@ var SysAccount = data.ObjectSchema.create({
759
837
  label: "Password Hash",
760
838
  required: false,
761
839
  description: "Hashed password for email/password provider"
840
+ }),
841
+ // ADR-0069 D1 — bounded ring of previous password hashes (JSON array of
842
+ // strings), used to reject password reuse on change/reset. Maintained by
843
+ // the auth manager; never exposed in UI.
844
+ previous_password_hashes: data.Field.textarea({
845
+ label: "Previous Password Hashes",
846
+ required: false,
847
+ readonly: true,
848
+ hidden: true,
849
+ description: "JSON array of prior password hashes (bounded by password_history_count); reuse-prevention only. System-managed."
762
850
  })
763
851
  },
764
852
  indexes: [
@@ -824,7 +912,13 @@ var SysVerification = data.ObjectSchema.create({
824
912
  })
825
913
  },
826
914
  indexes: [
827
- { fields: ["value"], unique: true },
915
+ // `value` must NOT be unique. better-auth's oauth-provider stores OIDC
916
+ // authorization codes in this table with `value` = a JSON blob keyed by
917
+ // user+client+state, which can legitimately repeat. A UNIQUE constraint
918
+ // makes `/api/v1/auth/oauth2/authorize` fail (`UNIQUE constraint failed:
919
+ // sys_verification.value`) → 503, breaking cloud-as-IdP SSO entirely.
920
+ // better-auth keys verification lookups on `identifier`, not `value`.
921
+ { fields: ["value"], unique: false },
828
922
  { fields: ["identifier"], unique: false },
829
923
  { fields: ["expires_at"], unique: false }
830
924
  ],
@@ -892,6 +986,10 @@ var SysOrganization = data.ObjectSchema.create({
892
986
  recordIdParam: "organizationId",
893
987
  // better-auth `organization/update` nests editable fields under `data`.
894
988
  bodyShape: { wrap: "data" },
989
+ // Org-admin actions are multi-org-only; hide them in single-org for
990
+ // consistency with `create_organization` (the org list is empty there,
991
+ // but this also guards direct record-URL access).
992
+ visible: "features.multiOrgEnabled != false",
895
993
  successMessage: "Organization updated",
896
994
  refreshAfter: true,
897
995
  params: [
@@ -910,6 +1008,7 @@ var SysOrganization = data.ObjectSchema.create({
910
1008
  type: "api",
911
1009
  target: "/api/v1/auth/organization/delete",
912
1010
  recordIdParam: "organizationId",
1011
+ visible: "features.multiOrgEnabled != false",
913
1012
  confirmText: "Delete this organization? All members will lose access immediately. This cannot be undone.",
914
1013
  successMessage: "Organization deleted",
915
1014
  refreshAfter: true
@@ -928,6 +1027,7 @@ var SysOrganization = data.ObjectSchema.create({
928
1027
  type: "api",
929
1028
  target: "/api/v1/auth/organization/set-active",
930
1029
  recordIdParam: "organizationId",
1030
+ visible: "features.multiOrgEnabled != false",
931
1031
  successMessage: "Active organization switched",
932
1032
  refreshAfter: true
933
1033
  },
@@ -944,6 +1044,7 @@ var SysOrganization = data.ObjectSchema.create({
944
1044
  type: "api",
945
1045
  target: "/api/v1/auth/organization/leave",
946
1046
  recordIdParam: "organizationId",
1047
+ visible: "features.multiOrgEnabled != false",
947
1048
  confirmText: "Leave this organization? You will lose access to all of its resources.",
948
1049
  successMessage: "You have left the organization",
949
1050
  refreshAfter: true
@@ -965,6 +1066,7 @@ var SysOrganization = data.ObjectSchema.create({
965
1066
  type: "api",
966
1067
  target: "/api/v1/cloud/organizations/{id}/change-slug",
967
1068
  method: "POST",
1069
+ visible: "features.multiOrgEnabled != false",
968
1070
  confirmText: "Renaming the slug rewrites every platform subdomain for this org and parks the old slug for 90 days. Continue?",
969
1071
  successMessage: "Organization slug changed",
970
1072
  refreshAfter: true,
@@ -1063,7 +1165,10 @@ var SysMember = data.ObjectSchema.create({
1063
1165
  docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
1064
1166
  },
1065
1167
  description: "Organization membership records",
1066
- titleFormat: "{user_id} in {organization_id}",
1168
+ // Org-independent title: organization_id is null in single-org mode, so a
1169
+ // '{user_id} in {organization_id}' format renders "… in null". User + role
1170
+ // identifies the membership in both single- and multi-org deployments.
1171
+ titleFormat: "{user_id} ({role})",
1067
1172
  compactLayout: ["user_id", "organization_id", "role"],
1068
1173
  // Row-level actions: better-auth `organization/update-member-role` and
1069
1174
  // `organization/remove-member`. Generic CRUD is suppressed on better-auth
@@ -1084,6 +1189,11 @@ var SysMember = data.ObjectSchema.create({
1084
1189
  locations: ["list_toolbar"],
1085
1190
  type: "api",
1086
1191
  target: "/api/v1/auth/organization/add-member",
1192
+ // Org-membership mutations are multi-org-only: the better-auth
1193
+ // endpoints resolve an active org that does not exist in single-org
1194
+ // mode, so these actions would fail at the API. Gate every one on
1195
+ // the multi-org flag (mirrors sys_organization.create_organization).
1196
+ visible: "features.multiOrgEnabled != false",
1087
1197
  successMessage: "Member added",
1088
1198
  refreshAfter: true,
1089
1199
  params: [
@@ -1101,6 +1211,7 @@ var SysMember = data.ObjectSchema.create({
1101
1211
  type: "api",
1102
1212
  target: "/api/v1/auth/organization/update-member-role",
1103
1213
  recordIdParam: "memberId",
1214
+ visible: "features.multiOrgEnabled != false",
1104
1215
  successMessage: "Member role updated",
1105
1216
  refreshAfter: true,
1106
1217
  params: [
@@ -1117,6 +1228,7 @@ var SysMember = data.ObjectSchema.create({
1117
1228
  type: "api",
1118
1229
  target: "/api/v1/auth/organization/remove-member",
1119
1230
  recordIdParam: "memberIdOrEmail",
1231
+ visible: "features.multiOrgEnabled != false",
1120
1232
  confirmText: "Remove this member from the organization? They will lose access to all org resources.",
1121
1233
  successMessage: "Member removed",
1122
1234
  refreshAfter: true
@@ -1138,7 +1250,7 @@ var SysMember = data.ObjectSchema.create({
1138
1250
  target: "/api/v1/auth/organization/update-member-role",
1139
1251
  recordIdParam: "memberId",
1140
1252
  bodyExtra: { role: "owner" },
1141
- visible: "record.role != 'owner'",
1253
+ visible: "record.role != 'owner' && features.multiOrgEnabled != false",
1142
1254
  confirmText: "Transfer ownership of this organization to the selected member? You will be demoted to admin and lose owner-only privileges.",
1143
1255
  successMessage: "Ownership transferred",
1144
1256
  refreshAfter: true
@@ -1223,7 +1335,10 @@ var SysInvitation = data.ObjectSchema.create({
1223
1335
  docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
1224
1336
  },
1225
1337
  description: "Organization invitations for user onboarding",
1226
- titleFormat: "Invitation to {organization_id}",
1338
+ // Title by invitee email rather than organization_id: the latter is null in
1339
+ // single-org mode (renders "Invitation to null"), and the recipient email is
1340
+ // the more useful identifier in both modes anyway.
1341
+ titleFormat: "Invitation for {email}",
1227
1342
  compactLayout: ["email", "organization_id", "status"],
1228
1343
  // Custom actions — generic CRUD is suppressed (better-auth-managed).
1229
1344
  // Mirror the `invite_user` toolbar action from sys_user here so admins
@@ -1237,6 +1352,13 @@ var SysInvitation = data.ObjectSchema.create({
1237
1352
  locations: ["list_toolbar"],
1238
1353
  type: "api",
1239
1354
  target: "/api/v1/auth/organization/invite-member",
1355
+ // Inviting/managing invitations is a multi-org-only flow (the
1356
+ // endpoint resolves an active org absent in single-org mode). Gate
1357
+ // the admin-side actions on the multi-org flag (mirrors
1358
+ // sys_organization.create_organization). The recipient-side
1359
+ // accept/reject actions below stay record-gated — they are
1360
+ // unreachable in single-org anyway (no invitation rows exist).
1361
+ visible: "features.multiOrgEnabled != false",
1240
1362
  successMessage: "Invitation sent",
1241
1363
  refreshAfter: true,
1242
1364
  params: [
@@ -1254,6 +1376,7 @@ var SysInvitation = data.ObjectSchema.create({
1254
1376
  type: "api",
1255
1377
  target: "/api/v1/auth/organization/cancel-invitation",
1256
1378
  recordIdParam: "invitationId",
1379
+ visible: "features.multiOrgEnabled != false",
1257
1380
  confirmText: "Cancel this invitation? The recipient will no longer be able to accept it.",
1258
1381
  successMessage: "Invitation canceled",
1259
1382
  refreshAfter: true
@@ -1267,6 +1390,7 @@ var SysInvitation = data.ObjectSchema.create({
1267
1390
  type: "api",
1268
1391
  target: "/api/v1/auth/organization/invite-member",
1269
1392
  bodyExtra: { resend: true },
1393
+ visible: "features.multiOrgEnabled != false",
1270
1394
  successMessage: "Invitation resent",
1271
1395
  refreshAfter: true,
1272
1396
  params: [
@@ -1451,6 +1575,10 @@ var SysTeam = data.ObjectSchema.create({
1451
1575
  locations: ["list_toolbar"],
1452
1576
  type: "api",
1453
1577
  target: "/api/v1/auth/organization/create-team",
1578
+ // Teams are nested inside organizations — a multi-org-only concept.
1579
+ // Gate every team mutation on the multi-org flag so the affordances
1580
+ // disappear in single-org (mirrors sys_organization.create_organization).
1581
+ visible: "features.multiOrgEnabled != false",
1454
1582
  successMessage: "Team created",
1455
1583
  refreshAfter: true,
1456
1584
  params: [
@@ -1471,6 +1599,7 @@ var SysTeam = data.ObjectSchema.create({
1471
1599
  target: "/api/v1/auth/organization/update-team",
1472
1600
  recordIdParam: "teamId",
1473
1601
  bodyShape: { wrap: "data" },
1602
+ visible: "features.multiOrgEnabled != false",
1474
1603
  successMessage: "Team updated",
1475
1604
  refreshAfter: true,
1476
1605
  params: [
@@ -1489,6 +1618,7 @@ var SysTeam = data.ObjectSchema.create({
1489
1618
  type: "api",
1490
1619
  target: "/api/v1/auth/organization/remove-team",
1491
1620
  recordIdParam: "teamId",
1621
+ visible: "features.multiOrgEnabled != false",
1492
1622
  confirmText: "Delete this team? Members will lose any team-scoped access. This cannot be undone.",
1493
1623
  successMessage: "Team deleted",
1494
1624
  refreshAfter: true
@@ -1599,6 +1729,10 @@ var SysTeamMember = data.ObjectSchema.create({
1599
1729
  locations: ["list_toolbar"],
1600
1730
  type: "api",
1601
1731
  target: "/api/v1/auth/organization/add-team-member",
1732
+ // Team membership lives under organizations — multi-org-only. Gate
1733
+ // both mutations so they vanish in single-org (mirrors
1734
+ // sys_organization.create_organization).
1735
+ visible: "features.multiOrgEnabled != false",
1602
1736
  successMessage: "Team member added",
1603
1737
  refreshAfter: true,
1604
1738
  params: [
@@ -1619,6 +1753,7 @@ var SysTeamMember = data.ObjectSchema.create({
1619
1753
  locations: ["list_item"],
1620
1754
  type: "api",
1621
1755
  target: "/api/v1/auth/organization/remove-team-member",
1756
+ visible: "features.multiOrgEnabled != false",
1622
1757
  confirmText: "Remove this user from the team? They will lose any team-scoped access.",
1623
1758
  successMessage: "Team member removed",
1624
1759
  refreshAfter: true,
@@ -3158,6 +3293,224 @@ var SysJwks = data.ObjectSchema.create({
3158
3293
  mru: false
3159
3294
  }
3160
3295
  });
3296
+ var SysSsoProvider = data.ObjectSchema.create({
3297
+ name: "sys_sso_provider",
3298
+ label: "SSO Provider",
3299
+ pluralLabel: "SSO Providers",
3300
+ icon: "shield-check",
3301
+ isSystem: true,
3302
+ managedBy: "better-auth",
3303
+ // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
3304
+ protection: {
3305
+ lock: "full",
3306
+ reason: "Identity table managed by better-auth (@better-auth/sso) \u2014 see ADR-0024.",
3307
+ docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
3308
+ },
3309
+ description: "External SSO identity providers (OIDC / SAML) this environment federates login to",
3310
+ displayNameField: "provider_id",
3311
+ titleFormat: "{provider_id}",
3312
+ compactLayout: ["provider_id", "issuer", "domain"],
3313
+ // All mutations go through @better-auth/sso's endpoints under
3314
+ // /api/v1/auth/sso/* (register / delete-provider) rather than the generic
3315
+ // data layer, so server-side config validation + secret handling run.
3316
+ actions: [
3317
+ {
3318
+ name: "register_sso_provider",
3319
+ label: "Register SSO Provider",
3320
+ icon: "plus-circle",
3321
+ variant: "primary",
3322
+ mode: "create",
3323
+ locations: ["list_toolbar"],
3324
+ type: "api",
3325
+ method: "POST",
3326
+ target: "/api/v1/auth/sso/register",
3327
+ refreshAfter: true,
3328
+ params: [
3329
+ { name: "providerId", label: "Provider ID", type: "text", required: true, helpText: 'Stable identifier, e.g. "okta" or "acme-entra".' },
3330
+ { name: "issuer", label: "Issuer URL", type: "text", required: true, helpText: "IdP issuer / discovery base, e.g. https://acme.okta.com." },
3331
+ { name: "domain", label: "Email Domain", type: "text", required: true, helpText: "Users with this email domain are routed to this IdP." },
3332
+ { name: "clientId", label: "Client ID", type: "text", required: true },
3333
+ { name: "clientSecret", label: "Client Secret", type: "text", required: true }
3334
+ ]
3335
+ },
3336
+ {
3337
+ name: "delete_sso_provider",
3338
+ label: "Delete SSO Provider",
3339
+ icon: "trash-2",
3340
+ variant: "danger",
3341
+ mode: "delete",
3342
+ locations: ["list_item", "record_header"],
3343
+ type: "api",
3344
+ method: "POST",
3345
+ target: "/api/v1/auth/sso/delete-provider",
3346
+ confirmText: "Delete this SSO provider? Users from its domain will no longer be able to sign in through it.",
3347
+ successMessage: "SSO provider deleted",
3348
+ refreshAfter: true,
3349
+ params: [
3350
+ { name: "providerId", field: "provider_id", defaultFromRow: true, required: true }
3351
+ ]
3352
+ }
3353
+ ],
3354
+ listViews: {
3355
+ all: {
3356
+ type: "grid",
3357
+ name: "all",
3358
+ label: "All",
3359
+ data: { provider: "object", object: "sys_sso_provider" },
3360
+ columns: ["provider_id", "issuer", "domain", "created_at"],
3361
+ sort: [{ field: "provider_id", order: "asc" }],
3362
+ pagination: { pageSize: 50 }
3363
+ }
3364
+ },
3365
+ fields: {
3366
+ id: data.Field.text({ label: "ID", required: true, readonly: true, group: "System" }),
3367
+ provider_id: data.Field.text({
3368
+ label: "Provider ID",
3369
+ required: true,
3370
+ searchable: true,
3371
+ maxLength: 255,
3372
+ description: "Stable provider identifier (unique within the environment)",
3373
+ group: "Identity"
3374
+ }),
3375
+ issuer: data.Field.text({
3376
+ label: "Issuer",
3377
+ required: true,
3378
+ maxLength: 2048,
3379
+ description: "IdP issuer URL",
3380
+ group: "Identity"
3381
+ }),
3382
+ domain: data.Field.text({
3383
+ label: "Email Domain",
3384
+ required: true,
3385
+ maxLength: 255,
3386
+ description: "Email domain routed to this IdP (e.g. acme.com)",
3387
+ group: "Identity"
3388
+ }),
3389
+ oidc_config: data.Field.textarea({
3390
+ label: "OIDC Config",
3391
+ required: false,
3392
+ description: "JSON: clientId, clientSecret, endpoints, scopes, mapping, pkce (managed by better-auth)",
3393
+ group: "Protocol"
3394
+ }),
3395
+ saml_config: data.Field.textarea({
3396
+ label: "SAML Config",
3397
+ required: false,
3398
+ description: "JSON: entryPoint, cert, identifierFormat, mapping (managed by better-auth)",
3399
+ group: "Protocol"
3400
+ }),
3401
+ user_id: data.Field.lookup("sys_user", {
3402
+ label: "Registered By",
3403
+ required: false,
3404
+ description: "User who registered this provider",
3405
+ group: "System"
3406
+ }),
3407
+ organization_id: data.Field.text({
3408
+ label: "Organization",
3409
+ required: false,
3410
+ maxLength: 255,
3411
+ description: "Organization scope (when org-scoped SSO is used)",
3412
+ group: "System"
3413
+ }),
3414
+ created_at: data.Field.datetime({ label: "Created At", defaultValue: "NOW()", readonly: true, group: "System" }),
3415
+ updated_at: data.Field.datetime({ label: "Updated At", defaultValue: "NOW()", readonly: true, group: "System" })
3416
+ },
3417
+ indexes: [
3418
+ { fields: ["provider_id"], unique: true },
3419
+ { fields: ["domain"] },
3420
+ { fields: ["user_id"] }
3421
+ ],
3422
+ enable: {
3423
+ trackHistory: true,
3424
+ searchable: true,
3425
+ apiEnabled: true,
3426
+ // Mutations go through /api/v1/auth/sso/* (register / delete-provider);
3427
+ // the generic data layer is read-only so sysadmins cannot bypass
3428
+ // server-side validation / secret handling.
3429
+ apiMethods: ["get", "list"],
3430
+ trash: false,
3431
+ mru: false
3432
+ }
3433
+ });
3434
+ var SysScimProvider = data.ObjectSchema.create({
3435
+ name: "sys_scim_provider",
3436
+ label: "SCIM Provider",
3437
+ pluralLabel: "SCIM Providers",
3438
+ icon: "users",
3439
+ isSystem: true,
3440
+ managedBy: "better-auth",
3441
+ // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
3442
+ protection: {
3443
+ lock: "full",
3444
+ reason: "Identity table managed by better-auth (@better-auth/scim) \u2014 see ADR-0071.",
3445
+ docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
3446
+ },
3447
+ description: "SCIM 2.0 connections (bearer tokens) external IdPs use to provision/deprovision this environment's users",
3448
+ displayNameField: "provider_id",
3449
+ titleFormat: "{provider_id}",
3450
+ compactLayout: ["provider_id", "organization_id"],
3451
+ listViews: {
3452
+ all: {
3453
+ type: "grid",
3454
+ name: "all",
3455
+ label: "All",
3456
+ data: { provider: "object", object: "sys_scim_provider" },
3457
+ // scim_token is intentionally excluded — never surface the credential.
3458
+ columns: ["provider_id", "organization_id", "created_at"],
3459
+ sort: [{ field: "provider_id", order: "asc" }],
3460
+ pagination: { pageSize: 50 }
3461
+ }
3462
+ },
3463
+ fields: {
3464
+ id: data.Field.text({ label: "ID", required: true, readonly: true, group: "System" }),
3465
+ provider_id: data.Field.text({
3466
+ label: "Provider ID",
3467
+ required: true,
3468
+ searchable: true,
3469
+ maxLength: 255,
3470
+ description: 'Stable SCIM provider identifier (e.g. "okta-scim")',
3471
+ group: "Identity"
3472
+ }),
3473
+ scim_token: data.Field.text({
3474
+ label: "SCIM Token (hash)",
3475
+ required: false,
3476
+ readonly: true,
3477
+ maxLength: 1024,
3478
+ description: "Hashed bearer credential for this SCIM connection \u2014 the plaintext is shown once at generate-token. Sensitive; do not expose.",
3479
+ group: "Secret"
3480
+ }),
3481
+ organization_id: data.Field.text({
3482
+ label: "Organization",
3483
+ required: false,
3484
+ maxLength: 255,
3485
+ description: "Organization scope of this token (org-scoped tokens restrict provisioning to that org)",
3486
+ group: "System"
3487
+ }),
3488
+ user_id: data.Field.lookup("sys_user", {
3489
+ label: "Owned By",
3490
+ required: false,
3491
+ description: "User who generated this token (when provider-ownership is enabled)",
3492
+ group: "System"
3493
+ }),
3494
+ created_at: data.Field.datetime({ label: "Created At", defaultValue: "NOW()", readonly: true, group: "System" }),
3495
+ updated_at: data.Field.datetime({ label: "Updated At", defaultValue: "NOW()", readonly: true, group: "System" })
3496
+ },
3497
+ indexes: [
3498
+ { fields: ["provider_id"], unique: true },
3499
+ { fields: ["organization_id"] },
3500
+ { fields: ["user_id"] }
3501
+ ],
3502
+ enable: {
3503
+ trackHistory: true,
3504
+ searchable: false,
3505
+ apiEnabled: true,
3506
+ // Mutations + token issuance go through @better-auth/scim's endpoints
3507
+ // under /api/v1/auth/scim/*; the generic data layer is read-only so the
3508
+ // credential cannot be written/bypassed through it.
3509
+ apiMethods: ["list"],
3510
+ trash: false,
3511
+ mru: false
3512
+ }
3513
+ });
3161
3514
  var SysNotification = data.ObjectSchema.create({
3162
3515
  name: "sys_notification",
3163
3516
  label: "Notification Event",
@@ -4613,7 +4966,7 @@ var SETUP_NAV_CONTRIBUTIONS = [
4613
4966
  items: [
4614
4967
  { id: "nav_users", type: "object", label: "Users", objectName: "sys_user", icon: "user" },
4615
4968
  { id: "nav_business_units", type: "object", label: "Business Units", objectName: "sys_business_unit", icon: "building", requiresObject: "sys_business_unit" },
4616
- { id: "nav_teams", type: "object", label: "Teams", objectName: "sys_team", icon: "users-round" },
4969
+ { id: "nav_teams", type: "object", label: "Teams", objectName: "sys_team", icon: "users-round", requiresService: "org-scoping" },
4617
4970
  { id: "nav_organizations", type: "object", label: "Organizations", objectName: "sys_organization", icon: "building-2", requiresService: "org-scoping" },
4618
4971
  { id: "nav_invitations", type: "object", label: "Invitations", objectName: "sys_invitation", icon: "mail", requiresService: "org-scoping" }
4619
4972
  ]
@@ -4641,6 +4994,16 @@ var SETUP_NAV_CONTRIBUTIONS = [
4641
4994
  priority: BASE_PRIORITY,
4642
4995
  items: [
4643
4996
  { id: "nav_settings_hub", type: "url", label: "All Settings", url: "/apps/setup/system/settings", icon: "settings-2", requiredPermissions: ["manage_platform_settings"] },
4997
+ // Workspace identity first — Localization (order 2) and Company (order 3)
4998
+ // are the lowest-`order` settings manifests and the first thing a new
4999
+ // company admin configures. They ship as `service-settings` manifests
5000
+ // (tenant scope, read=`setup.access`) but were never pinned here, so they
5001
+ // were reachable only by drilling into the "All Settings" hub. Mainstream
5002
+ // admin consoles (Salesforce "Company Information", ServiceNow) surface
5003
+ // both directly. No `requiredPermissions` — matches Branding (read perm is
5004
+ // the app's base `setup.access`).
5005
+ { id: "nav_settings_localization", type: "url", label: "Localization", url: "/apps/setup/system/settings/localization", icon: "globe" },
5006
+ { id: "nav_settings_company", type: "url", label: "Company", url: "/apps/setup/system/settings/company", icon: "building-2" },
4644
5007
  { id: "nav_settings_branding", type: "url", label: "Branding", url: "/apps/setup/system/settings/branding", icon: "palette" },
4645
5008
  { id: "nav_settings_auth", type: "url", label: "Authentication", url: "/apps/setup/system/settings/auth", icon: "lock-keyhole", requiredPermissions: ["manage_platform_settings"] },
4646
5009
  { id: "nav_settings_mail", type: "url", label: "Email", url: "/apps/setup/system/settings/mail", icon: "mail", requiredPermissions: ["manage_platform_settings"] },
@@ -4668,8 +5031,12 @@ var SETUP_NAV_CONTRIBUTIONS = [
4668
5031
  items: [
4669
5032
  { id: "nav_oauth_apps", type: "object", label: "OAuth Applications", objectName: "sys_oauth_application", icon: "app-window" },
4670
5033
  { id: "nav_jwks", type: "object", label: "Signing Keys (JWKS)", objectName: "sys_jwks", icon: "key-round" },
4671
- { id: "nav_verifications", type: "object", label: "Verifications", objectName: "sys_verification", icon: "mail-check" },
4672
- { id: "nav_device_codes", type: "object", label: "Device Codes", objectName: "sys_device_code", icon: "qr-code" },
5034
+ // `sys_verification` (email/phone tokens) and `sys_device_code` (OAuth
5035
+ // device-grant codes) deliberately omit `list` from their `apiMethods`
5036
+ // (sensitive, ephemeral secrets — not browsable), so an object/list-view
5037
+ // nav entry for them can only ever render "failed to load". They're
5038
+ // reachable by id (get) when needed; no browse menu. (Re-adding requires
5039
+ // enabling `list` on the object — a security decision.)
4673
5040
  { id: "nav_accounts", type: "object", label: "Identity Links", objectName: "sys_account", icon: "link-2" },
4674
5041
  { id: "nav_user_preferences", type: "object", label: "User Preferences", objectName: "sys_user_preference", icon: "sliders" }
4675
5042
  ]
@@ -5055,7 +5422,13 @@ var ACCOUNT_APP = {
5055
5422
  objectName: "sys_member",
5056
5423
  viewName: "mine",
5057
5424
  icon: "building-2",
5058
- requiresObject: "sys_member"
5425
+ // Membership is a multi-org concept: in single-org mode the
5426
+ // `mine` view is always empty (no sys_organization rows, no
5427
+ // auto-stamped memberships). Gate on the org-scoping service so
5428
+ // this entry disappears entirely — matching Organizations/
5429
+ // Invitations in setup-nav. `requiresObject: 'sys_member'` was
5430
+ // the wrong gate (the system object is always registered).
5431
+ requiresService: "org-scoping"
5059
5432
  }
5060
5433
  ]
5061
5434
  },
@@ -5148,6 +5521,12 @@ var SystemOverviewDashboard = ui.Dashboard.create({
5148
5521
  values: ["org_count"],
5149
5522
  title: "Organizations",
5150
5523
  type: "metric",
5524
+ // Organizations only exist under multi-tenant org-scoping. In a
5525
+ // single-tenant runtime the count is always 0 and the matching
5526
+ // nav entries (nav_organizations / nav_invitations) are hidden via
5527
+ // `requiresService: 'org-scoping'` — gate this KPI the same way so the
5528
+ // overview doesn't dangle a metric the admin can't act on.
5529
+ requiresService: "org-scoping",
5151
5530
  layout: { x: 3, y: 0, w: 3, h: 2 },
5152
5531
  colorVariant: "orange",
5153
5532
  description: "Total organizations on the platform"
@@ -10663,6 +11042,7 @@ var zhCN = {
10663
11042
  group_apps: { label: "\u5E94\u7528" },
10664
11043
  nav_marketplace_browse: { label: "\u6D4F\u89C8\u5E94\u7528\u5E02\u573A" },
10665
11044
  nav_marketplace_installed: { label: "\u5DF2\u5B89\u88C5\u5E94\u7528" },
11045
+ nav_cloud_connection: { label: "\u4E91\u8FDE\u63A5" },
10666
11046
  group_people_org: { label: "\u4EBA\u5458\u4E0E\u7EC4\u7EC7" },
10667
11047
  group_access_control: { label: "\u8BBF\u95EE\u63A7\u5236" },
10668
11048
  group_approvals: { label: "\u5BA1\u6279" },
@@ -10677,6 +11057,7 @@ var zhCN = {
10677
11057
  nav_organizations: { label: "\u7EC4\u7EC7" },
10678
11058
  nav_invitations: { label: "\u9080\u8BF7" },
10679
11059
  nav_roles: { label: "\u89D2\u8272" },
11060
+ nav_capabilities: { label: "\u80FD\u529B" },
10680
11061
  nav_permission_sets: { label: "\u6743\u9650\u96C6" },
10681
11062
  nav_sharing_rules: { label: "\u5171\u4EAB\u89C4\u5219" },
10682
11063
  nav_record_shares: { label: "\u8BB0\u5F55\u5171\u4EAB" },
@@ -10685,6 +11066,8 @@ var zhCN = {
10685
11066
  nav_approval_requests: { label: "\u5BA1\u6279\u7533\u8BF7" },
10686
11067
  nav_approval_actions: { label: "\u5BA1\u6279\u5386\u53F2" },
10687
11068
  nav_settings_hub: { label: "\u5168\u90E8\u8BBE\u7F6E" },
11069
+ nav_settings_localization: { label: "\u672C\u5730\u5316" },
11070
+ nav_settings_company: { label: "\u516C\u53F8\u4FE1\u606F" },
10688
11071
  nav_settings_mail: { label: "\u90AE\u4EF6" },
10689
11072
  nav_settings_branding: { label: "\u54C1\u724C" },
10690
11073
  nav_settings_auth: { label: "\u8BA4\u8BC1" },
@@ -10698,10 +11081,9 @@ var zhCN = {
10698
11081
  nav_sessions: { label: "\u4F1A\u8BDD" },
10699
11082
  nav_audit_logs: { label: "\u5BA1\u8BA1\u65E5\u5FD7" },
10700
11083
  nav_notifications: { label: "\u901A\u77E5" },
11084
+ nav_datasources: { label: "\u6570\u636E\u6E90" },
10701
11085
  nav_oauth_apps: { label: "OAuth \u5E94\u7528" },
10702
11086
  nav_jwks: { label: "\u7B7E\u540D\u5BC6\u94A5 (JWKS)" },
10703
- nav_verifications: { label: "\u9A8C\u8BC1\u8BB0\u5F55" },
10704
- nav_device_codes: { label: "\u8BBE\u5907\u4EE3\u7801" },
10705
11087
  nav_accounts: { label: "\u8EAB\u4EFD\u94FE\u63A5" },
10706
11088
  nav_user_preferences: { label: "\u7528\u6237\u504F\u597D" },
10707
11089
  nav_metadata: { label: "\u5168\u90E8\u5143\u6570\u636E" }
@@ -23207,10 +23589,12 @@ exports.SysOrganization = SysOrganization;
23207
23589
  exports.SysOrganizationDetailPage = SysOrganizationDetailPage;
23208
23590
  exports.SysReportSchedule = SysReportSchedule;
23209
23591
  exports.SysSavedReport = SysSavedReport;
23592
+ exports.SysScimProvider = SysScimProvider;
23210
23593
  exports.SysSecret = SysSecret;
23211
23594
  exports.SysSession = SysSession;
23212
23595
  exports.SysSetting = SysSetting;
23213
23596
  exports.SysSettingAudit = SysSettingAudit;
23597
+ exports.SysSsoProvider = SysSsoProvider;
23214
23598
  exports.SysTeam = SysTeam;
23215
23599
  exports.SysTeamMember = SysTeamMember;
23216
23600
  exports.SysTwoFactor = SysTwoFactor;