@checkstack/backend-api 0.1.0 → 0.3.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 +189 -0
- package/package.json +1 -1
- package/src/assertions.test.ts +128 -0
- package/src/assertions.ts +77 -2
- package/src/chart-metadata.ts +1 -24
- package/src/collector-registry.ts +2 -0
- package/src/hooks.ts +6 -6
- package/src/notification-strategy.ts +5 -5
- package/src/plugin-admin-contract.ts +13 -15
- package/src/plugin-system.ts +6 -3
- package/src/rpc.test.ts +530 -0
- package/src/rpc.ts +308 -93
- package/src/test-utils.ts +6 -1
- package/src/types.ts +34 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,194 @@
|
|
|
1
1
|
# @checkstack/backend-api
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9faec1f: # Unified AccessRule Terminology Refactoring
|
|
8
|
+
|
|
9
|
+
This release completes a comprehensive terminology refactoring from "permission" to "accessRule" across the entire codebase, establishing a consistent and modern access control vocabulary.
|
|
10
|
+
|
|
11
|
+
## Changes
|
|
12
|
+
|
|
13
|
+
### Core Infrastructure (`@checkstack/common`)
|
|
14
|
+
|
|
15
|
+
- Introduced `AccessRule` interface as the primary access control type
|
|
16
|
+
- Added `accessPair()` helper for creating read/manage access rule pairs
|
|
17
|
+
- Added `access()` builder for individual access rules
|
|
18
|
+
- Replaced `Permission` type with `AccessRule` throughout
|
|
19
|
+
|
|
20
|
+
### API Changes
|
|
21
|
+
|
|
22
|
+
- `env.registerPermissions()` → `env.registerAccessRules()`
|
|
23
|
+
- `meta.permissions` → `meta.access` in RPC contracts
|
|
24
|
+
- `usePermission()` → `useAccess()` in frontend hooks
|
|
25
|
+
- Route `permission:` field → `accessRule:` field
|
|
26
|
+
|
|
27
|
+
### UI Changes
|
|
28
|
+
|
|
29
|
+
- "Roles & Permissions" tab → "Roles & Access Rules"
|
|
30
|
+
- "You don't have permission..." → "You don't have access..."
|
|
31
|
+
- All permission-related UI text updated
|
|
32
|
+
|
|
33
|
+
### Documentation & Templates
|
|
34
|
+
|
|
35
|
+
- Updated 18 documentation files with AccessRule terminology
|
|
36
|
+
- Updated 7 scaffolding templates with `accessPair()` pattern
|
|
37
|
+
- All code examples use new AccessRule API
|
|
38
|
+
|
|
39
|
+
## Migration Guide
|
|
40
|
+
|
|
41
|
+
### Backend Plugins
|
|
42
|
+
|
|
43
|
+
```diff
|
|
44
|
+
- import { permissionList } from "./permissions";
|
|
45
|
+
- env.registerPermissions(permissionList);
|
|
46
|
+
+ import { accessRules } from "./access";
|
|
47
|
+
+ env.registerAccessRules(accessRules);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### RPC Contracts
|
|
51
|
+
|
|
52
|
+
```diff
|
|
53
|
+
- .meta({ userType: "user", permissions: [permissions.read.id] })
|
|
54
|
+
+ .meta({ userType: "user", access: [access.read] })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Frontend Hooks
|
|
58
|
+
|
|
59
|
+
```diff
|
|
60
|
+
- const canRead = accessApi.usePermission(permissions.read.id);
|
|
61
|
+
+ const canRead = accessApi.useAccess(access.read);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Routes
|
|
65
|
+
|
|
66
|
+
```diff
|
|
67
|
+
- permission: permissions.entityRead.id,
|
|
68
|
+
+ accessRule: access.read,
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- 827b286: Add array assertion operators for string array fields
|
|
72
|
+
|
|
73
|
+
New operators for asserting on array fields (e.g., playerNames in RCON collectors):
|
|
74
|
+
|
|
75
|
+
- **includes** - Check if array contains a specific value
|
|
76
|
+
- **notIncludes** - Check if array does NOT contain a specific value
|
|
77
|
+
- **lengthEquals** - Check if array length equals a value
|
|
78
|
+
- **lengthGreaterThan** - Check if array length is greater than a value
|
|
79
|
+
- **lengthLessThan** - Check if array length is less than a value
|
|
80
|
+
- **isEmpty** - Check if array is empty
|
|
81
|
+
- **isNotEmpty** - Check if array has at least one element
|
|
82
|
+
|
|
83
|
+
Also exports a new `arrayField()` schema factory for creating array assertion schemas.
|
|
84
|
+
|
|
85
|
+
### Patch Changes
|
|
86
|
+
|
|
87
|
+
- f533141: Enforce health result factory function usage via branded types
|
|
88
|
+
|
|
89
|
+
- Added `healthResultSchema()` builder that enforces the use of factory functions at compile-time
|
|
90
|
+
- Added `healthResultArray()` factory for array fields (e.g., DNS resolved values)
|
|
91
|
+
- Added branded `HealthResultField<T>` type to mark schemas created by factory functions
|
|
92
|
+
- Consolidated `ChartType` and `HealthResultMeta` into `@checkstack/common` as single source of truth
|
|
93
|
+
- Updated all 12 health check strategies and 11 collectors to use `healthResultSchema()`
|
|
94
|
+
- Using raw `z.number()` etc. inside `healthResultSchema()` now causes a TypeScript error
|
|
95
|
+
|
|
96
|
+
- aa4a8ab: Fix anonymous users not seeing public list endpoints
|
|
97
|
+
|
|
98
|
+
Anonymous users with global access rules (e.g., `catalog.system.read` assigned to the "anonymous" role) were incorrectly getting empty results from list endpoints with `instanceAccess.listKey`. The middleware now properly checks if anonymous users have global access before filtering.
|
|
99
|
+
|
|
100
|
+
Added comprehensive test suite for `autoAuthMiddleware` covering:
|
|
101
|
+
|
|
102
|
+
- Anonymous endpoints (userType: "anonymous")
|
|
103
|
+
- Public endpoints with global and instance-level access rules
|
|
104
|
+
- Authenticated, user-only, and service-only endpoints
|
|
105
|
+
- Single resource access with team-based filtering
|
|
106
|
+
|
|
107
|
+
- Updated dependencies [9faec1f]
|
|
108
|
+
- Updated dependencies [f533141]
|
|
109
|
+
- @checkstack/common@0.2.0
|
|
110
|
+
- @checkstack/signal-common@0.1.0
|
|
111
|
+
- @checkstack/queue-api@0.0.5
|
|
112
|
+
|
|
113
|
+
## 0.2.0
|
|
114
|
+
|
|
115
|
+
### Minor Changes
|
|
116
|
+
|
|
117
|
+
- 8e43507: # Teams and Resource-Level Access Control
|
|
118
|
+
|
|
119
|
+
This release introduces a comprehensive Teams system for organizing users and controlling access to resources at a granular level.
|
|
120
|
+
|
|
121
|
+
## Features
|
|
122
|
+
|
|
123
|
+
### Team Management
|
|
124
|
+
|
|
125
|
+
- Create, update, and delete teams with name and description
|
|
126
|
+
- Add/remove users from teams
|
|
127
|
+
- Designate team managers with elevated privileges
|
|
128
|
+
- View team membership and manager status
|
|
129
|
+
|
|
130
|
+
### Resource-Level Access Control
|
|
131
|
+
|
|
132
|
+
- Grant teams access to specific resources (systems, health checks, incidents, maintenances)
|
|
133
|
+
- Configure read-only or manage permissions per team
|
|
134
|
+
- Resource-level "Team Only" mode that restricts access exclusively to team members
|
|
135
|
+
- Separate `resourceAccessSettings` table for resource-level settings (not per-grant)
|
|
136
|
+
- Automatic cleanup of grants when teams are deleted (database cascade)
|
|
137
|
+
|
|
138
|
+
### Middleware Integration
|
|
139
|
+
|
|
140
|
+
- Extended `autoAuthMiddleware` to support resource access checks
|
|
141
|
+
- Single-resource pre-handler validation for detail endpoints
|
|
142
|
+
- Automatic list filtering for collection endpoints
|
|
143
|
+
- S2S endpoints for access verification
|
|
144
|
+
|
|
145
|
+
### Frontend Components
|
|
146
|
+
|
|
147
|
+
- `TeamsTab` component for managing teams in Auth Settings
|
|
148
|
+
- `TeamAccessEditor` component for assigning team access to resources
|
|
149
|
+
- Resource-level "Team Only" toggle in `TeamAccessEditor`
|
|
150
|
+
- Integration into System, Health Check, Incident, and Maintenance editors
|
|
151
|
+
|
|
152
|
+
## Breaking Changes
|
|
153
|
+
|
|
154
|
+
### API Response Format Changes
|
|
155
|
+
|
|
156
|
+
List endpoints now return objects with named keys instead of arrays directly:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// Before
|
|
160
|
+
const systems = await catalogApi.getSystems();
|
|
161
|
+
|
|
162
|
+
// After
|
|
163
|
+
const { systems } = await catalogApi.getSystems();
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Affected endpoints:
|
|
167
|
+
|
|
168
|
+
- `catalog.getSystems` → `{ systems: [...] }`
|
|
169
|
+
- `healthcheck.getConfigurations` → `{ configurations: [...] }`
|
|
170
|
+
- `incident.listIncidents` → `{ incidents: [...] }`
|
|
171
|
+
- `maintenance.listMaintenances` → `{ maintenances: [...] }`
|
|
172
|
+
|
|
173
|
+
### User Identity Enrichment
|
|
174
|
+
|
|
175
|
+
`RealUser` and `ApplicationUser` types now include `teamIds: string[]` field with team memberships.
|
|
176
|
+
|
|
177
|
+
## Documentation
|
|
178
|
+
|
|
179
|
+
See `docs/backend/teams.md` for complete API reference and integration guide.
|
|
180
|
+
|
|
181
|
+
### Patch Changes
|
|
182
|
+
|
|
183
|
+
- 97c5a6b: Fix collector lookup when health check is assigned to a system
|
|
184
|
+
|
|
185
|
+
Collectors are now stored in the registry with their fully-qualified ID format (ownerPluginId.collectorId) to match how they are referenced in health check configurations. Added `qualifiedId` field to `RegisteredCollector` interface to avoid re-constructing the ID at query time. This fixes the "Collector not found" warning that occurred when executing health checks with assigned systems.
|
|
186
|
+
|
|
187
|
+
- Updated dependencies [8e43507]
|
|
188
|
+
- @checkstack/common@0.1.0
|
|
189
|
+
- @checkstack/queue-api@0.0.4
|
|
190
|
+
- @checkstack/signal-common@0.0.4
|
|
191
|
+
|
|
3
192
|
## 0.1.0
|
|
4
193
|
|
|
5
194
|
### Minor Changes
|
package/package.json
CHANGED
package/src/assertions.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
booleanField,
|
|
10
10
|
enumField,
|
|
11
11
|
jsonPathField,
|
|
12
|
+
arrayField,
|
|
12
13
|
} from "./assertions";
|
|
13
14
|
import { z } from "zod";
|
|
14
15
|
|
|
@@ -130,6 +131,32 @@ describe("Assertion Schema Factories", () => {
|
|
|
130
131
|
});
|
|
131
132
|
});
|
|
132
133
|
|
|
134
|
+
describe("arrayField", () => {
|
|
135
|
+
it("creates a schema with array operators", () => {
|
|
136
|
+
const schema = arrayField("playerNames");
|
|
137
|
+
|
|
138
|
+
const includes = schema.safeParse({
|
|
139
|
+
field: "playerNames",
|
|
140
|
+
operator: "includes",
|
|
141
|
+
value: "Steve",
|
|
142
|
+
});
|
|
143
|
+
expect(includes.success).toBe(true);
|
|
144
|
+
|
|
145
|
+
const lengthEquals = schema.safeParse({
|
|
146
|
+
field: "playerNames",
|
|
147
|
+
operator: "lengthEquals",
|
|
148
|
+
value: 5,
|
|
149
|
+
});
|
|
150
|
+
expect(lengthEquals.success).toBe(true);
|
|
151
|
+
|
|
152
|
+
const isEmpty = schema.safeParse({
|
|
153
|
+
field: "playerNames",
|
|
154
|
+
operator: "isEmpty",
|
|
155
|
+
});
|
|
156
|
+
expect(isEmpty.success).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
133
160
|
describe("jsonPathField", () => {
|
|
134
161
|
it("creates a schema with dynamic operators", () => {
|
|
135
162
|
const schema = jsonPathField();
|
|
@@ -239,6 +266,107 @@ describe("evaluateAssertion", () => {
|
|
|
239
266
|
});
|
|
240
267
|
});
|
|
241
268
|
|
|
269
|
+
describe("array operators", () => {
|
|
270
|
+
it("evaluates includes correctly", () => {
|
|
271
|
+
const result = evaluateAssertion(
|
|
272
|
+
{ field: "playerNames", operator: "includes", value: "Steve" },
|
|
273
|
+
{ playerNames: ["Steve", "Alex", "Notch"] }
|
|
274
|
+
);
|
|
275
|
+
expect(result.passed).toBe(true);
|
|
276
|
+
|
|
277
|
+
const failed = evaluateAssertion(
|
|
278
|
+
{ field: "playerNames", operator: "includes", value: "Herobrine" },
|
|
279
|
+
{ playerNames: ["Steve", "Alex", "Notch"] }
|
|
280
|
+
);
|
|
281
|
+
expect(failed.passed).toBe(false);
|
|
282
|
+
expect(failed.message).toContain("to include");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("evaluates notIncludes correctly", () => {
|
|
286
|
+
const result = evaluateAssertion(
|
|
287
|
+
{ field: "playerNames", operator: "notIncludes", value: "Herobrine" },
|
|
288
|
+
{ playerNames: ["Steve", "Alex"] }
|
|
289
|
+
);
|
|
290
|
+
expect(result.passed).toBe(true);
|
|
291
|
+
|
|
292
|
+
const failed = evaluateAssertion(
|
|
293
|
+
{ field: "playerNames", operator: "notIncludes", value: "Steve" },
|
|
294
|
+
{ playerNames: ["Steve", "Alex"] }
|
|
295
|
+
);
|
|
296
|
+
expect(failed.passed).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("evaluates lengthEquals correctly", () => {
|
|
300
|
+
const result = evaluateAssertion(
|
|
301
|
+
{ field: "playerNames", operator: "lengthEquals", value: 3 },
|
|
302
|
+
{ playerNames: ["Steve", "Alex", "Notch"] }
|
|
303
|
+
);
|
|
304
|
+
expect(result.passed).toBe(true);
|
|
305
|
+
|
|
306
|
+
const failed = evaluateAssertion(
|
|
307
|
+
{ field: "playerNames", operator: "lengthEquals", value: 5 },
|
|
308
|
+
{ playerNames: ["Steve", "Alex", "Notch"] }
|
|
309
|
+
);
|
|
310
|
+
expect(failed.passed).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("evaluates lengthGreaterThan correctly", () => {
|
|
314
|
+
const result = evaluateAssertion(
|
|
315
|
+
{ field: "playerNames", operator: "lengthGreaterThan", value: 2 },
|
|
316
|
+
{ playerNames: ["Steve", "Alex", "Notch"] }
|
|
317
|
+
);
|
|
318
|
+
expect(result.passed).toBe(true);
|
|
319
|
+
|
|
320
|
+
const failed = evaluateAssertion(
|
|
321
|
+
{ field: "playerNames", operator: "lengthGreaterThan", value: 5 },
|
|
322
|
+
{ playerNames: ["Steve", "Alex"] }
|
|
323
|
+
);
|
|
324
|
+
expect(failed.passed).toBe(false);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("evaluates lengthLessThan correctly", () => {
|
|
328
|
+
const result = evaluateAssertion(
|
|
329
|
+
{ field: "playerNames", operator: "lengthLessThan", value: 5 },
|
|
330
|
+
{ playerNames: ["Steve", "Alex"] }
|
|
331
|
+
);
|
|
332
|
+
expect(result.passed).toBe(true);
|
|
333
|
+
|
|
334
|
+
const failed = evaluateAssertion(
|
|
335
|
+
{ field: "playerNames", operator: "lengthLessThan", value: 2 },
|
|
336
|
+
{ playerNames: ["Steve", "Alex", "Notch"] }
|
|
337
|
+
);
|
|
338
|
+
expect(failed.passed).toBe(false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("evaluates isEmpty correctly for arrays", () => {
|
|
342
|
+
const result = evaluateAssertion(
|
|
343
|
+
{ field: "playerNames", operator: "isEmpty" },
|
|
344
|
+
{ playerNames: [] }
|
|
345
|
+
);
|
|
346
|
+
expect(result.passed).toBe(true);
|
|
347
|
+
|
|
348
|
+
const failed = evaluateAssertion(
|
|
349
|
+
{ field: "playerNames", operator: "isEmpty" },
|
|
350
|
+
{ playerNames: ["Steve"] }
|
|
351
|
+
);
|
|
352
|
+
expect(failed.passed).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("evaluates isNotEmpty correctly for arrays", () => {
|
|
356
|
+
const result = evaluateAssertion(
|
|
357
|
+
{ field: "playerNames", operator: "isNotEmpty" },
|
|
358
|
+
{ playerNames: ["Steve", "Alex"] }
|
|
359
|
+
);
|
|
360
|
+
expect(result.passed).toBe(true);
|
|
361
|
+
|
|
362
|
+
const failed = evaluateAssertion(
|
|
363
|
+
{ field: "playerNames", operator: "isNotEmpty" },
|
|
364
|
+
{ playerNames: [] }
|
|
365
|
+
);
|
|
366
|
+
expect(failed.passed).toBe(false);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
242
370
|
describe("existence operators", () => {
|
|
243
371
|
it("evaluates exists correctly", () => {
|
|
244
372
|
const result = evaluateAssertion(
|
package/src/assertions.ts
CHANGED
|
@@ -40,6 +40,21 @@ export const StringOperators = z.enum([
|
|
|
40
40
|
*/
|
|
41
41
|
export const BooleanOperators = z.enum(["isTrue", "isFalse"]);
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Operators for array fields.
|
|
45
|
+
*/
|
|
46
|
+
export const ArrayOperators = z.enum([
|
|
47
|
+
"includes",
|
|
48
|
+
"notIncludes",
|
|
49
|
+
"lengthEquals",
|
|
50
|
+
"lengthGreaterThan",
|
|
51
|
+
"lengthLessThan",
|
|
52
|
+
"isEmpty",
|
|
53
|
+
"isNotEmpty",
|
|
54
|
+
"exists",
|
|
55
|
+
"notExists",
|
|
56
|
+
]);
|
|
57
|
+
|
|
43
58
|
/**
|
|
44
59
|
* Universal operators for dynamic/unknown types (JSONPath values).
|
|
45
60
|
* Works via runtime type coercion.
|
|
@@ -118,6 +133,23 @@ export function booleanField(name: string) {
|
|
|
118
133
|
});
|
|
119
134
|
}
|
|
120
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Creates an assertion schema for array fields.
|
|
138
|
+
* Supports checking array contents and length.
|
|
139
|
+
*/
|
|
140
|
+
export function arrayField(name: string) {
|
|
141
|
+
return z.object({
|
|
142
|
+
field: z.literal(name),
|
|
143
|
+
operator: ArrayOperators,
|
|
144
|
+
value: z
|
|
145
|
+
.union([z.string(), z.number()])
|
|
146
|
+
.optional()
|
|
147
|
+
.describe(
|
|
148
|
+
"Value to check (string for includes, number for length operators)"
|
|
149
|
+
),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
121
153
|
/**
|
|
122
154
|
* Creates an assertion schema for enum fields (e.g., status codes).
|
|
123
155
|
*/
|
|
@@ -258,8 +290,38 @@ function evaluateOperator(
|
|
|
258
290
|
if (op === "isTrue") return actual === true;
|
|
259
291
|
if (op === "isFalse") return actual === false;
|
|
260
292
|
|
|
261
|
-
//
|
|
293
|
+
// Array operators
|
|
294
|
+
if (Array.isArray(actual)) {
|
|
295
|
+
switch (op) {
|
|
296
|
+
case "includes": {
|
|
297
|
+
const strExpected = String(expected ?? "");
|
|
298
|
+
return actual.some((item) => String(item) === strExpected);
|
|
299
|
+
}
|
|
300
|
+
case "notIncludes": {
|
|
301
|
+
const strExpected = String(expected ?? "");
|
|
302
|
+
return !actual.some((item) => String(item) === strExpected);
|
|
303
|
+
}
|
|
304
|
+
case "lengthEquals": {
|
|
305
|
+
return actual.length === Number(expected);
|
|
306
|
+
}
|
|
307
|
+
case "lengthGreaterThan": {
|
|
308
|
+
return actual.length > Number(expected);
|
|
309
|
+
}
|
|
310
|
+
case "lengthLessThan": {
|
|
311
|
+
return actual.length < Number(expected);
|
|
312
|
+
}
|
|
313
|
+
case "isEmpty": {
|
|
314
|
+
return actual.length === 0;
|
|
315
|
+
}
|
|
316
|
+
case "isNotEmpty": {
|
|
317
|
+
return actual.length > 0;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Empty check (for strings)
|
|
262
323
|
if (op === "isEmpty") return !actual || String(actual).trim() === "";
|
|
324
|
+
if (op === "isNotEmpty") return !!actual && String(actual).trim() !== "";
|
|
263
325
|
|
|
264
326
|
// For numeric operators, try to coerce to numbers
|
|
265
327
|
if (
|
|
@@ -350,17 +412,30 @@ function formatFailureMessage(
|
|
|
350
412
|
endsWith: "to end with",
|
|
351
413
|
matches: "to match pattern",
|
|
352
414
|
isEmpty: "to be empty",
|
|
415
|
+
isNotEmpty: "to not be empty",
|
|
353
416
|
exists: "to exist",
|
|
354
417
|
notExists: "not to exist",
|
|
355
418
|
isTrue: "to be true",
|
|
356
419
|
isFalse: "to be false",
|
|
420
|
+
includes: "to include",
|
|
421
|
+
notIncludes: "to not include",
|
|
422
|
+
lengthEquals: "to have length equal to",
|
|
423
|
+
lengthGreaterThan: "to have length greater than",
|
|
424
|
+
lengthLessThan: "to have length less than",
|
|
357
425
|
};
|
|
358
426
|
|
|
359
427
|
const opLabel = opLabels[operator] || operator;
|
|
360
428
|
|
|
361
429
|
// For operators without expected values
|
|
362
430
|
if (
|
|
363
|
-
[
|
|
431
|
+
[
|
|
432
|
+
"isEmpty",
|
|
433
|
+
"isNotEmpty",
|
|
434
|
+
"exists",
|
|
435
|
+
"notExists",
|
|
436
|
+
"isTrue",
|
|
437
|
+
"isFalse",
|
|
438
|
+
].includes(operator)
|
|
364
439
|
) {
|
|
365
440
|
return `${field}: expected ${opLabel}, got ${JSON.stringify(actual)}`;
|
|
366
441
|
}
|
package/src/chart-metadata.ts
CHANGED
|
@@ -26,30 +26,7 @@
|
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
* Available chart types for auto-generated visualizations.
|
|
31
|
-
*
|
|
32
|
-
* Numeric types:
|
|
33
|
-
* - line: Time series line chart for numeric metrics over time
|
|
34
|
-
* - bar: Bar chart for distributions (record of string to number)
|
|
35
|
-
* - counter: Simple count display with trend indicator
|
|
36
|
-
* - gauge: Percentage gauge for rates/percentages (0-100)
|
|
37
|
-
*
|
|
38
|
-
* Non-numeric types:
|
|
39
|
-
* - boolean: Boolean indicator (success/failure, connected/disconnected)
|
|
40
|
-
* - text: Text display for string values
|
|
41
|
-
* - status: Status badge for error/warning states
|
|
42
|
-
*
|
|
43
|
-
* Note: Fields without chart annotations simply won't render - no "hidden" type needed.
|
|
44
|
-
*/
|
|
45
|
-
export type ChartType =
|
|
46
|
-
| "line"
|
|
47
|
-
| "bar"
|
|
48
|
-
| "counter"
|
|
49
|
-
| "gauge"
|
|
50
|
-
| "boolean"
|
|
51
|
-
| "text"
|
|
52
|
-
| "status";
|
|
29
|
+
import type { ChartType } from "@checkstack/common";
|
|
53
30
|
|
|
54
31
|
/**
|
|
55
32
|
* Chart metadata to attach to Zod schema fields via .meta().
|
|
@@ -6,6 +6,8 @@ import type { TransportClient } from "./transport-client";
|
|
|
6
6
|
* A registered collector with its owning plugin metadata.
|
|
7
7
|
*/
|
|
8
8
|
export interface RegisteredCollector {
|
|
9
|
+
/** The fully-qualified collector ID (ownerPluginId.collectorId) */
|
|
10
|
+
qualifiedId: string;
|
|
9
11
|
/** The collector strategy */
|
|
10
12
|
collector: CollectorStrategy<TransportClient<unknown, unknown>>;
|
|
11
13
|
/** The plugin that registered this collector */
|
package/src/hooks.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AccessRule } from "@checkstack/common";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Hook definition for type-safe event emission and subscription
|
|
@@ -20,12 +20,12 @@ export function createHook<T>(id: string): Hook<T> {
|
|
|
20
20
|
*/
|
|
21
21
|
export const coreHooks = {
|
|
22
22
|
/**
|
|
23
|
-
* Emitted when a plugin registers
|
|
23
|
+
* Emitted when a plugin registers access rules
|
|
24
24
|
*/
|
|
25
|
-
|
|
25
|
+
accessRulesRegistered: createHook<{
|
|
26
26
|
pluginId: string;
|
|
27
|
-
|
|
28
|
-
}>("core.
|
|
27
|
+
accessRules: AccessRule[];
|
|
28
|
+
}>("core.accessRules.registered"),
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Emitted when plugin configuration is updated
|
|
@@ -65,7 +65,7 @@ export const coreHooks = {
|
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Emitted AFTER a plugin has been fully removed.
|
|
68
|
-
* Use this for orphan cleanup (e.g., removing
|
|
68
|
+
* Use this for orphan cleanup (e.g., removing access rules from DB).
|
|
69
69
|
* Should be emitted with work-queue mode for DB operations.
|
|
70
70
|
*/
|
|
71
71
|
pluginDeregistered: createHook<{
|
|
@@ -366,10 +366,10 @@ export interface RegisteredNotificationStrategy<
|
|
|
366
366
|
/** Plugin that registered this strategy */
|
|
367
367
|
ownerPluginId: string;
|
|
368
368
|
/**
|
|
369
|
-
* Dynamically generated
|
|
369
|
+
* Dynamically generated access rule ID for this strategy.
|
|
370
370
|
* Format: `{ownerPluginId}.strategy.{id}.use`
|
|
371
371
|
*/
|
|
372
|
-
|
|
372
|
+
accessRuleId: string;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
/**
|
|
@@ -404,12 +404,12 @@ export interface NotificationStrategyRegistry {
|
|
|
404
404
|
getStrategies(): RegisteredNotificationStrategy<unknown, unknown, unknown>[];
|
|
405
405
|
|
|
406
406
|
/**
|
|
407
|
-
* Get all strategies that a user has
|
|
407
|
+
* Get all strategies that a user has access to use.
|
|
408
408
|
*
|
|
409
|
-
* @param
|
|
409
|
+
* @param userAccessRules - Set of access rule IDs the user has
|
|
410
410
|
*/
|
|
411
411
|
getStrategiesForUser(
|
|
412
|
-
|
|
412
|
+
userAccessRules: Set<string>
|
|
413
413
|
): RegisteredNotificationStrategy<unknown, unknown, unknown>[];
|
|
414
414
|
}
|
|
415
415
|
|
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { oc } from "@orpc/contract";
|
|
3
|
-
import type
|
|
4
|
-
import type { Permission } from "@checkstack/common";
|
|
3
|
+
import { access, type ProcedureMetadata } from "@checkstack/common";
|
|
5
4
|
|
|
6
5
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
-
//
|
|
6
|
+
// Access Rules
|
|
8
7
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
8
|
|
|
10
|
-
export const
|
|
11
|
-
install:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} as const satisfies Record<string, Permission>;
|
|
9
|
+
export const pluginAdminAccess = {
|
|
10
|
+
install: access("plugin", "manage", "Install new plugins from npm"),
|
|
11
|
+
deregister: access("plugin", "manage", "Deregister (uninstall) plugins"),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const pluginAdminAccessRules = [
|
|
15
|
+
pluginAdminAccess.install,
|
|
16
|
+
pluginAdminAccess.deregister,
|
|
17
|
+
];
|
|
20
18
|
|
|
21
19
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
20
|
// Contract
|
|
@@ -31,7 +29,7 @@ export const pluginAdminContract = {
|
|
|
31
29
|
install: _base
|
|
32
30
|
.meta({
|
|
33
31
|
userType: "user",
|
|
34
|
-
|
|
32
|
+
access: [pluginAdminAccess.install],
|
|
35
33
|
})
|
|
36
34
|
.input(
|
|
37
35
|
z.object({
|
|
@@ -52,7 +50,7 @@ export const pluginAdminContract = {
|
|
|
52
50
|
deregister: _base
|
|
53
51
|
.meta({
|
|
54
52
|
userType: "user",
|
|
55
|
-
|
|
53
|
+
access: [pluginAdminAccess.deregister],
|
|
56
54
|
})
|
|
57
55
|
.input(
|
|
58
56
|
z.object({
|
package/src/plugin-system.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
2
|
import { ServiceRef } from "./service-ref";
|
|
3
3
|
import { ExtensionPoint } from "./extension-point";
|
|
4
|
-
import type {
|
|
4
|
+
import type { AccessRule, PluginMetadata } from "@checkstack/common";
|
|
5
5
|
import type { Hook, HookSubscribeOptions, HookUnsubscribe } from "./hooks";
|
|
6
6
|
import { Router } from "@orpc/server";
|
|
7
7
|
import { RpcContext } from "./rpc";
|
|
@@ -70,7 +70,10 @@ export type BackendPluginRegistry = {
|
|
|
70
70
|
registerService: <S>(ref: ServiceRef<S>, impl: S) => void;
|
|
71
71
|
registerExtensionPoint: <T>(ref: ExtensionPoint<T>, impl: T) => void;
|
|
72
72
|
getExtensionPoint: <T>(ref: ExtensionPoint<T>) => T;
|
|
73
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Register access rules for this plugin.
|
|
75
|
+
*/
|
|
76
|
+
registerAccessRules: (accessRules: AccessRule[]) => void;
|
|
74
77
|
/**
|
|
75
78
|
* Registers an oRPC router and its contract for this plugin.
|
|
76
79
|
* The contract is used for OpenAPI generation.
|
|
@@ -85,7 +88,7 @@ export type BackendPluginRegistry = {
|
|
|
85
88
|
*/
|
|
86
89
|
registerCleanup: (cleanup: () => Promise<void>) => void;
|
|
87
90
|
pluginManager: {
|
|
88
|
-
|
|
91
|
+
getAllAccessRules: () => { id: string; description?: string }[];
|
|
89
92
|
};
|
|
90
93
|
};
|
|
91
94
|
|