@objectstack/platform-objects 11.0.0 → 11.2.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.
@@ -16,6 +16,8 @@ var SysUser = ObjectSchema.create({
16
16
  },
17
17
  description: "User accounts for authentication",
18
18
  displayNameField: "name",
19
+ nameField: "name",
20
+ // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
19
21
  titleFormat: "{name}",
20
22
  compactLayout: ["name", "email", "email_verified"],
21
23
  // Custom actions — generic CRUD is suppressed because user accounts are
@@ -420,6 +422,25 @@ var SysUser = ObjectSchema.create({
420
422
  group: "Admin",
421
423
  description: "When set and in the future, sign-in is rejected (brute-force lockout). Auto-clears past this time; an admin can clear it early via Unlock."
422
424
  }),
425
+ // ADR-0069 D1 — last password change; drives password-expiry enforcement.
426
+ // Stamped on sign-up / change-password / reset-password. Null = never
427
+ // expires (until the user next changes their password).
428
+ password_changed_at: Field.datetime({
429
+ label: "Password Changed At",
430
+ required: false,
431
+ readonly: true,
432
+ group: "Admin",
433
+ description: "Timestamp of the last password change. Backs password_expiry_days; system-managed."
434
+ }),
435
+ // ADR-0069 D3 — when enforced MFA first applied to this user; starts the
436
+ // grace clock. Stamped lazily at session validation; system-managed.
437
+ mfa_required_at: Field.datetime({
438
+ label: "MFA Required At",
439
+ required: false,
440
+ readonly: true,
441
+ group: "Admin",
442
+ description: "When enforced MFA first applied to this user (grace-period clock). Backs mfa_required; system-managed."
443
+ }),
423
444
  ai_access: Field.boolean({
424
445
  label: "AI Access",
425
446
  defaultValue: false,
@@ -521,6 +542,8 @@ var SysSession = ObjectSchema.create({
521
542
  },
522
543
  description: "Active user sessions",
523
544
  displayNameField: "user_id",
545
+ nameField: "user_id",
546
+ // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
524
547
  titleFormat: "Session \u2014 {user_id}",
525
548
  compactLayout: ["user_id", "ip_address", "expires_at"],
526
549
  // Custom actions — sessions are managed by better-auth (generic CRUD
@@ -592,6 +615,29 @@ var SysSession = ObjectSchema.create({
592
615
  required: true,
593
616
  group: "Session"
594
617
  }),
618
+ // ── ADR-0069 D4 — session controls (idle / absolute / revoke) ──
619
+ last_activity_at: 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: 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: 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
+ }),
595
641
  // ── Active context (multi-org/team) ──────────────────────────
596
642
  active_organization_id: Field.lookup("sys_organization", {
597
643
  label: "Active Organization",
@@ -944,6 +990,8 @@ var SysOrganization = ObjectSchema.create({
944
990
  },
945
991
  description: "Organizations for multi-tenant grouping",
946
992
  displayNameField: "name",
993
+ nameField: "name",
994
+ // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
947
995
  titleFormat: "{name}",
948
996
  compactLayout: ["name", "slug"],
949
997
  // Custom actions — generic CRUD is suppressed (better-auth-managed),
@@ -1112,6 +1160,17 @@ var SysOrganization = ObjectSchema.create({
1112
1160
  description: "JSON-serialized organization metadata",
1113
1161
  group: "Configuration"
1114
1162
  }),
1163
+ // ADR-0069 D3 — per-org MFA tightening above the global floor. When true,
1164
+ // members of this org must enrol TOTP to access data (enforced at the
1165
+ // session-validation gate). An org can only tighten, never loosen, the
1166
+ // global `mfa_required` setting.
1167
+ require_mfa: Field.boolean({
1168
+ label: "Require Multi-Factor Auth",
1169
+ required: false,
1170
+ defaultValue: false,
1171
+ group: "Configuration",
1172
+ description: "When true, every member of this organization must enroll an authenticator app to access data."
1173
+ }),
1115
1174
  // ── System ───────────────────────────────────────────────────
1116
1175
  id: Field.text({
1117
1176
  label: "Organization ID",
@@ -1555,6 +1614,8 @@ var SysTeam = ObjectSchema.create({
1555
1614
  },
1556
1615
  description: "Teams within organizations for fine-grained grouping",
1557
1616
  displayNameField: "name",
1617
+ nameField: "name",
1618
+ // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
1558
1619
  titleFormat: "{name}",
1559
1620
  compactLayout: ["name", "organization_id"],
1560
1621
  // Custom actions calling better-auth's team endpoints. Generic CRUD is
@@ -1801,6 +1862,8 @@ var SysBusinessUnit = ObjectSchema.create({
1801
1862
  managedBy: "platform",
1802
1863
  description: "Canonical Business Unit tree \u2014 hierarchical org/data-partition node (company / division / department / region / office). ADR-0057 D2.",
1803
1864
  displayNameField: "name",
1865
+ nameField: "name",
1866
+ // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
1804
1867
  titleFormat: "{name}",
1805
1868
  compactLayout: ["name", "kind", "parent_business_unit_id", "manager_user_id"],
1806
1869
  listViews: {
@@ -2074,6 +2137,8 @@ var SysApiKey = ObjectSchema.create({
2074
2137
  },
2075
2138
  description: "API keys for programmatic access",
2076
2139
  displayNameField: "name",
2140
+ nameField: "name",
2141
+ // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
2077
2142
  titleFormat: "{name}",
2078
2143
  compactLayout: ["name", "prefix", "user_id", "expires_at", "revoked"],
2079
2144
  // Custom actions — sys_api_key is managed-by 'better-auth' but the
@@ -2417,6 +2482,8 @@ var SysDeviceCode = ObjectSchema.create({
2417
2482
  docsUrl: "https://docs.objectstack.ai/adr/0010-metadata-protection"
2418
2483
  },
2419
2484
  description: "OAuth 2.0 Device Authorization Grant (RFC 8628) pending requests",
2485
+ nameField: "user_code",
2486
+ // [ADR-0079] canonical primary-title pointer (single-field titleFormat)
2420
2487
  titleFormat: "{user_code}",
2421
2488
  compactLayout: ["user_code", "status", "client_id", "expires_at"],
2422
2489
  fields: {
@@ -2510,6 +2577,8 @@ var SysUserPreference = ObjectSchema.create({
2510
2577
  // surface in Setup is a support/diagnostic view only.
2511
2578
  managedBy: "system",
2512
2579
  description: "Per-user key-value preferences (theme, locale, etc.)",
2580
+ nameField: "key",
2581
+ // [ADR-0079] canonical primary-title pointer (single-field titleFormat)
2513
2582
  titleFormat: "{key}",
2514
2583
  compactLayout: ["user_id", "key"],
2515
2584
  listViews: {
@@ -2605,6 +2674,8 @@ var SysOauthApplication = ObjectSchema.create({
2605
2674
  },
2606
2675
  description: "Registered OAuth/OIDC client applications",
2607
2676
  displayNameField: "name",
2677
+ nameField: "name",
2678
+ // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
2608
2679
  titleFormat: "{name}",
2609
2680
  compactLayout: ["name", "client_id", "type", "disabled"],
2610
2681
  // Custom actions — all OAuth-application mutations are routed through
@@ -3296,6 +3367,22 @@ var SysSsoProvider = ObjectSchema.create({
3296
3367
  icon: "shield-check",
3297
3368
  isSystem: true,
3298
3369
  managedBy: "better-auth",
3370
+ // ADR-0024 — env-global, ADMIN-ONLY identity config. Two orthogonal controls:
3371
+ // • `tenancy.enabled: false` — the env IS the tenant; providers are env-wide,
3372
+ // not org-partitioned. Opting out of multi-tenancy lets a platform admin's
3373
+ // `viewAllRecords` superuser bypass see every provider (without it, the
3374
+ // `member_default` wildcard `tenant_isolation` RLS denies every row, since
3375
+ // better-auth writes via its adapter with no tenantId → `organization_id`
3376
+ // is never stamped).
3377
+ // • `requiredPermissions: ['manage_platform_settings']` — object-level
3378
+ // capability gate (ADR-0066 D3) so ordinary members are denied entirely
3379
+ // (without it, tenancy-disabled + `member_default`'s `'*': allowRead` would
3380
+ // leak providers to every authenticated user).
3381
+ // Together: admins see all env providers; non-admins get 403. better-auth's
3382
+ // own endpoints already read via a system context. (Env-only object — no
3383
+ // control-plane cross-tenant risk.)
3384
+ tenancy: { enabled: false, strategy: "shared" },
3385
+ requiredPermissions: ["manage_platform_settings"],
3299
3386
  // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
3300
3387
  protection: {
3301
3388
  lock: "full",
@@ -3304,6 +3391,8 @@ var SysSsoProvider = ObjectSchema.create({
3304
3391
  },
3305
3392
  description: "External SSO identity providers (OIDC / SAML) this environment federates login to",
3306
3393
  displayNameField: "provider_id",
3394
+ nameField: "provider_id",
3395
+ // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
3307
3396
  titleFormat: "{provider_id}",
3308
3397
  compactLayout: ["provider_id", "issuer", "domain"],
3309
3398
  // All mutations go through @better-auth/sso's endpoints under
@@ -3319,14 +3408,101 @@ var SysSsoProvider = ObjectSchema.create({
3319
3408
  locations: ["list_toolbar"],
3320
3409
  type: "api",
3321
3410
  method: "POST",
3322
- target: "/api/v1/auth/sso/register",
3411
+ // Routed through the env-side bridge (plugin-auth `auth-plugin.ts`), which
3412
+ // reshapes these FLAT form fields into the nested `oidcConfig` body that
3413
+ // `@better-auth/sso`'s /sso/register requires, then re-dispatches to it
3414
+ // (so the admin gate + discovery hydration run). Posting straight to
3415
+ // /sso/register would drop clientId/clientSecret (top-level → Zod-stripped)
3416
+ // and persist an unusable `oidc_config = null` provider.
3417
+ target: "/api/v1/auth/admin/sso/register",
3323
3418
  refreshAfter: true,
3324
3419
  params: [
3325
3420
  { name: "providerId", label: "Provider ID", type: "text", required: true, helpText: 'Stable identifier, e.g. "okta" or "acme-entra".' },
3326
- { name: "issuer", label: "Issuer URL", type: "text", required: true, helpText: "IdP issuer / discovery base, e.g. https://acme.okta.com." },
3327
- { name: "domain", label: "Email Domain", type: "text", required: true, helpText: "Users with this email domain are routed to this IdP." },
3328
- { name: "clientId", label: "Client ID", type: "text", required: true },
3329
- { name: "clientSecret", label: "Client Secret", type: "text", required: true }
3421
+ { 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." },
3422
+ { name: "domain", label: "Email Domain", type: "text", required: true, helpText: "Users with this email domain are routed to this IdP, e.g. acme.com." },
3423
+ { name: "clientId", label: "Client ID", type: "text", required: true, helpText: "OAuth client ID issued by the IdP for this environment." },
3424
+ { name: "clientSecret", label: "Client Secret", type: "text", required: true, helpText: "OAuth client secret (stored encrypted by better-auth)." },
3425
+ { name: "discoveryEndpoint", label: "Discovery URL", type: "text", required: false, helpText: "Optional. OIDC discovery document URL. Leave blank to derive `<issuer>/.well-known/openid-configuration`." },
3426
+ { 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".' },
3427
+ { 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".' },
3428
+ { name: "mapEmail", label: "Map: Email claim", type: "text", required: false, placeholder: "email", helpText: 'Optional. Claim mapped to email. Defaults to "email".' },
3429
+ { name: "mapName", label: "Map: Name claim", type: "text", required: false, placeholder: "name", helpText: 'Optional. Claim mapped to display name. Defaults to "name".' }
3430
+ ]
3431
+ },
3432
+ {
3433
+ name: "register_saml_provider",
3434
+ label: "Register SAML Provider",
3435
+ icon: "shield",
3436
+ variant: "primary",
3437
+ mode: "create",
3438
+ locations: ["list_toolbar"],
3439
+ type: "api",
3440
+ method: "POST",
3441
+ // SAML 2.0 via @better-auth/sso (samlify-backed). Routed through the
3442
+ // env-side bridge (plugin-auth `auth-plugin.ts` → register-saml), which
3443
+ // reshapes these FLAT IdP fields into the nested `samlConfig` body that
3444
+ // @better-auth/sso's /sso/register requires (entryPoint/cert/callbackUrl/
3445
+ // spMetadata/identifierFormat), derives the per-provider ACS URL, and
3446
+ // re-dispatches to /sso/register (admin gate runs). The response returns
3447
+ // the SP ACS + metadata URLs to configure on the IdP.
3448
+ target: "/api/v1/auth/admin/sso/register-saml",
3449
+ refreshAfter: true,
3450
+ params: [
3451
+ { name: "providerId", label: "Provider ID", type: "text", required: true, helpText: 'Stable identifier, e.g. "acme-saml".' },
3452
+ { name: "issuer", label: "IdP Entity ID", type: "text", required: true, helpText: "The IdP\u2019s SAML EntityID (issuer), e.g. https://saml.acme.com/entityid." },
3453
+ { name: "domain", label: "Email Domain", type: "text", required: true, helpText: "Users with this email domain are routed to this IdP, e.g. acme.com." },
3454
+ { name: "entryPoint", label: "IdP SSO URL", type: "text", required: true, helpText: "The IdP\u2019s SAML single sign-on (redirect) endpoint that receives the SAMLRequest." },
3455
+ { 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." },
3456
+ { 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." }
3457
+ ]
3458
+ },
3459
+ {
3460
+ name: "request_domain_verification",
3461
+ label: "Request Domain Verification",
3462
+ icon: "globe",
3463
+ variant: "secondary",
3464
+ locations: ["list_item", "record_header"],
3465
+ type: "api",
3466
+ method: "POST",
3467
+ // ADR-0024 ② (opt-in OS_SSO_DOMAIN_VERIFICATION). Asks @better-auth/sso
3468
+ // for a one-time DNS-TXT challenge and reveals the ready-to-paste record
3469
+ // ONCE via `resultDialog`. Routed through the env bridge (plugin-auth /
3470
+ // cloud AuthProxyPlugin) which reshapes the `{domainVerificationToken}`
3471
+ // response into the `{ data: { dnsRecordName, dnsRecordValue } }` envelope
3472
+ // the dialog reads. When the feature is OFF the bridge returns a clear
3473
+ // "not enabled for this environment" error instead of a bare 404.
3474
+ target: "/api/v1/auth/admin/sso/request-domain-verification",
3475
+ params: [
3476
+ { name: "providerId", field: "provider_id", defaultFromRow: true, required: true },
3477
+ { name: "domain", field: "domain", defaultFromRow: true, required: false }
3478
+ ],
3479
+ resultDialog: {
3480
+ title: "Verify your domain",
3481
+ description: "Add the DNS TXT record below at your domain\u2019s DNS provider, then run \u201CVerify Domain\u201D. The token is shown once.",
3482
+ acknowledge: "Done",
3483
+ fields: [
3484
+ { path: "data.dnsRecordType", label: "Record type", format: "text" },
3485
+ { path: "data.dnsRecordName", label: "Name / Host", format: "secret" },
3486
+ { path: "data.dnsRecordValue", label: "Value", format: "secret" }
3487
+ ]
3488
+ }
3489
+ },
3490
+ {
3491
+ name: "verify_domain",
3492
+ label: "Verify Domain",
3493
+ icon: "shield-check",
3494
+ variant: "secondary",
3495
+ locations: ["list_item", "record_header"],
3496
+ type: "api",
3497
+ method: "POST",
3498
+ // ADR-0024 ②. Re-checks the DNS-TXT record and flips `domain_verified`
3499
+ // on success. Routed through the env bridge, which maps @better-auth/sso's
3500
+ // empty 204 / 502 into a clear success/error toast.
3501
+ target: "/api/v1/auth/admin/sso/verify-domain",
3502
+ successMessage: "Domain ownership verified",
3503
+ refreshAfter: true,
3504
+ params: [
3505
+ { name: "providerId", field: "provider_id", defaultFromRow: true, required: true }
3330
3506
  ]
3331
3507
  },
3332
3508
  {
@@ -3353,9 +3529,17 @@ var SysSsoProvider = ObjectSchema.create({
3353
3529
  name: "all",
3354
3530
  label: "All",
3355
3531
  data: { provider: "object", object: "sys_sso_provider" },
3356
- columns: ["provider_id", "issuer", "domain", "created_at"],
3532
+ columns: ["provider_id", "issuer", "domain", "domain_verified", "created_at"],
3357
3533
  sort: [{ field: "provider_id", order: "asc" }],
3358
- pagination: { pageSize: 50 }
3534
+ pagination: { pageSize: 50 },
3535
+ // Per-object empty state — the shared identity-object copy ("created
3536
+ // automatically … cannot be added here") is wrong for this object, which
3537
+ // HAS a "Register SSO Provider" action. Point admins at it instead.
3538
+ emptyState: {
3539
+ title: "No SSO providers yet",
3540
+ 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.",
3541
+ icon: "log-in"
3542
+ }
3359
3543
  }
3360
3544
  },
3361
3545
  fields: {
@@ -3382,6 +3566,13 @@ var SysSsoProvider = ObjectSchema.create({
3382
3566
  description: "Email domain routed to this IdP (e.g. acme.com)",
3383
3567
  group: "Identity"
3384
3568
  }),
3569
+ domain_verified: Field.boolean({
3570
+ label: "Domain Verified",
3571
+ defaultValue: false,
3572
+ readonly: true,
3573
+ 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.",
3574
+ group: "Identity"
3575
+ }),
3385
3576
  oidc_config: Field.textarea({
3386
3577
  label: "OIDC Config",
3387
3578
  required: false,
@@ -3442,6 +3633,8 @@ var SysScimProvider = ObjectSchema.create({
3442
3633
  },
3443
3634
  description: "SCIM 2.0 connections (bearer tokens) external IdPs use to provision/deprovision this environment's users",
3444
3635
  displayNameField: "provider_id",
3636
+ nameField: "provider_id",
3637
+ // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
3445
3638
  titleFormat: "{provider_id}",
3446
3639
  compactLayout: ["provider_id", "organization_id"],
3447
3640
  listViews: {