@farthershore/farthershore-js 0.26.0 → 0.28.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/CHANGELOG.md +71 -0
- package/README.md +1 -1
- package/dist/cache.d.ts +1 -1
- package/dist/cache.js +1 -1
- package/dist/component-policy.d.ts +25 -1
- package/dist/component-policy.d.ts.map +1 -1
- package/dist/component-policy.js +59 -4
- package/dist/component-policy.js.map +1 -1
- package/dist/components/access-control.d.ts.map +1 -1
- package/dist/components/access-control.js +73 -7
- package/dist/components/access-control.js.map +1 -1
- package/dist/components/api-keys-manager.d.ts +7 -0
- package/dist/components/api-keys-manager.d.ts.map +1 -0
- package/dist/components/api-keys-manager.js +238 -0
- package/dist/components/api-keys-manager.js.map +1 -0
- package/dist/components/audit-log-table.d.ts.map +1 -1
- package/dist/components/audit-log-table.js +24 -3
- package/dist/components/audit-log-table.js.map +1 -1
- package/dist/components/bill-preview-card.d.ts +6 -7
- package/dist/components/bill-preview-card.d.ts.map +1 -1
- package/dist/components/bill-preview-card.js +93 -24
- package/dist/components/bill-preview-card.js.map +1 -1
- package/dist/components/billing.d.ts.map +1 -1
- package/dist/components/billing.js +9 -2
- package/dist/components/billing.js.map +1 -1
- package/dist/components/cancel-subscription.d.ts +4 -6
- package/dist/components/cancel-subscription.d.ts.map +1 -1
- package/dist/components/cancel-subscription.js +113 -40
- package/dist/components/cancel-subscription.js.map +1 -1
- package/dist/components/component-access-panel.d.ts.map +1 -1
- package/dist/components/component-access-panel.js +10 -3
- package/dist/components/component-access-panel.js.map +1 -1
- package/dist/components/index.d.ts +12 -4
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +13 -2
- package/dist/components/index.js.map +1 -1
- package/dist/components/keys.d.ts.map +1 -1
- package/dist/components/keys.js +7 -1
- package/dist/components/keys.js.map +1 -1
- package/dist/components/notification-preferences.d.ts +7 -0
- package/dist/components/notification-preferences.d.ts.map +1 -0
- package/dist/components/notification-preferences.js +119 -0
- package/dist/components/notification-preferences.js.map +1 -0
- package/dist/components/onboarding-plan-rail.d.ts +22 -0
- package/dist/components/onboarding-plan-rail.d.ts.map +1 -0
- package/dist/components/onboarding-plan-rail.js +103 -0
- package/dist/components/onboarding-plan-rail.js.map +1 -0
- package/dist/components/org-switcher.d.ts +11 -6
- package/dist/components/org-switcher.d.ts.map +1 -1
- package/dist/components/org-switcher.js +88 -22
- package/dist/components/org-switcher.js.map +1 -1
- package/dist/components/payment-required-gate.d.ts.map +1 -1
- package/dist/components/payment-required-gate.js +13 -5
- package/dist/components/payment-required-gate.js.map +1 -1
- package/dist/components/permission-gate.d.ts.map +1 -1
- package/dist/components/permission-gate.js +26 -10
- package/dist/components/permission-gate.js.map +1 -1
- package/dist/components/pricing-plan-card.d.ts +11 -0
- package/dist/components/pricing-plan-card.d.ts.map +1 -0
- package/dist/components/pricing-plan-card.js +15 -0
- package/dist/components/pricing-plan-card.js.map +1 -0
- package/dist/components/team.d.ts +92 -0
- package/dist/components/team.d.ts.map +1 -0
- package/dist/components/team.js +104 -0
- package/dist/components/team.js.map +1 -0
- package/dist/components/usage-limits.d.ts +1 -1
- package/dist/components/usage-limits.d.ts.map +1 -1
- package/dist/components/usage-limits.js +26 -3
- package/dist/components/usage-limits.js.map +1 -1
- package/dist/components/usage-surfaces.d.ts +43 -0
- package/dist/components/usage-surfaces.d.ts.map +1 -0
- package/dist/components/usage-surfaces.js +70 -0
- package/dist/components/usage-surfaces.js.map +1 -0
- package/dist/format/catalog-display.d.ts +8 -1
- package/dist/format/catalog-display.d.ts.map +1 -1
- package/dist/format/catalog-display.js +28 -8
- package/dist/format/catalog-display.js.map +1 -1
- package/dist/format/index.d.ts +1 -1
- package/dist/format/index.d.ts.map +1 -1
- package/dist/format/index.js +4 -1
- package/dist/format/index.js.map +1 -1
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mock/transport.d.ts.map +1 -1
- package/dist/mock/transport.js +0 -4
- package/dist/mock/transport.js.map +1 -1
- package/dist/react/hooks.d.ts +0 -9
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/hooks.js +0 -6
- package/dist/react/hooks.js.map +1 -1
- package/dist/resources/billing.d.ts +0 -12
- package/dist/resources/billing.d.ts.map +1 -1
- package/dist/resources/billing.js +0 -14
- package/dist/resources/billing.js.map +1 -1
- package/dist/test-utils/mock-client.d.ts.map +1 -1
- package/dist/test-utils/mock-client.js +0 -6
- package/dist/test-utils/mock-client.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/components/move-to-latest.d.ts +0 -16
- package/dist/components/move-to-latest.d.ts.map +0 -1
- package/dist/components/move-to-latest.js +0 -68
- package/dist/components/move-to-latest.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,77 @@
|
|
|
3
3
|
All notable changes to `@farthershore/farthershore-js` are documented here.
|
|
4
4
|
This project adheres to Semantic Versioning (pre-1.0: minors may break).
|
|
5
5
|
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.28.0] - 2026-09-03
|
|
9
|
+
|
|
10
|
+
### Changed — `FsUsageLimits` is now render-gated on `usage_limit:read` (BREAKING)
|
|
11
|
+
|
|
12
|
+
`FsUsageLimits` joined the managed component map as `usage_limits` and is
|
|
13
|
+
wrapped in `withGate`: a viewer WITHOUT `usage_limit:read` no longer mounts
|
|
14
|
+
the component (previously it mounted anywhere and attempted the load, then
|
|
15
|
+
surfaced the API error). A consumer mounting it outside an already-gated
|
|
16
|
+
route now sees the resolved deny rendering (`AccessDenied` by default, or
|
|
17
|
+
nothing/disabled per the org's configured mode) instead of the old
|
|
18
|
+
error-loading card. The three mutating controls keep their own
|
|
19
|
+
per-control `usePermissionAction` gates. Additionally, a viewer whose
|
|
20
|
+
policy resolves a MOUNTING deny mode (`readOnly` / `disable`) but who
|
|
21
|
+
cannot read the backing data renders `AccessDenied` — the panel's list
|
|
22
|
+
fetch requires `usage_limit:read` server-side, so a mounted-but-unfetchable
|
|
23
|
+
view would be an error card, not a read-only view.
|
|
24
|
+
|
|
25
|
+
### Changed — unconfigured component denies render an AccessDenied panel (BREAKING, visual)
|
|
26
|
+
|
|
27
|
+
The default gate mode for a component with NO configured mode (no builder
|
|
28
|
+
`registerComponent` gateMode, no subscriber-org override) is now **per-key**
|
|
29
|
+
via the new `defaultGateModeFor(componentKey)`: `"denied"` for most
|
|
30
|
+
components, `"hide"` only for the security-sensitive set (`audit_log`,
|
|
31
|
+
`api_keys_panel`, `team_panel`), whose existence is the confidential fact.
|
|
32
|
+
Previously every unconfigured deny was `"hide"`, which made a permission miss
|
|
33
|
+
look like a rendering bug.
|
|
34
|
+
|
|
35
|
+
What changes visually: an embedded data component (`UsageCard`,
|
|
36
|
+
`BillingSummary`, a `withGate`-wrapped custom component, …) shown to a member
|
|
37
|
+
without its permission now renders the explicit "you don't have access" panel
|
|
38
|
+
instead of vanishing. The dev-portal template's component-backed route gates
|
|
39
|
+
stop forcing `mode="denied"` and follow the resolver instead: most denied
|
|
40
|
+
route visits still render the AccessDenied panel (now via the per-key
|
|
41
|
+
default), but the SENSITIVE routes (`/dashboard/keys`, `/audit-log`) render
|
|
42
|
+
NOTHING for a denied member unless the org configured a mode — their
|
|
43
|
+
existence is the confidential fact, and the old forced mode leaked it. A
|
|
44
|
+
bare `<PermissionGate permission="…">` (no `component`) still defaults to
|
|
45
|
+
`hide` — hiding an affordance a member cannot use stays correct.
|
|
46
|
+
|
|
47
|
+
Also: `SENSITIVE_COMPONENT_KEYS`, `isSensitiveComponentKey`, and
|
|
48
|
+
`defaultGateModeFor` are exported publicly, and `registerComponent` /
|
|
49
|
+
`unregisterComponent` / `ComponentRegistration` are re-exported from
|
|
50
|
+
`@farthershore/farthershore-js/components`.
|
|
51
|
+
|
|
52
|
+
## [0.27.0] - 2026-08-31
|
|
53
|
+
|
|
54
|
+
### Removed — subscriber-facing "move to latest plan" (BREAKING)
|
|
55
|
+
|
|
56
|
+
The billing redesign decoupled meters from plans, which removed the reason this
|
|
57
|
+
control existed. A release's `measurement_schema` is built from the
|
|
58
|
+
business-wide commerce manifest (every measure, meter, and route binding), so
|
|
59
|
+
every plan in the release cohort sees a newly declared meter — and new measures
|
|
60
|
+
adopt the current published rate. A subscriber whose plan spec is unchanged
|
|
61
|
+
stays in the new release's cohort and picks the new meter up automatically. The
|
|
62
|
+
old failure mode this control worked around — "the feature I want reports a
|
|
63
|
+
meter my pinned plan's dimension map doesn't have, so I must move plans" — no
|
|
64
|
+
longer exists.
|
|
65
|
+
|
|
66
|
+
- **REMOVED `MoveToLatestPlan`** and its `MoveToLatestPlanProps`.
|
|
67
|
+
- **REMOVED `fs.billing.migrateToLatest()`** and **`useBilling().migrateToLatest`**.
|
|
68
|
+
- The backing `POST /portal/businesses/:businessId/me/migrate-latest` route is
|
|
69
|
+
removed from core in the same change, along with the unused
|
|
70
|
+
`POST /consumer/subscriptions/:subscriptionId/migrate-latest`.
|
|
71
|
+
|
|
72
|
+
Converging a subscriber whose plan's own economics changed (price, included
|
|
73
|
+
allowance, funding buckets, limits) is unchanged and remains an OPERATOR action:
|
|
74
|
+
the builder's Users tab, or `farthershore consumer migrate-latest`. Subscribers
|
|
75
|
+
still change plans with `changePlan()`.
|
|
76
|
+
|
|
6
77
|
## [0.26.0] - 2026-08-30
|
|
7
78
|
|
|
8
79
|
Three efforts ship together: the billing redesign (already on `main` but never
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The **Farther Shore Frontend SDK** — the browser + React integration layer for
|
|
4
4
|
your product's subscriber portal.
|
|
5
5
|
|
|
6
|
-
> **Status: `0.
|
|
6
|
+
> **Status: `0.27.0` (pre-1.0).** A MINOR bump may include breaking changes;
|
|
7
7
|
> patches are additive. Pin the version you build against and read
|
|
8
8
|
> [`CHANGELOG.md`](./CHANGELOG.md) before upgrading.
|
|
9
9
|
|
package/dist/cache.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export declare function bustReadCache(ctx: ClientContext): void;
|
|
|
25
25
|
* `setSessionToken()` landing after a 401 left the payment gate open (its `/me`
|
|
26
26
|
* read had FAILED, and failures evict rather than cache — so there was no entry
|
|
27
27
|
* to drop, only a live reader to poke), and sibling billing surfaces kept the
|
|
28
|
-
* old plan after `
|
|
28
|
+
* old plan after `changePlan()`.
|
|
29
29
|
*
|
|
30
30
|
* Listeners are woken unconditionally, never gated on the cache being
|
|
31
31
|
* non-empty, precisely because the failed-read case leaves nothing cached.
|
package/dist/cache.js
CHANGED
|
@@ -50,7 +50,7 @@ export function bustReadCache(ctx) {
|
|
|
50
50
|
* `setSessionToken()` landing after a 401 left the payment gate open (its `/me`
|
|
51
51
|
* read had FAILED, and failures evict rather than cache — so there was no entry
|
|
52
52
|
* to drop, only a live reader to poke), and sibling billing surfaces kept the
|
|
53
|
-
* old plan after `
|
|
53
|
+
* old plan after `changePlan()`.
|
|
54
54
|
*
|
|
55
55
|
* Listeners are woken unconditionally, never gated on the cache being
|
|
56
56
|
* non-empty, precisely because the failed-read case leaves nothing cached.
|
|
@@ -16,12 +16,31 @@ export interface ComponentPermissionDefault {
|
|
|
16
16
|
* (parity-tested in test/component-policy-parity.test.ts — the shipped SDK
|
|
17
17
|
* bundle must stay contracts-free). */
|
|
18
18
|
export declare const MANAGED_COMPONENT_DEFAULTS: Readonly<Record<string, ComponentPermissionDefault>>;
|
|
19
|
+
/** FAITHFUL COPY of contracts `DATA_FLOOR_COMPONENT_KEYS` (parity-tested):
|
|
20
|
+
* the managed default permission is a DATA floor — the panel's fetch is
|
|
21
|
+
* server-gated on it regardless of a subscriber-org re-gate, so it stays an
|
|
22
|
+
* AND requirement alongside any override (Codex #473). */
|
|
23
|
+
export declare const DATA_FLOOR_COMPONENT_KEYS: readonly string[];
|
|
24
|
+
/** The managed data-floor permission for `componentId`, or null when the id
|
|
25
|
+
* carries none — the AND requirement its server-gated fetch imposes
|
|
26
|
+
* regardless of overrides. */
|
|
27
|
+
export declare function dataFloorPermissionFor(componentId: string): string | null;
|
|
19
28
|
/** FAITHFUL COPY of contracts/evolution `SENSITIVE_COMPONENT_KEYS`.
|
|
20
29
|
* The SDK cannot import contracts at runtime, so parity is pinned in test/. */
|
|
21
30
|
export declare const SENSITIVE_COMPONENT_KEYS: readonly ["audit_log", "api_keys_panel", "team_panel"];
|
|
31
|
+
/**
|
|
32
|
+
* FAITHFUL COPY of contracts `defaultGateModeFor` (Subscriber-RBAC R5) — the
|
|
33
|
+
* gate mode used when NOTHING configured one. Non-sensitive components render
|
|
34
|
+
* an explicit AccessDenied (`"denied"`) instead of silently vanishing; the
|
|
35
|
+
* sensitive set keeps `"hide"` (their existence is the confidential fact).
|
|
36
|
+
* Parity is pinned in test/component-policy-parity.test.ts.
|
|
37
|
+
*/
|
|
38
|
+
export declare function defaultGateModeFor(componentId: string): "hide" | "denied";
|
|
22
39
|
/** True iff `id` names a confidentiality-sensitive managed component. */
|
|
23
40
|
export declare function isSensitiveComponentKey(id: string): boolean;
|
|
24
|
-
/** Faithful copy of the contracts `CUSTOM_COMPONENT_KEY_PATTERN
|
|
41
|
+
/** Faithful copy of the contracts `CUSTOM_COMPONENT_KEY_PATTERN` — the slug
|
|
42
|
+
* budget matches the custom-subject grammar (≤32) so the derived permission
|
|
43
|
+
* subject is always declarable. */
|
|
25
44
|
export declare const CUSTOM_COMPONENT_KEY_PATTERN: RegExp;
|
|
26
45
|
/** True iff `id` is a well-formed builder-custom component key. */
|
|
27
46
|
export declare function isCustomComponentKey(id: string): boolean;
|
|
@@ -62,6 +81,11 @@ export interface ResolvedComponentPolicy {
|
|
|
62
81
|
writePermission: string;
|
|
63
82
|
/** How the component renders when the viewer lacks `permission`. */
|
|
64
83
|
gateMode: ComponentGateMode;
|
|
84
|
+
/** Where `gateMode` came from: an explicit host registration, a
|
|
85
|
+
* subscriber-org overlay, or the platform default. Hosts that wrap a
|
|
86
|
+
* component-gated surface (e.g. a route) must not force their own mode
|
|
87
|
+
* when the source is not `"default"` — the configured choice wins. */
|
|
88
|
+
gateModeSource: "registration" | "overlay" | "default";
|
|
65
89
|
/** Renders ungated (explicit flag only). */
|
|
66
90
|
presentational: boolean;
|
|
67
91
|
/** Whether the id is one of the contracts-owned sensitive components. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component-policy.d.ts","sourceRoot":"","sources":["../src/component-policy.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAE3D;oEACoE;AACpE,eAAO,MAAM,oBAAoB,oDAKvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,iBAAiB,CAE3E;AAED;0CAC0C;AAC1C,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,IAAI,CAAC;CACvB;AAED;;wCAEwC;AACxC,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAC/C,MAAM,CAAC,MAAM,EAAE,0BAA0B,CAAC,
|
|
1
|
+
{"version":3,"file":"component-policy.d.ts","sourceRoot":"","sources":["../src/component-policy.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAE3D;oEACoE;AACpE,eAAO,MAAM,oBAAoB,oDAKvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,iBAAiB,CAE3E;AAED;0CAC0C;AAC1C,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,IAAI,CAAC;CACvB;AAED;;wCAEwC;AACxC,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAC/C,MAAM,CAAC,MAAM,EAAE,0BAA0B,CAAC,CAuC3C,CAAC;AAEF;;;2DAG2D;AAC3D,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EAAqB,CAAC;AAE7E;;+BAE+B;AAC/B,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGzE;AAED;gFACgF;AAChF,eAAO,MAAM,wBAAwB,wDAI3B,CAAC;AAEX;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAEzE;AAED,yEAAyE;AACzE,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE3D;AAaD;;oCAEoC;AACpC,eAAO,MAAM,4BAA4B,QAAmC,CAAC;AAE7E,mEAAmE;AACnE,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAExD;AAMD,MAAM,WAAW,qBAAqB;IACpC;wCACoC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX;sBACkB;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,iEAAiE;IACjE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAID;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,qBAAqB,GAAG,IAAI,CAgB3E;AAED,qDAAqD;AACrD,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAEpD;AAED,4CAA4C;AAC5C,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAMD,qDAAqD;AACrD,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,+DAA+D;IAC/D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;4EACwE;IACxE,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,2DAA2D;IAC3D,eAAe,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,QAAQ,EAAE,iBAAiB,CAAC;IAC5B;;;2EAGuE;IACvE,cAAc,EAAE,cAAc,GAAG,SAAS,GAAG,SAAS,CAAC;IACvD,4CAA4C;IAC5C,cAAc,EAAE,OAAO,CAAC;IACxB,yEAAyE;IACzE,SAAS,EAAE,OAAO,CAAC;IACnB;yDACqD;IACrD,OAAO,EAAE,OAAO,CAAC;CAClB;AAiBD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,SAAS,wBAAwB,EAAE,GAAG,IAAI,GACpD,uBAAuB,CAoGzB"}
|
package/dist/component-policy.js
CHANGED
|
@@ -62,7 +62,37 @@ export const MANAGED_COMPONENT_DEFAULTS = {
|
|
|
62
62
|
audit_log: { permission: "audit_log:read" },
|
|
63
63
|
team_panel: { permission: "team:read", writePermission: "team:manage_rbac" },
|
|
64
64
|
markdown: { permission: null, presentational: true },
|
|
65
|
+
usage_limits: {
|
|
66
|
+
permission: "usage_limit:read",
|
|
67
|
+
// Primary mutating verb (create); update/delete controls gate on their
|
|
68
|
+
// own concrete verbs at the call site (per-control > panel default —
|
|
69
|
+
// the api_keys_panel pattern). NOT sensitive: a member reading the
|
|
70
|
+
// limits set on them is a product feature, and the destructive verb
|
|
71
|
+
// (usage_limit:delete) is already escalatory / grant-by-exact-name.
|
|
72
|
+
writePermission: "usage_limit:create",
|
|
73
|
+
},
|
|
74
|
+
notification_preferences: {
|
|
75
|
+
// notification:read is the concrete self-scoped inbox verb, and it
|
|
76
|
+
// includes updating the current member's OWN channel opt-outs — reading
|
|
77
|
+
// and muting your own notifications are one capability, so read and
|
|
78
|
+
// write share the verb.
|
|
79
|
+
permission: "notification:read",
|
|
80
|
+
writePermission: "notification:read",
|
|
81
|
+
},
|
|
65
82
|
};
|
|
83
|
+
/** FAITHFUL COPY of contracts `DATA_FLOOR_COMPONENT_KEYS` (parity-tested):
|
|
84
|
+
* the managed default permission is a DATA floor — the panel's fetch is
|
|
85
|
+
* server-gated on it regardless of a subscriber-org re-gate, so it stays an
|
|
86
|
+
* AND requirement alongside any override (Codex #473). */
|
|
87
|
+
export const DATA_FLOOR_COMPONENT_KEYS = ["usage_limits"];
|
|
88
|
+
/** The managed data-floor permission for `componentId`, or null when the id
|
|
89
|
+
* carries none — the AND requirement its server-gated fetch imposes
|
|
90
|
+
* regardless of overrides. */
|
|
91
|
+
export function dataFloorPermissionFor(componentId) {
|
|
92
|
+
if (!DATA_FLOOR_COMPONENT_KEYS.includes(componentId))
|
|
93
|
+
return null;
|
|
94
|
+
return MANAGED_COMPONENT_DEFAULTS[componentId]?.permission ?? null;
|
|
95
|
+
}
|
|
66
96
|
/** FAITHFUL COPY of contracts/evolution `SENSITIVE_COMPONENT_KEYS`.
|
|
67
97
|
* The SDK cannot import contracts at runtime, so parity is pinned in test/. */
|
|
68
98
|
export const SENSITIVE_COMPONENT_KEYS = [
|
|
@@ -70,6 +100,16 @@ export const SENSITIVE_COMPONENT_KEYS = [
|
|
|
70
100
|
"api_keys_panel",
|
|
71
101
|
"team_panel",
|
|
72
102
|
];
|
|
103
|
+
/**
|
|
104
|
+
* FAITHFUL COPY of contracts `defaultGateModeFor` (Subscriber-RBAC R5) — the
|
|
105
|
+
* gate mode used when NOTHING configured one. Non-sensitive components render
|
|
106
|
+
* an explicit AccessDenied (`"denied"`) instead of silently vanishing; the
|
|
107
|
+
* sensitive set keeps `"hide"` (their existence is the confidential fact).
|
|
108
|
+
* Parity is pinned in test/component-policy-parity.test.ts.
|
|
109
|
+
*/
|
|
110
|
+
export function defaultGateModeFor(componentId) {
|
|
111
|
+
return isSensitiveComponentKey(componentId) ? "hide" : "denied";
|
|
112
|
+
}
|
|
73
113
|
/** True iff `id` names a confidentiality-sensitive managed component. */
|
|
74
114
|
export function isSensitiveComponentKey(id) {
|
|
75
115
|
return SENSITIVE_COMPONENT_KEYS.includes(id);
|
|
@@ -79,8 +119,10 @@ function managedDefault(componentId) {
|
|
|
79
119
|
? MANAGED_COMPONENT_DEFAULTS[componentId]
|
|
80
120
|
: undefined;
|
|
81
121
|
}
|
|
82
|
-
/** Faithful copy of the contracts `CUSTOM_COMPONENT_KEY_PATTERN
|
|
83
|
-
|
|
122
|
+
/** Faithful copy of the contracts `CUSTOM_COMPONENT_KEY_PATTERN` — the slug
|
|
123
|
+
* budget matches the custom-subject grammar (≤32) so the derived permission
|
|
124
|
+
* subject is always declarable. */
|
|
125
|
+
export const CUSTOM_COMPONENT_KEY_PATTERN = /^custom:[a-z][a-z0-9_-]{1,31}$/;
|
|
84
126
|
/** True iff `id` is a well-formed builder-custom component key. */
|
|
85
127
|
export function isCustomComponentKey(id) {
|
|
86
128
|
return CUSTOM_COMPONENT_KEY_PATTERN.test(id);
|
|
@@ -148,6 +190,7 @@ export function resolveComponentPolicy(componentId, overlays) {
|
|
|
148
190
|
requiredPermissions: [],
|
|
149
191
|
writePermission: `${derivedSubject(componentId)}:write`,
|
|
150
192
|
gateMode: "denied",
|
|
193
|
+
gateModeSource: "default",
|
|
151
194
|
presentational: false,
|
|
152
195
|
sensitive: false,
|
|
153
196
|
unknown: true,
|
|
@@ -169,7 +212,14 @@ export function resolveComponentPolicy(componentId, overlays) {
|
|
|
169
212
|
const overlayGateMode = overlay?.gateMode != null && isComponentGateMode(overlay.gateMode)
|
|
170
213
|
? overlay.gateMode
|
|
171
214
|
: undefined;
|
|
172
|
-
const requestedGateMode = registration?.gateMode ??
|
|
215
|
+
const requestedGateMode = registration?.gateMode ??
|
|
216
|
+
overlayGateMode ??
|
|
217
|
+
defaultGateModeFor(componentId);
|
|
218
|
+
const gateModeSource = registration?.gateMode !== undefined
|
|
219
|
+
? "registration"
|
|
220
|
+
: overlayGateMode !== undefined
|
|
221
|
+
? "overlay"
|
|
222
|
+
: "default";
|
|
173
223
|
// `disable` and `readOnly` mount denied children. A sensitive component's
|
|
174
224
|
// managed hide default is a confidentiality floor, so only non-mounting
|
|
175
225
|
// deny modes may survive an overlay/registration.
|
|
@@ -180,7 +230,8 @@ export function resolveComponentPolicy(componentId, overlays) {
|
|
|
180
230
|
const permissionCandidates = presentational
|
|
181
231
|
? []
|
|
182
232
|
: [
|
|
183
|
-
...(sensitive
|
|
233
|
+
...((sensitive || DATA_FLOOR_COMPONENT_KEYS.includes(componentId)) &&
|
|
234
|
+
managed?.permission != null
|
|
184
235
|
? [managed.permission]
|
|
185
236
|
: []),
|
|
186
237
|
...(permission != null ? [permission] : []),
|
|
@@ -202,6 +253,10 @@ export function resolveComponentPolicy(componentId, overlays) {
|
|
|
202
253
|
managed?.writePermission ??
|
|
203
254
|
`${subject}:write`,
|
|
204
255
|
gateMode: denyByDefault ? "denied" : gateMode,
|
|
256
|
+
// A denyByDefault (malformed policy) forces "denied" regardless of the
|
|
257
|
+
// configured mode, so the source degrades to "default" — a wrapper must
|
|
258
|
+
// not treat a broken configuration as an explicit choice.
|
|
259
|
+
gateModeSource: denyByDefault ? "default" : gateModeSource,
|
|
205
260
|
presentational,
|
|
206
261
|
sensitive,
|
|
207
262
|
unknown: denyByDefault,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component-policy.js","sourceRoot":"","sources":["../src/component-policy.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,8EAA8E;AAC9E,6EAA6E;AAC7E,mEAAmE;AACnE,EAAE;AACF,gEAAgE;AAChE,+DAA+D;AAC/D,2EAA2E;AAC3E,6DAA6D;AAC7D,6EAA6E;AAC7E,qCAAqC;AACrC,gEAAgE;AAChE,wDAAwD;AACxD,EAAE;AACF,mEAAmE;AACnE,wEAAwE;AACxE,mEAAmE;AACnE,8CAA8C;AAC9C,EAAE;AACF,yEAAyE;AACzE,wEAAwE;AACxE,iEAAiE;AACjE,EAAE;AACF,4EAA4E;AAC5E,4BAA4B;AAC5B,8EAA8E;AAE9E,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAGhE;oEACoE;AACpE,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,MAAM;IACN,SAAS;IACT,UAAU;IACV,QAAQ;CACA,CAAC;AAIX,mEAAmE;AACnE,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,OAAQ,oBAA0C,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACpE,CAAC;AAUD;;wCAEwC;AACxC,MAAM,CAAC,MAAM,0BAA0B,GAEnC;IACF,WAAW,EAAE;QACX,UAAU,EAAE,mBAAmB;QAC/B,uEAAuE;QACvE,oEAAoE;QACpE,sDAAsD;QACtD,eAAe,EAAE,0BAA0B;KAC5C;IACD,UAAU,EAAE,EAAE,UAAU,EAAE,YAAY,EAAE;IACxC,cAAc,EAAE;QACd,UAAU,EAAE,aAAa;QACzB,uEAAuE;QACvE,qEAAqE;QACrE,eAAe,EAAE,eAAe;KACjC;IACD,eAAe,EAAE,EAAE,UAAU,EAAE,cAAc,EAAE;IAC/C,WAAW,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE;IACjC,aAAa,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE;IACzD,SAAS,EAAE,EAAE,UAAU,EAAE,gBAAgB,EAAE;IAC3C,UAAU,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,kBAAkB,EAAE;IAC5E,QAAQ,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE;
|
|
1
|
+
{"version":3,"file":"component-policy.js","sourceRoot":"","sources":["../src/component-policy.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-control.d.ts","sourceRoot":"","sources":["../../src/components/access-control.tsx"],"names":[],"mappings":"AA8CA;;8DAE8D;AAC9D,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"access-control.d.ts","sourceRoot":"","sources":["../../src/components/access-control.tsx"],"names":[],"mappings":"AA8CA;;8DAE8D;AAC9D,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AAyEzD,MAAM,WAAW,iBAAiB;IAChC,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;6EAGyE;IACzE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;oDACgD;IAChD,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,EAC3B,SAAS,EACT,SAAS,EACT,cAAc,GACf,GAAE,iBAAsB,sCAuexB;AAID,MAAM,WAAW,kBAAkB;IACjC,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+DAC2D;IAC3D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,qEAAqE;IACrE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;uDAEmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,EAC5B,SAAS,EACT,SAAS,EACT,SAAS,EACT,KAAsB,EACtB,aAAa,GACd,GAAE,kBAAuB,+BA0PzB;AAMD,MAAM,WAAW,oBAAoB;IACnC,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;sCAEkC;IAClC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;0DACsD;IACtD,cAAc,CAAC,EAAE,SAAS;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC/D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,EAC9B,SAAS,EACT,SAAiB,EACjB,cAAmB,GACpB,GAAE,oBAAyB,sCAgM3B;AAID,MAAM,WAAW,mBAAmB;IAClC,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;iEAC6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC7B,SAAS,EACT,KAAK,EAAE,SAAS,EAChB,UAAU,GACX,GAAE,mBAAwB,+BA+D1B;AAID,MAAM,WAAW,oBAAoB;IACnC,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,GAAE,oBAAyB,sCAqJvE;AAID,MAAM,WAAW,0BAA0B;IACzC,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,SAAiB,GAClB,GAAE,0BAA+B,sCAoGjC;AAID,MAAM,WAAW,oBAAoB;IACnC,+BAA+B;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;wDAGoD;IACpD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,qEAAqE;IACrE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC9B,SAAS,EACT,SAAS,EACT,SAAS,GACV,GAAE,oBAAyB,+BAyC3B"}
|
|
@@ -75,6 +75,8 @@ function draftFromRole(role) {
|
|
|
75
75
|
name: role.name,
|
|
76
76
|
permissions: new Set(role.permissions.filter((p) => p !== "*")),
|
|
77
77
|
fullAccess: role.permissions.includes("*"),
|
|
78
|
+
isAccountRole: role.kind === "ACCOUNT",
|
|
79
|
+
vocabulary: role.vocabulary,
|
|
78
80
|
};
|
|
79
81
|
}
|
|
80
82
|
function draftPermissions(draft) {
|
|
@@ -90,12 +92,24 @@ function draftPermissions(draft) {
|
|
|
90
92
|
export function FsRoleEditor({ className, canManage, onRolesChanged, } = {}) {
|
|
91
93
|
const fs = useFartherShore();
|
|
92
94
|
const auth = useOptionalFsAuth();
|
|
93
|
-
|
|
95
|
+
// The /me claim is PRODUCT-plane (ACCOUNT rows deliberately excluded), so
|
|
96
|
+
// an ADMIN whose team:manage_rbac comes from account_admin has no claim —
|
|
97
|
+
// yet every read this editor performs is server-gated on that very
|
|
98
|
+
// permission. A successful load IS the live subscriber-plane capability
|
|
99
|
+
// check, so it backs the affordance (Codex #472 round 23). An explicit
|
|
100
|
+
// canManage prop still overrides.
|
|
101
|
+
const [serverCanManage, setServerCanManage] = useState(false);
|
|
102
|
+
const resolvedCanManage = canManage ??
|
|
103
|
+
((auth?.hasPermission(MANAGE_RBAC_PERMISSION) ?? false) || serverCanManage);
|
|
94
104
|
const [status, setStatus] = useState("loading");
|
|
95
105
|
const [settings, setSettings] = useState({ enabled: false });
|
|
96
106
|
const [roles, setRoles] = useState([]);
|
|
97
107
|
const [catalog, setCatalog] = useState([]);
|
|
98
108
|
const [draft, setDraft] = useState(null);
|
|
109
|
+
// ACCOUNT rows enforce for every business, product Managed-RBAC or not —
|
|
110
|
+
// when the settings/catalog surface is product-gated the editor degrades
|
|
111
|
+
// to an account-only view instead of hiding (Codex #472 round 14).
|
|
112
|
+
const [accountOnly, setAccountOnly] = useState(false);
|
|
99
113
|
const [busy, setBusy] = useState(false);
|
|
100
114
|
const [error, setError] = useState(null);
|
|
101
115
|
const loadAll = useCallback(async () => {
|
|
@@ -111,10 +125,40 @@ export function FsRoleEditor({ className, canManage, onRolesChanged, } = {}) {
|
|
|
111
125
|
setSettings(s);
|
|
112
126
|
setCatalog(entries);
|
|
113
127
|
setRoles(roleList);
|
|
128
|
+
// A remount-free client swap (different product) must not inherit a
|
|
129
|
+
// previous account-only degradation (Codex #472 round 19).
|
|
130
|
+
setAccountOnly(false);
|
|
131
|
+
// The settings/catalog/roles reads are server-gated on
|
|
132
|
+
// team:manage_rbac — reaching here proves the live capability.
|
|
133
|
+
setServerCanManage(true);
|
|
114
134
|
setStatus("ready");
|
|
115
135
|
}
|
|
116
136
|
catch (err) {
|
|
117
137
|
const code = apiErrorCode(err);
|
|
138
|
+
if (code === "RBAC_NOT_ENABLED_BY_PRODUCT") {
|
|
139
|
+
// The role LIST stays reachable: ACCOUNT rows are live enforcement
|
|
140
|
+
// for every business, so surface them in an account-only editor
|
|
141
|
+
// (no enable toggle, no default role, no create/delete).
|
|
142
|
+
try {
|
|
143
|
+
const roleList = await fs.rbac.roles.list();
|
|
144
|
+
const accountRows = roleList.filter((role) => role.kind === "ACCOUNT");
|
|
145
|
+
if (accountRows.length > 0) {
|
|
146
|
+
setSettings({ enabled: false });
|
|
147
|
+
setCatalog([]);
|
|
148
|
+
setRoles(accountRows);
|
|
149
|
+
setAccountOnly(true);
|
|
150
|
+
// The role list is server-gated on team:manage_rbac too.
|
|
151
|
+
setServerCanManage(true);
|
|
152
|
+
setStatus("ready");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// fall through to hidden
|
|
158
|
+
}
|
|
159
|
+
setStatus("hidden");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
118
162
|
if ((code && RBAC_HIDDEN_CODES.has(code)) ||
|
|
119
163
|
(err instanceof FartherShoreApiError && err.status === 403)) {
|
|
120
164
|
setStatus("hidden");
|
|
@@ -156,6 +200,12 @@ export function FsRoleEditor({ className, canManage, onRolesChanged, } = {}) {
|
|
|
156
200
|
permissions: draftPermissions(draft),
|
|
157
201
|
});
|
|
158
202
|
}
|
|
203
|
+
else if (draft.isAccountRole) {
|
|
204
|
+
// ACCOUNT rows: permissions only — a rename is refused server-side.
|
|
205
|
+
await fs.rbac.roles.update(draft.roleKey, {
|
|
206
|
+
permissions: draftPermissions(draft),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
159
209
|
else {
|
|
160
210
|
await fs.rbac.roles.update(draft.roleKey, {
|
|
161
211
|
name: draft.name.trim(),
|
|
@@ -212,22 +262,38 @@ export function FsRoleEditor({ className, canManage, onRolesChanged, } = {}) {
|
|
|
212
262
|
if (status === "error") {
|
|
213
263
|
return (_jsx("section", { className: cx("fs-card", className), children: _jsx("p", { className: "fs-error", children: "Access control settings could not be loaded. Refresh to retry." }) }));
|
|
214
264
|
}
|
|
265
|
+
// ACCOUNT rows are live enforcement whether or not the org's product-role
|
|
266
|
+
// toggle is on — they stay listed and editable with enforcement OFF, while
|
|
267
|
+
// the product-role machinery (default picker, create, CUSTOM rows) shows
|
|
268
|
+
// only when the toggle is ON (Codex #472 rounds 14+17).
|
|
269
|
+
const showFullEditor = settings.enabled && !accountOnly;
|
|
270
|
+
const visibleRoles = showFullEditor
|
|
271
|
+
? roles
|
|
272
|
+
: roles.filter((role) => role.kind === "ACCOUNT");
|
|
215
273
|
const draftValid = draft !== null &&
|
|
216
274
|
draft.name.trim().length > 0 &&
|
|
217
275
|
(draft.mode === "edit" ||
|
|
218
276
|
/^[a-z0-9][a-z0-9_-]{0,63}$/.test(draft.roleKey)) &&
|
|
219
|
-
|
|
220
|
-
|
|
277
|
+
// ACCOUNT rows may be narrowed to NOTHING — the PATCH path accepts
|
|
278
|
+
// permissions: [] (Codex #472 round 17); a CUSTOM role still needs at
|
|
279
|
+
// least one grant (or the "*" claim) to mean anything.
|
|
280
|
+
(draft.fullAccess ||
|
|
281
|
+
draft.isAccountRole === true ||
|
|
282
|
+
draft.permissions.size > 0);
|
|
283
|
+
return (_jsxs("section", { className: cx("fs-card", "fs-rbac", className), "aria-busy": busy, children: [_jsxs("div", { className: "fs-card__head", children: [_jsxs("div", { children: [_jsx("h3", { className: "fs-card__title", children: "Access control" }), _jsx("p", { className: "fs-card__note fs-muted", children: "Role-based access for this subscription's members. Roles limit which product routes a member's API access can call \u2014 enforced at the gateway, configured here." })] }), resolvedCanManage && !accountOnly && (_jsx("button", { type: "button", className: cx("fs-btn fs-btn--sm", settings.enabled ? "fs-btn--outline" : "fs-btn--cta"), disabled: busy, onClick: () => void putSettings({
|
|
221
284
|
enabled: !settings.enabled,
|
|
222
285
|
defaultRoleKey: settings.defaultRoleKey ?? null,
|
|
223
|
-
}), children: settings.enabled ? "Disable" : "Enable role-based access" }))] }), error && (_jsx("p", { className: "fs-error fs-card__note", "aria-live": "polite", children: error })), settings.enabled && (_jsxs("div", { className: "fs-rbac__body", children: [_jsxs("div", { className: "fs-rbac__block", children: [_jsx("label", { className: "fs-label", htmlFor: "fs-rbac-default", children: "Default role for unassigned members" }), _jsxs("select", { id: "fs-rbac-default", className: "fs-select", value: settings.defaultRoleKey ?? "", disabled: !resolvedCanManage || busy, onChange: (e) => void putSettings({
|
|
286
|
+
}), children: settings.enabled ? "Disable" : "Enable role-based access" }))] }), error && (_jsx("p", { className: "fs-error fs-card__note", "aria-live": "polite", children: error })), (settings.enabled || visibleRoles.length > 0) && (_jsxs("div", { className: "fs-rbac__body", children: [showFullEditor && (_jsxs("div", { className: "fs-rbac__block", children: [_jsx("label", { className: "fs-label", htmlFor: "fs-rbac-default", children: "Default role for unassigned members" }), _jsxs("select", { id: "fs-rbac-default", className: "fs-select", value: settings.defaultRoleKey ?? "", disabled: !resolvedCanManage || busy, onChange: (e) => void putSettings({
|
|
224
287
|
enabled: true,
|
|
225
288
|
defaultRoleKey: e.target.value || null,
|
|
226
|
-
}), children: [_jsx("option", { value: "", children: "No default (unassigned members locked out)" }), roles
|
|
289
|
+
}), children: [_jsx("option", { value: "", children: "No default (unassigned members locked out)" }), roles
|
|
290
|
+
.filter((role) => role.kind !== "ACCOUNT")
|
|
291
|
+
.map((role) => (_jsx("option", { value: role.roleKey, children: role.name }, role.roleKey)))] })] })), _jsxs("div", { className: "fs-rbac__block", children: [_jsxs("div", { className: "fs-card__head", children: [_jsx("h4", { className: "fs-rbac__subtitle", children: "Roles" }), resolvedCanManage && !draft && showFullEditor && (_jsx("button", { type: "button", className: "fs-btn fs-btn--outline fs-btn--sm", disabled: busy, onClick: () => setDraft(emptyDraft()), children: "New role" }))] }), visibleRoles.length === 0 ? (_jsx("p", { className: "fs-muted", children: "No roles yet." })) : (_jsx("ul", { className: "fs-rows", children: visibleRoles.map((role) => (_jsxs("li", { className: "fs-row", children: [_jsxs("span", { className: "fs-row__main", children: [role.name, " ", _jsx("code", { className: "fs-code-inline", children: role.roleKey }), _jsx("span", { className: "fs-muted fs-row__label", children: role.permissions.includes("*")
|
|
227
292
|
? "Full access"
|
|
228
|
-
: role.permissions.join(", ") || "—" })] }), role.roleKey === "owner" ? (_jsx("span", { className: "fs-muted fs-row__label", children: "Owner \u2014 always full access, not editable" })) : (resolvedCanManage && (_jsxs("span", { className: "fs-row__actions", children: [_jsx("button", { type: "button", className: "fs-btn fs-btn--outline fs-btn--sm", disabled: busy, onClick: () => setDraft(draftFromRole(role)), children: "Edit" }), _jsx("button", { type: "button", className: "fs-linkbtn fs-linkbtn--danger", disabled: busy, onClick: () => void deleteRole(role.roleKey), children: "Delete" })] })))] }, role.roleKey))) }))] }), resolvedCanManage && draft && (_jsxs("div", { className: "fs-rbac__block fs-rbac__editor", children: [_jsx("h4", { className: "fs-rbac__subtitle", children: draft.mode === "create"
|
|
293
|
+
: role.permissions.join(", ") || "—" })] }), role.roleKey === "owner" ? (_jsx("span", { className: "fs-muted fs-row__label", children: "Owner \u2014 always full access, not editable" })) : (resolvedCanManage && (_jsxs("span", { className: "fs-row__actions", children: [role.kind === "ACCOUNT" && (_jsx("span", { className: "fs-muted fs-row__label", children: "Account role" })), _jsx("button", { type: "button", className: "fs-btn fs-btn--outline fs-btn--sm", disabled: busy, onClick: () => setDraft(draftFromRole(role)), children: "Edit" }), role.kind !== "ACCOUNT" && (_jsx("button", { type: "button", className: "fs-linkbtn fs-linkbtn--danger", disabled: busy, onClick: () => void deleteRole(role.roleKey), children: "Delete" }))] })))] }, role.roleKey))) }))] }), resolvedCanManage && draft && (_jsxs("div", { className: "fs-rbac__block fs-rbac__editor", children: [_jsx("h4", { className: "fs-rbac__subtitle", children: draft.mode === "create"
|
|
229
294
|
? "New role"
|
|
230
|
-
: `Edit role: ${draft.roleKey}` }), _jsxs("div", { className: "fs-card__actions", children: [draft.mode === "create" && (_jsxs("div", { className: "fs-rbac__field", children: [_jsx("label", { className: "fs-label", htmlFor: "fs-rbac-rolekey", children: "Role key" }), _jsx("input", { id: "fs-rbac-rolekey", className: "fs-input", placeholder: "e.g. analyst", value: draft.roleKey, onChange: (e) => setDraft({ ...draft, roleKey: e.target.value }) }), _jsx("p", { className: "fs-muted", children: "Lowercase letters, digits, hyphens, underscores." })] })), _jsxs("div", { className: "fs-rbac__field", children: [_jsx("label", { className: "fs-label", htmlFor: "fs-rbac-name", children: "Display name" }), _jsx("input", { id: "fs-rbac-name", className: "fs-input", placeholder: "e.g. Analyst", value: draft.name, onChange: (e) => setDraft({ ...draft, name: e.target.value }) })] })] }), _jsxs("label", { className: "fs-rbac__fullaccess", children: [_jsx("input", { type: "checkbox", checked: draft.fullAccess, onChange: (e) => setDraft({ ...draft, fullAccess: e.target.checked }) }), " ", "Full access (every route, including future ones)"] }), !draft.fullAccess &&
|
|
295
|
+
: `Edit role: ${draft.roleKey}` }), _jsxs("div", { className: "fs-card__actions", children: [draft.mode === "create" && (_jsxs("div", { className: "fs-rbac__field", children: [_jsx("label", { className: "fs-label", htmlFor: "fs-rbac-rolekey", children: "Role key" }), _jsx("input", { id: "fs-rbac-rolekey", className: "fs-input", placeholder: "e.g. analyst", value: draft.roleKey, onChange: (e) => setDraft({ ...draft, roleKey: e.target.value }) }), _jsx("p", { className: "fs-muted", children: "Lowercase letters, digits, hyphens, underscores." })] })), _jsxs("div", { className: "fs-rbac__field", children: [_jsx("label", { className: "fs-label", htmlFor: "fs-rbac-name", children: "Display name" }), _jsx("input", { id: "fs-rbac-name", className: "fs-input", placeholder: "e.g. Analyst", value: draft.name, disabled: draft.isAccountRole, onChange: (e) => setDraft({ ...draft, name: e.target.value }) }), draft.isAccountRole && (_jsx("p", { className: "fs-muted", children: "Account roles keep their name \u2014 only their permissions can be narrowed or restored." }))] })] }), !draft.isAccountRole && (_jsxs("label", { className: "fs-rbac__fullaccess", children: [_jsx("input", { type: "checkbox", checked: draft.fullAccess, onChange: (e) => setDraft({ ...draft, fullAccess: e.target.checked }) }), " ", "Full access (every route, including future ones)"] })), draft.isAccountRole && (_jsxs("fieldset", { className: "fs-rbac__group", children: [_jsx("legend", { className: "fs-label", children: "Account permissions" }), (draft.vocabulary ?? [...draft.permissions].sort()).map((permission) => (_jsxs("label", { className: "fs-rbac__perm", children: [_jsx("input", { type: "checkbox", checked: draft.permissions.has(permission), onChange: () => toggleDraftPermission(permission) }), " ", permission] }, permission)))] })), !draft.fullAccess &&
|
|
296
|
+
!draft.isAccountRole &&
|
|
231
297
|
catalog.map((entry) => (_jsxs("fieldset", { className: "fs-keygrant__group", children: [_jsx("legend", { className: "fs-keygrant__legend", children: entry.title ?? entry.subject }), _jsx("div", { className: "fs-keygrant__opts", children: entry.permissions.map((permission) => (_jsxs("label", { className: "fs-keygrant__opt", children: [_jsx("input", { type: "checkbox", checked: draft.permissions.has(permission), onChange: () => toggleDraftPermission(permission) }), " ", _jsx("span", { className: "fs-code-inline", children: permission })] }, permission))) })] }, entry.subject))), _jsxs("div", { className: "fs-card__actions", children: [_jsx("button", { type: "button", className: "fs-btn fs-btn--cta fs-btn--sm", disabled: busy || !draftValid, onClick: () => void saveDraft(), children: busy
|
|
232
298
|
? "Saving…"
|
|
233
299
|
: draft.mode === "create"
|