@adhdev/mesh-shared 1.0.60-rc.9 → 1.0.61-rc.1
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/dist/brain-routing.d.ts +6 -8
- package/dist/git-diff.d.ts +21 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +2876 -77
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2625 -72
- package/dist/index.mjs.map +1 -1
- package/dist/interpolation.d.ts +0 -9
- package/dist/json.d.ts +4 -0
- package/dist/keyed-doc-delta.d.ts +105 -0
- package/dist/mesh-tool-names.d.ts +31 -2
- package/dist/mesh-vocabulary.d.ts +66 -0
- package/dist/mission-brief.d.ts +62 -0
- package/dist/outbound-message.d.ts +201 -0
- package/dist/path-ownership.d.ts +101 -0
- package/dist/protocol/daemon-server.d.ts +356 -0
- package/dist/protocol/dashboard-daemon-p2p.d.ts +213 -0
- package/dist/protocol/dashboard-server.d.ts +240 -0
- package/dist/protocol/dashboard-wire-version.d.ts +21 -0
- package/dist/protocol/envelope.d.ts +55 -0
- package/dist/protocol/index.d.ts +16 -0
- package/dist/session-launch.d.ts +105 -0
- package/dist/session-status.d.ts +75 -0
- package/dist/slot-proposal.d.ts +2 -28
- package/dist/turn-evidence.d.ts +434 -0
- package/dist/turn-ipc-guards.d.ts +18 -0
- package/dist/turn-ipc-mission.d.ts +250 -0
- package/dist/turn-ipc-queue.d.ts +196 -0
- package/dist/turn-ipc-stats.d.ts +72 -0
- package/dist/turn-ipc-turn.d.ts +176 -0
- package/dist/turn-ipc.d.ts +90 -0
- package/dist/types.d.ts +11 -0
- package/dist/worker-protocol.d.ts +69 -0
- package/dist/ws-protocol.d.ts +69 -41
- package/package.json +1 -1
- package/src/brain-routing.ts +9 -11
- package/src/git-diff.ts +48 -0
- package/src/index.ts +12 -1
- package/src/interpolation.ts +0 -18
- package/src/json.ts +10 -0
- package/src/keyed-doc-delta.ts +413 -0
- package/src/mesh-tool-names.ts +66 -25
- package/src/mesh-vocabulary.ts +94 -0
- package/src/mission-brief.ts +153 -0
- package/src/outbound-message.ts +319 -0
- package/src/path-ownership.ts +271 -0
- package/src/protocol/daemon-server.ts +510 -0
- package/src/protocol/dashboard-daemon-p2p.ts +273 -0
- package/src/protocol/dashboard-server.ts +340 -0
- package/src/protocol/dashboard-wire-version.ts +27 -0
- package/src/protocol/envelope.ts +75 -0
- package/src/protocol/index.ts +17 -0
- package/src/session-launch.ts +245 -0
- package/src/session-status.ts +164 -0
- package/src/slot-proposal.ts +2 -48
- package/src/turn-evidence.ts +596 -0
- package/src/turn-ipc-guards.ts +61 -0
- package/src/turn-ipc-mission.ts +647 -0
- package/src/turn-ipc-queue.ts +493 -0
- package/src/turn-ipc-stats.ts +193 -0
- package/src/turn-ipc-turn.ts +453 -0
- package/src/turn-ipc.ts +137 -0
- package/src/types.ts +8 -0
- package/src/worker-protocol.ts +164 -0
- package/src/ws-protocol.ts +98 -93
- package/dist/magi.d.ts +0 -281
- package/src/magi.ts +0 -310
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* protocol/daemon-server — the daemon ↔ Workers-server WebSocket link.
|
|
3
|
+
*
|
|
4
|
+
* Wiring-unification Phase A2 (docs/design/2026-09-23-wiring-unification.md §3 A2).
|
|
5
|
+
*
|
|
6
|
+
* Before this file the link shared NAME lists only (`ws-protocol.ts`), payloads
|
|
7
|
+
* were `any` on both ends, `daemon_mesh_command` was sent by a raw `ws.send`
|
|
8
|
+
* outside the typed funnel, and the P2P signaling names in the shared list
|
|
9
|
+
* (`offer|answer|ice`) had drifted from the wire (`p2p_offer|p2p_answer|p2p_ice`)
|
|
10
|
+
* without anything noticing, because nothing imported them.
|
|
11
|
+
*
|
|
12
|
+
* Two directions:
|
|
13
|
+
* - `DaemonToServerMsg` — every frame the daemon writes to the server WS.
|
|
14
|
+
* - `ServerToDaemonMsg` — every frame the server writes to a daemon WS. This
|
|
15
|
+
* is a CLOSED control set plus one OPEN namespace: the public REST API
|
|
16
|
+
* (`POST /api/v1/daemons/:id/command`, `daemonCommandSchema` = any string)
|
|
17
|
+
* relays a caller-chosen command name as the frame `type`, so that member is
|
|
18
|
+
* typed with a branded string rather than a literal list. The brand is what
|
|
19
|
+
* keeps `msg.type === 'auth_ok'` narrowing to the control member instead of
|
|
20
|
+
* collapsing into "some string".
|
|
21
|
+
*
|
|
22
|
+
* Payloads are structural mirrors of the daemon-core types the two ends assign
|
|
23
|
+
* into them (see ./envelope.ts for why mesh-shared cannot import daemon-core).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { AuthOkPlanLimits } from '../ws-protocol'
|
|
27
|
+
import type { SessionStatus } from '../session-status'
|
|
28
|
+
import {
|
|
29
|
+
type CommandPayload,
|
|
30
|
+
type Envelope,
|
|
31
|
+
type MessageId,
|
|
32
|
+
type AssertSameMembers,
|
|
33
|
+
isRecord,
|
|
34
|
+
isNonEmptyString,
|
|
35
|
+
makeTypeGuard,
|
|
36
|
+
readEnvelopeFields,
|
|
37
|
+
} from './envelope'
|
|
38
|
+
|
|
39
|
+
// ─── Status events (mirror of daemon-core `DaemonStatusEventName`) ─────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Daemon-originated status events. The daemon relays these to the server
|
|
43
|
+
* (push notifications, webhooks) AND over P2P (dashboard). Mirror of
|
|
44
|
+
* daemon-core `DaemonStatusEventName`; the server pins the two equal at compile
|
|
45
|
+
* time (UserSession.ts), so adding a member here without adding it there — or
|
|
46
|
+
* vice versa — fails `tsc`.
|
|
47
|
+
*/
|
|
48
|
+
export const DAEMON_STATUS_EVENT_NAMES = [
|
|
49
|
+
'agent:generating_started',
|
|
50
|
+
'agent:waiting_approval',
|
|
51
|
+
'agent:waiting_choice',
|
|
52
|
+
'agent:generating_completed',
|
|
53
|
+
'agent:stopped',
|
|
54
|
+
'monitor:no_progress',
|
|
55
|
+
] as const
|
|
56
|
+
export type DaemonStatusEventName = typeof DAEMON_STATUS_EVENT_NAMES[number]
|
|
57
|
+
export const isDaemonStatusEventName = makeTypeGuard(DAEMON_STATUS_EVENT_NAMES)
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Server-bound status event. Content boundary: identifiers, enums, counters
|
|
61
|
+
* and booleans only — plus the approved approval-modal exception
|
|
62
|
+
* (`modalMessage`/`modalButtons`, CLAUDE.md "Push notification exception").
|
|
63
|
+
* Mirror of daemon-core `DaemonStatusEventPayload`.
|
|
64
|
+
*/
|
|
65
|
+
export type DaemonStatusEventWirePayload = {
|
|
66
|
+
event: DaemonStatusEventName
|
|
67
|
+
timestamp: number
|
|
68
|
+
targetSessionId?: string
|
|
69
|
+
providerType?: string
|
|
70
|
+
providerSessionId?: string
|
|
71
|
+
workspaceName?: string
|
|
72
|
+
duration?: number
|
|
73
|
+
elapsedSec?: number
|
|
74
|
+
modalMessage?: string
|
|
75
|
+
modalButtons?: string[]
|
|
76
|
+
surfaceHidden?: boolean
|
|
77
|
+
muted?: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Status report (mirror of daemon-core `CloudStatusReportPayload`) ──────
|
|
81
|
+
|
|
82
|
+
/** Routing-only session entry the daemon reports over the server WS. Mirror of daemon-core `RoutingSessionEntry`. */
|
|
83
|
+
export type RoutingSessionWireEntry = {
|
|
84
|
+
id: string
|
|
85
|
+
parentId: string | null
|
|
86
|
+
providerType: string
|
|
87
|
+
providerName: string
|
|
88
|
+
kind: string
|
|
89
|
+
transport: string
|
|
90
|
+
status: SessionStatus
|
|
91
|
+
workspace: string | null
|
|
92
|
+
cdpConnected?: boolean
|
|
93
|
+
surfaceHidden?: boolean
|
|
94
|
+
muted?: boolean
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Counters and enums only — never a peer id or address. Mirror of daemon-core `P2PStatusSummary`. */
|
|
98
|
+
export type P2PStatusWireSummary = {
|
|
99
|
+
available: boolean
|
|
100
|
+
state: string
|
|
101
|
+
peers: number
|
|
102
|
+
screenshotActive?: boolean
|
|
103
|
+
direct?: number
|
|
104
|
+
relay?: number
|
|
105
|
+
unknownTransport?: number
|
|
106
|
+
directTotal?: number
|
|
107
|
+
relayTotal?: number
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* seqscribe replication health — pre-bucketed counters and booleans only,
|
|
112
|
+
* never a topic name or peer id. Mirror of daemon-core `SeqscribeStatusSummary`
|
|
113
|
+
* (which should derive from this once shared-types is on the protocol).
|
|
114
|
+
*/
|
|
115
|
+
export type SeqscribeStatusWireSummary = {
|
|
116
|
+
topics: number
|
|
117
|
+
peers: number
|
|
118
|
+
peersReady: number
|
|
119
|
+
pendingBucket: number
|
|
120
|
+
consumerLagBucket: number
|
|
121
|
+
queueBucket: number
|
|
122
|
+
fgenAgeBucket: number
|
|
123
|
+
quarantined: boolean
|
|
124
|
+
authority: boolean
|
|
125
|
+
transcriptPublish?: boolean
|
|
126
|
+
transcriptPublishedBucket?: number
|
|
127
|
+
transcriptPublishFailedBucket?: number
|
|
128
|
+
transcriptDedupedBucket?: number
|
|
129
|
+
transcriptOversizedBucket?: number
|
|
130
|
+
transcriptDroppedBucket?: number
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type CloudStatusReportWirePayload = {
|
|
134
|
+
sessions: RoutingSessionWireEntry[]
|
|
135
|
+
p2p?: P2PStatusWireSummary
|
|
136
|
+
seqscribe?: SeqscribeStatusWireSummary
|
|
137
|
+
timestamp: number
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Release / update vocabulary (mirror of daemon-core shared-types) ──────
|
|
141
|
+
|
|
142
|
+
export const RELEASE_CHANNELS = ['stable', 'preview'] as const
|
|
143
|
+
export type ReleaseChannelWire = typeof RELEASE_CHANNELS[number]
|
|
144
|
+
export const NPM_UPDATE_TAGS = ['latest', 'next'] as const
|
|
145
|
+
export type NpmUpdateTagWire = typeof NPM_UPDATE_TAGS[number]
|
|
146
|
+
export const VERSION_UPDATE_REASONS = ['force_update_below', 'major_minor_mismatch', 'patch_mismatch', 'daemon_ahead'] as const
|
|
147
|
+
export type VersionUpdateReasonWire = typeof VERSION_UPDATE_REASONS[number]
|
|
148
|
+
|
|
149
|
+
/** Mirror of daemon-core `VersionUpdatePolicy`. */
|
|
150
|
+
export type VersionUpdatePolicyWire = {
|
|
151
|
+
channel: ReleaseChannelWire
|
|
152
|
+
npmTag: NpmUpdateTagWire
|
|
153
|
+
targetVersion: string
|
|
154
|
+
minVersion?: string
|
|
155
|
+
updateCommand: string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type StatusHeartbeatWirePayload = {
|
|
159
|
+
timestamp?: number
|
|
160
|
+
p2pPeers?: number
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─── Auth ──────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
/** What the daemon says about itself at auth time (`ServerConnectionOptions.cliInfo`). */
|
|
166
|
+
export type DaemonAuthIdentity = {
|
|
167
|
+
type: string
|
|
168
|
+
version: string
|
|
169
|
+
platform: string
|
|
170
|
+
hostname?: string
|
|
171
|
+
machineId?: string
|
|
172
|
+
/** Canonical mesh identity `daemon_<machineId>`; the server keys its DO and status by it. */
|
|
173
|
+
instanceId: string
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type DaemonAuthWirePayload = {
|
|
177
|
+
token: string
|
|
178
|
+
daemon: DaemonAuthIdentity
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type IceServerWireEntry = {
|
|
182
|
+
urls: string | string[]
|
|
183
|
+
username?: string
|
|
184
|
+
credential?: string
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export type AuthOkWirePayload = {
|
|
188
|
+
message?: string
|
|
189
|
+
plan: string
|
|
190
|
+
iceServers: IceServerWireEntry[]
|
|
191
|
+
turnenabled: boolean
|
|
192
|
+
serverVersion: string
|
|
193
|
+
releaseChannel?: ReleaseChannelWire
|
|
194
|
+
updatePolicy?: VersionUpdatePolicyWire
|
|
195
|
+
machineNickname: string | null
|
|
196
|
+
seqscribeFleetSecret?: string
|
|
197
|
+
seqscribeFleetSecretVersion?: number
|
|
198
|
+
limits: AuthOkPlanLimits
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export type AuthErrorWirePayload = {
|
|
202
|
+
reason: string
|
|
203
|
+
message: string
|
|
204
|
+
limit?: number
|
|
205
|
+
current?: number
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** `machine_evicted` / `force_disconnect` / `token_revoked` — a reason token plus human text, then the socket closes. */
|
|
209
|
+
export type DisconnectNoticeWirePayload = {
|
|
210
|
+
reason: string
|
|
211
|
+
message: string
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export type VersionMismatchWirePayload = {
|
|
215
|
+
current: string
|
|
216
|
+
latest: string
|
|
217
|
+
required: false
|
|
218
|
+
reason: VersionUpdateReasonWire
|
|
219
|
+
channel: ReleaseChannelWire
|
|
220
|
+
npmTag: NpmUpdateTagWire
|
|
221
|
+
updatePolicy: VersionUpdatePolicyWire
|
|
222
|
+
updateCommand: string
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export type ForceUpdateRequiredWirePayload = {
|
|
226
|
+
current: string
|
|
227
|
+
latest: string
|
|
228
|
+
minVersion: string
|
|
229
|
+
required: true
|
|
230
|
+
reason: VersionUpdateReasonWire
|
|
231
|
+
channel: ReleaseChannelWire
|
|
232
|
+
npmTag: NpmUpdateTagWire
|
|
233
|
+
updatePolicy: VersionUpdatePolicyWire
|
|
234
|
+
updateCommand: string
|
|
235
|
+
forceUpdateBelow?: string
|
|
236
|
+
message: string
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Commands and results ──────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
export type CommandResultWirePayload = {
|
|
242
|
+
requestId: MessageId
|
|
243
|
+
success: boolean
|
|
244
|
+
source?: string
|
|
245
|
+
} & Record<string, unknown>
|
|
246
|
+
|
|
247
|
+
export type DaemonErrorWirePayload = {
|
|
248
|
+
message?: string
|
|
249
|
+
detail?: unknown
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─── P2P signaling (dashboard ↔ daemon through the server) ─────────────────
|
|
253
|
+
|
|
254
|
+
export const P2P_SIGNAL_TYPES = ['p2p_ready', 'p2p_offer', 'p2p_answer', 'p2p_ice'] as const
|
|
255
|
+
export type P2PSignalType = typeof P2P_SIGNAL_TYPES[number]
|
|
256
|
+
export const isP2PSignalType = makeTypeGuard(P2P_SIGNAL_TYPES)
|
|
257
|
+
|
|
258
|
+
/** Signals the daemon originates (it is the WebRTC offerer). */
|
|
259
|
+
export const DAEMON_P2P_SIGNAL_TYPES = ['p2p_offer', 'p2p_ice'] as const
|
|
260
|
+
export type DaemonP2PSignalType = typeof DAEMON_P2P_SIGNAL_TYPES[number]
|
|
261
|
+
export const isDaemonP2PSignalType = makeTypeGuard(DAEMON_P2P_SIGNAL_TYPES)
|
|
262
|
+
|
|
263
|
+
export type P2POfferWirePayload = { sdp: string; type: string; peerId: string }
|
|
264
|
+
export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string }
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* A dashboard-originated signal as relayed to the daemon. `peerId` is stamped
|
|
268
|
+
* by UserSession/SharedSession, `sharePermission` and `shareTargetSessionId`
|
|
269
|
+
* (the one session a share link may see) only by SharedSession (server
|
|
270
|
+
* authority, never the client). Field names follow the browser/node-datachannel
|
|
271
|
+
* spellings both ends already read.
|
|
272
|
+
*/
|
|
273
|
+
export type RelayedP2PSignalWirePayload = {
|
|
274
|
+
peerId?: string
|
|
275
|
+
daemonId?: string
|
|
276
|
+
sharePermission?: string
|
|
277
|
+
shareTargetSessionId?: string
|
|
278
|
+
sdp?: string
|
|
279
|
+
type?: string
|
|
280
|
+
candidate?: string
|
|
281
|
+
mid?: string
|
|
282
|
+
sdpMid?: string | null
|
|
283
|
+
sdpMLineIndex?: number | null
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export const MESH_P2P_SIGNAL_TYPES = ['mesh_p2p_ready', 'mesh_p2p_offer', 'mesh_p2p_answer', 'mesh_p2p_ice'] as const
|
|
287
|
+
export type MeshP2PSignalType = typeof MESH_P2P_SIGNAL_TYPES[number]
|
|
288
|
+
export const isMeshP2PSignalType = makeTypeGuard(MESH_P2P_SIGNAL_TYPES)
|
|
289
|
+
|
|
290
|
+
/** Daemon→daemon signal as delivered to the target daemon: the sender's args plus the server-stamped canonical sender id. */
|
|
291
|
+
export type MeshP2PSignalWirePayload = { senderDaemonId: string } & Record<string, unknown>
|
|
292
|
+
|
|
293
|
+
// ─── Daemon → Server ───────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
export const DAEMON_TO_SERVER_TYPES = [
|
|
296
|
+
'auth',
|
|
297
|
+
'status_report',
|
|
298
|
+
'status_heartbeat',
|
|
299
|
+
'status_event',
|
|
300
|
+
'command_result',
|
|
301
|
+
'error',
|
|
302
|
+
'agent_event',
|
|
303
|
+
'daemon_mesh_command',
|
|
304
|
+
'p2p_offer',
|
|
305
|
+
'p2p_ice',
|
|
306
|
+
] as const
|
|
307
|
+
export type DaemonToServerType = typeof DAEMON_TO_SERVER_TYPES[number]
|
|
308
|
+
export const isDaemonToServerType = makeTypeGuard(DAEMON_TO_SERVER_TYPES)
|
|
309
|
+
|
|
310
|
+
export type DaemonAuthMsg = Envelope<'auth', DaemonAuthWirePayload>
|
|
311
|
+
export type StatusReportMsg = Envelope<'status_report', CloudStatusReportWirePayload>
|
|
312
|
+
export type StatusHeartbeatMsg = Envelope<'status_heartbeat', StatusHeartbeatWirePayload>
|
|
313
|
+
export type DaemonStatusEventMsg = Envelope<'status_event', DaemonStatusEventWirePayload>
|
|
314
|
+
export type CommandResultMsg = Envelope<'command_result', CommandResultWirePayload>
|
|
315
|
+
export type DaemonErrorMsg = Envelope<'error', DaemonErrorWirePayload>
|
|
316
|
+
/** Agent-level event forwarded verbatim to the `agent:status` webhook (throttled server-side). Dynamic by design. */
|
|
317
|
+
export type AgentEventMsg = Envelope<'agent_event', CommandPayload>
|
|
318
|
+
export type DaemonP2POfferMsg = Envelope<'p2p_offer', P2POfferWirePayload>
|
|
319
|
+
export type DaemonP2PIceMsg = Envelope<'p2p_ice', P2PIceWirePayload>
|
|
320
|
+
/**
|
|
321
|
+
* Daemon→daemon signaling relay request. Fields are TOP-LEVEL (no `payload`):
|
|
322
|
+
* this is the wire shape the live fleet speaks, kept as-is so mixed-version
|
|
323
|
+
* daemons and servers keep interoperating during a rollout.
|
|
324
|
+
*/
|
|
325
|
+
export type DaemonMeshCommandMsg = {
|
|
326
|
+
type: 'daemon_mesh_command'
|
|
327
|
+
requestId: string
|
|
328
|
+
targetDaemonId: string
|
|
329
|
+
command: MeshP2PSignalType
|
|
330
|
+
args: Record<string, unknown>
|
|
331
|
+
timestamp: number
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export type DaemonToServerMsg =
|
|
335
|
+
| DaemonAuthMsg
|
|
336
|
+
| StatusReportMsg
|
|
337
|
+
| StatusHeartbeatMsg
|
|
338
|
+
| DaemonStatusEventMsg
|
|
339
|
+
| CommandResultMsg
|
|
340
|
+
| DaemonErrorMsg
|
|
341
|
+
| AgentEventMsg
|
|
342
|
+
| DaemonMeshCommandMsg
|
|
343
|
+
| DaemonP2POfferMsg
|
|
344
|
+
| DaemonP2PIceMsg
|
|
345
|
+
|
|
346
|
+
const _daemonToServerNamesCoverUnion: AssertSameMembers<typeof DAEMON_TO_SERVER_TYPES, DaemonToServerMsg['type']> = true
|
|
347
|
+
void _daemonToServerNamesCoverUnion
|
|
348
|
+
|
|
349
|
+
/** Members that carry a `payload` object (everything except `daemon_mesh_command`). */
|
|
350
|
+
export type DaemonToServerPayloadType = Exclude<DaemonToServerMsg, DaemonMeshCommandMsg>['type']
|
|
351
|
+
export type DaemonToServerPayloadOf<T extends DaemonToServerPayloadType> = Extract<DaemonToServerMsg, { type: T }>['payload']
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Parse boundary for the server: is `raw` a frame this link accepts from a daemon?
|
|
355
|
+
* Checks membership and the minimal envelope shape; `auth` additionally needs a
|
|
356
|
+
* token string because nothing downstream can proceed without one.
|
|
357
|
+
*/
|
|
358
|
+
export function decodeDaemonToServer(raw: unknown): DaemonToServerMsg | null {
|
|
359
|
+
if (!isRecord(raw) || !isDaemonToServerType(raw.type)) return null
|
|
360
|
+
if (raw.type === 'daemon_mesh_command') {
|
|
361
|
+
// Handler-side validation replies with a correlated `daemon_mesh_result`
|
|
362
|
+
// error, so only the discriminant is checked here.
|
|
363
|
+
return {
|
|
364
|
+
type: 'daemon_mesh_command',
|
|
365
|
+
requestId: typeof raw.requestId === 'string' ? raw.requestId : String(raw.requestId ?? ''),
|
|
366
|
+
targetDaemonId: typeof raw.targetDaemonId === 'string' ? raw.targetDaemonId : '',
|
|
367
|
+
command: raw.command as MeshP2PSignalType,
|
|
368
|
+
args: isRecord(raw.args) ? raw.args : {},
|
|
369
|
+
timestamp: typeof raw.timestamp === 'number' ? raw.timestamp : Date.now(),
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (!isRecord(raw.payload)) return null
|
|
373
|
+
if (raw.type === 'auth' && !isNonEmptyString(raw.payload.token)) return null
|
|
374
|
+
return { type: raw.type, payload: raw.payload, ...readEnvelopeFields(raw) } as DaemonToServerMsg
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ─── Server → Daemon ───────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
export const SERVER_TO_DAEMON_CONTROL_TYPES = [
|
|
380
|
+
'auth_ok',
|
|
381
|
+
'auth_error',
|
|
382
|
+
'machine_evicted',
|
|
383
|
+
'force_disconnect',
|
|
384
|
+
'token_revoked',
|
|
385
|
+
'version_mismatch',
|
|
386
|
+
'force_update_required',
|
|
387
|
+
'command',
|
|
388
|
+
'agent_command',
|
|
389
|
+
'resolve_action',
|
|
390
|
+
'daemon_mesh_result',
|
|
391
|
+
'p2p_ready',
|
|
392
|
+
'p2p_offer',
|
|
393
|
+
'p2p_answer',
|
|
394
|
+
'p2p_ice',
|
|
395
|
+
'mesh_p2p_ready',
|
|
396
|
+
'mesh_p2p_offer',
|
|
397
|
+
'mesh_p2p_answer',
|
|
398
|
+
'mesh_p2p_ice',
|
|
399
|
+
] as const
|
|
400
|
+
export type ServerToDaemonControlType = typeof SERVER_TO_DAEMON_CONTROL_TYPES[number]
|
|
401
|
+
export const isServerToDaemonControlType = makeTypeGuard(SERVER_TO_DAEMON_CONTROL_TYPES)
|
|
402
|
+
|
|
403
|
+
/** Optional routing fields the server stamps on relayed commands. */
|
|
404
|
+
type ServerRelayFields = { id?: MessageId; source?: string; timestamp?: number }
|
|
405
|
+
|
|
406
|
+
export type AuthOkMsg = Envelope<'auth_ok', AuthOkWirePayload>
|
|
407
|
+
export type AuthErrorMsg = Envelope<'auth_error', AuthErrorWirePayload>
|
|
408
|
+
export type MachineEvictedMsg = Envelope<'machine_evicted', DisconnectNoticeWirePayload>
|
|
409
|
+
export type ForceDisconnectMsg = Envelope<'force_disconnect', DisconnectNoticeWirePayload>
|
|
410
|
+
export type TokenRevokedMsg = Envelope<'token_revoked', DisconnectNoticeWirePayload>
|
|
411
|
+
export type VersionMismatchMsg = Envelope<'version_mismatch', VersionMismatchWirePayload>
|
|
412
|
+
export type ForceUpdateRequiredMsg = Envelope<'force_update_required', ForceUpdateRequiredWirePayload>
|
|
413
|
+
/** Wrapped command: `payload.command` names the command, `payload.args` carries it. */
|
|
414
|
+
export type ServerCommandMsg = Envelope<'command', { command: string; args?: CommandPayload } & Record<string, unknown>> & ServerRelayFields
|
|
415
|
+
export type ServerAgentCommandMsg = Envelope<'agent_command', { action: string } & Record<string, unknown>> & ServerRelayFields
|
|
416
|
+
export type ServerResolveActionMsg = Envelope<'resolve_action', CommandPayload> & ServerRelayFields
|
|
417
|
+
export type ServerP2PSignalMsg = Envelope<P2PSignalType, RelayedP2PSignalWirePayload> & ServerRelayFields
|
|
418
|
+
export type ServerMeshP2PSignalMsg = Envelope<MeshP2PSignalType, MeshP2PSignalWirePayload> & ServerRelayFields
|
|
419
|
+
/** Correlated ack/nack of a `daemon_mesh_command` (top-level, no payload). */
|
|
420
|
+
export type DaemonMeshResultMsg = {
|
|
421
|
+
type: 'daemon_mesh_result'
|
|
422
|
+
requestId: string
|
|
423
|
+
success: boolean
|
|
424
|
+
result?: unknown
|
|
425
|
+
error?: string
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export type ServerToDaemonControlMsg =
|
|
429
|
+
| AuthOkMsg
|
|
430
|
+
| AuthErrorMsg
|
|
431
|
+
| MachineEvictedMsg
|
|
432
|
+
| ForceDisconnectMsg
|
|
433
|
+
| TokenRevokedMsg
|
|
434
|
+
| VersionMismatchMsg
|
|
435
|
+
| ForceUpdateRequiredMsg
|
|
436
|
+
| ServerCommandMsg
|
|
437
|
+
| ServerAgentCommandMsg
|
|
438
|
+
| ServerResolveActionMsg
|
|
439
|
+
| DaemonMeshResultMsg
|
|
440
|
+
| ServerP2PSignalMsg
|
|
441
|
+
| ServerMeshP2PSignalMsg
|
|
442
|
+
|
|
443
|
+
const _serverToDaemonControlNamesCoverUnion: AssertSameMembers<typeof SERVER_TO_DAEMON_CONTROL_TYPES, ServerToDaemonControlMsg['type']> = true
|
|
444
|
+
void _serverToDaemonControlNamesCoverUnion
|
|
445
|
+
|
|
446
|
+
declare const directCommandBrand: unique symbol
|
|
447
|
+
/**
|
|
448
|
+
* A command name relayed as the frame `type` — the OPEN namespace the REST API
|
|
449
|
+
* and `shortcuts` routes feed (`read_chat`, `send_chat`, `stop_cli`, `git_*`,
|
|
450
|
+
* `clone_mesh_node`, `terminal_exec`, ...). The daemon routes it by name
|
|
451
|
+
* through its command router. Branded so it cannot be confused with a control
|
|
452
|
+
* member at the type level; `decodeServerToDaemon` is the only producer.
|
|
453
|
+
*/
|
|
454
|
+
export type DaemonDirectCommandType = string & { readonly [directCommandBrand]?: true }
|
|
455
|
+
|
|
456
|
+
export type ServerDirectCommandMsg = {
|
|
457
|
+
type: DaemonDirectCommandType
|
|
458
|
+
payload: CommandPayload
|
|
459
|
+
id?: MessageId
|
|
460
|
+
source?: string
|
|
461
|
+
timestamp?: number
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export type ServerToDaemonMsg = ServerToDaemonControlMsg | ServerDirectCommandMsg
|
|
465
|
+
export type ServerToDaemonType = ServerToDaemonMsg['type']
|
|
466
|
+
|
|
467
|
+
export function isServerDirectCommandMsg(msg: ServerToDaemonMsg): msg is ServerDirectCommandMsg {
|
|
468
|
+
return !isServerToDaemonControlType(msg.type)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Members of the control set that carry a `payload` object. */
|
|
472
|
+
export type ServerToDaemonPayloadType = Exclude<ServerToDaemonControlMsg, DaemonMeshResultMsg>['type']
|
|
473
|
+
export type ServerToDaemonPayloadOf<T extends ServerToDaemonPayloadType> = Extract<ServerToDaemonControlMsg, { type: T }>['payload']
|
|
474
|
+
|
|
475
|
+
/** Command types a daemon must never receive as a direct command: they are daemon→server frames or reserved envelope words. */
|
|
476
|
+
const DIRECT_COMMAND_TYPE_PATTERN = /^[a-z][a-z0-9_:.-]{0,63}$/
|
|
477
|
+
|
|
478
|
+
export function isDaemonDirectCommandType(value: unknown): value is DaemonDirectCommandType {
|
|
479
|
+
return typeof value === 'string' && DIRECT_COMMAND_TYPE_PATTERN.test(value) && !isServerToDaemonControlType(value)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Parse boundary for the daemon: is `raw` a frame this link accepts from the server?
|
|
484
|
+
* Control members are checked for their discriminant and envelope shape; the
|
|
485
|
+
* payload-less `daemon_mesh_result` reply needs its `requestId`; any other well-formed
|
|
486
|
+
* command name becomes a `ServerDirectCommandMsg`.
|
|
487
|
+
*/
|
|
488
|
+
export function decodeServerToDaemon(raw: unknown): ServerToDaemonMsg | null {
|
|
489
|
+
if (!isRecord(raw) || typeof raw.type !== 'string') return null
|
|
490
|
+
const relay = {
|
|
491
|
+
...readEnvelopeFields(raw),
|
|
492
|
+
...(isNonEmptyString(raw.source) ? { source: raw.source } : {}),
|
|
493
|
+
}
|
|
494
|
+
if (raw.type === 'daemon_mesh_result') {
|
|
495
|
+
if (!isNonEmptyString(raw.requestId)) return null
|
|
496
|
+
const base = { requestId: raw.requestId, success: raw.success === true }
|
|
497
|
+
return {
|
|
498
|
+
type: 'daemon_mesh_result',
|
|
499
|
+
...base,
|
|
500
|
+
...('result' in raw ? { result: raw.result } : {}),
|
|
501
|
+
...(typeof raw.error === 'string' ? { error: raw.error } : {}),
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (!isRecord(raw.payload)) return null
|
|
505
|
+
if (isServerToDaemonControlType(raw.type)) {
|
|
506
|
+
return { type: raw.type, payload: raw.payload, ...relay } as ServerToDaemonControlMsg
|
|
507
|
+
}
|
|
508
|
+
if (!isDaemonDirectCommandType(raw.type)) return null
|
|
509
|
+
return { type: raw.type, payload: raw.payload, ...relay }
|
|
510
|
+
}
|