@checkstack/backend-api 0.11.1 → 0.13.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,82 @@
1
1
  # @checkstack/backend-api
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8d1ef12: ## Infrastructure Configuration Shell & Cache System
8
+
9
+ ### New Packages
10
+
11
+ - **`@checkstack/cache-api`**: Core cache abstractions — `CacheProvider` interface, `createScopedCache` factory for plugin key isolation, `CachePlugin`/`CacheManager` lifecycle interfaces.
12
+ - **`@checkstack/cache-common`**: Shared cache types, RPC contract (`getPlugins`, `getConfiguration`, `updateConfiguration`), access rules, and plugin metadata.
13
+ - **`@checkstack/cache-backend`**: Cache settings RPC router — exposes plugin discovery, configuration read/write endpoints with access-gated authorization.
14
+ - **`@checkstack/cache-frontend`**: Cache configuration tab component for the Infrastructure Settings page.
15
+ - **`@checkstack/infrastructure-common`**: Infrastructure tab registry, routes, and shared types for the IDE-style configuration shell.
16
+ - **`@checkstack/infrastructure-frontend`**: Infrastructure Settings page with vertical tab bar, per-tab access control, and user menu integration.
17
+
18
+ ### Modified Packages
19
+
20
+ - **`@checkstack/backend-api`**: Added `cachePluginRegistry` and `cacheManager` to `RpcContext` and `coreServices`.
21
+ - **`@checkstack/backend`**: Registered cache services in boot sequence, added cache config loading, extended dependency sorter for cache plugin ordering.
22
+ - **`@checkstack/queue-frontend`**: Refactored from standalone `/queue/config` route to an infrastructure tab. Queue settings now live inside the Infrastructure Settings page.
23
+
24
+ ### Architecture
25
+
26
+ The former monolithic Queue Config page is replaced by a pluggable Infrastructure Settings shell (`/infrastructure/config`). Plugins register configuration tabs via `registerInfrastructureTab()` with their own access rules, icons, and components. The shell evaluates per-tab access and only renders tabs the user can see.
27
+
28
+ ### Patch Changes
29
+
30
+ - Updated dependencies [8d1ef12]
31
+ - Updated dependencies [8d1ef12]
32
+ - Updated dependencies [8d1ef12]
33
+ - Updated dependencies [8d1ef12]
34
+ - @checkstack/healthcheck-common@0.12.0
35
+ - @checkstack/common@0.7.0
36
+ - @checkstack/cache-api@0.2.0
37
+ - @checkstack/signal-common@0.1.10
38
+ - @checkstack/queue-api@0.2.14
39
+
40
+ ## 0.12.0
41
+
42
+ ### Minor Changes
43
+
44
+ - 26d8bae: Distributed satellite health checks and Assignment IDE page
45
+
46
+ **Satellite System**
47
+
48
+ - New `satellite-backend`, `satellite-common`, `satellite-frontend`, and `satellite` agent packages for distributed health check execution
49
+ - WebSocket-based satellite connectivity with authentication, heartbeats, and live configuration push
50
+ - Satellite management UI with create dialog, status badges, and list page
51
+
52
+ **Live Configuration Updates**
53
+
54
+ - Added `assignmentChanged` hook to `healthcheck-backend` for cross-plugin communication
55
+ - `satellite-backend` subscribes to assignment changes and pushes config updates to connected satellites in real-time
56
+
57
+ **Assignment IDE Page**
58
+
59
+ - Replaced the 1028-line modal-based `SystemHealthCheckAssignment` component with a full-page IDE layout
60
+ - New modular components: `AssignmentTree`, `GeneralPanel`, `ThresholdsPanel`, `RetentionPanel`, `ExecutionPanel`
61
+ - Added unassign capability and sorted assignment lists for stable ordering
62
+
63
+ **Shared IDE Primitives**
64
+
65
+ - Extracted `IDETreeNode`, `IDETreeSection`, `IDEStatusBar`, `IDELayout` to `@checkstack/ui` for cross-plugin reuse
66
+ - Migrated existing health check IDE editor to use shared primitives
67
+
68
+ **Infrastructure**
69
+
70
+ - Added `Dockerfile.satellite` for containerized satellite deployment
71
+ - WebSocket route registry in `@checkstack/backend` and `@checkstack/backend-api`
72
+
73
+ ### Patch Changes
74
+
75
+ - Updated dependencies [26d8bae]
76
+ - Updated dependencies [26d8bae]
77
+ - @checkstack/healthcheck-common@0.11.0
78
+ - @checkstack/queue-api@0.2.13
79
+
3
80
  ## 0.11.1
