@mh-gg/cli 0.1.1-alpha.20260613T085325975Z

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 (71) hide show
  1. package/README.md +5 -0
  2. package/bin/matterhorn.cjs +57 -0
  3. package/package.json +49 -0
  4. package/runtime/bin/appFrontend/artifacts.cjs +25 -0
  5. package/runtime/bin/appFrontend/buildServers.cjs +176 -0
  6. package/runtime/bin/appFrontend/commandEnv.cjs +74 -0
  7. package/runtime/bin/appFrontend/commandPolicy.cjs +23 -0
  8. package/runtime/bin/appFrontend/devServers.cjs +150 -0
  9. package/runtime/bin/appFrontend/httpServers.cjs +221 -0
  10. package/runtime/bin/appFrontend/paths.cjs +103 -0
  11. package/runtime/bin/appFrontend/ports.cjs +36 -0
  12. package/runtime/bin/appFrontend/processes.cjs +127 -0
  13. package/runtime/bin/appFrontend.cjs +45 -0
  14. package/runtime/bin/appHostCommand.cjs +381 -0
  15. package/runtime/bin/matterhorn.cjs +501 -0
  16. package/runtime/bin/matterhornAppLoader.cjs +588 -0
  17. package/runtime/bin/matterhornApps.cjs +223 -0
  18. package/runtime/bin/matterhornDeploy.cjs +108 -0
  19. package/runtime/bin/matterhornEmitAppBundle.cjs +20 -0
  20. package/runtime/bin/matterhornInstall.cjs +609 -0
  21. package/runtime/host/callAuth.cjs +76 -0
  22. package/runtime/host/host.cjs +103 -0
  23. package/runtime/host/hostAnnouncement.cjs +70 -0
  24. package/runtime/host/hostClients/constants.cjs +7 -0
  25. package/runtime/host/hostClients/frontendBundleRefresh.cjs +158 -0
  26. package/runtime/host/hostClients/frontendRequests.cjs +166 -0
  27. package/runtime/host/hostClients/index.cjs +68 -0
  28. package/runtime/host/hostClients/rejections.cjs +37 -0
  29. package/runtime/host/hostSession.cjs +160 -0
  30. package/runtime/host/inlineProgressBar.cjs +128 -0
  31. package/runtime/host/localPeerServer.cjs +114 -0
  32. package/runtime/host/localRelayClient.cjs +151 -0
  33. package/runtime/host/matterhornrc.cjs +75 -0
  34. package/runtime/host/memberRootRegistry.cjs +132 -0
  35. package/runtime/host/nodePeer.cjs +127 -0
  36. package/runtime/host/nodePeerRacePatch.cjs +106 -0
  37. package/runtime/host/peerJsConfig.cjs +26 -0
  38. package/runtime/host/pushEgress.cjs +48 -0
  39. package/runtime/host/pushStorage.cjs +233 -0
  40. package/runtime/host/relay/config.cjs +179 -0
  41. package/runtime/host/relay/connectionCleanup.cjs +34 -0
  42. package/runtime/host/relay/connectionDispatcher.cjs +140 -0
  43. package/runtime/host/relay/matterhornOperationEvents.cjs +100 -0
  44. package/runtime/host/relay/matterhornRuntimeEventBridge.cjs +182 -0
  45. package/runtime/host/relay/nostrRelay.cjs +30 -0
  46. package/runtime/host/relay/peerStartup.cjs +81 -0
  47. package/runtime/host/relay.cjs +653 -0
  48. package/runtime/host/relayClientRouting.cjs +1054 -0
  49. package/runtime/host/relayConfig.cjs +156 -0
  50. package/runtime/host/relayHostAuth.cjs +39 -0
  51. package/runtime/host/relayHostMessages.cjs +367 -0
  52. package/runtime/host/relayHttp.cjs +48 -0
  53. package/runtime/host/relayIdentity.cjs +496 -0
  54. package/runtime/host/relayIncomingGate.cjs +153 -0
  55. package/runtime/host/relayMeshEnvelopes.cjs +522 -0
  56. package/runtime/host/relayPeerLifecycle.cjs +96 -0
  57. package/runtime/host/relayPeerSignals.cjs +175 -0
  58. package/runtime/host/relayRoomRuntimePersistence.cjs +129 -0
  59. package/runtime/host/relayStatus.cjs +160 -0
  60. package/runtime/host/sfuRelay.cjs +553 -0
  61. package/runtime/host/sqliteRelayStorage.cjs +352 -0
  62. package/runtime/host/wireValidation/client.cjs +213 -0
  63. package/runtime/host/wireValidation/host.cjs +33 -0
  64. package/runtime/host/wireValidation/index.cjs +13 -0
  65. package/runtime/host/wireValidation/peerSignal.cjs +35 -0
  66. package/runtime/host/wireValidation/presenceEvent.cjs +49 -0
  67. package/runtime/host/wireValidation/push.cjs +49 -0
  68. package/runtime/host/wireValidation/relay.cjs +131 -0
  69. package/runtime/host/wireValidation/shared.cjs +49 -0
  70. package/runtime/scripts/ensureWorkspaceSdkBuild.cjs +148 -0
  71. package/runtime/scripts/killChildTree.cjs +18 -0
