@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,160 @@
|
|
|
1
|
+
const WebSocket = require("ws");
|
|
2
|
+
const { announceHost } = require("./hostAnnouncement.cjs");
|
|
3
|
+
const { createHostClients } = require("./hostClients/index.cjs");
|
|
4
|
+
const { relayIpcUrl } = require("./localRelayClient.cjs");
|
|
5
|
+
const { createRelayMessageReceiver, parseRelayMessage, sendToRelay } = require("@mh-gg/host-ipc");
|
|
6
|
+
|
|
7
|
+
function createHostSession(options) {
|
|
8
|
+
const {
|
|
9
|
+
store,
|
|
10
|
+
args,
|
|
11
|
+
logger = console
|
|
12
|
+
} = options;
|
|
13
|
+
const ipcSecret = options.localRelay?.config?.ipcSecret;
|
|
14
|
+
const reconnectRelay = options.reconnectRelay === true;
|
|
15
|
+
const relayReconnectMs = Number.isFinite(options.relayReconnectMs) ? Math.max(100, options.relayReconnectMs) : 1000;
|
|
16
|
+
let activeRelaySocket;
|
|
17
|
+
let reconnectTimer;
|
|
18
|
+
const clients = createHostClients({
|
|
19
|
+
store,
|
|
20
|
+
logger,
|
|
21
|
+
frontendBundle: options.frontendBundle,
|
|
22
|
+
frontendProgress: options.frontendProgress,
|
|
23
|
+
onStoreChange: syncStoreToRelay
|
|
24
|
+
});
|
|
25
|
+
let announced = false;
|
|
26
|
+
|
|
27
|
+
function debugHost(message, ...args) {
|
|
28
|
+
if (process.env.MATTERHORN_DEBUG === "1") logger.log(message, ...args);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function announceRegisteredRelay() {
|
|
32
|
+
if (announced) return;
|
|
33
|
+
announced = true;
|
|
34
|
+
announceHost(options);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function roomStoreSnapshot() {
|
|
38
|
+
return JSON.parse(JSON.stringify(store));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function relayAuth() {
|
|
42
|
+
if (!ipcSecret) return undefined;
|
|
43
|
+
return {
|
|
44
|
+
type: "relay-ipc",
|
|
45
|
+
secret: ipcSecret
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hostIpcMessage(message) {
|
|
50
|
+
const auth = relayAuth();
|
|
51
|
+
return auth ? { ...message, auth } : message;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function syncStoreToRelay() {
|
|
55
|
+
if (!activeRelaySocket) return;
|
|
56
|
+
sendToRelay(activeRelaySocket, hostIpcMessage({
|
|
57
|
+
type: "host.snapshot",
|
|
58
|
+
roomName: store.roomName,
|
|
59
|
+
store: roomStoreSnapshot(),
|
|
60
|
+
appRef: options.appRef,
|
|
61
|
+
appCwd: options.appCwd
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createReconnectSocket() {
|
|
66
|
+
if (typeof options.createRelaySocket === "function") return options.createRelaySocket();
|
|
67
|
+
if (!options.localRelay?.config) return undefined;
|
|
68
|
+
return new WebSocket(relayIpcUrl(options.localRelay.config));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function scheduleRelayReconnect() {
|
|
72
|
+
if (!reconnectRelay || reconnectTimer || activeRelaySocket) return;
|
|
73
|
+
reconnectTimer = setTimeout(() => {
|
|
74
|
+
reconnectTimer = undefined;
|
|
75
|
+
let relaySocket;
|
|
76
|
+
try {
|
|
77
|
+
relaySocket = createReconnectSocket();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logger.error("Local relay IPC reconnect failed:", error);
|
|
80
|
+
scheduleRelayReconnect();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (relaySocket) attachRelaySocket(relaySocket);
|
|
84
|
+
else scheduleRelayReconnect();
|
|
85
|
+
}, relayReconnectMs);
|
|
86
|
+
reconnectTimer.unref?.();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function handleRelayMessage(socket, data) {
|
|
90
|
+
const message = typeof data === "object" && !Buffer.isBuffer(data) ? data : parseRelayMessage(data);
|
|
91
|
+
if (!message) return;
|
|
92
|
+
|
|
93
|
+
if (message.type === "host.register.ok") {
|
|
94
|
+
announceRegisteredRelay();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (message.type === "host.register.error") {
|
|
99
|
+
logger.error(message.message || "Relay rejected host registration.");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (message.type === "relay/client-message") {
|
|
104
|
+
clients.handleClientMessage(socket, message.peerId, message.message);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (message.type === "relay/client-close") {
|
|
109
|
+
debugHost("Relay client connection closed", message.peerId);
|
|
110
|
+
clients.removeConnection(message.peerId);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function attachRelaySocket(relaySocket) {
|
|
115
|
+
const receiveRelayMessage = createRelayMessageReceiver();
|
|
116
|
+
relaySocket.on("open", () => {
|
|
117
|
+
if (reconnectTimer) {
|
|
118
|
+
clearTimeout(reconnectTimer);
|
|
119
|
+
reconnectTimer = undefined;
|
|
120
|
+
}
|
|
121
|
+
activeRelaySocket = relaySocket;
|
|
122
|
+
sendToRelay(relaySocket, hostIpcMessage({
|
|
123
|
+
type: "host.register",
|
|
124
|
+
roomName: store.roomName,
|
|
125
|
+
store: roomStoreSnapshot(),
|
|
126
|
+
appRef: options.appRef,
|
|
127
|
+
appCwd: options.appCwd,
|
|
128
|
+
relayHints: [store.relayAddress, ...args.relayPeers].filter(Boolean)
|
|
129
|
+
}));
|
|
130
|
+
});
|
|
131
|
+
relaySocket.on("message", (data) => {
|
|
132
|
+
const message = receiveRelayMessage(data);
|
|
133
|
+
if (message) handleRelayMessage(relaySocket, message);
|
|
134
|
+
});
|
|
135
|
+
relaySocket.on("close", () => {
|
|
136
|
+
if (activeRelaySocket === relaySocket) activeRelaySocket = undefined;
|
|
137
|
+
if (reconnectRelay) {
|
|
138
|
+
logger.error("Local relay IPC closed. Reconnecting when the relay is available.");
|
|
139
|
+
scheduleRelayReconnect();
|
|
140
|
+
} else {
|
|
141
|
+
logger.error("Local relay IPC closed. Stop this host or restart it after the relay is available.");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
relaySocket.on("error", (error) => {
|
|
145
|
+
logger.error("Local relay IPC error:", error);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
attachRelaySocket,
|
|
151
|
+
connectionRoles: clients.connectionRoles,
|
|
152
|
+
handleClientMessage: clients.handleClientMessage,
|
|
153
|
+
handleRelayMessage,
|
|
154
|
+
pendingJoinConnections: clients.pendingJoinConnections
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
createHostSession
|
|
160
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function sanitizeProgressText(value) {
|
|
4
|
+
return String(value || "").replace(/[\r\n\t]+/g, " ").slice(0, 80);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function normalizeCount(value, fallback = 0) {
|
|
8
|
+
return Number.isFinite(value) ? value : fallback;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeProgressState(update = {}, previous = {}) {
|
|
12
|
+
const total = normalizeCount(update.total, normalizeCount(previous.total, 0));
|
|
13
|
+
const rawCompleted = Object.prototype.hasOwnProperty.call(update, "completed")
|
|
14
|
+
? update.completed
|
|
15
|
+
: update.progress;
|
|
16
|
+
const completed = normalizeCount(rawCompleted, normalizeCount(previous.completed, 0));
|
|
17
|
+
const currentItem = typeof update.currentItem === "string"
|
|
18
|
+
? sanitizeProgressText(update.currentItem)
|
|
19
|
+
: (previous.currentItem || "");
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
completed: Math.max(0, total > 0 ? Math.min(completed, total) : completed),
|
|
23
|
+
total: Math.max(0, total),
|
|
24
|
+
currentItem
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function progressRatio(done, total) {
|
|
29
|
+
if (!Number.isFinite(total) || total <= 0) return 0;
|
|
30
|
+
const safeDone = Math.max(0, Math.min(done, total));
|
|
31
|
+
return safeDone / total;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatProgressPercent(done, total) {
|
|
35
|
+
if (!Number.isFinite(total) || total <= 0) return "";
|
|
36
|
+
const percent = progressRatio(done, total) * 100;
|
|
37
|
+
return Number.isInteger(percent) ? `${percent}%` : `${percent.toFixed(1)}%`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderProgressBar(done, total, width = 24) {
|
|
41
|
+
if (!Number.isFinite(total) || total <= 0) return "";
|
|
42
|
+
const safeWidth = Math.max(0, Math.floor(Number.isFinite(width) ? width : 24));
|
|
43
|
+
if (safeWidth <= 0) return "[]";
|
|
44
|
+
|
|
45
|
+
const filled = Math.max(0, Math.min(safeWidth, Math.floor(progressRatio(done, total) * safeWidth)));
|
|
46
|
+
const percentText = formatProgressPercent(done, total);
|
|
47
|
+
const label = percentText.length > safeWidth ? percentText.slice(0, safeWidth) : percentText;
|
|
48
|
+
const labelStart = Math.max(0, Math.floor((safeWidth - label.length) / 2));
|
|
49
|
+
let content = "";
|
|
50
|
+
|
|
51
|
+
for (let index = 0; index < safeWidth; index += 1) {
|
|
52
|
+
const labelIndex = index - labelStart;
|
|
53
|
+
if (labelIndex >= 0 && labelIndex < label.length) {
|
|
54
|
+
content += label[labelIndex];
|
|
55
|
+
} else {
|
|
56
|
+
content += index < filled ? "=" : " ";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return `[${content}]`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderProgressLine(message, state, options = {}) {
|
|
64
|
+
const progress = normalizeProgressState(state);
|
|
65
|
+
const width = Number.isFinite(options.width) ? options.width : 24;
|
|
66
|
+
const bar = renderProgressBar(progress.completed, progress.total, width);
|
|
67
|
+
const count = progress.total > 0
|
|
68
|
+
? `${Math.min(progress.completed, progress.total)}/${progress.total}`
|
|
69
|
+
: String(progress.completed);
|
|
70
|
+
const item = progress.currentItem ? ` ${progress.currentItem}` : "";
|
|
71
|
+
return `${message} ${bar ? `${bar} ` : ""}${count}${item}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createNoopProgressLoader() {
|
|
75
|
+
return {
|
|
76
|
+
start() {},
|
|
77
|
+
update() {},
|
|
78
|
+
stop() {}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createInlineProgressLoader(message, options = {}) {
|
|
83
|
+
const stream = options.stream || process.stderr;
|
|
84
|
+
const enabled = options.enabled !== undefined
|
|
85
|
+
? options.enabled
|
|
86
|
+
: Boolean(stream?.isTTY);
|
|
87
|
+
|
|
88
|
+
if (!enabled || !stream || typeof stream.write !== "function") {
|
|
89
|
+
return createNoopProgressLoader();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const width = Number.isFinite(options.width) ? options.width : 24;
|
|
93
|
+
let state = normalizeProgressState();
|
|
94
|
+
let lastLine = "";
|
|
95
|
+
const clearOnStop = options.clearOnStop !== false;
|
|
96
|
+
const newlineOnStop = options.newlineOnStop === true;
|
|
97
|
+
|
|
98
|
+
function draw() {
|
|
99
|
+
const line = renderProgressLine(message, state, { width });
|
|
100
|
+
const clearLength = Math.max(0, lastLine.length - line.length);
|
|
101
|
+
stream.write(`\r${line}${" ".repeat(clearLength)}`);
|
|
102
|
+
lastLine = line;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
start() {
|
|
107
|
+
draw();
|
|
108
|
+
},
|
|
109
|
+
update(update) {
|
|
110
|
+
state = normalizeProgressState(update, state);
|
|
111
|
+
draw();
|
|
112
|
+
},
|
|
113
|
+
stop() {
|
|
114
|
+
if (clearOnStop && lastLine) stream.write("\r\x1b[2K");
|
|
115
|
+
if (newlineOnStop) stream.write("\n");
|
|
116
|
+
lastLine = "";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
createInlineProgressLoader,
|
|
123
|
+
formatProgressPercent,
|
|
124
|
+
normalizeProgressState,
|
|
125
|
+
renderProgressBar,
|
|
126
|
+
renderProgressLine,
|
|
127
|
+
sanitizeProgressText
|
|
128
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const { setTimeout: delay } = require("node:timers/promises");
|
|
2
|
+
|
|
3
|
+
const DEFAULT_LOCAL_PEERJS_HOST = "127.0.0.1";
|
|
4
|
+
const DEFAULT_LOCAL_PEERJS_PORT = 9000;
|
|
5
|
+
const DEFAULT_LOCAL_PEERJS_PATH = "/peerjs";
|
|
6
|
+
|
|
7
|
+
function localPeerServerConfig(options = {}) {
|
|
8
|
+
return {
|
|
9
|
+
host: options.host || process.env.MATTERHORN_PEERJS_LOCAL_HOST || DEFAULT_LOCAL_PEERJS_HOST,
|
|
10
|
+
port: Number(options.port || process.env.MATTERHORN_PEERJS_LOCAL_PORT || DEFAULT_LOCAL_PEERJS_PORT),
|
|
11
|
+
path: options.path || process.env.MATTERHORN_PEERJS_LOCAL_PATH || DEFAULT_LOCAL_PEERJS_PATH,
|
|
12
|
+
secure: false
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function applyPeerServerEnv(peerServer) {
|
|
17
|
+
process.env.MATTERHORN_PEERJS_HOST = peerServer.host;
|
|
18
|
+
process.env.MATTERHORN_PEERJS_PORT = String(peerServer.port);
|
|
19
|
+
process.env.MATTERHORN_PEERJS_PATH = peerServer.path;
|
|
20
|
+
process.env.MATTERHORN_PEERJS_SECURE = String(peerServer.secure);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function peerServerUrl(config) {
|
|
24
|
+
return `http://${config.host}:${config.port}${config.path}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function peerServerIdUrl(config) {
|
|
28
|
+
const base = peerServerUrl(config).replace(/\/$/, "");
|
|
29
|
+
return `${base}/peerjs/id`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function waitForPeerServer(config, options = {}) {
|
|
33
|
+
const timeoutMs = Number(options.timeoutMs || 5000);
|
|
34
|
+
const deadline = Date.now() + timeoutMs;
|
|
35
|
+
let lastError;
|
|
36
|
+
while (Date.now() < deadline) {
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(peerServerIdUrl(config));
|
|
39
|
+
if (response.ok) return true;
|
|
40
|
+
lastError = new Error(`PeerJS health returned ${response.status}`);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
lastError = error;
|
|
43
|
+
}
|
|
44
|
+
await delay(100);
|
|
45
|
+
}
|
|
46
|
+
throw lastError || new Error("Timed out waiting for local PeerJS server");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function closePeerServer(server) {
|
|
50
|
+
const target = server?.close ? server : server?._server;
|
|
51
|
+
if (!target?.close) return Promise.resolve();
|
|
52
|
+
return new Promise((resolve) => target.close(() => resolve()));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function startLocalPeerServer(options = {}) {
|
|
56
|
+
const config = localPeerServerConfig(options);
|
|
57
|
+
if (options.reuseExisting !== false) {
|
|
58
|
+
try {
|
|
59
|
+
await waitForPeerServer(config, { timeoutMs: options.reuseTimeoutMs || 300 });
|
|
60
|
+
applyPeerServerEnv(config);
|
|
61
|
+
return {
|
|
62
|
+
...config,
|
|
63
|
+
url: peerServerUrl(config),
|
|
64
|
+
reused: true,
|
|
65
|
+
server: undefined,
|
|
66
|
+
close: () => Promise.resolve()
|
|
67
|
+
};
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let PeerServer;
|
|
72
|
+
try {
|
|
73
|
+
({ PeerServer } = require("peer"));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new Error(`The local PeerJS server requires the peer package. Run pnpm install once at the Matterhorn workspace root. ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
if (typeof PeerServer !== "function") throw new Error("The peer package did not export PeerServer.");
|
|
78
|
+
const server = PeerServer({
|
|
79
|
+
host: config.host,
|
|
80
|
+
port: config.port,
|
|
81
|
+
path: config.path,
|
|
82
|
+
proxied: false
|
|
83
|
+
});
|
|
84
|
+
let startupError;
|
|
85
|
+
server?.once?.("error", (error) => {
|
|
86
|
+
startupError = error;
|
|
87
|
+
});
|
|
88
|
+
applyPeerServerEnv(config);
|
|
89
|
+
try {
|
|
90
|
+
await waitForPeerServer(config, options);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
await closePeerServer(server).catch(() => undefined);
|
|
93
|
+
throw startupError || error;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
...config,
|
|
97
|
+
url: peerServerUrl(config),
|
|
98
|
+
reused: false,
|
|
99
|
+
server,
|
|
100
|
+
close: () => closePeerServer(server)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
DEFAULT_LOCAL_PEERJS_HOST,
|
|
106
|
+
DEFAULT_LOCAL_PEERJS_PATH,
|
|
107
|
+
DEFAULT_LOCAL_PEERJS_PORT,
|
|
108
|
+
applyPeerServerEnv,
|
|
109
|
+
localPeerServerConfig,
|
|
110
|
+
peerServerIdUrl,
|
|
111
|
+
peerServerUrl,
|
|
112
|
+
startLocalPeerServer,
|
|
113
|
+
waitForPeerServer
|
|
114
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const { spawn } = require("node:child_process");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { ensureRelayConfig } = require("./relayConfig.cjs");
|
|
4
|
+
const { startLocalPeerServer } = require("./localPeerServer.cjs");
|
|
5
|
+
|
|
6
|
+
const DEFAULT_RELAY_START_TIMEOUT_MS = 6000;
|
|
7
|
+
|
|
8
|
+
function delay(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function relayIpcUrl(config) {
|
|
13
|
+
return `ws://${config.ipcHost}:${config.ipcPort}/room`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function relayHealthUrl(config) {
|
|
17
|
+
return `http://${config.ipcHost}:${config.ipcPort}/health`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function fetchRelayInfo(config, fetcher = fetch) {
|
|
21
|
+
const response = await fetcher(relayHealthUrl(config));
|
|
22
|
+
if (!response.ok) throw new Error(`Relay health returned ${response.status}`);
|
|
23
|
+
const info = await response.json();
|
|
24
|
+
if (info.protocol !== "matterhorn-peer-relay") throw new Error("Local process is not a matterhorn relay");
|
|
25
|
+
if (info.roomPeerOpen === false) throw new Error("Local relay room peer is not live");
|
|
26
|
+
return info;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function waitForRelay(config, timeoutMs = DEFAULT_RELAY_START_TIMEOUT_MS) {
|
|
30
|
+
const deadline = Date.now() + timeoutMs;
|
|
31
|
+
let lastError;
|
|
32
|
+
|
|
33
|
+
while (Date.now() < deadline) {
|
|
34
|
+
try {
|
|
35
|
+
return await fetchRelayInfo(config);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
lastError = error;
|
|
38
|
+
await delay(250);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
throw lastError || new Error("Timed out waiting for local relay");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function spawnRelayProcess(config, options = {}) {
|
|
46
|
+
const relayPath = path.join(__dirname, "relay.cjs");
|
|
47
|
+
const args = [
|
|
48
|
+
relayPath,
|
|
49
|
+
"--ipc-host",
|
|
50
|
+
config.ipcHost,
|
|
51
|
+
"--ipc-port",
|
|
52
|
+
String(config.ipcPort),
|
|
53
|
+
"--relay-peer-id",
|
|
54
|
+
config.relayPeerId
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
for (const relayPeer of options.relayPeers || []) {
|
|
58
|
+
args.push("--relay-peer", relayPeer);
|
|
59
|
+
}
|
|
60
|
+
if (options.iceServers) {
|
|
61
|
+
args.push("--ice", options.iceServers);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const env = { ...process.env };
|
|
65
|
+
if (options.dataDir) env.MATTERHORN_HOME = options.dataDir;
|
|
66
|
+
if (options.localPeerjs) env.MATTERHORN_PEERJS_LOCAL = "1";
|
|
67
|
+
|
|
68
|
+
const child = spawn(process.execPath, args, {
|
|
69
|
+
detached: true,
|
|
70
|
+
stdio: "ignore",
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
env
|
|
73
|
+
});
|
|
74
|
+
child.unref();
|
|
75
|
+
return child;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseIceServers(value) {
|
|
79
|
+
if (Array.isArray(value)) return value.length > 0 ? value : undefined;
|
|
80
|
+
if (!value) return undefined;
|
|
81
|
+
return String(value || "")
|
|
82
|
+
.split(",")
|
|
83
|
+
.map((item) => item.trim())
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.map((urls) => ({ urls }));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function startEmbeddedRelay(config, options = {}) {
|
|
89
|
+
const { createMatterhornRelay } = require("./relay.cjs");
|
|
90
|
+
const relay = createMatterhornRelay({
|
|
91
|
+
dataDir: options.dataDir,
|
|
92
|
+
ipcHost: config.ipcHost,
|
|
93
|
+
ipcPort: config.ipcPort,
|
|
94
|
+
storagePath: options.storagePath,
|
|
95
|
+
relayPeerId: config.relayPeerId,
|
|
96
|
+
relayPeers: options.relayPeers || [],
|
|
97
|
+
iceServers: parseIceServers(options.iceServers),
|
|
98
|
+
startPeer: options.startPeer,
|
|
99
|
+
authenticateActor: options.authenticateActor,
|
|
100
|
+
peerJsSignaling: options.peerJsSignaling
|
|
101
|
+
});
|
|
102
|
+
await relay.listen();
|
|
103
|
+
config.ipcPort = relay.config.ipcPort;
|
|
104
|
+
const info = await fetchRelayInfo(config);
|
|
105
|
+
return { relay, info };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function ensureLocalRelay(options = {}) {
|
|
109
|
+
let localPeerServer;
|
|
110
|
+
if (options.localPeerjs) {
|
|
111
|
+
localPeerServer = await startLocalPeerServer(options.localPeerjs === true ? {} : options.localPeerjs);
|
|
112
|
+
options = {
|
|
113
|
+
...options,
|
|
114
|
+
peerJsSignaling: {
|
|
115
|
+
host: localPeerServer.host,
|
|
116
|
+
port: localPeerServer.port,
|
|
117
|
+
path: localPeerServer.path,
|
|
118
|
+
secure: localPeerServer.secure
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const config = ensureRelayConfig({
|
|
124
|
+
...options,
|
|
125
|
+
forcePeerJsSignaling: Boolean(options.localPeerjs),
|
|
126
|
+
ipcHost: options.ipcHost || process.env.MATTERHORN_RELAY_IPC_HOST,
|
|
127
|
+
ipcPort: options.ipcPort ?? (process.env.MATTERHORN_RELAY_IPC_PORT ? Number(process.env.MATTERHORN_RELAY_IPC_PORT) : undefined)
|
|
128
|
+
});
|
|
129
|
+
try {
|
|
130
|
+
const info = await fetchRelayInfo(config);
|
|
131
|
+
return { config, info, localPeerServer, spawned: false };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
try {
|
|
134
|
+
const embedded = await startEmbeddedRelay(config, options);
|
|
135
|
+
return { config, info: embedded.info, relay: embedded.relay, localPeerServer, spawned: true };
|
|
136
|
+
} catch (startError) {
|
|
137
|
+
await localPeerServer?.close?.().catch(() => undefined);
|
|
138
|
+
throw startError || error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
ensureLocalRelay,
|
|
145
|
+
fetchRelayInfo,
|
|
146
|
+
relayHealthUrl,
|
|
147
|
+
relayIpcUrl,
|
|
148
|
+
spawnRelayProcess,
|
|
149
|
+
startEmbeddedRelay,
|
|
150
|
+
waitForRelay
|
|
151
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const os = require("node:os");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
|
|
5
|
+
function userDataDir() {
|
|
6
|
+
return process.env.MATTERHORN_HOME || path.join(os.homedir(), ".matterhorn");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function usermatterhornrcFile() {
|
|
10
|
+
return path.join(userDataDir(), ".matterhornrc");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseKeyValueText(text) {
|
|
14
|
+
const values = {};
|
|
15
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
16
|
+
const line = rawLine.trim();
|
|
17
|
+
if (!line || line.startsWith("#")) continue;
|
|
18
|
+
const match = /^([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*(.*)$/.exec(line);
|
|
19
|
+
if (!match) continue;
|
|
20
|
+
values[match[1]] = match[2].trim().replace(/^["']|["']$/g, "");
|
|
21
|
+
}
|
|
22
|
+
return values;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizematterhornrc(values) {
|
|
26
|
+
const host = values.host || values.HOST_URL || values.appUrl || values.app_url;
|
|
27
|
+
const normalized = host ? { host: String(host).trim() } : {};
|
|
28
|
+
const players = Array.isArray(values.players) ? values.players : values.playerPacks;
|
|
29
|
+
if (Array.isArray(players)) {
|
|
30
|
+
normalized.players = players
|
|
31
|
+
.filter((player) => player && typeof player === "object" && typeof player.url === "string")
|
|
32
|
+
.map((player) => ({
|
|
33
|
+
url: player.url,
|
|
34
|
+
...(player.integrity === undefined ? {} : { integrity: String(player.integrity) }),
|
|
35
|
+
...(player.id === undefined ? {} : { id: String(player.id) }),
|
|
36
|
+
...(player.name === undefined ? {} : { name: String(player.name) }),
|
|
37
|
+
...(player.version === undefined ? {} : { version: String(player.version) }),
|
|
38
|
+
...(player.addedAt === undefined ? {} : { addedAt: String(player.addedAt) })
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
return normalized;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parsematterhornrc(text) {
|
|
45
|
+
try {
|
|
46
|
+
return normalizematterhornrc(JSON.parse(text));
|
|
47
|
+
} catch {
|
|
48
|
+
return normalizematterhornrc(parseKeyValueText(text));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readmatterhornrc(file = usermatterhornrcFile()) {
|
|
53
|
+
if (!fs.existsSync(file)) return {};
|
|
54
|
+
return parsematterhornrc(fs.readFileSync(file, "utf8"));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writematterhornrc(values, file = usermatterhornrcFile()) {
|
|
58
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
59
|
+
fs.writeFileSync(file, `${JSON.stringify(values, null, 2)}\n`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function applymatterhornrcDefaults(target = process.env, file = usermatterhornrcFile()) {
|
|
63
|
+
const values = readmatterhornrc(file);
|
|
64
|
+
if (values.host && target.APP_URL === undefined && target.HOST_URL === undefined) {
|
|
65
|
+
target.HOST_URL = values.host;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
applymatterhornrcDefaults,
|
|
71
|
+
readmatterhornrc,
|
|
72
|
+
userDataDir,
|
|
73
|
+
usermatterhornrcFile,
|
|
74
|
+
writematterhornrc
|
|
75
|
+
};
|