@econ-v1/rpc 6.7.0 → 6.11.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.
Files changed (79) hide show
  1. package/dist/client-device-pending-contract.d.ts +14 -0
  2. package/dist/client-device-pending-contract.d.ts.map +1 -0
  3. package/dist/client.d.ts +15 -13
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/device-boot-contract.d.ts +7 -0
  6. package/dist/device-boot-contract.d.ts.map +1 -0
  7. package/dist/device-cleanup-contract.d.ts +17 -0
  8. package/dist/device-cleanup-contract.d.ts.map +1 -0
  9. package/dist/dialog-contract.d.ts +26 -0
  10. package/dist/dialog-contract.d.ts.map +1 -0
  11. package/dist/errors.d.ts +33 -6
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/index.d.ts +144 -14
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/message-port-client.d.ts +134 -16
  16. package/dist/message-port-client.d.ts.map +1 -1
  17. package/dist/node-app-memory-contract.d.ts +10 -0
  18. package/dist/node-app-memory-contract.d.ts.map +1 -0
  19. package/dist/onboarding/discovery-contract.d.ts +61 -34
  20. package/dist/onboarding/discovery-contract.d.ts.map +1 -1
  21. package/dist/onboarding/onboarding-contract.d.ts +119 -27
  22. package/dist/onboarding/onboarding-contract.d.ts.map +1 -1
  23. package/dist/onboarding/owner-claim-contract.d.ts +50 -0
  24. package/dist/onboarding/owner-claim-contract.d.ts.map +1 -0
  25. package/dist/pairing-status-contract.d.ts +40 -0
  26. package/dist/pairing-status-contract.d.ts.map +1 -0
  27. package/dist/protocol.d.ts +50 -12
  28. package/dist/protocol.d.ts.map +1 -1
  29. package/dist/stage-context.d.ts +31 -16
  30. package/dist/stage-context.d.ts.map +1 -1
  31. package/dist/sync-cursors-contract.d.ts +6 -0
  32. package/dist/sync-cursors-contract.d.ts.map +1 -0
  33. package/dist/system-notice-contract.d.ts +70 -0
  34. package/dist/system-notice-contract.d.ts.map +1 -0
  35. package/dist/transport-contract.d.ts +11 -0
  36. package/dist/transport-contract.d.ts.map +1 -0
  37. package/dist/transport.d.ts +2 -2
  38. package/dist/transport.d.ts.map +1 -1
  39. package/dist/trust-contract.d.ts +23 -0
  40. package/dist/trust-contract.d.ts.map +1 -0
  41. package/package.json +16 -12
  42. package/src/client-device-pending-contract.js +6 -0
  43. package/src/client.js +26 -0
  44. package/src/device-boot-contract.js +4 -0
  45. package/src/device-cleanup-contract.js +9 -0
  46. package/src/dialog-contract.js +6 -0
  47. package/src/errors.js +111 -0
  48. package/src/index.js +194 -0
  49. package/src/message-port-client.js +628 -0
  50. package/src/node-app-memory-contract.js +10 -0
  51. package/src/onboarding/discovery-contract.js +83 -0
  52. package/src/onboarding/onboarding-contract.js +106 -0
  53. package/src/onboarding/owner-claim-contract.js +59 -0
  54. package/src/pairing-status-contract.js +36 -0
  55. package/src/protocol.js +758 -0
  56. package/src/stage-context.js +27 -0
  57. package/src/sync-cursors-contract.js +3 -0
  58. package/src/system-notice-contract.js +64 -0
  59. package/src/transport-contract.js +11 -0
  60. package/src/transport.js +6 -0
  61. package/src/trust-contract.js +36 -0
  62. package/dist/client.js +0 -2
  63. package/dist/client.js.map +0 -1
  64. package/dist/errors.js +0 -57
  65. package/dist/errors.js.map +0 -1
  66. package/dist/index.js +0 -7
  67. package/dist/index.js.map +0 -1
  68. package/dist/message-port-client.js +0 -365
  69. package/dist/message-port-client.js.map +0 -1
  70. package/dist/onboarding/discovery-contract.js +0 -42
  71. package/dist/onboarding/discovery-contract.js.map +0 -1
  72. package/dist/onboarding/onboarding-contract.js +0 -37
  73. package/dist/onboarding/onboarding-contract.js.map +0 -1
  74. package/dist/protocol.js +0 -295
  75. package/dist/protocol.js.map +0 -1
  76. package/dist/stage-context.js +0 -2
  77. package/dist/stage-context.js.map +0 -1
  78. package/dist/transport.js +0 -2
  79. package/dist/transport.js.map +0 -1
