memeloop 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/browser.d.cts +14 -14
  2. package/dist/browser.d.ts +14 -14
  3. package/dist/{chunk-5JEV7632.js → chunk-5VYZV33O.js} +2 -1
  4. package/dist/{chunk-5JEV7632.js.map → chunk-5VYZV33O.js.map} +1 -1
  5. package/dist/chunk-ECNKBH5A.js +1235 -0
  6. package/dist/chunk-ECNKBH5A.js.map +1 -0
  7. package/dist/{chunk-MRKY4HGF.js → chunk-EPHRIRAL.js} +20 -11
  8. package/dist/chunk-EPHRIRAL.js.map +1 -0
  9. package/dist/{chunk-P6L3444O.js → chunk-KV6TC5UJ.js} +2 -2
  10. package/dist/chunk-MXRXYGWW.js +1 -0
  11. package/dist/conversation.d.cts +3 -3
  12. package/dist/conversation.d.ts +3 -3
  13. package/dist/device-network-portable.cjs +757 -144
  14. package/dist/device-network-portable.cjs.map +1 -1
  15. package/dist/device-network-portable.d.cts +24 -14
  16. package/dist/device-network-portable.d.ts +24 -14
  17. package/dist/device-network-portable.js +18 -2
  18. package/dist/device-network.cjs +776 -154
  19. package/dist/device-network.cjs.map +1 -1
  20. package/dist/device-network.d.cts +5 -5
  21. package/dist/device-network.d.ts +5 -5
  22. package/dist/device-network.js +19 -3
  23. package/dist/{fetchProvider-WWnWnQFP.d.ts → fetchProvider-BhqRqi5-.d.ts} +1 -1
  24. package/dist/{fetchProvider-toTHy5Nu.d.cts → fetchProvider-HJ_FkORN.d.cts} +1 -1
  25. package/dist/index.cjs +782 -155
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +16 -16
  28. package/dist/index.d.ts +16 -16
  29. package/dist/index.js +24 -6
  30. package/dist/index.js.map +1 -1
  31. package/dist/{interface-IrKBFtL1.d.ts → interface-3C3eG8Hn.d.cts} +13 -1
  32. package/dist/{interface-IrKBFtL1.d.cts → interface-3C3eG8Hn.d.ts} +13 -1
  33. package/dist/llm-providers.d.cts +2 -2
  34. package/dist/llm-providers.d.ts +2 -2
  35. package/dist/loop-api.cjs +1 -0
  36. package/dist/loop-api.cjs.map +1 -1
  37. package/dist/loop-api.d.cts +7 -7
  38. package/dist/loop-api.d.ts +7 -7
  39. package/dist/loop-api.js +2 -2
  40. package/dist/mobile.d.cts +5 -5
  41. package/dist/mobile.d.ts +5 -5
  42. package/dist/{orchestration-portable-BHB_YrNN.d.ts → orchestration-portable-BbkwLbC9.d.ts} +4 -4
  43. package/dist/{orchestration-portable-mvYBvGBb.d.cts → orchestration-portable-DNKzAtNq.d.cts} +4 -4
  44. package/dist/orchestration-portable.d.cts +5 -5
  45. package/dist/orchestration-portable.d.ts +5 -5
  46. package/dist/pairingInvite-DDx_-IAp.d.cts +271 -0
  47. package/dist/pairingInvite-Dkz8CLsb.d.ts +271 -0
  48. package/dist/{promptConcat-0WNFTg4l.d.ts → promptConcat-CUEIA_3T.d.ts} +3 -3
  49. package/dist/{promptConcat-D6wr5V-V.d.cts → promptConcat-VLe5SEN4.d.cts} +3 -3
  50. package/dist/{providerRegistry-CPsktuiX.d.cts → providerRegistry-BDGGs5gz.d.cts} +2 -2
  51. package/dist/{providerRegistry-Bi6pAvSj.d.ts → providerRegistry-CaFK_aUd.d.ts} +2 -2
  52. package/dist/{registry-D0kbxksn.d.cts → registry-DNSEVYug.d.cts} +3 -3
  53. package/dist/{registry-DDbvhlhs.d.ts → registry-yGnYHWhq.d.ts} +3 -3
  54. package/dist/{remoteClient-CBggnRy3.d.cts → remoteClient-BKmQEQY9.d.cts} +1 -1
  55. package/dist/{remoteClient-B_spK019.d.ts → remoteClient-D5hco6cS.d.ts} +1 -1
  56. package/dist/{runtime-UONPTAYK.js → runtime-6EELHWVB.js} +2 -2
  57. package/dist/{scriptDeploymentPipeline-CXJcC82H.d.ts → scriptDeploymentPipeline-B1q-UhrO.d.ts} +1 -1
  58. package/dist/{scriptDeploymentPipeline-yG1ZhVVI.d.cts → scriptDeploymentPipeline-XJzQyYys.d.cts} +1 -1
  59. package/dist/{syncCloudDevices-Ds_Kcgrv.d.cts → syncCloudDevices-BFw_c1g5.d.cts} +6 -5
  60. package/dist/{syncCloudDevices-BEqwgrhD.d.ts → syncCloudDevices-C8fCUhnX.d.ts} +6 -5
  61. package/dist/{types-CdFVSntj.d.ts → types-C8gnhG9M.d.cts} +6 -4
  62. package/dist/{types-7pYti-k9.d.cts → types-CcFFIk8W.d.ts} +6 -4
  63. package/package.json +1 -1
  64. package/dist/chunk-426CJUZP.js +0 -630
  65. package/dist/chunk-426CJUZP.js.map +0 -1
  66. package/dist/chunk-MRKY4HGF.js.map +0 -1
  67. package/dist/chunk-VZKQX5PP.js +0 -1
  68. package/dist/pairingInvite-BXPWY3aM.d.cts +0 -149
  69. package/dist/pairingInvite-Cuz0wuq9.d.ts +0 -149
  70. /package/dist/{chunk-P6L3444O.js.map → chunk-KV6TC5UJ.js.map} +0 -0
  71. /package/dist/{chunk-VZKQX5PP.js.map → chunk-MXRXYGWW.js.map} +0 -0
  72. /package/dist/{runtime-UONPTAYK.js.map → runtime-6EELHWVB.js.map} +0 -0
