@checkstack/satellite-common 0.6.0 → 0.7.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,66 @@
1
1
  # @checkstack/satellite-common
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 270ef29: Extend the satellite protocol for script-package distribution: the
8
+ `authenticated` / `config_updated` payloads now carry an optional
9
+ `scriptPackagesLockfileHash` (the durable convergence backstop), a new
10
+ `refresh_script_packages { lockfileHash }` core->satellite control push,
11
+ and a new `script_package_sync_state` satellite->core report message. All
12
+ additions are optional / additive, so existing satellites and protocol
13
+ tests are unaffected.
14
+ - 270ef29: Satellite-side script-package reconciliation over the WS channel.
15
+
16
+ - `satellite-common`: WS request/reply messages for pulling the manifest +
17
+ blobs from core (`request_script_package_manifest` /
18
+ `request_script_package_blob` -> `script_package_manifest` /
19
+ `script_package_blob`).
20
+ - `satellite-backend`: the WS handler answers those requests from the
21
+ script-packages store (satellites pull from core, never the registry).
22
+ - `@checkstack/satellite`: the client gains request/reply plumbing + a
23
+ `SatelliteScriptPackages` manager that reuses the Phase 2 reconciler
24
+ (`reconcileToHash` + `createReconcileFsDeps`) over the WS transport. It
25
+ reconciles on a `refresh_script_packages` push and on the
26
+ assignment-carried hash (startup / reconnect backstop), pulls only missing
27
+ blobs (delta), materializes via `bun install --offline`, atomically flips
28
+ `current`, reports sync state back, and degrades cleanly (error state, no
29
+ stale tree, no registry access) when a blob can't be fetched. Reconciles
30
+ are serialized + coalesced + idempotent.
31
+
32
+ - 270ef29: Secrets platform Phase 3: just-in-time secret delivery to satellites + source-side masking, and central-execution injection for healthcheck collectors.
33
+
34
+ - New satellite WS messages `request_run_secrets` / `run_secrets`: just
35
+ before a satellite runs a collector that declares a `secretEnv`, it asks
36
+ core for that collector's resolved env; core resolves ONLY the secrets the
37
+ collector's OWN persisted assignment declares (least-privilege — the
38
+ satellite cannot choose) and replies with the env map (or a clear error).
39
+ The satellite injects it memory-only for the run and drops it on
40
+ completion. Secrets never ride the persisted assignment and never touch
41
+ disk.
42
+ - Source-side masking: the satellite runs `maskSecrets` over the collector's
43
+ stdout/stderr/result/error using the run's delivered values BEFORE the
44
+ result leaves the satellite (defense in depth).
45
+ - `CollectorStrategy.execute` gains an optional `secretEnv`. The
46
+ inline-script and shell collectors inject it into the runner
47
+ (`process.env` / `$VAR`) and mask the values out of their output.
48
+ - Healthcheck collectors running centrally (the queue executor) also resolve
49
+ - inject `secretEnv` via `secretResolverRef`, closing the gap where a
50
+ centrally-run secretEnv collector got no secrets. A missing required
51
+ secret fails the run clearly in all paths.
52
+
53
+ ### Patch Changes
54
+
55
+ - Updated dependencies [270ef29]
56
+ - Updated dependencies [270ef29]
57
+ - Updated dependencies [270ef29]
58
+ - Updated dependencies [b995afb]
59
+ - Updated dependencies [b995afb]
60
+ - Updated dependencies [270ef29]
61
+ - Updated dependencies [270ef29]
62
+ - @checkstack/healthcheck-common@1.4.0
63
+
3
64
  ## 0.6.0
4
65
 
5
66
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/satellite-common",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -9,16 +9,16 @@
9
9
  }
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/common": "0.11.0",
13
- "@checkstack/healthcheck-common": "1.2.0",
14
- "@checkstack/signal-common": "0.2.4",
12
+ "@checkstack/common": "0.12.0",
13
+ "@checkstack/healthcheck-common": "1.3.0",
14
+ "@checkstack/signal-common": "0.2.5",
15
15
  "@orpc/contract": "^1.13.14",
16
16
  "zod": "^4.2.1"
17
17
  },