@@ -0,0 +1,758 @@
1
+ // @ts-check
2
+
3
+ /** @import { AppEventPayload, AppEventType, ClientError, CoreKernelEventType, DomainEvent, JsonObject, JsonValue, KernelEventPayload, QueryDiff, QueryName } from '@econ-v1/domain' */
4
+ /** @typedef {{ readonly occurredAt: string; readonly payload: { readonly state?: string }; readonly type: "device-cleanup.changed" }} DeviceCleanupDomainEvent */
5
+ /** @typedef {DomainEvent | DeviceCleanupDomainEvent} ProtocolDomainEvent */
6
+ /** @typedef {CoreKernelEventType | "device-cleanup.changed"} ProtocolCoreKernelEventType */
7
+ import { appEventInstance, appEventScope, isCoreKernelEventType, isKernelEventType, isQueryName, } from "@econ-v1/domain";
8
+ import { deserializeClientError, isJsonObject, isSerializableClientError, serializeClientError, } from "./errors.js";
9
+ /**
10
+ * @param {RpcRequest} request
11
+ * @returns {string}
12
+ */
13
+ export function serializeRpcRequest(request) {
14
+ switch (request.type) {
15
+ case "command":
16
+ return stringify({ id: request.id, name: request.name, payload: request.payload, type: request.type });
17
+ case "query":
18
+ return stringify({ id: request.id, name: request.name, params: request.params, type: request.type });
19
+ case "live-query.subscribe":
20
+ return stringify({ id: request.id, name: request.name, params: request.params, subscriptionId: request.subscriptionId, type: request.type });
21
+ case "live-query.unsubscribe":
22
+ case "event.unsubscribe":
23
+ return stringify({ id: request.id, subscriptionId: request.subscriptionId, type: request.type });
24
+ case "event.subscribe":
25
+ return stringify({ eventType: request.eventType, id: request.id, subscriptionId: request.subscriptionId, type: request.type });
26
+ case "event.publish":
27
+ return stringify({ event: projectDomainEvent(request.event), id: request.id, type: request.type });
28
+ case "stage.prepare":
29
+ return stringify({ appName: request.appName, id: request.id, type: request.type });
30
+ case "stage.open-session":
31
+ return stringify({ appName: request.appName, id: request.id, manifestSha256: request.manifestSha256, type: request.type });
32
+ case "stage.query":
33
+ return stringify({ id: request.id, name: request.name, params: request.params, sessionId: request.sessionId, type: request.type });
34
+ case "stage.live-query.subscribe":
35
+ return stringify({ id: request.id, name: request.name, params: request.params, sessionId: request.sessionId, subscriptionId: request.subscriptionId, type: request.type });
36
+ case "stage.live-query.unsubscribe":
37
+ case "stage.event.unsubscribe":
38
+ return stringify({ id: request.id, sessionId: request.sessionId, subscriptionId: request.subscriptionId, type: request.type });
39
+ case "stage.event.subscribe":
40
+ return stringify({ eventType: request.eventType, id: request.id, sessionId: request.sessionId, subscriptionId: request.subscriptionId, type: request.type });
41
+ case "stage.event.publish":
42
+ return stringify({ event: projectDomainEvent(request.event), id: request.id, sessionId: request.sessionId, type: request.type });
43
+ case "stage.capability.invoke":
44
+ return stringify({ capability: request.capability, id: request.id, payload: request.payload, sessionId: request.sessionId, type: request.type });
45
+ case "stage.close-session":
46
+ return stringify({ id: request.id, sessionId: request.sessionId, type: request.type });
47
+ }
48
+ }
49
+ /**
50
+ * @param {string} serialized
51
+ * @returns {RpcRequest}
52
+ */
53
+ export function deserializeRpcRequest(serialized) {
54
+ const value = parseRecord(serialized);
55
+ const type = stringField(value, "type");
56
+ const id = stringField(value, "id");
57
+ switch (type) {
58
+ case "command": return { id, name: versionedNameField(value, "name"), payload: jsonField(value, "payload"), type };
59
+ case "query": return { id, name: queryNameField(value), params: jsonField(value, "params"), type };
60
+ case "live-query.subscribe": return { id, name: queryNameField(value), params: jsonField(value, "params"), subscriptionId: stringField(value, "subscriptionId"), type };
61
+ case "live-query.unsubscribe": return { id, subscriptionId: stringField(value, "subscriptionId"), type };
62
+ case "event.subscribe": return { eventType: eventTypeField(value), id, subscriptionId: stringField(value, "subscriptionId"), type };
63
+ case "event.unsubscribe": return { id, subscriptionId: stringField(value, "subscriptionId"), type };
64
+ case "event.publish": return { event: domainEventField(value), id, type };
65
+ case "stage.prepare": return { appName: stringField(value, "appName"), id, type };
66
+ case "stage.open-session": return { appName: stringField(value, "appName"), id, manifestSha256: stringField(value, "manifestSha256"), type };
67
+ case "stage.query": return { id, name: queryNameField(value), params: jsonField(value, "params"), sessionId: stringField(value, "sessionId"), type };
68
+ case "stage.live-query.subscribe": return { id, name: queryNameField(value), params: jsonField(value, "params"), sessionId: stringField(value, "sessionId"), subscriptionId: stringField(value, "subscriptionId"), type };
69
+ case "stage.live-query.unsubscribe": return { id, sessionId: stringField(value, "sessionId"), subscriptionId: stringField(value, "subscriptionId"), type };
70
+ case "stage.event.subscribe": return { eventType: eventTypeField(value), id, sessionId: stringField(value, "sessionId"), subscriptionId: stringField(value, "subscriptionId"), type };
71
+ case "stage.event.unsubscribe": return { id, sessionId: stringField(value, "sessionId"), subscriptionId: stringField(value, "subscriptionId"), type };
72
+ case "stage.event.publish": return { event: domainEventField(value), id, sessionId: stringField(value, "sessionId"), type };
73
+ case "stage.capability.invoke": return { capability: stringField(value, "capability"), id, payload: jsonField(value, "payload"), sessionId: stringField(value, "sessionId"), type };
74
+ case "stage.close-session": return { id, sessionId: stringField(value, "sessionId"), type };
75
+ default: throw new TypeError("Unknown RPC request type");
76
+ }
77
+ }
78
+ /**
79
+ * @param {JsonObject} value
80
+ * @param {string} field
81
+ * @returns {string}
82
+ */
83
+ function versionedNameField(value, field) {
84
+ const name = stringField(value, field);
85
+ if (!/^[-a-z0-9.]+\.v[1-9][0-9]*$/u.test(name))
86
+ throw new TypeError(`Expected ${field} to be versioned`);
87
+ return name;
88
+ }
89
+ /**
90
+ * @param {RpcResponse} response
91
+ * @returns {string}
92
+ */
93
+ export function serializeRpcResponse(response) {
94
+ return response.ok
95
+ ? stringify({ id: response.id, ok: true, value: response.value })
96
+ : stringify({ error: serializeClientError(response.error), id: response.id, ok: false });
97
+ }
98
+ /**
99
+ * @param {string} serialized
100
+ * @returns {RpcResponse}
101
+ */
102
+ export function deserializeRpcResponse(serialized) {
103
+ const value = parseRecord(serialized);
104
+ const id = stringField(value, "id");
105
+ if (value.ok === true)
106
+ return { id, ok: true, value: jsonField(value, "value") };
107
+ if (value.ok === false && isSerializableClientError(value.error))
108
+ return { error: deserializeClientError(value.error), id, ok: false };
109
+ throw new TypeError("Invalid RPC response");
110
+ }
111
+ /**
112
+ * @param {RpcEvent} event
113
+ * @returns {string}
114
+ */
115
+ export function serializeRpcEvent(event) {
116
+ return event.type === "live-query.changed"
117
+ ? stringify({ diff: projectQueryDiff(event.diff), subscriptionId: event.subscriptionId, type: event.type })
118
+ : stringify({ event: projectDomainEvent(event.event), subscriptionId: event.subscriptionId, type: event.type });
119
+ }
120
+ /**
121
+ * @param {string} serialized
122
+ * @returns {RpcEvent}
123
+ */
124
+ export function deserializeRpcEvent(serialized) {
125
+ const value = parseRecord(serialized);
126
+ const subscriptionId = stringField(value, "subscriptionId");
127
+ if (value.type === "live-query.changed")
128
+ return { diff: queryDiffField(value), subscriptionId, type: value.type };
129
+ if (value.type === "kernel.event")
130
+ return { event: domainEventField(value), subscriptionId, type: value.type };
131
+ throw new TypeError("Unknown RPC event type");
132
+ }
133
+ /**
134
+ * @param {unknown} value
135
+ * @returns {string}
136
+ */
137
+ function stringify(value) {
138
+ return JSON.stringify(value);
139
+ }
140
+ /**
141
+ * @param {string} serialized
142
+ * @returns {JsonObject}
143
+ */
144
+ function parseRecord(serialized) {
145
+ const value = JSON.parse(serialized);
146
+ if (!isJsonObject(value))
147
+ throw new TypeError("Expected an RPC object");
148
+ return value;
149
+ }
150
+ /**
151
+ * @param {JsonObject} value
152
+ * @param {string} field
153
+ * @returns {string}
154
+ */
155
+ function stringField(value, field) {
156
+ const candidate = value[field];
157
+ if (typeof candidate !== "string")
158
+ throw new TypeError(`Expected ${field} to be a string`);
159
+ return candidate;
160
+ }
161
+ /**
162
+ * @param {JsonObject} value
163
+ * @param {string} field
164
+ * @returns {JsonValue}
165
+ */
166
+ function jsonField(value, field) {
167
+ const candidate = value[field];
168
+ if (candidate === undefined)
169
+ throw new TypeError(`Expected ${field}`);
170
+ return candidate;
171
+ }
172
+ /**
173
+ * @param {JsonObject} value
174
+ * @returns {QueryName}
175
+ */
176
+ function queryNameField(value) {
177
+ const name = stringField(value, "name");
178
+ if (!isQueryName(name))
179
+ throw new TypeError("Expected a versioned query name");
180
+ return name;
181
+ }
182
+ /**
183
+ * @param {JsonObject} value
184
+ * @returns {ProtocolDomainEvent["type"]}
185
+ */
186
+ function eventTypeField(value) {
187
+ const type = stringField(value, "eventType");
188
+ if (!isKernelEventType(type))
189
+ throw new TypeError("Expected a known domain event type");
190
+ return /** @type {ProtocolDomainEvent["type"]} */ (type);
191
+ }
192
+ /**
193
+ * @param {JsonObject} value
194
+ * @returns {ProtocolDomainEvent}
195
+ */
196
+ function domainEventField(value) {
197
+ const event = objectField(value, "event", "Expected a domain event");
198
+ const occurredAt = timestampField(event, "occurredAt");
199
+ const type = eventTypeField({ eventType: stringField(event, "type") });
200
+ // App events (`app.<resource>`) carry an app-defined payload, so they are decoded
201
+ // structurally rather than by the exhaustive core-event switch below. The switch
202
+ // stays exhaustive over `CoreKernelEventType`, which is what makes a new core event
203
+ // type a compile error here instead of a silent decode failure.
204
+ //
205
+ // The CLOSED vocabulary is tested FIRST. `AppEventType` is spelled `app.${string}`,
206
+ // which at the type level also matches core events that happen to start with `app.`
207
+ // (`app.data.changed`, `app.data.committed`) — testing the app branch first would
208
+ // narrow those two out of the switch below and break its exhaustiveness. The runtime
209
+ // guards already agree on this precedence: `isAppEventType` rejects a dotted resource,
210
+ // so `app.data.changed` was never an app event. This just makes the compiler agree.
211
+ if (type !== "device-cleanup.changed" && !isCoreKernelEventType(type)) {
212
+ const appType = /** @type {AppEventType} */ (type);
213
+ return { occurredAt, payload: appEventPayload(event, appType), type: appType };
214
+ }
215
+ const coreType = /** @type {ProtocolCoreKernelEventType} */ (type);
216
+ switch (coreType) {
217
+ case "transport.state-changed":
218
+ return { occurredAt, payload: transportStateChangedPayload(event), type: coreType };
219
+ case "stage.catalog-invalidated":
220
+ return { occurredAt, payload: stageCatalogInvalidatedPayload(event), type: coreType };
221
+ case "stage.registry-changed":
222
+ return { occurredAt, payload: stageRegistryChangedPayload(event), type: coreType };
223
+ case "query.invalidated":
224
+ return { occurredAt, payload: queryInvalidatedPayload(event), type: coreType };
225
+ case "outbox.state-changed":
226
+ return { occurredAt, payload: outboxStateChangedPayload(event), type: coreType };
227
+ case "domain.entity-changed":
228
+ return { occurredAt, payload: domainEntityChangedPayload(event), type: coreType };
229
+ case "sync.delta-applied":
230
+ return { occurredAt, payload: syncDeltaAppliedPayload(event), type: coreType };
231
+ case "client-storage.state-changed":
232
+ return { occurredAt, payload: clientStorageStateChangedPayload(event), type: coreType };
233
+ case "offline.stage-readiness-changed":
234
+ return { occurredAt, payload: offlineStageReadinessChangedPayload(event), type: coreType };
235
+ case "offline.identity-invalidated":
236
+ return { occurredAt, payload: offlineIdentityInvalidatedPayload(event), type: coreType };
237
+ case "app.data.changed":
238
+ return { occurredAt, payload: appDataEventPayload(event), type: coreType };
239
+ case "app.data.committed":
240
+ return { occurredAt, payload: appDataEventPayload(event), type: coreType };
241
+ case "onboarding.flow-changed":
242
+ return { occurredAt, payload: onboardingFlowChangedPayload(event), type: coreType };
243
+ case "device-cleanup.changed":
244
+ return { occurredAt, payload: deviceCleanupChangedPayload(event), type: coreType };
245
+ case "room.turn-stream":
246
+ return { occurredAt, payload: roomTurnStreamPayload(event), type: coreType };
247
+ case "room.job-stream":
248
+ return { occurredAt, payload: roomJobStreamPayload(event), type: coreType };
249
+ }
250
+ }
251
+ /**
252
+ * Decode an `app.<resource>` payload. The `resource` on the wire must match the
253
+ * resource encoded in the event type — a frame claiming `app.contest` while carrying
254
+ * `resource: "wallet"` is rejected rather than delivered, so a stage's declared
255
+ * `app.<resource>` scope cannot be widened by a malformed or hostile frame.
256
+ * @param {JsonObject} event
257
+ * @param {AppEventType} type
258
+ * @returns {AppEventPayload}
259
+ */
260
+ function appEventPayload(event, type) {
261
+ const payload = objectField(event, "payload");
262
+ const resource = stringField(payload, "resource");
263
+ // The scope encoded in the type must match the payload's resource, so a frame
264
+ // claiming `app.contest` while carrying `resource: "wallet"` is rejected
265
+ // rather than delivered. The INSTANCE is compared separately: an
266
+ // instance-addressed type may legitimately carry the matching id.
267
+ if (appEventScope(type) !== `app.${resource}`) {
268
+ throw new TypeError("App event resource does not match its event type");
269
+ }
270
+ const instance = payload["instance"];
271
+ if (instance !== undefined && typeof instance !== "string") {
272
+ throw new TypeError("Expected instance to be a string");
273
+ }
274
+ const declaredInstance = appEventInstance(type);
275
+ if (declaredInstance !== undefined && instance !== undefined && instance !== declaredInstance) {
276
+ throw new TypeError("App event instance does not match its event type");
277
+ }
278
+ return {
279
+ data: objectField(payload, "data"),
280
+ ...(instance === undefined ? {} : { instance }),
281
+ resource,
282
+ type: stringField(payload, "type"),
283
+ };
284
+ }
285
+ /**
286
+ * @param {JsonObject} event
287
+ * @returns {KernelEventPayload<"room.job-stream">}
288
+ */
289
+ function roomJobStreamPayload(event) {
290
+ const payload = objectField(event, "payload");
291
+ const kind = stringField(payload, "kind");
292
+ if (kind !== "job" && kind !== "job_step")
293
+ throw new TypeError("Expected a known room job-stream kind");
294
+ const job = objectField(payload, "job");
295
+ /** @type {"open" | "done" | "aborted" | "capped"} */
296
+ const status = /** @type {"open" | "done" | "aborted" | "capped"} */ (stringField(job, "status"));
297
+ if (status !== "open" && status !== "done" && status !== "aborted" && status !== "capped")
298
+ throw new TypeError("Expected a known job status");
299
+ /** @type {"user" | "scheduled"} */
300
+ const origin = /** @type {"user" | "scheduled"} */ (stringField(job, "origin"));
301
+ if (origin !== "user" && origin !== "scheduled")
302
+ throw new TypeError("Expected a known job origin");
303
+ /** @param {JsonObject} source @param {string} field */
304
+ const optionalStr = (source, field) => {
305
+ const value = source[field];
306
+ if (value !== undefined && typeof value !== "string")
307
+ throw new TypeError(`Expected ${field} to be a string`);
308
+ return value;
309
+ };
310
+ /** @param {JsonObject} source @param {string} field */
311
+ const optionalNum = (source, field) => {
312
+ const value = source[field];
313
+ if (value !== undefined && typeof value !== "number")
314
+ throw new TypeError(`Expected ${field} to be a number`);
315
+ return value;
316
+ };
317
+ /** @param {JsonObject} source @param {string} field */
318
+ const numField = (source, field) => {
319
+ const value = source[field];
320
+ if (typeof value !== "number")
321
+ throw new TypeError(`Expected ${field} to be a number`);
322
+ return value;
323
+ };
324
+ const closedAt = optionalStr(job, "closedAt");
325
+ const openedBySeq = optionalNum(job, "openedBySeq");
326
+ const mappedJob = {
327
+ ...(closedAt === undefined ? {} : { closedAt }), jobId: stringField(job, "jobId"),
328
+ openedAt: stringField(job, "openedAt"), ...(openedBySeq === undefined ? {} : { openedBySeq }),
329
+ origin, pendingArrivals: numField(job, "pendingArrivals"), queueCount: numField(job, "queueCount"),
330
+ roomId: stringField(job, "roomId"), status, stepCount: numField(job, "stepCount"),
331
+ turnsUsed: numField(job, "turnsUsed"),
332
+ };
333
+ /** @type {KernelEventPayload<"room.job-stream">["step"]} */
334
+ let mappedStep;
335
+ if (payload.step !== undefined) {
336
+ const step = objectField(payload, "step");
337
+ const actor = objectField(step, "actor");
338
+ const actorKind = stringField(actor, "kind");
339
+ if (actorKind !== "orchestrator" && actorKind !== "specialist")
340
+ throw new TypeError("Expected a known step actor kind");
341
+ const name = optionalStr(actor, "name");
342
+ const slug = optionalStr(actor, "slug");
343
+ const outcome = optionalStr(step, "outcome");
344
+ const runId = optionalStr(step, "runId");
345
+ const seq = optionalNum(step, "seq");
346
+ mappedStep = {
347
+ actor: { kind: actorKind, ...(name === undefined ? {} : { name }), ...(slug === undefined ? {} : { slug }) },
348
+ detail: stringField(step, "detail"), kind: stringField(step, "kind"),
349
+ ...(outcome === undefined ? {} : { outcome }), ...(runId === undefined ? {} : { runId }),
350
+ ...(seq === undefined ? {} : { seq }), stepId: stringField(step, "stepId"), ts: stringField(step, "ts"),
351
+ };
352
+ }
353
+ return { job: mappedJob, kind, roomId: stringField(payload, "roomId"),
354
+ ...(mappedStep === undefined ? {} : { step: mappedStep }) };
355
+ }
356
+ /**
357
+ * @param {JsonObject} event
358
+ * @returns {KernelEventPayload<"room.turn-stream">}
359
+ */
360
+ function roomTurnStreamPayload(event) {
361
+ const payload = objectField(event, "payload");
362
+ const kind = stringField(payload, "kind");
363
+ if (kind !== "room.turn_started" && kind !== "turn.completed" && kind !== "turn.delta" &&
364
+ kind !== "turn.failed" && kind !== "turn.tool_delta" && kind !== "work.delta")
365
+ throw new TypeError("Expected a known room turn-stream kind");
366
+ /** @param {string} field */
367
+ const optional = (field) => {
368
+ const value = payload[field];
369
+ if (value !== undefined && typeof value !== "string")
370
+ throw new TypeError(`Expected ${field} to be a string`);
371
+ return value;
372
+ };
373
+ const anchorSession = stringField(payload, "anchorSession");
374
+ const agentName = optional("agentName");
375
+ const argsDelta = optional("argsDelta");
376
+ const runId = optional("runId");
377
+ const callId = optional("callId");
378
+ const callIndex = payload.callIndex;
379
+ if (callIndex !== undefined && typeof callIndex !== "number")
380
+ throw new TypeError("Expected callIndex to be a number");
381
+ const reason = optional("reason");
382
+ const responderName = optional("responderName");
383
+ const responderSlug = optional("responderSlug");
384
+ const text = optional("text");
385
+ const toolName = optional("toolName");
386
+ return {
387
+ ...(agentName === undefined ? {} : { agentName }), anchorSession,
388
+ ...(argsDelta === undefined ? {} : { argsDelta }), ...(callId === undefined ? {} : { callId }),
389
+ ...(callIndex === undefined ? {} : { callIndex }), kind, ...(reason === undefined ? {} : { reason }),
390
+ ...(responderName === undefined ? {} : { responderName }),
391
+ ...(responderSlug === undefined ? {} : { responderSlug }), roomId: stringField(payload, "roomId"),
392
+ ...(runId === undefined ? {} : { runId }), ...(text === undefined ? {} : { text }),
393
+ ...(toolName === undefined ? {} : { toolName }),
394
+ };
395
+ }
396
+ /**
397
+ * @param {JsonObject} event
398
+ * @returns {KernelEventPayload<"client-storage.state-changed">}
399
+ */
400
+ function clientStorageStateChangedPayload(event) {
401
+ const payload = objectField(event, "payload");
402
+ const status = stringField(payload, "status");
403
+ if (status !== "ready" && status !== "recovery-required" && status !== "starting") {
404
+ throw new TypeError("Expected a known client-storage status");
405
+ }
406
+ // `code`/`message` accompany "recovery-required" only; a wire frame that omits them for
407
+ // the other two states is well-formed, so they are decoded as optional rather than
408
+ // required-and-empty (which would erase the distinction the shell renders on).
409
+ const code = payload["code"];
410
+ const message = payload["message"];
411
+ if (code !== undefined && typeof code !== "string")
412
+ throw new TypeError("Expected code to be a string");
413
+ if (message !== undefined && typeof message !== "string") {
414
+ throw new TypeError("Expected message to be a string");
415
+ }
416
+ return {
417
+ ...(code === undefined ? {} : { code }),
418
+ ...(message === undefined ? {} : { message }),
419
+ status,
420
+ };
421
+ }
422
+ /**
423
+ * @param {JsonObject} event
424
+ * @returns {KernelEventPayload<"offline.stage-readiness-changed">}
425
+ */
426
+ function offlineStageReadinessChangedPayload(event) {
427
+ const payload = objectField(event, "payload");
428
+ const readiness = objectField(payload, "readiness");
429
+ const state = stringField(readiness, "state");
430
+ if (state !== "caching-assets" && state !== "needs-first-sync" &&
431
+ state !== "offline-ready" && state !== "update-available" &&
432
+ state !== "failed" && state !== "removing") {
433
+ throw new TypeError("Expected a known offline stage state");
434
+ }
435
+ const failureCode = optionalStringField(readiness, "failureCode");
436
+ if (failureCode !== undefined && failureCode !== "asset-fetch-failed" &&
437
+ failureCode !== "asset-integrity-failed" && failureCode !== "projection-seed-failed" &&
438
+ failureCode !== "schema-incompatible" && failureCode !== "quota-exceeded" &&
439
+ failureCode !== "identity-mismatch" && failureCode !== "active-resource-corrupt") {
440
+ throw new TypeError("Expected a known offline readiness failure code");
441
+ }
442
+ const activeManifestSha256 = optionalStringField(readiness, "activeManifestSha256");
443
+ const candidateManifestSha256 = optionalStringField(readiness, "candidateManifestSha256");
444
+ return {
445
+ readiness: {
446
+ ...(activeManifestSha256 === undefined ? {} : { activeManifestSha256 }),
447
+ appName: stringField(readiness, "appName"),
448
+ ...(candidateManifestSha256 === undefined ? {} : { candidateManifestSha256 }),
449
+ ...(failureCode === undefined ? {} : { failureCode }),
450
+ state,
451
+ updatedAt: timestampField(readiness, "updatedAt"),
452
+ },
453
+ };
454
+ }
455
+ /**
456
+ * @param {JsonObject} event
457
+ * @returns {KernelEventPayload<"offline.identity-invalidated">}
458
+ */
459
+ function offlineIdentityInvalidatedPayload(event) {
460
+ const payload = objectField(event, "payload");
461
+ const scope = objectField(payload, "scope");
462
+ return {
463
+ scope: {
464
+ deviceDid: stringField(scope, "deviceDid"),
465
+ homeNodeId: stringField(scope, "homeNodeId"),
466
+ },
467
+ };
468
+ }
469
+ /**
470
+ * @param {JsonObject} event
471
+ * @returns {KernelEventPayload<"app.data.changed">}
472
+ */
473
+ function appDataEventPayload(event) {
474
+ const payload = objectField(event, "payload");
475
+ return {
476
+ appName: stringField(payload, "appName"),
477
+ cursor: stringField(payload, "cursor"),
478
+ revision: stringField(payload, "revision"),
479
+ stream: stringField(payload, "stream"),
480
+ };
481
+ }
482
+ /**
483
+ * Decodes only `stepId`, because that is the entire payload the domain declares. Any other
484
+ * field a frame carries is dropped here rather than forwarded: this event crosses the wire to
485
+ * every subscriber, and the flow's secrets (the Wi-Fi password, the match-code digits, the
486
+ * invitation code) must not be able to ride along even if a future producer misbehaves.
487
+ * @param {JsonObject} event
488
+ * @returns {KernelEventPayload<"onboarding.flow-changed">}
489
+ */
490
+ function onboardingFlowChangedPayload(event) {
491
+ const payload = objectField(event, "payload");
492
+ return { stepId: stringField(payload, "stepId") };
493
+ }
494
+ /**
495
+ * @param {JsonObject} event
496
+ * @returns {{ readonly state?: string }}
497
+ */
498
+ function deviceCleanupChangedPayload(event) {
499
+ const payload = objectField(event, "payload");
500
+ const state = optionalStringField(payload, "state");
501
+ return { ...(state === undefined ? {} : { state }) };
502
+ }
503
+ /**
504
+ * @param {JsonObject} event
505
+ * @returns {KernelEventPayload<"stage.catalog-invalidated">}
506
+ */
507
+ function stageCatalogInvalidatedPayload(event) {
508
+ const payload = objectField(event, "payload");
509
+ return { reason: stringField(payload, "reason") };
510
+ }
511
+ /**
512
+ * @param {JsonObject} value
513
+ * @returns {QueryDiff<JsonValue>}
514
+ */
515
+ function queryDiffField(value) {
516
+ const record = objectField(value, "diff", "Expected a query diff");
517
+ return {
518
+ added: arrayField(record, "added"),
519
+ deleted: arrayField(record, "deleted"),
520
+ revision: numberField(record, "revision"),
521
+ updated: arrayField(record, "updated"),
522
+ };
523
+ }
524
+ /**
525
+ * @param {ProtocolDomainEvent} event
526
+ * @returns {object}
527
+ */
528
+ function projectDomainEvent(event) {
529
+ return { occurredAt: event.occurredAt, payload: event.payload, type: event.type };
530
+ }
531
+ /**
532
+ * @param {QueryDiff<JsonValue>} diff
533
+ * @returns {object}
534
+ */
535
+ function projectQueryDiff(diff) {
536
+ return { added: diff.added, deleted: diff.deleted, revision: diff.revision, updated: diff.updated };
537
+ }
538
+ /**
539
+ * @param {JsonObject} value
540
+ * @param {string} field
541
+ * @returns {readonly JsonValue[]}
542
+ */
543
+ function arrayField(value, field) {
544
+ const candidate = value[field];
545
+ if (!Array.isArray(candidate))
546
+ throw new TypeError(`Expected ${field} to be an array`);
547
+ return candidate;
548
+ }
549
+ /**
550
+ * @param {JsonObject} value
551
+ * @param {string} field
552
+ * @returns {number}
553
+ */
554
+ function numberField(value, field) {
555
+ const candidate = value[field];
556
+ if (typeof candidate !== "number" || !Number.isFinite(candidate)) {
557
+ throw new TypeError(`Expected ${field} to be a finite number`);
558
+ }
559
+ return candidate;
560
+ }
561
+ /** @param {JsonObject} value @param {string} field @returns {number} */
562
+ function integerField(value, field) {
563
+ const candidate = numberField(value, field);
564
+ if (!Number.isInteger(candidate))
565
+ throw new TypeError(`Expected ${field} to be an integer`);
566
+ return candidate;
567
+ }
568
+ /** @param {JsonObject} value @param {string} field @returns {string | undefined} */
569
+ function optionalStringField(value, field) {
570
+ const candidate = value[field];
571
+ if (candidate === undefined)
572
+ return undefined;
573
+ if (typeof candidate !== "string")
574
+ throw new TypeError(`Expected ${field} to be a string`);
575
+ return candidate;
576
+ }
577
+ /**
578
+ * @param {JsonObject} value
579
+ * @param {string} field
580
+ * @param {string} [message=`Expected ${field} to be an object`]
581
+ * @returns {JsonObject}
582
+ */
583
+ function objectField(value, field, message = `Expected ${field} to be an object`) {
584
+ const candidate = value[field];
585
+ if (!isJsonObject(candidate))
586
+ throw new TypeError(message);
587
+ return candidate;
588
+ }
589
+ /**
590
+ * @param {JsonObject} value
591
+ * @param {string} field
592
+ * @returns {readonly string[]}
593
+ */
594
+ function stringArrayField(value, field) {
595
+ const candidate = value[field];
596
+ if (!Array.isArray(candidate) || !candidate.every((item) => typeof item === "string")) {
597
+ throw new TypeError(`Expected ${field} to be an array of strings`);
598
+ }
599
+ return candidate;
600
+ }
601
+ /**
602
+ * @param {JsonObject} value
603
+ * @param {string} field
604
+ * @returns {string}
605
+ */
606
+ function timestampField(value, field) {
607
+ const candidate = stringField(value, field);
608
+ const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u;
609
+ if (!timestampPattern.test(candidate) || !Number.isFinite(Date.parse(candidate))) {
610
+ throw new TypeError(`Expected ${field} to be an RFC 3339 timestamp`);
611
+ }
612
+ return candidate;
613
+ }
614
+ /**
615
+ * @param {JsonObject} event
616
+ * @returns {KernelEventPayload<"transport.state-changed">}
617
+ */
618
+ function transportStateChangedPayload(event) {
619
+ const payload = objectField(event, "payload");
620
+ const state = objectField(payload, "state");
621
+ const changedAt = timestampField(state, "changedAt");
622
+ const path = stringField(state, "path");
623
+ const status = stringField(state, "status");
624
+ if (status === "connected" && isConnectedTransportPath(path)) {
625
+ return { state: { changedAt, path, status } };
626
+ }
627
+ if (path === "offline") {
628
+ if (status === "connecting")
629
+ return { state: { attempt: integerField(state, "attempt"), changedAt, path, status } };
630
+ if (status === "reconnecting") {
631
+ return { state: { attempt: integerField(state, "attempt"), changedAt,
632
+ nextRetryAt: timestampField(state, "nextRetryAt"), path, status } };
633
+ }
634
+ if (status === "offline") {
635
+ const reason = optionalStringField(state, "reason");
636
+ if (reason !== undefined && !isTransportOfflineReason(reason))
637
+ throw new TypeError("Expected a valid transport offline reason");
638
+ return { state: { changedAt, path, status, ...(reason === undefined ? {} : { reason }) } };
639
+ }
640
+ }
641
+ throw new TypeError("Expected a valid transport state");
642
+ }
643
+ /**
644
+ * @param {JsonObject} event
645
+ * @returns {KernelEventPayload<"stage.registry-changed">}
646
+ */
647
+ function stageRegistryChangedPayload(event) {
648
+ const payload = objectField(event, "payload");
649
+ return { appNames: stringArrayField(payload, "appNames") };
650
+ }
651
+ /**
652
+ * @param {JsonObject} event
653
+ * @returns {KernelEventPayload<"query.invalidated">}
654
+ */
655
+ function queryInvalidatedPayload(event) {
656
+ const payload = objectField(event, "payload");
657
+ return { name: queryNameField(payload) };
658
+ }
659
+ /**
660
+ * @param {JsonObject} event
661
+ * @returns {KernelEventPayload<"outbox.state-changed">}
662
+ */
663
+ function outboxStateChangedPayload(event) {
664
+ const payload = objectField(event, "payload");
665
+ const intent = objectField(payload, "intent");
666
+ return {
667
+ intent: {
668
+ domain: stringField(intent, "domain"),
669
+ id: stringField(intent, "id"),
670
+ idempotencyKey: stringField(intent, "idempotencyKey"),
671
+ payload: jsonField(intent, "payload"),
672
+ state: outboxIntentStateField(intent),
673
+ },
674
+ };
675
+ }
676
+ /**
677
+ * @param {JsonObject} event
678
+ * @returns {KernelEventPayload<"domain.entity-changed">}
679
+ */
680
+ function domainEntityChangedPayload(event) {
681
+ const payload = objectField(event, "payload");
682
+ return {
683
+ action: entityActionField(payload),
684
+ entityId: stringField(payload, "entityId"),
685
+ entityType: stringField(payload, "entityType"),
686
+ };
687
+ }
688
+ /**
689
+ * @param {JsonObject} event
690
+ * @returns {KernelEventPayload<"sync.delta-applied">}
691
+ */
692
+ function syncDeltaAppliedPayload(event) {
693
+ const payload = objectField(event, "payload");
694
+ return {
695
+ cursor: stringField(payload, "cursor"),
696
+ domain: stringField(payload, "domain"),
697
+ };
698
+ }
699
+ /** @param {string} value */
700
+ function isTransportOfflineReason(value) {
701
+ return value === "no-network" || value === "node-unreachable" || value === "session-required";
702
+ }
703
+ /**
704
+ * @param {string} value
705
+ * @returns {value is "iroh-relay" | "lan-direct" | "lan-webtransport" | "wan-direct" | "webrtc-direct"}
706
+ */
707
+ function isConnectedTransportPath(value) {
708
+ return (value === "iroh-relay" || value === "lan-direct" || value === "lan-webtransport" ||
709
+ value === "wan-direct" || value === "webrtc-direct");
710
+ }
711
+ /**
712
+ * @param {JsonObject} value
713
+ * @returns {"acknowledged" | "failed" | "pending" | "replaying"}
714
+ */
715
+ function outboxIntentStateField(value) {
716
+ const state = stringField(value, "state");
717
+ if (state === "acknowledged" || state === "failed" || state === "pending" || state === "replaying") {
718
+ return state;
719
+ }
720
+ throw new TypeError("Expected a known outbox intent state");
721
+ }
722
+ /**
723
+ * @param {JsonObject} value
724
+ * @returns {"created" | "deleted" | "updated"}
725
+ */
726
+ function entityActionField(value) {
727
+ const action = stringField(value, "action");
728
+ if (action === "created" || action === "deleted" || action === "updated")
729
+ return action;
730
+ throw new TypeError("Expected a known entity action");
731
+ }
732
+ /**
733
+ * @typedef {| { readonly id: string; readonly name: string; readonly payload: JsonValue; readonly type: "command" }
734
+ * | { readonly id: string; readonly name: QueryName; readonly params: JsonValue; readonly type: "query" }
735
+ * | { readonly id: string; readonly name: QueryName; readonly params: JsonValue; readonly subscriptionId: string; readonly type: "live-query.subscribe" }
736
+ * | { readonly id: string; readonly subscriptionId: string; readonly type: "live-query.unsubscribe" }
737
+ * | { readonly eventType: ProtocolDomainEvent["type"]; readonly id: string; readonly subscriptionId: string; readonly type: "event.subscribe" }
738
+ * | { readonly id: string; readonly subscriptionId: string; readonly type: "event.unsubscribe" }
739
+ * | { readonly event: ProtocolDomainEvent; readonly id: string; readonly type: "event.publish" }
740
+ * | { readonly appName: string; readonly id: string; readonly type: "stage.prepare" }
741
+ * | { readonly appName: string; readonly id: string; readonly manifestSha256: string; readonly type: "stage.open-session" }
742
+ * | { readonly id: string; readonly name: QueryName; readonly params: JsonValue; readonly sessionId: string; readonly type: "stage.query" }
743
+ * | { readonly id: string; readonly name: QueryName; readonly params: JsonValue; readonly sessionId: string; readonly subscriptionId: string; readonly type: "stage.live-query.subscribe" }
744
+ * | { readonly id: string; readonly sessionId: string; readonly subscriptionId: string; readonly type: "stage.live-query.unsubscribe" }
745
+ * | { readonly eventType: ProtocolDomainEvent["type"]; readonly id: string; readonly sessionId: string; readonly subscriptionId: string; readonly type: "stage.event.subscribe" }
746
+ * | { readonly id: string; readonly sessionId: string; readonly subscriptionId: string; readonly type: "stage.event.unsubscribe" }
747
+ * | { readonly event: ProtocolDomainEvent; readonly id: string; readonly sessionId: string; readonly type: "stage.event.publish" }
748
+ * | { readonly capability: string; readonly id: string; readonly payload: JsonValue; readonly sessionId: string; readonly type: "stage.capability.invoke" }
749
+ * | { readonly id: string; readonly sessionId: string; readonly type: "stage.close-session" }} RpcRequest
750
+ */
751
+ /**
752
+ * @typedef {| { readonly id: string; readonly ok: true; readonly value: JsonValue }
753
+ * | { readonly error: ClientError; readonly id: string; readonly ok: false }} RpcResponse
754
+ */
755
+ /**
756
+ * @typedef {| { readonly diff: QueryDiff<JsonValue>; readonly subscriptionId: string; readonly type: "live-query.changed" }
757
+ * | { readonly event: ProtocolDomainEvent; readonly subscriptionId: string; readonly type: "kernel.event" }} RpcEvent
758
+ */