@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,49 @@
|
|
|
1
|
+
const { validateRelayEvent } = require("@mh-gg/relay-core");
|
|
2
|
+
const { MATTERHORN_PRESENCE_NOSTR_KIND } = require("@mh-gg/event");
|
|
3
|
+
const { invalid, isObject, ok } = require("./shared.cjs");
|
|
4
|
+
|
|
5
|
+
function tagValue(tags, name) {
|
|
6
|
+
const tag = Array.isArray(tags) ? tags.find((item) => Array.isArray(item) && item[0] === name) : undefined;
|
|
7
|
+
return tag?.[1];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseEventContent(event) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(event.content || "{}");
|
|
13
|
+
} catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isEncryptedRoomPayload(value) {
|
|
19
|
+
return value
|
|
20
|
+
&& typeof value === "object"
|
|
21
|
+
&& value.encrypted === true
|
|
22
|
+
&& value.alg === "A256GCM"
|
|
23
|
+
&& value.kind === "presence.update"
|
|
24
|
+
&& typeof value.iv === "string"
|
|
25
|
+
&& typeof value.data === "string";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validatePresenceEvent(message) {
|
|
29
|
+
const event = message.presenceEvent;
|
|
30
|
+
if (!isObject(event)) return invalid("client/presence", "presenceEvent is required");
|
|
31
|
+
if (event.kind !== MATTERHORN_PRESENCE_NOSTR_KIND) return invalid("client/presence", "presenceEvent kind is invalid");
|
|
32
|
+
const eventValidation = validateRelayEvent(event);
|
|
33
|
+
if (!eventValidation.ok) return invalid("client/presence", `presenceEvent is invalid: ${eventValidation.message}`);
|
|
34
|
+
if (tagValue(event.tags, "protocol") !== "matterhorn-sdk") return invalid("client/presence", "presenceEvent protocol tag is invalid");
|
|
35
|
+
if (tagValue(event.tags, "d") !== message.roomName) return invalid("client/presence", "presenceEvent room tag is invalid");
|
|
36
|
+
if (tagValue(event.tags, "t") !== "matterhorn:presence") return invalid("client/presence", "presenceEvent topic tag is invalid");
|
|
37
|
+
if (tagValue(event.tags, "client") !== message.clientId) return invalid("client/presence", "presenceEvent client tag is invalid");
|
|
38
|
+
if (tagValue(event.tags, "status") !== undefined) return invalid("client/presence", "presenceEvent status tag must be encrypted");
|
|
39
|
+
|
|
40
|
+
const content = parseEventContent(event);
|
|
41
|
+
if (!isEncryptedRoomPayload(content)) {
|
|
42
|
+
return invalid("client/presence", "presenceEvent content is invalid");
|
|
43
|
+
}
|
|
44
|
+
return ok();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
validatePresenceEvent
|
|
49
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const { safeValidate, validateClientPushRegister, validateClientPushGrant } = require("@mh-gg/protocol");
|
|
2
|
+
const { hasText, invalid, isObject, ok, protocolOne, protocolResult } = require("./shared.cjs");
|
|
3
|
+
|
|
4
|
+
const MAX_PUSH_SEND_TARGETS = 256;
|
|
5
|
+
const MAX_PUSH_SEND_PAYLOAD_BYTES = 3 * 1024;
|
|
6
|
+
const PUSH_URGENCY_VALUES = new Set(["very-low", "low", "normal", "high"]);
|
|
7
|
+
|
|
8
|
+
function hasValidTtlSeconds(value) {
|
|
9
|
+
if (value === undefined) return true;
|
|
10
|
+
return Number.isInteger(value) && value > 0 && value <= 24 * 60 * 60;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Client-originated request asking the relay to deliver a push to other users.
|
|
14
|
+
// The relay never decrypts: payload is forwarded opaquely to registered
|
|
15
|
+
// subscriptions, so we only validate shape, target count, and payload size.
|
|
16
|
+
function validateClientPushSendMessage(message) {
|
|
17
|
+
if (!isObject(message)) return invalid("client/push-send", "message must be an object");
|
|
18
|
+
if (!protocolOne(message)) return invalid("client/push-send", "protocol is invalid");
|
|
19
|
+
if (!hasText(message.roomName)) return invalid("client/push-send", "roomName is required");
|
|
20
|
+
if (!Array.isArray(message.userIds) || message.userIds.length === 0) return invalid("client/push-send", "userIds is required");
|
|
21
|
+
if (message.userIds.length > MAX_PUSH_SEND_TARGETS) return invalid("client/push-send", "too many userIds");
|
|
22
|
+
if (message.userIds.some((id) => !hasText(id))) return invalid("client/push-send", "userIds must be non-empty strings");
|
|
23
|
+
if (!hasText(message.payload)) return invalid("client/push-send", "payload is required");
|
|
24
|
+
if (Buffer.byteLength(message.payload, "utf8") > MAX_PUSH_SEND_PAYLOAD_BYTES) return invalid("client/push-send", "payload is too large");
|
|
25
|
+
if (message.pushId !== undefined && !hasText(message.pushId)) return invalid("client/push-send", "pushId is invalid");
|
|
26
|
+
if (!hasValidTtlSeconds(message.ttlSeconds)) return invalid("client/push-send", "ttlSeconds is invalid");
|
|
27
|
+
if (message.urgency !== undefined && !PUSH_URGENCY_VALUES.has(message.urgency)) return invalid("client/push-send", "urgency is invalid");
|
|
28
|
+
return ok();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validateClientPushRegisterMessage(message) {
|
|
32
|
+
const result = safeValidate(validateClientPushRegister, message);
|
|
33
|
+
if (!result.ok) return protocolResult("client/push-register", result);
|
|
34
|
+
if (!protocolOne(message)) return invalid("client/push-register", "protocol is invalid");
|
|
35
|
+
return ok();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateClientPushGrantMessage(message) {
|
|
39
|
+
const result = safeValidate(validateClientPushGrant, message);
|
|
40
|
+
if (!result.ok) return protocolResult("client/push-grant", result);
|
|
41
|
+
if (!protocolOne(message)) return invalid("client/push-grant", "protocol is invalid");
|
|
42
|
+
return ok();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
validateClientPushRegisterMessage,
|
|
47
|
+
validateClientPushGrantMessage,
|
|
48
|
+
validateClientPushSendMessage
|
|
49
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const { validateClientMessage, validatePeerSignalFields } = require("./client.cjs");
|
|
2
|
+
const { validateHostMessage } = require("./host.cjs");
|
|
3
|
+
const { safeValidate, validateRoomOperation } = require("@mh-gg/protocol");
|
|
4
|
+
const { hasText, invalid, isObject, ok } = require("./shared.cjs");
|
|
5
|
+
|
|
6
|
+
const PUSH_URGENCY_VALUES = new Set(["very-low", "low", "normal", "high"]);
|
|
7
|
+
|
|
8
|
+
function hasValidTtlSeconds(value) {
|
|
9
|
+
if (value === undefined) return true;
|
|
10
|
+
return Number.isInteger(value) && value > 0 && value <= 24 * 60 * 60;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function validateRelayClientEnvelope(message) {
|
|
14
|
+
if (!hasText(message.id)) return invalid("relay.client", "id is required");
|
|
15
|
+
if (!hasText(message.roomName)) return invalid("relay.client", "roomName is required");
|
|
16
|
+
if (!hasText(message.peerId)) return invalid("relay.client", "peerId is required");
|
|
17
|
+
if (!isObject(message.message)) return invalid("relay.client", "message is required");
|
|
18
|
+
const nested = validateClientMessage(message.message);
|
|
19
|
+
if (!nested.ok) {
|
|
20
|
+
if (nested.message.startsWith("Invalid client: unknown client message type")) return invalid("relay.client", nested.message.replace(/^Invalid client: /, ""));
|
|
21
|
+
return invalid("relay.client", nested.message.replace(/^Invalid client\/?[^:]*: /, ""));
|
|
22
|
+
}
|
|
23
|
+
if (message.message.roomName !== message.roomName) return invalid("relay.client", "roomName is required");
|
|
24
|
+
return ok();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validateRelayHostEnvelope(message) {
|
|
28
|
+
if (!hasText(message.id)) return invalid("relay.host", "id is required");
|
|
29
|
+
if (!hasText(message.peerId)) return invalid("relay.host", "peerId is required");
|
|
30
|
+
if (message.roomName !== undefined && !hasText(message.roomName)) return invalid("relay.host", "roomName is invalid");
|
|
31
|
+
if (!isObject(message.message)) return invalid("relay.host", "message is required");
|
|
32
|
+
return validateHostMessage(message.message, "relay.host.message");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function validateRelayBroadcastEnvelope(message) {
|
|
36
|
+
if (!hasText(message.id)) return invalid("relay.broadcast", "id is required");
|
|
37
|
+
if (!hasText(message.roomName)) return invalid("relay.broadcast", "roomName is required");
|
|
38
|
+
if (!isObject(message.message)) return invalid("relay.broadcast", "message is required");
|
|
39
|
+
return validateHostMessage(message.message, "relay.broadcast.message");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validateRelayClientCloseEnvelope(message) {
|
|
43
|
+
if (!hasText(message.id) && message.id !== undefined) return invalid("relay.client.close", "id is invalid");
|
|
44
|
+
if (message.roomName !== undefined && !hasText(message.roomName)) return invalid("relay.client.close", "roomName is invalid");
|
|
45
|
+
if (!hasText(message.peerId)) return invalid("relay.client.close", "peerId is required");
|
|
46
|
+
return ok();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function validateRelayPeerSignalEnvelope(message) {
|
|
50
|
+
if (!hasText(message.id) && message.id !== undefined) return invalid("relay.peer-signal", "id is invalid");
|
|
51
|
+
if (message.type !== "relay.peer-signal") return invalid("relay.peer-signal", "type is invalid");
|
|
52
|
+
return validatePeerSignalFields(message, "relay.peer-signal", { relay: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function validateRelayHeartbeatEnvelope(message) {
|
|
56
|
+
if (!hasText(message.id)) return invalid(message.type, "id is required");
|
|
57
|
+
if (message.relayAddress !== undefined && !hasText(message.relayAddress)) return invalid(message.type, "relayAddress is invalid");
|
|
58
|
+
if (message.targetRelayAddress !== undefined && !hasText(message.targetRelayAddress)) return invalid(message.type, "targetRelayAddress is invalid");
|
|
59
|
+
if (message.requestId !== undefined && !hasText(message.requestId)) return invalid(message.type, "requestId is invalid");
|
|
60
|
+
if (message.sentAt !== undefined && (!Number.isFinite(message.sentAt) || message.sentAt < 0)) return invalid(message.type, "sentAt is invalid");
|
|
61
|
+
return ok();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateRelayMatterhornOperationEnvelope(message) {
|
|
65
|
+
if (!hasText(message.id)) return invalid("relay.matterhorn-operation", "id is required");
|
|
66
|
+
if (!hasText(message.roomName)) return invalid("relay.matterhorn-operation", "roomName is required");
|
|
67
|
+
if (!isObject(message.operation)) return invalid("relay.matterhorn-operation", "operation is required");
|
|
68
|
+
const operationValidation = safeValidate(validateRoomOperation, message.operation);
|
|
69
|
+
if (!operationValidation.ok) return invalid("relay.matterhorn-operation", "operation is invalid");
|
|
70
|
+
if (message.operation.roomId !== message.roomName) return invalid("relay.matterhorn-operation", "operation room does not match roomName");
|
|
71
|
+
if (message.operations !== undefined) {
|
|
72
|
+
if (!Array.isArray(message.operations)) return invalid("relay.matterhorn-operation", "operations must be an array");
|
|
73
|
+
for (const [index, operation] of message.operations.entries()) {
|
|
74
|
+
const proofValidation = safeValidate(validateRoomOperation, operation);
|
|
75
|
+
if (!proofValidation.ok) return invalid("relay.matterhorn-operation", `operations[${index}] is invalid`);
|
|
76
|
+
if (operation.roomId !== message.roomName) return invalid("relay.matterhorn-operation", `operations[${index}] room does not match roomName`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (message.state !== undefined && !isObject(message.state)) return invalid("relay.matterhorn-operation", "state must be an object");
|
|
80
|
+
if (isObject(message.state) && message.state.roomId !== undefined && message.state.roomId !== message.roomName) return invalid("relay.matterhorn-operation", "state room does not match roomName");
|
|
81
|
+
return ok();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validateRelayPushEnvelope(message) {
|
|
85
|
+
if (!hasText(message.id)) return invalid("relay.push", "id is required");
|
|
86
|
+
if (message.pushId !== undefined && !hasText(message.pushId)) return invalid("relay.push", "pushId is invalid");
|
|
87
|
+
if (!isObject(message.target)) return invalid("relay.push", "target is required");
|
|
88
|
+
if (!hasText(message.target.userId)) return invalid("relay.push", "target.userId is required");
|
|
89
|
+
if (message.payload === undefined) return invalid("relay.push", "payload is required");
|
|
90
|
+
let serialized;
|
|
91
|
+
try {
|
|
92
|
+
serialized = JSON.stringify(message.payload);
|
|
93
|
+
} catch {
|
|
94
|
+
return invalid("relay.push", "payload must be JSON serializable");
|
|
95
|
+
}
|
|
96
|
+
const maxBytes = 3 * 1024;
|
|
97
|
+
if (Buffer.byteLength(serialized, "utf8") > maxBytes) {
|
|
98
|
+
return invalid("relay.push", "payload is too large");
|
|
99
|
+
}
|
|
100
|
+
if (!hasValidTtlSeconds(message.ttlSeconds)) return invalid("relay.push", "ttlSeconds is invalid");
|
|
101
|
+
if (message.urgency !== undefined && !PUSH_URGENCY_VALUES.has(message.urgency)) return invalid("relay.push", "urgency is invalid");
|
|
102
|
+
return ok();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validateRelayPushAckEnvelope(message) {
|
|
106
|
+
if (!hasText(message.id)) return invalid("relay.push.ack", "id is required");
|
|
107
|
+
if (!hasText(message.pushId)) return invalid("relay.push.ack", "pushId is required");
|
|
108
|
+
if (message.status !== undefined && !["accepted", "delivered"].includes(message.status)) return invalid("relay.push.ack", "status is invalid");
|
|
109
|
+
return ok();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function validateRelayEnvelope(message) {
|
|
113
|
+
if (!isObject(message)) return invalid("relay", "message must be an object");
|
|
114
|
+
switch (message.type) {
|
|
115
|
+
case "relay.client": return validateRelayClientEnvelope(message);
|
|
116
|
+
case "relay.host": return validateRelayHostEnvelope(message);
|
|
117
|
+
case "relay.broadcast": return validateRelayBroadcastEnvelope(message);
|
|
118
|
+
case "relay.client.close": return validateRelayClientCloseEnvelope(message);
|
|
119
|
+
case "relay.ping": return validateRelayHeartbeatEnvelope(message);
|
|
120
|
+
case "relay.pong": return validateRelayHeartbeatEnvelope(message);
|
|
121
|
+
case "relay.matterhorn-operation": return validateRelayMatterhornOperationEnvelope(message);
|
|
122
|
+
case "relay.peer-signal": return validateRelayPeerSignalEnvelope(message);
|
|
123
|
+
case "relay.push": return validateRelayPushEnvelope(message);
|
|
124
|
+
case "relay.push.ack": return validateRelayPushAckEnvelope(message);
|
|
125
|
+
default: return invalid("relay", "unknown relay message type");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
validateRelayEnvelope
|
|
131
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const { PROTOCOL } = require("@mh-gg/host-config");
|
|
2
|
+
|
|
3
|
+
const INVALID_MESSAGE_CODE = "invalid-message";
|
|
4
|
+
|
|
5
|
+
function ok() {
|
|
6
|
+
return { ok: true };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function invalid(scope, reason) {
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
code: INVALID_MESSAGE_CODE,
|
|
13
|
+
message: `Invalid ${scope}: ${reason}`
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isObject(value) {
|
|
18
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasText(value) {
|
|
22
|
+
return typeof value === "string" && value.length > 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function protocolOne(value) {
|
|
26
|
+
return value.protocol === PROTOCOL;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function protocolResult(scope, result) {
|
|
30
|
+
if (result.ok) return ok();
|
|
31
|
+
let reason = result.message || "message is invalid";
|
|
32
|
+
const prefix = `${scope}.`;
|
|
33
|
+
if (typeof result.path === "string" && result.path.startsWith(prefix)) {
|
|
34
|
+
reason = `${result.path.slice(prefix.length)} ${reason.includes(":") ? reason.split(":").slice(1).join(":").trim() : "is invalid"}`;
|
|
35
|
+
} else if (reason.includes(":")) {
|
|
36
|
+
reason = reason.split(":").slice(1).join(":").trim();
|
|
37
|
+
}
|
|
38
|
+
return invalid(scope, reason || "message is invalid");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
INVALID_MESSAGE_CODE,
|
|
43
|
+
hasText,
|
|
44
|
+
invalid,
|
|
45
|
+
isObject,
|
|
46
|
+
ok,
|
|
47
|
+
protocolOne,
|
|
48
|
+
protocolResult
|
|
49
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { spawn } = require("node:child_process");
|
|
4
|
+
|
|
5
|
+
const REQUIRED_OUTPUTS = [
|
|
6
|
+
"dist/app.js",
|
|
7
|
+
"dist/app.d.ts",
|
|
8
|
+
"dist/browser/hashRoute.js",
|
|
9
|
+
"dist/browser/hashRoute.d.ts",
|
|
10
|
+
"dist/browser/roomLink.js",
|
|
11
|
+
"dist/browser/roomLink.d.ts",
|
|
12
|
+
"dist/browser/sfuCalls.js",
|
|
13
|
+
"dist/browser/sfuCalls.d.ts",
|
|
14
|
+
"dist/browser/emojiPicker.js",
|
|
15
|
+
"dist/browser/emojiPicker.d.ts"
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function defaultRepoRoot() {
|
|
19
|
+
return path.resolve(__dirname, "..");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function quoteCommandArg(value) {
|
|
23
|
+
const text = String(value);
|
|
24
|
+
if (/^[A-Za-z0-9_./:@=${}\\-]+$/.test(text)) return text;
|
|
25
|
+
return `"${text.replaceAll("\"", "\\\"")}"`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function pnpmCommand(args) {
|
|
29
|
+
if (process.platform !== "win32") return { command: "pnpm", args };
|
|
30
|
+
return {
|
|
31
|
+
command: process.env.ComSpec || "cmd.exe",
|
|
32
|
+
args: ["/d", "/s", "/c", ["pnpm.cmd", ...args].map(quoteCommandArg).join(" ")]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
function resolveFromRoots(request, roots) {
|
|
38
|
+
for (const root of roots) {
|
|
39
|
+
try {
|
|
40
|
+
return require.resolve(request, { paths: [root] });
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Unable to resolve ${request}. Run pnpm install once at the Matterhorn workspace root.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function runDefaultSdkBuild(root, sdkRoot, options) {
|
|
47
|
+
fs.rmSync(path.join(sdkRoot, "dist"), { recursive: true, force: true });
|
|
48
|
+
const tsc = resolveFromRoots("typescript/bin/tsc", [sdkRoot, root]);
|
|
49
|
+
await run(process.execPath, [tsc, "-p", "tsconfig.build.json"], {
|
|
50
|
+
cwd: sdkRoot,
|
|
51
|
+
label: options.label || "matterhorn sdk build",
|
|
52
|
+
logger: options.logger,
|
|
53
|
+
timeoutMs: options.timeoutMs || 120000
|
|
54
|
+
});
|
|
55
|
+
await run(process.execPath, [path.join(sdkRoot, "scripts", "fix-dist-imports.cjs")], {
|
|
56
|
+
cwd: sdkRoot,
|
|
57
|
+
label: options.label || "matterhorn sdk build",
|
|
58
|
+
logger: options.logger,
|
|
59
|
+
timeoutMs: options.timeoutMs || 120000
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function latestMtimeMs(target) {
|
|
64
|
+
if (!fs.existsSync(target)) return 0;
|
|
65
|
+
const stat = fs.statSync(target);
|
|
66
|
+
if (!stat.isDirectory()) return stat.mtimeMs;
|
|
67
|
+
let latest = stat.mtimeMs;
|
|
68
|
+
for (const entry of fs.readdirSync(target, { withFileTypes: true })) {
|
|
69
|
+
latest = Math.max(latest, latestMtimeMs(path.join(target, entry.name)));
|
|
70
|
+
}
|
|
71
|
+
return latest;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function oldestOutputMtimeMs(sdkRoot) {
|
|
75
|
+
let oldest = Infinity;
|
|
76
|
+
for (const relative of REQUIRED_OUTPUTS) {
|
|
77
|
+
const file = path.join(sdkRoot, relative);
|
|
78
|
+
if (!fs.existsSync(file)) return 0;
|
|
79
|
+
oldest = Math.min(oldest, fs.statSync(file).mtimeMs);
|
|
80
|
+
}
|
|
81
|
+
return oldest;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function newestInputMtimeMs(sdkRoot) {
|
|
85
|
+
return Math.max(
|
|
86
|
+
latestMtimeMs(path.join(sdkRoot, "src")),
|
|
87
|
+
latestMtimeMs(path.join(sdkRoot, "package.json")),
|
|
88
|
+
latestMtimeMs(path.join(sdkRoot, "tsconfig.build.json")),
|
|
89
|
+
latestMtimeMs(path.join(sdkRoot, "scripts"))
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sdkBuildRequired(sdkRoot) {
|
|
94
|
+
const oldestOutput = oldestOutputMtimeMs(sdkRoot);
|
|
95
|
+
if (oldestOutput === 0) return true;
|
|
96
|
+
return newestInputMtimeMs(sdkRoot) > oldestOutput;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function run(command, args, options) {
|
|
100
|
+
const child = spawn(command, args, {
|
|
101
|
+
cwd: options.cwd,
|
|
102
|
+
env: { ...process.env },
|
|
103
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
104
|
+
windowsHide: true
|
|
105
|
+
});
|
|
106
|
+
child.stdout.on("data", (chunk) => options.logger?.log?.(`${options.label} stdout: ${chunk.toString("utf8").trimEnd()}`));
|
|
107
|
+
child.stderr.on("data", (chunk) => options.logger?.error?.(`${options.label} stderr: ${chunk.toString("utf8").trimEnd()}`));
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const timer = setTimeout(() => {
|
|
110
|
+
child.kill();
|
|
111
|
+
reject(new Error(`${options.label} timed out after ${options.timeoutMs}ms`));
|
|
112
|
+
}, options.timeoutMs);
|
|
113
|
+
child.on("exit", (code) => {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
if (code === 0) resolve();
|
|
116
|
+
else reject(new Error(`${options.label} exited ${code}`));
|
|
117
|
+
});
|
|
118
|
+
child.on("error", (error) => {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
reject(error);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function ensureWorkspaceSdkBuild(options = {}) {
|
|
126
|
+
const root = options.repoRoot || defaultRepoRoot();
|
|
127
|
+
const sdkRoot = options.sdkRoot || path.join(root, "packages", "matterhorn-sdk");
|
|
128
|
+
if (!fs.existsSync(path.join(sdkRoot, "package.json"))) return false;
|
|
129
|
+
if (!options.force && !sdkBuildRequired(sdkRoot)) return false;
|
|
130
|
+
if (options.command) {
|
|
131
|
+
const spec = options.command;
|
|
132
|
+
await run(spec.command, spec.args, {
|
|
133
|
+
cwd: sdkRoot,
|
|
134
|
+
label: options.label || "matterhorn sdk build",
|
|
135
|
+
logger: options.logger,
|
|
136
|
+
timeoutMs: options.timeoutMs || 120000
|
|
137
|
+
});
|
|
138
|
+
} else {
|
|
139
|
+
await runDefaultSdkBuild(root, sdkRoot, options);
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
ensureWorkspaceSdkBuild,
|
|
146
|
+
REQUIRED_OUTPUTS,
|
|
147
|
+
sdkBuildRequired
|
|
148
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { spawn } = require("node:child_process");
|
|
2
|
+
|
|
3
|
+
function killChildTree(child) {
|
|
4
|
+
if (!child?.pid) return;
|
|
5
|
+
if (process.platform === "win32") {
|
|
6
|
+
child.kill();
|
|
7
|
+
const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
|
|
8
|
+
stdio: "ignore",
|
|
9
|
+
windowsHide: true
|
|
10
|
+
});
|
|
11
|
+
killer.unref();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
child.kill("SIGTERM");
|
|
15
|
+
setTimeout(() => child.kill("SIGKILL"), 1000).unref();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { killChildTree };
|