@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
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require("node:child_process");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
|
|
7
|
+
function localWorkspaceCli() {
|
|
8
|
+
const candidate = path.resolve(__dirname, "..", "..", "..", "bin", "matterhorn.cjs");
|
|
9
|
+
return fs.existsSync(candidate) ? candidate : "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function packagedCli() {
|
|
13
|
+
const candidate = path.resolve(__dirname, "..", "runtime", "bin", "matterhorn.cjs");
|
|
14
|
+
return fs.existsSync(candidate) ? candidate : "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveCliPath() {
|
|
18
|
+
return localWorkspaceCli() || packagedCli();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function runNodeScript(script, args) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const child = spawn(process.execPath, [script, ...args], {
|
|
24
|
+
stdio: "inherit",
|
|
25
|
+
windowsHide: true
|
|
26
|
+
});
|
|
27
|
+
child.on("exit", (code, signal) => {
|
|
28
|
+
if (signal) {
|
|
29
|
+
process.kill(process.pid, signal);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
resolve(code ?? 0);
|
|
33
|
+
});
|
|
34
|
+
child.on("error", reject);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main(args = process.argv.slice(2)) {
|
|
39
|
+
const cliPath = resolveCliPath();
|
|
40
|
+
if (!cliPath) {
|
|
41
|
+
throw new Error("Matterhorn CLI runtime is missing. Reinstall @mh-gg/cli or run pnpm --dir packages/matterhorn-cli run prepare:files.");
|
|
42
|
+
}
|
|
43
|
+
const code = await runNodeScript(cliPath, args);
|
|
44
|
+
process.exitCode = code;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (require.main === module) {
|
|
48
|
+
main().catch((error) => {
|
|
49
|
+
console.error(error.message || error);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
main,
|
|
56
|
+
resolveCliPath
|
|
57
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mh-gg/cli",
|
|
3
|
+
"version": "0.1.1-alpha.20260613T085325975Z",
|
|
4
|
+
"description": "Matterhorn command-line interface for hosting, deploying, and launching Matterhorn apps.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"matterhorn": "./bin/matterhorn.cjs"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
"./bin/matterhorn.cjs": "./bin/matterhorn.cjs",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"runtime",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@roamhq/wrtc": "^0.10.0",
|
|
20
|
+
"nostr-tools": "^2.23.5",
|
|
21
|
+
"peer": "^1.0.2",
|
|
22
|
+
"peerjs": "^1.5.5",
|
|
23
|
+
"qrcode-terminal": "^0.12.0",
|
|
24
|
+
"ws": "^8.18.0",
|
|
25
|
+
"xhr2": "^0.2.1",
|
|
26
|
+
"@mh-gg/host-config": "^0.1.1-alpha.20260613T085325975Z",
|
|
27
|
+
"@mh-gg/base-plugins": "^0.1.1-alpha.20260613T085325975Z",
|
|
28
|
+
"@mh-gg/base": "^0.1.1-alpha.20260613T085325975Z",
|
|
29
|
+
"@mh-gg/host-store": "^0.1.1-alpha.20260613T085325975Z",
|
|
30
|
+
"@mh-gg/event": "^0.1.1-alpha.20260613T085325975Z",
|
|
31
|
+
"@mh-gg/protocol": "^0.1.1-alpha.20260613T085325975Z",
|
|
32
|
+
"@mh-gg/host-runtime": "^0.1.1-alpha.20260613T085325975Z",
|
|
33
|
+
"@mh-gg/host-ipc": "^0.1.1-alpha.20260613T085325975Z",
|
|
34
|
+
"@mh-gg/schema": "^0.1.1-alpha.20260613T085325975Z",
|
|
35
|
+
"@mh-gg/relay-mesh": "^0.1.1-alpha.20260613T085325975Z",
|
|
36
|
+
"@mh-gg/relay-core": "^0.1.1-alpha.20260613T085325975Z",
|
|
37
|
+
"@mh-gg/room-security": "^0.1.1-alpha.20260613T085325975Z",
|
|
38
|
+
"@mh-gg/relay-runtime": "^0.1.1-alpha.20260613T085325975Z",
|
|
39
|
+
"@mh-gg/room-link": "^0.1.1-alpha.20260613T085325975Z"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22.12"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"prepare:files": "node scripts/prepare-files.cjs",
|
|
46
|
+
"test:package": "node scripts/smoke-package.cjs",
|
|
47
|
+
"pack:verify": "pnpm run prepare:files && pnpm run test:package"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const { createHash } = require("node:crypto");
|
|
3
|
+
const { bundleDistDir, bundleEntryFile, bundleEntryName, bundleMountPath } = require("./paths.cjs");
|
|
4
|
+
|
|
5
|
+
function fileIntegrity(filePath) {
|
|
6
|
+
return `sha256-${createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function appBundleArtifact(frontend) {
|
|
10
|
+
const file = bundleEntryFile(frontend);
|
|
11
|
+
const stat = fs.statSync(file);
|
|
12
|
+
if (!stat.isFile()) throw new Error(`Built app bundle is missing ${file}`);
|
|
13
|
+
return {
|
|
14
|
+
file,
|
|
15
|
+
entry: bundleEntryName(frontend),
|
|
16
|
+
distDir: bundleDistDir(frontend),
|
|
17
|
+
mountPath: bundleMountPath(frontend),
|
|
18
|
+
byteLength: stat.size,
|
|
19
|
+
integrity: fileIntegrity(file)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
appBundleArtifact
|
|
25
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { appBundleArtifact } = require("./artifacts.cjs");
|
|
4
|
+
const { bundleConfig, bundleDistDir, bundleEntryFile, bundleMountPath, frontendRoot, launcherBuildCommand, repoRoot } = require("./paths.cjs");
|
|
5
|
+
const { commandSpec, runCommand } = require("./processes.cjs");
|
|
6
|
+
const { assertAppCommandAllowed, commandEnvTrustForPolicy } = require("./commandPolicy.cjs");
|
|
7
|
+
const { selectFrontendPort } = require("./ports.cjs");
|
|
8
|
+
const { startBundleStaticFrontend, startBundledStaticFrontend, startStaticFrontend, waitForHttpOk } = require("./httpServers.cjs");
|
|
9
|
+
const { ensureWorkspaceSdkBuild } = require("../../scripts/ensureWorkspaceSdkBuild.cjs");
|
|
10
|
+
|
|
11
|
+
const DEFAULT_FRONTEND_BUILD_TIMEOUT_MS = 120000;
|
|
12
|
+
|
|
13
|
+
function isIgnoredSourcePath(candidate, ignoredRoots) {
|
|
14
|
+
const base = path.basename(candidate);
|
|
15
|
+
if (base === "node_modules" || base === ".git" || base === ".matterhorn-live" || base === ".matterhorn-live-objects" || base === "coverage") return true;
|
|
16
|
+
const resolved = path.resolve(candidate);
|
|
17
|
+
return ignoredRoots.some((ignoredRoot) => {
|
|
18
|
+
const relative = path.relative(ignoredRoot, resolved);
|
|
19
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function latestSourceMtime(root, ignoredRoots = []) {
|
|
24
|
+
if (!root || !fs.existsSync(root)) return 0;
|
|
25
|
+
const resolvedIgnoredRoots = ignoredRoots.filter(Boolean).map((item) => path.resolve(item));
|
|
26
|
+
const pending = [path.resolve(root)];
|
|
27
|
+
let latest = 0;
|
|
28
|
+
|
|
29
|
+
while (pending.length > 0) {
|
|
30
|
+
const current = pending.pop();
|
|
31
|
+
if (!current || isIgnoredSourcePath(current, resolvedIgnoredRoots)) continue;
|
|
32
|
+
let stat;
|
|
33
|
+
try {
|
|
34
|
+
stat = fs.statSync(current);
|
|
35
|
+
} catch {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!stat.isDirectory()) {
|
|
39
|
+
if (stat.isFile()) latest = Math.max(latest, stat.mtimeMs);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
45
|
+
} catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const entryPath = path.join(current, entry.name);
|
|
50
|
+
if (isIgnoredSourcePath(entryPath, resolvedIgnoredRoots)) continue;
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
pending.push(entryPath);
|
|
53
|
+
} else if (entry.isFile()) {
|
|
54
|
+
try {
|
|
55
|
+
latest = Math.max(latest, fs.statSync(entryPath).mtimeMs);
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return latest;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function workspaceSdkBuildInputs() {
|
|
65
|
+
const sdkRoot = path.join(repoRoot(), "packages", "matterhorn-sdk");
|
|
66
|
+
return [
|
|
67
|
+
path.join(sdkRoot, "dist"),
|
|
68
|
+
path.join(sdkRoot, "package.json")
|
|
69
|
+
].filter((target) => fs.existsSync(target));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function appBundleBuildRequired(frontend) {
|
|
73
|
+
const bundleFile = bundleEntryFile(frontend);
|
|
74
|
+
if (!fs.existsSync(bundleFile)) return true;
|
|
75
|
+
let bundleMtime = 0;
|
|
76
|
+
try {
|
|
77
|
+
const stat = fs.statSync(bundleFile);
|
|
78
|
+
if (!stat.isFile()) return true;
|
|
79
|
+
bundleMtime = stat.mtimeMs;
|
|
80
|
+
} catch {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ignoredRoots = [bundleDistDir(frontend)];
|
|
85
|
+
const inputs = [frontendRoot(frontend), ...workspaceSdkBuildInputs()];
|
|
86
|
+
return inputs.some((input) => latestSourceMtime(input, ignoredRoots) > bundleMtime);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async function startBuiltFrontend(frontend, options = {}) {
|
|
91
|
+
const root = frontendRoot(frontend);
|
|
92
|
+
const port = await selectFrontendPort(options.port, frontend.defaultPort);
|
|
93
|
+
if (frontend.build) {
|
|
94
|
+
const policy = assertAppCommandAllowed(options.trust || frontend.trust, "matterhorn frontend build");
|
|
95
|
+
await runCommand(commandSpec(frontend.build, "build"), {
|
|
96
|
+
cwd: root,
|
|
97
|
+
label: "matterhorn frontend build",
|
|
98
|
+
logger: options.logger,
|
|
99
|
+
port,
|
|
100
|
+
trust: commandEnvTrustForPolicy(policy),
|
|
101
|
+
timeoutMs: options.buildTimeoutMs || DEFAULT_FRONTEND_BUILD_TIMEOUT_MS
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const server = await startStaticFrontend(frontend, { port });
|
|
105
|
+
await waitForHttpOk(server.appUrl, options.timeoutMs);
|
|
106
|
+
return server;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function startBundledBuiltFrontend(frontend, options = {}) {
|
|
110
|
+
const root = frontendRoot(frontend);
|
|
111
|
+
const port = await selectFrontendPort(options.port, frontend.defaultPort);
|
|
112
|
+
const bundle = bundleConfig(frontend);
|
|
113
|
+
const policy = assertAppCommandAllowed(options.trust || frontend.trust, "matterhorn bundled frontend build");
|
|
114
|
+
await ensureWorkspaceSdkBuild({ logger: options.logger, timeoutMs: options.buildTimeoutMs || DEFAULT_FRONTEND_BUILD_TIMEOUT_MS });
|
|
115
|
+
await runCommand(commandSpec(bundle.build, "bundle.build"), {
|
|
116
|
+
cwd: root,
|
|
117
|
+
label: "matterhorn app bundle build",
|
|
118
|
+
logger: options.logger,
|
|
119
|
+
port,
|
|
120
|
+
trust: commandEnvTrustForPolicy(policy),
|
|
121
|
+
timeoutMs: options.buildTimeoutMs || DEFAULT_FRONTEND_BUILD_TIMEOUT_MS
|
|
122
|
+
});
|
|
123
|
+
await runCommand(commandSpec(frontend.launcher?.build || launcherBuildCommand(), "launcher.build"), {
|
|
124
|
+
cwd: repoRoot(),
|
|
125
|
+
label: "matterhorn launcher build",
|
|
126
|
+
logger: options.logger,
|
|
127
|
+
port,
|
|
128
|
+
trust: commandEnvTrustForPolicy(policy),
|
|
129
|
+
timeoutMs: options.buildTimeoutMs || DEFAULT_FRONTEND_BUILD_TIMEOUT_MS
|
|
130
|
+
});
|
|
131
|
+
const server = await startBundledStaticFrontend(frontend, { port });
|
|
132
|
+
await waitForHttpOk(server.appUrl, options.timeoutMs);
|
|
133
|
+
await waitForHttpOk(server.bundleUrl, options.timeoutMs);
|
|
134
|
+
return server;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function buildAppBundleFrontend(appOrFrontend, options = {}) {
|
|
138
|
+
const frontend = appOrFrontend?.frontend || appOrFrontend;
|
|
139
|
+
if (!frontend?.bundle) throw new Error("App does not declare a bundled frontend");
|
|
140
|
+
const root = frontendRoot(frontend);
|
|
141
|
+
const port = options.port ?? options.bundlePort ?? frontend.bundle.defaultPort ?? 0;
|
|
142
|
+
const policy = assertAppCommandAllowed(options.trust || frontend.trust || appOrFrontend?.trust, "matterhorn app bundle build");
|
|
143
|
+
await ensureWorkspaceSdkBuild({ logger: options.logger, timeoutMs: options.buildTimeoutMs || DEFAULT_FRONTEND_BUILD_TIMEOUT_MS });
|
|
144
|
+
if (!options.force && !appBundleBuildRequired(frontend)) {
|
|
145
|
+
options.logger?.log?.(`matterhorn app bundle build skipped: ${bundleEntryFile(frontend)} is current`);
|
|
146
|
+
return appBundleArtifact(frontend);
|
|
147
|
+
}
|
|
148
|
+
await runCommand(commandSpec(frontend.bundle.build, "bundle.build"), {
|
|
149
|
+
cwd: root,
|
|
150
|
+
label: "matterhorn app bundle build",
|
|
151
|
+
logger: options.logger,
|
|
152
|
+
port,
|
|
153
|
+
bundlePort: options.bundlePort ?? port,
|
|
154
|
+
mountPath: bundleMountPath(frontend),
|
|
155
|
+
trust: commandEnvTrustForPolicy(policy),
|
|
156
|
+
timeoutMs: options.buildTimeoutMs || DEFAULT_FRONTEND_BUILD_TIMEOUT_MS
|
|
157
|
+
});
|
|
158
|
+
return appBundleArtifact(frontend);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function startBundleBuiltFrontend(frontend, options = {}) {
|
|
162
|
+
const bundle = bundleConfig(frontend);
|
|
163
|
+
const port = await selectFrontendPort(options.port ?? options.bundlePort, bundle.defaultPort);
|
|
164
|
+
await buildAppBundleFrontend(frontend, { ...options, port, bundlePort: port });
|
|
165
|
+
const server = await startBundleStaticFrontend(frontend, { port });
|
|
166
|
+
await waitForHttpOk(server.bundleUrl, options.timeoutMs);
|
|
167
|
+
return server;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
appBundleBuildRequired,
|
|
172
|
+
buildAppBundleFrontend,
|
|
173
|
+
startBundleBuiltFrontend,
|
|
174
|
+
startBuiltFrontend,
|
|
175
|
+
startBundledBuiltFrontend
|
|
176
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const MATTERHORN_PUBLIC_KEYS = new Set([
|
|
2
|
+
"MATTERHORN_APP_BUNDLE_MOUNT",
|
|
3
|
+
"MATTERHORN_APP_BUNDLE_TARGET",
|
|
4
|
+
"MATTERHORN_BUNDLE_BASE",
|
|
5
|
+
"MATTERHORN_BUNDLE_PORT",
|
|
6
|
+
"MATTERHORN_FRONTEND_PORT"
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
function baseProcessEnv() {
|
|
10
|
+
const env = {};
|
|
11
|
+
for (const key of ["PATH", "Path", "ComSpec", "SystemRoot", "WINDIR", "TEMP", "TMP", "HOME", "USERPROFILE"]) {
|
|
12
|
+
if (process.env[key] !== undefined) env[key] = process.env[key];
|
|
13
|
+
}
|
|
14
|
+
return env;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sensitiveEnvKey(key) {
|
|
18
|
+
const upper = String(key).toUpperCase();
|
|
19
|
+
if (MATTERHORN_PUBLIC_KEYS.has(upper)) return false;
|
|
20
|
+
if (upper === "NODE_OPTIONS" || upper === "SSH_AUTH_SOCK") return true;
|
|
21
|
+
if (upper.startsWith("AWS_") || upper.startsWith("GOOGLE_") || upper.startsWith("AZURE_")) return true;
|
|
22
|
+
return /(?:^|_)(?:TOKEN|SECRET|PASSWORD|KEY)$/.test(upper);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validateCommandEnv(commandEnv = {}, options = {}) {
|
|
26
|
+
for (const key of Object.keys(commandEnv || {})) {
|
|
27
|
+
if (!sensitiveEnvKey(key)) continue;
|
|
28
|
+
if (options.trust === "trusted-local-dev" && options.allowSensitiveCommandEnv === true) continue;
|
|
29
|
+
throw new Error(`App command env key ${key} is not allowed by Matterhorn command env policy`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function commandPortEnv(input = {}) {
|
|
34
|
+
const env = {};
|
|
35
|
+
if (input.port !== undefined) {
|
|
36
|
+
env.PORT = String(input.port);
|
|
37
|
+
env.MATTERHORN_FRONTEND_PORT = String(input.port);
|
|
38
|
+
}
|
|
39
|
+
if (input.bundlePort !== undefined) env.MATTERHORN_BUNDLE_PORT = String(input.bundlePort);
|
|
40
|
+
if (input.mountPath !== undefined) env.MATTERHORN_BUNDLE_BASE = String(input.mountPath || "/");
|
|
41
|
+
return env;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function canUnsafeInherit(input) {
|
|
45
|
+
if (input.trust !== "trusted-local-dev") return false;
|
|
46
|
+
return input.unsafeInheritEnv === true || process.env.MATTERHORN_UNSAFE_APP_FULL_ENV === "1";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildMatterhornCommandEnv(input = {}) {
|
|
50
|
+
const trust = input.trust || "untrusted-metadata";
|
|
51
|
+
const commandEnv = input.commandEnv || {};
|
|
52
|
+
validateCommandEnv(commandEnv, {
|
|
53
|
+
trust,
|
|
54
|
+
allowSensitiveCommandEnv: input.allowSensitiveCommandEnv
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
...(canUnsafeInherit({ ...input, trust }) ? process.env : baseProcessEnv()),
|
|
58
|
+
...commandPortEnv(input),
|
|
59
|
+
...commandEnv
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const SECRET_VALUE_PATTERN = /(?:npm|github|ghp|aws|secret|token|password|key)[A-Za-z0-9_./+=:-]{8,}/gi;
|
|
64
|
+
|
|
65
|
+
function redactCommandOutput(value) {
|
|
66
|
+
return String(value).replace(SECRET_VALUE_PATTERN, "[REDACTED]");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
buildMatterhornCommandEnv,
|
|
71
|
+
redactCommandOutput,
|
|
72
|
+
sensitiveEnvKey,
|
|
73
|
+
validateCommandEnv
|
|
74
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const RUNNABLE_COMMAND_POLICIES = new Set(["allowlisted", "sandboxed", "trusted-local-dev"]);
|
|
2
|
+
|
|
3
|
+
function commandPolicyFromTrust(trust) {
|
|
4
|
+
return trust?.commandExecution || "trusted-local-dev";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function commandEnvTrustForPolicy(policy) {
|
|
8
|
+
if (policy === "trusted-local-dev") return "trusted-local-dev";
|
|
9
|
+
if (policy === "allowlisted" || policy === "sandboxed") return "trusted-installed";
|
|
10
|
+
return "untrusted-metadata";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function assertAppCommandAllowed(trust, label) {
|
|
14
|
+
const policy = commandPolicyFromTrust(trust);
|
|
15
|
+
if (RUNNABLE_COMMAND_POLICIES.has(policy)) return policy;
|
|
16
|
+
throw new Error(`${label} requires app command execution trust; metadata-only app sources cannot run frontend commands`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
assertAppCommandAllowed,
|
|
21
|
+
commandEnvTrustForPolicy,
|
|
22
|
+
commandPolicyFromTrust
|
|
23
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const { bundleConfig, bundleMountPath, bundleUrl, frontendRoot, frontendUrl, launcherDevCommand, repoRoot } = require("./paths.cjs");
|
|
2
|
+
const { closeChildProcess, commandSpec, spawnCommand } = require("./processes.cjs");
|
|
3
|
+
const { assertAppCommandAllowed, commandEnvTrustForPolicy } = require("./commandPolicy.cjs");
|
|
4
|
+
const { selectFrontendPort } = require("./ports.cjs");
|
|
5
|
+
const { waitForHttpOk } = require("./httpServers.cjs");
|
|
6
|
+
|
|
7
|
+
async function startDevFrontend(frontend, options = {}) {
|
|
8
|
+
const root = frontendRoot(frontend);
|
|
9
|
+
const port = await selectFrontendPort(options.port, frontend.defaultPort);
|
|
10
|
+
const spec = commandSpec(frontend.dev, "dev");
|
|
11
|
+
const policy = assertAppCommandAllowed(options.trust || frontend.trust, "matterhorn frontend dev");
|
|
12
|
+
const child = spawnCommand(spec, {
|
|
13
|
+
cwd: root,
|
|
14
|
+
label: "matterhorn frontend dev",
|
|
15
|
+
logger: options.logger,
|
|
16
|
+
port,
|
|
17
|
+
trust: commandEnvTrustForPolicy(policy)
|
|
18
|
+
});
|
|
19
|
+
const appUrl = frontendUrl(port, frontend.devBasePath || frontend.basePath || "/");
|
|
20
|
+
try {
|
|
21
|
+
await waitForHttpOk(new URL(frontend.dev?.healthPath || "/", appUrl).toString(), options.timeoutMs);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
await closeChildProcess(child);
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
let closed = false;
|
|
27
|
+
return {
|
|
28
|
+
appUrl,
|
|
29
|
+
mode: "dev",
|
|
30
|
+
port,
|
|
31
|
+
process: child,
|
|
32
|
+
close: async () => {
|
|
33
|
+
if (closed) return;
|
|
34
|
+
closed = true;
|
|
35
|
+
await closeChildProcess(child);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function startBundleDevFrontend(frontend, options = {}) {
|
|
41
|
+
const root = frontendRoot(frontend);
|
|
42
|
+
const bundle = bundleConfig(frontend);
|
|
43
|
+
const bundlePort = await selectFrontendPort(options.port ?? options.bundlePort, bundle.defaultPort);
|
|
44
|
+
const mountPath = bundleMountPath(frontend);
|
|
45
|
+
const bundleSpec = commandSpec(bundle.dev, "bundle.dev");
|
|
46
|
+
const policy = assertAppCommandAllowed(options.trust || frontend.trust, "matterhorn app bundle dev");
|
|
47
|
+
const bundleChild = spawnCommand(bundleSpec, {
|
|
48
|
+
cwd: root,
|
|
49
|
+
label: "matterhorn app bundle dev",
|
|
50
|
+
logger: options.logger,
|
|
51
|
+
port: bundlePort,
|
|
52
|
+
bundlePort,
|
|
53
|
+
mountPath,
|
|
54
|
+
trust: commandEnvTrustForPolicy(policy)
|
|
55
|
+
});
|
|
56
|
+
const appUrl = frontendUrl(bundlePort, "/");
|
|
57
|
+
try {
|
|
58
|
+
const healthPath = bundle.healthPath || bundle.devEntry || "";
|
|
59
|
+
await waitForHttpOk(new URL(`${mountPath}${healthPath}`, appUrl).toString(), options.timeoutMs);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
await closeChildProcess(bundleChild);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
let closed = false;
|
|
65
|
+
return {
|
|
66
|
+
appUrl,
|
|
67
|
+
bundleUrl: bundleUrl(appUrl, mountPath, bundle.devEntry || "src/index.tsx"),
|
|
68
|
+
mode: "dev",
|
|
69
|
+
port: bundlePort,
|
|
70
|
+
bundlePort,
|
|
71
|
+
process: bundleChild,
|
|
72
|
+
close: async () => {
|
|
73
|
+
if (closed) return;
|
|
74
|
+
closed = true;
|
|
75
|
+
await closeChildProcess(bundleChild);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function startBundledDevFrontend(frontend, options = {}) {
|
|
81
|
+
const root = frontendRoot(frontend);
|
|
82
|
+
const launcherPort = await selectFrontendPort(options.port, frontend.defaultPort);
|
|
83
|
+
const appBundlePort = await selectFrontendPort(options.bundlePort, frontend.bundle?.defaultPort);
|
|
84
|
+
const bundle = bundleConfig(frontend);
|
|
85
|
+
const mountPath = bundleMountPath(frontend);
|
|
86
|
+
const bundleSpec = commandSpec(bundle.dev, "bundle.dev");
|
|
87
|
+
const policy = assertAppCommandAllowed(options.trust || frontend.trust, "matterhorn bundled frontend dev");
|
|
88
|
+
const bundleChild = spawnCommand(bundleSpec, {
|
|
89
|
+
cwd: root,
|
|
90
|
+
label: "matterhorn app bundle dev",
|
|
91
|
+
logger: options.logger,
|
|
92
|
+
port: launcherPort,
|
|
93
|
+
bundlePort: appBundlePort,
|
|
94
|
+
mountPath,
|
|
95
|
+
trust: commandEnvTrustForPolicy(policy)
|
|
96
|
+
});
|
|
97
|
+
const bundleBaseUrl = frontendUrl(appBundlePort, "/");
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const healthPath = bundle.healthPath || bundle.devEntry || "";
|
|
101
|
+
await waitForHttpOk(new URL(`${mountPath}${healthPath}`, bundleBaseUrl).toString(), options.timeoutMs);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
await closeChildProcess(bundleChild);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const launcherSpec = commandSpec(frontend.launcher?.dev || launcherDevCommand(), "launcher.dev");
|
|
108
|
+
const launcherChild = spawnCommand(launcherSpec, {
|
|
109
|
+
cwd: repoRoot(),
|
|
110
|
+
label: "matterhorn launcher dev",
|
|
111
|
+
logger: options.logger,
|
|
112
|
+
port: launcherPort,
|
|
113
|
+
bundlePort: appBundlePort,
|
|
114
|
+
mountPath,
|
|
115
|
+
env: {
|
|
116
|
+
MATTERHORN_APP_BUNDLE_TARGET: bundleBaseUrl,
|
|
117
|
+
MATTERHORN_APP_BUNDLE_MOUNT: mountPath
|
|
118
|
+
},
|
|
119
|
+
trust: commandEnvTrustForPolicy(policy)
|
|
120
|
+
});
|
|
121
|
+
const appUrl = frontendUrl(launcherPort, frontend.launcher?.devBasePath || "/");
|
|
122
|
+
try {
|
|
123
|
+
await waitForHttpOk(new URL(frontend.launcher?.healthPath || "/", appUrl).toString(), options.timeoutMs);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
await closeChildProcess(launcherChild);
|
|
126
|
+
await closeChildProcess(bundleChild);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let closed = false;
|
|
131
|
+
return {
|
|
132
|
+
appUrl,
|
|
133
|
+
bundleUrl: bundleUrl(appUrl, mountPath, bundle.devEntry || "src/index.tsx"),
|
|
134
|
+
mode: "dev",
|
|
135
|
+
port: launcherPort,
|
|
136
|
+
bundlePort: appBundlePort,
|
|
137
|
+
close: async () => {
|
|
138
|
+
if (closed) return;
|
|
139
|
+
closed = true;
|
|
140
|
+
await closeChildProcess(launcherChild);
|
|
141
|
+
await closeChildProcess(bundleChild);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
startBundleDevFrontend,
|
|
148
|
+
startBundledDevFrontend,
|
|
149
|
+
startDevFrontend
|
|
150
|
+
};
|