@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-backend",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
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
- permissionList,
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.registerPermissions(permissionList);
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
- requiredPermissions: [permissions.healthCheckManage],
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
- requiredPermissions: [permissions.healthCheckManage],
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 () => ({})),
@@ -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
- errorMessage = `Assertion failed: ${failedAssertion.field} ${
349
+ assertionFailed = `${failedAssertion.field} ${
349
350
  failedAssertion.operator
350
351
  } ${failedAssertion.value ?? ""}`;
352
+ errorMessage = `Assertion failed: ${assertionFailed}`;
351
353
  logger.debug(
352
- `Collector ${collectorEntry.collectorId} assertion failed: ${errorMessage}`
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[collectorEntry.collectorId] = {
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 {
@@ -8,7 +8,7 @@ describe("HealthCheck Router", () => {
8
8
  const mockUser = {
9
9
  type: "user" as const,
10
10
  id: "test-user",
11
- permissions: ["*"],
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(Array.isArray(result)).toBe(true);
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 permissions are automatically enforced via autoAuthMiddleware
18
- * based on the contract's meta.userType and meta.permissions.
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(({ collector, ownerPlugin }) => ({
72
- // Fully-qualified ID: ownerPluginId.collectorId
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 permission.
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 permission
533
+ // Return with full result data for manage access
534
534
  return {
535
535
  runs: runs.map((run) => ({
536
536
  id: run.id,