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.
- package/dist/browser.d.cts +14 -14
- package/dist/browser.d.ts +14 -14
- package/dist/{chunk-5JEV7632.js → chunk-5VYZV33O.js} +2 -1
- package/dist/{chunk-5JEV7632.js.map → chunk-5VYZV33O.js.map} +1 -1
- package/dist/chunk-ECNKBH5A.js +1235 -0
- package/dist/chunk-ECNKBH5A.js.map +1 -0
- package/dist/{chunk-MRKY4HGF.js → chunk-EPHRIRAL.js} +20 -11
- package/dist/chunk-EPHRIRAL.js.map +1 -0
- package/dist/{chunk-P6L3444O.js → chunk-KV6TC5UJ.js} +2 -2
- package/dist/chunk-MXRXYGWW.js +1 -0
- package/dist/conversation.d.cts +3 -3
- package/dist/conversation.d.ts +3 -3
- package/dist/device-network-portable.cjs +757 -144
- package/dist/device-network-portable.cjs.map +1 -1
- package/dist/device-network-portable.d.cts +24 -14
- package/dist/device-network-portable.d.ts +24 -14
- package/dist/device-network-portable.js +18 -2
- package/dist/device-network.cjs +776 -154
- package/dist/device-network.cjs.map +1 -1
- package/dist/device-network.d.cts +5 -5
- package/dist/device-network.d.ts +5 -5
- package/dist/device-network.js +19 -3
- package/dist/{fetchProvider-WWnWnQFP.d.ts → fetchProvider-BhqRqi5-.d.ts} +1 -1
- package/dist/{fetchProvider-toTHy5Nu.d.cts → fetchProvider-HJ_FkORN.d.cts} +1 -1
- package/dist/index.cjs +782 -155
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -16
- package/dist/index.d.ts +16 -16
- package/dist/index.js +24 -6
- package/dist/index.js.map +1 -1
- package/dist/{interface-IrKBFtL1.d.ts → interface-3C3eG8Hn.d.cts} +13 -1
- package/dist/{interface-IrKBFtL1.d.cts → interface-3C3eG8Hn.d.ts} +13 -1
- package/dist/llm-providers.d.cts +2 -2
- package/dist/llm-providers.d.ts +2 -2
- package/dist/loop-api.cjs +1 -0
- package/dist/loop-api.cjs.map +1 -1
- package/dist/loop-api.d.cts +7 -7
- package/dist/loop-api.d.ts +7 -7
- package/dist/loop-api.js +2 -2
- package/dist/mobile.d.cts +5 -5
- package/dist/mobile.d.ts +5 -5
- package/dist/{orchestration-portable-BHB_YrNN.d.ts → orchestration-portable-BbkwLbC9.d.ts} +4 -4
- package/dist/{orchestration-portable-mvYBvGBb.d.cts → orchestration-portable-DNKzAtNq.d.cts} +4 -4
- package/dist/orchestration-portable.d.cts +5 -5
- package/dist/orchestration-portable.d.ts +5 -5
- package/dist/pairingInvite-DDx_-IAp.d.cts +271 -0
- package/dist/pairingInvite-Dkz8CLsb.d.ts +271 -0
- package/dist/{promptConcat-0WNFTg4l.d.ts → promptConcat-CUEIA_3T.d.ts} +3 -3
- package/dist/{promptConcat-D6wr5V-V.d.cts → promptConcat-VLe5SEN4.d.cts} +3 -3
- package/dist/{providerRegistry-CPsktuiX.d.cts → providerRegistry-BDGGs5gz.d.cts} +2 -2
- package/dist/{providerRegistry-Bi6pAvSj.d.ts → providerRegistry-CaFK_aUd.d.ts} +2 -2
- package/dist/{registry-D0kbxksn.d.cts → registry-DNSEVYug.d.cts} +3 -3
- package/dist/{registry-DDbvhlhs.d.ts → registry-yGnYHWhq.d.ts} +3 -3
- package/dist/{remoteClient-CBggnRy3.d.cts → remoteClient-BKmQEQY9.d.cts} +1 -1
- package/dist/{remoteClient-B_spK019.d.ts → remoteClient-D5hco6cS.d.ts} +1 -1
- package/dist/{runtime-UONPTAYK.js → runtime-6EELHWVB.js} +2 -2
- package/dist/{scriptDeploymentPipeline-CXJcC82H.d.ts → scriptDeploymentPipeline-B1q-UhrO.d.ts} +1 -1
- package/dist/{scriptDeploymentPipeline-yG1ZhVVI.d.cts → scriptDeploymentPipeline-XJzQyYys.d.cts} +1 -1
- package/dist/{syncCloudDevices-Ds_Kcgrv.d.cts → syncCloudDevices-BFw_c1g5.d.cts} +6 -5
- package/dist/{syncCloudDevices-BEqwgrhD.d.ts → syncCloudDevices-C8fCUhnX.d.ts} +6 -5
- package/dist/{types-CdFVSntj.d.ts → types-C8gnhG9M.d.cts} +6 -4
- package/dist/{types-7pYti-k9.d.cts → types-CcFFIk8W.d.ts} +6 -4
- package/package.json +1 -1
- package/dist/chunk-426CJUZP.js +0 -630
- package/dist/chunk-426CJUZP.js.map +0 -1
- package/dist/chunk-MRKY4HGF.js.map +0 -1
- package/dist/chunk-VZKQX5PP.js +0 -1
- package/dist/pairingInvite-BXPWY3aM.d.cts +0 -149
- package/dist/pairingInvite-Cuz0wuq9.d.ts +0 -149
- /package/dist/{chunk-P6L3444O.js.map → chunk-KV6TC5UJ.js.map} +0 -0
- /package/dist/{chunk-VZKQX5PP.js.map → chunk-MXRXYGWW.js.map} +0 -0
- /package/dist/{runtime-UONPTAYK.js.map → runtime-6EELHWVB.js.map} +0 -0
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OrchestrationError
|
|
3
|
+
} from "./chunk-FWSY5HOA.js";
|
|
4
|
+
|
|
5
|
+
// src/sync/attachmentRpcCodec.ts
|
|
6
|
+
function decodeAttachmentBlobRpc(result) {
|
|
7
|
+
if (result == null || typeof result !== "object") return null;
|
|
8
|
+
const r = result;
|
|
9
|
+
if (r.error || !r.found || typeof r.dataBase64 !== "string") return null;
|
|
10
|
+
let data;
|
|
11
|
+
try {
|
|
12
|
+
const binary = atob(r.dataBase64);
|
|
13
|
+
if (!binary.length) return null;
|
|
14
|
+
data = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
data,
|
|
20
|
+
filename: typeof r.filename === "string" ? r.filename : "attachment",
|
|
21
|
+
mimeType: typeof r.mimeType === "string" ? r.mimeType : "application/octet-stream",
|
|
22
|
+
size: typeof r.size === "number" && r.size > 0 ? r.size : data.byteLength
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/device-network/deviceSyncStateStore.ts
|
|
27
|
+
var MemoryDeviceSyncStateStore = class {
|
|
28
|
+
versionVector;
|
|
29
|
+
constructor(initial = {}) {
|
|
30
|
+
this.versionVector = { ...initial };
|
|
31
|
+
}
|
|
32
|
+
async loadVersionVector() {
|
|
33
|
+
return { ...this.versionVector };
|
|
34
|
+
}
|
|
35
|
+
async saveVersionVector(versionVector) {
|
|
36
|
+
this.versionVector = { ...versionVector };
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/sync/chatSyncEngine.ts
|
|
41
|
+
var ChatSyncEngine = class {
|
|
42
|
+
nodeId;
|
|
43
|
+
storage;
|
|
44
|
+
getPeers;
|
|
45
|
+
stateStore;
|
|
46
|
+
versionVector = {};
|
|
47
|
+
loadStatePromise;
|
|
48
|
+
stateMutation = Promise.resolve();
|
|
49
|
+
constructor(options) {
|
|
50
|
+
this.nodeId = options.nodeId;
|
|
51
|
+
this.storage = options.storage;
|
|
52
|
+
this.getPeers = options.peers;
|
|
53
|
+
this.stateStore = options.stateStore ?? new MemoryDeviceSyncStateStore();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 在本节点新产生一条消息时调用,增加本节点的 lamportClock。
|
|
57
|
+
*/
|
|
58
|
+
bumpLocalVersion() {
|
|
59
|
+
let nextClock = 0;
|
|
60
|
+
const mutation = this.stateMutation.then(async () => {
|
|
61
|
+
await this.ensureStateLoaded();
|
|
62
|
+
nextClock = (this.versionVector[this.nodeId] ?? 0) + 1;
|
|
63
|
+
this.versionVector[this.nodeId] = nextClock;
|
|
64
|
+
await this.stateStore.saveVersionVector(this.versionVector);
|
|
65
|
+
});
|
|
66
|
+
this.stateMutation = mutation.catch(() => void 0);
|
|
67
|
+
return mutation.then(() => nextClock);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 执行一次与所有 peer 的增量同步(metadata 级别),并尽力拉取缺失消息。
|
|
71
|
+
*/
|
|
72
|
+
async syncOnce() {
|
|
73
|
+
await this.ensureStateLoaded();
|
|
74
|
+
await this.stateMutation;
|
|
75
|
+
await this.reconcileVersionVectorFromStorage();
|
|
76
|
+
const peers = this.getPeers();
|
|
77
|
+
if (peers.length === 0) return;
|
|
78
|
+
const pulledConversations = /* @__PURE__ */ new Set();
|
|
79
|
+
for (const peer of peers) {
|
|
80
|
+
const { remoteVersion } = await peer.exchangeVersionVector({ ...this.versionVector });
|
|
81
|
+
let cursor;
|
|
82
|
+
do {
|
|
83
|
+
const page = await peer.pullMissingMetadata(this.versionVector, cursor);
|
|
84
|
+
for (const meta of page.items) {
|
|
85
|
+
await this.storage.upsertConversationMetadata(meta);
|
|
86
|
+
pulledConversations.add(meta.conversationId);
|
|
87
|
+
}
|
|
88
|
+
cursor = page.nextCursor;
|
|
89
|
+
} while (cursor !== void 0);
|
|
90
|
+
for (const [nodeId, clock] of Object.entries(remoteVersion)) {
|
|
91
|
+
const current = this.versionVector[nodeId] ?? 0;
|
|
92
|
+
if (clock > current) this.versionVector[nodeId] = clock;
|
|
93
|
+
}
|
|
94
|
+
await this.stateStore.saveVersionVector(this.versionVector);
|
|
95
|
+
}
|
|
96
|
+
const pageSize = 100;
|
|
97
|
+
let offset = 0;
|
|
98
|
+
for (; ; ) {
|
|
99
|
+
const conversations = await this.storage.listConversations({ limit: pageSize, offset });
|
|
100
|
+
for (const conversation of conversations) {
|
|
101
|
+
pulledConversations.add(conversation.conversationId);
|
|
102
|
+
}
|
|
103
|
+
if (conversations.length < pageSize) break;
|
|
104
|
+
offset += conversations.length;
|
|
105
|
+
}
|
|
106
|
+
for (const conversationId of pulledConversations) {
|
|
107
|
+
await this.pullMessagesForConversationFromPeers(conversationId, peers);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Anti-entropy:先 syncOnce,再对已知会话逐个向各 peer 拉取缺失消息(参考审查单「定期对账」)。
|
|
112
|
+
*/
|
|
113
|
+
async antiEntropyOnce() {
|
|
114
|
+
await this.syncOnce();
|
|
115
|
+
}
|
|
116
|
+
async pullMessagesForConversationFromPeers(conversationId, peers) {
|
|
117
|
+
const localMsgs = await this.storage.getMessages(conversationId, { mode: "full-content" });
|
|
118
|
+
const knownIds = localMsgs.map((m) => m.messageId);
|
|
119
|
+
for (const peer of peers) {
|
|
120
|
+
if (!peer.pullMissingMessages) continue;
|
|
121
|
+
try {
|
|
122
|
+
const incoming = await peer.pullMissingMessages(conversationId, knownIds);
|
|
123
|
+
if (incoming.length > 0) {
|
|
124
|
+
await this.storage.insertMessagesIfAbsent(incoming);
|
|
125
|
+
await this.ensureAttachmentsFromMessages(incoming, peers);
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** 从消息元数据收集 `attachments[].contentHash`,对本地缺失 BLOB 的哈希向各 peer 拉取并 `saveAttachment`。 */
|
|
132
|
+
async ensureAttachmentsFromMessages(messages, peers) {
|
|
133
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
134
|
+
for (const m of messages) {
|
|
135
|
+
for (const a of m.attachments ?? []) {
|
|
136
|
+
if (a.contentHash) hashes.add(a.contentHash);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const h of hashes) {
|
|
140
|
+
const reference = await this.storage.getAttachment(h);
|
|
141
|
+
if (reference) {
|
|
142
|
+
const reader = this.storage.readAttachmentData?.bind(this.storage);
|
|
143
|
+
if (!reader) continue;
|
|
144
|
+
const bytes = await reader(h);
|
|
145
|
+
if (bytes && bytes.length > 0) continue;
|
|
146
|
+
}
|
|
147
|
+
for (const peer of peers) {
|
|
148
|
+
const pull = peer.pullAttachmentBlob?.bind(peer);
|
|
149
|
+
if (!pull) continue;
|
|
150
|
+
try {
|
|
151
|
+
const blob = await pull(h);
|
|
152
|
+
if (blob?.data?.length) {
|
|
153
|
+
const ar = {
|
|
154
|
+
contentHash: h,
|
|
155
|
+
filename: blob.filename,
|
|
156
|
+
mimeType: blob.mimeType,
|
|
157
|
+
size: blob.size > 0 ? blob.size : blob.data.length
|
|
158
|
+
};
|
|
159
|
+
await this.storage.saveAttachment(ar, blob.data);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async getVersionVector() {
|
|
168
|
+
await this.ensureStateLoaded();
|
|
169
|
+
await this.stateMutation;
|
|
170
|
+
return { ...this.versionVector };
|
|
171
|
+
}
|
|
172
|
+
getStorage() {
|
|
173
|
+
return this.storage;
|
|
174
|
+
}
|
|
175
|
+
ensureStateLoaded() {
|
|
176
|
+
this.loadStatePromise ??= (async () => {
|
|
177
|
+
const stored = await this.stateStore.loadVersionVector();
|
|
178
|
+
this.versionVector = { ...stored, [this.nodeId]: stored[this.nodeId] ?? 0 };
|
|
179
|
+
})();
|
|
180
|
+
return this.loadStatePromise;
|
|
181
|
+
}
|
|
182
|
+
async reconcileVersionVectorFromStorage() {
|
|
183
|
+
const pageSize = 100;
|
|
184
|
+
let offset = 0;
|
|
185
|
+
let changed = false;
|
|
186
|
+
for (; ; ) {
|
|
187
|
+
const conversations = await this.storage.listConversations({ limit: pageSize, offset });
|
|
188
|
+
for (const conversation of conversations) {
|
|
189
|
+
const current = this.versionVector[conversation.originNodeId] ?? 0;
|
|
190
|
+
if (conversation.originClock > current) {
|
|
191
|
+
this.versionVector[conversation.originNodeId] = conversation.originClock;
|
|
192
|
+
changed = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (conversations.length < pageSize) break;
|
|
196
|
+
offset += conversations.length;
|
|
197
|
+
}
|
|
198
|
+
if (changed) await this.stateStore.saveVersionVector(this.versionVector);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/sync/peerNodeAdapter.ts
|
|
203
|
+
var PeerNodeSyncAdapter = class {
|
|
204
|
+
nodeId;
|
|
205
|
+
transport;
|
|
206
|
+
constructor(nodeId, transport) {
|
|
207
|
+
this.nodeId = nodeId;
|
|
208
|
+
this.transport = transport;
|
|
209
|
+
}
|
|
210
|
+
exchangeVersionVector(localVersion) {
|
|
211
|
+
return this.transport.exchangeVersionVector(this.nodeId, localVersion);
|
|
212
|
+
}
|
|
213
|
+
pullMissingMetadata(sinceVersion, cursor) {
|
|
214
|
+
return this.transport.pullMissingMetadata(this.nodeId, sinceVersion, cursor);
|
|
215
|
+
}
|
|
216
|
+
pullMissingMessages(conversationId, knownMessageIds) {
|
|
217
|
+
const function_ = this.transport.pullMissingMessages?.bind(this.transport);
|
|
218
|
+
if (!function_) {
|
|
219
|
+
return Promise.resolve([]);
|
|
220
|
+
}
|
|
221
|
+
return function_(this.nodeId, conversationId, knownMessageIds);
|
|
222
|
+
}
|
|
223
|
+
pullAttachmentBlob(contentHash) {
|
|
224
|
+
const function_ = this.transport.pullAttachmentBlob?.bind(this.transport);
|
|
225
|
+
if (!function_) {
|
|
226
|
+
return Promise.resolve(null);
|
|
227
|
+
}
|
|
228
|
+
return function_(this.nodeId, contentHash);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// src/device-network/deviceCloudConnectionCoordinator.ts
|
|
233
|
+
var DEFAULT_HEARTBEAT_INTERVAL_MS = 6e4;
|
|
234
|
+
var DEFAULT_INITIAL_BACKOFF_MS = 1e3;
|
|
235
|
+
var DEFAULT_MAX_BACKOFF_MS = 6e4;
|
|
236
|
+
var DEFAULT_JITTER_RATIO = 0.2;
|
|
237
|
+
function emptyComponents() {
|
|
238
|
+
return {
|
|
239
|
+
authorizer: "not-run",
|
|
240
|
+
registration: "not-run",
|
|
241
|
+
relay: "not-run",
|
|
242
|
+
heartbeat: "not-run",
|
|
243
|
+
directory: "not-run"
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function assertNonNegativeFinite(value, name) {
|
|
247
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
248
|
+
throw new TypeError(`${name} must be a finite non-negative number`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
var DeviceCloudConnectionCoordinator = class {
|
|
252
|
+
constructor(options) {
|
|
253
|
+
this.options = options;
|
|
254
|
+
this.configuration = options.configuration;
|
|
255
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
256
|
+
this.initialBackoffMs = options.initialBackoffMs ?? DEFAULT_INITIAL_BACKOFF_MS;
|
|
257
|
+
this.maxBackoffMs = options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
|
|
258
|
+
this.jitterRatio = options.jitterRatio ?? DEFAULT_JITTER_RATIO;
|
|
259
|
+
this.now = options.now ?? Date.now;
|
|
260
|
+
this.random = options.random ?? Math.random;
|
|
261
|
+
assertNonNegativeFinite(this.heartbeatIntervalMs, "heartbeatIntervalMs");
|
|
262
|
+
assertNonNegativeFinite(this.initialBackoffMs, "initialBackoffMs");
|
|
263
|
+
assertNonNegativeFinite(this.maxBackoffMs, "maxBackoffMs");
|
|
264
|
+
assertNonNegativeFinite(this.jitterRatio, "jitterRatio");
|
|
265
|
+
if (this.jitterRatio > 1) throw new TypeError("jitterRatio must not exceed 1");
|
|
266
|
+
this.resetSnapshot();
|
|
267
|
+
}
|
|
268
|
+
configuration;
|
|
269
|
+
generation = 0;
|
|
270
|
+
controller = new AbortController();
|
|
271
|
+
timer;
|
|
272
|
+
inFlight;
|
|
273
|
+
started = false;
|
|
274
|
+
consecutiveFailures = 0;
|
|
275
|
+
snapshotValue = {
|
|
276
|
+
status: "not-configured",
|
|
277
|
+
generation: 0,
|
|
278
|
+
components: emptyComponents()
|
|
279
|
+
};
|
|
280
|
+
heartbeatIntervalMs;
|
|
281
|
+
initialBackoffMs;
|
|
282
|
+
maxBackoffMs;
|
|
283
|
+
jitterRatio;
|
|
284
|
+
now;
|
|
285
|
+
random;
|
|
286
|
+
get snapshot() {
|
|
287
|
+
return {
|
|
288
|
+
...this.snapshotValue,
|
|
289
|
+
components: { ...this.snapshotValue.components }
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
async start() {
|
|
293
|
+
if (this.started) return this.runNow();
|
|
294
|
+
this.started = true;
|
|
295
|
+
await this.runNow();
|
|
296
|
+
}
|
|
297
|
+
async stop() {
|
|
298
|
+
this.started = false;
|
|
299
|
+
this.clearTimer();
|
|
300
|
+
this.controller.abort(new Error("device cloud coordinator stopped"));
|
|
301
|
+
await this.inFlight?.catch(() => void 0);
|
|
302
|
+
}
|
|
303
|
+
async setConfiguration(configuration) {
|
|
304
|
+
this.configuration = configuration;
|
|
305
|
+
this.generation += 1;
|
|
306
|
+
this.consecutiveFailures = 0;
|
|
307
|
+
this.clearTimer();
|
|
308
|
+
this.controller.abort(new Error("device cloud configuration changed"));
|
|
309
|
+
this.controller = new AbortController();
|
|
310
|
+
this.inFlight = void 0;
|
|
311
|
+
this.resetSnapshot();
|
|
312
|
+
if (this.started && this.options.adapter.isConfigured(configuration)) await this.runNow();
|
|
313
|
+
}
|
|
314
|
+
runNow() {
|
|
315
|
+
if (this.inFlight) return this.inFlight;
|
|
316
|
+
if (!this.options.adapter.isConfigured(this.configuration)) {
|
|
317
|
+
this.resetSnapshot();
|
|
318
|
+
return Promise.resolve();
|
|
319
|
+
}
|
|
320
|
+
const generation = this.generation;
|
|
321
|
+
const configuration = this.configuration;
|
|
322
|
+
const signal = this.controller.signal;
|
|
323
|
+
const run = this.maintain(configuration, generation, signal);
|
|
324
|
+
const tracked = run.finally(() => {
|
|
325
|
+
if (this.inFlight === tracked) this.inFlight = void 0;
|
|
326
|
+
});
|
|
327
|
+
this.inFlight = tracked;
|
|
328
|
+
return tracked;
|
|
329
|
+
}
|
|
330
|
+
async maintain(configuration, generation, signal) {
|
|
331
|
+
if (!this.isCurrent(generation, signal)) return;
|
|
332
|
+
await this.updateSnapshot(generation, {
|
|
333
|
+
status: "connecting",
|
|
334
|
+
lastError: void 0,
|
|
335
|
+
nextRetryAt: void 0
|
|
336
|
+
});
|
|
337
|
+
try {
|
|
338
|
+
if (this.snapshotValue.components.authorizer !== "ready") {
|
|
339
|
+
await this.runStep("authorizer", generation, signal, () => this.options.adapter.ensureAuthorizer(configuration, signal));
|
|
340
|
+
}
|
|
341
|
+
if (this.snapshotValue.components.registration !== "ready") {
|
|
342
|
+
await this.runStep("registration", generation, signal, () => this.options.adapter.registerDevice(configuration, signal));
|
|
343
|
+
}
|
|
344
|
+
let relayError;
|
|
345
|
+
try {
|
|
346
|
+
await this.runStep("relay", generation, signal, () => this.options.adapter.ensureRelay(configuration, signal));
|
|
347
|
+
} catch (error) {
|
|
348
|
+
relayError = error;
|
|
349
|
+
this.options.logWarning?.("Cloud relay maintenance failed", error);
|
|
350
|
+
}
|
|
351
|
+
await this.runStep("heartbeat", generation, signal, () => this.options.adapter.heartbeat(configuration, signal));
|
|
352
|
+
let directoryError;
|
|
353
|
+
try {
|
|
354
|
+
await this.runStep("directory", generation, signal, () => this.options.adapter.syncDirectory(configuration, signal));
|
|
355
|
+
} catch (error) {
|
|
356
|
+
directoryError = error;
|
|
357
|
+
this.options.logWarning?.("Cloud directory synchronization failed", error);
|
|
358
|
+
}
|
|
359
|
+
if (!this.isCurrent(generation, signal)) return;
|
|
360
|
+
this.consecutiveFailures = 0;
|
|
361
|
+
const relayRequired = this.options.adapter.relayRequiredForOnline(configuration);
|
|
362
|
+
await this.updateSnapshot(generation, {
|
|
363
|
+
status: directoryError || relayRequired && relayError ? "degraded" : "online",
|
|
364
|
+
lastError: directoryError ?? relayError,
|
|
365
|
+
nextRetryAt: this.now() + this.heartbeatIntervalMs
|
|
366
|
+
});
|
|
367
|
+
this.schedule(this.heartbeatIntervalMs, generation);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (!this.isCurrent(generation, signal)) return;
|
|
370
|
+
this.consecutiveFailures += 1;
|
|
371
|
+
if (this.snapshotValue.components.heartbeat === "failed") {
|
|
372
|
+
this.snapshotValue.components.registration = "not-run";
|
|
373
|
+
}
|
|
374
|
+
const status = this.options.adapter.classifyError?.(error) ?? "offline";
|
|
375
|
+
const delay = this.backoffDelay();
|
|
376
|
+
await this.updateSnapshot(generation, {
|
|
377
|
+
status,
|
|
378
|
+
lastError: error,
|
|
379
|
+
nextRetryAt: this.now() + delay
|
|
380
|
+
});
|
|
381
|
+
this.schedule(delay, generation);
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async runStep(component, generation, signal, operation) {
|
|
386
|
+
if (!this.isCurrent(generation, signal)) return;
|
|
387
|
+
await this.setComponent(component, "pending", generation);
|
|
388
|
+
try {
|
|
389
|
+
const result = await operation();
|
|
390
|
+
if (!this.isCurrent(generation, signal)) return;
|
|
391
|
+
await result?.commit?.();
|
|
392
|
+
if (!this.isCurrent(generation, signal)) return;
|
|
393
|
+
await this.setComponent(component, "ready", generation);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (this.isCurrent(generation, signal)) await this.setComponent(component, "failed", generation);
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
backoffDelay() {
|
|
400
|
+
const exponential = Math.min(
|
|
401
|
+
this.maxBackoffMs,
|
|
402
|
+
this.initialBackoffMs * 2 ** Math.max(0, this.consecutiveFailures - 1)
|
|
403
|
+
);
|
|
404
|
+
const jitter = exponential * this.jitterRatio * (this.random() * 2 - 1);
|
|
405
|
+
return Math.max(0, Math.round(exponential + jitter));
|
|
406
|
+
}
|
|
407
|
+
schedule(delayMs, generation) {
|
|
408
|
+
if (!this.started || generation !== this.generation) return;
|
|
409
|
+
this.clearTimer();
|
|
410
|
+
this.timer = setTimeout(() => {
|
|
411
|
+
if (generation !== this.generation) return;
|
|
412
|
+
void this.runNow().catch((error) => {
|
|
413
|
+
this.options.logWarning?.("Cloud maintenance failed", error);
|
|
414
|
+
});
|
|
415
|
+
}, delayMs);
|
|
416
|
+
}
|
|
417
|
+
clearTimer() {
|
|
418
|
+
if (this.timer) clearTimeout(this.timer);
|
|
419
|
+
this.timer = void 0;
|
|
420
|
+
}
|
|
421
|
+
isCurrent(generation, signal) {
|
|
422
|
+
return generation === this.generation && !signal.aborted;
|
|
423
|
+
}
|
|
424
|
+
async setComponent(component, status, generation) {
|
|
425
|
+
if (generation !== this.generation) return;
|
|
426
|
+
this.snapshotValue = {
|
|
427
|
+
...this.snapshotValue,
|
|
428
|
+
components: { ...this.snapshotValue.components, [component]: status }
|
|
429
|
+
};
|
|
430
|
+
await this.emit();
|
|
431
|
+
}
|
|
432
|
+
async updateSnapshot(generation, update) {
|
|
433
|
+
if (generation !== this.generation) return;
|
|
434
|
+
this.snapshotValue = { ...this.snapshotValue, ...update };
|
|
435
|
+
await this.emit();
|
|
436
|
+
}
|
|
437
|
+
resetSnapshot() {
|
|
438
|
+
this.snapshotValue = {
|
|
439
|
+
status: this.options.adapter.isConfigured(this.configuration) ? "offline" : "not-configured",
|
|
440
|
+
generation: this.generation,
|
|
441
|
+
components: emptyComponents()
|
|
442
|
+
};
|
|
443
|
+
void this.emit();
|
|
444
|
+
}
|
|
445
|
+
async emit() {
|
|
446
|
+
await this.options.onStatus?.(this.snapshot);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// src/device-network/jsonFrame.ts
|
|
451
|
+
var JsonFrameError = class extends Error {
|
|
452
|
+
code;
|
|
453
|
+
constructor(code, message, options) {
|
|
454
|
+
super(message, options);
|
|
455
|
+
this.name = "JsonFrameError";
|
|
456
|
+
this.code = code;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
var HEADER_BYTES = 4;
|
|
460
|
+
var MAX_UINT32 = 4294967295;
|
|
461
|
+
function assertPositiveInteger(value, name) {
|
|
462
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
463
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function validateMaxPayloadBytes(maxPayloadBytes) {
|
|
467
|
+
assertPositiveInteger(maxPayloadBytes, "maxPayloadBytes");
|
|
468
|
+
if (maxPayloadBytes > MAX_UINT32) {
|
|
469
|
+
throw new TypeError("maxPayloadBytes must fit in an unsigned 32-bit frame length");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function encodeJsonFrame(value, maxPayloadBytes = MAX_UINT32) {
|
|
473
|
+
validateMaxPayloadBytes(maxPayloadBytes);
|
|
474
|
+
let serialized;
|
|
475
|
+
try {
|
|
476
|
+
serialized = JSON.stringify(value);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
throw new JsonFrameError("INVALID_JSON", "JSON frame value cannot be serialized", {
|
|
479
|
+
cause: error
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (serialized === void 0) {
|
|
483
|
+
throw new JsonFrameError("INVALID_JSON", "JSON frame value cannot be serialized");
|
|
484
|
+
}
|
|
485
|
+
const payload = new TextEncoder().encode(serialized);
|
|
486
|
+
if (payload.byteLength > maxPayloadBytes) {
|
|
487
|
+
throw new JsonFrameError(
|
|
488
|
+
"FRAME_TOO_LARGE",
|
|
489
|
+
`JSON frame payload exceeds ${maxPayloadBytes} bytes`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const frame = new Uint8Array(HEADER_BYTES + payload.byteLength);
|
|
493
|
+
new DataView(frame.buffer, frame.byteOffset, HEADER_BYTES).setUint32(0, payload.byteLength, false);
|
|
494
|
+
frame.set(payload, HEADER_BYTES);
|
|
495
|
+
return frame;
|
|
496
|
+
}
|
|
497
|
+
async function* encodeJsonFrames(values, maxPayloadBytes = MAX_UINT32) {
|
|
498
|
+
for await (const value of values) {
|
|
499
|
+
yield encodeJsonFrame(value, maxPayloadBytes);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function concatBytes(left, right) {
|
|
503
|
+
if (left.byteLength === 0) return right.slice();
|
|
504
|
+
if (right.byteLength === 0) return left;
|
|
505
|
+
const combined = new Uint8Array(left.byteLength + right.byteLength);
|
|
506
|
+
combined.set(left);
|
|
507
|
+
combined.set(right, left.byteLength);
|
|
508
|
+
return combined;
|
|
509
|
+
}
|
|
510
|
+
function abortedError(signal) {
|
|
511
|
+
return new JsonFrameError("ABORTED", "JSON frame reading was aborted", {
|
|
512
|
+
cause: signal?.reason
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
async function readNextChunk(iterator, idleDeadline, totalDeadline, signal) {
|
|
516
|
+
if (signal?.aborted) throw abortedError(signal);
|
|
517
|
+
const now = Date.now();
|
|
518
|
+
const deadline = totalDeadline === void 0 ? idleDeadline : Math.min(idleDeadline, totalDeadline);
|
|
519
|
+
if (deadline <= now) {
|
|
520
|
+
throw new JsonFrameError(
|
|
521
|
+
totalDeadline !== void 0 && totalDeadline <= idleDeadline ? "TOTAL_TIMEOUT" : "IDLE_TIMEOUT",
|
|
522
|
+
totalDeadline !== void 0 && totalDeadline <= idleDeadline ? "JSON frame total timeout expired" : "JSON frame idle timeout expired"
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
return new Promise((resolve, reject) => {
|
|
526
|
+
let settled = false;
|
|
527
|
+
const finish = (callback) => {
|
|
528
|
+
if (settled) return;
|
|
529
|
+
settled = true;
|
|
530
|
+
clearTimeout(timer);
|
|
531
|
+
signal?.removeEventListener("abort", onAbort);
|
|
532
|
+
callback();
|
|
533
|
+
};
|
|
534
|
+
const onAbort = () => {
|
|
535
|
+
finish(() => {
|
|
536
|
+
reject(abortedError(signal));
|
|
537
|
+
});
|
|
538
|
+
};
|
|
539
|
+
const timer = setTimeout(() => {
|
|
540
|
+
const currentTime = Date.now();
|
|
541
|
+
const totalExpired = totalDeadline !== void 0 && currentTime >= totalDeadline;
|
|
542
|
+
finish(() => {
|
|
543
|
+
reject(
|
|
544
|
+
new JsonFrameError(
|
|
545
|
+
totalExpired ? "TOTAL_TIMEOUT" : "IDLE_TIMEOUT",
|
|
546
|
+
totalExpired ? "JSON frame total timeout expired" : "JSON frame idle timeout expired"
|
|
547
|
+
)
|
|
548
|
+
);
|
|
549
|
+
});
|
|
550
|
+
}, Math.max(1, deadline - now));
|
|
551
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
552
|
+
void iterator.next().then(
|
|
553
|
+
(result) => {
|
|
554
|
+
finish(() => {
|
|
555
|
+
resolve(result);
|
|
556
|
+
});
|
|
557
|
+
},
|
|
558
|
+
(error) => {
|
|
559
|
+
finish(() => {
|
|
560
|
+
reject(
|
|
561
|
+
error instanceof Error ? error : new Error("JSON frame source failed", {
|
|
562
|
+
cause: error
|
|
563
|
+
})
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
function createJsonFrameReader(source, options) {
|
|
571
|
+
validateMaxPayloadBytes(options.maxPayloadBytes);
|
|
572
|
+
assertPositiveInteger(options.idleTimeoutMs, "idleTimeoutMs");
|
|
573
|
+
assertPositiveInteger(options.totalTimeoutMs, "totalTimeoutMs");
|
|
574
|
+
return (async function* readFrames() {
|
|
575
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
576
|
+
let buffered = new Uint8Array(0);
|
|
577
|
+
let expectedPayloadBytes;
|
|
578
|
+
let idleDeadline = Date.now() + options.idleTimeoutMs;
|
|
579
|
+
let totalDeadline;
|
|
580
|
+
let abortCalled = false;
|
|
581
|
+
const abortOnce = async (error) => {
|
|
582
|
+
if (abortCalled) return;
|
|
583
|
+
abortCalled = true;
|
|
584
|
+
await Promise.resolve(options.abort?.(error)).catch(() => void 0);
|
|
585
|
+
};
|
|
586
|
+
try {
|
|
587
|
+
for (; ; ) {
|
|
588
|
+
if (expectedPayloadBytes === void 0 && buffered.byteLength >= HEADER_BYTES) {
|
|
589
|
+
expectedPayloadBytes = new DataView(
|
|
590
|
+
buffered.buffer,
|
|
591
|
+
buffered.byteOffset,
|
|
592
|
+
HEADER_BYTES
|
|
593
|
+
).getUint32(0, false);
|
|
594
|
+
buffered = buffered.slice(HEADER_BYTES);
|
|
595
|
+
totalDeadline ??= Date.now() + options.totalTimeoutMs;
|
|
596
|
+
if (expectedPayloadBytes > options.maxPayloadBytes) {
|
|
597
|
+
throw new JsonFrameError(
|
|
598
|
+
"FRAME_TOO_LARGE",
|
|
599
|
+
`JSON frame payload exceeds ${options.maxPayloadBytes} bytes`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (expectedPayloadBytes !== void 0 && buffered.byteLength >= expectedPayloadBytes) {
|
|
604
|
+
const payload = buffered.slice(0, expectedPayloadBytes);
|
|
605
|
+
buffered = buffered.slice(expectedPayloadBytes);
|
|
606
|
+
expectedPayloadBytes = void 0;
|
|
607
|
+
totalDeadline = buffered.byteLength > 0 ? Date.now() + options.totalTimeoutMs : void 0;
|
|
608
|
+
let text;
|
|
609
|
+
try {
|
|
610
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(payload);
|
|
611
|
+
} catch (error) {
|
|
612
|
+
throw new JsonFrameError("INVALID_UTF8", "JSON frame payload is not valid UTF-8", {
|
|
613
|
+
cause: error
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
let value;
|
|
617
|
+
try {
|
|
618
|
+
value = JSON.parse(text);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
throw new JsonFrameError("INVALID_JSON", "JSON frame payload is not valid JSON", {
|
|
621
|
+
cause: error
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
yield value;
|
|
625
|
+
idleDeadline = Date.now() + options.idleTimeoutMs;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const result = await readNextChunk(
|
|
629
|
+
iterator,
|
|
630
|
+
idleDeadline,
|
|
631
|
+
totalDeadline,
|
|
632
|
+
options.signal
|
|
633
|
+
);
|
|
634
|
+
if (result.done) {
|
|
635
|
+
if (buffered.byteLength > 0 || expectedPayloadBytes !== void 0) {
|
|
636
|
+
throw new JsonFrameError("TRUNCATED_FRAME", "JSON frame stream ended mid-frame");
|
|
637
|
+
}
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (!(result.value instanceof Uint8Array)) {
|
|
641
|
+
throw new TypeError("JSON frame source must yield Uint8Array chunks");
|
|
642
|
+
}
|
|
643
|
+
if (result.value.byteLength === 0) continue;
|
|
644
|
+
const now = Date.now();
|
|
645
|
+
idleDeadline = now + options.idleTimeoutMs;
|
|
646
|
+
totalDeadline ??= now + options.totalTimeoutMs;
|
|
647
|
+
buffered = concatBytes(buffered, result.value);
|
|
648
|
+
}
|
|
649
|
+
} catch (error) {
|
|
650
|
+
const frameError = error instanceof JsonFrameError ? error : new JsonFrameError("ABORTED", "JSON frame source failed", { cause: error });
|
|
651
|
+
await abortOnce(frameError);
|
|
652
|
+
throw frameError;
|
|
653
|
+
} finally {
|
|
654
|
+
void iterator.return?.().catch(() => void 0);
|
|
655
|
+
}
|
|
656
|
+
})();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/device-network/deviceOrchestrationTransport.ts
|
|
660
|
+
var DEVICE_ORCHESTRATION_PROTOCOL = "/memeloop/orchestration/2.0.0";
|
|
661
|
+
var DEFAULT_MAX_FRAME_BYTES = 1024 * 1024;
|
|
662
|
+
var REQUEST_IDLE_TIMEOUT_MS = 1e4;
|
|
663
|
+
var REQUEST_TOTAL_TIMEOUT_MS = 3e4;
|
|
664
|
+
var WATCH_IDLE_TIMEOUT_MS = 9e4;
|
|
665
|
+
var WATCH_BOOKMARK_INTERVAL_MS = 3e4;
|
|
666
|
+
function orchestrationTransportError(code, message) {
|
|
667
|
+
return new OrchestrationError({
|
|
668
|
+
code,
|
|
669
|
+
message,
|
|
670
|
+
retryable: code === "UNAVAILABLE"
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
function assertFrameLimit(limit) {
|
|
674
|
+
if (!Number.isSafeInteger(limit) || limit < 256) {
|
|
675
|
+
throw new TypeError("maxFrameBytes must be a safe integer of at least 256");
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function throwIfAborted(signal) {
|
|
679
|
+
if (signal?.aborted) {
|
|
680
|
+
throw orchestrationTransportError("CANCELLED", "device orchestration request was cancelled");
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async function openAuthenticatedStream(options) {
|
|
684
|
+
const grant = await options.grantProvider?.(options.peerId);
|
|
685
|
+
return {
|
|
686
|
+
stream: await options.deviceNetwork.openStream(
|
|
687
|
+
options.peerId,
|
|
688
|
+
DEVICE_ORCHESTRATION_PROTOCOL,
|
|
689
|
+
grant
|
|
690
|
+
),
|
|
691
|
+
grant
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
async function writeRequest(stream, request, grant, maxFrameBytes) {
|
|
695
|
+
const envelope = {
|
|
696
|
+
type: "memeloop-device-orchestration-request-v2",
|
|
697
|
+
request,
|
|
698
|
+
...grant ? { grant } : {}
|
|
699
|
+
};
|
|
700
|
+
await stream.sink(encodeJsonFrames([envelope], maxFrameBytes));
|
|
701
|
+
}
|
|
702
|
+
function abortStreamOnce(stream) {
|
|
703
|
+
let aborted = false;
|
|
704
|
+
return async (error) => {
|
|
705
|
+
if (aborted) return;
|
|
706
|
+
aborted = true;
|
|
707
|
+
await stream.abort(error);
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
function frameReader(stream, maxFrameBytes, idleTimeoutMs, signal, abort) {
|
|
711
|
+
return createJsonFrameReader(stream.source, {
|
|
712
|
+
maxPayloadBytes: maxFrameBytes,
|
|
713
|
+
idleTimeoutMs,
|
|
714
|
+
totalTimeoutMs: REQUEST_TOTAL_TIMEOUT_MS,
|
|
715
|
+
signal,
|
|
716
|
+
abort
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
function mapFrameError(error) {
|
|
720
|
+
if (!(error instanceof JsonFrameError)) throw error;
|
|
721
|
+
if (error.code === "ABORTED") {
|
|
722
|
+
throw orchestrationTransportError("CANCELLED", error.message);
|
|
723
|
+
}
|
|
724
|
+
if (error.code === "FRAME_TOO_LARGE") {
|
|
725
|
+
throw orchestrationTransportError("EXHAUSTED", error.message);
|
|
726
|
+
}
|
|
727
|
+
if (error.code === "IDLE_TIMEOUT" || error.code === "TOTAL_TIMEOUT") {
|
|
728
|
+
throw orchestrationTransportError("UNAVAILABLE", error.message);
|
|
729
|
+
}
|
|
730
|
+
throw orchestrationTransportError("INVALID", error.message);
|
|
731
|
+
}
|
|
732
|
+
function createDeviceOrchestrationTransport(options) {
|
|
733
|
+
const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
|
|
734
|
+
assertFrameLimit(maxFrameBytes);
|
|
735
|
+
return {
|
|
736
|
+
async request(request, transportOptions = {}) {
|
|
737
|
+
throwIfAborted(transportOptions.signal);
|
|
738
|
+
const { stream, grant } = await openAuthenticatedStream(options);
|
|
739
|
+
const abort = abortStreamOnce(stream);
|
|
740
|
+
const closeOnAbort = () => {
|
|
741
|
+
void abort(orchestrationTransportError("CANCELLED", "device orchestration request was cancelled"));
|
|
742
|
+
};
|
|
743
|
+
transportOptions.signal?.addEventListener("abort", closeOnAbort, {
|
|
744
|
+
once: true
|
|
745
|
+
});
|
|
746
|
+
try {
|
|
747
|
+
await writeRequest(stream, request, grant, maxFrameBytes);
|
|
748
|
+
let response;
|
|
749
|
+
for await (const value of frameReader(
|
|
750
|
+
stream,
|
|
751
|
+
maxFrameBytes,
|
|
752
|
+
REQUEST_IDLE_TIMEOUT_MS,
|
|
753
|
+
transportOptions.signal,
|
|
754
|
+
abort
|
|
755
|
+
)) {
|
|
756
|
+
if (response) {
|
|
757
|
+
throw orchestrationTransportError(
|
|
758
|
+
"INVALID",
|
|
759
|
+
"device orchestration request returned multiple responses"
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
response = value;
|
|
763
|
+
}
|
|
764
|
+
throwIfAborted(transportOptions.signal);
|
|
765
|
+
if (!response) {
|
|
766
|
+
throw orchestrationTransportError(
|
|
767
|
+
"UNAVAILABLE",
|
|
768
|
+
"device orchestration request returned no response"
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
return response;
|
|
772
|
+
} catch (error) {
|
|
773
|
+
if (error instanceof JsonFrameError) await abort(error);
|
|
774
|
+
mapFrameError(error);
|
|
775
|
+
} finally {
|
|
776
|
+
transportOptions.signal?.removeEventListener("abort", closeOnAbort);
|
|
777
|
+
await stream.close().catch(() => void 0);
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
async *watch(request, transportOptions = {}) {
|
|
781
|
+
throwIfAborted(transportOptions.signal);
|
|
782
|
+
const { stream, grant } = await openAuthenticatedStream(options);
|
|
783
|
+
const abort = abortStreamOnce(stream);
|
|
784
|
+
const closeOnAbort = () => {
|
|
785
|
+
void abort(orchestrationTransportError("CANCELLED", "device orchestration watch was cancelled"));
|
|
786
|
+
};
|
|
787
|
+
transportOptions.signal?.addEventListener("abort", closeOnAbort, {
|
|
788
|
+
once: true
|
|
789
|
+
});
|
|
790
|
+
try {
|
|
791
|
+
await writeRequest(stream, request, grant, maxFrameBytes);
|
|
792
|
+
for await (const value of frameReader(
|
|
793
|
+
stream,
|
|
794
|
+
maxFrameBytes,
|
|
795
|
+
WATCH_IDLE_TIMEOUT_MS,
|
|
796
|
+
transportOptions.signal,
|
|
797
|
+
abort
|
|
798
|
+
)) {
|
|
799
|
+
throwIfAborted(transportOptions.signal);
|
|
800
|
+
yield value;
|
|
801
|
+
}
|
|
802
|
+
throwIfAborted(transportOptions.signal);
|
|
803
|
+
} catch (error) {
|
|
804
|
+
if (error instanceof JsonFrameError) await abort(error);
|
|
805
|
+
mapFrameError(error);
|
|
806
|
+
} finally {
|
|
807
|
+
transportOptions.signal?.removeEventListener("abort", closeOnAbort);
|
|
808
|
+
await stream.close().catch(() => void 0);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
async function readSingleRequest(stream, maxFrameBytes) {
|
|
814
|
+
const abort = abortStreamOnce(stream);
|
|
815
|
+
let envelope;
|
|
816
|
+
for await (const value of frameReader(
|
|
817
|
+
stream,
|
|
818
|
+
maxFrameBytes,
|
|
819
|
+
REQUEST_IDLE_TIMEOUT_MS,
|
|
820
|
+
void 0,
|
|
821
|
+
abort
|
|
822
|
+
)) {
|
|
823
|
+
if (envelope) {
|
|
824
|
+
throw orchestrationTransportError(
|
|
825
|
+
"INVALID",
|
|
826
|
+
"device orchestration stream accepts exactly one request"
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || value.type !== "memeloop-device-orchestration-request-v2" || value.request === null || typeof value.request !== "object") {
|
|
830
|
+
throw orchestrationTransportError(
|
|
831
|
+
"INVALID",
|
|
832
|
+
"device orchestration request envelope is invalid"
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
envelope = value;
|
|
836
|
+
}
|
|
837
|
+
if (!envelope) {
|
|
838
|
+
throw orchestrationTransportError("INVALID", "device orchestration request is missing");
|
|
839
|
+
}
|
|
840
|
+
return envelope;
|
|
841
|
+
}
|
|
842
|
+
async function* withWatchBookmarks(source, request, signal) {
|
|
843
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
844
|
+
let pending = iterator.next();
|
|
845
|
+
try {
|
|
846
|
+
for (; ; ) {
|
|
847
|
+
const result = await Promise.race([
|
|
848
|
+
pending.then((value) => ({ kind: "value", value })),
|
|
849
|
+
new Promise((resolve) => {
|
|
850
|
+
const timer = setTimeout(() => {
|
|
851
|
+
resolve({ kind: "bookmark" });
|
|
852
|
+
}, WATCH_BOOKMARK_INTERVAL_MS);
|
|
853
|
+
signal.addEventListener("abort", () => {
|
|
854
|
+
clearTimeout(timer);
|
|
855
|
+
resolve({ kind: "bookmark" });
|
|
856
|
+
}, { once: true });
|
|
857
|
+
})
|
|
858
|
+
]);
|
|
859
|
+
if (signal.aborted) return;
|
|
860
|
+
if (result.kind === "bookmark") {
|
|
861
|
+
yield {
|
|
862
|
+
protocol: request.protocol,
|
|
863
|
+
requestId: request.requestId,
|
|
864
|
+
ok: true,
|
|
865
|
+
result: { type: "BOOKMARK", resourceVersion: String(Date.now()) }
|
|
866
|
+
};
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (result.value.done) return;
|
|
870
|
+
yield result.value.value;
|
|
871
|
+
pending = iterator.next();
|
|
872
|
+
}
|
|
873
|
+
} finally {
|
|
874
|
+
void iterator.return?.();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
function createDeviceOrchestrationStreamHandler(options) {
|
|
878
|
+
const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
|
|
879
|
+
assertFrameLimit(maxFrameBytes);
|
|
880
|
+
return async ({ remotePeerId, stream, authorize }) => {
|
|
881
|
+
const abort = new AbortController();
|
|
882
|
+
try {
|
|
883
|
+
const envelope = await readSingleRequest(stream, maxFrameBytes);
|
|
884
|
+
if (authorize && !await authorize(envelope.grant)) {
|
|
885
|
+
throw orchestrationTransportError("UNAVAILABLE", "device_not_trusted");
|
|
886
|
+
}
|
|
887
|
+
const request = envelope.request;
|
|
888
|
+
const handler = await options.resolveHandler(remotePeerId);
|
|
889
|
+
if (request.operation === "watch") {
|
|
890
|
+
await stream.sink(
|
|
891
|
+
encodeJsonFrames(
|
|
892
|
+
withWatchBookmarks(handler.watch(request, { signal: abort.signal }), request, abort.signal),
|
|
893
|
+
maxFrameBytes
|
|
894
|
+
)
|
|
895
|
+
);
|
|
896
|
+
} else {
|
|
897
|
+
await stream.sink(encodeJsonFrames([await handler.request(request)], maxFrameBytes));
|
|
898
|
+
}
|
|
899
|
+
} finally {
|
|
900
|
+
abort.abort();
|
|
901
|
+
await stream.close().catch(() => void 0);
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/device-network/libp2pSyncProtocol.ts
|
|
907
|
+
var LIBP2P_SYNC_REQUEST_TYPE = "memeloop-sync-request-v2";
|
|
908
|
+
var LIBP2P_SYNC_RESPONSE_TYPE = "memeloop-sync-response-v2";
|
|
909
|
+
var syncMethods = /* @__PURE__ */ new Set([
|
|
910
|
+
"exchangeVersionVector",
|
|
911
|
+
"pullMissingMetadata",
|
|
912
|
+
"pullMissingMessages",
|
|
913
|
+
"pullAttachmentBlob"
|
|
914
|
+
]);
|
|
915
|
+
function isLibp2pSyncRequest(value) {
|
|
916
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
917
|
+
const record = value;
|
|
918
|
+
return record.type === LIBP2P_SYNC_REQUEST_TYPE && typeof record.id === "string" && typeof record.method === "string" && syncMethods.has(record.method);
|
|
919
|
+
}
|
|
920
|
+
function isLibp2pSyncResponse(value) {
|
|
921
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
922
|
+
const record = value;
|
|
923
|
+
if (record.type !== LIBP2P_SYNC_RESPONSE_TYPE || typeof record.id !== "string" || typeof record.ok !== "boolean") return false;
|
|
924
|
+
if (record.ok) return "result" in record;
|
|
925
|
+
return typeof record.error === "string";
|
|
926
|
+
}
|
|
927
|
+
function isAttachmentBlobWire(value) {
|
|
928
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
929
|
+
const record = value;
|
|
930
|
+
return typeof record.dataBase64Url === "string" && typeof record.filename === "string" && typeof record.mimeType === "string" && typeof record.size === "number";
|
|
931
|
+
}
|
|
932
|
+
async function attachmentBlobFromWire(value) {
|
|
933
|
+
if (value === null) return null;
|
|
934
|
+
if (!isAttachmentBlobWire(value)) throw new Error("invalid_sync_response");
|
|
935
|
+
let data;
|
|
936
|
+
try {
|
|
937
|
+
const base64 = value.dataBase64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
938
|
+
const paddedBase64 = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
939
|
+
const binary = atob(paddedBase64);
|
|
940
|
+
data = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
941
|
+
} catch {
|
|
942
|
+
throw new Error("invalid_sync_response");
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
data,
|
|
946
|
+
filename: value.filename,
|
|
947
|
+
mimeType: value.mimeType,
|
|
948
|
+
size: value.size
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/device-network/libp2pDeviceSyncTransport.ts
|
|
953
|
+
var Libp2pDeviceSyncTransport = class {
|
|
954
|
+
nodeId;
|
|
955
|
+
deviceNetwork;
|
|
956
|
+
grantProvider;
|
|
957
|
+
constructor(options) {
|
|
958
|
+
this.nodeId = options.nodeId;
|
|
959
|
+
this.deviceNetwork = options.deviceNetwork;
|
|
960
|
+
this.grantProvider = options.grantProvider;
|
|
961
|
+
}
|
|
962
|
+
listPeers() {
|
|
963
|
+
return this.deviceNetwork.listDevices();
|
|
964
|
+
}
|
|
965
|
+
exchangeVersionVector(peerId, localVersion) {
|
|
966
|
+
return this.request(peerId, "exchangeVersionVector", { localVersion });
|
|
967
|
+
}
|
|
968
|
+
pullMissingMetadata(peerId, sinceVersion, cursor) {
|
|
969
|
+
return this.request(peerId, "pullMissingMetadata", {
|
|
970
|
+
sinceVersion,
|
|
971
|
+
cursor
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
pullMissingMessages(peerId, conversationId, knownMessageIds) {
|
|
975
|
+
return this.request(peerId, "pullMissingMessages", { conversationId, knownMessageIds });
|
|
976
|
+
}
|
|
977
|
+
async pullAttachmentBlob(peerId, contentHash) {
|
|
978
|
+
const result = await this.request(peerId, "pullAttachmentBlob", { contentHash });
|
|
979
|
+
return attachmentBlobFromWire(result);
|
|
980
|
+
}
|
|
981
|
+
async request(peerId, method, parameters) {
|
|
982
|
+
const grant = await this.grantProvider?.(peerId);
|
|
983
|
+
const stream = await this.deviceNetwork.openStream(peerId, "/memeloop/sync/2.0.0", grant);
|
|
984
|
+
const request = {
|
|
985
|
+
type: LIBP2P_SYNC_REQUEST_TYPE,
|
|
986
|
+
id: crypto.randomUUID(),
|
|
987
|
+
method,
|
|
988
|
+
params: parameters,
|
|
989
|
+
grant
|
|
990
|
+
};
|
|
991
|
+
try {
|
|
992
|
+
await stream.sink(encodeJsonFrames([request], 16 * 1024 * 1024));
|
|
993
|
+
let response;
|
|
994
|
+
for await (const value of createJsonFrameReader(stream.source, {
|
|
995
|
+
maxPayloadBytes: 16 * 1024 * 1024,
|
|
996
|
+
idleTimeoutMs: 15e3,
|
|
997
|
+
totalTimeoutMs: 12e4,
|
|
998
|
+
abort: async (error) => stream.abort(error)
|
|
999
|
+
})) {
|
|
1000
|
+
if (response !== void 0) throw new Error("sync_response_multiple");
|
|
1001
|
+
response = value;
|
|
1002
|
+
}
|
|
1003
|
+
if (!isLibp2pSyncResponse(response)) throw new Error("invalid_sync_response");
|
|
1004
|
+
if (response.id !== request.id) throw new Error("sync_response_id_mismatch");
|
|
1005
|
+
if (!response.ok) throw new Error(response.error);
|
|
1006
|
+
return response.result;
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
if (error instanceof JsonFrameError) await stream.abort(error);
|
|
1009
|
+
throw error;
|
|
1010
|
+
} finally {
|
|
1011
|
+
await stream.close().catch(() => void 0);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
// src/device-network/libp2pRpcProtocol.ts
|
|
1017
|
+
var LIBP2P_RPC_REQUEST_TYPE = "memeloop-rpc-request-v2";
|
|
1018
|
+
var LIBP2P_RPC_RESPONSE_TYPE = "memeloop-rpc-response-v2";
|
|
1019
|
+
function isLibp2pRpcRequest(value) {
|
|
1020
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1021
|
+
const record = value;
|
|
1022
|
+
return record.type === LIBP2P_RPC_REQUEST_TYPE && typeof record.id === "string" && typeof record.method === "string";
|
|
1023
|
+
}
|
|
1024
|
+
function isLibp2pRpcResponse(value) {
|
|
1025
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1026
|
+
const record = value;
|
|
1027
|
+
if (record.type !== LIBP2P_RPC_RESPONSE_TYPE || typeof record.id !== "string" || typeof record.ok !== "boolean") return false;
|
|
1028
|
+
if (record.ok) return "result" in record;
|
|
1029
|
+
return typeof record.error === "string";
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/device-network/localTrustDeviceAuthorizer.ts
|
|
1033
|
+
var PAIRING_PROTOCOL = "/memeloop/pairing/2.0.0";
|
|
1034
|
+
var LocalTrustDeviceAuthorizer = class {
|
|
1035
|
+
constructor(options = {}) {
|
|
1036
|
+
this.options = options;
|
|
1037
|
+
this.allowPairingProtocol = options.allowPairingProtocol ?? true;
|
|
1038
|
+
for (const record of options.trustedDevices ?? []) {
|
|
1039
|
+
this.trustedDevices.set(record.peerId, record);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
trustedDevices = /* @__PURE__ */ new Map();
|
|
1043
|
+
allowPairingProtocol;
|
|
1044
|
+
setTrustedDevice(record) {
|
|
1045
|
+
this.trustedDevices.set(record.peerId, record);
|
|
1046
|
+
}
|
|
1047
|
+
removeTrustedDevice(peerId) {
|
|
1048
|
+
this.trustedDevices.delete(peerId);
|
|
1049
|
+
}
|
|
1050
|
+
async canOpenProtocol(input) {
|
|
1051
|
+
const record = this.options.getTrustedDevice?.(input.remotePeerId) ?? this.trustedDevices.get(input.remotePeerId);
|
|
1052
|
+
if (record?.revokedAt) return false;
|
|
1053
|
+
if (input.protocol === PAIRING_PROTOCOL) return this.allowPairingProtocol;
|
|
1054
|
+
return record !== void 0;
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
// src/device-network/pairingInvite.ts
|
|
1059
|
+
var DEVICE_PAIRING_INVITE_PROTOCOL = "memeloop-device-pairing-v2";
|
|
1060
|
+
var DEVICE_PAIRING_INVITE_TTL_MS = 5 * 6e4;
|
|
1061
|
+
var MAX_INVITE_LENGTH = 16 * 1024;
|
|
1062
|
+
var MAX_ADDRESSES = 8;
|
|
1063
|
+
function requireText(value, name, maxLength) {
|
|
1064
|
+
const trimmed = value.trim();
|
|
1065
|
+
if (!trimmed || trimmed.length > maxLength) {
|
|
1066
|
+
throw new Error(`invalid device pairing invite ${name}`);
|
|
1067
|
+
}
|
|
1068
|
+
return trimmed;
|
|
1069
|
+
}
|
|
1070
|
+
function finalPeerId(address) {
|
|
1071
|
+
const parts = address.split("/");
|
|
1072
|
+
for (let index = parts.length - 2; index >= 0; index -= 1) {
|
|
1073
|
+
if (parts[index] === "p2p") return parts[index + 1];
|
|
1074
|
+
}
|
|
1075
|
+
return void 0;
|
|
1076
|
+
}
|
|
1077
|
+
function validateAddresses(addresses, peerId) {
|
|
1078
|
+
const unique = [...new Set(addresses.map((address) => address.trim()))].filter(Boolean);
|
|
1079
|
+
if (unique.length === 0 || unique.length > MAX_ADDRESSES || unique.some(
|
|
1080
|
+
(address) => address.length > 2048 || !address.startsWith("/") || !address.includes("/ws") && !address.includes("/wss") || finalPeerId(address) !== peerId
|
|
1081
|
+
)) {
|
|
1082
|
+
throw new Error("device pairing invite requires PeerId-bound WebSocket multiaddrs");
|
|
1083
|
+
}
|
|
1084
|
+
return unique;
|
|
1085
|
+
}
|
|
1086
|
+
function validateLifetime(createdAt, expiresAt, now) {
|
|
1087
|
+
if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(expiresAt) || expiresAt <= createdAt || expiresAt - createdAt > DEVICE_PAIRING_INVITE_TTL_MS) {
|
|
1088
|
+
throw new Error("invalid device pairing invite lifetime");
|
|
1089
|
+
}
|
|
1090
|
+
if (expiresAt <= now) throw new Error("device pairing invite has expired");
|
|
1091
|
+
}
|
|
1092
|
+
function unsignedPayload(invite) {
|
|
1093
|
+
const peerId = requireText(invite.peerId, "peerId", 256);
|
|
1094
|
+
return {
|
|
1095
|
+
protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
|
|
1096
|
+
peerId,
|
|
1097
|
+
publicKeyMultibase: requireText(invite.publicKeyMultibase, "publicKeyMultibase", 4096),
|
|
1098
|
+
deviceName: requireText(invite.deviceName, "deviceName", 256),
|
|
1099
|
+
multiaddrs: [...validateAddresses(invite.multiaddrs, peerId)].sort(),
|
|
1100
|
+
createdAt: invite.createdAt,
|
|
1101
|
+
expiresAt: invite.expiresAt
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
function canonicalDevicePairingInviteBytes(invite) {
|
|
1105
|
+
const payload = unsignedPayload(invite);
|
|
1106
|
+
const canonical = [
|
|
1107
|
+
DEVICE_PAIRING_INVITE_PROTOCOL,
|
|
1108
|
+
`peerId=${payload.peerId}`,
|
|
1109
|
+
`publicKey=${payload.publicKeyMultibase}`,
|
|
1110
|
+
`deviceName=${payload.deviceName}`,
|
|
1111
|
+
`multiaddrs=${payload.multiaddrs.join(",")}`,
|
|
1112
|
+
`createdAt=${payload.createdAt}`,
|
|
1113
|
+
`expiresAt=${payload.expiresAt}`
|
|
1114
|
+
].join("\n");
|
|
1115
|
+
return new TextEncoder().encode(canonical);
|
|
1116
|
+
}
|
|
1117
|
+
async function createDevicePairingInvite(device, options) {
|
|
1118
|
+
const createdAt = options.now ?? Date.now();
|
|
1119
|
+
const ttlMs = options.ttlMs ?? DEVICE_PAIRING_INVITE_TTL_MS;
|
|
1120
|
+
if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
|
|
1121
|
+
throw new Error("invalid device pairing invite lifetime");
|
|
1122
|
+
}
|
|
1123
|
+
const payload = unsignedPayload({
|
|
1124
|
+
protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
|
|
1125
|
+
peerId: device.peerId,
|
|
1126
|
+
publicKeyMultibase: device.publicKeyMultibase,
|
|
1127
|
+
deviceName: device.displayName,
|
|
1128
|
+
multiaddrs: device.multiaddrs ?? [],
|
|
1129
|
+
createdAt,
|
|
1130
|
+
expiresAt: createdAt + ttlMs
|
|
1131
|
+
});
|
|
1132
|
+
validateLifetime(payload.createdAt, payload.expiresAt, createdAt - 1);
|
|
1133
|
+
const signature = requireText(
|
|
1134
|
+
await options.sign(canonicalDevicePairingInviteBytes(payload)),
|
|
1135
|
+
"signature",
|
|
1136
|
+
4096
|
|
1137
|
+
);
|
|
1138
|
+
return { ...payload, signature };
|
|
1139
|
+
}
|
|
1140
|
+
function encodeDevicePairingInvite(invite) {
|
|
1141
|
+
const payload = unsignedPayload(invite);
|
|
1142
|
+
const signature = requireText(invite.signature, "signature", 4096);
|
|
1143
|
+
return JSON.stringify({ ...payload, signature });
|
|
1144
|
+
}
|
|
1145
|
+
async function parseDevicePairingInvite(serialized, options) {
|
|
1146
|
+
if (!serialized || serialized.length > MAX_INVITE_LENGTH) {
|
|
1147
|
+
throw new Error("invalid device pairing invite length");
|
|
1148
|
+
}
|
|
1149
|
+
let parsed;
|
|
1150
|
+
try {
|
|
1151
|
+
parsed = JSON.parse(serialized);
|
|
1152
|
+
} catch {
|
|
1153
|
+
throw new Error("device pairing invite is not valid JSON");
|
|
1154
|
+
}
|
|
1155
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1156
|
+
throw new Error("invalid device pairing invite");
|
|
1157
|
+
}
|
|
1158
|
+
const record = parsed;
|
|
1159
|
+
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") {
|
|
1160
|
+
throw new Error("invalid device pairing invite");
|
|
1161
|
+
}
|
|
1162
|
+
validateLifetime(record.createdAt, record.expiresAt, options.now ?? Date.now());
|
|
1163
|
+
const payload = unsignedPayload({
|
|
1164
|
+
protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
|
|
1165
|
+
peerId: record.peerId,
|
|
1166
|
+
publicKeyMultibase: record.publicKeyMultibase,
|
|
1167
|
+
deviceName: record.deviceName,
|
|
1168
|
+
multiaddrs: record.multiaddrs,
|
|
1169
|
+
createdAt: record.createdAt,
|
|
1170
|
+
expiresAt: record.expiresAt
|
|
1171
|
+
});
|
|
1172
|
+
const invite = {
|
|
1173
|
+
...payload,
|
|
1174
|
+
signature: requireText(record.signature, "signature", 4096)
|
|
1175
|
+
};
|
|
1176
|
+
if (!await options.verifyIdentity({
|
|
1177
|
+
invite,
|
|
1178
|
+
payload: canonicalDevicePairingInviteBytes(payload)
|
|
1179
|
+
})) {
|
|
1180
|
+
throw new Error("device pairing invite identity verification failed");
|
|
1181
|
+
}
|
|
1182
|
+
return invite;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// src/sync/protocol.ts
|
|
1186
|
+
function isConversationMeta(value) {
|
|
1187
|
+
if (value === null || typeof value !== "object") return false;
|
|
1188
|
+
const o = value;
|
|
1189
|
+
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";
|
|
1190
|
+
}
|
|
1191
|
+
function computeMissingVersionRanges(currentVersion, availableVersion) {
|
|
1192
|
+
const ranges = [];
|
|
1193
|
+
for (const [originNodeId, availableClock] of Object.entries(availableVersion)) {
|
|
1194
|
+
const currentClock = currentVersion[originNodeId] ?? 0;
|
|
1195
|
+
if (availableClock > currentClock) {
|
|
1196
|
+
ranges.push({ originNodeId, fromExclusive: currentClock, toInclusive: availableClock });
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return ranges.sort((left, right) => left.originNodeId.localeCompare(right.originNodeId));
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
export {
|
|
1203
|
+
decodeAttachmentBlobRpc,
|
|
1204
|
+
MemoryDeviceSyncStateStore,
|
|
1205
|
+
ChatSyncEngine,
|
|
1206
|
+
PeerNodeSyncAdapter,
|
|
1207
|
+
DeviceCloudConnectionCoordinator,
|
|
1208
|
+
JsonFrameError,
|
|
1209
|
+
encodeJsonFrame,
|
|
1210
|
+
encodeJsonFrames,
|
|
1211
|
+
createJsonFrameReader,
|
|
1212
|
+
DEVICE_ORCHESTRATION_PROTOCOL,
|
|
1213
|
+
createDeviceOrchestrationTransport,
|
|
1214
|
+
createDeviceOrchestrationStreamHandler,
|
|
1215
|
+
LIBP2P_SYNC_REQUEST_TYPE,
|
|
1216
|
+
LIBP2P_SYNC_RESPONSE_TYPE,
|
|
1217
|
+
isLibp2pSyncRequest,
|
|
1218
|
+
isLibp2pSyncResponse,
|
|
1219
|
+
attachmentBlobFromWire,
|
|
1220
|
+
Libp2pDeviceSyncTransport,
|
|
1221
|
+
LIBP2P_RPC_REQUEST_TYPE,
|
|
1222
|
+
LIBP2P_RPC_RESPONSE_TYPE,
|
|
1223
|
+
isLibp2pRpcRequest,
|
|
1224
|
+
isLibp2pRpcResponse,
|
|
1225
|
+
LocalTrustDeviceAuthorizer,
|
|
1226
|
+
DEVICE_PAIRING_INVITE_PROTOCOL,
|
|
1227
|
+
DEVICE_PAIRING_INVITE_TTL_MS,
|
|
1228
|
+
canonicalDevicePairingInviteBytes,
|
|
1229
|
+
createDevicePairingInvite,
|
|
1230
|
+
encodeDevicePairingInvite,
|
|
1231
|
+
parseDevicePairingInvite,
|
|
1232
|
+
isConversationMeta,
|
|
1233
|
+
computeMissingVersionRanges
|
|
1234
|
+
};
|
|
1235
|
+
//# sourceMappingURL=chunk-ECNKBH5A.js.map
|