@checkstack/satellite-common 0.2.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 ADDED
@@ -0,0 +1,40 @@
1
+ # @checkstack/satellite-common
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 26d8bae: Distributed satellite health checks and Assignment IDE page
8
+
9
+ **Satellite System**
10
+
11
+ - New `satellite-backend`, `satellite-common`, `satellite-frontend`, and `satellite` agent packages for distributed health check execution
12
+ - WebSocket-based satellite connectivity with authentication, heartbeats, and live configuration push
13
+ - Satellite management UI with create dialog, status badges, and list page
14
+
15
+ **Live Configuration Updates**
16
+
17
+ - Added `assignmentChanged` hook to `healthcheck-backend` for cross-plugin communication
18
+ - `satellite-backend` subscribes to assignment changes and pushes config updates to connected satellites in real-time
19
+
20
+ **Assignment IDE Page**
21
+
22
+ - Replaced the 1028-line modal-based `SystemHealthCheckAssignment` component with a full-page IDE layout
23
+ - New modular components: `AssignmentTree`, `GeneralPanel`, `ThresholdsPanel`, `RetentionPanel`, `ExecutionPanel`
24
+ - Added unassign capability and sorted assignment lists for stable ordering
25
+
26
+ **Shared IDE Primitives**
27
+
28
+ - Extracted `IDETreeNode`, `IDETreeSection`, `IDEStatusBar`, `IDELayout` to `@checkstack/ui` for cross-plugin reuse
29
+ - Migrated existing health check IDE editor to use shared primitives
30
+
31
+ **Infrastructure**
32
+
33
+ - Added `Dockerfile.satellite` for containerized satellite deployment
34
+ - WebSocket route registry in `@checkstack/backend` and `@checkstack/backend-api`
35
+
36
+ ### Patch Changes
37
+
38
+ - Updated dependencies [26d8bae]
39
+ - Updated dependencies [26d8bae]
40
+ - @checkstack/healthcheck-common@0.11.0
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@checkstack/satellite-common",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./src/index.ts"
8
+ }
9
+ },
10
+ "dependencies": {
11
+ "@checkstack/common": "0.6.5",
12
+ "@checkstack/healthcheck-common": "0.10.1",
13
+ "@checkstack/signal-common": "0.1.9",
14
+ "@orpc/contract": "^1.13.14",
15
+ "zod": "^4.2.1"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.7.2",
19
+ "@checkstack/tsconfig": "0.0.5",
20
+ "@checkstack/scripts": "0.1.2"
21
+ },
22
+ "scripts": {
23
+ "typecheck": "tsc --noEmit",
24
+ "lint": "bun run lint:code",
25
+ "lint:code": "eslint . --max-warnings 0"
26
+ },
27
+ "checkstack": {
28
+ "type": "common"
29
+ }
30
+ }
package/src/access.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { accessPair } from "@checkstack/common";
2
+
3
+ /**
4
+ * Access rules for the Satellite plugin.
5
+ */
6
+ export const satelliteAccess = {
7
+ /**
8
+ * Satellite management access with read and manage levels.
9
+ * Read allows viewing satellite list and status.
10
+ * Manage allows creating, deleting, and managing satellites.
11
+ */
12
+ satellite: accessPair("satellite", {
13
+ read: {
14
+ description: "View satellite list and status",
15
+ },
16
+ manage: {
17
+ description:
18
+ "Manage satellites - create, delete, and configure satellite nodes",
19
+ },
20
+ }),
21
+ };
22
+
23
+ /**
24
+ * All access rules for registration with the plugin system.
25
+ */
26
+ export const satelliteAccessRules = [
27
+ satelliteAccess.satellite.read,
28
+ satelliteAccess.satellite.manage,
29
+ ];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * How often satellites send heartbeats to the core (in milliseconds).
3
+ */
4
+ export const HEARTBEAT_INTERVAL_MS = 15_000;
5
+
6
+ /**
7
+ * How long the core waits before considering a satellite offline (in milliseconds).
8
+ * Set to 3× the heartbeat interval to tolerate brief network hiccups.
9
+ */
10
+ export const OFFLINE_THRESHOLD_MS = 45_000;
11
+
12
+ /**
13
+ * Maximum number of health check results to buffer in-memory
14
+ * on the satellite when the WebSocket connection is lost.
15
+ * Oldest results are dropped when the buffer is full (FIFO ring buffer).
16
+ */
17
+ export const RESULT_BUFFER_CAPACITY = 100;
18
+
19
+ /**
20
+ * Base delay for reconnection backoff (in milliseconds).
21
+ * Actual delay uses exponential backoff with jitter.
22
+ */
23
+ export const RECONNECT_BASE_MS = 1000;
24
+
25
+ /**
26
+ * Maximum delay between reconnection attempts (in milliseconds).
27
+ */
28
+ export const RECONNECT_MAX_MS = 30_000;
package/src/index.ts ADDED
@@ -0,0 +1,45 @@
1
+ export { satelliteAccess, satelliteAccessRules } from "./access";
2
+ export {
3
+ satelliteContract,
4
+ SatelliteApi,
5
+ type SatelliteContract,
6
+ } from "./rpc-contract";
7
+ export {
8
+ SatelliteStatusSchema,
9
+ SatelliteSchema,
10
+ SatelliteWithStatusSchema,
11
+ CreateSatelliteSchema,
12
+ type SatelliteStatus,
13
+ type Satellite,
14
+ type SatelliteWithStatus,
15
+ type CreateSatellite,
16
+ } from "./schemas";
17
+ export {
18
+ SatelliteAssignmentSchema,
19
+ SatelliteToCoreMessageSchema,
20
+ CoreToSatelliteMessageSchema,
21
+ type SatelliteAssignment,
22
+ type SatelliteToCoreMessage,
23
+ type CoreToSatelliteMessage,
24
+ type AuthenticateMessage,
25
+ type HeartbeatMessage,
26
+ type ResultMessage,
27
+ type StrategyErrorMessage,
28
+ type AuthenticatedMessage,
29
+ type AuthFailedMessage,
30
+ type ConfigUpdatedMessage,
31
+ type ShutdownMessage,
32
+ } from "./protocol";
33
+ export {
34
+ SATELLITE_STATUS_CHANGED,
35
+ SATELLITE_CONFIG_CHANGED,
36
+ } from "./signals";
37
+ export {
38
+ HEARTBEAT_INTERVAL_MS,
39
+ OFFLINE_THRESHOLD_MS,
40
+ RESULT_BUFFER_CAPACITY,
41
+ RECONNECT_BASE_MS,
42
+ RECONNECT_MAX_MS,
43
+ } from "./constants";
44
+ export * from "./plugin-metadata";
45
+ export { satelliteRoutes } from "./routes";
@@ -0,0 +1,9 @@
1
+ import { definePluginMetadata } from "@checkstack/common";
2
+
3
+ /**
4
+ * Plugin metadata for the satellite plugin.
5
+ * Exported from the common package so both backend and frontend can reference it.
6
+ */
7
+ export const pluginMetadata = definePluginMetadata({
8
+ pluginId: "satellite",
9
+ });
@@ -0,0 +1,141 @@
1
+ import { z } from "zod";
2
+ import {
3
+ HealthCheckStatusSchema,
4
+ HealthCheckRunResultSchema,
5
+ } from "@checkstack/healthcheck-common";
6
+
7
+ // =============================================================================
8
+ // SATELLITE ASSIGNMENT (Core → Satellite configuration payload)
9
+ // =============================================================================
10
+
11
+ /**
12
+ * A single collector configuration sent to the satellite for execution.
13
+ */
14
+ const SatelliteCollectorConfigSchema = z.object({
15
+ id: z.string(),
16
+ collectorId: z.string(),
17
+ config: z.record(z.string(), z.unknown()),
18
+ assertions: z
19
+ .array(
20
+ z.object({
21
+ field: z.string(),
22
+ jsonPath: z.string().optional(),
23
+ operator: z.string(),
24
+ value: z.unknown().optional(),
25
+ }),
26
+ )
27
+ .optional(),
28
+ });
29
+
30
+ /**
31
+ * A health check assignment sent from the core to a satellite.
32
+ * Contains everything the satellite needs to execute the check.
33
+ */
34
+ export const SatelliteAssignmentSchema = z.object({
35
+ configId: z.string(),
36
+ systemId: z.string(),
37
+ strategyId: z.string(),
38
+ config: z.record(z.string(), z.unknown()),
39
+ collectors: z.array(SatelliteCollectorConfigSchema).optional(),
40
+ intervalSeconds: z.number(),
41
+ });
42
+
43
+ export type SatelliteAssignment = z.infer<typeof SatelliteAssignmentSchema>;
44
+
45
+ // =============================================================================
46
+ // SATELLITE → CORE MESSAGES
47
+ // =============================================================================
48
+
49
+ const AuthenticateMessageSchema = z.object({
50
+ type: z.literal("authenticate"),
51
+ clientId: z.string(),
52
+ token: z.string(),
53
+ });
54
+
55
+ const HeartbeatMessageSchema = z.object({
56
+ type: z.literal("heartbeat"),
57
+ version: z.string(),
58
+ uptimeSeconds: z.number(),
59
+ });
60
+
61
+ const ResultMessageSchema = z.object({
62
+ type: z.literal("result"),
63
+ configId: z.string(),
64
+ systemId: z.string(),
65
+ status: HealthCheckStatusSchema,
66
+ latencyMs: z.number().optional(),
67
+ /** Structured run result — typed to enforce parity with the local executor */
68
+ result: HealthCheckRunResultSchema.optional(),
69
+ executedAt: z.string(),
70
+ });
71
+
72
+ const StrategyErrorMessageSchema = z.object({
73
+ type: z.literal("strategy_error"),
74
+ strategyId: z.string(),
75
+ message: z.string(),
76
+ });
77
+
78
+ /**
79
+ * Discriminated union of all messages that a satellite can send to the core.
80
+ */
81
+ export const SatelliteToCoreMessageSchema = z.discriminatedUnion("type", [
82
+ AuthenticateMessageSchema,
83
+ HeartbeatMessageSchema,
84
+ ResultMessageSchema,
85
+ StrategyErrorMessageSchema,
86
+ ]);
87
+
88
+ export type SatelliteToCoreMessage = z.infer<
89
+ typeof SatelliteToCoreMessageSchema
90
+ >;
91
+
92
+ // Re-export individual message types for use in handler type narrowing
93
+ export type AuthenticateMessage = z.infer<typeof AuthenticateMessageSchema>;
94
+ export type HeartbeatMessage = z.infer<typeof HeartbeatMessageSchema>;
95
+ export type ResultMessage = z.infer<typeof ResultMessageSchema>;
96
+ export type StrategyErrorMessage = z.infer<typeof StrategyErrorMessageSchema>;
97
+
98
+ // =============================================================================
99
+ // CORE → SATELLITE MESSAGES
100
+ // =============================================================================
101
+
102
+ const AuthenticatedMessageSchema = z.object({
103
+ type: z.literal("authenticated"),
104
+ satelliteId: z.string(),
105
+ assignments: z.array(SatelliteAssignmentSchema),
106
+ });
107
+
108
+ const AuthFailedMessageSchema = z.object({
109
+ type: z.literal("auth_failed"),
110
+ reason: z.string(),
111
+ });
112
+
113
+ const ConfigUpdatedMessageSchema = z.object({
114
+ type: z.literal("config_updated"),
115
+ assignments: z.array(SatelliteAssignmentSchema),
116
+ });
117
+
118
+ const ShutdownMessageSchema = z.object({
119
+ type: z.literal("shutdown"),
120
+ reason: z.string(),
121
+ });
122
+
123
+ /**
124
+ * Discriminated union of all messages that the core can send to a satellite.
125
+ */
126
+ export const CoreToSatelliteMessageSchema = z.discriminatedUnion("type", [
127
+ AuthenticatedMessageSchema,
128
+ AuthFailedMessageSchema,
129
+ ConfigUpdatedMessageSchema,
130
+ ShutdownMessageSchema,
131
+ ]);
132
+
133
+ export type CoreToSatelliteMessage = z.infer<
134
+ typeof CoreToSatelliteMessageSchema
135
+ >;
136
+
137
+ // Re-export individual message types
138
+ export type AuthenticatedMessage = z.infer<typeof AuthenticatedMessageSchema>;
139
+ export type AuthFailedMessage = z.infer<typeof AuthFailedMessageSchema>;
140
+ export type ConfigUpdatedMessage = z.infer<typeof ConfigUpdatedMessageSchema>;
141
+ export type ShutdownMessage = z.infer<typeof ShutdownMessageSchema>;
package/src/routes.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { createRoutes } from "@checkstack/common";
2
+
3
+ /**
4
+ * Route definitions for the satellite plugin.
5
+ * Import and use these routes in both frontend plugins and for link generation.
6
+ *
7
+ * @example Frontend plugin usage
8
+ * ```tsx
9
+ * import { satelliteRoutes } from "@checkstack/satellite-common";
10
+ *
11
+ * createFrontendPlugin({
12
+ * routes: [
13
+ * { route: satelliteRoutes.routes.list, element: <SatelliteListPage /> },
14
+ * ],
15
+ * });
16
+ * ```
17
+ */
18
+ export const satelliteRoutes = createRoutes("satellite", {
19
+ list: "/",
20
+ });
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ import { createClientDefinition, proc } from "@checkstack/common";
3
+ import { satelliteAccess } from "./access";
4
+ import { pluginMetadata } from "./plugin-metadata";
5
+ import { SatelliteWithStatusSchema, CreateSatelliteSchema } from "./schemas";
6
+
7
+ /**
8
+ * RPC contract for satellite management.
9
+ * Consumed by satellite-frontend for the management UI.
10
+ */
11
+ export const satelliteContract = {
12
+ /** List all satellites with computed online/offline status */
13
+ listSatellites: proc({
14
+ operationType: "query",
15
+ userType: "authenticated",
16
+ access: [satelliteAccess.satellite.read],
17
+ }).output(
18
+ z.object({ satellites: z.array(SatelliteWithStatusSchema) }),
19
+ ),
20
+
21
+ /** Get a single satellite by ID */
22
+ getSatellite: proc({
23
+ operationType: "query",
24
+ userType: "authenticated",
25
+ access: [satelliteAccess.satellite.read],
26
+ })
27
+ .input(z.object({ id: z.string() }))
28
+ .output(SatelliteWithStatusSchema.nullable()),
29
+
30
+ /**
31
+ * Create a new satellite.
32
+ * Returns the satellite record AND the plaintext token (shown once).
33
+ * The clientId is the satellite's UUID (satellite.id).
34
+ */
35
+ createSatellite: proc({
36
+ operationType: "mutation",
37
+ userType: "authenticated",
38
+ access: [satelliteAccess.satellite.manage],
39
+ })
40
+ .input(CreateSatelliteSchema)
41
+ .output(
42
+ z.object({
43
+ satellite: SatelliteWithStatusSchema,
44
+ /** Plaintext token — shown once, never stored */
45
+ token: z.string(),
46
+ }),
47
+ ),
48
+
49
+ /** Delete a satellite by ID */
50
+ deleteSatellite: proc({
51
+ operationType: "mutation",
52
+ userType: "authenticated",
53
+ access: [satelliteAccess.satellite.manage],
54
+ })
55
+ .input(z.object({ id: z.string() }))
56
+ .output(z.void()),
57
+
58
+ /**
59
+ * Get the list of online satellite IDs.
60
+ * Used internally by healthcheck-backend for stale source exclusion
61
+ * during health state evaluation.
62
+ */
63
+ getOnlineSatelliteIds: proc({
64
+ operationType: "query",
65
+ userType: "service",
66
+ access: [],
67
+ }).output(z.object({ satelliteIds: z.array(z.string()) })),
68
+ };
69
+
70
+ // Export contract type
71
+ export type SatelliteContract = typeof satelliteContract;
72
+
73
+ // Export client definition for type-safe forPlugin usage
74
+ // Use: const client = rpcApi.forPlugin(SatelliteApi);
75
+ export const SatelliteApi = createClientDefinition(
76
+ satelliteContract,
77
+ pluginMetadata,
78
+ );
package/src/schemas.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+
3
+ // =============================================================================
4
+ // SATELLITE ENTITY SCHEMAS
5
+ // =============================================================================
6
+
7
+ /**
8
+ * Satellite status derived from lastHeartbeatAt vs OFFLINE_THRESHOLD_MS.
9
+ */
10
+ export const SatelliteStatusSchema = z.enum(["online", "offline"]);
11
+
12
+ export type SatelliteStatus = z.infer<typeof SatelliteStatusSchema>;
13
+
14
+ /**
15
+ * Full satellite record as stored in the database.
16
+ * tokenHash is intentionally excluded from the schema —
17
+ * it should never leave the backend.
18
+ */
19
+ export const SatelliteSchema = z.object({
20
+ id: z.string().uuid(),
21
+ name: z.string(),
22
+ region: z.string(),
23
+ tags: z.record(z.string(), z.string()),
24
+ lastHeartbeatAt: z.date().optional(),
25
+ version: z.string().optional(),
26
+ createdAt: z.date(),
27
+ });
28
+
29
+ export type Satellite = z.infer<typeof SatelliteSchema>;
30
+
31
+ /**
32
+ * Satellite with computed online/offline status.
33
+ * Used in API responses — the status is derived from lastHeartbeatAt.
34
+ */
35
+ export const SatelliteWithStatusSchema = SatelliteSchema.extend({
36
+ status: SatelliteStatusSchema,
37
+ });
38
+
39
+ export type SatelliteWithStatus = z.infer<typeof SatelliteWithStatusSchema>;
40
+
41
+ /**
42
+ * Input schema for creating a new satellite.
43
+ */
44
+ export const CreateSatelliteSchema = z.object({
45
+ name: z.string().min(1, "Name is required"),
46
+ region: z.string().min(1, "Region is required"),
47
+ tags: z.record(z.string(), z.string()).default({}),
48
+ });
49
+
50
+ export type CreateSatellite = z.infer<typeof CreateSatelliteSchema>;
package/src/signals.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { createSignal } from "@checkstack/signal-common";
2
+ import { z } from "zod";
3
+ import { SatelliteStatusSchema } from "./schemas";
4
+
5
+ /**
6
+ * Broadcast when a satellite's connection status changes (online ↔ offline).
7
+ * Frontend components listening to this signal can update status badges in real-time.
8
+ */
9
+ export const SATELLITE_STATUS_CHANGED = createSignal(
10
+ "satellite.status.changed",
11
+ z.object({
12
+ satelliteId: z.string(),
13
+ status: SatelliteStatusSchema,
14
+ name: z.string(),
15
+ region: z.string(),
16
+ }),
17
+ );
18
+
19
+ /**
20
+ * Broadcast when satellite configuration changes (assignments updated).
21
+ * Frontend components listening to this signal can refetch satellite data.
22
+ */
23
+ export const SATELLITE_CONFIG_CHANGED = createSignal(
24
+ "satellite.config.changed",
25
+ z.object({
26
+ satelliteId: z.string(),
27
+ }),
28
+ );
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/common.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }