@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/apps/index.d.mts +8 -8
- package/dist/apps/index.d.ts +8 -8
- package/dist/apps/index.js +35 -6
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/index.mjs +35 -6
- package/dist/apps/index.mjs.map +1 -1
- package/dist/audit/index.d.mts +173 -364
- package/dist/audit/index.d.ts +173 -364
- package/dist/identity/index.d.mts +23108 -16447
- package/dist/identity/index.d.ts +23108 -16447
- package/dist/identity/index.js +534 -8
- package/dist/identity/index.js.map +1 -1
- package/dist/identity/index.mjs +533 -9
- package/dist/identity/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +569 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +568 -15
- package/dist/index.mjs.map +1 -1
- package/dist/plugin.js +5 -2
- package/dist/plugin.js.map +1 -1
- package/dist/plugin.mjs +5 -2
- package/dist/plugin.mjs.map +1 -1
- package/dist/system/index.d.mts +50 -106
- package/dist/system/index.d.ts +50 -106
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4672
|
-
|
|
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
|
-
|
|
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;
|