@checkstack/satellite-backend 0.3.6 → 0.5.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 +204 -0
- package/drizzle/0001_tiresome_terror.sql +3 -0
- package/drizzle/0002_graceful_mac_gargan.sql +2 -0
- package/drizzle/meta/0001_snapshot.json +102 -0
- package/drizzle/meta/0002_snapshot.json +89 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +22 -13
- package/src/automations.ts +107 -0
- package/src/entity.test.ts +313 -0
- package/src/entity.ts +221 -0
- package/src/heartbeat-monitor.it.test.ts +232 -0
- package/src/heartbeat-monitor.test.ts +156 -83
- package/src/heartbeat-monitor.ts +106 -35
- package/src/hooks.ts +9 -2
- package/src/index.ts +180 -0
- package/src/run-secret-resolver.test.ts +121 -0
- package/src/run-secret-resolver.ts +66 -0
- package/src/satellite-ws-handler.test.ts +267 -0
- package/src/satellite-ws-handler.ts +266 -6
- package/src/schema.ts +22 -1
- package/src/service.test.ts +274 -0
- package/src/service.ts +133 -15
- package/src/status.ts +18 -0
- package/tsconfig.json +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,209 @@
|
|
|
1
1
|
# @checkstack/satellite-backend
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b995afb: Make `satellite-connection` a plugin-backed, COMPUTE-ON-READ reactive entity over the DURABLE, globally-readable `satellites` table via the Model-B entity state machine.
|
|
8
|
+
|
|
9
|
+
Satellite defines a `satellite-connection` entity `{ status: "online" | "offline", name, region, lastSeenAt, lastEvent }` keyed by satellite id. Its `status` is COMPUTED on read from the durable `last_heartbeat_at` column (via `computeStatus` / `OFFLINE_THRESHOLD_MS` - the SAME single liveness source of truth the admin satellite list uses), not stored. The only extra durable connection column is `last_connection_event` (`connected` / `disconnected` / `heartbeat_lost`), which the change-deriver reads as `lastEvent`. `lastSeenAt` is derived from `last_heartbeat_at` (null after a clean disconnect). There is no `entity_state` mirror and no stored status copy. `defineEntity({ kind: "satellite-connection", read })` resolves via `SatelliteService.getManyConnectionStates`. The three lifecycle sites - WS authentication (sets `last_heartbeat_at = now`, event `connected`), WS socket close (clears `last_heartbeat_at = null` so status flips offline immediately, event `disconnected`), and the heartbeat monitor's online->offline edge (flips only the event to `heartbeat_lost`) - drive `handle.mutate({ id: satelliteId, apply })`, where `apply` UPDATEs the satellite row's liveness columns and returns the computed view. The platform still records full transition HISTORY in `entity_transitions` for every change. `last_heartbeat_at` is the reactive liveness input now, so the `declareNonReactiveState` escape-hatch is retained for raw bookkeeping but the status it drives is the entity's.
|
|
10
|
+
|
|
11
|
+
This fixes a horizontal-scaling defect twice over. (a) Connection state originally lived in a process-local in-memory map, so a satellite connected to pod A was invisible to pod B's reads. (b) A prior fix made the STATUS durable but left the online->offline (`heartbeat_lost`) transition DETECTION pod-local: the heartbeat-check job runs under one consumer group claimed by a VARYING pod, and a pod with an empty in-memory `previousStatuses` map never observed the satellite online, so it never fired `heartbeat_lost` - leaving the stored `connection_status` stuck `online` forever after a pod crash. Computing status on read removes the stored copy that could get stuck (a stale row self-heals to `offline` once `last_heartbeat_at` ages past the threshold, from any pod), and the heartbeat monitor now detects the lost edge from DURABLE state alone - any pod, idempotent across pods + redelivery: it reads every satellite's `(last_heartbeat_at, last_connection_event)`, computes status, and for any that is `offline` while still marked `connected` drives the `heartbeat_lost` mutate; once `last_connection_event = "heartbeat_lost"`, re-runs are no-ops (a small in-memory set is kept ONLY as a per-pod broadcast-dedup optimisation, never as the source of truth). A new env-gated cross-pod IT (`heartbeat-monitor.it.test.ts`) proves a `heartbeat_lost` detected by a fresh "pod" that never saw the satellite online, so the pod-local-baseline bug cannot recur. `toSatelliteWithStatus` / `getOnlineSatelliteIds` and the entity now derive from the same `last_heartbeat_at` - one source of truth, no disagreement.
|
|
12
|
+
|
|
13
|
+
A registered change-deriver maps these entity changes back to the `satellite.connected` / `satellite.disconnected` / `satellite.heartbeat_lost` trigger events, so existing automations keep firing. The three-way distinction is preserved by an explicit `lastEvent` discriminator on the entity state: a bare `status` diff cannot tell a socket drop (`disconnected`) apart from the heartbeat-lost offline edge (`heartbeat_lost`), so the deriver reads `lastEvent` to fire the exact original event. The old connection hooks are removed in favor of the reactive entity.
|
|
14
|
+
|
|
15
|
+
BREAKING CHANGES:
|
|
16
|
+
|
|
17
|
+
- DROPPED the `satellites.connection_status` and `satellites.last_seen_at` columns added by the prior fix (migration `0002_graceful_mac_gargan.sql`, forward-only). Status is now computed on read from `last_heartbeat_at` (no stored copy to drift or get stuck), and `lastSeenAt` is derived from `last_heartbeat_at`. The `last_connection_event` column is KEPT (the deriver's event discriminator + the monitor's idempotency key). Existing rows with a non-null `last_connection_event` keep reading their derived status; rows that never connected (null `last_connection_event`) report no current state until their next lifecycle edge.
|
|
18
|
+
- Removed the `satellite.connected`, `satellite.disconnected`, and `satellite.heartbeat_lost` hooks (`createHook`). Use the `satellite-connection` entity's auto-emitted change events (subscribe via the `automation.entity` extension point's `onEntityChanged`, or author automations against the derived trigger events). The `satellite.removed` deletion/cleanup hook is unaffected and stays.
|
|
19
|
+
- The `connected` / `disconnected` / `heartbeat_lost` automation triggers are now ENTITY-DRIVEN instead of hook-backed: they are fired by the `satellite-connection` entity change-deriver (Stage-1 routing) rather than a `createHook`, but stay REGISTERED in the automation editor's trigger catalog (a no-op `setup` via `makeEntityDrivenTriggerSetup`), so they remain offered as picker entries and payload-introspectable. Already-authored automations referencing them continue to fire. A registered `toPayload` mapper keeps the runtime `trigger.payload` matching each trigger's declared `payloadSchema` (`satelliteId`, `name`, `region`, `status`, `lastSeenAt` (nullable - `null` after a clean disconnect)), rather than degrading to the generic entity-change shape.
|
|
20
|
+
- The `satellite-connection` entity's current state is COMPUTED on read from the durable `satellites.last_heartbeat_at` (+ `last_connection_event`), NOT a framework `entity_state` row and NOT a stored status column. Any code reading current connection state must read through the entity `read` accessor / `handle.get` / `getMany`. Durable history in `entity_transitions` is unaffected.
|
|
21
|
+
|
|
22
|
+
- 270ef29: Core-side satellite script-package distribution.
|
|
23
|
+
|
|
24
|
+
- `satellite-backend`: the WS handler now carries the desired script-package
|
|
25
|
+
lockfile hash in `authenticated` / `config_updated` payloads (the durable
|
|
26
|
+
backstop), exposes `pushRefreshScriptPackagesToAll` (wired to the
|
|
27
|
+
`script-packages.changed` broadcast hook in `mode: "broadcast"`, so each
|
|
28
|
+
core instance fans the refresh out to its own connected satellites), and
|
|
29
|
+
persists `script_package_sync_state` reports from satellites.
|
|
30
|
+
- `script-packages-*`: new `reportSatelliteSyncState` RPC + store method so
|
|
31
|
+
satellite-backend can record per-satellite reconcile state for the admin
|
|
32
|
+
UI. Satellites pull blobs from core via the existing `getManifest` /
|
|
33
|
+
`downloadBlob` endpoints, never the registry.
|
|
34
|
+
|
|
35
|
+
- 270ef29: Satellite-side script-package reconciliation over the WS channel.
|
|
36
|
+
|
|
37
|
+
- `satellite-common`: WS request/reply messages for pulling the manifest +
|
|
38
|
+
blobs from core (`request_script_package_manifest` /
|
|
39
|
+
`request_script_package_blob` -> `script_package_manifest` /
|
|
40
|
+
`script_package_blob`).
|
|
41
|
+
- `satellite-backend`: the WS handler answers those requests from the
|
|
42
|
+
script-packages store (satellites pull from core, never the registry).
|
|
43
|
+
- `@checkstack/satellite`: the client gains request/reply plumbing + a
|
|
44
|
+
`SatelliteScriptPackages` manager that reuses the Phase 2 reconciler
|
|
45
|
+
(`reconcileToHash` + `createReconcileFsDeps`) over the WS transport. It
|
|
46
|
+
reconciles on a `refresh_script_packages` push and on the
|
|
47
|
+
assignment-carried hash (startup / reconnect backstop), pulls only missing
|
|
48
|
+
blobs (delta), materializes via `bun install --offline`, atomically flips
|
|
49
|
+
`current`, reports sync state back, and degrades cleanly (error state, no
|
|
50
|
+
stale tree, no registry access) when a blob can't be fetched. Reconciles
|
|
51
|
+
are serialized + coalesced + idempotent.
|
|
52
|
+
|
|
53
|
+
- 270ef29: Secrets platform Phase 3: just-in-time secret delivery to satellites + source-side masking, and central-execution injection for healthcheck collectors.
|
|
54
|
+
|
|
55
|
+
- New satellite WS messages `request_run_secrets` / `run_secrets`: just
|
|
56
|
+
before a satellite runs a collector that declares a `secretEnv`, it asks
|
|
57
|
+
core for that collector's resolved env; core resolves ONLY the secrets the
|
|
58
|
+
collector's OWN persisted assignment declares (least-privilege — the
|
|
59
|
+
satellite cannot choose) and replies with the env map (or a clear error).
|
|
60
|
+
The satellite injects it memory-only for the run and drops it on
|
|
61
|
+
completion. Secrets never ride the persisted assignment and never touch
|
|
62
|
+
disk.
|
|
63
|
+
- Source-side masking: the satellite runs `maskSecrets` over the collector's
|
|
64
|
+
stdout/stderr/result/error using the run's delivered values BEFORE the
|
|
65
|
+
result leaves the satellite (defense in depth).
|
|
66
|
+
- `CollectorStrategy.execute` gains an optional `secretEnv`. The
|
|
67
|
+
inline-script and shell collectors inject it into the runner
|
|
68
|
+
(`process.env` / `$VAR`) and mask the values out of their output.
|
|
69
|
+
- Healthcheck collectors running centrally (the queue executor) also resolve
|
|
70
|
+
- inject `secretEnv` via `secretResolverRef`, closing the gap where a
|
|
71
|
+
centrally-run secretEnv collector got no secrets. A missing required
|
|
72
|
+
secret fails the run clearly in all paths.
|
|
73
|
+
|
|
74
|
+
### Patch Changes
|
|
75
|
+
|
|
76
|
+
- b995afb: Extract a shared `withEntityWrite` / `withEntityRemove` guard for PLUGIN-BACKED (Model B) reactive entities and refactor the per-domain copies onto it.
|
|
77
|
+
|
|
78
|
+
Every plugin-backed domain (incident, catalog, dependency, maintenance, slo, satellite) reimplemented the same "no handle wired → run the plugin write directly; handle wired → route through `handle.mutate` / `handle.remove`" guard, varying only in the id-key name. `@checkstack/automation-backend` now exports `withEntityWrite` / `withEntityRemove` (from the entity barrel) and each domain's thin, well-named wrappers (`writeIncidentEntity`, `writeMaintenanceEntity`, satellite's `mirror`, …) delegate to it, so the branch lives in exactly one place. Behavior is unchanged.
|
|
79
|
+
|
|
80
|
+
`writeHealthEntity` (healthcheck-backend) is intentionally NOT migrated onto the helper — it is genuinely bespoke (closure-captured durable state, distinct rethrow-vs-fail-soft branches, a per-system serializer, and it returns the computed state). SLO keeps its fail-soft `onError` wrapper around the shared guard.
|
|
81
|
+
|
|
82
|
+
- Updated dependencies [270ef29]
|
|
83
|
+
- Updated dependencies [b995afb]
|
|
84
|
+
- Updated dependencies [b995afb]
|
|
85
|
+
- Updated dependencies [b995afb]
|
|
86
|
+
- Updated dependencies [270ef29]
|
|
87
|
+
- Updated dependencies [270ef29]
|
|
88
|
+
- Updated dependencies [270ef29]
|
|
89
|
+
- Updated dependencies [270ef29]
|
|
90
|
+
- Updated dependencies [270ef29]
|
|
91
|
+
- Updated dependencies [270ef29]
|
|
92
|
+
- Updated dependencies [270ef29]
|
|
93
|
+
- Updated dependencies [270ef29]
|
|
94
|
+
- Updated dependencies [270ef29]
|
|
95
|
+
- Updated dependencies [b995afb]
|
|
96
|
+
- Updated dependencies [b995afb]
|
|
97
|
+
- Updated dependencies [270ef29]
|
|
98
|
+
- Updated dependencies [b995afb]
|
|
99
|
+
- Updated dependencies [270ef29]
|
|
100
|
+
- Updated dependencies [b995afb]
|
|
101
|
+
- Updated dependencies [b995afb]
|
|
102
|
+
- Updated dependencies [270ef29]
|
|
103
|
+
- Updated dependencies [b995afb]
|
|
104
|
+
- Updated dependencies [b995afb]
|
|
105
|
+
- Updated dependencies [270ef29]
|
|
106
|
+
- Updated dependencies [b995afb]
|
|
107
|
+
- Updated dependencies [b995afb]
|
|
108
|
+
- Updated dependencies [b995afb]
|
|
109
|
+
- Updated dependencies [b995afb]
|
|
110
|
+
- Updated dependencies [b995afb]
|
|
111
|
+
- Updated dependencies [b995afb]
|
|
112
|
+
- Updated dependencies [b995afb]
|
|
113
|
+
- Updated dependencies [270ef29]
|
|
114
|
+
- Updated dependencies [270ef29]
|
|
115
|
+
- Updated dependencies [270ef29]
|
|
116
|
+
- Updated dependencies [270ef29]
|
|
117
|
+
- Updated dependencies [270ef29]
|
|
118
|
+
- Updated dependencies [270ef29]
|
|
119
|
+
- Updated dependencies [270ef29]
|
|
120
|
+
- Updated dependencies [b995afb]
|
|
121
|
+
- Updated dependencies [270ef29]
|
|
122
|
+
- Updated dependencies [b995afb]
|
|
123
|
+
- Updated dependencies [270ef29]
|
|
124
|
+
- Updated dependencies [270ef29]
|
|
125
|
+
- Updated dependencies [270ef29]
|
|
126
|
+
- Updated dependencies [b995afb]
|
|
127
|
+
- Updated dependencies [270ef29]
|
|
128
|
+
- Updated dependencies [b995afb]
|
|
129
|
+
- Updated dependencies [270ef29]
|
|
130
|
+
- Updated dependencies [270ef29]
|
|
131
|
+
- Updated dependencies [b995afb]
|
|
132
|
+
- Updated dependencies [270ef29]
|
|
133
|
+
- Updated dependencies [270ef29]
|
|
134
|
+
- Updated dependencies [270ef29]
|
|
135
|
+
- Updated dependencies [270ef29]
|
|
136
|
+
- Updated dependencies [270ef29]
|
|
137
|
+
- Updated dependencies [270ef29]
|
|
138
|
+
- Updated dependencies [270ef29]
|
|
139
|
+
- Updated dependencies [b995afb]
|
|
140
|
+
- Updated dependencies [b995afb]
|
|
141
|
+
- Updated dependencies [b995afb]
|
|
142
|
+
- @checkstack/backend-api@0.19.0
|
|
143
|
+
- @checkstack/automation-backend@0.3.0
|
|
144
|
+
- @checkstack/gitops-common@0.5.0
|
|
145
|
+
- @checkstack/gitops-backend@0.4.0
|
|
146
|
+
- @checkstack/automation-common@0.3.0
|
|
147
|
+
- @checkstack/healthcheck-backend@1.4.0
|
|
148
|
+
- @checkstack/healthcheck-common@1.4.0
|
|
149
|
+
- @checkstack/secrets-backend@0.1.0
|
|
150
|
+
- @checkstack/script-packages-backend@0.2.0
|
|
151
|
+
- @checkstack/script-packages-common@0.2.0
|
|
152
|
+
- @checkstack/satellite-common@0.7.0
|
|
153
|
+
- @checkstack/secrets-common@0.1.0
|
|
154
|
+
- @checkstack/queue-api@0.3.7
|
|
155
|
+
|
|
156
|
+
## 0.4.0
|
|
157
|
+
|
|
158
|
+
### Minor Changes
|
|
159
|
+
|
|
160
|
+
- 41c77f4: feat(satellite): Phase 9 — connection lifecycle triggers
|
|
161
|
+
|
|
162
|
+
- New hooks `satelliteHooks.connected`, `satelliteHooks.disconnected`,
|
|
163
|
+
and `satelliteHooks.heartbeatLost`. `connected` and `disconnected`
|
|
164
|
+
fire from the WS handler at auth completion and `onClose`
|
|
165
|
+
respectively; `heartbeatLost` fires from the heartbeat monitor on
|
|
166
|
+
the `online → offline` edge only (the opposite edge is observable
|
|
167
|
+
via `connected`).
|
|
168
|
+
- Triggers `satellite.connected`, `satellite.disconnected`,
|
|
169
|
+
`satellite.heartbeat_lost` registered against the Automation
|
|
170
|
+
Platform. All carry `contextKey: (p) => p.satelliteId` so a
|
|
171
|
+
long-running automation can resume on the same satellite.
|
|
172
|
+
- No mutation actions in this chunk — connection lifecycle is
|
|
173
|
+
observed only, not commanded.
|
|
174
|
+
|
|
175
|
+
Plumbing: `SatelliteWsHandler` and `HeartbeatMonitor` both take an
|
|
176
|
+
optional hook sink in their constructor. The sink is provided from
|
|
177
|
+
`afterPluginsReady` where `emitHook` is available; until then, the
|
|
178
|
+
classes behave exactly as before (no hooks fired, no behavioural
|
|
179
|
+
change).
|
|
180
|
+
|
|
181
|
+
### Patch Changes
|
|
182
|
+
|
|
183
|
+
- Updated dependencies [e2d6f25]
|
|
184
|
+
- Updated dependencies [41c77f4]
|
|
185
|
+
- Updated dependencies [41c77f4]
|
|
186
|
+
- Updated dependencies [e1a2077]
|
|
187
|
+
- Updated dependencies [41c77f4]
|
|
188
|
+
- Updated dependencies [41c77f4]
|
|
189
|
+
- Updated dependencies [41c77f4]
|
|
190
|
+
- Updated dependencies [41c77f4]
|
|
191
|
+
- Updated dependencies [41c77f4]
|
|
192
|
+
- Updated dependencies [41c77f4]
|
|
193
|
+
- Updated dependencies [6d52276]
|
|
194
|
+
- Updated dependencies [6d52276]
|
|
195
|
+
- Updated dependencies [35bc682]
|
|
196
|
+
- @checkstack/automation-backend@0.2.0
|
|
197
|
+
- @checkstack/healthcheck-backend@1.3.0
|
|
198
|
+
- @checkstack/common@0.12.0
|
|
199
|
+
- @checkstack/backend-api@0.18.0
|
|
200
|
+
- @checkstack/healthcheck-common@1.3.0
|
|
201
|
+
- @checkstack/satellite-common@0.6.0
|
|
202
|
+
- @checkstack/gitops-backend@0.3.7
|
|
203
|
+
- @checkstack/gitops-common@0.4.2
|
|
204
|
+
- @checkstack/signal-common@0.2.5
|
|
205
|
+
- @checkstack/queue-api@0.3.6
|
|
206
|
+
|
|
3
207
|
## 0.3.6
|
|
4
208
|
|
|
5
209
|
### Patch Changes
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "4ec7195f-656f-46cf-bdf7-966a470b8a0c",
|
|
3
|
+
"prevId": "8c89df94-7c41-44ec-9197-02edea2f23f2",
|
|
4
|
+
"version": "7",
|
|
5
|
+
"dialect": "postgresql",
|
|
6
|
+
"tables": {
|
|
7
|
+
"public.satellites": {
|
|
8
|
+
"name": "satellites",
|
|
9
|
+
"schema": "",
|
|
10
|
+
"columns": {
|
|
11
|
+
"id": {
|
|
12
|
+
"name": "id",
|
|
13
|
+
"type": "uuid",
|
|
14
|
+
"primaryKey": true,
|
|
15
|
+
"notNull": true,
|
|
16
|
+
"default": "gen_random_uuid()"
|
|
17
|
+
},
|
|
18
|
+
"name": {
|
|
19
|
+
"name": "name",
|
|
20
|
+
"type": "text",
|
|
21
|
+
"primaryKey": false,
|
|
22
|
+
"notNull": true
|
|
23
|
+
},
|
|
24
|
+
"region": {
|
|
25
|
+
"name": "region",
|
|
26
|
+
"type": "text",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true
|
|
29
|
+
},
|
|
30
|
+
"tags": {
|
|
31
|
+
"name": "tags",
|
|
32
|
+
"type": "jsonb",
|
|
33
|
+
"primaryKey": false,
|
|
34
|
+
"notNull": true,
|
|
35
|
+
"default": "'{}'::jsonb"
|
|
36
|
+
},
|
|
37
|
+
"token_hash": {
|
|
38
|
+
"name": "token_hash",
|
|
39
|
+
"type": "text",
|
|
40
|
+
"primaryKey": false,
|
|
41
|
+
"notNull": true
|
|
42
|
+
},
|
|
43
|
+
"last_heartbeat_at": {
|
|
44
|
+
"name": "last_heartbeat_at",
|
|
45
|
+
"type": "timestamp",
|
|
46
|
+
"primaryKey": false,
|
|
47
|
+
"notNull": false
|
|
48
|
+
},
|
|
49
|
+
"version": {
|
|
50
|
+
"name": "version",
|
|
51
|
+
"type": "text",
|
|
52
|
+
"primaryKey": false,
|
|
53
|
+
"notNull": false
|
|
54
|
+
},
|
|
55
|
+
"connection_status": {
|
|
56
|
+
"name": "connection_status",
|
|
57
|
+
"type": "text",
|
|
58
|
+
"primaryKey": false,
|
|
59
|
+
"notNull": true,
|
|
60
|
+
"default": "'offline'"
|
|
61
|
+
},
|
|
62
|
+
"last_seen_at": {
|
|
63
|
+
"name": "last_seen_at",
|
|
64
|
+
"type": "timestamp",
|
|
65
|
+
"primaryKey": false,
|
|
66
|
+
"notNull": false
|
|
67
|
+
},
|
|
68
|
+
"last_connection_event": {
|
|
69
|
+
"name": "last_connection_event",
|
|
70
|
+
"type": "text",
|
|
71
|
+
"primaryKey": false,
|
|
72
|
+
"notNull": false
|
|
73
|
+
},
|
|
74
|
+
"created_at": {
|
|
75
|
+
"name": "created_at",
|
|
76
|
+
"type": "timestamp",
|
|
77
|
+
"primaryKey": false,
|
|
78
|
+
"notNull": true,
|
|
79
|
+
"default": "now()"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"indexes": {},
|
|
83
|
+
"foreignKeys": {},
|
|
84
|
+
"compositePrimaryKeys": {},
|
|
85
|
+
"uniqueConstraints": {},
|
|
86
|
+
"policies": {},
|
|
87
|
+
"checkConstraints": {},
|
|
88
|
+
"isRLSEnabled": false
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"enums": {},
|
|
92
|
+
"schemas": {},
|
|
93
|
+
"sequences": {},
|
|
94
|
+
"roles": {},
|
|
95
|
+
"policies": {},
|
|
96
|
+
"views": {},
|
|
97
|
+
"_meta": {
|
|
98
|
+
"columns": {},
|
|
99
|
+
"schemas": {},
|
|
100
|
+
"tables": {}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "937ff36d-f1f0-4f70-bf75-2a6eb15e4dd7",
|
|
3
|
+
"prevId": "4ec7195f-656f-46cf-bdf7-966a470b8a0c",
|
|
4
|
+
"version": "7",
|
|
5
|
+
"dialect": "postgresql",
|
|
6
|
+
"tables": {
|
|
7
|
+
"public.satellites": {
|
|
8
|
+
"name": "satellites",
|
|
9
|
+
"schema": "",
|
|
10
|
+
"columns": {
|
|
11
|
+
"id": {
|
|
12
|
+
"name": "id",
|
|
13
|
+
"type": "uuid",
|
|
14
|
+
"primaryKey": true,
|
|
15
|
+
"notNull": true,
|
|
16
|
+
"default": "gen_random_uuid()"
|
|
17
|
+
},
|
|
18
|
+
"name": {
|
|
19
|
+
"name": "name",
|
|
20
|
+
"type": "text",
|
|
21
|
+
"primaryKey": false,
|
|
22
|
+
"notNull": true
|
|
23
|
+
},
|
|
24
|
+
"region": {
|
|
25
|
+
"name": "region",
|
|
26
|
+
"type": "text",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true
|
|
29
|
+
},
|
|
30
|
+
"tags": {
|
|
31
|
+
"name": "tags",
|
|
32
|
+
"type": "jsonb",
|
|
33
|
+
"primaryKey": false,
|
|
34
|
+
"notNull": true,
|
|
35
|
+
"default": "'{}'::jsonb"
|
|
36
|
+
},
|
|
37
|
+
"token_hash": {
|
|
38
|
+
"name": "token_hash",
|
|
39
|
+
"type": "text",
|
|
40
|
+
"primaryKey": false,
|
|
41
|
+
"notNull": true
|
|
42
|
+
},
|
|
43
|
+
"last_heartbeat_at": {
|
|
44
|
+
"name": "last_heartbeat_at",
|
|
45
|
+
"type": "timestamp",
|
|
46
|
+
"primaryKey": false,
|
|
47
|
+
"notNull": false
|
|
48
|
+
},
|
|
49
|
+
"version": {
|
|
50
|
+
"name": "version",
|
|
51
|
+
"type": "text",
|
|
52
|
+
"primaryKey": false,
|
|
53
|
+
"notNull": false
|
|
54
|
+
},
|
|
55
|
+
"last_connection_event": {
|
|
56
|
+
"name": "last_connection_event",
|
|
57
|
+
"type": "text",
|
|
58
|
+
"primaryKey": false,
|
|
59
|
+
"notNull": false
|
|
60
|
+
},
|
|
61
|
+
"created_at": {
|
|
62
|
+
"name": "created_at",
|
|
63
|
+
"type": "timestamp",
|
|
64
|
+
"primaryKey": false,
|
|
65
|
+
"notNull": true,
|
|
66
|
+
"default": "now()"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"indexes": {},
|
|
70
|
+
"foreignKeys": {},
|
|
71
|
+
"compositePrimaryKeys": {},
|
|
72
|
+
"uniqueConstraints": {},
|
|
73
|
+
"policies": {},
|
|
74
|
+
"checkConstraints": {},
|
|
75
|
+
"isRLSEnabled": false
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"enums": {},
|
|
79
|
+
"schemas": {},
|
|
80
|
+
"sequences": {},
|
|
81
|
+
"roles": {},
|
|
82
|
+
"policies": {},
|
|
83
|
+
"views": {},
|
|
84
|
+
"_meta": {
|
|
85
|
+
"columns": {},
|
|
86
|
+
"schemas": {},
|
|
87
|
+
"tables": {}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
"when": 1776590977685,
|
|
9
9
|
"tag": "0000_melted_gargoyle",
|
|
10
10
|
"breakpoints": true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"idx": 1,
|
|
14
|
+
"version": "7",
|
|
15
|
+
"when": 1780215564477,
|
|
16
|
+
"tag": "0001_tiresome_terror",
|
|
17
|
+
"breakpoints": true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"idx": 2,
|
|
21
|
+
"version": "7",
|
|
22
|
+
"when": 1780221476961,
|
|
23
|
+
"tag": "0002_graceful_mac_gargan",
|
|
24
|
+
"breakpoints": true
|
|
11
25
|
}
|
|
12
26
|
]
|
|
13
27
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/satellite-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -11,29 +11,38 @@
|
|
|
11
11
|
"typecheck": "tsgo -b",
|
|
12
12
|
"generate": "drizzle-kit generate",
|
|
13
13
|
"lint": "bun run lint:code",
|
|
14
|
-
"lint:code": "eslint . --max-warnings 0"
|
|
14
|
+
"lint:code": "eslint . --max-warnings 0",
|
|
15
|
+
"test": "bun test"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
17
|
-
"@checkstack/backend-api": "0.
|
|
18
|
-
"@checkstack/
|
|
19
|
-
"@checkstack/
|
|
20
|
-
"@checkstack/
|
|
21
|
-
"@checkstack/healthcheck-
|
|
22
|
-
"@checkstack/
|
|
23
|
-
"@checkstack/
|
|
24
|
-
"@checkstack/common": "0.
|
|
25
|
-
"@checkstack/
|
|
18
|
+
"@checkstack/backend-api": "0.18.0",
|
|
19
|
+
"@checkstack/automation-backend": "0.2.0",
|
|
20
|
+
"@checkstack/automation-common": "0.2.0",
|
|
21
|
+
"@checkstack/satellite-common": "0.6.0",
|
|
22
|
+
"@checkstack/healthcheck-common": "1.3.0",
|
|
23
|
+
"@checkstack/signal-common": "0.2.5",
|
|
24
|
+
"@checkstack/healthcheck-backend": "1.3.0",
|
|
25
|
+
"@checkstack/script-packages-common": "0.1.0",
|
|
26
|
+
"@checkstack/script-packages-backend": "0.1.0",
|
|
27
|
+
"@checkstack/secrets-common": "0.0.1",
|
|
28
|
+
"@checkstack/secrets-backend": "0.0.1",
|
|
29
|
+
"@checkstack/gitops-backend": "0.3.7",
|
|
30
|
+
"@checkstack/gitops-common": "0.4.2",
|
|
31
|
+
"@checkstack/common": "0.12.0",
|
|
32
|
+
"@checkstack/queue-api": "0.3.6",
|
|
26
33
|
"drizzle-orm": "^0.45.0",
|
|
27
34
|
"zod": "^4.2.1",
|
|
28
35
|
"@orpc/server": "^1.13.2"
|
|
29
36
|
},
|
|
30
37
|
"devDependencies": {
|
|
31
38
|
"@checkstack/drizzle-helper": "0.0.5",
|
|
32
|
-
"@checkstack/scripts": "0.3.
|
|
33
|
-
"@checkstack/test-utils-backend": "0.1.
|
|
39
|
+
"@checkstack/scripts": "0.3.4",
|
|
40
|
+
"@checkstack/test-utils-backend": "0.1.31",
|
|
34
41
|
"@checkstack/tsconfig": "0.0.7",
|
|
35
42
|
"@types/bun": "^1.0.0",
|
|
43
|
+
"@types/pg": "^8.20.0",
|
|
36
44
|
"drizzle-kit": "^0.31.10",
|
|
45
|
+
"pg": "^8.21.0",
|
|
37
46
|
"typescript": "^5.0.0"
|
|
38
47
|
}
|
|
39
48
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Satellite connection triggers registered with the Automation Platform.
|
|
3
|
+
*
|
|
4
|
+
* These three triggers are ENTITY-DRIVEN (reactive automation engine §10.6):
|
|
5
|
+
* the `satellite-connection` entity's change deriver fires
|
|
6
|
+
* `satellite.connected` / `.disconnected` / `.heartbeat_lost` via Stage-1
|
|
7
|
+
* routing, so they no longer subscribe to a hook. A no-op `setup`
|
|
8
|
+
* (`makeEntityDrivenTriggerSetup`) keeps them in the editor's trigger catalog
|
|
9
|
+
* (and payload-introspectable) without re-introducing a hook — mirroring how
|
|
10
|
+
* the incident / catalog / dependency / healthcheck domains kept their
|
|
11
|
+
* registrations after migrating. The runtime `trigger.payload` matches these
|
|
12
|
+
* schemas via the `satelliteChangeToPayload` mapper registered alongside the
|
|
13
|
+
* deriver.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import type { TriggerDefinition } from "@checkstack/automation-backend";
|
|
17
|
+
import { makeEntityDrivenTriggerSetup } from "@checkstack/automation-backend";
|
|
18
|
+
|
|
19
|
+
// ─── Payload schemas ───────────────────────────────────────────────────
|
|
20
|
+
//
|
|
21
|
+
// The reactive `satellite-connection` entity state is `{ status, name, region,
|
|
22
|
+
// lastSeenAt, lastEvent }`. The entity-driven `trigger.payload` (see
|
|
23
|
+
// `satelliteChangeToPayload`) carries `satelliteId` (the entity id) + `name` /
|
|
24
|
+
// `region` / `status` / `lastSeenAt`. `lastSeenAt` is nullable (`null` after a
|
|
25
|
+
// clean disconnect / before the satellite was ever seen).
|
|
26
|
+
const satelliteConnectedPayloadSchema = z.object({
|
|
27
|
+
satelliteId: z.string(),
|
|
28
|
+
name: z.string(),
|
|
29
|
+
region: z.string(),
|
|
30
|
+
status: z.enum(["online", "offline"]),
|
|
31
|
+
lastSeenAt: z.string().nullable(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const satelliteDisconnectedPayloadSchema = z.object({
|
|
35
|
+
satelliteId: z.string(),
|
|
36
|
+
name: z.string(),
|
|
37
|
+
region: z.string(),
|
|
38
|
+
status: z.enum(["online", "offline"]),
|
|
39
|
+
lastSeenAt: z.string().nullable(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const satelliteHeartbeatLostPayloadSchema = z.object({
|
|
43
|
+
satelliteId: z.string(),
|
|
44
|
+
name: z.string(),
|
|
45
|
+
region: z.string(),
|
|
46
|
+
status: z.enum(["online", "offline"]),
|
|
47
|
+
lastSeenAt: z.string().nullable(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ─── Triggers ──────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export const satelliteConnectedTrigger: TriggerDefinition<
|
|
53
|
+
z.infer<typeof satelliteConnectedPayloadSchema>
|
|
54
|
+
> = {
|
|
55
|
+
id: "connected",
|
|
56
|
+
displayName: "Satellite Connected",
|
|
57
|
+
description: "Fires when a satellite establishes a connection",
|
|
58
|
+
category: "Satellites",
|
|
59
|
+
icon: "SatelliteDish",
|
|
60
|
+
payloadSchema: satelliteConnectedPayloadSchema,
|
|
61
|
+
setup: makeEntityDrivenTriggerSetup<
|
|
62
|
+
z.infer<typeof satelliteConnectedPayloadSchema>
|
|
63
|
+
>(),
|
|
64
|
+
contextKey: (p) => p.satelliteId,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const satelliteDisconnectedTrigger: TriggerDefinition<
|
|
68
|
+
z.infer<typeof satelliteDisconnectedPayloadSchema>
|
|
69
|
+
> = {
|
|
70
|
+
id: "disconnected",
|
|
71
|
+
displayName: "Satellite Disconnected",
|
|
72
|
+
description: "Fires when a satellite's connection is closed",
|
|
73
|
+
category: "Satellites",
|
|
74
|
+
icon: "SatelliteDish",
|
|
75
|
+
payloadSchema: satelliteDisconnectedPayloadSchema,
|
|
76
|
+
setup: makeEntityDrivenTriggerSetup<
|
|
77
|
+
z.infer<typeof satelliteDisconnectedPayloadSchema>
|
|
78
|
+
>(),
|
|
79
|
+
contextKey: (p) => p.satelliteId,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const satelliteHeartbeatLostTrigger: TriggerDefinition<
|
|
83
|
+
z.infer<typeof satelliteHeartbeatLostPayloadSchema>
|
|
84
|
+
> = {
|
|
85
|
+
id: "heartbeat_lost",
|
|
86
|
+
displayName: "Satellite Heartbeat Lost",
|
|
87
|
+
description:
|
|
88
|
+
"Fires when a connected satellite stops sending heartbeats and ages out to offline",
|
|
89
|
+
category: "Satellites",
|
|
90
|
+
icon: "SatelliteDish",
|
|
91
|
+
payloadSchema: satelliteHeartbeatLostPayloadSchema,
|
|
92
|
+
setup: makeEntityDrivenTriggerSetup<
|
|
93
|
+
z.infer<typeof satelliteHeartbeatLostPayloadSchema>
|
|
94
|
+
>(),
|
|
95
|
+
contextKey: (p) => p.satelliteId,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* All satellite connection triggers as a heterogeneous list. Typed as
|
|
100
|
+
* `TriggerDefinition<unknown>[]` so the array can be iterated in the plugin
|
|
101
|
+
* entry without TypeScript collapsing the union to a single payload shape.
|
|
102
|
+
*/
|
|
103
|
+
export const satelliteTriggers: TriggerDefinition<unknown>[] = [
|
|
104
|
+
satelliteConnectedTrigger as TriggerDefinition<unknown>,
|
|
105
|
+
satelliteDisconnectedTrigger as TriggerDefinition<unknown>,
|
|
106
|
+
satelliteHeartbeatLostTrigger as TriggerDefinition<unknown>,
|
|
107
|
+
];
|