@objectstack/platform-objects 6.5.0 → 6.6.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.mjs CHANGED
@@ -423,6 +423,30 @@ var SysAccount = ObjectSchema.create({
423
423
  description: "OAuth and authentication provider accounts",
424
424
  titleFormat: "{provider_id} - {account_id}",
425
425
  compactLayout: ["provider_id", "user_id", "account_id"],
426
+ // Custom actions — sysadmins routinely need to revoke a user's OAuth
427
+ // link (e.g. when an SSO provider is decommissioned or the user
428
+ // requests it). Better-auth exposes `/unlink-account { providerId,
429
+ // accountId }` for this. The form is locked to the row's values so
430
+ // it acts as a one-click confirmation rather than a free-form edit.
431
+ actions: [
432
+ {
433
+ name: "unlink_account",
434
+ label: "Unlink Account",
435
+ icon: "unlink",
436
+ variant: "danger",
437
+ mode: "delete",
438
+ locations: ["list_item", "record_header"],
439
+ type: "api",
440
+ target: "/api/v1/auth/unlink-account",
441
+ confirmText: "Unlink this identity link? The user will no longer be able to sign in with this provider until they re-link it from their account settings.",
442
+ successMessage: "Identity link removed",
443
+ refreshAfter: true,
444
+ params: [
445
+ { name: "providerId", field: "provider_id", defaultFromRow: true, required: true },
446
+ { name: "accountId", field: "account_id", defaultFromRow: true, required: true }
447
+ ]
448
+ }
449
+ ],
426
450
  listViews: {
427
451
  mine: {
428
452
  type: "grid",
@@ -1977,6 +2001,96 @@ var SysOauthApplication = ObjectSchema.create({
1977
2001
  displayNameField: "name",
1978
2002
  titleFormat: "{name}",
1979
2003
  compactLayout: ["name", "client_id", "type", "disabled"],
2004
+ // Custom actions — all OAuth-application mutations are routed through
2005
+ // better-auth's `@better-auth/oauth-provider` endpoints (and a thin
2006
+ // ObjectStack-added auth route for the enable/disable toggle) rather
2007
+ // than the generic data layer, so server-side validation, secret
2008
+ // hashing, and audit hooks all run. The generic `delete` API method
2009
+ // is intentionally dropped from `apiMethods` below so the only delete
2010
+ // path is the better-auth wrapper.
2011
+ //
2012
+ // Upstream gap (better-auth 1.6.11): the stock `/admin/oauth2/update-client`
2013
+ // endpoint's Zod body schema does NOT accept the `disabled` flag, even
2014
+ // though the column exists and the runtime honours it. We bridge the
2015
+ // gap with `POST /api/v1/auth/admin/oauth2/toggle-disabled`, registered
2016
+ // by plugin-auth, which writes through better-auth's own adapter under
2017
+ // the auth namespace (no generic data-layer bypass). When upstream
2018
+ // ships `disabled` support, retarget the enable/disable actions and
2019
+ // delete the bridge route.
2020
+ actions: [
2021
+ {
2022
+ name: "disable_oauth_application",
2023
+ label: "Disable OAuth Application",
2024
+ icon: "pause-circle",
2025
+ variant: "secondary",
2026
+ mode: "custom",
2027
+ locations: ["list_item", "record_header"],
2028
+ type: "api",
2029
+ method: "POST",
2030
+ target: "/api/v1/auth/admin/oauth2/toggle-disabled",
2031
+ confirmText: "Disable this OAuth application? Active access/refresh tokens issued to it will continue to be rejected at the token, authorize, and introspect endpoints. Existing integrations will stop working immediately.",
2032
+ successMessage: "OAuth application disabled",
2033
+ refreshAfter: true,
2034
+ visible: "!record.disabled",
2035
+ bodyExtra: { disabled: true },
2036
+ params: [
2037
+ { name: "client_id", field: "client_id", defaultFromRow: true, required: true }
2038
+ ]
2039
+ },
2040
+ {
2041
+ name: "enable_oauth_application",
2042
+ label: "Enable OAuth Application",
2043
+ icon: "play-circle",
2044
+ variant: "primary",
2045
+ mode: "custom",
2046
+ locations: ["list_item", "record_header"],
2047
+ type: "api",
2048
+ method: "POST",
2049
+ target: "/api/v1/auth/admin/oauth2/toggle-disabled",
2050
+ confirmText: "Re-enable this OAuth application? Token issuance, authorization, and introspection will resume immediately.",
2051
+ successMessage: "OAuth application enabled",
2052
+ refreshAfter: true,
2053
+ visible: "record.disabled",
2054
+ bodyExtra: { disabled: false },
2055
+ params: [
2056
+ { name: "client_id", field: "client_id", defaultFromRow: true, required: true }
2057
+ ]
2058
+ },
2059
+ {
2060
+ name: "rotate_client_secret",
2061
+ label: "Rotate Client Secret",
2062
+ icon: "refresh-cw",
2063
+ variant: "secondary",
2064
+ mode: "custom",
2065
+ locations: ["list_item", "record_header"],
2066
+ type: "api",
2067
+ method: "POST",
2068
+ target: "/api/v1/auth/oauth2/client/rotate-secret",
2069
+ confirmText: "Rotate this OAuth client's secret? The previous secret will stop working immediately and any integrations using it will break until they are updated with the new secret. The new secret is shown only once.",
2070
+ successMessage: "Client secret rotated \u2014 copy the new value from the response now.",
2071
+ refreshAfter: true,
2072
+ params: [
2073
+ { name: "client_id", field: "client_id", defaultFromRow: true, required: true }
2074
+ ]
2075
+ },
2076
+ {
2077
+ name: "delete_oauth_application",
2078
+ label: "Delete OAuth Application",
2079
+ icon: "trash-2",
2080
+ variant: "danger",
2081
+ mode: "delete",
2082
+ locations: ["list_item", "record_header"],
2083
+ type: "api",
2084
+ method: "POST",
2085
+ target: "/api/v1/auth/oauth2/delete-client",
2086
+ confirmText: "Permanently delete this OAuth application? All issued tokens and consents will be invalidated and integrations using this client_id will stop working immediately. This cannot be undone.",
2087
+ successMessage: "OAuth application deleted",
2088
+ refreshAfter: true,
2089
+ params: [
2090
+ { name: "client_id", field: "client_id", defaultFromRow: true, required: true }
2091
+ ]
2092
+ }
2093
+ ],
1980
2094
  listViews: {
1981
2095
  active: {
1982
2096
  type: "grid",
@@ -2208,7 +2322,12 @@ var SysOauthApplication = ObjectSchema.create({
2208
2322
  trackHistory: true,
2209
2323
  searchable: true,
2210
2324
  apiEnabled: true,
2211
- apiMethods: ["get", "list", "delete"],
2325
+ // All mutations (create/update/delete) must go through better-auth's
2326
+ // oauth-provider endpoints under /api/v1/auth/{admin/,}oauth2/* — the
2327
+ // generic data layer is read-only for this object so sysadmins cannot
2328
+ // bypass server-side OAuth validation. The Delete row action above is
2329
+ // wired to /api/v1/auth/oauth2/delete-client.
2330
+ apiMethods: ["get", "list"],
2212
2331
  trash: false,
2213
2332
  mru: false
2214
2333
  }
@@ -2491,6 +2610,83 @@ var SysRole = ObjectSchema.create({
2491
2610
  displayNameField: "label",
2492
2611
  titleFormat: "{label}",
2493
2612
  compactLayout: ["label", "name", "active", "is_default"],
2613
+ // Custom actions — system roles drive RBAC and are edited rarely but
2614
+ // require the four high-frequency sysadmin affordances every IdP
2615
+ // (Salesforce, ServiceNow, Okta) ships: activate/deactivate (lifecycle
2616
+ // without losing assignments), mark default (auto-assign to new users),
2617
+ // and clone (template for new roles). All operations hit the generic
2618
+ // data CRUD endpoint exposed by `apiEnabled` — no custom server route
2619
+ // required because `managedBy: 'config'` allows direct mutation.
2620
+ actions: [
2621
+ {
2622
+ name: "activate_role",
2623
+ label: "Activate Role",
2624
+ icon: "circle-check",
2625
+ variant: "secondary",
2626
+ mode: "custom",
2627
+ locations: ["list_item", "record_header"],
2628
+ type: "api",
2629
+ method: "PATCH",
2630
+ target: "/api/v1/data/sys_role/{id}",
2631
+ bodyExtra: { active: true },
2632
+ successMessage: "Role activated",
2633
+ refreshAfter: true
2634
+ },
2635
+ {
2636
+ name: "deactivate_role",
2637
+ label: "Deactivate Role",
2638
+ icon: "circle-off",
2639
+ variant: "danger",
2640
+ mode: "custom",
2641
+ locations: ["list_item", "record_header"],
2642
+ type: "api",
2643
+ method: "PATCH",
2644
+ target: "/api/v1/data/sys_role/{id}",
2645
+ bodyExtra: { active: false },
2646
+ confirmText: "Deactivate this role? Users with the role keep their assignment but the role stops granting permissions until re-activated.",
2647
+ successMessage: "Role deactivated",
2648
+ refreshAfter: true
2649
+ },
2650
+ {
2651
+ name: "set_default_role",
2652
+ label: "Set as Default",
2653
+ icon: "star",
2654
+ variant: "secondary",
2655
+ mode: "custom",
2656
+ locations: ["list_item", "record_header"],
2657
+ type: "api",
2658
+ method: "PATCH",
2659
+ target: "/api/v1/data/sys_role/{id}",
2660
+ bodyExtra: { is_default: true },
2661
+ confirmText: "Make this the default role for new users? Existing users are unaffected.",
2662
+ successMessage: "Default role updated",
2663
+ refreshAfter: true
2664
+ },
2665
+ {
2666
+ // Clone — POST a new sys_role row pre-filled from the source. The
2667
+ // dialog asks only for the new API name / label so the operator
2668
+ // can rename atomically; permissions JSON is copied wholesale via
2669
+ // defaultFromRow.
2670
+ name: "clone_role",
2671
+ label: "Clone Role",
2672
+ icon: "copy",
2673
+ variant: "secondary",
2674
+ mode: "custom",
2675
+ locations: ["list_item", "record_header"],
2676
+ type: "api",
2677
+ method: "POST",
2678
+ target: "/api/v1/data/sys_role",
2679
+ bodyExtra: { is_default: false, active: true },
2680
+ successMessage: "Role cloned",
2681
+ refreshAfter: true,
2682
+ params: [
2683
+ { name: "label", label: "New Display Name", type: "text", required: true },
2684
+ { name: "name", label: "New API Name", type: "text", required: true, helpText: "Unique snake_case machine name" },
2685
+ { field: "description", defaultFromRow: true },
2686
+ { field: "permissions", defaultFromRow: true }
2687
+ ]
2688
+ }
2689
+ ],
2494
2690
  listViews: {
2495
2691
  active: {
2496
2692
  type: "grid",
@@ -2617,6 +2813,64 @@ var SysPermissionSet = ObjectSchema.create({
2617
2813
  displayNameField: "label",
2618
2814
  titleFormat: "{label}",
2619
2815
  compactLayout: ["label", "name", "active"],
2816
+ // Custom actions — permission sets are templates assigned to roles or
2817
+ // users (via sys_role_permission_set / sys_user_permission_set). The
2818
+ // sysadmin operations that don't live on the parent-detail tabs are
2819
+ // lifecycle (activate/deactivate without losing assignments) and
2820
+ // clone (build a new permset by tweaking an existing one). Both hit
2821
+ // the generic data CRUD endpoint — managedBy: 'config' permits it.
2822
+ actions: [
2823
+ {
2824
+ name: "activate_permission_set",
2825
+ label: "Activate",
2826
+ icon: "circle-check",
2827
+ variant: "secondary",
2828
+ mode: "custom",
2829
+ locations: ["list_item", "record_header"],
2830
+ type: "api",
2831
+ method: "PATCH",
2832
+ target: "/api/v1/data/sys_permission_set/{id}",
2833
+ bodyExtra: { active: true },
2834
+ successMessage: "Permission set activated",
2835
+ refreshAfter: true
2836
+ },
2837
+ {
2838
+ name: "deactivate_permission_set",
2839
+ label: "Deactivate",
2840
+ icon: "circle-off",
2841
+ variant: "danger",
2842
+ mode: "custom",
2843
+ locations: ["list_item", "record_header"],
2844
+ type: "api",
2845
+ method: "PATCH",
2846
+ target: "/api/v1/data/sys_permission_set/{id}",
2847
+ bodyExtra: { active: false },
2848
+ confirmText: "Deactivate this permission set? Existing assignments stay in place but stop granting access until re-activated.",
2849
+ successMessage: "Permission set deactivated",
2850
+ refreshAfter: true
2851
+ },
2852
+ {
2853
+ name: "clone_permission_set",
2854
+ label: "Clone",
2855
+ icon: "copy",
2856
+ variant: "secondary",
2857
+ mode: "custom",
2858
+ locations: ["list_item", "record_header"],
2859
+ type: "api",
2860
+ method: "POST",
2861
+ target: "/api/v1/data/sys_permission_set",
2862
+ bodyExtra: { active: true },
2863
+ successMessage: "Permission set cloned",
2864
+ refreshAfter: true,
2865
+ params: [
2866
+ { name: "label", label: "New Display Name", type: "text", required: true },
2867
+ { name: "name", label: "New API Name", type: "text", required: true, helpText: "Unique snake_case machine name" },
2868
+ { field: "description", defaultFromRow: true },
2869
+ { field: "object_permissions", defaultFromRow: true },
2870
+ { field: "field_permissions", defaultFromRow: true }
2871
+ ]
2872
+ }
2873
+ ],
2620
2874
  listViews: {
2621
2875
  active: {
2622
2876
  type: "grid",