@objectstack/platform-objects 10.3.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,51 @@ 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
+ // ADR-0069 D1 — last password change; drives password-expiry enforcement.
428
+ // Stamped on sign-up / change-password / reset-password. Null = never
429
+ // expires (until the user next changes their password).
430
+ password_changed_at: data.Field.datetime({
431
+ label: "Password Changed At",
432
+ required: false,
433
+ readonly: true,
434
+ group: "Admin",
435
+ description: "Timestamp of the last password change. Backs password_expiry_days; system-managed."
436
+ }),
437
+ // ADR-0069 D3 — when enforced MFA first applied to this user; starts the
438
+ // grace clock. Stamped lazily at session validation; system-managed.
439
+ mfa_required_at: data.Field.datetime({
440
+ label: "MFA Required At",
441
+ required: false,
442
+ readonly: true,
443
+ group: "Admin",
444
+ description: "When enforced MFA first applied to this user (grace-period clock). Backs mfa_required; system-managed."
445
+ }),
446
+ ai_access: data.Field.boolean({
447
+ label: "AI Access",
448
+ defaultValue: false,
449
+ group: "Admin",
450
+ 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)."
451
+ }),
377
452
  // ── Profile ──────────────────────────────────────────────────
378
453
  image: data.Field.url({
379
454
  label: "Profile Image",
@@ -394,6 +469,28 @@ var SysUser = data.ObjectSchema.create({
394
469
  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
470
  }),
396
471
  // ── System (auto-managed, hidden from create/edit forms) ─────
472
+ // Identity provenance (ADR-0024 D4). `idp_provisioned` users were
473
+ // JIT-created on first federated login (a `sys_account` exists for an
474
+ // external/OIDC provider — e.g. the cloud-as-IdP `objectstack-cloud`
475
+ // provider, or a customer's own IdP); `env_native` users registered
476
+ // locally (email/password) or are app end-users. Stamped automatically by
477
+ // the AuthManager `account.create.after` hook — never edited by hand.
478
+ // Drives the managed-vs-native user-mgmt UI gating (the password /
479
+ // identity-edit actions hide for managed users, who hold no local
480
+ // credential) and is the marker SCIM lifecycle keys off. Owned by objectql
481
+ // (better-auth is oblivious to this column — like `ai_access`).
482
+ source: data.Field.select({
483
+ label: "Identity Source",
484
+ required: false,
485
+ readonly: true,
486
+ group: "System",
487
+ defaultValue: "env_native",
488
+ options: [
489
+ { label: "IdP-Provisioned", value: "idp_provisioned" },
490
+ { label: "Env-Native", value: "env_native" }
491
+ ],
492
+ 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."
493
+ }),
397
494
  id: data.Field.text({
398
495
  label: "User ID",
399
496
  required: true,
@@ -518,6 +615,29 @@ var SysSession = data.ObjectSchema.create({
518
615
  required: true,
519
616
  group: "Session"
520
617
  }),
618
+ // ── ADR-0069 D4 — session controls (idle / absolute / revoke) ──
619
+ last_activity_at: data.Field.datetime({
620
+ label: "Last Activity At",
621
+ required: false,
622
+ readonly: true,
623
+ group: "Session",
624
+ description: "Timestamp of the last request on this session; drives idle-timeout. System-managed."
625
+ }),
626
+ revoked_at: data.Field.datetime({
627
+ label: "Revoked At",
628
+ required: false,
629
+ readonly: true,
630
+ group: "Session",
631
+ description: "When set, this session was revoked (idle / absolute-max / concurrent-cap / admin). System-managed."
632
+ }),
633
+ revoke_reason: data.Field.text({
634
+ label: "Revoke Reason",
635
+ required: false,
636
+ maxLength: 64,
637
+ readonly: true,
638
+ group: "Session",
639
+ description: "Why the session was revoked (idle_timeout, absolute_max, concurrent_cap, \u2026)."
640
+ }),
521
641
  // ── Active context (multi-org/team) ──────────────────────────
522
642
  active_organization_id: data.Field.lookup("sys_organization", {
523
643
  label: "Active Organization",
@@ -759,6 +879,16 @@ var SysAccount = data.ObjectSchema.create({
759
879
  label: "Password Hash",
760
880
  required: false,
761
881
  description: "Hashed password for email/password provider"
882
+ }),
883
+ // ADR-0069 D1 — bounded ring of previous password hashes (JSON array of
884
+ // strings), used to reject password reuse on change/reset. Maintained by
885
+ // the auth manager; never exposed in UI.
886
+ previous_password_hashes: data.Field.textarea({
887
+ label: "Previous Password Hashes",
888
+ required: false,
889
+ readonly: true,
890
+ hidden: true,
891
+ description: "JSON array of prior password hashes (bounded by password_history_count); reuse-prevention only. System-managed."
762
892
  })
763
893
  },
764
894
  indexes: [
@@ -824,7 +954,13 @@ var SysVerification = data.ObjectSchema.create({
824
954
  })
825
955
  },
826
956
  indexes: [
827
- { fields: ["value"], unique: true },
957
+ // `value` must NOT be unique. better-auth's oauth-provider stores OIDC
958
+ // authorization codes in this table with `value` = a JSON blob keyed by
959
+ // user+client+state, which can legitimately repeat. A UNIQUE constraint
960
+ // makes `/api/v1/auth/oauth2/authorize` fail (`UNIQUE constraint failed:
961
+ // sys_verification.value`) → 503, breaking cloud-as-IdP SSO entirely.
962
+ // better-auth keys verification lookups on `identifier`, not `value`.
963
+ { fields: ["value"], unique: false },
828
964
  { fields: ["identifier"], unique: false },
829
965
  { fields: ["expires_at"], unique: false }
830
966
  ],
@@ -892,6 +1028,10 @@ var SysOrganization = data.ObjectSchema.create({
892
1028
  recordIdParam: "organizationId",
893
1029
  // better-auth `organization/update` nests editable fields under `data`.
894
1030
  bodyShape: { wrap: "data" },
1031
+ // Org-admin actions are multi-org-only; hide them in single-org for
1032
+ // consistency with `create_organization` (the org list is empty there,
1033
+ // but this also guards direct record-URL access).
1034
+ visible: "features.multiOrgEnabled != false",
895
1035
  successMessage: "Organization updated",
896
1036
  refreshAfter: true,
897
1037
  params: [
@@ -910,6 +1050,7 @@ var SysOrganization = data.ObjectSchema.create({
910
1050
  type: "api",
911
1051
  target: "/api/v1/auth/organization/delete",
912
1052
  recordIdParam: "organizationId",
1053
+ visible: "features.multiOrgEnabled != false",
913
1054
  confirmText: "Delete this organization? All members will lose access immediately. This cannot be undone.",
914
1055
  successMessage: "Organization deleted",
915
1056
  refreshAfter: true
@@ -928,6 +1069,7 @@ var SysOrganization = data.ObjectSchema.create({
928
1069
  type: "api",
929
1070
  target: "/api/v1/auth/organization/set-active",
930
1071
  recordIdParam: "organizationId",
1072
+ visible: "features.multiOrgEnabled != false",
931
1073
  successMessage: "Active organization switched",
932
1074
  refreshAfter: true
933
1075
  },
@@ -944,6 +1086,7 @@ var SysOrganization = data.ObjectSchema.create({
944
1086
  type: "api",
945
1087
  target: "/api/v1/auth/organization/leave",
946
1088
  recordIdParam: "organizationId",
1089
+ visible: "features.multiOrgEnabled != false",
947
1090
  confirmText: "Leave this organization? You will lose access to all of its resources.",
948
1091
  successMessage: "You have left the organization",
949
1092
  refreshAfter: true
@@ -965,6 +1108,7 @@ var SysOrganization = data.ObjectSchema.create({
965
1108
  type: "api",
966
1109
  target: "/api/v1/cloud/organizations/{id}/change-slug",
967
1110
  method: "POST",
1111
+ visible: "features.multiOrgEnabled != false",
968
1112
  confirmText: "Renaming the slug rewrites every platform subdomain for this org and parks the old slug for 90 days. Continue?",
969
1113
  successMessage: "Organization slug changed",
970
1114
  refreshAfter: true,
@@ -1014,6 +1158,17 @@ var SysOrganization = data.ObjectSchema.create({
1014
1158
  description: "JSON-serialized organization metadata",
1015
1159
  group: "Configuration"
1016
1160
  }),
1161
+ // ADR-0069 D3 — per-org MFA tightening above the global floor. When true,
1162
+ // members of this org must enrol TOTP to access data (enforced at the
1163
+ // session-validation gate). An org can only tighten, never loosen, the
1164
+ // global `mfa_required` setting.
1165
+ require_mfa: data.Field.boolean({
1166
+ label: "Require Multi-Factor Auth",
1167
+ required: false,
1168
+ defaultValue: false,
1169
+ group: "Configuration",
1170
+ description: "When true, every member of this organization must enroll an authenticator app to access data."
1171
+ }),
1017
1172
  // ── System ───────────────────────────────────────────────────
1018
1173
  id: data.Field.text({
1019
1174
  label: "Organization ID",
@@ -1063,7 +1218,10 @@ var SysMember = data.ObjectSchema.create({
1063
1218
  docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
1064
1219
  },
1065
1220
  description: "Organization membership records",
1066
- titleFormat: "{user_id} in {organization_id}",
1221
+ // Org-independent title: organization_id is null in single-org mode, so a
1222
+ // '{user_id} in {organization_id}' format renders "… in null". User + role
1223
+ // identifies the membership in both single- and multi-org deployments.
1224
+ titleFormat: "{user_id} ({role})",
1067
1225
  compactLayout: ["user_id", "organization_id", "role"],
1068
1226
  // Row-level actions: better-auth `organization/update-member-role` and
1069
1227
  // `organization/remove-member`. Generic CRUD is suppressed on better-auth
@@ -1084,6 +1242,11 @@ var SysMember = data.ObjectSchema.create({
1084
1242
  locations: ["list_toolbar"],
1085
1243
  type: "api",
1086
1244
  target: "/api/v1/auth/organization/add-member",
1245
+ // Org-membership mutations are multi-org-only: the better-auth
1246
+ // endpoints resolve an active org that does not exist in single-org
1247
+ // mode, so these actions would fail at the API. Gate every one on
1248
+ // the multi-org flag (mirrors sys_organization.create_organization).
1249
+ visible: "features.multiOrgEnabled != false",
1087
1250
  successMessage: "Member added",
1088
1251
  refreshAfter: true,
1089
1252
  params: [
@@ -1101,6 +1264,7 @@ var SysMember = data.ObjectSchema.create({
1101
1264
  type: "api",
1102
1265
  target: "/api/v1/auth/organization/update-member-role",
1103
1266
  recordIdParam: "memberId",
1267
+ visible: "features.multiOrgEnabled != false",
1104
1268
  successMessage: "Member role updated",
1105
1269
  refreshAfter: true,
1106
1270
  params: [
@@ -1117,6 +1281,7 @@ var SysMember = data.ObjectSchema.create({
1117
1281
  type: "api",
1118
1282
  target: "/api/v1/auth/organization/remove-member",
1119
1283
  recordIdParam: "memberIdOrEmail",
1284
+ visible: "features.multiOrgEnabled != false",
1120
1285
  confirmText: "Remove this member from the organization? They will lose access to all org resources.",
1121
1286
  successMessage: "Member removed",
1122
1287
  refreshAfter: true
@@ -1138,7 +1303,7 @@ var SysMember = data.ObjectSchema.create({
1138
1303
  target: "/api/v1/auth/organization/update-member-role",
1139
1304
  recordIdParam: "memberId",
1140
1305
  bodyExtra: { role: "owner" },
1141
- visible: "record.role != 'owner'",
1306
+ visible: "record.role != 'owner' && features.multiOrgEnabled != false",
1142
1307
  confirmText: "Transfer ownership of this organization to the selected member? You will be demoted to admin and lose owner-only privileges.",
1143
1308
  successMessage: "Ownership transferred",
1144
1309
  refreshAfter: true
@@ -1223,7 +1388,10 @@ var SysInvitation = data.ObjectSchema.create({
1223
1388
  docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
1224
1389
  },
1225
1390
  description: "Organization invitations for user onboarding",
1226
- titleFormat: "Invitation to {organization_id}",
1391
+ // Title by invitee email rather than organization_id: the latter is null in
1392
+ // single-org mode (renders "Invitation to null"), and the recipient email is
1393
+ // the more useful identifier in both modes anyway.
1394
+ titleFormat: "Invitation for {email}",
1227
1395
  compactLayout: ["email", "organization_id", "status"],
1228
1396
  // Custom actions — generic CRUD is suppressed (better-auth-managed).
1229
1397
  // Mirror the `invite_user` toolbar action from sys_user here so admins
@@ -1237,6 +1405,13 @@ var SysInvitation = data.ObjectSchema.create({
1237
1405
  locations: ["list_toolbar"],
1238
1406
  type: "api",
1239
1407
  target: "/api/v1/auth/organization/invite-member",
1408
+ // Inviting/managing invitations is a multi-org-only flow (the
1409
+ // endpoint resolves an active org absent in single-org mode). Gate
1410
+ // the admin-side actions on the multi-org flag (mirrors
1411
+ // sys_organization.create_organization). The recipient-side
1412
+ // accept/reject actions below stay record-gated — they are
1413
+ // unreachable in single-org anyway (no invitation rows exist).
1414
+ visible: "features.multiOrgEnabled != false",
1240
1415
  successMessage: "Invitation sent",
1241
1416
  refreshAfter: true,
1242
1417
  params: [
@@ -1254,6 +1429,7 @@ var SysInvitation = data.ObjectSchema.create({
1254
1429
  type: "api",
1255
1430
  target: "/api/v1/auth/organization/cancel-invitation",
1256
1431
  recordIdParam: "invitationId",
1432
+ visible: "features.multiOrgEnabled != false",
1257
1433
  confirmText: "Cancel this invitation? The recipient will no longer be able to accept it.",
1258
1434
  successMessage: "Invitation canceled",
1259
1435
  refreshAfter: true
@@ -1267,6 +1443,7 @@ var SysInvitation = data.ObjectSchema.create({
1267
1443
  type: "api",
1268
1444
  target: "/api/v1/auth/organization/invite-member",
1269
1445
  bodyExtra: { resend: true },
1446
+ visible: "features.multiOrgEnabled != false",
1270
1447
  successMessage: "Invitation resent",
1271
1448
  refreshAfter: true,
1272
1449
  params: [
@@ -1451,6 +1628,10 @@ var SysTeam = data.ObjectSchema.create({
1451
1628
  locations: ["list_toolbar"],
1452
1629
  type: "api",
1453
1630
  target: "/api/v1/auth/organization/create-team",
1631
+ // Teams are nested inside organizations — a multi-org-only concept.
1632
+ // Gate every team mutation on the multi-org flag so the affordances
1633
+ // disappear in single-org (mirrors sys_organization.create_organization).
1634
+ visible: "features.multiOrgEnabled != false",
1454
1635
  successMessage: "Team created",
1455
1636
  refreshAfter: true,
1456
1637
  params: [
@@ -1471,6 +1652,7 @@ var SysTeam = data.ObjectSchema.create({
1471
1652
  target: "/api/v1/auth/organization/update-team",
1472
1653
  recordIdParam: "teamId",
1473
1654
  bodyShape: { wrap: "data" },
1655
+ visible: "features.multiOrgEnabled != false",
1474
1656
  successMessage: "Team updated",
1475
1657
  refreshAfter: true,
1476
1658
  params: [
@@ -1489,6 +1671,7 @@ var SysTeam = data.ObjectSchema.create({
1489
1671
  type: "api",
1490
1672
  target: "/api/v1/auth/organization/remove-team",
1491
1673
  recordIdParam: "teamId",
1674
+ visible: "features.multiOrgEnabled != false",
1492
1675
  confirmText: "Delete this team? Members will lose any team-scoped access. This cannot be undone.",
1493
1676
  successMessage: "Team deleted",
1494
1677
  refreshAfter: true
@@ -1599,6 +1782,10 @@ var SysTeamMember = data.ObjectSchema.create({
1599
1782
  locations: ["list_toolbar"],
1600
1783
  type: "api",
1601
1784
  target: "/api/v1/auth/organization/add-team-member",
1785
+ // Team membership lives under organizations — multi-org-only. Gate
1786
+ // both mutations so they vanish in single-org (mirrors
1787
+ // sys_organization.create_organization).
1788
+ visible: "features.multiOrgEnabled != false",
1602
1789
  successMessage: "Team member added",
1603
1790
  refreshAfter: true,
1604
1791
  params: [
@@ -1619,6 +1806,7 @@ var SysTeamMember = data.ObjectSchema.create({
1619
1806
  locations: ["list_item"],
1620
1807
  type: "api",
1621
1808
  target: "/api/v1/auth/organization/remove-team-member",
1809
+ visible: "features.multiOrgEnabled != false",
1622
1810
  confirmText: "Remove this user from the team? They will lose any team-scoped access.",
1623
1811
  successMessage: "Team member removed",
1624
1812
  refreshAfter: true,
@@ -3158,6 +3346,342 @@ var SysJwks = data.ObjectSchema.create({
3158
3346
  mru: false
3159
3347
  }
3160
3348
  });
3349
+ var SysSsoProvider = data.ObjectSchema.create({
3350
+ name: "sys_sso_provider",
3351
+ label: "SSO Provider",
3352
+ pluralLabel: "SSO Providers",
3353
+ icon: "shield-check",
3354
+ isSystem: true,
3355
+ managedBy: "better-auth",
3356
+ // ADR-0024 — env-global, ADMIN-ONLY identity config. Two orthogonal controls:
3357
+ // • `tenancy.enabled: false` — the env IS the tenant; providers are env-wide,
3358
+ // not org-partitioned. Opting out of multi-tenancy lets a platform admin's
3359
+ // `viewAllRecords` superuser bypass see every provider (without it, the
3360
+ // `member_default` wildcard `tenant_isolation` RLS denies every row, since
3361
+ // better-auth writes via its adapter with no tenantId → `organization_id`
3362
+ // is never stamped).
3363
+ // • `requiredPermissions: ['manage_platform_settings']` — object-level
3364
+ // capability gate (ADR-0066 D3) so ordinary members are denied entirely
3365
+ // (without it, tenancy-disabled + `member_default`'s `'*': allowRead` would
3366
+ // leak providers to every authenticated user).
3367
+ // Together: admins see all env providers; non-admins get 403. better-auth's
3368
+ // own endpoints already read via a system context. (Env-only object — no
3369
+ // control-plane cross-tenant risk.)
3370
+ tenancy: { enabled: false, strategy: "shared" },
3371
+ requiredPermissions: ["manage_platform_settings"],
3372
+ // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
3373
+ protection: {
3374
+ lock: "full",
3375
+ reason: "Identity table managed by better-auth (@better-auth/sso) \u2014 see ADR-0024.",
3376
+ docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
3377
+ },
3378
+ description: "External SSO identity providers (OIDC / SAML) this environment federates login to",
3379
+ displayNameField: "provider_id",
3380
+ titleFormat: "{provider_id}",
3381
+ compactLayout: ["provider_id", "issuer", "domain"],
3382
+ // All mutations go through @better-auth/sso's endpoints under
3383
+ // /api/v1/auth/sso/* (register / delete-provider) rather than the generic
3384
+ // data layer, so server-side config validation + secret handling run.
3385
+ actions: [
3386
+ {
3387
+ name: "register_sso_provider",
3388
+ label: "Register SSO Provider",
3389
+ icon: "plus-circle",
3390
+ variant: "primary",
3391
+ mode: "create",
3392
+ locations: ["list_toolbar"],
3393
+ type: "api",
3394
+ method: "POST",
3395
+ // Routed through the env-side bridge (plugin-auth `auth-plugin.ts`), which
3396
+ // reshapes these FLAT form fields into the nested `oidcConfig` body that
3397
+ // `@better-auth/sso`'s /sso/register requires, then re-dispatches to it
3398
+ // (so the admin gate + discovery hydration run). Posting straight to
3399
+ // /sso/register would drop clientId/clientSecret (top-level → Zod-stripped)
3400
+ // and persist an unusable `oidc_config = null` provider.
3401
+ target: "/api/v1/auth/admin/sso/register",
3402
+ refreshAfter: true,
3403
+ params: [
3404
+ { name: "providerId", label: "Provider ID", type: "text", required: true, helpText: 'Stable identifier, e.g. "okta" or "acme-entra".' },
3405
+ { name: "issuer", label: "Issuer URL", type: "text", required: true, helpText: "IdP issuer, e.g. https://acme.okta.com. Discovery is fetched from here unless an explicit URL is given below." },
3406
+ { name: "domain", label: "Email Domain", type: "text", required: true, helpText: "Users with this email domain are routed to this IdP, e.g. acme.com." },
3407
+ { name: "clientId", label: "Client ID", type: "text", required: true, helpText: "OAuth client ID issued by the IdP for this environment." },
3408
+ { name: "clientSecret", label: "Client Secret", type: "text", required: true, helpText: "OAuth client secret (stored encrypted by better-auth)." },
3409
+ { name: "discoveryEndpoint", label: "Discovery URL", type: "text", required: false, helpText: "Optional. OIDC discovery document URL. Leave blank to derive `<issuer>/.well-known/openid-configuration`." },
3410
+ { name: "scopes", label: "Scopes", type: "text", required: false, placeholder: "openid email profile", helpText: 'Optional. Space- or comma-separated OAuth scopes. Defaults to "openid email profile".' },
3411
+ { name: "mapId", label: "Map: User ID claim", type: "text", required: false, placeholder: "sub", helpText: 'Optional. ID-token claim mapped to the user ID. Defaults to "sub".' },
3412
+ { name: "mapEmail", label: "Map: Email claim", type: "text", required: false, placeholder: "email", helpText: 'Optional. Claim mapped to email. Defaults to "email".' },
3413
+ { name: "mapName", label: "Map: Name claim", type: "text", required: false, placeholder: "name", helpText: 'Optional. Claim mapped to display name. Defaults to "name".' }
3414
+ ]
3415
+ },
3416
+ {
3417
+ name: "register_saml_provider",
3418
+ label: "Register SAML Provider",
3419
+ icon: "shield",
3420
+ variant: "primary",
3421
+ mode: "create",
3422
+ locations: ["list_toolbar"],
3423
+ type: "api",
3424
+ method: "POST",
3425
+ // SAML 2.0 via @better-auth/sso (samlify-backed). Routed through the
3426
+ // env-side bridge (plugin-auth `auth-plugin.ts` → register-saml), which
3427
+ // reshapes these FLAT IdP fields into the nested `samlConfig` body that
3428
+ // @better-auth/sso's /sso/register requires (entryPoint/cert/callbackUrl/
3429
+ // spMetadata/identifierFormat), derives the per-provider ACS URL, and
3430
+ // re-dispatches to /sso/register (admin gate runs). The response returns
3431
+ // the SP ACS + metadata URLs to configure on the IdP.
3432
+ target: "/api/v1/auth/admin/sso/register-saml",
3433
+ refreshAfter: true,
3434
+ params: [
3435
+ { name: "providerId", label: "Provider ID", type: "text", required: true, helpText: 'Stable identifier, e.g. "acme-saml".' },
3436
+ { name: "issuer", label: "IdP Entity ID", type: "text", required: true, helpText: "The IdP\u2019s SAML EntityID (issuer), e.g. https://saml.acme.com/entityid." },
3437
+ { name: "domain", label: "Email Domain", type: "text", required: true, helpText: "Users with this email domain are routed to this IdP, e.g. acme.com." },
3438
+ { name: "entryPoint", label: "IdP SSO URL", type: "text", required: true, helpText: "The IdP\u2019s SAML single sign-on (redirect) endpoint that receives the SAMLRequest." },
3439
+ { name: "cert", label: "IdP Signing Certificate", type: "textarea", required: true, helpText: "The IdP\u2019s X.509 signing certificate (PEM body). Used to verify assertion signatures." },
3440
+ { name: "identifierFormat", label: "NameID Format", type: "text", required: false, placeholder: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", helpText: "Optional. Requested SAML NameID format. Defaults to the IdP\u2019s configured format." }
3441
+ ]
3442
+ },
3443
+ {
3444
+ name: "request_domain_verification",
3445
+ label: "Request Domain Verification",
3446
+ icon: "globe",
3447
+ variant: "secondary",
3448
+ locations: ["list_item", "record_header"],
3449
+ type: "api",
3450
+ method: "POST",
3451
+ // ADR-0024 ② (opt-in OS_SSO_DOMAIN_VERIFICATION). Asks @better-auth/sso
3452
+ // for a one-time DNS-TXT challenge and reveals the ready-to-paste record
3453
+ // ONCE via `resultDialog`. Routed through the env bridge (plugin-auth /
3454
+ // cloud AuthProxyPlugin) which reshapes the `{domainVerificationToken}`
3455
+ // response into the `{ data: { dnsRecordName, dnsRecordValue } }` envelope
3456
+ // the dialog reads. When the feature is OFF the bridge returns a clear
3457
+ // "not enabled for this environment" error instead of a bare 404.
3458
+ target: "/api/v1/auth/admin/sso/request-domain-verification",
3459
+ params: [
3460
+ { name: "providerId", field: "provider_id", defaultFromRow: true, required: true },
3461
+ { name: "domain", field: "domain", defaultFromRow: true, required: false }
3462
+ ],
3463
+ resultDialog: {
3464
+ title: "Verify your domain",
3465
+ description: "Add the DNS TXT record below at your domain\u2019s DNS provider, then run \u201CVerify Domain\u201D. The token is shown once.",
3466
+ acknowledge: "Done",
3467
+ fields: [
3468
+ { path: "data.dnsRecordType", label: "Record type", format: "text" },
3469
+ { path: "data.dnsRecordName", label: "Name / Host", format: "secret" },
3470
+ { path: "data.dnsRecordValue", label: "Value", format: "secret" }
3471
+ ]
3472
+ }
3473
+ },
3474
+ {
3475
+ name: "verify_domain",
3476
+ label: "Verify Domain",
3477
+ icon: "shield-check",
3478
+ variant: "secondary",
3479
+ locations: ["list_item", "record_header"],
3480
+ type: "api",
3481
+ method: "POST",
3482
+ // ADR-0024 ②. Re-checks the DNS-TXT record and flips `domain_verified`
3483
+ // on success. Routed through the env bridge, which maps @better-auth/sso's
3484
+ // empty 204 / 502 into a clear success/error toast.
3485
+ target: "/api/v1/auth/admin/sso/verify-domain",
3486
+ successMessage: "Domain ownership verified",
3487
+ refreshAfter: true,
3488
+ params: [
3489
+ { name: "providerId", field: "provider_id", defaultFromRow: true, required: true }
3490
+ ]
3491
+ },
3492
+ {
3493
+ name: "delete_sso_provider",
3494
+ label: "Delete SSO Provider",
3495
+ icon: "trash-2",
3496
+ variant: "danger",
3497
+ mode: "delete",
3498
+ locations: ["list_item", "record_header"],
3499
+ type: "api",
3500
+ method: "POST",
3501
+ target: "/api/v1/auth/sso/delete-provider",
3502
+ confirmText: "Delete this SSO provider? Users from its domain will no longer be able to sign in through it.",
3503
+ successMessage: "SSO provider deleted",
3504
+ refreshAfter: true,
3505
+ params: [
3506
+ { name: "providerId", field: "provider_id", defaultFromRow: true, required: true }
3507
+ ]
3508
+ }
3509
+ ],
3510
+ listViews: {
3511
+ all: {
3512
+ type: "grid",
3513
+ name: "all",
3514
+ label: "All",
3515
+ data: { provider: "object", object: "sys_sso_provider" },
3516
+ columns: ["provider_id", "issuer", "domain", "domain_verified", "created_at"],
3517
+ sort: [{ field: "provider_id", order: "asc" }],
3518
+ pagination: { pageSize: 50 },
3519
+ // Per-object empty state — the shared identity-object copy ("created
3520
+ // automatically … cannot be added here") is wrong for this object, which
3521
+ // HAS a "Register SSO Provider" action. Point admins at it instead.
3522
+ emptyState: {
3523
+ title: "No SSO providers yet",
3524
+ message: "Register your organization\u2019s external IdP \u2014 OIDC (Okta, Entra, Auth0, \u2026) with \u201CRegister SSO Provider\u201D, or SAML 2.0 with \u201CRegister SAML Provider\u201D. Members whose email domain matches can then sign in through it.",
3525
+ icon: "log-in"
3526
+ }
3527
+ }
3528
+ },
3529
+ fields: {
3530
+ id: data.Field.text({ label: "ID", required: true, readonly: true, group: "System" }),
3531
+ provider_id: data.Field.text({
3532
+ label: "Provider ID",
3533
+ required: true,
3534
+ searchable: true,
3535
+ maxLength: 255,
3536
+ description: "Stable provider identifier (unique within the environment)",
3537
+ group: "Identity"
3538
+ }),
3539
+ issuer: data.Field.text({
3540
+ label: "Issuer",
3541
+ required: true,
3542
+ maxLength: 2048,
3543
+ description: "IdP issuer URL",
3544
+ group: "Identity"
3545
+ }),
3546
+ domain: data.Field.text({
3547
+ label: "Email Domain",
3548
+ required: true,
3549
+ maxLength: 255,
3550
+ description: "Email domain routed to this IdP (e.g. acme.com)",
3551
+ group: "Identity"
3552
+ }),
3553
+ domain_verified: data.Field.boolean({
3554
+ label: "Domain Verified",
3555
+ defaultValue: false,
3556
+ readonly: true,
3557
+ description: "Whether DNS ownership of the email domain has been proven (ADR-0024 \u2461). Set by \u201CVerify Domain\u201D after the DNS TXT record resolves. Managed by better-auth \u2014 not directly editable. Only enforced when domain verification is enabled for the environment.",
3558
+ group: "Identity"
3559
+ }),
3560
+ oidc_config: data.Field.textarea({
3561
+ label: "OIDC Config",
3562
+ required: false,
3563
+ description: "JSON: clientId, clientSecret, endpoints, scopes, mapping, pkce (managed by better-auth)",
3564
+ group: "Protocol"
3565
+ }),
3566
+ saml_config: data.Field.textarea({
3567
+ label: "SAML Config",
3568
+ required: false,
3569
+ description: "JSON: entryPoint, cert, identifierFormat, mapping (managed by better-auth)",
3570
+ group: "Protocol"
3571
+ }),
3572
+ user_id: data.Field.lookup("sys_user", {
3573
+ label: "Registered By",
3574
+ required: false,
3575
+ description: "User who registered this provider",
3576
+ group: "System"
3577
+ }),
3578
+ organization_id: data.Field.text({
3579
+ label: "Organization",
3580
+ required: false,
3581
+ maxLength: 255,
3582
+ description: "Organization scope (when org-scoped SSO is used)",
3583
+ group: "System"
3584
+ }),
3585
+ created_at: data.Field.datetime({ label: "Created At", defaultValue: "NOW()", readonly: true, group: "System" }),
3586
+ updated_at: data.Field.datetime({ label: "Updated At", defaultValue: "NOW()", readonly: true, group: "System" })
3587
+ },
3588
+ indexes: [
3589
+ { fields: ["provider_id"], unique: true },
3590
+ { fields: ["domain"] },
3591
+ { fields: ["user_id"] }
3592
+ ],
3593
+ enable: {
3594
+ trackHistory: true,
3595
+ searchable: true,
3596
+ apiEnabled: true,
3597
+ // Mutations go through /api/v1/auth/sso/* (register / delete-provider);
3598
+ // the generic data layer is read-only so sysadmins cannot bypass
3599
+ // server-side validation / secret handling.
3600
+ apiMethods: ["get", "list"],
3601
+ trash: false,
3602
+ mru: false
3603
+ }
3604
+ });
3605
+ var SysScimProvider = data.ObjectSchema.create({
3606
+ name: "sys_scim_provider",
3607
+ label: "SCIM Provider",
3608
+ pluralLabel: "SCIM Providers",
3609
+ icon: "users",
3610
+ isSystem: true,
3611
+ managedBy: "better-auth",
3612
+ // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
3613
+ protection: {
3614
+ lock: "full",
3615
+ reason: "Identity table managed by better-auth (@better-auth/scim) \u2014 see ADR-0071.",
3616
+ docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
3617
+ },
3618
+ description: "SCIM 2.0 connections (bearer tokens) external IdPs use to provision/deprovision this environment's users",
3619
+ displayNameField: "provider_id",
3620
+ titleFormat: "{provider_id}",
3621
+ compactLayout: ["provider_id", "organization_id"],
3622
+ listViews: {
3623
+ all: {
3624
+ type: "grid",
3625
+ name: "all",
3626
+ label: "All",
3627
+ data: { provider: "object", object: "sys_scim_provider" },
3628
+ // scim_token is intentionally excluded — never surface the credential.
3629
+ columns: ["provider_id", "organization_id", "created_at"],
3630
+ sort: [{ field: "provider_id", order: "asc" }],
3631
+ pagination: { pageSize: 50 }
3632
+ }
3633
+ },
3634
+ fields: {
3635
+ id: data.Field.text({ label: "ID", required: true, readonly: true, group: "System" }),
3636
+ provider_id: data.Field.text({
3637
+ label: "Provider ID",
3638
+ required: true,
3639
+ searchable: true,
3640
+ maxLength: 255,
3641
+ description: 'Stable SCIM provider identifier (e.g. "okta-scim")',
3642
+ group: "Identity"
3643
+ }),
3644
+ scim_token: data.Field.text({
3645
+ label: "SCIM Token (hash)",
3646
+ required: false,
3647
+ readonly: true,
3648
+ maxLength: 1024,
3649
+ description: "Hashed bearer credential for this SCIM connection \u2014 the plaintext is shown once at generate-token. Sensitive; do not expose.",
3650
+ group: "Secret"
3651
+ }),
3652
+ organization_id: data.Field.text({
3653
+ label: "Organization",
3654
+ required: false,
3655
+ maxLength: 255,
3656
+ description: "Organization scope of this token (org-scoped tokens restrict provisioning to that org)",
3657
+ group: "System"
3658
+ }),
3659
+ user_id: data.Field.lookup("sys_user", {
3660
+ label: "Owned By",
3661
+ required: false,
3662
+ description: "User who generated this token (when provider-ownership is enabled)",
3663
+ group: "System"
3664
+ }),
3665
+ created_at: data.Field.datetime({ label: "Created At", defaultValue: "NOW()", readonly: true, group: "System" }),
3666
+ updated_at: data.Field.datetime({ label: "Updated At", defaultValue: "NOW()", readonly: true, group: "System" })
3667
+ },
3668
+ indexes: [
3669
+ { fields: ["provider_id"], unique: true },
3670
+ { fields: ["organization_id"] },
3671
+ { fields: ["user_id"] }
3672
+ ],
3673
+ enable: {
3674
+ trackHistory: true,
3675
+ searchable: false,
3676
+ apiEnabled: true,
3677
+ // Mutations + token issuance go through @better-auth/scim's endpoints
3678
+ // under /api/v1/auth/scim/*; the generic data layer is read-only so the
3679
+ // credential cannot be written/bypassed through it.
3680
+ apiMethods: ["list"],
3681
+ trash: false,
3682
+ mru: false
3683
+ }
3684
+ });
3161
3685
  var SysNotification = data.ObjectSchema.create({
3162
3686
  name: "sys_notification",
3163
3687
  label: "Notification Event",
@@ -4613,7 +5137,7 @@ var SETUP_NAV_CONTRIBUTIONS = [
4613
5137
  items: [
4614
5138
  { id: "nav_users", type: "object", label: "Users", objectName: "sys_user", icon: "user" },
4615
5139
  { 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" },
5140
+ { id: "nav_teams", type: "object", label: "Teams", objectName: "sys_team", icon: "users-round", requiresService: "org-scoping" },
4617
5141
  { id: "nav_organizations", type: "object", label: "Organizations", objectName: "sys_organization", icon: "building-2", requiresService: "org-scoping" },
4618
5142
  { id: "nav_invitations", type: "object", label: "Invitations", objectName: "sys_invitation", icon: "mail", requiresService: "org-scoping" }
4619
5143
  ]
@@ -4641,6 +5165,16 @@ var SETUP_NAV_CONTRIBUTIONS = [
4641
5165
  priority: BASE_PRIORITY,
4642
5166
  items: [
4643
5167
  { id: "nav_settings_hub", type: "url", label: "All Settings", url: "/apps/setup/system/settings", icon: "settings-2", requiredPermissions: ["manage_platform_settings"] },
5168
+ // Workspace identity first — Localization (order 2) and Company (order 3)
5169
+ // are the lowest-`order` settings manifests and the first thing a new
5170
+ // company admin configures. They ship as `service-settings` manifests
5171
+ // (tenant scope, read=`setup.access`) but were never pinned here, so they
5172
+ // were reachable only by drilling into the "All Settings" hub. Mainstream
5173
+ // admin consoles (Salesforce "Company Information", ServiceNow) surface
5174
+ // both directly. No `requiredPermissions` — matches Branding (read perm is
5175
+ // the app's base `setup.access`).
5176
+ { id: "nav_settings_localization", type: "url", label: "Localization", url: "/apps/setup/system/settings/localization", icon: "globe" },
5177
+ { id: "nav_settings_company", type: "url", label: "Company", url: "/apps/setup/system/settings/company", icon: "building-2" },
4644
5178
  { id: "nav_settings_branding", type: "url", label: "Branding", url: "/apps/setup/system/settings/branding", icon: "palette" },
4645
5179
  { id: "nav_settings_auth", type: "url", label: "Authentication", url: "/apps/setup/system/settings/auth", icon: "lock-keyhole", requiredPermissions: ["manage_platform_settings"] },
4646
5180
  { id: "nav_settings_mail", type: "url", label: "Email", url: "/apps/setup/system/settings/mail", icon: "mail", requiredPermissions: ["manage_platform_settings"] },
@@ -4668,8 +5202,12 @@ var SETUP_NAV_CONTRIBUTIONS = [
4668
5202
  items: [
4669
5203
  { id: "nav_oauth_apps", type: "object", label: "OAuth Applications", objectName: "sys_oauth_application", icon: "app-window" },
4670
5204
  { 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" },
5205
+ // `sys_verification` (email/phone tokens) and `sys_device_code` (OAuth
5206
+ // device-grant codes) deliberately omit `list` from their `apiMethods`
5207
+ // (sensitive, ephemeral secrets — not browsable), so an object/list-view
5208
+ // nav entry for them can only ever render "failed to load". They're
5209
+ // reachable by id (get) when needed; no browse menu. (Re-adding requires
5210
+ // enabling `list` on the object — a security decision.)
4673
5211
  { id: "nav_accounts", type: "object", label: "Identity Links", objectName: "sys_account", icon: "link-2" },
4674
5212
  { id: "nav_user_preferences", type: "object", label: "User Preferences", objectName: "sys_user_preference", icon: "sliders" }
4675
5213
  ]
@@ -5055,7 +5593,13 @@ var ACCOUNT_APP = {
5055
5593
  objectName: "sys_member",
5056
5594
  viewName: "mine",
5057
5595
  icon: "building-2",
5058
- requiresObject: "sys_member"
5596
+ // Membership is a multi-org concept: in single-org mode the
5597
+ // `mine` view is always empty (no sys_organization rows, no
5598
+ // auto-stamped memberships). Gate on the org-scoping service so
5599
+ // this entry disappears entirely — matching Organizations/
5600
+ // Invitations in setup-nav. `requiresObject: 'sys_member'` was
5601
+ // the wrong gate (the system object is always registered).
5602
+ requiresService: "org-scoping"
5059
5603
  }
5060
5604
  ]
5061
5605
  },
@@ -5148,6 +5692,12 @@ var SystemOverviewDashboard = ui.Dashboard.create({
5148
5692
  values: ["org_count"],
5149
5693
  title: "Organizations",
5150
5694
  type: "metric",
5695
+ // Organizations only exist under multi-tenant org-scoping. In a
5696
+ // single-tenant runtime the count is always 0 and the matching
5697
+ // nav entries (nav_organizations / nav_invitations) are hidden via
5698
+ // `requiresService: 'org-scoping'` — gate this KPI the same way so the
5699
+ // overview doesn't dangle a metric the admin can't act on.
5700
+ requiresService: "org-scoping",
5151
5701
  layout: { x: 3, y: 0, w: 3, h: 2 },
5152
5702
  colorVariant: "orange",
5153
5703
  description: "Total organizations on the platform"
@@ -10663,6 +11213,7 @@ var zhCN = {
10663
11213
  group_apps: { label: "\u5E94\u7528" },
10664
11214
  nav_marketplace_browse: { label: "\u6D4F\u89C8\u5E94\u7528\u5E02\u573A" },
10665
11215
  nav_marketplace_installed: { label: "\u5DF2\u5B89\u88C5\u5E94\u7528" },
11216
+ nav_cloud_connection: { label: "\u4E91\u8FDE\u63A5" },
10666
11217
  group_people_org: { label: "\u4EBA\u5458\u4E0E\u7EC4\u7EC7" },
10667
11218
  group_access_control: { label: "\u8BBF\u95EE\u63A7\u5236" },
10668
11219
  group_approvals: { label: "\u5BA1\u6279" },
@@ -10677,6 +11228,7 @@ var zhCN = {
10677
11228
  nav_organizations: { label: "\u7EC4\u7EC7" },
10678
11229
  nav_invitations: { label: "\u9080\u8BF7" },
10679
11230
  nav_roles: { label: "\u89D2\u8272" },
11231
+ nav_capabilities: { label: "\u80FD\u529B" },
10680
11232
  nav_permission_sets: { label: "\u6743\u9650\u96C6" },
10681
11233
  nav_sharing_rules: { label: "\u5171\u4EAB\u89C4\u5219" },
10682
11234
  nav_record_shares: { label: "\u8BB0\u5F55\u5171\u4EAB" },
@@ -10685,6 +11237,8 @@ var zhCN = {
10685
11237
  nav_approval_requests: { label: "\u5BA1\u6279\u7533\u8BF7" },
10686
11238
  nav_approval_actions: { label: "\u5BA1\u6279\u5386\u53F2" },
10687
11239
  nav_settings_hub: { label: "\u5168\u90E8\u8BBE\u7F6E" },
11240
+ nav_settings_localization: { label: "\u672C\u5730\u5316" },
11241
+ nav_settings_company: { label: "\u516C\u53F8\u4FE1\u606F" },
10688
11242
  nav_settings_mail: { label: "\u90AE\u4EF6" },
10689
11243
  nav_settings_branding: { label: "\u54C1\u724C" },
10690
11244
  nav_settings_auth: { label: "\u8BA4\u8BC1" },
@@ -10698,10 +11252,9 @@ var zhCN = {
10698
11252
  nav_sessions: { label: "\u4F1A\u8BDD" },
10699
11253
  nav_audit_logs: { label: "\u5BA1\u8BA1\u65E5\u5FD7" },
10700
11254
  nav_notifications: { label: "\u901A\u77E5" },
11255
+ nav_datasources: { label: "\u6570\u636E\u6E90" },
10701
11256
  nav_oauth_apps: { label: "OAuth \u5E94\u7528" },
10702
11257
  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
11258
  nav_accounts: { label: "\u8EAB\u4EFD\u94FE\u63A5" },
10706
11259
  nav_user_preferences: { label: "\u7528\u6237\u504F\u597D" },
10707
11260
  nav_metadata: { label: "\u5168\u90E8\u5143\u6570\u636E" }
@@ -23207,10 +23760,12 @@ exports.SysOrganization = SysOrganization;
23207
23760
  exports.SysOrganizationDetailPage = SysOrganizationDetailPage;
23208
23761
  exports.SysReportSchedule = SysReportSchedule;
23209
23762
  exports.SysSavedReport = SysSavedReport;
23763
+ exports.SysScimProvider = SysScimProvider;
23210
23764
  exports.SysSecret = SysSecret;
23211
23765
  exports.SysSession = SysSession;
23212
23766
  exports.SysSetting = SysSetting;
23213
23767
  exports.SysSettingAudit = SysSettingAudit;
23768
+ exports.SysSsoProvider = SysSsoProvider;
23214
23769
  exports.SysTeam = SysTeam;
23215
23770
  exports.SysTeamMember = SysTeamMember;
23216
23771
  exports.SysTwoFactor = SysTwoFactor;