@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.
- package/README.md +5 -0
- package/bin/matterhorn.cjs +57 -0
- package/package.json +49 -0
- package/runtime/bin/appFrontend/artifacts.cjs +25 -0
- package/runtime/bin/appFrontend/buildServers.cjs +176 -0
- package/runtime/bin/appFrontend/commandEnv.cjs +74 -0
- package/runtime/bin/appFrontend/commandPolicy.cjs +23 -0
- package/runtime/bin/appFrontend/devServers.cjs +150 -0
- package/runtime/bin/appFrontend/httpServers.cjs +221 -0
- package/runtime/bin/appFrontend/paths.cjs +103 -0
- package/runtime/bin/appFrontend/ports.cjs +36 -0
- package/runtime/bin/appFrontend/processes.cjs +127 -0
- package/runtime/bin/appFrontend.cjs +45 -0
- package/runtime/bin/appHostCommand.cjs +381 -0
- package/runtime/bin/matterhorn.cjs +501 -0
- package/runtime/bin/matterhornAppLoader.cjs +588 -0
- package/runtime/bin/matterhornApps.cjs +223 -0
- package/runtime/bin/matterhornDeploy.cjs +108 -0
- package/runtime/bin/matterhornEmitAppBundle.cjs +20 -0
- package/runtime/bin/matterhornInstall.cjs +609 -0
- package/runtime/host/callAuth.cjs +76 -0
- package/runtime/host/host.cjs +103 -0
- package/runtime/host/hostAnnouncement.cjs +70 -0
- package/runtime/host/hostClients/constants.cjs +7 -0
- package/runtime/host/hostClients/frontendBundleRefresh.cjs +158 -0
- package/runtime/host/hostClients/frontendRequests.cjs +166 -0
- package/runtime/host/hostClients/index.cjs +68 -0
- package/runtime/host/hostClients/rejections.cjs +37 -0
- package/runtime/host/hostSession.cjs +160 -0
- package/runtime/host/inlineProgressBar.cjs +128 -0
- package/runtime/host/localPeerServer.cjs +114 -0
- package/runtime/host/localRelayClient.cjs +151 -0
- package/runtime/host/matterhornrc.cjs +75 -0
- package/runtime/host/memberRootRegistry.cjs +132 -0
- package/runtime/host/nodePeer.cjs +127 -0
- package/runtime/host/nodePeerRacePatch.cjs +106 -0
- package/runtime/host/peerJsConfig.cjs +26 -0
- package/runtime/host/pushEgress.cjs +48 -0
- package/runtime/host/pushStorage.cjs +233 -0
- package/runtime/host/relay/config.cjs +179 -0
- package/runtime/host/relay/connectionCleanup.cjs +34 -0
- package/runtime/host/relay/connectionDispatcher.cjs +140 -0
- package/runtime/host/relay/matterhornOperationEvents.cjs +100 -0
- package/runtime/host/relay/matterhornRuntimeEventBridge.cjs +182 -0
- package/runtime/host/relay/nostrRelay.cjs +30 -0
- package/runtime/host/relay/peerStartup.cjs +81 -0
- package/runtime/host/relay.cjs +653 -0
- package/runtime/host/relayClientRouting.cjs +1054 -0
- package/runtime/host/relayConfig.cjs +156 -0
- package/runtime/host/relayHostAuth.cjs +39 -0
- package/runtime/host/relayHostMessages.cjs +367 -0
- package/runtime/host/relayHttp.cjs +48 -0
- package/runtime/host/relayIdentity.cjs +496 -0
- package/runtime/host/relayIncomingGate.cjs +153 -0
- package/runtime/host/relayMeshEnvelopes.cjs +522 -0
- package/runtime/host/relayPeerLifecycle.cjs +96 -0
- package/runtime/host/relayPeerSignals.cjs +175 -0
- package/runtime/host/relayRoomRuntimePersistence.cjs +129 -0
- package/runtime/host/relayStatus.cjs +160 -0
- package/runtime/host/sfuRelay.cjs +553 -0
- package/runtime/host/sqliteRelayStorage.cjs +352 -0
- package/runtime/host/wireValidation/client.cjs +213 -0
- package/runtime/host/wireValidation/host.cjs +33 -0
- package/runtime/host/wireValidation/index.cjs +13 -0
- package/runtime/host/wireValidation/peerSignal.cjs +35 -0
- package/runtime/host/wireValidation/presenceEvent.cjs +49 -0
- package/runtime/host/wireValidation/push.cjs +49 -0
- package/runtime/host/wireValidation/relay.cjs +131 -0
- package/runtime/host/wireValidation/shared.cjs +49 -0
- package/runtime/scripts/ensureWorkspaceSdkBuild.cjs +148 -0
- package/runtime/scripts/killChildTree.cjs +18 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const WebSocket = require("ws");
|
|
4
|
+
const { createRelaySfu } = require("./sfuRelay.cjs");
|
|
5
|
+
const { WebSocketRelayConnection } = require("@mh-gg/host-ipc");
|
|
6
|
+
const {
|
|
7
|
+
createEncryptedRoomRuntimeManager,
|
|
8
|
+
createPeerReconnectTracker,
|
|
9
|
+
createRelayPluginRuntimeManager,
|
|
10
|
+
isNewerRoomState,
|
|
11
|
+
waitForPeerOpen
|
|
12
|
+
} = require("@mh-gg/relay-runtime");
|
|
13
|
+
const {
|
|
14
|
+
createRelayInformation,
|
|
15
|
+
createRelayChunkAssembler,
|
|
16
|
+
isRelayChunk,
|
|
17
|
+
isNostrMessage,
|
|
18
|
+
peerJsOptionsFromAddress,
|
|
19
|
+
sendMessage
|
|
20
|
+
} = require("@mh-gg/relay-core");
|
|
21
|
+
const {
|
|
22
|
+
createRelayMeshRouter
|
|
23
|
+
} = require("@mh-gg/relay-mesh");
|
|
24
|
+
const { createRelayHttpServer } = require("./relayHttp.cjs");
|
|
25
|
+
const { createRelayClientRouting } = require("./relayClientRouting.cjs");
|
|
26
|
+
const { createPushStorage } = require("./pushStorage.cjs");
|
|
27
|
+
const { createPushEgress } = require("./pushEgress.cjs");
|
|
28
|
+
const { createRelayHostAuth } = require("./relayHostAuth.cjs");
|
|
29
|
+
const { createRelayHostMessageHandler } = require("./relayHostMessages.cjs");
|
|
30
|
+
const { createRelayIncomingGate, isRelayEnvelopeMessage } = require("./relayIncomingGate.cjs");
|
|
31
|
+
const { createRelayMeshEnvelopeHandler } = require("./relayMeshEnvelopes.cjs");
|
|
32
|
+
const { createRelayPeerLifecycle, isUnavailablePeerError } = require("./relayPeerLifecycle.cjs");
|
|
33
|
+
const { createRelayPeerSignalRouter } = require("./relayPeerSignals.cjs");
|
|
34
|
+
const { createRelayStatus } = require("./relayStatus.cjs");
|
|
35
|
+
const { validateRelayHints } = require("./wireValidation/index.cjs");
|
|
36
|
+
const { createConnectionCleanup } = require("./relay/connectionCleanup.cjs");
|
|
37
|
+
const { createConnectionDataHandler } = require("./relay/connectionDispatcher.cjs");
|
|
38
|
+
const { startRelayPeers } = require("./relay/peerStartup.cjs");
|
|
39
|
+
const { createRelayEventStore } = require("./relay/nostrRelay.cjs");
|
|
40
|
+
const { createMatterhornRuntimeEventBridge } = require("./relay/matterhornRuntimeEventBridge.cjs");
|
|
41
|
+
const { createRelayOptionsFromArgs, createRelayRuntimeConfig, defaultStoragePath, parseArgs, resolveStoragePath } = require("./relay/config.cjs");
|
|
42
|
+
const { startLocalPeerServer } = require("./localPeerServer.cjs");
|
|
43
|
+
const { signRelayControl } = require("./relayIdentity.cjs");
|
|
44
|
+
const { createRelayRoomRuntimePersistence, loadRelayRoomRuntimeSnapshot } = require("./relayRoomRuntimePersistence.cjs");
|
|
45
|
+
|
|
46
|
+
function allowTimerExit(timer) {
|
|
47
|
+
if (typeof timer?.unref === "function") timer.unref();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createMatterhornRelay(options = {}) {
|
|
51
|
+
const config = createRelayRuntimeConfig(options);
|
|
52
|
+
|
|
53
|
+
const startedAt = Date.now();
|
|
54
|
+
let relayPluginRuntimes;
|
|
55
|
+
let relayClientRouting;
|
|
56
|
+
let matterhornRuntimeEvents;
|
|
57
|
+
let encryptedRoomRuntimes;
|
|
58
|
+
const { relay, storage } = createRelayEventStore(config, {
|
|
59
|
+
...options,
|
|
60
|
+
onEvent: (event, context) => matterhornRuntimeEvents?.handleMatterhornOperationEvent(event, context)
|
|
61
|
+
});
|
|
62
|
+
const pushStorage = createPushStorage(storage.db);
|
|
63
|
+
const pushEgress = createPushEgress({
|
|
64
|
+
pushStorage,
|
|
65
|
+
subject: config.pushSubject || "mailto:admin@matterhorn.gg"
|
|
66
|
+
});
|
|
67
|
+
const wss = new WebSocket.WebSocketServer({
|
|
68
|
+
noServer: true,
|
|
69
|
+
maxPayload: Math.max(
|
|
70
|
+
config.maxHostIpcMessageBytes,
|
|
71
|
+
config.maxMeshMessageBytes,
|
|
72
|
+
config.maxNostrMessageBytes,
|
|
73
|
+
config.maxRoomMessageBytes,
|
|
74
|
+
config.maxCallSignalBytes
|
|
75
|
+
)
|
|
76
|
+
});
|
|
77
|
+
const clientConnections = new Map();
|
|
78
|
+
const clientRooms = new Map();
|
|
79
|
+
const hostConnections = new Map();
|
|
80
|
+
const roomStores = new Map();
|
|
81
|
+
const relayChunkAssemblers = new WeakMap();
|
|
82
|
+
let roomPeer;
|
|
83
|
+
let relayPeer;
|
|
84
|
+
let closePromise;
|
|
85
|
+
let closing = false;
|
|
86
|
+
const relayDialPeers = new Map();
|
|
87
|
+
|
|
88
|
+
const sfu = createRelaySfu({
|
|
89
|
+
enabled: config.sfuEnabled,
|
|
90
|
+
peerId: config.sfuPeerId,
|
|
91
|
+
peerAddress: config.sfuPeerAddress,
|
|
92
|
+
maxParticipants: config.sfuMaxParticipants,
|
|
93
|
+
iceServers: config.iceServers,
|
|
94
|
+
createPeer: config.createPeer,
|
|
95
|
+
getGuest: sfuGuest,
|
|
96
|
+
roomSecretForRoom: (roomName) => roomStores.get(roomName)?.roomSecret || config.roomSecret,
|
|
97
|
+
onStatus: (roomName) => relayClientRouting?.broadcastSfuStatus(roomName),
|
|
98
|
+
logger: console
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
function debugRelay(message) {
|
|
102
|
+
if (process.env.MATTERHORN_DEBUG === "1") console.log(`matterhorn relay debug: ${message}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function runtimeInputRoomName(input = {}) {
|
|
106
|
+
return input.roomName || input.room?.id || input.roomId;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function runtimeInputAppPackId(input = {}) {
|
|
110
|
+
return input.room?.appPack?.id || input.appPack?.id || input.installed?.roomAppPack?.id || input.installed?.appPack?.id;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function runtimeInputState(input = {}) {
|
|
114
|
+
if (input.state && typeof input.state === "object") return input.state;
|
|
115
|
+
if (input.snapshot?.state && typeof input.snapshot.state === "object") return input.snapshot.state;
|
|
116
|
+
if (input.snapshot && typeof input.snapshot === "object" && input.snapshot.schemaVersion) return input.snapshot;
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function runtimeInputOperations(input = {}) {
|
|
121
|
+
if (Array.isArray(input.operations)) return input.operations;
|
|
122
|
+
if (Array.isArray(input.snapshot?.operations)) return input.snapshot.operations;
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function persistedStateMatchesInput(state, input) {
|
|
127
|
+
if (!state || typeof state !== "object") return false;
|
|
128
|
+
const roomName = runtimeInputRoomName(input);
|
|
129
|
+
if (state.roomId !== roomName) return false;
|
|
130
|
+
const appPackId = runtimeInputAppPackId(input);
|
|
131
|
+
if (appPackId && state.appPack?.id !== appPackId) return false;
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function prepareRelayRoomRuntimeInput(input = {}) {
|
|
136
|
+
const roomName = runtimeInputRoomName(input);
|
|
137
|
+
if (!roomName || !config.storagePath) return input;
|
|
138
|
+
|
|
139
|
+
const persisted = loadRelayRoomRuntimeSnapshot(config.storagePath, roomName);
|
|
140
|
+
const persistedState = persistedStateMatchesInput(persisted?.state, input) ? persisted.state : undefined;
|
|
141
|
+
const incomingState = runtimeInputState(input);
|
|
142
|
+
const usePersisted = Boolean(persistedState && (!incomingState || isNewerRoomState(persistedState, incomingState)));
|
|
143
|
+
const initialState = usePersisted ? persistedState : incomingState;
|
|
144
|
+
const initialOperations = usePersisted ? persisted.operations : runtimeInputOperations(input);
|
|
145
|
+
const persistence = createRelayRoomRuntimePersistence({
|
|
146
|
+
storagePath: config.storagePath,
|
|
147
|
+
roomName,
|
|
148
|
+
initialState,
|
|
149
|
+
initialOperations
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const prepared = { ...input };
|
|
153
|
+
if (!prepared.store) prepared.store = persistence.store;
|
|
154
|
+
if (!prepared.operationLog) prepared.operationLog = persistence.operationLog;
|
|
155
|
+
if (usePersisted) {
|
|
156
|
+
prepared.state = initialState;
|
|
157
|
+
prepared.operations = initialOperations;
|
|
158
|
+
}
|
|
159
|
+
return prepared;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function peerJsSignalingKey(signaling) {
|
|
163
|
+
return `${signaling.secure ? "wss" : "ws"}://${signaling.host}:${signaling.port}${signaling.path}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function relayAddressSignaling(address) {
|
|
167
|
+
return peerJsOptionsFromAddress(address, config.peerJsSignaling);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function peerForRelayAddress(address) {
|
|
171
|
+
const signaling = relayAddressSignaling(address);
|
|
172
|
+
if (!signaling) throw new Error(`Relay address has no PeerJS signaling endpoint: ${address}`);
|
|
173
|
+
const key = peerJsSignalingKey(signaling);
|
|
174
|
+
const localKey = peerJsSignalingKey(config.peerJsSignaling);
|
|
175
|
+
if (key === localKey) {
|
|
176
|
+
if (!relayPeer?.open) throw new Error("Local relay mesh PeerJS peer is not open");
|
|
177
|
+
return relayPeer;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const existing = relayDialPeers.get(key);
|
|
181
|
+
if (existing?.peer && !existing.peer.destroyed) {
|
|
182
|
+
if (existing.peer.open) return existing.peer;
|
|
183
|
+
return existing.openPromise.then(() => existing.peer);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const peer = await config.createPeer(undefined, config.iceServers, { signaling });
|
|
187
|
+
peer.MatterhornStableId = `relay-dial:${key}`;
|
|
188
|
+
peer.on("error", (error) => {
|
|
189
|
+
peerLifecycle.handlePeerError(peer, `matterhorn relay dial peer ${key}`, error);
|
|
190
|
+
});
|
|
191
|
+
peer.on("disconnected", () => {
|
|
192
|
+
peerLifecycle.reconnectPeer(peer, `matterhorn relay dial peer ${key}`, "disconnected event");
|
|
193
|
+
});
|
|
194
|
+
const openPromise = waitForPeerOpen(peer, `matterhorn relay dial peer ${key}`);
|
|
195
|
+
relayDialPeers.set(key, { peer, openPromise });
|
|
196
|
+
await openPromise;
|
|
197
|
+
return peer;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function connectRelayAddress(address, dialOptions) {
|
|
201
|
+
const signaling = relayAddressSignaling(address);
|
|
202
|
+
if (signaling && peerJsSignalingKey(signaling) === peerJsSignalingKey(config.peerJsSignaling)) {
|
|
203
|
+
if (!relayPeer?.open) return undefined;
|
|
204
|
+
return relayPeer.connect(dialOptions.peerId, dialOptions.options);
|
|
205
|
+
}
|
|
206
|
+
return peerForRelayAddress(address).then((peer) => peer.connect(dialOptions.peerId, dialOptions.options));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let relayStatus;
|
|
210
|
+
let peerLifecycle;
|
|
211
|
+
const mesh = createRelayMeshRouter({
|
|
212
|
+
relayAddress: config.relayAddress,
|
|
213
|
+
relayMeshPeerId: config.relayMeshPeerId,
|
|
214
|
+
relayPeers: config.relayPeers,
|
|
215
|
+
defaultSignaling: config.peerJsSignaling,
|
|
216
|
+
activeRelayFanout: config.activeRelayFanout,
|
|
217
|
+
heartbeatMs: config.relayHeartbeatMs,
|
|
218
|
+
heartbeatTimeoutMs: config.relayHeartbeatTimeoutMs,
|
|
219
|
+
heartbeatFailoverMs: config.relayFailoverMs,
|
|
220
|
+
getRelayPeer: () => relayPeer,
|
|
221
|
+
connectRelayAddress,
|
|
222
|
+
attachConnection: (conn) => attachConnection(conn, { relayPeer: true }),
|
|
223
|
+
isUnavailablePeerError,
|
|
224
|
+
markRelayConnection: (conn) => relay.markRelayConnection(conn),
|
|
225
|
+
relayStatusMessage: (requestId, statusOptions) => relayStatus.relayStatusMessage(requestId, statusOptions),
|
|
226
|
+
debug: debugRelay
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
relayPluginRuntimes = createRelayPluginRuntimeManager({
|
|
230
|
+
relayAddress: config.relayAddress,
|
|
231
|
+
logger: console,
|
|
232
|
+
authenticateActor: options.authenticateActor,
|
|
233
|
+
sendRelayEnvelope: (message) => mesh.sendEnvelope(message)
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
encryptedRoomRuntimes = createEncryptedRoomRuntimeManager({
|
|
237
|
+
logger: console,
|
|
238
|
+
maxEventsPerStream: config.maxEventsPerStream
|
|
239
|
+
});
|
|
240
|
+
// Rebuild encrypted-room per-stream indexes from persisted v2 events on startup.
|
|
241
|
+
// This requires no room secrets; only plaintext v2 header tags are inspected.
|
|
242
|
+
try {
|
|
243
|
+
const rebuilt = encryptedRoomRuntimes.rebuildFromEvents((filters) => relay.query(filters));
|
|
244
|
+
if (rebuilt.inserted > 0) {
|
|
245
|
+
debugRelay(`rebuilt encrypted room indexes: ${rebuilt.operationEvents || 0} v2 events, ${rebuilt.ngramIndexEvents || 0} ngram index events`);
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
// Relay may not be fully ready yet; events will be indexed as they arrive.
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
matterhornRuntimeEvents = createMatterhornRuntimeEventBridge({
|
|
252
|
+
debug: debugRelay,
|
|
253
|
+
encryptedRoomRuntimes,
|
|
254
|
+
operationEventIdentity: options.operationEventIdentity,
|
|
255
|
+
relay: () => relay,
|
|
256
|
+
relayClientRouting: () => relayClientRouting,
|
|
257
|
+
relayPluginRuntimes: () => relayPluginRuntimes,
|
|
258
|
+
roomStores: () => roomStores,
|
|
259
|
+
startedAt
|
|
260
|
+
});
|
|
261
|
+
let relayHostMessages;
|
|
262
|
+
const peerReconnects = createPeerReconnectTracker({
|
|
263
|
+
debug: debugRelay,
|
|
264
|
+
warn: (message) => console.warn(message)
|
|
265
|
+
});
|
|
266
|
+
peerLifecycle = createRelayPeerLifecycle({
|
|
267
|
+
debug: debugRelay,
|
|
268
|
+
isClosing: () => closing,
|
|
269
|
+
mesh,
|
|
270
|
+
peerReconnects,
|
|
271
|
+
relayPeer: () => relayPeer
|
|
272
|
+
});
|
|
273
|
+
const hostAuth = createRelayHostAuth(config.ipcSecret);
|
|
274
|
+
const incomingGate = createRelayIncomingGate(config);
|
|
275
|
+
const peerSignals = createRelayPeerSignalRouter({
|
|
276
|
+
config,
|
|
277
|
+
clientConnections,
|
|
278
|
+
mesh,
|
|
279
|
+
sfu
|
|
280
|
+
});
|
|
281
|
+
relayClientRouting = createRelayClientRouting({
|
|
282
|
+
clientConnections,
|
|
283
|
+
clientRooms,
|
|
284
|
+
config,
|
|
285
|
+
debug: debugRelay,
|
|
286
|
+
hostConnections,
|
|
287
|
+
isClosing: () => closing,
|
|
288
|
+
mesh,
|
|
289
|
+
peerSignals,
|
|
290
|
+
publishMatterhornOperation: matterhornRuntimeEvents.publishMatterhornOperation,
|
|
291
|
+
relayPluginRuntimes,
|
|
292
|
+
encryptedRoomRuntimes,
|
|
293
|
+
sfu,
|
|
294
|
+
pushStorage,
|
|
295
|
+
pushEgress,
|
|
296
|
+
eventStorage: storage
|
|
297
|
+
});
|
|
298
|
+
relayHostMessages = createRelayHostMessageHandler({
|
|
299
|
+
config,
|
|
300
|
+
relayPluginRuntimes,
|
|
301
|
+
hostConnections,
|
|
302
|
+
clientConnections,
|
|
303
|
+
mesh,
|
|
304
|
+
roomStores,
|
|
305
|
+
sendToClient: relayClientRouting.sendToClient,
|
|
306
|
+
catchUpMatterhornRuntime: matterhornRuntimeEvents.catchUpMatterhornRuntime,
|
|
307
|
+
prepareRoomRuntimeInput: prepareRelayRoomRuntimeInput
|
|
308
|
+
});
|
|
309
|
+
relayStatus = createRelayStatus({
|
|
310
|
+
config,
|
|
311
|
+
startedAt,
|
|
312
|
+
storage,
|
|
313
|
+
mesh,
|
|
314
|
+
relay,
|
|
315
|
+
sfu,
|
|
316
|
+
peerReconnects,
|
|
317
|
+
clientConnections,
|
|
318
|
+
hostConnections,
|
|
319
|
+
roomPeer: () => roomPeer,
|
|
320
|
+
relayPeer: () => relayPeer
|
|
321
|
+
});
|
|
322
|
+
const relayMeshEnvelopes = createRelayMeshEnvelopeHandler({
|
|
323
|
+
clientConnections,
|
|
324
|
+
clientRooms,
|
|
325
|
+
hostConnections,
|
|
326
|
+
mesh,
|
|
327
|
+
peerSignals,
|
|
328
|
+
relayPluginRuntimes,
|
|
329
|
+
encryptedRoomRuntimes,
|
|
330
|
+
relayStatus,
|
|
331
|
+
broadcastToRoom: relayClientRouting.broadcastToRoom,
|
|
332
|
+
rememberPersonalStateFromHostMessage: relayClientRouting.rememberPersonalStateFromHostMessage,
|
|
333
|
+
personalStateHostMessageForRoom: relayClientRouting.personalStateHostMessageForRoom,
|
|
334
|
+
pushStorage,
|
|
335
|
+
pushEgress,
|
|
336
|
+
eventStorage: storage,
|
|
337
|
+
config
|
|
338
|
+
});
|
|
339
|
+
async function retryPendingPushes() {
|
|
340
|
+
if (closing || !pushStorage?.selectPendingPushes) return;
|
|
341
|
+
pushStorage.pruneExpiredPendingPushes?.();
|
|
342
|
+
for (const pending of pushStorage.selectPendingPushes(100)) {
|
|
343
|
+
const relayPush = pending.payload;
|
|
344
|
+
const userId = relayPush?.target?.userId || pending.userId;
|
|
345
|
+
if (!relayPush || !userId) {
|
|
346
|
+
pushStorage.deletePendingPush?.(pending.pushId);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
pushStorage.incrementPendingPushAttempts?.(pending.pushId);
|
|
350
|
+
const grant = pushStorage.selectGrant(userId);
|
|
351
|
+
if (grant) {
|
|
352
|
+
try {
|
|
353
|
+
const result = await pushEgress.sendRelayPush(relayPush);
|
|
354
|
+
if (result?.delivered > 0) pushStorage.deletePendingPush?.(pending.pushId);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
if (process.env.MATTERHORN_DEBUG === "1") console.error(`Failed to retry pending push ${pending.pushId}:`, err);
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
mesh.sendEnvelope(relayPush);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const pendingPushSweep = setInterval(() => {
|
|
364
|
+
void retryPendingPushes();
|
|
365
|
+
}, 30_000);
|
|
366
|
+
allowTimerExit(pendingPushSweep);
|
|
367
|
+
const handleConnectionData = createConnectionDataHandler({
|
|
368
|
+
hostAuth,
|
|
369
|
+
isNostrMessage,
|
|
370
|
+
isRelayEnvelopeMessage,
|
|
371
|
+
mesh,
|
|
372
|
+
promoteRelayConnection,
|
|
373
|
+
relayClientRouting,
|
|
374
|
+
relayHostMessages,
|
|
375
|
+
relayMeshEnvelopes,
|
|
376
|
+
relayStatus,
|
|
377
|
+
relayTrust: config.relayTrustStore,
|
|
378
|
+
validateRelayHints
|
|
379
|
+
});
|
|
380
|
+
const cleanupConnection = createConnectionCleanup({
|
|
381
|
+
clientConnections,
|
|
382
|
+
clientRooms,
|
|
383
|
+
hostConnections,
|
|
384
|
+
incomingGate,
|
|
385
|
+
mesh,
|
|
386
|
+
relayClientRouting
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const server = createRelayHttpServer({
|
|
390
|
+
wss,
|
|
391
|
+
allowedWebSocketPaths: ["/room", "/nostr"],
|
|
392
|
+
relayInformation: () => createRelayInformation({
|
|
393
|
+
name: "matterhorn peer relay",
|
|
394
|
+
description: "Nostr relay bridge for matterhorn PeerJS data channels.",
|
|
395
|
+
maxMessageLength: config.maxEventBytes,
|
|
396
|
+
maxEvents: config.maxEvents
|
|
397
|
+
}),
|
|
398
|
+
health: () => relayStatus.healthDocument()
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
wss.on("connection", (socket) => {
|
|
402
|
+
attachConnection(new WebSocketRelayConnection(socket, {
|
|
403
|
+
maxMessageBytes: config.maxHostIpcMessageBytes
|
|
404
|
+
}));
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
function findGuestForClient(runtimeState, clientId) {
|
|
408
|
+
for (const pluginState of Object.values(runtimeState?.plugins || {})) {
|
|
409
|
+
const guests = pluginState?.guests || {};
|
|
410
|
+
const guest = guestFromCollection(guests, clientId);
|
|
411
|
+
if (guest) return guest;
|
|
412
|
+
}
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function guestFromCollection(guests, clientId) {
|
|
417
|
+
if (guests[clientId]) return guests[clientId];
|
|
418
|
+
return Object.values(guests).find((guest) => {
|
|
419
|
+
return guest?.callClientId === clientId || guest?.clientId === clientId || guest?.id === clientId || guest?.memberId === clientId;
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function sfuGuest(roomName, clientId) {
|
|
424
|
+
const runtime = relayPluginRuntimes?.get?.(roomName)?.runtime;
|
|
425
|
+
if (!runtime) return undefined;
|
|
426
|
+
return findGuestForClient(await runtime.getState(), clientId);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function relayDisconnectingMessage() {
|
|
430
|
+
const relayControl = signRelayControl(config.relayIdentity, {
|
|
431
|
+
type: "relay.disconnecting",
|
|
432
|
+
relayAddress: config.relayAddress,
|
|
433
|
+
roomPeerId: config.relayPeerId,
|
|
434
|
+
relayMeshPeerId: config.relayMeshPeerId
|
|
435
|
+
});
|
|
436
|
+
return {
|
|
437
|
+
type: "relay.disconnecting",
|
|
438
|
+
relayAddress: config.relayAddress,
|
|
439
|
+
roomPeerId: config.relayPeerId,
|
|
440
|
+
relayMeshPeerId: config.relayMeshPeerId,
|
|
441
|
+
relayClaim: config.relayClaim,
|
|
442
|
+
relayControl
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function announceRelayDisconnecting() {
|
|
447
|
+
const message = relayDisconnectingMessage();
|
|
448
|
+
for (const conn of clientConnections.values()) sendMessage(conn, message);
|
|
449
|
+
for (const conn of mesh.connections()) sendMessage(conn, message);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function receiveRelayMessage(conn, message) {
|
|
453
|
+
if (!isRelayChunk(message)) return message;
|
|
454
|
+
let assembler = relayChunkAssemblers.get(conn);
|
|
455
|
+
if (!assembler) {
|
|
456
|
+
assembler = createRelayChunkAssembler({
|
|
457
|
+
maxBytes: Math.max(
|
|
458
|
+
config.maxHostIpcMessageBytes,
|
|
459
|
+
config.maxMeshMessageBytes,
|
|
460
|
+
config.maxNostrMessageBytes,
|
|
461
|
+
config.maxRoomMessageBytes,
|
|
462
|
+
config.maxCallSignalBytes
|
|
463
|
+
)
|
|
464
|
+
});
|
|
465
|
+
relayChunkAssemblers.set(conn, assembler);
|
|
466
|
+
}
|
|
467
|
+
return assembler.accept(message);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function closePeerConnections() {
|
|
471
|
+
const connections = new Set([
|
|
472
|
+
...clientConnections.values(),
|
|
473
|
+
...mesh.connections()
|
|
474
|
+
]);
|
|
475
|
+
for (const conn of connections) conn.close?.();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function attachConnection(conn, options = {}) {
|
|
479
|
+
const relayPeerConnection = Boolean(options.relayPeer || mesh.connectionHasRelayMetadata(conn));
|
|
480
|
+
conn.on("data", (message) => {
|
|
481
|
+
if (!incomingGate.accept(conn, message)) return;
|
|
482
|
+
const receivedMessage = receiveRelayMessage(conn, message);
|
|
483
|
+
if (!receivedMessage) return;
|
|
484
|
+
if (receivedMessage !== message && !incomingGate.accept(conn, receivedMessage)) return;
|
|
485
|
+
if (isNostrMessage(receivedMessage)) {
|
|
486
|
+
if (!mesh.hasRelayConnection(conn) && mesh.connectionHasRelayMetadata(conn) && conn.MatterhornRelayAddress) promoteRelayConnection(conn);
|
|
487
|
+
relay.handleMessage(conn, receivedMessage);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
handleConnectionData(conn, receivedMessage);
|
|
491
|
+
});
|
|
492
|
+
relay.attachConnection(conn, { relayPeer: false, skipData: true });
|
|
493
|
+
conn.on("close", () => {
|
|
494
|
+
cleanupConnection(conn);
|
|
495
|
+
});
|
|
496
|
+
conn.on("error", () => {
|
|
497
|
+
if (conn.open === false) cleanupConnection(conn);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
function sendRelayStatusHandshake() {
|
|
501
|
+
if (conn.MatterhornRelayHandshakeSent) return;
|
|
502
|
+
conn.MatterhornRelayHandshakeSent = true;
|
|
503
|
+
sendMessage(conn, relayStatus.relayStatusMessage(undefined, { includeKnownRelays: false }));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (!relayPeerConnection) return;
|
|
507
|
+
if (conn.open === false) {
|
|
508
|
+
conn.on("open", sendRelayStatusHandshake);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
sendRelayStatusHandshake();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function promoteRelayConnection(conn) {
|
|
515
|
+
const promoted = mesh.promoteConnection(conn);
|
|
516
|
+
return promoted;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function startPeer() {
|
|
520
|
+
const peers = await startRelayPeers({
|
|
521
|
+
attachConnection,
|
|
522
|
+
config,
|
|
523
|
+
debugRelay,
|
|
524
|
+
mesh,
|
|
525
|
+
peerLifecycle,
|
|
526
|
+
peerReconnects,
|
|
527
|
+
sfu
|
|
528
|
+
});
|
|
529
|
+
if (peers.roomPeer) roomPeer = peers.roomPeer;
|
|
530
|
+
if (peers.relayPeer) relayPeer = peers.relayPeer;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
config,
|
|
535
|
+
encryptedRoomRuntimes,
|
|
536
|
+
relay,
|
|
537
|
+
relayPluginRuntimes,
|
|
538
|
+
async registerRoom(input = {}) {
|
|
539
|
+
const prepared = prepareRelayRoomRuntimeInput(input);
|
|
540
|
+
const result = await relayPluginRuntimes.enableRoomComposition(prepared);
|
|
541
|
+
const roomName = runtimeInputRoomName(prepared);
|
|
542
|
+
if (roomName) await matterhornRuntimeEvents.catchUpMatterhornRuntime(roomName);
|
|
543
|
+
return result;
|
|
544
|
+
},
|
|
545
|
+
server,
|
|
546
|
+
wss,
|
|
547
|
+
attachConnection,
|
|
548
|
+
async listen() {
|
|
549
|
+
await startPeer();
|
|
550
|
+
return new Promise((resolve, reject) => {
|
|
551
|
+
server.once("error", reject);
|
|
552
|
+
server.listen(config.ipcPort, config.ipcHost, () => {
|
|
553
|
+
server.off("error", reject);
|
|
554
|
+
const address = server.address();
|
|
555
|
+
if (address && typeof address === "object") config.ipcPort = address.port;
|
|
556
|
+
resolve(address);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
},
|
|
560
|
+
async close() {
|
|
561
|
+
if (closePromise) return closePromise;
|
|
562
|
+
closePromise = (async () => {
|
|
563
|
+
closing = true;
|
|
564
|
+
await Promise.resolve();
|
|
565
|
+
announceRelayDisconnecting();
|
|
566
|
+
clearInterval(pendingPushSweep);
|
|
567
|
+
closePeerConnections();
|
|
568
|
+
const peerToDestroy = roomPeer;
|
|
569
|
+
const relayPeerToDestroy = relayPeer;
|
|
570
|
+
const dialPeersToDestroy = Array.from(relayDialPeers.values()).map((entry) => entry.peer);
|
|
571
|
+
relayDialPeers.clear();
|
|
572
|
+
roomPeer = undefined;
|
|
573
|
+
relayPeer = undefined;
|
|
574
|
+
sfu.close();
|
|
575
|
+
mesh.close();
|
|
576
|
+
await new Promise((resolve) => {
|
|
577
|
+
for (const socket of wss.clients) socket.close();
|
|
578
|
+
wss.close(() => {
|
|
579
|
+
if (!server.listening) {
|
|
580
|
+
resolve();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
server.close(() => resolve());
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
storage.close?.();
|
|
587
|
+
pushStorage?.close?.();
|
|
588
|
+
setTimeout(() => {
|
|
589
|
+
try {
|
|
590
|
+
peerToDestroy?.destroy();
|
|
591
|
+
} catch {}
|
|
592
|
+
try {
|
|
593
|
+
relayPeerToDestroy?.destroy();
|
|
594
|
+
} catch {}
|
|
595
|
+
for (const peer of dialPeersToDestroy) {
|
|
596
|
+
try {
|
|
597
|
+
peer?.destroy?.();
|
|
598
|
+
} catch {}
|
|
599
|
+
}
|
|
600
|
+
}, 0);
|
|
601
|
+
})();
|
|
602
|
+
return closePromise;
|
|
603
|
+
},
|
|
604
|
+
crash() {
|
|
605
|
+
setTimeout(() => {
|
|
606
|
+
void this.close();
|
|
607
|
+
}, 0);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function main() {
|
|
613
|
+
const args = parseArgs(process.argv.slice(2));
|
|
614
|
+
const options = createRelayOptionsFromArgs(args);
|
|
615
|
+
let localPeerServer;
|
|
616
|
+
if (options.localPeerjs) {
|
|
617
|
+
localPeerServer = await startLocalPeerServer(options.localPeerjs === true ? {} : options.localPeerjs);
|
|
618
|
+
options.peerJsSignaling = {
|
|
619
|
+
host: localPeerServer.host,
|
|
620
|
+
port: localPeerServer.port,
|
|
621
|
+
path: localPeerServer.path,
|
|
622
|
+
secure: localPeerServer.secure
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
const relay = createMatterhornRelay(options);
|
|
626
|
+
await relay.listen();
|
|
627
|
+
console.log("matterhorn relay IPC is live.");
|
|
628
|
+
console.log(`IPC: ws://${relay.config.ipcHost}:${relay.config.ipcPort}/room`);
|
|
629
|
+
console.log(`Room peer: ${relay.config.relayPeerId}`);
|
|
630
|
+
console.log(`Mesh peer: ${relay.config.relayMeshPeerId}`);
|
|
631
|
+
console.log(`Mesh addr: ${relay.config.relayAddress}`);
|
|
632
|
+
if (relay.config.relayIdentity?.logicalName) console.log(`Name: ${relay.config.relayIdentity.logicalName}`);
|
|
633
|
+
if (relay.config.relayIdentity?.publicKeyFingerprint) console.log(`Identity: ${relay.config.relayIdentity.publicKeyFingerprint}`);
|
|
634
|
+
if (relay.config.lastRelayTransportRotationReason) console.log(`Rotated: ${relay.config.lastRelayTransportRotationReason}`);
|
|
635
|
+
if (relay.config.sfuEnabled) console.log(`SFU peer: ${relay.config.sfuPeerId}`);
|
|
636
|
+
if (localPeerServer) console.log(`PeerJS: ${localPeerServer.url}`);
|
|
637
|
+
console.log(`Storage: ${relay.config.storagePath}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (require.main === module) {
|
|
641
|
+
main().catch((error) => {
|
|
642
|
+
console.error(error);
|
|
643
|
+
process.exit(1);
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
module.exports = {
|
|
648
|
+
WebSocketRelayConnection,
|
|
649
|
+
createMatterhornRelay,
|
|
650
|
+
defaultStoragePath,
|
|
651
|
+
parseArgs,
|
|
652
|
+
resolveStoragePath
|
|
653
|
+
};
|