@checkstack/frontend-api 0.7.2 → 0.9.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 +103 -0
- package/package.json +6 -6
- package/src/core-apis.ts +15 -0
- package/src/plugin-registry.ts +13 -3
- package/src/plugin.ts +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,108 @@
|
|
|
1
1
|
# @checkstack/frontend-api
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 56e7c75: Hide navigation, actions and links that the current user cannot use, so anonymous
|
|
8
|
+
and read-only users no longer see entries that lead to "Access Denied" or to
|
|
9
|
+
actions the server would reject.
|
|
10
|
+
|
|
11
|
+
- **Sidebar**: a nav entry can now declare a dynamic `nav.isVisible({ accessRules, isAuthenticated })` predicate (in addition to the static `accessRule`). A group whose every entry is filtered out is no longer rendered. The filtering/grouping logic is extracted to a pure, unit-tested helper.
|
|
12
|
+
- **Infrastructure**: its sidebar entry is shown only when the user can READ at least one contributed tab (queue, cache, …), instead of always (it previously had no static rule because tabs are contributed at runtime).
|
|
13
|
+
- **Notification Settings**: hidden from anonymous users - notifications are per-user, so an anonymous visitor can't have any.
|
|
14
|
+
- **Anomaly Mute / Suppress**: the "Mute" / "Mute all" controls (a per-user preference) are hidden from anonymous visitors; the "Suppress" control is gated on `anomalyAccess.feed.manage`. Both were previously always visible.
|
|
15
|
+
- **Dashboard**: the "Open Catalog" actions (which open the manage-only Catalog config page) are hidden from users without `catalogAccess.system.manage`, and the "View catalog" link is gated on `catalogAccess.system.read`.
|
|
16
|
+
- **Dashboard status signals**: the per-system status rows contributed by plugins (`SystemSignalsSlot`) now render as a LINK only when the user can open the target, and as plain text otherwise. `SystemSignal` gains an optional `accessRule`; the healthcheck, anomaly, and dependency fillers set it for their gated targets (check-history / assignments / dependency-map). Signals pointing at ungated pages (incident / maintenance / SLO detail) stay links.
|
|
17
|
+
- **Plugin Manager**: the "Install plugin" button (which opens the install-gated page) is hidden from users with only `plugin` view access.
|
|
18
|
+
- **Satellites**: the page is entirely manage-gated, but its route/sidebar entry was gated on `read`, so read-only users saw the nav item and hit "Access Denied" on click. The route and nav entry now require `satellite.manage`.
|
|
19
|
+
|
|
20
|
+
The `@checkstack/ai-backend` bump is only the regenerated bundled docs index
|
|
21
|
+
(the frontend routing guide gained the `nav.isVisible` section); no code change.
|
|
22
|
+
|
|
23
|
+
**BREAKING (`@checkstack/frontend-api`):** the `AccessApi` interface gains a
|
|
24
|
+
required `useIsAuthenticated()` method. Custom `AccessApi` implementations must
|
|
25
|
+
add it (it returns `{ loading, isAuthenticated }`). The built-in auth
|
|
26
|
+
implementation and the no-auth fallback already do. `NavEntry` also gains an
|
|
27
|
+
optional `isVisible` predicate (purely additive).
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 56e7c75: Fix frontend access checks to use FULLY-QUALIFIED access-rule ids, and resolve
|
|
32
|
+
the anonymous role on the frontend.
|
|
33
|
+
|
|
34
|
+
Granted access-rule ids are stored fully-qualified as `{pluginId}.{ruleId}` (e.g.
|
|
35
|
+
`incident.incident.read`) so two plugins defining the same short rule id never
|
|
36
|
+
collide. The frontend, however, was checking the UNqualified id (`incident.read`)
|
|
37
|
+
via `isAccessRuleSatisfied`, so every check failed for any user without the `*`
|
|
38
|
+
(admin) grant - masked in development because dev-auth grants `*`. This silently
|
|
39
|
+
broke ALL non-admin frontend gating (route guards, sidebar entries, and
|
|
40
|
+
`useAccess`-based button/link gating).
|
|
41
|
+
|
|
42
|
+
- **`@checkstack/common`**: `AccessRule` now carries a REQUIRED owning `pluginId`;
|
|
43
|
+
`access()` / `accessPair()` require and stamp it; `isAccessRuleSatisfied`
|
|
44
|
+
qualifies the rule (`{pluginId}.{id}`, plus the manage->read escalation) and
|
|
45
|
+
matches ONLY the qualified form. There is intentionally NO unqualified fallback
|
|
46
|
+
- matching a bare id would let one plugin's grant satisfy another plugin's
|
|
47
|
+
identically-named rule (a cross-plugin privilege-escalation flaw). Every plugin
|
|
48
|
+
that defines access rules now passes its own `pluginId`.
|
|
49
|
+
- **`@checkstack/backend`**: `pluginManager.getAllAccessRules()` no longer strips
|
|
50
|
+
the `pluginId` field (the rule `id` is already fully-qualified for the DB sync).
|
|
51
|
+
- **Route guard** (`@checkstack/frontend` / `@checkstack/frontend-api`) now
|
|
52
|
+
checks the FULL rule object (so it qualifies and escalates), not a bare id.
|
|
53
|
+
- **Anonymous role on the frontend**: the `accessRules` procedure is now
|
|
54
|
+
`public`, returning the configurable anonymous role's grants to unauthenticated
|
|
55
|
+
callers; `useAccessRules` fetches them for guests instead of returning an empty
|
|
56
|
+
set. So anonymous UI now reflects exactly what the anonymous role is allowed -
|
|
57
|
+
which an admin can change (`isPublic` is only the seeded default).
|
|
58
|
+
- Incident / maintenance / SLO detail routes are now read-gated (their read rule
|
|
59
|
+
is an `isPublic` default, so the anonymous role holds it unless an admin
|
|
60
|
+
revokes it); their dashboard status signals carry that rule and render as a
|
|
61
|
+
link only when the viewer may open it.
|
|
62
|
+
|
|
63
|
+
**BREAKING (`@checkstack/common`):** `AccessRule.pluginId` is now REQUIRED, and
|
|
64
|
+
`access()` / `accessPair()` require a `pluginId` option. `isAccessRuleSatisfied`
|
|
65
|
+
matches ONLY the fully-qualified `{pluginId}.{ruleId}` form - the previous
|
|
66
|
+
unqualified fallback is removed, because it was a cross-plugin
|
|
67
|
+
privilege-escalation flaw. Any code constructing an `AccessRule` or calling
|
|
68
|
+
`access()`/`accessPair()` must supply the owning `pluginId`.
|
|
69
|
+
|
|
70
|
+
Verified live against an anonymous caller: read pages resolve (qualified match),
|
|
71
|
+
manage actions are denied, manage->read escalation and `*` still work.
|
|
72
|
+
|
|
73
|
+
- Updated dependencies [56e7c75]
|
|
74
|
+
- @checkstack/common@0.15.0
|
|
75
|
+
- @checkstack/signal-common@0.2.9
|
|
76
|
+
|
|
77
|
+
## 0.8.0
|
|
78
|
+
|
|
79
|
+
### Minor Changes
|
|
80
|
+
|
|
81
|
+
- fb705df: Upgrade React 18 to React 19 across the platform.
|
|
82
|
+
|
|
83
|
+
**BREAKING (runtime frontend plugins):** React is shared as a Module Federation
|
|
84
|
+
singleton, so the host now provides **React 19** to every runtime plugin.
|
|
85
|
+
Frontend plugins built against React 18 must be rebuilt against React 19
|
|
86
|
+
(`react` / `react-dom` `^19`). The scaffold templates and the host/plugin MF
|
|
87
|
+
`requiredVersion` are updated to `^19`. `react` (and now `react-dom`) are pinned
|
|
88
|
+
to a single version across the workspace via syncpack so the singleton can never
|
|
89
|
+
skew (react and react-dom must match exactly).
|
|
90
|
+
|
|
91
|
+
The React 19 removed-API surface was audited - the codebase used only no-arg
|
|
92
|
+
`useRef()` (now `useRef<T | undefined>(undefined)`); no `ReactDOM.render`,
|
|
93
|
+
legacy context, string refs, or function-component `defaultProps`. This also
|
|
94
|
+
clears the `IMPORT_IS_UNDEFINED` build warnings for `React.use` /
|
|
95
|
+
`React.useOptimistic` (react-router 7 feature-detection), which React 19 exports.
|
|
96
|
+
|
|
97
|
+
The downstream `*-frontend` packages (and `@checkstack/infrastructure-common`)
|
|
98
|
+
receive only the mechanical `react` dependency bump (`patch`); the framework
|
|
99
|
+
packages carrying the shared-singleton change are bumped `minor`.
|
|
100
|
+
|
|
101
|
+
### Patch Changes
|
|
102
|
+
|
|
103
|
+
- @checkstack/common@0.14.1
|
|
104
|
+
- @checkstack/signal-common@0.2.8
|
|
105
|
+
|
|
3
106
|
## 0.7.2
|
|
4
107
|
|
|
5
108
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/frontend-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
"lint:code": "eslint . --max-warnings 0"
|
|
11
11
|
},
|
|
12
12
|
"peerDependencies": {
|
|
13
|
-
"react": "
|
|
13
|
+
"react": "19.2.7"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@checkstack/common": "0.
|
|
17
|
-
"@checkstack/signal-common": "0.2.
|
|
16
|
+
"@checkstack/common": "0.15.0",
|
|
17
|
+
"@checkstack/signal-common": "0.2.9",
|
|
18
18
|
"@orpc/client": "^1.14.4",
|
|
19
19
|
"@orpc/contract": "^1.14.4",
|
|
20
20
|
"@orpc/tanstack-query": "^1.14.4",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/bun": "^1.3.5",
|
|
25
|
-
"@types/react": "^
|
|
25
|
+
"@types/react": "^19.0.0",
|
|
26
26
|
"typescript": "^5.0.0",
|
|
27
27
|
"@checkstack/tsconfig": "0.0.7",
|
|
28
|
-
"@checkstack/scripts": "0.
|
|
28
|
+
"@checkstack/scripts": "0.6.1"
|
|
29
29
|
},
|
|
30
30
|
"checkstack": {
|
|
31
31
|
"type": "tooling"
|
package/src/core-apis.ts
CHANGED
|
@@ -39,6 +39,21 @@ export interface AccessApi {
|
|
|
39
39
|
* ```
|
|
40
40
|
*/
|
|
41
41
|
useAccess(accessRule: AccessRule): { loading: boolean; allowed: boolean };
|
|
42
|
+
/**
|
|
43
|
+
* Whether the current user is authenticated (logged in). Use to gate UI that
|
|
44
|
+
* needs a real user but no specific access rule - e.g. per-user actions like
|
|
45
|
+
* muting your own anomaly notifications, which any logged-in user may do but
|
|
46
|
+
* an anonymous visitor may not.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* const { isAuthenticated } = accessApi.useIsAuthenticated();
|
|
51
|
+
* if (isAuthenticated) {
|
|
52
|
+
* // show the per-user action
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
useIsAuthenticated(): { loading: boolean; isAuthenticated: boolean };
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
export const accessApiRef = createApiRef<AccessApi>("core.access");
|
package/src/plugin-registry.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
import type { AccessRule } from "@checkstack/common";
|
|
2
3
|
import { FrontendPlugin, Extension } from "./plugin";
|
|
3
4
|
|
|
4
5
|
/** Lazy page loader, as declared on a {@link PluginRoute}. */
|
|
@@ -20,7 +21,12 @@ interface ResolvedRoute {
|
|
|
20
21
|
load?: RouteLoader;
|
|
21
22
|
element?: ReactNode;
|
|
22
23
|
title?: string;
|
|
23
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The FULL access rule (not just its id) so the route guard can qualify it
|
|
26
|
+
* (`{pluginId}.{id}`) against the user's granted ids AND apply manage->read
|
|
27
|
+
* escalation. A bare id string could do neither.
|
|
28
|
+
*/
|
|
29
|
+
accessRule?: AccessRule;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
class PluginRegistry {
|
|
@@ -73,7 +79,7 @@ class PluginRegistry {
|
|
|
73
79
|
load: route.load,
|
|
74
80
|
element: route.element,
|
|
75
81
|
title: route.title,
|
|
76
|
-
accessRule: route.accessRule
|
|
82
|
+
accessRule: route.accessRule,
|
|
77
83
|
};
|
|
78
84
|
|
|
79
85
|
// Add to route map for resolution
|
|
@@ -189,7 +195,7 @@ class PluginRegistry {
|
|
|
189
195
|
load: route.load,
|
|
190
196
|
element: route.element,
|
|
191
197
|
title: route.title,
|
|
192
|
-
accessRule: route.accessRule
|
|
198
|
+
accessRule: route.accessRule,
|
|
193
199
|
// Resolved sidebar entry (defaults applied) for routes that opt in.
|
|
194
200
|
// `accessRule` here is the EFFECTIVE rule object (nav override, else
|
|
195
201
|
// the route's own rule) the sidebar gates visibility on.
|
|
@@ -200,6 +206,10 @@ class PluginRegistry {
|
|
|
200
206
|
label: route.nav.label ?? route.title ?? route.route.id,
|
|
201
207
|
order: route.nav.order ?? 0,
|
|
202
208
|
accessRule: route.nav.accessRule ?? route.accessRule,
|
|
209
|
+
// Dynamic visibility predicate (e.g. Infrastructure: any
|
|
210
|
+
// readable tab; per-user entries: authenticated) — passed
|
|
211
|
+
// through so the sidebar can evaluate it.
|
|
212
|
+
isVisible: route.nav.isVisible,
|
|
203
213
|
}
|
|
204
214
|
: undefined,
|
|
205
215
|
};
|
package/src/plugin.ts
CHANGED
|
@@ -122,6 +122,22 @@ export interface NavEntry {
|
|
|
122
122
|
* (e.g. show on `read` while the page itself needs `manage`).
|
|
123
123
|
*/
|
|
124
124
|
accessRule?: AccessRule;
|
|
125
|
+
/**
|
|
126
|
+
* Dynamic visibility predicate, evaluated with the current user's access
|
|
127
|
+
* rules and auth state. Use for entries whose visibility cannot be expressed
|
|
128
|
+
* as a single static `accessRule`:
|
|
129
|
+
* - the Infrastructure entry, visible only if the user can read at least one
|
|
130
|
+
* of the tabs contributed by other plugins at runtime;
|
|
131
|
+
* - per-user entries (e.g. Notification Settings) that require an
|
|
132
|
+
* authenticated user rather than a specific rule.
|
|
133
|
+
* Evaluated IN ADDITION to `accessRule` (both must pass). Returning `false`
|
|
134
|
+
* hides the entry; a group with no visible entries is not rendered.
|
|
135
|
+
*/
|
|
136
|
+
isVisible?: (context: {
|
|
137
|
+
/** The current user's granted access-rule IDs (as `isAccessRuleSatisfied` expects). */
|
|
138
|
+
accessRules: string[];
|
|
139
|
+
isAuthenticated: boolean;
|
|
140
|
+
}) => boolean;
|
|
125
141
|
}
|
|
126
142
|
|
|
127
143
|
/** Fields common to every route, regardless of eager/lazy. */
|