@@ -0,0 +1,522 @@
1
+ const { PROTOCOL } = require("@mh-gg/host-config");
2
+ const { sendMessage } = require("@mh-gg/relay-core");
3
+ const { validateRelayEnvelope } = require("./wireValidation/index.cjs");
4
+ const crypto = require("node:crypto");
5
+ const { unwrapVapidGrant, vapidPublicKeyFromPrivateKey } = require("@mh-gg/push");
6
+
7
+ function sendInvalidRelayEnvelope(conn, validation) {
8
+ sendMessage(conn, {
9
+ type: "relay.error",
10
+ code: validation.code,
11
+ message: validation.message
12
+ });
13
+ }
14
+
15
+ function createRelayMeshEnvelopeHandler({
16
+ clientConnections,
17
+ clientRooms,
18
+ hostConnections,
19
+ mesh,
20
+ peerSignals,
21
+ relayPluginRuntimes,
22
+ encryptedRoomRuntimes,
23
+ relayStatus,
24
+ broadcastToRoom,
25
+ rememberPersonalStateFromHostMessage = () => {},
26
+ personalStateHostMessageForRoom = () => undefined,
27
+ pushStorage,
28
+ pushEgress,
29
+ eventStorage,
30
+ config
31
+ }) {
32
+ function sendRelayHost(conn, peerId, roomName, message) {
33
+ sendMessage(conn, {
34
+ type: "relay.host",
35
+ id: mesh.nextMessageId("relay.host"),
36
+ roomName,
37
+ peerId,
38
+ message
39
+ });
40
+ }
41
+
42
+ function pendingPushTtlSeconds(message) {
43
+ return Number.isInteger(message?.ttlSeconds) && message.ttlSeconds > 0 ? message.ttlSeconds : 60 * 60;
44
+ }
45
+
46
+ function persistPendingPush(message, reason) {
47
+ const pushId = message?.pushId || message?.id;
48
+ const userId = message?.target?.userId;
49
+ if (!pushStorage?.upsertPendingPush || !pushId || !userId) return false;
50
+ pushStorage.upsertPendingPush({
51
+ pushId,
52
+ userId,
53
+ payload: message,
54
+ ttlSeconds: pendingPushTtlSeconds(message)
55
+ });
56
+ if (process.env.MATTERHORN_DEBUG === "1") console.log(`matterhorn relay debug: push pending ${pushId} ${reason || "deferred"}`);
57
+ return true;
58
+ }
59
+
60
+ function sendPushAck(conn, message, status = "accepted") {
61
+ if (!conn || !message?.pushId) return;
62
+ sendMessage(conn, {
63
+ type: "relay.push.ack",
64
+ id: mesh.nextMessageId("relay.push.ack"),
65
+ pushId: message.pushId,
66
+ status
67
+ });
68
+ }
69
+
70
+ function off(conn, event, listener) {
71
+ if (typeof conn.off === "function") conn.off(event, listener);
72
+ else if (typeof conn.removeListener === "function") conn.removeListener(event, listener);
73
+ }
74
+
75
+ function sendPushWithAck(targetConn, message, timeoutMs = 500) {
76
+ return new Promise((resolve) => {
77
+ let timer;
78
+ let done = false;
79
+ const finish = (ok) => {
80
+ if (done) return;
81
+ done = true;
82
+ clearTimeout(timer);
83
+ off(targetConn, "data", onData);
84
+ off(targetConn, "close", onClose);
85
+ off(targetConn, "error", onError);
86
+ resolve(ok);
87
+ };
88
+ const onData = (incoming) => {
89
+ if (incoming?.type === "relay.push.ack" && incoming.pushId === message.pushId) finish(true);
90
+ };
91
+ const onClose = () => finish(false);
92
+ const onError = () => finish(false);
93
+ timer = setTimeout(() => finish(false), timeoutMs);
94
+ targetConn.on("data", onData);
95
+ targetConn.on("close", onClose);
96
+ targetConn.on("error", onError);
97
+ try {
98
+ sendMessage(targetConn, message);
99
+ } catch {
100
+ finish(false);
101
+ }
102
+ });
103
+ }
104
+
105
+ function sendHostUnavailable(conn, peerId, roomName) {
106
+ sendRelayHost(conn, peerId, roomName, {
107
+ type: "host/error",
108
+ protocol: PROTOCOL,
109
+ code: "host-unavailable",
110
+ message: "The room runtime is not connected to this relay."
111
+ });
112
+ }
113
+
114
+ function roomInfoMessage(roomName) {
115
+ const roomApp = relayPluginRuntimes?.roomApp?.(roomName);
116
+ if (!roomApp) return undefined;
117
+ const status = relayStatus.relayStatusMessage(undefined);
118
+ const runtimeCapabilities = relayPluginRuntimes?.runtimeCapabilities?.(roomName);
119
+ return {
120
+ type: "room.info",
121
+ protocol: PROTOCOL,
122
+ roomName,
123
+ roomApp,
124
+ relayHints: status.relayHints || [],
125
+ icedRelayHints: status.icedRelayHints || [],
126
+ relayClaim: status.relayClaim,
127
+ relayClaims: status.relayClaims || [],
128
+ relayHealth: status.relayHealth || [],
129
+ ...(runtimeCapabilities ? { runtimeCapabilities } : {}),
130
+ sfu: status.sfu
131
+ };
132
+ }
133
+
134
+ function relayHintMessage(roomName) {
135
+ const status = relayStatus.relayStatusMessage(undefined);
136
+ const roomApp = relayPluginRuntimes?.roomApp?.(roomName);
137
+ const runtimeCapabilities = relayPluginRuntimes?.runtimeCapabilities?.(roomName);
138
+ const message = {
139
+ type: "relay.hints",
140
+ roomName,
141
+ relayHints: status.relayHints || [],
142
+ icedRelayHints: status.icedRelayHints || [],
143
+ relayClaim: status.relayClaim,
144
+ relayClaims: status.relayClaims || [],
145
+ relayHealth: status.relayHealth || [],
146
+ sfu: status.sfu
147
+ };
148
+ if (roomApp) message.roomApp = roomApp;
149
+ if (runtimeCapabilities) message.runtimeCapabilities = runtimeCapabilities;
150
+ return message;
151
+ }
152
+
153
+ function forwardFrontendRequest(conn, envelope) {
154
+ const host = hostConnections.get(envelope.roomName);
155
+ if (host) {
156
+ sendMessage(host, {
157
+ type: "relay/client-message",
158
+ peerId: envelope.peerId,
159
+ message: envelope.message
160
+ });
161
+ return true;
162
+ }
163
+ return mesh.sendEnvelope(envelope, conn, { alreadyRemembered: true }) > 0;
164
+ }
165
+
166
+ function sendSnapshot(conn, peerId, roomName) {
167
+ if (!relayPluginRuntimes?.hasRoom?.(roomName)) return false;
168
+ void relayPluginRuntimes.snapshot(roomName).then((snapshot) => {
169
+ if (!snapshot) {
170
+ sendHostUnavailable(conn, peerId, roomName);
171
+ return;
172
+ }
173
+ sendRelayHost(conn, peerId, roomName, {
174
+ type: "host/matterhorn-state",
175
+ protocol: PROTOCOL,
176
+ roomName,
177
+ state: snapshot.state
178
+ });
179
+ }).catch((error) => {
180
+ sendRelayHost(conn, peerId, roomName, {
181
+ type: "host/error",
182
+ protocol: PROTOCOL,
183
+ code: "matterhorn-state-unavailable",
184
+ message: error?.message || "Matterhorn state is unavailable."
185
+ });
186
+ });
187
+ return true;
188
+ }
189
+
190
+ function handleRuntimeOperation(conn, envelope) {
191
+ if (!relayPluginRuntimes?.hasRoom?.(envelope.roomName)) return false;
192
+ void relayPluginRuntimes.handleClientOperation({
193
+ roomName: envelope.roomName,
194
+ peerId: envelope.peerId,
195
+ message: envelope.message,
196
+ sendToClient: (peerId, message, roomName) => sendRelayHost(conn, peerId, roomName || envelope.roomName, message),
197
+ broadcastToRoom,
198
+ broadcastToMesh: (message) => mesh.sendEnvelope(message, conn)
199
+ }).catch((error) => {
200
+ sendRelayHost(conn, envelope.peerId, envelope.roomName, {
201
+ type: "host/error",
202
+ protocol: PROTOCOL,
203
+ code: "operation-runtime-error",
204
+ message: error?.message || "Relay operation runtime failed."
205
+ });
206
+ });
207
+ return true;
208
+ }
209
+
210
+ function roomIndexSearchResultsMessage(roomName, request, result) {
211
+ return {
212
+ type: "host/room-index-search-results",
213
+ protocol: PROTOCOL,
214
+ roomName,
215
+ clientId: request.clientId,
216
+ requestId: request.requestId,
217
+ suite: request.suite,
218
+ keyId: request.keyId,
219
+ stream: request.stream,
220
+ mode: request.mode || "all",
221
+ events: Array.isArray(result?.events) ? result.events : [],
222
+ matches: Array.isArray(result?.matches) ? result.matches : [],
223
+ total: Number.isInteger(result?.total) ? result.total : 0,
224
+ queryTokenCount: Number.isInteger(result?.queryTokenCount) ? result.queryTokenCount : 0
225
+ };
226
+ }
227
+
228
+ function handleRoomIndexSearch(conn, envelope) {
229
+ const runtime = encryptedRoomRuntimes?.getRoom?.(envelope.roomName);
230
+ if (!runtime || typeof runtime.searchNgrams !== "function") return false;
231
+ const msg = envelope.message || {};
232
+ const result = runtime.searchNgrams({
233
+ suite: msg.suite,
234
+ keyId: msg.keyId,
235
+ tokens: msg.tokens,
236
+ stream: msg.stream,
237
+ mode: msg.mode,
238
+ minScore: msg.minScore,
239
+ limit: msg.limit,
240
+ offset: msg.offset,
241
+ order: msg.order
242
+ });
243
+ sendRelayHost(conn, envelope.peerId, envelope.roomName, roomIndexSearchResultsMessage(envelope.roomName, msg, result));
244
+ return true;
245
+ }
246
+
247
+ function handleRelayClient(conn, message) {
248
+ if (typeof message.peerId !== "string" || typeof message.roomName !== "string") return;
249
+ mesh.rememberRemoteClientRoute(message.peerId, conn);
250
+ mesh.rememberClientRoute(conn, message.roomName, message.message?.clientId, message.peerId);
251
+
252
+ switch (message.message?.type) {
253
+ case "client/hello": {
254
+ sendRelayHost(conn, message.peerId, message.roomName, relayHintMessage(message.roomName));
255
+ if (relayPluginRuntimes?.hasRoom?.(message.roomName)) return;
256
+ const host = hostConnections.get(message.roomName);
257
+ if (host) {
258
+ sendMessage(host, {
259
+ type: "relay/client-message",
260
+ peerId: message.peerId,
261
+ message: message.message
262
+ });
263
+ return;
264
+ }
265
+ break;
266
+ }
267
+ case "client/room-info": {
268
+ const info = roomInfoMessage(message.roomName);
269
+ if (info) {
270
+ sendRelayHost(conn, message.peerId, message.roomName, info);
271
+ return;
272
+ }
273
+ break;
274
+ }
275
+ case "client/matterhorn-state":
276
+ case "client/snapshot-request":
277
+ if (sendSnapshot(conn, message.peerId, message.roomName)) return;
278
+ break;
279
+ case "client/operation":
280
+ if (handleRuntimeOperation(conn, message)) return;
281
+ break;
282
+ case "client/room-index-search":
283
+ if (handleRoomIndexSearch(conn, message)) return;
284
+ break;
285
+ case "client/personal-state":
286
+ if (message.message.action === "subscribe") {
287
+ sendRelayHost(conn, message.peerId, message.roomName, relayHintMessage(message.roomName));
288
+ const cached = personalStateHostMessageForRoom(message.roomName);
289
+ if (cached) sendRelayHost(conn, message.peerId, message.roomName, cached);
290
+ }
291
+ break;
292
+ case "client/frontend-manifest":
293
+ case "client/frontend-chunk":
294
+ if (forwardFrontendRequest(conn, message)) return;
295
+ break;
296
+ case "client/push-grant": {
297
+ const msg = message.message;
298
+ const isTargetLocal = (msg.relayId === config?.relayPeerId || msg.relayId === config?.relayMeshPeerId);
299
+ if (isTargetLocal) {
300
+ try {
301
+ // X25519 grant key, not the Ed25519 identity key (see relayClientRouting).
302
+ const relayPrivateKey = crypto.createPrivateKey(config.relayIdentity.grantPrivateKeyPem);
303
+ const { vapidPrivateKey } = unwrapVapidGrant({
304
+ grant: msg.grant,
305
+ relayPrivateKey
306
+ });
307
+ const vapidPublicKey = vapidPublicKeyFromPrivateKey(vapidPrivateKey);
308
+ pushStorage?.upsertGrant({
309
+ userId: msg.userId,
310
+ vapidPrivateKey,
311
+ vapidPublicKey
312
+ });
313
+ sendRelayHost(conn, message.peerId, message.roomName, {
314
+ type: "client/push-grant.ok",
315
+ protocol: PROTOCOL
316
+ });
317
+ } catch (err) {
318
+ if (process.env.MATTERHORN_DEBUG === "1") {
319
+ console.error("Failed to unwrap VAPID grant from relay.client envelope:", err);
320
+ }
321
+ sendRelayHost(conn, message.peerId, message.roomName, {
322
+ type: "client/push-grant.error",
323
+ protocol: PROTOCOL,
324
+ code: "grant-unwrap-failed",
325
+ message: err?.message || "Failed to unwrap push grant."
326
+ });
327
+ }
328
+ } else {
329
+ sendRelayHost(conn, message.peerId, message.roomName, {
330
+ type: "client/push-grant.ok",
331
+ protocol: PROTOCOL
332
+ });
333
+ }
334
+ return;
335
+ }
336
+ case "client/push-register": {
337
+ const msg = message.message;
338
+ pushStorage?.upsertSubscription({
339
+ userId: msg.userId,
340
+ deviceId: msg.clientId,
341
+ subscription: msg.subscription
342
+ });
343
+ const grant = pushStorage?.selectGrant(msg.userId);
344
+ sendRelayHost(conn, message.peerId, message.roomName, {
345
+ type: "client/push-register.ok",
346
+ protocol: PROTOCOL,
347
+ hasGrant: Boolean(grant)
348
+ });
349
+ return;
350
+ }
351
+ default:
352
+ break;
353
+ }
354
+
355
+ if (mesh.sendEnvelope(message, conn, { alreadyRemembered: true }) === 0) {
356
+ sendHostUnavailable(conn, message.peerId, message.roomName);
357
+ }
358
+ }
359
+
360
+ function handleRelayHost(conn, message) {
361
+ if (typeof message.peerId !== "string") return;
362
+ const client = clientConnections.get(message.peerId);
363
+ if (client) {
364
+ sendMessage(client, message.message);
365
+ return;
366
+ }
367
+ if (mesh.sendDiscoveredClientRoute(message.peerId, message.message, conn, message.roomName)) return;
368
+ const relayConn = mesh.remoteClientRoute(message.peerId);
369
+ if (relayConn && relayConn !== conn) {
370
+ sendMessage(relayConn, message);
371
+ return;
372
+ }
373
+ mesh.sendEnvelope(message, conn, { alreadyRemembered: true });
374
+ }
375
+
376
+ function handleRelayEnvelope(conn, message) {
377
+ if (process.env.MATTERHORN_DEBUG === "1" && message?.type === "relay.peer-signal") {
378
+ console.log(`[relay-peer-signal] mesh-envelope-recv ${JSON.stringify({ roomName: message.roomName, sourceClientId: message.sourceClientId, targetClientId: message.targetClientId, hasId: Boolean(message.id) })}`);
379
+ }
380
+ const validation = validateRelayEnvelope(message);
381
+ if (!validation.ok) {
382
+ if (process.env.MATTERHORN_DEBUG === "1" && message?.type === "relay.peer-signal") {
383
+ console.log(`[relay-peer-signal] mesh-envelope-invalid ${JSON.stringify({ message: validation.message })}`);
384
+ }
385
+ sendInvalidRelayEnvelope(conn, validation);
386
+ return;
387
+ }
388
+ mesh.recordRelayLiveness?.(conn, message.relayAddress);
389
+
390
+ if (message.type === "relay.ping") {
391
+ mesh.sendPong?.(conn, message);
392
+ return;
393
+ }
394
+ if (message.type === "relay.pong") return;
395
+
396
+ if (!mesh.rememberMessage(message.id)) return;
397
+
398
+ switch (message.type) {
399
+ case "relay.status":
400
+ sendMessage(conn, relayStatus.relayStatusMessage(message.requestId));
401
+ return;
402
+ case "relay.client":
403
+ handleRelayClient(conn, message);
404
+ return;
405
+ case "relay.host":
406
+ handleRelayHost(conn, message);
407
+ return;
408
+ case "relay.peer-signal": {
409
+ if (typeof message.roomName !== "string") return;
410
+ if (typeof message.sourcePeerId === "string") mesh.rememberRemoteClientRoute(message.sourcePeerId, conn);
411
+ mesh.rememberClientRoute(conn, message.roomName, message.sourceClientId, message.sourcePeerId);
412
+ if (peerSignals.sendPeerSignalToClient(message, conn)) return;
413
+ mesh.sendEnvelope(message, conn, { alreadyRemembered: true });
414
+ return;
415
+ }
416
+ case "relay.matterhorn-operation":
417
+ if (typeof message.roomName !== "string") return;
418
+ void relayPluginRuntimes?.handleRelayOperation?.(message).catch((error) => {
419
+ sendMessage(conn, { type: "relay.error", code: "matterhorn-operation-failed", message: error?.message || "Relay Matterhorn operation failed" });
420
+ });
421
+ mesh.sendEnvelope(message, conn, { alreadyRemembered: true });
422
+ return;
423
+ case "relay.broadcast":
424
+ if (typeof message.roomName !== "string") return;
425
+ rememberPersonalStateFromHostMessage(message.roomName, message.message);
426
+ broadcastToRoom(message.roomName, message.message);
427
+ mesh.sendEnvelope(message, conn, { alreadyRemembered: true });
428
+ return;
429
+ case "relay.client.close": {
430
+ if (typeof message.peerId !== "string") return;
431
+ mesh.forgetRemoteClientRoute(message.peerId);
432
+ const client = clientConnections.get(message.peerId);
433
+ if (client) {
434
+ client.close?.();
435
+ return;
436
+ }
437
+ mesh.sendEnvelope(message, conn, { alreadyRemembered: true });
438
+ return;
439
+ }
440
+ case "relay.push": {
441
+ const userId = message.target?.userId;
442
+ if (!userId) return;
443
+ const grant = pushStorage?.selectGrant(userId);
444
+ if (grant) {
445
+ pushEgress?.sendRelayPush(message).then((result) => {
446
+ if (result?.delivered > 0) {
447
+ pushStorage?.deletePendingPush?.(message.pushId);
448
+ sendPushAck(conn, message, "delivered");
449
+ return;
450
+ }
451
+ if (persistPendingPush(message, result?.reason || "local-egress-deferred")) sendPushAck(conn, message);
452
+ }).catch((err) => {
453
+ if (process.env.MATTERHORN_DEBUG === "1") console.error(`Failed to send web push egress for user ${userId}:`, err);
454
+ if (persistPendingPush(message, "local-egress-error")) sendPushAck(conn, message);
455
+ });
456
+ return;
457
+ }
458
+ const events = eventStorage ? eventStorage.queryEvents([{ kinds: [9002] }]) : [];
459
+ let latestPayload = null;
460
+ for (const event of events) {
461
+ try {
462
+ const content = JSON.parse(event.content || "{}");
463
+ const payload = content.payload;
464
+ if (payload && payload.userId === userId) {
465
+ if (!latestPayload || payload.updatedAt > latestPayload.updatedAt) {
466
+ latestPayload = payload;
467
+ }
468
+ }
469
+ } catch {}
470
+ }
471
+ if (latestPayload && Array.isArray(latestPayload.relayIds)) {
472
+ const homeRelayIds = new Set(latestPayload.relayIds);
473
+ const connections = mesh.connections().filter((c) => {
474
+ const addr = c.MatterhornRelayAddress || c.MatterhornExpectedRelayAddress;
475
+ return addr && homeRelayIds.has(addr);
476
+ });
477
+ connections.sort((a, b) => {
478
+ const loadA = (a.MatterhornRelayLoad?.clients || 0) + (a.MatterhornRelayLoad?.roomHosts || 0);
479
+ const loadB = (b.MatterhornRelayLoad?.clients || 0) + (b.MatterhornRelayLoad?.roomHosts || 0);
480
+ return loadA - loadB;
481
+ });
482
+ const tryNext = async () => {
483
+ if (index >= connections.length) {
484
+ if (persistPendingPush(message, "forward-unconfirmed")) sendPushAck(conn, message);
485
+ mesh.sendEnvelope(message, conn, { alreadyRemembered: true });
486
+ return;
487
+ }
488
+ const targetConn = connections[index++];
489
+ if (targetConn.open === false) {
490
+ await tryNext();
491
+ return;
492
+ }
493
+ if (await sendPushWithAck(targetConn, message)) {
494
+ sendPushAck(conn, message);
495
+ return;
496
+ }
497
+ await tryNext();
498
+ };
499
+ let index = 0;
500
+ void tryNext();
501
+ } else {
502
+ if (persistPendingPush(message, "no-home-relay")) sendPushAck(conn, message);
503
+ mesh.sendEnvelope(message, conn, { alreadyRemembered: true });
504
+ }
505
+ return;
506
+ }
507
+ case "relay.push.ack":
508
+ pushStorage?.deletePendingPush?.(message.pushId);
509
+ return;
510
+ default:
511
+ return;
512
+ }
513
+ }
514
+
515
+ return {
516
+ handleRelayEnvelope
517
+ };
518
+ }
519
+
520
+ module.exports = {
521
+ createRelayMeshEnvelopeHandler
522
+ };
@@ -0,0 +1,96 @@
1
+ const DEFAULT_PEER_RECONNECT_DEBOUNCE_MS = 1000;
2
+
3
+ function isTransientPeerError(error) {
4
+ const type = error?.type;
5
+ const message = error?.message || "";
6
+ return type === "network"
7
+ || type === "peer-unavailable"
8
+ || type === "webrtc"
9
+ || message.includes("Lost connection to server")
10
+ || message.includes("Could not connect to peer");
11
+ }
12
+
13
+ function unavailablePeerIdFromError(error) {
14
+ const message = error?.message || "";
15
+ const match = message.match(/Could not connect to peer ([^\s.]+)/);
16
+ return match?.[1];
17
+ }
18
+
19
+ function isUnavailablePeerError(error) {
20
+ return error?.type === "peer-unavailable" || Boolean(unavailablePeerIdFromError(error));
21
+ }
22
+
23
+ function transientPeerErrorLabel(error) {
24
+ const message = error?.message || "";
25
+ if (error?.type === "network" || message.includes("Lost connection to server")) return "signaling network error";
26
+ if (message.includes("Could not connect to peer")) return "remote relay peer unavailable";
27
+ return "transient peer error";
28
+ }
29
+
30
+ function createRelayPeerLifecycle({
31
+ debug,
32
+ isClosing,
33
+ mesh,
34
+ peerReconnects,
35
+ relayPeer,
36
+ reconnectDebounceMs = DEFAULT_PEER_RECONNECT_DEBOUNCE_MS
37
+ }) {
38
+ function iceUnavailableRelayPeer(error) {
39
+ const peerId = unavailablePeerIdFromError(error);
40
+ if (!peerId) return false;
41
+ const relayAddress = mesh.iceUnavailablePeer(peerId);
42
+ if (!relayAddress) return false;
43
+ debug(`relay peer unavailable; icing ${relayAddress}`);
44
+ return true;
45
+ }
46
+
47
+ function reconnectPeer(peer, label, reason = "disconnect") {
48
+ if (isClosing() || !peer || peer.destroyed || typeof peer.reconnect !== "function") return;
49
+ const peerId = peerReconnects.stablePeerId(peer);
50
+ if (peer.open && peer.disconnected !== true) {
51
+ debug(`${label} reconnect skipped after ${reason}; ${peerId} is already open`);
52
+ return;
53
+ }
54
+ const now = Date.now();
55
+ const lastReconnectAt = peer.MatterhornLastReconnectAt || 0;
56
+ if (now - lastReconnectAt < reconnectDebounceMs) {
57
+ debug(`${label} reconnect skipped after ${reason}; ${peerId} already attempted recently`);
58
+ return;
59
+ }
60
+ peer.MatterhornLastReconnectAt = now;
61
+ peerReconnects.warn(peer, label, reason);
62
+ try {
63
+ peer.reconnect();
64
+ } catch (error) {
65
+ const message = error?.message || String(error);
66
+ if (!message.includes("cannot reconnect because it is not disconnected")) {
67
+ debug(`${label} reconnect failed after ${reason}: ${message}`);
68
+ }
69
+ }
70
+ }
71
+
72
+ function handlePeerError(peer, label, error) {
73
+ if (!isTransientPeerError(error)) {
74
+ console.error(`${label} error:`, error);
75
+ return;
76
+ }
77
+ const errorLabel = transientPeerErrorLabel(error);
78
+ debug(`${label} ${errorLabel}: ${error?.message || String(error)}`);
79
+ if (peer === relayPeer() && isUnavailablePeerError(error)) iceUnavailableRelayPeer(error);
80
+ if (error?.type === "network") reconnectPeer(peer, label, errorLabel);
81
+ }
82
+
83
+ return {
84
+ handlePeerError,
85
+ reconnectPeer
86
+ };
87
+ }
88
+
89
+ module.exports = {
90
+ DEFAULT_PEER_RECONNECT_DEBOUNCE_MS,
91
+ createRelayPeerLifecycle,
92
+ isTransientPeerError,
93
+ isUnavailablePeerError,
94
+ transientPeerErrorLabel,
95
+ unavailablePeerIdFromError
96
+ };