@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,609 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const os = require("node:os");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
5
|
+
const { slugifyRoom } = require("@mh-gg/room-link");
|
|
6
|
+
const { userDataDir } = require("../host/matterhornrc.cjs");
|
|
7
|
+
const { canonicalAppRef, loadMatterhornDeployment, resolveMatterhornAppEntry } = require("./matterhornAppLoader.cjs");
|
|
8
|
+
const { assertDeployable, runAppDeployCommand } = require("./matterhornDeploy.cjs");
|
|
9
|
+
const { buildMatterhornCommandEnv } = require("./appFrontend/commandEnv.cjs");
|
|
10
|
+
|
|
11
|
+
function optionValue(args, index, name) {
|
|
12
|
+
const value = args[index + 1];
|
|
13
|
+
if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseMatterhornInstallArgs(args, options = {}) {
|
|
18
|
+
const [appRef, ...rest] = args;
|
|
19
|
+
if (!appRef || appRef.startsWith("--")) throw new Error("Usage: matterhorn launch <app-ref> [--room ROOM] [--relay RELAY] [--user USER]");
|
|
20
|
+
const parsed = {
|
|
21
|
+
appRef,
|
|
22
|
+
roomName: undefined,
|
|
23
|
+
relays: [],
|
|
24
|
+
serveMode: "built",
|
|
25
|
+
userId: undefined
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
29
|
+
const token = rest[index];
|
|
30
|
+
if (token === "--room") {
|
|
31
|
+
parsed.roomName = slugifyRoom(optionValue(rest, index, "--room"));
|
|
32
|
+
index += 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (token === "--relay" || token === "--relay-peer") {
|
|
36
|
+
parsed.relays.push(optionValue(rest, index, token));
|
|
37
|
+
index += 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (token === "--serve") {
|
|
41
|
+
parsed.serveMode = optionValue(rest, index, "--serve");
|
|
42
|
+
index += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (token === "--user" || token === "--user-id") {
|
|
46
|
+
parsed.userId = optionValue(rest, index, token);
|
|
47
|
+
index += 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Unexpected Matterhorn install argument ${token}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const bundleName = options.bundleName || bundleNameForRef(appRef, options);
|
|
54
|
+
return {
|
|
55
|
+
...parsed,
|
|
56
|
+
bundleName,
|
|
57
|
+
packageName: bundleName,
|
|
58
|
+
installId: installIdForBundle(bundleName),
|
|
59
|
+
roomName: parsed.roomName || defaultRoomName(bundleName)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseMatterhornRunArgs(args) {
|
|
64
|
+
const [installId, ...rest] = args;
|
|
65
|
+
if (!installId || installId.startsWith("--")) throw new Error("Usage: matterhorn run <install-id> [--room ROOM] [--relay RELAY]");
|
|
66
|
+
const parsed = {
|
|
67
|
+
installId,
|
|
68
|
+
roomName: undefined,
|
|
69
|
+
relays: [],
|
|
70
|
+
userId: undefined
|
|
71
|
+
};
|
|
72
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
73
|
+
const token = rest[index];
|
|
74
|
+
if (token === "--room") {
|
|
75
|
+
parsed.roomName = slugifyRoom(optionValue(rest, index, "--room"));
|
|
76
|
+
index += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (token === "--relay" || token === "--relay-peer") {
|
|
80
|
+
parsed.relays.push(optionValue(rest, index, token));
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (token === "--user" || token === "--user-id") {
|
|
85
|
+
parsed.userId = optionValue(rest, index, token);
|
|
86
|
+
index += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Unexpected Matterhorn run argument ${token}`);
|
|
90
|
+
}
|
|
91
|
+
return parsed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function bundleNameForRef(ref, options = {}) {
|
|
95
|
+
const value = canonicalAppRef(ref);
|
|
96
|
+
if (value.startsWith("workspace:")) return bundleNameForRef(value.slice("workspace:".length), options);
|
|
97
|
+
if (value.startsWith("file:")) return bundleNameForLocalPath(value.slice("file:".length), options.cwd);
|
|
98
|
+
if (isLocalPath(value)) return bundleNameForLocalPath(value, options.cwd);
|
|
99
|
+
if (value.startsWith("npm:")) return bundleNameForRef(value.slice("npm:".length), options);
|
|
100
|
+
if (value.startsWith("@")) {
|
|
101
|
+
const slash = value.indexOf("/");
|
|
102
|
+
if (slash === -1) return value;
|
|
103
|
+
const version = value.indexOf("@", slash + 1);
|
|
104
|
+
return version === -1 ? value : value.slice(0, version);
|
|
105
|
+
}
|
|
106
|
+
const version = value.indexOf("@");
|
|
107
|
+
return version === -1 ? value : value.slice(0, version);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function bundleNameForLocalPath(value, cwd = process.cwd()) {
|
|
111
|
+
const localPath = path.resolve(cwd, value);
|
|
112
|
+
const directory = fs.statSync(localPath).isDirectory() ? localPath : path.dirname(localPath);
|
|
113
|
+
const packageFile = path.join(directory, "package.json");
|
|
114
|
+
if (fs.existsSync(packageFile)) {
|
|
115
|
+
const manifest = JSON.parse(fs.readFileSync(packageFile, "utf8"));
|
|
116
|
+
if (manifest.name) return manifest.name;
|
|
117
|
+
}
|
|
118
|
+
return path.basename(directory);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isLocalPath(value) {
|
|
122
|
+
return value === "." || value === ".." || value.startsWith("./") || value.startsWith("../") || path.isAbsolute(value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function installIdForBundle(bundleName) {
|
|
126
|
+
return slugifyRoom(String(bundleName).replace(/^@/, "").replace("/", "-"));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function installIdForPackage(packageName) {
|
|
130
|
+
return installIdForBundle(packageName);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function packageNameForRef(ref, options = {}) {
|
|
134
|
+
return bundleNameForRef(ref, options);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function defaultRoomName(bundleName) {
|
|
138
|
+
const name = String(bundleName).split("/").pop() || bundleName;
|
|
139
|
+
return slugifyRoom(name.replace(/^matterhorn-/, "").replace(/^example-/, ""));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function installRoot(matterhornHome, installId) {
|
|
143
|
+
return path.join(matterhornHome, "apps", installId);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function installRecordFile(matterhornHome, installId) {
|
|
147
|
+
return path.join(installRoot(matterhornHome, installId), "matterhorn-install.json");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function bundleRoot(installDir) {
|
|
151
|
+
return path.join(installDir, "bundle");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function fileAppRef(filePath) {
|
|
155
|
+
return `file:${filePath}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function copyDirectory(sourceDir, targetDir) {
|
|
159
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
160
|
+
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
161
|
+
if (entry.name === ".git") continue;
|
|
162
|
+
const source = path.join(sourceDir, entry.name);
|
|
163
|
+
const target = path.join(targetDir, entry.name);
|
|
164
|
+
if (entry.isDirectory()) {
|
|
165
|
+
copyDirectory(source, target);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (entry.isFile()) {
|
|
169
|
+
fs.copyFileSync(source, target);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (entry.isSymbolicLink()) {
|
|
173
|
+
fs.symlinkSync(fs.readlinkSync(source), target);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function copyBundleSource(source, targetDir) {
|
|
179
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
180
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
181
|
+
copyDirectory(source.sourceRoot, targetDir);
|
|
182
|
+
const relativeEntry = path.relative(source.sourceRoot, source.entryFile);
|
|
183
|
+
if (relativeEntry.startsWith("..") || path.isAbsolute(relativeEntry)) {
|
|
184
|
+
throw new Error(`Matterhorn bundle entry ${source.entryFile} must be inside ${source.sourceRoot}`);
|
|
185
|
+
}
|
|
186
|
+
return path.join(targetDir, relativeEntry);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function bundleForEntry(plan, source, targetDir, provenance = {}) {
|
|
190
|
+
const relativeEntry = path.relative(targetDir, source.entryFile);
|
|
191
|
+
if (relativeEntry.startsWith("..") || path.isAbsolute(relativeEntry)) {
|
|
192
|
+
throw new Error(`Matterhorn bundle entry ${source.entryFile} must be inside ${targetDir}`);
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
kind: "matterhorn.bundle",
|
|
196
|
+
runner: "node",
|
|
197
|
+
sourceRef: plan.appRef,
|
|
198
|
+
sourceRoot: source.sourceRoot,
|
|
199
|
+
sourceEntry: source.entryFile,
|
|
200
|
+
bundleDir: targetDir,
|
|
201
|
+
entry: source.entryFile,
|
|
202
|
+
appRef: fileAppRef(source.entryFile),
|
|
203
|
+
provenance: Object.fromEntries(Object.entries(provenance).filter(([, value]) => value !== undefined))
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function npmCommand() {
|
|
208
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function tarCommand() {
|
|
212
|
+
return process.platform === "win32" ? "tar.exe" : "tar";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function tarPathArg(filePath, cwd) {
|
|
216
|
+
const relative = path.relative(cwd, filePath);
|
|
217
|
+
const value = relative && !path.isAbsolute(relative) ? relative : filePath;
|
|
218
|
+
return value.replaceAll("\\", "/");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function tarArchiveArg(tarball, cwd) {
|
|
222
|
+
return tarPathArg(tarball, cwd);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function runChecked(command, args, options = {}) {
|
|
226
|
+
const result = spawnSync(command, args, {
|
|
227
|
+
cwd: options.cwd,
|
|
228
|
+
encoding: "utf8",
|
|
229
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
230
|
+
windowsHide: true
|
|
231
|
+
});
|
|
232
|
+
if (result.error) throw new Error(`${options.label || command} failed: ${result.error.message}`);
|
|
233
|
+
if (result.status !== 0) {
|
|
234
|
+
const message = (result.stderr || result.stdout || "").trim();
|
|
235
|
+
throw new Error(`${options.label || command} failed: ${message || `${command} exited ${result.status}`}`);
|
|
236
|
+
}
|
|
237
|
+
return result.stdout || "";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function packedTarballFromNpmOutput(stdout, downloadDir) {
|
|
241
|
+
const trimmed = stdout.trim();
|
|
242
|
+
if (!trimmed) throw new Error("npm pack did not report a bundle tarball");
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(trimmed);
|
|
245
|
+
const item = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
246
|
+
const filename = item?.filename || item?.name;
|
|
247
|
+
if (filename) {
|
|
248
|
+
return {
|
|
249
|
+
tarball: path.resolve(downloadDir, filename),
|
|
250
|
+
packageName: item.name,
|
|
251
|
+
version: item.version,
|
|
252
|
+
integrity: item.integrity,
|
|
253
|
+
shasum: item.shasum
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
const lastLine = trimmed.split(/\r?\n/).filter(Boolean).pop();
|
|
258
|
+
if (lastLine) return { tarball: path.resolve(downloadDir, lastLine.trim()) };
|
|
259
|
+
}
|
|
260
|
+
throw new Error("npm pack did not report a bundle tarball");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function resolveExtractedPackageEntry(targetDir, options = {}) {
|
|
264
|
+
return (options.resolveMatterhornAppEntry || resolveMatterhornAppEntry)(".", {
|
|
265
|
+
...options,
|
|
266
|
+
cwd: targetDir,
|
|
267
|
+
localAppCodeTrusted: false
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function downloadPackedMatterhornBundle(plan, options = {}) {
|
|
272
|
+
const targetDir = options.targetDir || bundleRoot(plan.installDir);
|
|
273
|
+
const downloadDir = fs.mkdtempSync(path.join(options.tmpDir || os.tmpdir(), "matterhorn-bundle-"));
|
|
274
|
+
const appRef = canonicalAppRef(plan.appRef);
|
|
275
|
+
const stdout = runChecked(options.npmCommand || npmCommand(), [
|
|
276
|
+
"pack",
|
|
277
|
+
appRef,
|
|
278
|
+
"--json",
|
|
279
|
+
"--pack-destination",
|
|
280
|
+
downloadDir
|
|
281
|
+
], {
|
|
282
|
+
cwd: options.cwd || process.cwd(),
|
|
283
|
+
label: "Matterhorn bundle download"
|
|
284
|
+
});
|
|
285
|
+
const packed = packedTarballFromNpmOutput(stdout, downloadDir);
|
|
286
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
287
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
288
|
+
const extractCwd = path.dirname(targetDir);
|
|
289
|
+
runChecked(options.tarCommand || tarCommand(), [
|
|
290
|
+
"-xzf",
|
|
291
|
+
tarPathArg(packed.tarball, extractCwd),
|
|
292
|
+
"-C",
|
|
293
|
+
tarPathArg(targetDir, extractCwd),
|
|
294
|
+
"--strip-components",
|
|
295
|
+
"1"
|
|
296
|
+
], {
|
|
297
|
+
cwd: extractCwd,
|
|
298
|
+
label: "Matterhorn bundle extract"
|
|
299
|
+
});
|
|
300
|
+
const source = resolveExtractedPackageEntry(targetDir, options);
|
|
301
|
+
return bundleForEntry(plan, source, targetDir, {
|
|
302
|
+
packageName: packed.packageName,
|
|
303
|
+
version: packed.version,
|
|
304
|
+
integrity: packed.integrity,
|
|
305
|
+
shasum: packed.shasum
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function localInstallRef(ref) {
|
|
310
|
+
const value = canonicalAppRef(ref);
|
|
311
|
+
return value.startsWith("workspace:") || value.startsWith("file:") || value.startsWith("git+") || isLocalPath(value);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function materializeMatterhornBundle(plan, options = {}) {
|
|
315
|
+
const resolveEntry = options.resolveMatterhornAppEntry || resolveMatterhornAppEntry;
|
|
316
|
+
const targetDir = bundleRoot(plan.installDir);
|
|
317
|
+
try {
|
|
318
|
+
const source = resolveEntry(plan.appRef, options);
|
|
319
|
+
const installedEntry = copyBundleSource(source, targetDir);
|
|
320
|
+
return bundleForEntry(plan, { ...source, entryFile: installedEntry, sourceRoot: targetDir }, targetDir);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (localInstallRef(plan.appRef)) throw error;
|
|
323
|
+
return (options.downloadBundle || downloadPackedMatterhornBundle)(plan, { ...options, targetDir });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function shortcutExtension() {
|
|
328
|
+
if (process.platform === "win32") return ".cmd";
|
|
329
|
+
return "";
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function shortcutCommand(installId) {
|
|
333
|
+
if (process.platform === "win32") return `@echo off\r\npnpm --package matterhorn-sdk dlx matterhorn run ${JSON.stringify(installId)} %*\r\n`;
|
|
334
|
+
return `#!/usr/bin/env sh\nexec pnpm --package matterhorn-sdk dlx matterhorn run ${JSON.stringify(installId)} "$@"\n`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function createShortcut(record, options = {}) {
|
|
338
|
+
const shortcutDir = path.join(options.matterhornHome || userDataDir(), "shortcuts");
|
|
339
|
+
fs.mkdirSync(shortcutDir, { recursive: true });
|
|
340
|
+
const file = path.join(shortcutDir, `${record.installId}${shortcutExtension()}`);
|
|
341
|
+
fs.writeFileSync(file, shortcutCommand(record.installId));
|
|
342
|
+
if (process.platform !== "win32") fs.chmodSync(file, 0o755);
|
|
343
|
+
return file;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function silentIo() {
|
|
347
|
+
return { log() {}, warn() {}, error() {} };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function commandForPlatform(command) {
|
|
351
|
+
if (process.platform === "win32" && command === "npm") return "npm.cmd";
|
|
352
|
+
if (process.platform === "win32" && command === "npx") return "npx.cmd";
|
|
353
|
+
return command;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function launchCommand(record) {
|
|
357
|
+
const command = record.launch?.command;
|
|
358
|
+
if (!command) return command;
|
|
359
|
+
if (path.isAbsolute(command)) return command;
|
|
360
|
+
if (!command.includes("/") && !command.includes("\\")) return commandForPlatform(command);
|
|
361
|
+
return path.resolve(record.bundle?.dir || record.installDir, command);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function launchArgs(template = [], context = {}) {
|
|
365
|
+
return template.map((arg) => String(arg)
|
|
366
|
+
.replaceAll("{room}", context.roomName || "")
|
|
367
|
+
.replaceAll("{url}", context.roomUrl || ""));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function appLaunchEnv(context = {}, options = {}) {
|
|
371
|
+
const trust = options.trust || "trusted-installed";
|
|
372
|
+
if (trust === "trusted-local-dev" && (options.unsafeInheritEnv === true || process.env.MATTERHORN_UNSAFE_APP_FULL_ENV === "1")) {
|
|
373
|
+
options.io?.warn?.("WARNING: MATTERHORN_UNSAFE_APP_FULL_ENV exposes the full parent environment to trusted local app code.");
|
|
374
|
+
}
|
|
375
|
+
return buildMatterhornCommandEnv({
|
|
376
|
+
trust,
|
|
377
|
+
commandEnv: matterhornLaunchEnv(context),
|
|
378
|
+
unsafeInheritEnv: options.unsafeInheritEnv
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function matterhornLaunchEnv(context = {}) {
|
|
383
|
+
return {
|
|
384
|
+
MATTERHORN_ROOM_NAME: context.roomName || "",
|
|
385
|
+
MATTERHORN_ROOM_URL: context.roomUrl || "",
|
|
386
|
+
MATTERHORN_RELAY_ADDRESS: context.relayAddress || "",
|
|
387
|
+
MATTERHORN_ROOM_PEER_ID: context.roomPeerId || "",
|
|
388
|
+
MATTERHORN_INVITE_ID: context.inviteId || "",
|
|
389
|
+
MATTERHORN_USER_ID: context.userId || ""
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function launchInstalledSurface(record, context = {}, options = {}) {
|
|
394
|
+
const launch = record.launch;
|
|
395
|
+
if (!launch?.command) return undefined;
|
|
396
|
+
const child = spawn(launchCommand(record), launchArgs(launch.args || [], context), {
|
|
397
|
+
cwd: record.bundle?.dir || record.installDir,
|
|
398
|
+
env: appLaunchEnv(context, options),
|
|
399
|
+
stdio: options.launchStdio || "ignore",
|
|
400
|
+
detached: options.launchDetached !== false,
|
|
401
|
+
windowsHide: false
|
|
402
|
+
});
|
|
403
|
+
if (options.launchDetached !== false) child.unref();
|
|
404
|
+
return child;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function writeInstallRecord(record) {
|
|
408
|
+
fs.mkdirSync(record.installDir, { recursive: true });
|
|
409
|
+
fs.writeFileSync(path.join(record.installDir, "matterhorn-install.json"), `${JSON.stringify(record, null, 2)}\n`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function readInstallRecord(installId, options = {}) {
|
|
413
|
+
const matterhornHome = options.matterhornHome || userDataDir();
|
|
414
|
+
return JSON.parse(fs.readFileSync(installRecordFile(matterhornHome, installId), "utf8"));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function readExistingInstallRecord(plan, options = {}) {
|
|
418
|
+
const matterhornHome = options.matterhornHome || userDataDir();
|
|
419
|
+
const file = installRecordFile(matterhornHome, plan.installId);
|
|
420
|
+
if (!fs.existsSync(file)) return undefined;
|
|
421
|
+
const record = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
422
|
+
if (record.appRef !== plan.appRef && record.packageName !== plan.packageName) return undefined;
|
|
423
|
+
return record;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function installMatterhornApplication(parsed, io = console, options = {}) {
|
|
427
|
+
const matterhornHome = options.matterhornHome || userDataDir();
|
|
428
|
+
const installDir = installRoot(matterhornHome, parsed.installId);
|
|
429
|
+
const plan = { ...parsed, installDir };
|
|
430
|
+
const bundle = await Promise.resolve((options.materializeBundle || materializeMatterhornBundle)(plan, options));
|
|
431
|
+
const installedAppRef = bundle.appRef || fileAppRef(bundle.entry);
|
|
432
|
+
const deployment = (options.loadMatterhornDeployment || loadMatterhornDeployment)(installedAppRef, { cwd: bundle.bundleDir || installDir });
|
|
433
|
+
assertDeployable(deployment);
|
|
434
|
+
const record = {
|
|
435
|
+
kind: "matterhorn.installed-app",
|
|
436
|
+
version: 1,
|
|
437
|
+
installId: plan.installId,
|
|
438
|
+
appRef: plan.appRef,
|
|
439
|
+
bundleName: plan.bundleName,
|
|
440
|
+
packageName: plan.packageName,
|
|
441
|
+
installedAppRef,
|
|
442
|
+
installDir,
|
|
443
|
+
bundle: {
|
|
444
|
+
kind: bundle.kind || "matterhorn.bundle",
|
|
445
|
+
runner: bundle.runner || "node",
|
|
446
|
+
dir: bundle.bundleDir || bundleRoot(installDir),
|
|
447
|
+
entry: bundle.entry,
|
|
448
|
+
sourceRef: bundle.sourceRef || plan.appRef
|
|
449
|
+
},
|
|
450
|
+
roomName: plan.roomName,
|
|
451
|
+
relays: plan.relays,
|
|
452
|
+
serveMode: plan.serveMode,
|
|
453
|
+
userId: plan.userId,
|
|
454
|
+
launch: deployment.launch || deployment.app?.launch || undefined,
|
|
455
|
+
trust: deployment.trust,
|
|
456
|
+
app: {
|
|
457
|
+
id: deployment.id,
|
|
458
|
+
name: deployment.name,
|
|
459
|
+
version: deployment.version
|
|
460
|
+
},
|
|
461
|
+
installedAt: new Date(options.now ? options.now() : Date.now()).toISOString()
|
|
462
|
+
};
|
|
463
|
+
writeInstallRecord(record);
|
|
464
|
+
record.shortcut = createShortcut(record, { matterhornHome });
|
|
465
|
+
writeInstallRecord(record);
|
|
466
|
+
io.log(`Installed ${record.app.name} (${record.app.id}@${record.app.version || "unknown"})`);
|
|
467
|
+
if (!record.launch) {
|
|
468
|
+
io.log(`Bundle: ${record.bundle.dir}`);
|
|
469
|
+
io.log(`Shortcut: ${record.shortcut}`);
|
|
470
|
+
}
|
|
471
|
+
return record;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function ensureMatterhornApplication(parsed, io = console, options = {}) {
|
|
475
|
+
const matterhornHome = options.matterhornHome || userDataDir();
|
|
476
|
+
const installDir = installRoot(matterhornHome, parsed.installId);
|
|
477
|
+
const plan = { ...parsed, installDir };
|
|
478
|
+
const existing = (options.readExistingInstallRecord || readExistingInstallRecord)(plan, { ...options, matterhornHome });
|
|
479
|
+
if (existing) return existing;
|
|
480
|
+
return await installMatterhornApplication(parsed, io, options);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function activePublicInvite(store) {
|
|
484
|
+
return (store.publicInvites || []).find((invite) => invite?.status === "active" && invite.secret) || undefined;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function roomStoreFile(dataDir, roomName) {
|
|
488
|
+
return path.join(dataDir, `${roomName}.json`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function waitForLaunchRoomConfig(record, options = {}) {
|
|
492
|
+
const roomName = options.roomName || record.roomName;
|
|
493
|
+
const dataDir = options.dataDir || options.matterhornHome || userDataDir();
|
|
494
|
+
const file = roomStoreFile(dataDir, roomName);
|
|
495
|
+
const deadline = Date.now() + (options.launchConfigTimeoutMs || 5000);
|
|
496
|
+
let lastStore;
|
|
497
|
+
while (Date.now() <= deadline) {
|
|
498
|
+
if (fs.existsSync(file)) {
|
|
499
|
+
lastStore = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
500
|
+
const invite = activePublicInvite(lastStore);
|
|
501
|
+
if (lastStore.roomPeerId && invite?.secret) {
|
|
502
|
+
return {
|
|
503
|
+
roomName,
|
|
504
|
+
roomPeerId: lastStore.roomPeerId,
|
|
505
|
+
roomSecret: invite.secret,
|
|
506
|
+
inviteId: invite.id,
|
|
507
|
+
relayAddress: (options.relays && options.relays[0]) || (record.relays && record.relays[0]) || lastStore.relayAddress || "",
|
|
508
|
+
userId: options.userId || record.userId || ""
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
roomName,
|
|
516
|
+
roomPeerId: lastStore?.roomPeerId || "",
|
|
517
|
+
roomSecret: activePublicInvite(lastStore)?.secret || lastStore?.roomSecret || "",
|
|
518
|
+
inviteId: activePublicInvite(lastStore)?.id || "",
|
|
519
|
+
relayAddress: (options.relays && options.relays[0]) || (record.relays && record.relays[0]) || lastStore?.relayAddress || "",
|
|
520
|
+
userId: options.userId || record.userId || ""
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function deployArgsForInstallRecord(record, overrides = {}) {
|
|
525
|
+
const roomName = overrides.roomName || record.roomName;
|
|
526
|
+
const relays = overrides.relays && overrides.relays.length > 0 ? overrides.relays : record.relays || [];
|
|
527
|
+
const args = [record.installedAppRef, roomName, "--serve", record.serveMode || "built"];
|
|
528
|
+
if (relays.length > 0) {
|
|
529
|
+
args.push("--");
|
|
530
|
+
for (const relay of relays) args.push("--relay-peer", relay);
|
|
531
|
+
}
|
|
532
|
+
return args;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function runInstalledMatterhornApplication(record, io = console, options = {}) {
|
|
536
|
+
const args = deployArgsForInstallRecord(record, options);
|
|
537
|
+
const deployIo = record.launch ? silentIo() : io;
|
|
538
|
+
const code = await (options.runAppDeployCommand || runAppDeployCommand)(args, deployIo, {
|
|
539
|
+
...options,
|
|
540
|
+
cwd: record.bundle?.dir || record.installDir,
|
|
541
|
+
dataDir: options.dataDir || options.matterhornHome || userDataDir(),
|
|
542
|
+
background: Boolean(record.launch),
|
|
543
|
+
hostStdio: record.launch ? "ignore" : "inherit"
|
|
544
|
+
});
|
|
545
|
+
if (code !== 0) return code;
|
|
546
|
+
if (record.launch) {
|
|
547
|
+
const launchContext = await (options.waitForLaunchRoomConfig || waitForLaunchRoomConfig)(record, options);
|
|
548
|
+
(options.launchInstalledSurface || launchInstalledSurface)(record, {
|
|
549
|
+
...launchContext,
|
|
550
|
+
roomName: options.roomName || record.roomName,
|
|
551
|
+
userId: options.userId || record.userId || launchContext.userId || ""
|
|
552
|
+
}, options);
|
|
553
|
+
io.log(`Launched ${record.app.name}`);
|
|
554
|
+
}
|
|
555
|
+
return code;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function runAppInstallCommand(args, io = console, options = {}) {
|
|
559
|
+
const parsed = parseMatterhornInstallArgs(args, options);
|
|
560
|
+
const record = await ensureMatterhornApplication(parsed, io, options);
|
|
561
|
+
return await runInstalledMatterhornApplication(record, io, { ...options, userId: parsed.userId, roomName: parsed.roomName, relays: parsed.relays });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function runAppLaunchCommand(args, io = console, options = {}) {
|
|
565
|
+
const parsed = parseMatterhornInstallArgs(args, options);
|
|
566
|
+
const record = await ensureMatterhornApplication(parsed, io, options);
|
|
567
|
+
return await runInstalledMatterhornApplication(record, io, { ...options, userId: parsed.userId, roomName: parsed.roomName, relays: parsed.relays });
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function runInstalledAppCommand(args, io = console, options = {}) {
|
|
571
|
+
const parsed = parseMatterhornRunArgs(args);
|
|
572
|
+
const record = readInstallRecord(parsed.installId, options);
|
|
573
|
+
return await runInstalledMatterhornApplication(record, io, {
|
|
574
|
+
...options,
|
|
575
|
+
roomName: parsed.roomName,
|
|
576
|
+
relays: parsed.relays,
|
|
577
|
+
userId: parsed.userId
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
module.exports = {
|
|
582
|
+
bundleNameForRef,
|
|
583
|
+
bundleRoot,
|
|
584
|
+
createShortcut,
|
|
585
|
+
defaultRoomName,
|
|
586
|
+
deployArgsForInstallRecord,
|
|
587
|
+
downloadPackedMatterhornBundle,
|
|
588
|
+
installIdForBundle,
|
|
589
|
+
installIdForPackage,
|
|
590
|
+
installRecordFile,
|
|
591
|
+
installMatterhornApplication,
|
|
592
|
+
installRoot,
|
|
593
|
+
materializeMatterhornBundle,
|
|
594
|
+
packageNameForRef,
|
|
595
|
+
parseMatterhornInstallArgs,
|
|
596
|
+
parseMatterhornRunArgs,
|
|
597
|
+
readInstallRecord,
|
|
598
|
+
readExistingInstallRecord,
|
|
599
|
+
resolveExtractedPackageEntry,
|
|
600
|
+
ensureMatterhornApplication,
|
|
601
|
+
runAppInstallCommand,
|
|
602
|
+
runAppLaunchCommand,
|
|
603
|
+
runInstalledAppCommand,
|
|
604
|
+
runInstalledMatterhornApplication,
|
|
605
|
+
appLaunchEnv,
|
|
606
|
+
launchInstalledSurface,
|
|
607
|
+
tarArchiveArg,
|
|
608
|
+
tarPathArg
|
|
609
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const { canonicalJson, MATTERHORN_CALL_SIGNAL_NOSTR_KIND } = require("@mh-gg/event");
|
|
2
|
+
const { finalizeEvent, verifyEvent } = require("nostr-tools/pure");
|
|
3
|
+
const { hexToBytes } = require("nostr-tools/utils");
|
|
4
|
+
|
|
5
|
+
const CALL_SIGNAL_MAX_AGE_SECONDS = 10 * 60;
|
|
6
|
+
const CALL_SIGNAL_FUTURE_SKEW_SECONDS = 60;
|
|
7
|
+
|
|
8
|
+
function isHex(value, length) {
|
|
9
|
+
return typeof value === "string" && new RegExp(`^[0-9a-f]{${length}}$`, "i").test(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function signedContent(input) {
|
|
13
|
+
return canonicalJson({
|
|
14
|
+
protocol: "matterhorn-sdk",
|
|
15
|
+
version: 1,
|
|
16
|
+
kind: "call-signal",
|
|
17
|
+
roomName: input.roomName,
|
|
18
|
+
sourceClientId: input.sourceClientId,
|
|
19
|
+
targetClientId: input.targetClientId,
|
|
20
|
+
signal: input.signal
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function eventTemplate(input, createdAt) {
|
|
25
|
+
return {
|
|
26
|
+
kind: MATTERHORN_CALL_SIGNAL_NOSTR_KIND,
|
|
27
|
+
created_at: createdAt,
|
|
28
|
+
tags: [
|
|
29
|
+
["protocol", "matterhorn-sdk"],
|
|
30
|
+
["room", input.roomName],
|
|
31
|
+
["source", input.sourceClientId],
|
|
32
|
+
["target", input.targetClientId],
|
|
33
|
+
["session", input.signal.sessionId]
|
|
34
|
+
],
|
|
35
|
+
content: signedContent(input)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function eventForSignature(input) {
|
|
40
|
+
return {
|
|
41
|
+
...eventTemplate(input, input.auth.createdAt),
|
|
42
|
+
pubkey: input.auth.pubkey,
|
|
43
|
+
id: input.auth.eventId,
|
|
44
|
+
sig: input.auth.sig
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function verifyCallSignal(input) {
|
|
49
|
+
const auth = input.auth;
|
|
50
|
+
if (!auth || auth.alg !== "nostr-secp256k1") return false;
|
|
51
|
+
if (!isHex(input.expectedPubkey, 64) || auth.pubkey.toLowerCase() !== input.expectedPubkey.toLowerCase()) return false;
|
|
52
|
+
if (!isHex(auth.pubkey, 64) || !isHex(auth.eventId, 64) || !isHex(auth.sig, 128)) return false;
|
|
53
|
+
if (!Number.isInteger(auth.createdAt) || auth.createdAt <= 0) return false;
|
|
54
|
+
const now = Math.floor(Date.now() / 1000);
|
|
55
|
+
if (auth.createdAt < now - CALL_SIGNAL_MAX_AGE_SECONDS || auth.createdAt > now + CALL_SIGNAL_FUTURE_SKEW_SECONDS) return false;
|
|
56
|
+
return verifyEvent(eventForSignature(input));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function signCallSignal(input) {
|
|
60
|
+
const event = finalizeEvent(
|
|
61
|
+
eventTemplate(input, Math.floor(Date.now() / 1000)),
|
|
62
|
+
hexToBytes(input.privateKey)
|
|
63
|
+
);
|
|
64
|
+
return {
|
|
65
|
+
alg: "nostr-secp256k1",
|
|
66
|
+
pubkey: event.pubkey,
|
|
67
|
+
eventId: event.id,
|
|
68
|
+
sig: event.sig,
|
|
69
|
+
createdAt: event.created_at
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
signCallSignal,
|
|
75
|
+
verifyCallSignal
|
|
76
|
+
};
|