18
18
  "devDependencies": {
19
19
  "typescript": "^5.7.2",
20
20
  "@checkstack/tsconfig": "0.0.7",
21
- "@checkstack/scripts": "0.3.3"
21
+ "@checkstack/scripts": "0.3.4"
22
22
  },
23
23
  "scripts": {
24
24
  "typecheck": "tsgo -b",
package/src/index.ts CHANGED
@@ -29,6 +29,14 @@ export {
29
29
  type AuthFailedMessage,
30
30
  type ConfigUpdatedMessage,
31
31
  type ShutdownMessage,
32
+ type ScriptPackageSyncStateMessage,
33
+ type RefreshScriptPackagesMessage,
34
+ type ScriptPackageManifestMessage,
35
+ type ScriptPackageBlobMessage,
36
+ type RequestScriptPackageManifestMessage,
37
+ type RequestScriptPackageBlobMessage,
38
+ type RequestRunSecretsMessage,
39
+ type RunSecretsMessage,
32
40
  } from "./protocol";
33
41
  export {
34
42
  SATELLITE_STATUS_CHANGED,
@@ -1,5 +1,9 @@
1
1
  import { describe, test, expect } from "bun:test";
2
- import { SatelliteAssignmentSchema } from "./protocol";
2
+ import {
3
+ SatelliteAssignmentSchema,
4
+ CoreToSatelliteMessageSchema,
5
+ SatelliteToCoreMessageSchema,
6
+ } from "./protocol";
3
7
 
4
8
  describe("SatelliteAssignmentSchema", () => {
5
9
  const base = {
@@ -28,3 +32,119 @@ describe("SatelliteAssignmentSchema", () => {
28
32
  expect(parsed.systemName).toBeUndefined();
29
33
  });
30
34
  });
35
+
36
+ describe("script-packages protocol extensions", () => {
37
+ test("authenticated carries an optional scriptPackagesLockfileHash", () => {
38
+ const withHash = CoreToSatelliteMessageSchema.parse({
39
+ type: "authenticated",
40
+ satelliteId: "sat-1",
41
+ assignments: [],
42
+ scriptPackagesLockfileHash: "abc123",
43
+ });
44
+ expect(withHash.type).toBe("authenticated");
45
+ if (withHash.type === "authenticated") {
46
+ expect(withHash.scriptPackagesLockfileHash).toBe("abc123");
47
+ }
48
+ });
49
+
50
+ test("authenticated WITHOUT the hash still parses (version-skew safe)", () => {
51
+ const parsed = CoreToSatelliteMessageSchema.parse({
52
+ type: "authenticated",
53
+ satelliteId: "sat-1",
54
+ assignments: [],
55
+ });
56
+ expect(parsed.type).toBe("authenticated");
57
+ if (parsed.type === "authenticated") {
58
+ expect(parsed.scriptPackagesLockfileHash).toBeUndefined();
59
+ }
60
+ });
61
+
62
+ test("config_updated carries the optional hash", () => {
63
+ const parsed = CoreToSatelliteMessageSchema.parse({
64
+ type: "config_updated",
65
+ assignments: [],
66
+ scriptPackagesLockfileHash: null,
67
+ });
68
+ if (parsed.type === "config_updated") {
69
+ expect(parsed.scriptPackagesLockfileHash).toBeNull();
70
+ }
71
+ });
72
+
73
+ test("refresh_script_packages round-trips", () => {
74
+ const parsed = CoreToSatelliteMessageSchema.parse({
75
+ type: "refresh_script_packages",
76
+ lockfileHash: "deadbeef",
77
+ });
78
+ expect(parsed.type).toBe("refresh_script_packages");
79
+ if (parsed.type === "refresh_script_packages") {
80
+ expect(parsed.lockfileHash).toBe("deadbeef");
81
+ }
82
+ });
83
+
84
+ test("script_package_sync_state round-trips (satellite -> core)", () => {
85
+ const parsed = SatelliteToCoreMessageSchema.parse({
86
+ type: "script_package_sync_state",
87
+ lockfileHash: "abc",
88
+ status: "ready",
89
+ });
90
+ expect(parsed.type).toBe("script_package_sync_state");
91
+ if (parsed.type === "script_package_sync_state") {
92
+ expect(parsed.status).toBe("ready");
93
+ expect(parsed.lockfileHash).toBe("abc");
94
+ }
95
+ });
96
+
97
+ test("script_package_sync_state carries an error", () => {
98
+ const parsed = SatelliteToCoreMessageSchema.parse({
99
+ type: "script_package_sync_state",
100
+ lockfileHash: null,
101
+ status: "error",
102
+ errorMessage: "blob fetch failed",
103
+ });
104
+ if (parsed.type === "script_package_sync_state") {
105
+ expect(parsed.status).toBe("error");
106
+ expect(parsed.errorMessage).toBe("blob fetch failed");
107
+ }
108
+ });
109
+ });
110
+
111
+ describe("run-secrets request/reply (Phase 3 JIT delivery)", () => {
112
+ test("parses request_run_secrets", () => {
113
+ const parsed = SatelliteToCoreMessageSchema.parse({
114
+ type: "request_run_secrets",
115
+ requestId: "req-1",
116
+ configId: "config-1",
117
+ collectorId: "inline-script",
118
+ runId: "run-1",
119
+ });
120
+ expect(parsed.type).toBe("request_run_secrets");
121
+ if (parsed.type === "request_run_secrets") {
122
+ expect(parsed.requestId).toBe("req-1");
123
+ expect(parsed.collectorId).toBe("inline-script");
124
+ }
125
+ });
126
+
127
+ test("parses run_secrets reply with env", () => {
128
+ const parsed = CoreToSatelliteMessageSchema.parse({
129
+ type: "run_secrets",
130
+ requestId: "req-1",
131
+ env: { API_TOKEN: "resolved-value" },
132
+ });
133
+ if (parsed.type === "run_secrets") {
134
+ expect(parsed.env).toEqual({ API_TOKEN: "resolved-value" });
135
+ expect(parsed.error).toBeUndefined();
136
+ }
137
+ });
138
+
139
+ test("parses run_secrets reply with error (no env)", () => {
140
+ const parsed = CoreToSatelliteMessageSchema.parse({
141
+ type: "run_secrets",
142
+ requestId: "req-1",
143
+ error: "required secret not available",
144
+ });
145
+ if (parsed.type === "run_secrets") {
146
+ expect(parsed.error).toBe("required secret not available");
147
+ expect(parsed.env).toBeUndefined();
148
+ }
149
+ });
150
+ });
package/src/protocol.ts CHANGED
@@ -78,6 +78,64 @@ const StrategyErrorMessageSchema = z.object({
78
78
  message: z.string(),
79
79
  });
80
80
 
81
+ /**
82
+ * Reports the satellite's script-package reconcile state back to the core,
83
+ * which persists it in `script_package_satellite_state` for the admin UI.
84
+ * Sent after a reconcile attempt (success or failure).
85
+ */
86
+ const ScriptPackageSyncStateMessageSchema = z.object({
87
+ type: z.literal("script_package_sync_state"),
88
+ /** Active lockfile hash this satellite has materialized, or null. */
89
+ lockfileHash: z.string().nullable(),
90
+ /** "pending" | "syncing" | "ready" | "error" */
91
+ status: z.enum(["pending", "syncing", "ready", "error"]),
92
+ errorMessage: z.string().optional(),
93
+ });
94
+
95
+ /**
96
+ * Satellite -> core request for the manifest of a lockfile hash, so the
97
+ * satellite can diff against its local cache. The core replies with a
98
+ * `script_package_manifest` message. Delivered over the existing
99
+ * authenticated WS channel (no separate satellite HTTP auth surface).
100
+ */
101
+ const RequestScriptPackageManifestMessageSchema = z.object({
102
+ type: z.literal("request_script_package_manifest"),
103
+ lockfileHash: z.string(),
104
+ });
105
+
106
+ /**
107
+ * Satellite -> core request for one content-addressed blob by integrity.
108
+ * The core replies with a `script_package_blob` message (base64 bytes).
109
+ */
110
+ const RequestScriptPackageBlobMessageSchema = z.object({
111
+ type: z.literal("request_script_package_blob"),
112
+ integrity: z.string(),
113
+ });
114
+
115
+ /**
116
+ * Satellite -> core just-in-time request for a collector run's resolved
117
+ * secret env, sent right before the satellite executes a collector that
118
+ * declares a `secretEnv` mapping. The core resolves ONLY that collector's
119
+ * declared `secretEnv` (read from the persisted assignment — the satellite
120
+ * does not get to choose which secrets), and replies with a `run_secrets`
121
+ * message carrying the env map (or an error).
122
+ *
123
+ * Secrets NEVER ride the persisted assignment payload; they are delivered
124
+ * over this authenticated channel per-run and held in satellite memory only
125
+ * for the lifetime of the run. `requestId` correlates the reply (a config
126
+ * can run repeatedly; `runId` is for logging/audit).
127
+ */
128
+ const RequestRunSecretsMessageSchema = z.object({
129
+ type: z.literal("request_run_secrets"),
130
+ /** Correlation id for the reply. */
131
+ requestId: z.string(),
132
+ /** The assignment/collector whose declared secretEnv to resolve. */
133
+ configId: z.string(),
134
+ collectorId: z.string(),
135
+ /** Opaque per-run id for audit/logging on the core side. */
136
+ runId: z.string(),
137
+ });
138
+
81
139
  /**
82
140
  * Discriminated union of all messages that a satellite can send to the core.
83
141
  */
@@ -86,6 +144,10 @@ export const SatelliteToCoreMessageSchema = z.discriminatedUnion("type", [
86
144
  HeartbeatMessageSchema,
87
145
  ResultMessageSchema,
88
146
  StrategyErrorMessageSchema,
147
+ ScriptPackageSyncStateMessageSchema,
148
+ RequestScriptPackageManifestMessageSchema,
149
+ RequestScriptPackageBlobMessageSchema,
150
+ RequestRunSecretsMessageSchema,
89
151
  ]);
90
152
 
91
153
  export type SatelliteToCoreMessage = z.infer<
@@ -97,6 +159,18 @@ export type AuthenticateMessage = z.infer<typeof AuthenticateMessageSchema>;
97
159
  export type HeartbeatMessage = z.infer<typeof HeartbeatMessageSchema>;
98
160
  export type ResultMessage = z.infer<typeof ResultMessageSchema>;
99
161
  export type StrategyErrorMessage = z.infer<typeof StrategyErrorMessageSchema>;
162
+ export type ScriptPackageSyncStateMessage = z.infer<
163
+ typeof ScriptPackageSyncStateMessageSchema
164
+ >;
165
+ export type RequestScriptPackageManifestMessage = z.infer<
166
+ typeof RequestScriptPackageManifestMessageSchema
167
+ >;
168
+ export type RequestScriptPackageBlobMessage = z.infer<
169
+ typeof RequestScriptPackageBlobMessageSchema
170
+ >;
171
+ export type RequestRunSecretsMessage = z.infer<
172
+ typeof RequestRunSecretsMessageSchema
173
+ >;
100
174
 
101
175
  // =============================================================================
102
176
  // CORE → SATELLITE MESSAGES
@@ -106,6 +180,13 @@ const AuthenticatedMessageSchema = z.object({
106
180
  type: z.literal("authenticated"),
107
181
  satelliteId: z.string(),
108
182
  assignments: z.array(SatelliteAssignmentSchema),
183
+ /**
184
+ * Desired script-package lockfile hash. Carried alongside assignments as
185
+ * the durable convergence backstop: a satellite that booted after (or
186
+ * missed) a `refresh_script_packages` push still converges on connect.
187
+ * Optional for version-skew safety; null means "no packages installed".
188
+ */
189
+ scriptPackagesLockfileHash: z.string().nullable().optional(),
109
190
  });
110
191
 
111
192
  const AuthFailedMessageSchema = z.object({
@@ -116,6 +197,8 @@ const AuthFailedMessageSchema = z.object({
116
197
  const ConfigUpdatedMessageSchema = z.object({
117
198
  type: z.literal("config_updated"),
118
199
  assignments: z.array(SatelliteAssignmentSchema),
200
+ /** See {@link AuthenticatedMessageSchema.scriptPackagesLockfileHash}. */
201
+ scriptPackagesLockfileHash: z.string().nullable().optional(),
119
202
  });
120
203
 
121
204
  const ShutdownMessageSchema = z.object({
@@ -123,6 +206,60 @@ const ShutdownMessageSchema = z.object({
123
206
  reason: z.string(),
124
207
  });
125
208
 
209
+ /**
210
+ * Control push telling the satellite to reconcile its script packages to a
211
+ * new lockfile hash. Sent by each core instance's `script-packages.changed`
212
+ * broadcast handler to its currently-connected satellites. Best-effort
213
+ * liveness; the assignment-carried `scriptPackagesLockfileHash` is the
214
+ * durable backstop.
215
+ */
216
+ const RefreshScriptPackagesMessageSchema = z.object({
217
+ type: z.literal("refresh_script_packages"),
218
+ lockfileHash: z.string(),
219
+ });
220
+
221
+ /** One resolved package in a manifest reply. */
222
+ const ManifestEntryWireSchema = z.object({
223
+ name: z.string(),
224
+ version: z.string(),
225
+ integrity: z.string(),
226
+ });
227
+
228
+ /** Core reply to `request_script_package_manifest`. */
229
+ const ScriptPackageManifestMessageSchema = z.object({
230
+ type: z.literal("script_package_manifest"),
231
+ lockfileHash: z.string(),
232
+ entries: z.array(ManifestEntryWireSchema),
233
+ });
234
+
235
+ /** Core reply to `request_script_package_blob` (base64 compressed bytes). */
236
+ const ScriptPackageBlobMessageSchema = z.object({
237
+ type: z.literal("script_package_blob"),
238
+ integrity: z.string(),
239
+ /** base64-encoded compressed blob bytes, or null if not found. */
240
+ data: z.string().nullable(),
241
+ });
242
+
243
+ /**
244
+ * Core reply to `request_run_secrets`. Carries the resolved env map on
245
+ * success, or an `error` when a required secret could not be resolved /
246
+ * the collector was not found. The satellite injects `env` memory-only for
247
+ * the run and drops it on completion; on `error` it fails the run clearly
248
+ * rather than running without the secret.
249
+ *
250
+ * The env map never persists on the core side and is never written to disk
251
+ * on the satellite.
252
+ */
253
+ const RunSecretsMessageSchema = z.object({
254
+ type: z.literal("run_secrets"),
255
+ /** Correlates with the originating `request_run_secrets`. */
256
+ requestId: z.string(),
257
+ /** Resolved env, present only on success. */
258
+ env: z.record(z.string(), z.string()).optional(),
259
+ /** Set when resolution failed; `env` is then absent. */
260
+ error: z.string().optional(),
261
+ });
262
+
126
263
  /**
127
264
  * Discriminated union of all messages that the core can send to a satellite.
128
265
  */
@@ -131,6 +268,10 @@ export const CoreToSatelliteMessageSchema = z.discriminatedUnion("type", [
131
268
  AuthFailedMessageSchema,
132
269
  ConfigUpdatedMessageSchema,
133
270
  ShutdownMessageSchema,
271
+ RefreshScriptPackagesMessageSchema,
272
+ ScriptPackageManifestMessageSchema,
273
+ ScriptPackageBlobMessageSchema,
274
+ RunSecretsMessageSchema,
134
275
  ]);
135
276
 
136
277
  export type CoreToSatelliteMessage = z.infer<
@@ -142,3 +283,13 @@ export type AuthenticatedMessage = z.infer<typeof AuthenticatedMessageSchema>;
142
283
  export type AuthFailedMessage = z.infer<typeof AuthFailedMessageSchema>;
143
284
  export type ConfigUpdatedMessage = z.infer<typeof ConfigUpdatedMessageSchema>;
144
285
  export type ShutdownMessage = z.infer<typeof ShutdownMessageSchema>;
286
+ export type RefreshScriptPackagesMessage = z.infer<
287
+ typeof RefreshScriptPackagesMessageSchema
288
+ >;
289
+ export type ScriptPackageManifestMessage = z.infer<
290
+ typeof ScriptPackageManifestMessageSchema
291
+ >;
292
+ export type ScriptPackageBlobMessage = z.infer<
293
+ typeof ScriptPackageBlobMessageSchema
294
+ >;
295
+ export type RunSecretsMessage = z.infer<typeof RunSecretsMessageSchema>;