@checkstack/healthcheck-backend 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 +185 -0
- package/package.json +1 -1
- package/src/index.ts +5 -5
- package/src/queue-executor.test.ts +1 -1
- package/src/queue-executor.ts +18 -9
- package/src/router.test.ts +4 -2
- package/src/router.ts +5 -6
- package/src/service.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,190 @@
|
|
|
1
1
|
# @checkstack/healthcheck-backend
|
|
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
|
+
### Patch Changes
|
|
72
|
+
|
|
73
|
+
- Updated dependencies [9faec1f]
|
|
74
|
+
- Updated dependencies [827b286]
|
|
75
|
+
- Updated dependencies [f533141]
|
|
76
|
+
- Updated dependencies [aa4a8ab]
|
|
77
|
+
- @checkstack/backend-api@0.3.0
|
|
78
|
+
- @checkstack/catalog-backend@0.2.0
|
|
79
|
+
- @checkstack/catalog-common@1.1.0
|
|
80
|
+
- @checkstack/command-backend@0.1.0
|
|
81
|
+
- @checkstack/common@0.2.0
|
|
82
|
+
- @checkstack/healthcheck-common@0.3.0
|
|
83
|
+
- @checkstack/integration-backend@0.1.0
|
|
84
|
+
- @checkstack/signal-common@0.1.0
|
|
85
|
+
- @checkstack/queue-api@0.0.5
|
|
86
|
+
|
|
87
|
+
## 0.2.0
|
|
88
|
+
|
|
89
|
+
### Minor Changes
|
|
90
|
+
|
|
91
|
+
- 8e43507: # Teams and Resource-Level Access Control
|
|
92
|
+
|
|
93
|
+
This release introduces a comprehensive Teams system for organizing users and controlling access to resources at a granular level.
|
|
94
|
+
|
|
95
|
+
## Features
|
|
96
|
+
|
|
97
|
+
### Team Management
|
|
98
|
+
|
|
99
|
+
- Create, update, and delete teams with name and description
|
|
100
|
+
- Add/remove users from teams
|
|
101
|
+
- Designate team managers with elevated privileges
|
|
102
|
+
- View team membership and manager status
|
|
103
|
+
|
|
104
|
+
### Resource-Level Access Control
|
|
105
|
+
|
|
106
|
+
- Grant teams access to specific resources (systems, health checks, incidents, maintenances)
|
|
107
|
+
- Configure read-only or manage permissions per team
|
|
108
|
+
- Resource-level "Team Only" mode that restricts access exclusively to team members
|
|
109
|
+
- Separate `resourceAccessSettings` table for resource-level settings (not per-grant)
|
|
110
|
+
- Automatic cleanup of grants when teams are deleted (database cascade)
|
|
111
|
+
|
|
112
|
+
### Middleware Integration
|
|
113
|
+
|
|
114
|
+
- Extended `autoAuthMiddleware` to support resource access checks
|
|
115
|
+
- Single-resource pre-handler validation for detail endpoints
|
|
116
|
+
- Automatic list filtering for collection endpoints
|
|
117
|
+
- S2S endpoints for access verification
|
|
118
|
+
|
|
119
|
+
### Frontend Components
|
|
120
|
+
|
|
121
|
+
- `TeamsTab` component for managing teams in Auth Settings
|
|
122
|
+
- `TeamAccessEditor` component for assigning team access to resources
|
|
123
|
+
- Resource-level "Team Only" toggle in `TeamAccessEditor`
|
|
124
|
+
- Integration into System, Health Check, Incident, and Maintenance editors
|
|
125
|
+
|
|
126
|
+
## Breaking Changes
|
|
127
|
+
|
|
128
|
+
### API Response Format Changes
|
|
129
|
+
|
|
130
|
+
List endpoints now return objects with named keys instead of arrays directly:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// Before
|
|
134
|
+
const systems = await catalogApi.getSystems();
|
|
135
|
+
|
|
136
|
+
// After
|
|
137
|
+
const { systems } = await catalogApi.getSystems();
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Affected endpoints:
|
|
141
|
+
|
|
142
|
+
- `catalog.getSystems` → `{ systems: [...] }`
|
|
143
|
+
- `healthcheck.getConfigurations` → `{ configurations: [...] }`
|
|
144
|
+
- `incident.listIncidents` → `{ incidents: [...] }`
|
|
145
|
+
- `maintenance.listMaintenances` → `{ maintenances: [...] }`
|
|
146
|
+
|
|
147
|
+
### User Identity Enrichment
|
|
148
|
+
|
|
149
|
+
`RealUser` and `ApplicationUser` types now include `teamIds: string[]` field with team memberships.
|
|
150
|
+
|
|
151
|
+
## Documentation
|
|
152
|
+
|
|
153
|
+
See `docs/backend/teams.md` for complete API reference and integration guide.
|
|
154
|
+
|
|
155
|
+
- 97c5a6b: Add UUID-based collector identification for better multiple collector support
|
|
156
|
+
|
|
157
|
+
**Breaking Change**: Existing health check configurations with collectors need to be recreated.
|
|
158
|
+
|
|
159
|
+
- Each collector instance now has a unique UUID assigned on creation
|
|
160
|
+
- Collector results are stored under the UUID key with `_collectorId` and `_assertionFailed` metadata
|
|
161
|
+
- Auto-charts correctly display separate charts for each collector instance
|
|
162
|
+
- Charts are now grouped by collector instance with clear headings
|
|
163
|
+
- Assertion status card shows pass/fail for each collector
|
|
164
|
+
- Renamed "Success" to "HTTP Success" to clarify it's about HTTP request success
|
|
165
|
+
- Fixed deletion of collectors not persisting to database
|
|
166
|
+
- Fixed duplicate React key warnings in auto-chart grid
|
|
167
|
+
|
|
168
|
+
### Patch Changes
|
|
169
|
+
|
|
170
|
+
- 97c5a6b: Fix collector lookup when health check is assigned to a system
|
|
171
|
+
|
|
172
|
+
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.
|
|
173
|
+
|
|
174
|
+
- Updated dependencies [97c5a6b]
|
|
175
|
+
- Updated dependencies [8e43507]
|
|
176
|
+
- Updated dependencies [8e43507]
|
|
177
|
+
- Updated dependencies [97c5a6b]
|
|
178
|
+
- @checkstack/backend-api@0.2.0
|
|
179
|
+
- @checkstack/catalog-common@1.0.0
|
|
180
|
+
- @checkstack/catalog-backend@0.1.0
|
|
181
|
+
- @checkstack/common@0.1.0
|
|
182
|
+
- @checkstack/healthcheck-common@0.2.0
|
|
183
|
+
- @checkstack/command-backend@0.0.4
|
|
184
|
+
- @checkstack/integration-backend@0.0.4
|
|
185
|
+
- @checkstack/queue-api@0.0.4
|
|
186
|
+
- @checkstack/signal-common@0.0.4
|
|
187
|
+
|
|
3
188
|
## 0.1.0
|
|
4
189
|
|
|
5
190
|
### Minor Changes
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
import * as schema from "./schema";
|
|
6
6
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
healthCheckAccessRules,
|
|
9
|
+
healthCheckAccess,
|
|
9
10
|
pluginMetadata,
|
|
10
11
|
healthCheckContract,
|
|
11
12
|
healthcheckRoutes,
|
|
12
|
-
permissions,
|
|
13
13
|
} from "@checkstack/healthcheck-common";
|
|
14
14
|
import {
|
|
15
15
|
createBackendPlugin,
|
|
@@ -55,7 +55,7 @@ let storedEmitHook: EmitHookFn | undefined;
|
|
|
55
55
|
export default createBackendPlugin({
|
|
56
56
|
metadata: pluginMetadata,
|
|
57
57
|
register(env) {
|
|
58
|
-
env.
|
|
58
|
+
env.registerAccessRules(healthCheckAccessRules);
|
|
59
59
|
|
|
60
60
|
// Register hooks as integration events
|
|
61
61
|
const integrationEvents = env.getExtensionPoint(
|
|
@@ -142,7 +142,7 @@ export default createBackendPlugin({
|
|
|
142
142
|
route:
|
|
143
143
|
resolveRoute(healthcheckRoutes.routes.config) +
|
|
144
144
|
"?action=create",
|
|
145
|
-
|
|
145
|
+
requiredAccessRules: [healthCheckAccess.configuration.manage],
|
|
146
146
|
},
|
|
147
147
|
{
|
|
148
148
|
id: "manage",
|
|
@@ -151,7 +151,7 @@ export default createBackendPlugin({
|
|
|
151
151
|
iconName: "HeartPulse",
|
|
152
152
|
shortcuts: ["meta+shift+h", "ctrl+shift+h"],
|
|
153
153
|
route: resolveRoute(healthcheckRoutes.routes.config),
|
|
154
|
-
|
|
154
|
+
requiredAccessRules: [healthCheckAccess.configuration.manage],
|
|
155
155
|
},
|
|
156
156
|
],
|
|
157
157
|
});
|
|
@@ -61,7 +61,7 @@ const createMockCatalogClient = () => ({
|
|
|
61
61
|
notifySystemSubscribers: mock(async () => ({ notifiedCount: 0 })),
|
|
62
62
|
// Other methods not used in queue-executor
|
|
63
63
|
getEntities: mock(async () => ({ systems: [], groups: [] })),
|
|
64
|
-
getSystems: mock(async () => []),
|
|
64
|
+
getSystems: mock(async () => ({ systems: [] })),
|
|
65
65
|
getGroups: mock(async () => []),
|
|
66
66
|
createSystem: mock(async () => ({})),
|
|
67
67
|
updateSystem: mock(async () => ({})),
|
package/src/queue-executor.ts
CHANGED
|
@@ -316,6 +316,9 @@ async function executeHealthCheckJob(props: {
|
|
|
316
316
|
continue;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
// Use the collector's UUID as the storage key
|
|
320
|
+
const storageKey = collectorEntry.id;
|
|
321
|
+
|
|
319
322
|
try {
|
|
320
323
|
const collectorResult = await registered.collector.execute({
|
|
321
324
|
config: collectorEntry.config,
|
|
@@ -323,9 +326,6 @@ async function executeHealthCheckJob(props: {
|
|
|
323
326
|
pluginId: configRow.strategyId,
|
|
324
327
|
});
|
|
325
328
|
|
|
326
|
-
// Store result under collector ID
|
|
327
|
-
collectorResults[collectorEntry.collectorId] = collectorResult.result;
|
|
328
|
-
|
|
329
329
|
// Check for collector-level error
|
|
330
330
|
if (collectorResult.error) {
|
|
331
331
|
hasCollectorError = true;
|
|
@@ -333,6 +333,7 @@ async function executeHealthCheckJob(props: {
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
// Evaluate per-collector assertions
|
|
336
|
+
let assertionFailed: string | undefined;
|
|
336
337
|
if (
|
|
337
338
|
collectorEntry.assertions &&
|
|
338
339
|
collectorEntry.assertions.length > 0 &&
|
|
@@ -345,23 +346,31 @@ async function executeHealthCheckJob(props: {
|
|
|
345
346
|
);
|
|
346
347
|
if (failedAssertion) {
|
|
347
348
|
hasCollectorError = true;
|
|
348
|
-
|
|
349
|
+
assertionFailed = `${failedAssertion.field} ${
|
|
349
350
|
failedAssertion.operator
|
|
350
351
|
} ${failedAssertion.value ?? ""}`;
|
|
352
|
+
errorMessage = `Assertion failed: ${assertionFailed}`;
|
|
351
353
|
logger.debug(
|
|
352
|
-
`Collector ${
|
|
354
|
+
`Collector ${storageKey} assertion failed: ${errorMessage}`
|
|
353
355
|
);
|
|
354
356
|
}
|
|
355
357
|
}
|
|
358
|
+
|
|
359
|
+
// Store result under the collector's UUID, with collector type and assertion metadata
|
|
360
|
+
collectorResults[storageKey] = {
|
|
361
|
+
_collectorId: collectorEntry.collectorId, // Store the type for frontend schema linking
|
|
362
|
+
_assertionFailed: assertionFailed, // null if no assertion failed
|
|
363
|
+
...collectorResult.result,
|
|
364
|
+
};
|
|
356
365
|
} catch (error) {
|
|
357
366
|
hasCollectorError = true;
|
|
358
367
|
errorMessage = error instanceof Error ? error.message : String(error);
|
|
359
|
-
collectorResults[
|
|
368
|
+
collectorResults[storageKey] = {
|
|
369
|
+
_collectorId: collectorEntry.collectorId,
|
|
370
|
+
_assertionFailed: undefined,
|
|
360
371
|
error: errorMessage,
|
|
361
372
|
};
|
|
362
|
-
logger.debug(
|
|
363
|
-
`Collector ${collectorEntry.collectorId} failed: ${errorMessage}`
|
|
364
|
-
);
|
|
373
|
+
logger.debug(`Collector ${storageKey} failed: ${errorMessage}`);
|
|
365
374
|
}
|
|
366
375
|
}
|
|
367
376
|
} finally {
|
package/src/router.test.ts
CHANGED
|
@@ -8,7 +8,7 @@ describe("HealthCheck Router", () => {
|
|
|
8
8
|
const mockUser = {
|
|
9
9
|
type: "user" as const,
|
|
10
10
|
id: "test-user",
|
|
11
|
-
|
|
11
|
+
accessRules: ["*"],
|
|
12
12
|
roles: ["admin"],
|
|
13
13
|
};
|
|
14
14
|
|
|
@@ -82,11 +82,13 @@ describe("HealthCheck Router", () => {
|
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
const result = await call(router.getConfigurations, undefined, { context });
|
|
85
|
-
expect(
|
|
85
|
+
expect(result).toHaveProperty("configurations");
|
|
86
|
+
expect(Array.isArray(result.configurations)).toBe(true);
|
|
86
87
|
});
|
|
87
88
|
|
|
88
89
|
it("getCollectors returns collectors for strategy", async () => {
|
|
89
90
|
const mockCollector = {
|
|
91
|
+
qualifiedId: "collector-hardware.cpu",
|
|
90
92
|
collector: {
|
|
91
93
|
id: "cpu",
|
|
92
94
|
displayName: "CPU Metrics",
|
package/src/router.ts
CHANGED
|
@@ -14,8 +14,8 @@ import { toJsonSchemaWithChartMeta } from "./schema-utils";
|
|
|
14
14
|
/**
|
|
15
15
|
* Creates the healthcheck router using contract-based implementation.
|
|
16
16
|
*
|
|
17
|
-
* Auth and
|
|
18
|
-
* based on the contract's meta.userType and meta.
|
|
17
|
+
* Auth and access rules are automatically enforced via autoAuthMiddleware
|
|
18
|
+
* based on the contract's meta.userType and meta.access.
|
|
19
19
|
*/
|
|
20
20
|
export const createHealthCheckRouter = (
|
|
21
21
|
database: NodePgDatabase<typeof schema>,
|
|
@@ -68,9 +68,8 @@ export const createHealthCheckRouter = (
|
|
|
68
68
|
pluginId,
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
return registeredCollectors.map(({
|
|
72
|
-
|
|
73
|
-
id: `${ownerPlugin.pluginId}.${collector.id}`,
|
|
71
|
+
return registeredCollectors.map(({ qualifiedId, collector }) => ({
|
|
72
|
+
id: qualifiedId,
|
|
74
73
|
displayName: collector.displayName,
|
|
75
74
|
description: collector.description,
|
|
76
75
|
configSchema: toJsonSchema(collector.config.schema),
|
|
@@ -80,7 +79,7 @@ export const createHealthCheckRouter = (
|
|
|
80
79
|
}),
|
|
81
80
|
|
|
82
81
|
getConfigurations: os.getConfigurations.handler(async () => {
|
|
83
|
-
return service.getConfigurations();
|
|
82
|
+
return { configurations: await service.getConfigurations() };
|
|
84
83
|
}),
|
|
85
84
|
|
|
86
85
|
createConfiguration: os.createConfiguration.handler(async ({ input }) => {
|
package/src/service.ts
CHANGED
|
@@ -491,7 +491,7 @@ export class HealthCheckService {
|
|
|
491
491
|
|
|
492
492
|
/**
|
|
493
493
|
* Get detailed health check run history with full result data.
|
|
494
|
-
* Restricted to users with manage
|
|
494
|
+
* Restricted to users with manage access.
|
|
495
495
|
*/
|
|
496
496
|
async getDetailedHistory(props: {
|
|
497
497
|
systemId?: string;
|
|
@@ -530,7 +530,7 @@ export class HealthCheckService {
|
|
|
530
530
|
.limit(limit)
|
|
531
531
|
.offset(offset);
|
|
532
532
|
|
|
533
|
-
// Return with full result data for manage
|
|
533
|
+
// Return with full result data for manage access
|
|
534
534
|
return {
|
|
535
535
|
runs: runs.map((run) => ({
|
|
536
536
|
id: run.id,
|