@@ -24,6 +24,8 @@ __export(device_network_entry_exports, {
24
24
  DEVICE_ORCHESTRATION_PROTOCOL: () => DEVICE_ORCHESTRATION_PROTOCOL,
25
25
  DEVICE_PAIRING_INVITE_PROTOCOL: () => DEVICE_PAIRING_INVITE_PROTOCOL,
26
26
  DEVICE_PAIRING_INVITE_TTL_MS: () => DEVICE_PAIRING_INVITE_TTL_MS,
27
+ DeviceCloudConnectionCoordinator: () => DeviceCloudConnectionCoordinator,
28
+ JsonFrameError: () => JsonFrameError,
27
29
  LIBP2P_RPC_REQUEST_TYPE: () => LIBP2P_RPC_REQUEST_TYPE,
28
30
  LIBP2P_RPC_RESPONSE_TYPE: () => LIBP2P_RPC_RESPONSE_TYPE,
29
31
  LIBP2P_SYNC_REQUEST_TYPE: () => LIBP2P_SYNC_REQUEST_TYPE,
@@ -31,13 +33,19 @@ __export(device_network_entry_exports, {
31
33
  Libp2pDeviceSyncTransport: () => Libp2pDeviceSyncTransport,
32
34
  LocalTrustDeviceAuthorizer: () => LocalTrustDeviceAuthorizer,
33
35
  MemoryDeviceNetworkService: () => MemoryDeviceNetworkService,
36
+ MemoryDeviceSyncStateStore: () => MemoryDeviceSyncStateStore,
34
37
  PeerNodeSyncAdapter: () => PeerNodeSyncAdapter,
35
38
  attachmentBlobFromWire: () => attachmentBlobFromWire,
39
+ canonicalDevicePairingInviteBytes: () => canonicalDevicePairingInviteBytes,
40
+ computeMissingVersionRanges: () => computeMissingVersionRanges,
36
41
  createDeviceOrchestrationStreamHandler: () => createDeviceOrchestrationStreamHandler,
37
42
  createDeviceOrchestrationTransport: () => createDeviceOrchestrationTransport,
38
43
  createDevicePairingInvite: () => createDevicePairingInvite,
44
+ createJsonFrameReader: () => createJsonFrameReader,
39
45
  decodeAttachmentBlobRpc: () => decodeAttachmentBlobRpc,
40
46
  encodeDevicePairingInvite: () => encodeDevicePairingInvite,
47
+ encodeJsonFrame: () => encodeJsonFrame,
48
+ encodeJsonFrames: () => encodeJsonFrames,
41
49
  isConversationMeta: () => isConversationMeta,
42
50
  isLibp2pRpcRequest: () => isLibp2pRpcRequest,
43
51
  isLibp2pRpcResponse: () => isLibp2pRpcResponse,
@@ -48,6 +56,224 @@ __export(device_network_entry_exports, {
48
56
  });
49
57
  module.exports = __toCommonJS(device_network_entry_exports);
50
58
 
59
+ // src/device-network/deviceCloudConnectionCoordinator.ts
60
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 6e4;
61
+ var DEFAULT_INITIAL_BACKOFF_MS = 1e3;
62
+ var DEFAULT_MAX_BACKOFF_MS = 6e4;
63
+ var DEFAULT_JITTER_RATIO = 0.2;
64
+ function emptyComponents() {
65
+ return {
66
+ authorizer: "not-run",
67
+ registration: "not-run",
68
+ relay: "not-run",
69
+ heartbeat: "not-run",
70
+ directory: "not-run"
71
+ };
72
+ }
73
+ function assertNonNegativeFinite(value, name) {
74
+ if (!Number.isFinite(value) || value < 0) {
75
+ throw new TypeError(`${name} must be a finite non-negative number`);
76
+ }
77
+ }
78
+ var DeviceCloudConnectionCoordinator = class {
79
+ constructor(options) {
80
+ this.options = options;
81
+ this.configuration = options.configuration;
82
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
83
+ this.initialBackoffMs = options.initialBackoffMs ?? DEFAULT_INITIAL_BACKOFF_MS;
84
+ this.maxBackoffMs = options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
85
+ this.jitterRatio = options.jitterRatio ?? DEFAULT_JITTER_RATIO;
86
+ this.now = options.now ?? Date.now;
87
+ this.random = options.random ?? Math.random;
88
+ assertNonNegativeFinite(this.heartbeatIntervalMs, "heartbeatIntervalMs");
89
+ assertNonNegativeFinite(this.initialBackoffMs, "initialBackoffMs");
90
+ assertNonNegativeFinite(this.maxBackoffMs, "maxBackoffMs");
91
+ assertNonNegativeFinite(this.jitterRatio, "jitterRatio");
92
+ if (this.jitterRatio > 1) throw new TypeError("jitterRatio must not exceed 1");
93
+ this.resetSnapshot();
94
+ }
95
+ configuration;
96
+ generation = 0;
97
+ controller = new AbortController();
98
+ timer;
99
+ inFlight;
100
+ started = false;
101
+ consecutiveFailures = 0;
102
+ snapshotValue = {
103
+ status: "not-configured",
104
+ generation: 0,
105
+ components: emptyComponents()
106
+ };
107
+ heartbeatIntervalMs;
108
+ initialBackoffMs;
109
+ maxBackoffMs;
110
+ jitterRatio;
111
+ now;
112
+ random;
113
+ get snapshot() {
114
+ return {
115
+ ...this.snapshotValue,
116
+ components: { ...this.snapshotValue.components }
117
+ };
118
+ }
119
+ async start() {
120
+ if (this.started) return this.runNow();
121
+ this.started = true;
122
+ await this.runNow();
123
+ }
124
+ async stop() {
125
+ this.started = false;
126
+ this.clearTimer();
127
+ this.controller.abort(new Error("device cloud coordinator stopped"));
128
+ await this.inFlight?.catch(() => void 0);
129
+ }
130
+ async setConfiguration(configuration) {
131
+ this.configuration = configuration;
132
+ this.generation += 1;
133
+ this.consecutiveFailures = 0;
134
+ this.clearTimer();
135
+ this.controller.abort(new Error("device cloud configuration changed"));
136
+ this.controller = new AbortController();
137
+ this.inFlight = void 0;
138
+ this.resetSnapshot();
139
+ if (this.started && this.options.adapter.isConfigured(configuration)) await this.runNow();
140
+ }
141
+ runNow() {
142
+ if (this.inFlight) return this.inFlight;
143
+ if (!this.options.adapter.isConfigured(this.configuration)) {
144
+ this.resetSnapshot();
145
+ return Promise.resolve();
146
+ }
147
+ const generation = this.generation;
148
+ const configuration = this.configuration;
149
+ const signal = this.controller.signal;
150
+ const run = this.maintain(configuration, generation, signal);
151
+ const tracked = run.finally(() => {
152
+ if (this.inFlight === tracked) this.inFlight = void 0;
153
+ });
154
+ this.inFlight = tracked;
155
+ return tracked;
156
+ }
157
+ async maintain(configuration, generation, signal) {
158
+ if (!this.isCurrent(generation, signal)) return;
159
+ await this.updateSnapshot(generation, {
160
+ status: "connecting",
161
+ lastError: void 0,
162
+ nextRetryAt: void 0
163
+ });
164
+ try {
165
+ if (this.snapshotValue.components.authorizer !== "ready") {
166
+ await this.runStep("authorizer", generation, signal, () => this.options.adapter.ensureAuthorizer(configuration, signal));
167
+ }
168
+ if (this.snapshotValue.components.registration !== "ready") {
169
+ await this.runStep("registration", generation, signal, () => this.options.adapter.registerDevice(configuration, signal));
170
+ }
171
+ let relayError;
172
+ try {
173
+ await this.runStep("relay", generation, signal, () => this.options.adapter.ensureRelay(configuration, signal));
174
+ } catch (error) {
175
+ relayError = error;
176
+ this.options.logWarning?.("Cloud relay maintenance failed", error);
177
+ }
178
+ await this.runStep("heartbeat", generation, signal, () => this.options.adapter.heartbeat(configuration, signal));
179
+ let directoryError;
180
+ try {
181
+ await this.runStep("directory", generation, signal, () => this.options.adapter.syncDirectory(configuration, signal));
182
+ } catch (error) {
183
+ directoryError = error;
184
+ this.options.logWarning?.("Cloud directory synchronization failed", error);
185
+ }
186
+ if (!this.isCurrent(generation, signal)) return;
187
+ this.consecutiveFailures = 0;
188
+ const relayRequired = this.options.adapter.relayRequiredForOnline(configuration);
189
+ await this.updateSnapshot(generation, {
190
+ status: directoryError || relayRequired && relayError ? "degraded" : "online",
191
+ lastError: directoryError ?? relayError,
192
+ nextRetryAt: this.now() + this.heartbeatIntervalMs
193
+ });
194
+ this.schedule(this.heartbeatIntervalMs, generation);
195
+ } catch (error) {
196
+ if (!this.isCurrent(generation, signal)) return;
197
+ this.consecutiveFailures += 1;
198
+ if (this.snapshotValue.components.heartbeat === "failed") {
199
+ this.snapshotValue.components.registration = "not-run";
200
+ }
201
+ const status = this.options.adapter.classifyError?.(error) ?? "offline";
202
+ const delay = this.backoffDelay();
203
+ await this.updateSnapshot(generation, {
204
+ status,
205
+ lastError: error,
206
+ nextRetryAt: this.now() + delay
207
+ });
208
+ this.schedule(delay, generation);
209
+ throw error;
210
+ }
211
+ }
212
+ async runStep(component, generation, signal, operation) {
213
+ if (!this.isCurrent(generation, signal)) return;
214
+ await this.setComponent(component, "pending", generation);
215
+ try {
216
+ const result = await operation();
217
+ if (!this.isCurrent(generation, signal)) return;
218
+ await result?.commit?.();
219
+ if (!this.isCurrent(generation, signal)) return;
220
+ await this.setComponent(component, "ready", generation);
221
+ } catch (error) {
222
+ if (this.isCurrent(generation, signal)) await this.setComponent(component, "failed", generation);
223
+ throw error;
224
+ }
225
+ }
226
+ backoffDelay() {
227
+ const exponential = Math.min(
228
+ this.maxBackoffMs,
229
+ this.initialBackoffMs * 2 ** Math.max(0, this.consecutiveFailures - 1)
230
+ );
231
+ const jitter = exponential * this.jitterRatio * (this.random() * 2 - 1);
232
+ return Math.max(0, Math.round(exponential + jitter));
233
+ }
234
+ schedule(delayMs, generation) {
235
+ if (!this.started || generation !== this.generation) return;
236
+ this.clearTimer();
237
+ this.timer = setTimeout(() => {
238
+ if (generation !== this.generation) return;
239
+ void this.runNow().catch((error) => {
240
+ this.options.logWarning?.("Cloud maintenance failed", error);
241
+ });
242
+ }, delayMs);
243
+ }
244
+ clearTimer() {
245
+ if (this.timer) clearTimeout(this.timer);
246
+ this.timer = void 0;
247
+ }
248
+ isCurrent(generation, signal) {
249
+ return generation === this.generation && !signal.aborted;
250
+ }
251
+ async setComponent(component, status, generation) {
252
+ if (generation !== this.generation) return;
253
+ this.snapshotValue = {
254
+ ...this.snapshotValue,
255
+ components: { ...this.snapshotValue.components, [component]: status }
256
+ };
257
+ await this.emit();
258
+ }
259
+ async updateSnapshot(generation, update) {
260
+ if (generation !== this.generation) return;
261
+ this.snapshotValue = { ...this.snapshotValue, ...update };
262
+ await this.emit();
263
+ }
264
+ resetSnapshot() {
265
+ this.snapshotValue = {
266
+ status: this.options.adapter.isConfigured(this.configuration) ? "offline" : "not-configured",
267
+ generation: this.generation,
268
+ components: emptyComponents()
269
+ };
270
+ void this.emit();
271
+ }
272
+ async emit() {
273
+ await this.options.onStatus?.(this.snapshot);
274
+ }
275
+ };
276
+
51
277
  // src/orchestration/errors.ts
52
278
  var OrchestrationError = class extends Error {
53
279
  code;
@@ -76,9 +302,222 @@ var OrchestrationError = class extends Error {
76
302
  }
77
303
  };
78
304
 
305
+ // src/device-network/jsonFrame.ts
306
+ var JsonFrameError = class extends Error {
307
+ code;
308
+ constructor(code, message, options) {
309
+ super(message, options);
310
+ this.name = "JsonFrameError";
311
+ this.code = code;
312
+ }
313
+ };
314
+ var HEADER_BYTES = 4;
315
+ var MAX_UINT32 = 4294967295;
316
+ function assertPositiveInteger(value, name) {
317
+ if (!Number.isSafeInteger(value) || value <= 0) {
318
+ throw new TypeError(`${name} must be a positive safe integer`);
319
+ }
320
+ }
321
+ function validateMaxPayloadBytes(maxPayloadBytes) {
322
+ assertPositiveInteger(maxPayloadBytes, "maxPayloadBytes");
323
+ if (maxPayloadBytes > MAX_UINT32) {
324
+ throw new TypeError("maxPayloadBytes must fit in an unsigned 32-bit frame length");
325
+ }
326
+ }
327
+ function encodeJsonFrame(value, maxPayloadBytes = MAX_UINT32) {
328
+ validateMaxPayloadBytes(maxPayloadBytes);
329
+ let serialized;
330
+ try {
331
+ serialized = JSON.stringify(value);
332
+ } catch (error) {
333
+ throw new JsonFrameError("INVALID_JSON", "JSON frame value cannot be serialized", {
334
+ cause: error
335
+ });
336
+ }
337
+ if (serialized === void 0) {
338
+ throw new JsonFrameError("INVALID_JSON", "JSON frame value cannot be serialized");
339
+ }
340
+ const payload = new TextEncoder().encode(serialized);
341
+ if (payload.byteLength > maxPayloadBytes) {
342
+ throw new JsonFrameError(
343
+ "FRAME_TOO_LARGE",
344
+ `JSON frame payload exceeds ${maxPayloadBytes} bytes`
345
+ );
346
+ }
347
+ const frame = new Uint8Array(HEADER_BYTES + payload.byteLength);
348
+ new DataView(frame.buffer, frame.byteOffset, HEADER_BYTES).setUint32(0, payload.byteLength, false);
349
+ frame.set(payload, HEADER_BYTES);
350
+ return frame;
351
+ }
352
+ async function* encodeJsonFrames(values, maxPayloadBytes = MAX_UINT32) {
353
+ for await (const value of values) {
354
+ yield encodeJsonFrame(value, maxPayloadBytes);
355
+ }
356
+ }
357
+ function concatBytes(left, right) {
358
+ if (left.byteLength === 0) return right.slice();
359
+ if (right.byteLength === 0) return left;
360
+ const combined = new Uint8Array(left.byteLength + right.byteLength);
361
+ combined.set(left);
362
+ combined.set(right, left.byteLength);
363
+ return combined;
364
+ }
365
+ function abortedError(signal) {
366
+ return new JsonFrameError("ABORTED", "JSON frame reading was aborted", {
367
+ cause: signal?.reason
368
+ });
369
+ }
370
+ async function readNextChunk(iterator, idleDeadline, totalDeadline, signal) {
371
+ if (signal?.aborted) throw abortedError(signal);
372
+ const now = Date.now();
373
+ const deadline = totalDeadline === void 0 ? idleDeadline : Math.min(idleDeadline, totalDeadline);
374
+ if (deadline <= now) {
375
+ throw new JsonFrameError(
376
+ totalDeadline !== void 0 && totalDeadline <= idleDeadline ? "TOTAL_TIMEOUT" : "IDLE_TIMEOUT",
377
+ totalDeadline !== void 0 && totalDeadline <= idleDeadline ? "JSON frame total timeout expired" : "JSON frame idle timeout expired"
378
+ );
379
+ }
380
+ return new Promise((resolve, reject) => {
381
+ let settled = false;
382
+ const finish = (callback) => {
383
+ if (settled) return;
384
+ settled = true;
385
+ clearTimeout(timer);
386
+ signal?.removeEventListener("abort", onAbort);
387
+ callback();
388
+ };
389
+ const onAbort = () => {
390
+ finish(() => {
391
+ reject(abortedError(signal));
392
+ });
393
+ };
394
+ const timer = setTimeout(() => {
395
+ const currentTime = Date.now();
396
+ const totalExpired = totalDeadline !== void 0 && currentTime >= totalDeadline;
397
+ finish(() => {
398
+ reject(
399
+ new JsonFrameError(
400
+ totalExpired ? "TOTAL_TIMEOUT" : "IDLE_TIMEOUT",
401
+ totalExpired ? "JSON frame total timeout expired" : "JSON frame idle timeout expired"
402
+ )
403
+ );
404
+ });
405
+ }, Math.max(1, deadline - now));
406
+ signal?.addEventListener("abort", onAbort, { once: true });
407
+ void iterator.next().then(
408
+ (result) => {
409
+ finish(() => {
410
+ resolve(result);
411
+ });
412
+ },
413
+ (error) => {
414
+ finish(() => {
415
+ reject(
416
+ error instanceof Error ? error : new Error("JSON frame source failed", {
417
+ cause: error
418
+ })
419
+ );
420
+ });
421
+ }
422
+ );
423
+ });
424
+ }
425
+ function createJsonFrameReader(source, options) {
426
+ validateMaxPayloadBytes(options.maxPayloadBytes);
427
+ assertPositiveInteger(options.idleTimeoutMs, "idleTimeoutMs");
428
+ assertPositiveInteger(options.totalTimeoutMs, "totalTimeoutMs");
429
+ return (async function* readFrames() {
430
+ const iterator = source[Symbol.asyncIterator]();
431
+ let buffered = new Uint8Array(0);
432
+ let expectedPayloadBytes;
433
+ let idleDeadline = Date.now() + options.idleTimeoutMs;
434
+ let totalDeadline;
435
+ let abortCalled = false;
436
+ const abortOnce = async (error) => {
437
+ if (abortCalled) return;
438
+ abortCalled = true;
439
+ await Promise.resolve(options.abort?.(error)).catch(() => void 0);
440
+ };
441
+ try {
442
+ for (; ; ) {
443
+ if (expectedPayloadBytes === void 0 && buffered.byteLength >= HEADER_BYTES) {
444
+ expectedPayloadBytes = new DataView(
445
+ buffered.buffer,
446
+ buffered.byteOffset,
447
+ HEADER_BYTES
448
+ ).getUint32(0, false);
449
+ buffered = buffered.slice(HEADER_BYTES);
450
+ totalDeadline ??= Date.now() + options.totalTimeoutMs;
451
+ if (expectedPayloadBytes > options.maxPayloadBytes) {
452
+ throw new JsonFrameError(
453
+ "FRAME_TOO_LARGE",
454
+ `JSON frame payload exceeds ${options.maxPayloadBytes} bytes`
455
+ );
456
+ }
457
+ }
458
+ if (expectedPayloadBytes !== void 0 && buffered.byteLength >= expectedPayloadBytes) {
459
+ const payload = buffered.slice(0, expectedPayloadBytes);
460
+ buffered = buffered.slice(expectedPayloadBytes);
461
+ expectedPayloadBytes = void 0;
462
+ totalDeadline = buffered.byteLength > 0 ? Date.now() + options.totalTimeoutMs : void 0;
463
+ let text;
464
+ try {
465
+ text = new TextDecoder("utf-8", { fatal: true }).decode(payload);
466
+ } catch (error) {
467
+ throw new JsonFrameError("INVALID_UTF8", "JSON frame payload is not valid UTF-8", {
468
+ cause: error
469
+ });
470
+ }
471
+ let value;
472
+ try {
473
+ value = JSON.parse(text);
474
+ } catch (error) {
475
+ throw new JsonFrameError("INVALID_JSON", "JSON frame payload is not valid JSON", {
476
+ cause: error
477
+ });
478
+ }
479
+ yield value;
480
+ idleDeadline = Date.now() + options.idleTimeoutMs;
481
+ continue;
482
+ }
483
+ const result = await readNextChunk(
484
+ iterator,
485
+ idleDeadline,
486
+ totalDeadline,
487
+ options.signal
488
+ );
489
+ if (result.done) {
490
+ if (buffered.byteLength > 0 || expectedPayloadBytes !== void 0) {
491
+ throw new JsonFrameError("TRUNCATED_FRAME", "JSON frame stream ended mid-frame");
492
+ }
493
+ return;
494
+ }
495
+ if (!(result.value instanceof Uint8Array)) {
496
+ throw new TypeError("JSON frame source must yield Uint8Array chunks");
497
+ }
498
+ if (result.value.byteLength === 0) continue;
499
+ const now = Date.now();
500
+ idleDeadline = now + options.idleTimeoutMs;
501
+ totalDeadline ??= now + options.totalTimeoutMs;
502
+ buffered = concatBytes(buffered, result.value);
503
+ }
504
+ } catch (error) {
505
+ const frameError = error instanceof JsonFrameError ? error : new JsonFrameError("ABORTED", "JSON frame source failed", { cause: error });
506
+ await abortOnce(frameError);
507
+ throw frameError;
508
+ } finally {
509
+ void iterator.return?.().catch(() => void 0);
510
+ }
511
+ })();
512
+ }
513
+
79
514
  // src/device-network/deviceOrchestrationTransport.ts
80
- var DEVICE_ORCHESTRATION_PROTOCOL = "/memeloop/orchestration/1.0.0";
515
+ var DEVICE_ORCHESTRATION_PROTOCOL = "/memeloop/orchestration/2.0.0";
81
516
  var DEFAULT_MAX_FRAME_BYTES = 1024 * 1024;
517
+ var REQUEST_IDLE_TIMEOUT_MS = 1e4;
518
+ var REQUEST_TOTAL_TIMEOUT_MS = 3e4;
519
+ var WATCH_IDLE_TIMEOUT_MS = 9e4;
520
+ var WATCH_BOOKMARK_INTERVAL_MS = 3e4;
82
521
  function orchestrationTransportError(code, message) {
83
522
  return new OrchestrationError({
84
523
  code,
@@ -91,65 +530,6 @@ function assertFrameLimit(limit) {
91
530
  throw new TypeError("maxFrameBytes must be a safe integer of at least 256");
92
531
  }
93
532
  }
94
- async function* encodeLines(values, maxFrameBytes) {
95
- const encoder = new TextEncoder();
96
- for await (const value of values) {
97
- const frame = encoder.encode(`${JSON.stringify(value)}
98
- `);
99
- if (frame.byteLength > maxFrameBytes) {
100
- throw orchestrationTransportError(
101
- "EXHAUSTED",
102
- `device orchestration frame exceeds ${maxFrameBytes} bytes`
103
- );
104
- }
105
- yield frame;
106
- }
107
- }
108
- async function* decodeLines(source, maxFrameBytes) {
109
- const decoder = new TextDecoder();
110
- let buffered = "";
111
- for await (const chunk of source) {
112
- buffered += decoder.decode(chunk, { stream: true });
113
- for (; ; ) {
114
- const newline = buffered.indexOf("\n");
115
- if (newline < 0) break;
116
- const line = buffered.slice(0, newline);
117
- buffered = buffered.slice(newline + 1);
118
- if (!line) continue;
119
- if (new TextEncoder().encode(line).byteLength > maxFrameBytes) {
120
- throw orchestrationTransportError(
121
- "EXHAUSTED",
122
- `device orchestration frame exceeds ${maxFrameBytes} bytes`
123
- );
124
- }
125
- try {
126
- yield JSON.parse(line);
127
- } catch {
128
- throw orchestrationTransportError(
129
- "INVALID",
130
- "device orchestration stream contains invalid JSON"
131
- );
132
- }
133
- }
134
- if (new TextEncoder().encode(buffered).byteLength > maxFrameBytes) {
135
- throw orchestrationTransportError(
136
- "EXHAUSTED",
137
- `device orchestration frame exceeds ${maxFrameBytes} bytes`
138
- );
139
- }
140
- }
141
- buffered += decoder.decode();
142
- if (buffered.trim()) {
143
- try {
144
- yield JSON.parse(buffered);
145
- } catch {
146
- throw orchestrationTransportError(
147
- "INVALID",
148
- "device orchestration stream contains invalid trailing JSON"
149
- );
150
- }
151
- }
152
- }
153
533
  function throwIfAborted(signal) {
154
534
  if (signal?.aborted) {
155
535
  throw orchestrationTransportError("CANCELLED", "device orchestration request was cancelled");
@@ -168,11 +548,41 @@ async function openAuthenticatedStream(options) {
168
548
  }
169
549
  async function writeRequest(stream, request, grant, maxFrameBytes) {
170
550
  const envelope = {
171
- type: "memeloop-device-orchestration-request-v1",
551
+ type: "memeloop-device-orchestration-request-v2",
172
552
  request,
173
553
  ...grant ? { grant } : {}
174
554
  };
175
- await stream.sink(encodeLines([envelope], maxFrameBytes));
555
+ await stream.sink(encodeJsonFrames([envelope], maxFrameBytes));
556
+ }
557
+ function abortStreamOnce(stream) {
558
+ let aborted = false;
559
+ return async (error) => {
560
+ if (aborted) return;
561
+ aborted = true;
562
+ await stream.abort(error);
563
+ };
564
+ }
565
+ function frameReader(stream, maxFrameBytes, idleTimeoutMs, signal, abort) {
566
+ return createJsonFrameReader(stream.source, {
567
+ maxPayloadBytes: maxFrameBytes,
568
+ idleTimeoutMs,
569
+ totalTimeoutMs: REQUEST_TOTAL_TIMEOUT_MS,
570
+ signal,
571
+ abort
572
+ });
573
+ }
574
+ function mapFrameError(error) {
575
+ if (!(error instanceof JsonFrameError)) throw error;
576
+ if (error.code === "ABORTED") {
577
+ throw orchestrationTransportError("CANCELLED", error.message);
578
+ }
579
+ if (error.code === "FRAME_TOO_LARGE") {
580
+ throw orchestrationTransportError("EXHAUSTED", error.message);
581
+ }
582
+ if (error.code === "IDLE_TIMEOUT" || error.code === "TOTAL_TIMEOUT") {
583
+ throw orchestrationTransportError("UNAVAILABLE", error.message);
584
+ }
585
+ throw orchestrationTransportError("INVALID", error.message);
176
586
  }
177
587
  function createDeviceOrchestrationTransport(options) {
178
588
  const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
@@ -181,8 +591,9 @@ function createDeviceOrchestrationTransport(options) {
181
591
  async request(request, transportOptions = {}) {
182
592
  throwIfAborted(transportOptions.signal);
183
593
  const { stream, grant } = await openAuthenticatedStream(options);
594
+ const abort = abortStreamOnce(stream);
184
595
  const closeOnAbort = () => {
185
- void stream.close();
596
+ void abort(orchestrationTransportError("CANCELLED", "device orchestration request was cancelled"));
186
597
  };
187
598
  transportOptions.signal?.addEventListener("abort", closeOnAbort, {
188
599
  once: true
@@ -190,7 +601,13 @@ function createDeviceOrchestrationTransport(options) {
190
601
  try {
191
602
  await writeRequest(stream, request, grant, maxFrameBytes);
192
603
  let response;
193
- for await (const value of decodeLines(stream.source, maxFrameBytes)) {
604
+ for await (const value of frameReader(
605
+ stream,
606
+ maxFrameBytes,
607
+ REQUEST_IDLE_TIMEOUT_MS,
608
+ transportOptions.signal,
609
+ abort
610
+ )) {
194
611
  if (response) {
195
612
  throw orchestrationTransportError(
196
613
  "INVALID",
@@ -207,6 +624,9 @@ function createDeviceOrchestrationTransport(options) {
207
624
  );
208
625
  }
209
626
  return response;
627
+ } catch (error) {
628
+ if (error instanceof JsonFrameError) await abort(error);
629
+ mapFrameError(error);
210
630
  } finally {
211
631
  transportOptions.signal?.removeEventListener("abort", closeOnAbort);
212
632
  await stream.close().catch(() => void 0);
@@ -215,19 +635,29 @@ function createDeviceOrchestrationTransport(options) {
215
635
  async *watch(request, transportOptions = {}) {
216
636
  throwIfAborted(transportOptions.signal);
217
637
  const { stream, grant } = await openAuthenticatedStream(options);
638
+ const abort = abortStreamOnce(stream);
218
639
  const closeOnAbort = () => {
219
- void stream.close();
640
+ void abort(orchestrationTransportError("CANCELLED", "device orchestration watch was cancelled"));
220
641
  };
221
642
  transportOptions.signal?.addEventListener("abort", closeOnAbort, {
222
643
  once: true
223
644
  });
224
645
  try {
225
646
  await writeRequest(stream, request, grant, maxFrameBytes);
226
- for await (const value of decodeLines(stream.source, maxFrameBytes)) {
647
+ for await (const value of frameReader(
648
+ stream,
649
+ maxFrameBytes,
650
+ WATCH_IDLE_TIMEOUT_MS,
651
+ transportOptions.signal,
652
+ abort
653
+ )) {
227
654
  throwIfAborted(transportOptions.signal);
228
655
  yield value;
229
656
  }
230
657
  throwIfAborted(transportOptions.signal);
658
+ } catch (error) {
659
+ if (error instanceof JsonFrameError) await abort(error);
660
+ mapFrameError(error);
231
661
  } finally {
232
662
  transportOptions.signal?.removeEventListener("abort", closeOnAbort);
233
663
  await stream.close().catch(() => void 0);
@@ -236,15 +666,22 @@ function createDeviceOrchestrationTransport(options) {
236
666
  };
237
667
  }
238
668
  async function readSingleRequest(stream, maxFrameBytes) {
669
+ const abort = abortStreamOnce(stream);
239
670
  let envelope;
240
- for await (const value of decodeLines(stream.source, maxFrameBytes)) {
671
+ for await (const value of frameReader(
672
+ stream,
673
+ maxFrameBytes,
674
+ REQUEST_IDLE_TIMEOUT_MS,
675
+ void 0,
676
+ abort
677
+ )) {
241
678
  if (envelope) {
242
679
  throw orchestrationTransportError(
243
680
  "INVALID",
244
681
  "device orchestration stream accepts exactly one request"
245
682
  );
246
683
  }
247
- if (value === null || typeof value !== "object" || Array.isArray(value) || value.type !== "memeloop-device-orchestration-request-v1" || value.request === null || typeof value.request !== "object") {
684
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value.type !== "memeloop-device-orchestration-request-v2" || value.request === null || typeof value.request !== "object") {
248
685
  throw orchestrationTransportError(
249
686
  "INVALID",
250
687
  "device orchestration request envelope is invalid"
@@ -257,6 +694,41 @@ async function readSingleRequest(stream, maxFrameBytes) {
257
694
  }
258
695
  return envelope;
259
696
  }
697
+ async function* withWatchBookmarks(source, request, signal) {
698
+ const iterator = source[Symbol.asyncIterator]();
699
+ let pending = iterator.next();
700
+ try {
701
+ for (; ; ) {
702
+ const result = await Promise.race([
703
+ pending.then((value) => ({ kind: "value", value })),
704
+ new Promise((resolve) => {
705
+ const timer = setTimeout(() => {
706
+ resolve({ kind: "bookmark" });
707
+ }, WATCH_BOOKMARK_INTERVAL_MS);
708
+ signal.addEventListener("abort", () => {
709
+ clearTimeout(timer);
710
+ resolve({ kind: "bookmark" });
711
+ }, { once: true });
712
+ })
713
+ ]);
714
+ if (signal.aborted) return;
715
+ if (result.kind === "bookmark") {
716
+ yield {
717
+ protocol: request.protocol,
718
+ requestId: request.requestId,
719
+ ok: true,
720
+ result: { type: "BOOKMARK", resourceVersion: String(Date.now()) }
721
+ };
722
+ continue;
723
+ }
724
+ if (result.value.done) return;
725
+ yield result.value.value;
726
+ pending = iterator.next();
727
+ }
728
+ } finally {
729
+ void iterator.return?.();
730
+ }
731
+ }
260
732
  function createDeviceOrchestrationStreamHandler(options) {
261
733
  const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
262
734
  assertFrameLimit(maxFrameBytes);
@@ -271,10 +743,13 @@ function createDeviceOrchestrationStreamHandler(options) {
271
743
  const handler = await options.resolveHandler(remotePeerId);
272
744
  if (request.operation === "watch") {
273
745
  await stream.sink(
274
- encodeLines(handler.watch(request, { signal: abort.signal }), maxFrameBytes)
746
+ encodeJsonFrames(
747
+ withWatchBookmarks(handler.watch(request, { signal: abort.signal }), request, abort.signal),
748
+ maxFrameBytes
749
+ )
275
750
  );
276
751
  } else {
277
- await stream.sink(encodeLines([await handler.request(request)], maxFrameBytes));
752
+ await stream.sink(encodeJsonFrames([await handler.request(request)], maxFrameBytes));
278
753
  }
279
754
  } finally {
280
755
  abort.abort();
@@ -283,9 +758,23 @@ function createDeviceOrchestrationStreamHandler(options) {
283
758
  };
284
759
  }
285
760
 
761
+ // src/device-network/deviceSyncStateStore.ts
762
+ var MemoryDeviceSyncStateStore = class {
763
+ versionVector;
764
+ constructor(initial = {}) {
765
+ this.versionVector = { ...initial };
766
+ }
767
+ async loadVersionVector() {
768
+ return { ...this.versionVector };
769
+ }
770
+ async saveVersionVector(versionVector) {
771
+ this.versionVector = { ...versionVector };
772
+ }
773
+ };
774
+
286
775
  // src/device-network/libp2pSyncProtocol.ts
287
- var LIBP2P_SYNC_REQUEST_TYPE = "memeloop-sync-request-v1";
288
- var LIBP2P_SYNC_RESPONSE_TYPE = "memeloop-sync-response-v1";
776
+ var LIBP2P_SYNC_REQUEST_TYPE = "memeloop-sync-request-v2";
777
+ var LIBP2P_SYNC_RESPONSE_TYPE = "memeloop-sync-response-v2";
289
778
  var syncMethods = /* @__PURE__ */ new Set([
290
779
  "exchangeVersionVector",
291
780
  "pullMissingMetadata",
@@ -345,8 +834,11 @@ var Libp2pDeviceSyncTransport = class {
345
834
  exchangeVersionVector(peerId, localVersion) {
346
835
  return this.request(peerId, "exchangeVersionVector", { localVersion });
347
836
  }
348
- pullMissingMetadata(peerId, sinceVersion) {
349
- return this.request(peerId, "pullMissingMetadata", { sinceVersion });
837
+ pullMissingMetadata(peerId, sinceVersion, cursor) {
838
+ return this.request(peerId, "pullMissingMetadata", {
839
+ sinceVersion,
840
+ cursor
841
+ });
350
842
  }
351
843
  pullMissingMessages(peerId, conversationId, knownMessageIds) {
352
844
  return this.request(peerId, "pullMissingMessages", { conversationId, knownMessageIds });
@@ -357,7 +849,7 @@ var Libp2pDeviceSyncTransport = class {
357
849
  }
358
850
  async request(peerId, method, parameters) {
359
851
  const grant = await this.grantProvider?.(peerId);
360
- const stream = await this.deviceNetwork.openStream(peerId, "/memeloop/sync/1.0.0", grant);
852
+ const stream = await this.deviceNetwork.openStream(peerId, "/memeloop/sync/2.0.0", grant);
361
853
  const request = {
362
854
  type: LIBP2P_SYNC_REQUEST_TYPE,
363
855
  id: crypto.randomUUID(),
@@ -365,31 +857,34 @@ var Libp2pDeviceSyncTransport = class {
365
857
  params: parameters,
366
858
  grant
367
859
  };
368
- await writeJsonToStream(stream, request);
369
- const response = await readJsonFromStream(stream);
370
- await stream.close();
371
- if (!isLibp2pSyncResponse(response)) throw new Error("invalid_sync_response");
372
- if (response.id !== request.id) throw new Error("sync_response_id_mismatch");
373
- if (!response.ok) throw new Error(response.error);
374
- return response.result;
860
+ try {
861
+ await stream.sink(encodeJsonFrames([request], 16 * 1024 * 1024));
862
+ let response;
863
+ for await (const value of createJsonFrameReader(stream.source, {
864
+ maxPayloadBytes: 16 * 1024 * 1024,
865
+ idleTimeoutMs: 15e3,
866
+ totalTimeoutMs: 12e4,
867
+ abort: async (error) => stream.abort(error)
868
+ })) {
869
+ if (response !== void 0) throw new Error("sync_response_multiple");
870
+ response = value;
871
+ }
872
+ if (!isLibp2pSyncResponse(response)) throw new Error("invalid_sync_response");
873
+ if (response.id !== request.id) throw new Error("sync_response_id_mismatch");
874
+ if (!response.ok) throw new Error(response.error);
875
+ return response.result;
876
+ } catch (error) {
877
+ if (error instanceof JsonFrameError) await stream.abort(error);
878
+ throw error;
879
+ } finally {
880
+ await stream.close().catch(() => void 0);
881
+ }
375
882
  }
376
883
  };
377
- async function writeJsonToStream(stream, message) {
378
- const payload = new TextEncoder().encode(JSON.stringify(message));
379
- await stream.sink((async function* () {
380
- yield payload;
381
- })());
382
- }
383
- async function readJsonFromStream(stream) {
384
- const reader = stream.source[Symbol.asyncIterator]();
385
- const result = await reader.next();
386
- if (result.done || !result.value) throw new Error("sync_response_missing");
387
- return JSON.parse(new TextDecoder().decode(result.value));
388
- }
389
884
 
390
885
  // src/device-network/libp2pRpcProtocol.ts
391
- var LIBP2P_RPC_REQUEST_TYPE = "memeloop-rpc-request-v1";
392
- var LIBP2P_RPC_RESPONSE_TYPE = "memeloop-rpc-response-v1";
886
+ var LIBP2P_RPC_REQUEST_TYPE = "memeloop-rpc-request-v2";
887
+ var LIBP2P_RPC_RESPONSE_TYPE = "memeloop-rpc-response-v2";
393
888
  function isLibp2pRpcRequest(value) {
394
889
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
395
890
  const record = value;
@@ -404,7 +899,7 @@ function isLibp2pRpcResponse(value) {
404
899
  }
405
900
 
406
901
  // src/device-network/localTrustDeviceAuthorizer.ts
407
- var PAIRING_PROTOCOL = "/memeloop/pairing/1.0.0";
902
+ var PAIRING_PROTOCOL = "/memeloop/pairing/2.0.0";
408
903
  var LocalTrustDeviceAuthorizer = class {
409
904
  constructor(options = {}) {
410
905
  this.options = options;
@@ -430,7 +925,7 @@ var LocalTrustDeviceAuthorizer = class {
430
925
  };
431
926
 
432
927
  // src/device-network/pairingInvite.ts
433
- var DEVICE_PAIRING_INVITE_PROTOCOL = "memeloop-device-pairing-v1";
928
+ var DEVICE_PAIRING_INVITE_PROTOCOL = "memeloop-device-pairing-v2";
434
929
  var DEVICE_PAIRING_INVITE_TTL_MS = 5 * 6e4;
435
930
  var MAX_INVITE_LENGTH = 16 * 1024;
436
931
  var MAX_ADDRESSES = 8;
@@ -441,37 +936,82 @@ function requireText(value, name, maxLength) {
441
936
  }
442
937
  return trimmed;
443
938
  }
444
- function validateAddresses(addresses) {
939
+ function finalPeerId(address) {
940
+ const parts = address.split("/");
941
+ for (let index = parts.length - 2; index >= 0; index -= 1) {
942
+ if (parts[index] === "p2p") return parts[index + 1];
943
+ }
944
+ return void 0;
945
+ }
946
+ function validateAddresses(addresses, peerId) {
445
947
  const unique = [...new Set(addresses.map((address) => address.trim()))].filter(Boolean);
446
948
  if (unique.length === 0 || unique.length > MAX_ADDRESSES || unique.some(
447
- (address) => address.length > 2048 || !address.startsWith("/") || !address.includes("/ws") && !address.includes("/wss")
949
+ (address) => address.length > 2048 || !address.startsWith("/") || !address.includes("/ws") && !address.includes("/wss") || finalPeerId(address) !== peerId
448
950
  )) {
449
- throw new Error("device pairing invite requires WebSocket multiaddrs");
951
+ throw new Error("device pairing invite requires PeerId-bound WebSocket multiaddrs");
450
952
  }
451
953
  return unique;
452
954
  }
453
- function createDevicePairingInvite(device, options = {}) {
955
+ function validateLifetime(createdAt, expiresAt, now) {
956
+ if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(expiresAt) || expiresAt <= createdAt || expiresAt - createdAt > DEVICE_PAIRING_INVITE_TTL_MS) {
957
+ throw new Error("invalid device pairing invite lifetime");
958
+ }
959
+ if (expiresAt <= now) throw new Error("device pairing invite has expired");
960
+ }
961
+ function unsignedPayload(invite) {
962
+ const peerId = requireText(invite.peerId, "peerId", 256);
963
+ return {
964
+ protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
965
+ peerId,
966
+ publicKeyMultibase: requireText(invite.publicKeyMultibase, "publicKeyMultibase", 4096),
967
+ deviceName: requireText(invite.deviceName, "deviceName", 256),
968
+ multiaddrs: [...validateAddresses(invite.multiaddrs, peerId)].sort(),
969
+ createdAt: invite.createdAt,
970
+ expiresAt: invite.expiresAt
971
+ };
972
+ }
973
+ function canonicalDevicePairingInviteBytes(invite) {
974
+ const payload = unsignedPayload(invite);
975
+ const canonical = [
976
+ DEVICE_PAIRING_INVITE_PROTOCOL,
977
+ `peerId=${payload.peerId}`,
978
+ `publicKey=${payload.publicKeyMultibase}`,
979
+ `deviceName=${payload.deviceName}`,
980
+ `multiaddrs=${payload.multiaddrs.join(",")}`,
981
+ `createdAt=${payload.createdAt}`,
982
+ `expiresAt=${payload.expiresAt}`
983
+ ].join("\n");
984
+ return new TextEncoder().encode(canonical);
985
+ }
986
+ async function createDevicePairingInvite(device, options) {
454
987
  const createdAt = options.now ?? Date.now();
455
988
  const ttlMs = options.ttlMs ?? DEVICE_PAIRING_INVITE_TTL_MS;
456
- if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > DEVICE_PAIRING_INVITE_TTL_MS) {
989
+ if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
457
990
  throw new Error("invalid device pairing invite lifetime");
458
991
  }
459
- return {
992
+ const payload = unsignedPayload({
460
993
  protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
461
- peerId: requireText(device.peerId, "peerId", 256),
462
- deviceName: requireText(device.displayName, "deviceName", 256),
463
- multiaddrs: validateAddresses(device.multiaddrs ?? []),
994
+ peerId: device.peerId,
995
+ publicKeyMultibase: device.publicKeyMultibase,
996
+ deviceName: device.displayName,
997
+ multiaddrs: device.multiaddrs ?? [],
464
998
  createdAt,
465
999
  expiresAt: createdAt + ttlMs
466
- };
1000
+ });
1001
+ validateLifetime(payload.createdAt, payload.expiresAt, createdAt - 1);
1002
+ const signature = requireText(
1003
+ await options.sign(canonicalDevicePairingInviteBytes(payload)),
1004
+ "signature",
1005
+ 4096
1006
+ );
1007
+ return { ...payload, signature };
467
1008
  }
468
1009
  function encodeDevicePairingInvite(invite) {
469
- const validated = parseDevicePairingInvite(JSON.stringify(invite), {
470
- now: invite.createdAt
471
- });
472
- return JSON.stringify(validated);
1010
+ const payload = unsignedPayload(invite);
1011
+ const signature = requireText(invite.signature, "signature", 4096);
1012
+ return JSON.stringify({ ...payload, signature });
473
1013
  }
474
- function parseDevicePairingInvite(serialized, options = {}) {
1014
+ async function parseDevicePairingInvite(serialized, options) {
475
1015
  if (!serialized || serialized.length > MAX_INVITE_LENGTH) {
476
1016
  throw new Error("invalid device pairing invite length");
477
1017
  }
@@ -485,20 +1025,30 @@ function parseDevicePairingInvite(serialized, options = {}) {
485
1025
  throw new Error("invalid device pairing invite");
486
1026
  }
487
1027
  const record = parsed;
488
- if (record.protocol !== DEVICE_PAIRING_INVITE_PROTOCOL || typeof record.peerId !== "string" || typeof record.deviceName !== "string" || !Array.isArray(record.multiaddrs) || record.multiaddrs.some((address) => typeof address !== "string") || typeof record.createdAt !== "number" || typeof record.expiresAt !== "number" || !Number.isSafeInteger(record.createdAt) || !Number.isSafeInteger(record.expiresAt) || record.expiresAt <= record.createdAt || record.expiresAt - record.createdAt > DEVICE_PAIRING_INVITE_TTL_MS) {
1028
+ if (record.protocol !== DEVICE_PAIRING_INVITE_PROTOCOL || typeof record.peerId !== "string" || typeof record.publicKeyMultibase !== "string" || typeof record.deviceName !== "string" || !Array.isArray(record.multiaddrs) || record.multiaddrs.some((address) => typeof address !== "string") || typeof record.createdAt !== "number" || typeof record.expiresAt !== "number" || typeof record.signature !== "string") {
489
1029
  throw new Error("invalid device pairing invite");
490
1030
  }
491
- if (record.expiresAt <= (options.now ?? Date.now())) {
492
- throw new Error("device pairing invite has expired");
493
- }
494
- return {
1031
+ validateLifetime(record.createdAt, record.expiresAt, options.now ?? Date.now());
1032
+ const payload = unsignedPayload({
495
1033
  protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
496
- peerId: requireText(record.peerId, "peerId", 256),
497
- deviceName: requireText(record.deviceName, "deviceName", 256),
498
- multiaddrs: validateAddresses(record.multiaddrs),
1034
+ peerId: record.peerId,
1035
+ publicKeyMultibase: record.publicKeyMultibase,
1036
+ deviceName: record.deviceName,
1037
+ multiaddrs: record.multiaddrs,
499
1038
  createdAt: record.createdAt,
500
1039
  expiresAt: record.expiresAt
1040
+ });
1041
+ const invite = {
1042
+ ...payload,
1043
+ signature: requireText(record.signature, "signature", 4096)
501
1044
  };
1045
+ if (!await options.verifyIdentity({
1046
+ invite,
1047
+ payload: canonicalDevicePairingInviteBytes(payload)
1048
+ })) {
1049
+ throw new Error("device pairing invite identity verification failed");
1050
+ }
1051
+ return invite;
502
1052
  }
503
1053
 
504
1054
  // src/sync/attachmentRpcCodec.ts
@@ -527,43 +1077,66 @@ var ChatSyncEngine = class {
527
1077
  nodeId;
528
1078
  storage;
529
1079
  getPeers;
1080
+ stateStore;
530
1081
  versionVector = {};
1082
+ loadStatePromise;
1083
+ stateMutation = Promise.resolve();
531
1084
  constructor(options) {
532
1085
  this.nodeId = options.nodeId;
533
1086
  this.storage = options.storage;
534
1087
  this.getPeers = options.peers;
535
- this.versionVector[this.nodeId] = 0;
1088
+ this.stateStore = options.stateStore ?? new MemoryDeviceSyncStateStore();
536
1089
  }
537
1090
  /**
538
1091
  * 在本节点新产生一条消息时调用,增加本节点的 lamportClock。
539
1092
  */
540
1093
  bumpLocalVersion() {
541
- this.versionVector[this.nodeId] = (this.versionVector[this.nodeId] ?? 0) + 1;
1094
+ let nextClock = 0;
1095
+ const mutation = this.stateMutation.then(async () => {
1096
+ await this.ensureStateLoaded();
1097
+ nextClock = (this.versionVector[this.nodeId] ?? 0) + 1;
1098
+ this.versionVector[this.nodeId] = nextClock;
1099
+ await this.stateStore.saveVersionVector(this.versionVector);
1100
+ });
1101
+ this.stateMutation = mutation.catch(() => void 0);
1102
+ return mutation.then(() => nextClock);
542
1103
  }
543
1104
  /**
544
1105
  * 执行一次与所有 peer 的增量同步(metadata 级别),并尽力拉取缺失消息。
545
1106
  */
546
1107
  async syncOnce() {
1108
+ await this.ensureStateLoaded();
1109
+ await this.stateMutation;
1110
+ await this.reconcileVersionVectorFromStorage();
547
1111
  const peers = this.getPeers();
548
1112
  if (peers.length === 0) return;
549
- const localVersion = { ...this.versionVector };
550
1113
  const pulledConversations = /* @__PURE__ */ new Set();
551
1114
  for (const peer of peers) {
552
- const { remoteVersion, missingForRemote } = await peer.exchangeVersionVector(localVersion);
553
- const missingForLocal = await peer.pullMissingMetadata(this.versionVector);
554
- for (const meta of missingForLocal) {
555
- await this.storage.upsertConversationMetadata(meta);
556
- pulledConversations.add(meta.conversationId);
557
- }
1115
+ const { remoteVersion } = await peer.exchangeVersionVector({ ...this.versionVector });
1116
+ let cursor;
1117
+ do {
1118
+ const page = await peer.pullMissingMetadata(this.versionVector, cursor);
1119
+ for (const meta of page.items) {
1120
+ await this.storage.upsertConversationMetadata(meta);
1121
+ pulledConversations.add(meta.conversationId);
1122
+ }
1123
+ cursor = page.nextCursor;
1124
+ } while (cursor !== void 0);
558
1125
  for (const [nodeId, clock] of Object.entries(remoteVersion)) {
559
1126
  const current = this.versionVector[nodeId] ?? 0;
560
- if (clock > current) {
561
- this.versionVector[nodeId] = clock;
562
- }
1127
+ if (clock > current) this.versionVector[nodeId] = clock;
563
1128
  }
564
- if (missingForRemote.length > 0) {
565
- this.bumpLocalVersion();
1129
+ await this.stateStore.saveVersionVector(this.versionVector);
1130
+ }
1131
+ const pageSize = 100;
1132
+ let offset = 0;
1133
+ for (; ; ) {
1134
+ const conversations = await this.storage.listConversations({ limit: pageSize, offset });
1135
+ for (const conversation of conversations) {
1136
+ pulledConversations.add(conversation.conversationId);
566
1137
  }
1138
+ if (conversations.length < pageSize) break;
1139
+ offset += conversations.length;
567
1140
  }
568
1141
  for (const conversationId of pulledConversations) {
569
1142
  await this.pullMessagesForConversationFromPeers(conversationId, peers);
@@ -574,11 +1147,6 @@ var ChatSyncEngine = class {
574
1147
  */
575
1148
  async antiEntropyOnce() {
576
1149
  await this.syncOnce();
577
- const list = await this.storage.listConversations({ limit: 500 });
578
- const peers = this.getPeers();
579
- for (const meta of list) {
580
- await this.pullMessagesForConversationFromPeers(meta.conversationId, peers);
581
- }
582
1150
  }
583
1151
  async pullMessagesForConversationFromPeers(conversationId, peers) {
584
1152
  const localMsgs = await this.storage.getMessages(conversationId, { mode: "full-content" });
@@ -631,12 +1199,39 @@ var ChatSyncEngine = class {
631
1199
  }
632
1200
  }
633
1201
  }
634
- getVersionVector() {
1202
+ async getVersionVector() {
1203
+ await this.ensureStateLoaded();
1204
+ await this.stateMutation;
635
1205
  return { ...this.versionVector };
636
1206
  }
637
1207
  getStorage() {
638
1208
  return this.storage;
639
1209
  }
1210
+ ensureStateLoaded() {
1211
+ this.loadStatePromise ??= (async () => {
1212
+ const stored = await this.stateStore.loadVersionVector();
1213
+ this.versionVector = { ...stored, [this.nodeId]: stored[this.nodeId] ?? 0 };
1214
+ })();
1215
+ return this.loadStatePromise;
1216
+ }
1217
+ async reconcileVersionVectorFromStorage() {
1218
+ const pageSize = 100;
1219
+ let offset = 0;
1220
+ let changed = false;
1221
+ for (; ; ) {
1222
+ const conversations = await this.storage.listConversations({ limit: pageSize, offset });
1223
+ for (const conversation of conversations) {
1224
+ const current = this.versionVector[conversation.originNodeId] ?? 0;
1225
+ if (conversation.originClock > current) {
1226
+ this.versionVector[conversation.originNodeId] = conversation.originClock;
1227
+ changed = true;
1228
+ }
1229
+ }
1230
+ if (conversations.length < pageSize) break;
1231
+ offset += conversations.length;
1232
+ }
1233
+ if (changed) await this.stateStore.saveVersionVector(this.versionVector);
1234
+ }
640
1235
  };
641
1236
 
642
1237
  // src/sync/peerNodeAdapter.ts
@@ -650,8 +1245,8 @@ var PeerNodeSyncAdapter = class {
650
1245
  exchangeVersionVector(localVersion) {
651
1246
  return this.transport.exchangeVersionVector(this.nodeId, localVersion);
652
1247
  }
653
- pullMissingMetadata(sinceVersion) {
654
- return this.transport.pullMissingMetadata(this.nodeId, sinceVersion);
1248
+ pullMissingMetadata(sinceVersion, cursor) {
1249
+ return this.transport.pullMissingMetadata(this.nodeId, sinceVersion, cursor);
655
1250
  }
656
1251
  pullMissingMessages(conversationId, knownMessageIds) {
657
1252
  const function_ = this.transport.pullMissingMessages?.bind(this.transport);
@@ -673,7 +1268,17 @@ var PeerNodeSyncAdapter = class {
673
1268
  function isConversationMeta(value) {
674
1269
  if (value === null || typeof value !== "object") return false;
675
1270
  const o = value;
676
- return typeof o.conversationId === "string" && typeof o.title === "string" && typeof o.originNodeId === "string" && typeof o.definitionId === "string" && typeof o.messageCount === "number" && typeof o.lastMessageTimestamp === "number" && typeof o.isUserInitiated === "boolean";
1271
+ return typeof o.conversationId === "string" && typeof o.title === "string" && typeof o.originNodeId === "string" && typeof o.originClock === "number" && typeof o.definitionId === "string" && typeof o.messageCount === "number" && typeof o.lastMessageTimestamp === "number" && typeof o.isUserInitiated === "boolean";
1272
+ }
1273
+ function computeMissingVersionRanges(currentVersion, availableVersion) {
1274
+ const ranges = [];
1275
+ for (const [originNodeId, availableClock] of Object.entries(availableVersion)) {
1276
+ const currentClock = currentVersion[originNodeId] ?? 0;
1277
+ if (availableClock > currentClock) {
1278
+ ranges.push({ originNodeId, fromExclusive: currentClock, toInclusive: availableClock });
1279
+ }
1280
+ }
1281
+ return ranges.sort((left, right) => left.originNodeId.localeCompare(right.originNodeId));
677
1282
  }
678
1283
 
679
1284
  // src/device-network/memoryDeviceNetworkService.ts
@@ -813,6 +1418,8 @@ var MemoryDeviceNetworkService = class {
813
1418
  async sink() {
814
1419
  },
815
1420
  async close() {
1421
+ },
1422
+ abort() {
816
1423
  }
817
1424
  };
818
1425
  }
@@ -847,11 +1454,21 @@ var MemoryDeviceNetworkService = class {
847
1454
  // src/device-network/syncCloudDevices.ts
848
1455
  async function syncCloudDevices(input) {
849
1456
  const cloudDevices = await input.cloudClient.listDevices();
850
- if (cloudDevices.length === 0) return [];
851
1457
  const existingRecords = await input.trustStore.loadTrustedDevices();
852
1458
  const existingByPeerId = new Map(existingRecords.map((r) => [r.peerId, r]));
853
- for (const cloudDevice of cloudDevices) {
1459
+ const excludedPeerIds = new Set(input.excludePeerIds ?? []);
1460
+ const activeCloudDevices = cloudDevices.filter(
1461
+ (device) => device.revokedAt === void 0 && !excludedPeerIds.has(device.peerId)
1462
+ );
1463
+ const visibleCloudPeerIds = new Set(activeCloudDevices.map((device) => device.peerId));
1464
+ for (const existing of existingRecords) {
1465
+ if (existing.trustMode === "cloud-account" && !visibleCloudPeerIds.has(existing.peerId)) {
1466
+ await input.trustStore.removeTrustedDevice(existing.peerId);
1467
+ }
1468
+ }
1469
+ for (const cloudDevice of activeCloudDevices) {
854
1470
  const existing = existingByPeerId.get(cloudDevice.peerId);
1471
+ if (existing?.trustMode === "local-pairing") continue;
855
1472
  const record = {
856
1473
  peerId: cloudDevice.peerId,
857
1474
  publicKeyMultibase: cloudDevice.publicKeyMultibase,
@@ -863,17 +1480,14 @@ async function syncCloudDevices(input) {
863
1480
  lastSeen: cloudDevice.lastSeen,
864
1481
  revokedAt: cloudDevice.revokedAt
865
1482
  };
866
- if (existing) {
867
- existing.lastSeen = cloudDevice.lastSeen;
868
- existing.revokedAt = cloudDevice.revokedAt;
869
- if (existing.publicKeyMultibase !== record.publicKeyMultibase || existing.deviceName !== record.deviceName || existing.platform !== record.platform) {
870
- await input.trustStore.saveTrustedDevice(record);
871
- }
872
- } else {
1483
+ if (!existing || !sameTrustedDevice(existing, record)) {
873
1484
  await input.trustStore.saveTrustedDevice(record);
874
1485
  }
875
1486
  }
876
- return cloudDevices;
1487
+ return activeCloudDevices;
1488
+ }
1489
+ function sameTrustedDevice(left, right) {
1490
+ return left.peerId === right.peerId && left.publicKeyMultibase === right.publicKeyMultibase && left.deviceName === right.deviceName && left.platform === right.platform && left.trustMode === right.trustMode && left.accountId === right.accountId && left.createdAt === right.createdAt && left.lastSeen === right.lastSeen && left.revokedAt === right.revokedAt;
877
1491
  }
878
1492
  // Annotate the CommonJS export names for ESM import in node:
879
1493
  0 && (module.exports = {
@@ -881,6 +1495,8 @@ async function syncCloudDevices(input) {
881
1495
  DEVICE_ORCHESTRATION_PROTOCOL,
882
1496
  DEVICE_PAIRING_INVITE_PROTOCOL,
883
1497
  DEVICE_PAIRING_INVITE_TTL_MS,
1498
+ DeviceCloudConnectionCoordinator,
1499
+ JsonFrameError,
884
1500
  LIBP2P_RPC_REQUEST_TYPE,
885
1501
  LIBP2P_RPC_RESPONSE_TYPE,
886
1502
  LIBP2P_SYNC_REQUEST_TYPE,
@@ -888,13 +1504,19 @@ async function syncCloudDevices(input) {
888
1504
  Libp2pDeviceSyncTransport,
889
1505
  LocalTrustDeviceAuthorizer,
890
1506
  MemoryDeviceNetworkService,
1507
+ MemoryDeviceSyncStateStore,
891
1508
  PeerNodeSyncAdapter,
892
1509
  attachmentBlobFromWire,
1510
+ canonicalDevicePairingInviteBytes,
1511
+ computeMissingVersionRanges,
893
1512
  createDeviceOrchestrationStreamHandler,
894
1513
  createDeviceOrchestrationTransport,
895
1514
  createDevicePairingInvite,
1515
+ createJsonFrameReader,
896
1516
  decodeAttachmentBlobRpc,
897
1517
  encodeDevicePairingInvite,
1518
+ encodeJsonFrame,
1519
+ encodeJsonFrames,
898
1520
  isConversationMeta,
899
1521
  isLibp2pRpcRequest,
900
1522
  isLibp2pRpcResponse,