4
81
 
5
82
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/backend-api",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "scripts": {
@@ -9,10 +9,11 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/common": "0.6.4",
13
- "@checkstack/healthcheck-common": "0.10.0",
14
- "@checkstack/queue-api": "0.2.11",
15
- "@checkstack/signal-common": "0.1.8",
12
+ "@checkstack/common": "0.6.5",
13
+ "@checkstack/healthcheck-common": "0.11.0",
14
+ "@checkstack/cache-api": "0.1.0",
15
+ "@checkstack/queue-api": "0.2.13",
16
+ "@checkstack/signal-common": "0.1.9",
16
17
  "@orpc/client": "^1.13.14",
17
18
  "@orpc/contract": "^1.13.14",
18
19
  "@orpc/openapi": "^1.13.2",
@@ -13,6 +13,7 @@ describe("aggregatedAverage", () => {
13
13
  const field = aggregatedAverage({
14
14
  "x-chart-type": "line",
15
15
  "x-chart-label": "Avg Response Time",
16
+ "x-anomaly-enabled": false,
16
17
  });
17
18
 
18
19
  expect(field.type).toBe("average");
@@ -39,6 +40,7 @@ describe("aggregatedRate", () => {
39
40
  const field = aggregatedRate({
40
41
  "x-chart-type": "gauge",
41
42
  "x-chart-label": "Success Rate",
43
+ "x-anomaly-enabled": false,
42
44
  });
43
45
 
44
46
  expect(field.type).toBe("rate");
@@ -65,6 +67,7 @@ describe("aggregatedCounter", () => {
65
67
  it("creates field with correct type", () => {
66
68
  const field = aggregatedCounter({
67
69
  "x-chart-type": "counter",
70
+ "x-anomaly-enabled": false,
68
71
  });
69
72
 
70
73
  expect(field.type).toBe("counter");
@@ -81,6 +84,7 @@ describe("aggregatedMinMax", () => {
81
84
  it("creates field with correct type", () => {
82
85
  const field = aggregatedMinMax({
83
86
  "x-chart-type": "line",
87
+ "x-anomaly-enabled": false,
84
88
  });
85
89
 
86
90
  expect(field.type).toBe("minmax");
@@ -202,6 +206,7 @@ describe("chart metadata registration", () => {
202
206
  "x-chart-type": "line",
203
207
  "x-chart-label": "Avg Response Time",
204
208
  "x-chart-unit": "ms",
209
+ "x-anomaly-enabled": false,
205
210
  });
206
211
 
207
212
  // The stateSchema should have metadata registered via healthResultRegistry
@@ -219,6 +224,7 @@ describe("chart metadata registration", () => {
219
224
  "x-chart-type": "gauge",
220
225
  "x-chart-label": "Success Rate",
221
226
  "x-chart-unit": "%",
227
+ "x-anomaly-enabled": false,
222
228
  });
223
229
 
224
230
  const meta = getHealthResultMeta(field.stateSchema);
@@ -232,6 +238,7 @@ describe("chart metadata registration", () => {
232
238
  const field = aggregatedCounter({
233
239
  "x-chart-type": "counter",
234
240
  "x-chart-label": "Error Count",
241
+ "x-anomaly-enabled": false,
235
242
  });
236
243
 
237
244
  const meta = getHealthResultMeta(field.stateSchema);
@@ -245,6 +252,7 @@ describe("chart metadata registration", () => {
245
252
  const field = aggregatedMinMax({
246
253
  "x-chart-type": "line",
247
254
  "x-chart-label": "Latency Range",
255
+ "x-anomaly-enabled": false,
248
256
  });
249
257
 
250
258
  const meta = getHealthResultMeta(field.stateSchema);
@@ -258,11 +266,13 @@ describe("chart metadata registration", () => {
258
266
  const field1 = aggregatedAverage({
259
267
  "x-chart-type": "line",
260
268
  "x-chart-label": "First Field",
269
+ "x-anomaly-enabled": false,
261
270
  });
262
271
 
263
272
  const field2 = aggregatedAverage({
264
273
  "x-chart-type": "gauge",
265
274
  "x-chart-label": "Second Field",
275
+ "x-anomaly-enabled": false,
266
276
  });
267
277
 
268
278
  const meta1 = getHealthResultMeta(field1.stateSchema);
@@ -40,7 +40,8 @@ export type AggregationType = "average" | "rate" | "counter" | "minmax";
40
40
  // =============================================================================
41
41
 
42
42
  /** Base metadata for chart annotations (excludes x-jsonpath) */
43
- type ChartMeta = Omit<HealthResultMeta, "x-jsonpath">;
43
+ type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
44
+ type ChartMeta = DistributiveOmit<HealthResultMeta, "x-jsonpath">;
44
45
 
45
46
  /**
46
47
  * Base interface for aggregated field definitions.
@@ -207,6 +208,7 @@ export function aggregatedRate(meta: ChartMeta): AggregatedRateField {
207
208
  * errorCount: aggregatedCounter({
208
209
  * "x-chart-type": "counter",
209
210
  * "x-chart-label": "Errors",
211
+ "x-anomaly-direction": "lower-is-better",
210
212
  * }),
211
213
  * };
212
214
  * ```
@@ -3,6 +3,10 @@ import type { RpcService } from "./rpc";
3
3
  import type { HealthCheckRegistry } from "./health-check";
4
4
  import type { CollectorRegistry } from "./collector-registry";
5
5
  import type { QueuePluginRegistry, QueueManager } from "@checkstack/queue-api";
6
+ import type {
7
+ CachePluginRegistry,
8
+ CacheManager,
9
+ } from "@checkstack/cache-api";
6
10
  import type { ConfigService } from "./config-service";
7
11
  import type { SignalService } from "@checkstack/signal-common";
8
12
  import { SafeDatabase } from "./plugin-system";
@@ -14,6 +18,7 @@ import {
14
18
  RpcClient,
15
19
  } from "./types";
16
20
  import type { EventBus } from "./event-bus-types";
21
+ import type { WebSocketRouteRegistry } from "./ws-registry";
17
22
 
18
23
  export * from "./types";
19
24
 
@@ -43,4 +48,9 @@ export const coreServices = {
43
48
  config: createServiceRef<ConfigService>("core.config"),
44
49
  eventBus: createServiceRef<EventBus>("core.eventBus"),
45
50
  signalService: createServiceRef<SignalService>("core.signalService"),
51
+ wsRegistry: createServiceRef<WebSocketRouteRegistry>("core.wsRegistry"),
52
+ cachePluginRegistry: createServiceRef<CachePluginRegistry>(
53
+ "core.cachePluginRegistry",
54
+ ),
55
+ cacheManager: createServiceRef<CacheManager>("core.cacheManager"),
46
56
  };
package/src/index.ts CHANGED
@@ -27,3 +27,4 @@ export * from "./collector-strategy";
27
27
  export * from "./collector-registry";
28
28
  export * from "./incremental-aggregation";
29
29
  export * from "./aggregated-result";
30
+ export * from "./ws-registry";
package/src/rpc.ts CHANGED
@@ -3,6 +3,10 @@ import { AnyContractRouter } from "@orpc/contract";
3
3
  import { HealthCheckRegistry } from "./health-check";
4
4
  import { CollectorRegistry } from "./collector-registry";
5
5
  import { QueuePluginRegistry, QueueManager } from "@checkstack/queue-api";
6
+ import {
7
+ CachePluginRegistry,
8
+ CacheManager,
9
+ } from "@checkstack/cache-api";
6
10
  import {
7
11
  ProcedureMetadata,
8
12
  qualifyAccessRuleId,
@@ -45,6 +49,8 @@ export interface RpcContext {
45
49
  collectorRegistry: CollectorRegistry;
46
50
  queuePluginRegistry: QueuePluginRegistry;
47
51
  queueManager: QueueManager;
52
+ cachePluginRegistry: CachePluginRegistry;
53
+ cacheManager: CacheManager;
48
54
  /** Emit a hook event for cross-plugin communication */
49
55
  emitHook: EmitHookFn;
50
56
  }
package/src/test-utils.ts CHANGED
@@ -4,6 +4,10 @@ import { SafeDatabase } from "./plugin-system";
4
4
  import { HealthCheckRegistry } from "./health-check";
5
5
  import { CollectorRegistry } from "./collector-registry";
6
6
  import { QueuePluginRegistry, QueueManager } from "@checkstack/queue-api";
7
+ import {
8
+ CachePluginRegistry,
9
+ CacheManager,
10
+ } from "@checkstack/cache-api";
7
11
 
8
12
  /**
9
13
  * Creates a mocked oRPC context for testing.
@@ -69,6 +73,18 @@ export function createMockRpcContext(
69
73
  startPolling: mock(),
70
74
  shutdown: mock(),
71
75
  } as unknown as QueueManager,
76
+ cachePluginRegistry: {
77
+ register: mock(),
78
+ getPlugin: mock(),
79
+ getPlugins: mock().mockReturnValue([]),
80
+ } as unknown as CachePluginRegistry,
81
+ cacheManager: {
82
+ getProvider: mock(),
83
+ getActivePlugin: mock().mockReturnValue("memory"),
84
+ getActiveConfig: mock(),
85
+ setActiveBackend: mock(),
86
+ shutdown: mock(),
87
+ } as unknown as CacheManager,
72
88
  // Default: authenticated user with wildcard access for testing
73
89
  user: { type: "user" as const, id: "test-user", accessRules: ["*"] },
74
90
  emitHook: mock() as unknown as EmitHookFn,
@@ -0,0 +1,70 @@
1
+ /**
2
+ * A generic interface for a WebSocket connection managed by the core server.
3
+ * Plugins interact with WebSocket clients through this adapter, insulating
4
+ * them from the underlying Bun WebSocket API.
5
+ */
6
+ export interface WsConnection {
7
+ /** Send a string message to the client */
8
+ send(data: string): void;
9
+ /** Close the connection */
10
+ close(): void;
11
+ }
12
+
13
+ /**
14
+ * Per-connection handlers returned by a route handler's `onConnection` callback.
15
+ * These closures capture the connection's state (e.g., auth state, session data).
16
+ */
17
+ export interface WsConnectionHandlers {
18
+ onMessage: (message: string) => void | Promise<void>;
19
+ onClose: () => void;
20
+ }
21
+
22
+ /**
23
+ * A WebSocket route handler registered by a plugin.
24
+ * The core server calls `onConnection` when a new WebSocket is opened on
25
+ * the handler's registered path, and the handler returns per-connection
26
+ * event callbacks.
27
+ */
28
+ export interface WebSocketRouteHandler {
29
+ /**
30
+ * Called when a new WebSocket connection opens on this route.
31
+ * Return per-connection message/close handlers.
32
+ */
33
+ onConnection(ws: WsConnection): WsConnectionHandlers;
34
+ }
35
+
36
+ /**
37
+ * Scoped registry for plugin WebSocket route handlers.
38
+ * Each plugin receives a scoped instance (via factory) that auto-prefixes
39
+ * the pluginId — just like the RPC service.
40
+ *
41
+ * Plugins register handlers with a local path segment, and the core server
42
+ * makes them available under `/api/ws/{pluginId}{path}`.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * // In satellite-backend init (pluginId "satellite" is auto-prefixed):
47
+ * wsRegistry.register("/", handler);
48
+ * // → Available at /api/ws/satellite
49
+ *
50
+ * wsRegistry.register("/connect", handler);
51
+ * // → Available at /api/ws/satellite/connect
52
+ * ```
53
+ */
54
+ export interface WebSocketRouteRegistry {
55
+ /**
56
+ * Register a WebSocket route handler at the given path.
57
+ * The pluginId prefix is applied automatically by the scoped factory.
58
+ */
59
+ register(path: string, handler: WebSocketRouteHandler): void;
60
+ }
61
+
62
+ /**
63
+ * Core-level store for all registered WS routes.
64
+ * The backend server uses this to look up handlers during WebSocket upgrade.
65
+ * Not exposed to plugins — they interact via the scoped `WebSocketRouteRegistry`.
66
+ */
67
+ export interface WebSocketRouteStore {
68
+ /** Look up a handler by full path (e.g., "satellite" or "satellite/connect"). */
69
+ getHandler(fullPath: string): WebSocketRouteHandler | undefined;
70
+ }