@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.
Files changed (71) hide show
  1. package/README.md +5 -0
  2. package/bin/matterhorn.cjs +57 -0
  3. package/package.json +49 -0
  4. package/runtime/bin/appFrontend/artifacts.cjs +25 -0
  5. package/runtime/bin/appFrontend/buildServers.cjs +176 -0
  6. package/runtime/bin/appFrontend/commandEnv.cjs +74 -0
  7. package/runtime/bin/appFrontend/commandPolicy.cjs +23 -0
  8. package/runtime/bin/appFrontend/devServers.cjs +150 -0
  9. package/runtime/bin/appFrontend/httpServers.cjs +221 -0
  10. package/runtime/bin/appFrontend/paths.cjs +103 -0
  11. package/runtime/bin/appFrontend/ports.cjs +36 -0
  12. package/runtime/bin/appFrontend/processes.cjs +127 -0
  13. package/runtime/bin/appFrontend.cjs +45 -0
  14. package/runtime/bin/appHostCommand.cjs +381 -0
  15. package/runtime/bin/matterhorn.cjs +501 -0
  16. package/runtime/bin/matterhornAppLoader.cjs +588 -0
  17. package/runtime/bin/matterhornApps.cjs +223 -0
  18. package/runtime/bin/matterhornDeploy.cjs +108 -0
  19. package/runtime/bin/matterhornEmitAppBundle.cjs +20 -0
  20. package/runtime/bin/matterhornInstall.cjs +609 -0
  21. package/runtime/host/callAuth.cjs +76 -0
  22. package/runtime/host/host.cjs +103 -0
  23. package/runtime/host/hostAnnouncement.cjs +70 -0
  24. package/runtime/host/hostClients/constants.cjs +7 -0
  25. package/runtime/host/hostClients/frontendBundleRefresh.cjs +158 -0
  26. package/runtime/host/hostClients/frontendRequests.cjs +166 -0
  27. package/runtime/host/hostClients/index.cjs +68 -0
  28. package/runtime/host/hostClients/rejections.cjs +37 -0
  29. package/runtime/host/hostSession.cjs +160 -0
  30. package/runtime/host/inlineProgressBar.cjs +128 -0
  31. package/runtime/host/localPeerServer.cjs +114 -0
  32. package/runtime/host/localRelayClient.cjs +151 -0
  33. package/runtime/host/matterhornrc.cjs +75 -0
  34. package/runtime/host/memberRootRegistry.cjs +132 -0
  35. package/runtime/host/nodePeer.cjs +127 -0
  36. package/runtime/host/nodePeerRacePatch.cjs +106 -0
  37. package/runtime/host/peerJsConfig.cjs +26 -0
  38. package/runtime/host/pushEgress.cjs +48 -0
  39. package/runtime/host/pushStorage.cjs +233 -0
  40. package/runtime/host/relay/config.cjs +179 -0
  41. package/runtime/host/relay/connectionCleanup.cjs +34 -0
  42. package/runtime/host/relay/connectionDispatcher.cjs +140 -0
  43. package/runtime/host/relay/matterhornOperationEvents.cjs +100 -0
  44. package/runtime/host/relay/matterhornRuntimeEventBridge.cjs +182 -0
  45. package/runtime/host/relay/nostrRelay.cjs +30 -0
  46. package/runtime/host/relay/peerStartup.cjs +81 -0
  47. package/runtime/host/relay.cjs +653 -0
  48. package/runtime/host/relayClientRouting.cjs +1054 -0
  49. package/runtime/host/relayConfig.cjs +156 -0
  50. package/runtime/host/relayHostAuth.cjs +39 -0
  51. package/runtime/host/relayHostMessages.cjs +367 -0
  52. package/runtime/host/relayHttp.cjs +48 -0
  53. package/runtime/host/relayIdentity.cjs +496 -0
  54. package/runtime/host/relayIncomingGate.cjs +153 -0
  55. package/runtime/host/relayMeshEnvelopes.cjs +522 -0
  56. package/runtime/host/relayPeerLifecycle.cjs +96 -0
  57. package/runtime/host/relayPeerSignals.cjs +175 -0
  58. package/runtime/host/relayRoomRuntimePersistence.cjs +129 -0
  59. package/runtime/host/relayStatus.cjs +160 -0
  60. package/runtime/host/sfuRelay.cjs +553 -0
  61. package/runtime/host/sqliteRelayStorage.cjs +352 -0
  62. package/runtime/host/wireValidation/client.cjs +213 -0
  63. package/runtime/host/wireValidation/host.cjs +33 -0
  64. package/runtime/host/wireValidation/index.cjs +13 -0
  65. package/runtime/host/wireValidation/peerSignal.cjs +35 -0
  66. package/runtime/host/wireValidation/presenceEvent.cjs +49 -0
  67. package/runtime/host/wireValidation/push.cjs +49 -0
  68. package/runtime/host/wireValidation/relay.cjs +131 -0
  69. package/runtime/host/wireValidation/shared.cjs +49 -0
  70. package/runtime/scripts/ensureWorkspaceSdkBuild.cjs +148 -0
  71. package/runtime/scripts/killChildTree.cjs +18 -0
@@ -0,0 +1,179 @@
1
+ const { createNodePeer } = require("../nodePeer.cjs");
2
+ const { userDataDir } = require("../matterhornrc.cjs");
3
+ const { peerJsSignalingConfig } = require("../peerJsConfig.cjs");
4
+ const { DEFAULT_RELAY_IPC_HOST, DEFAULT_RELAY_IPC_PORT, ensureRelayConfig, rotateRelayConfigPeerId } = require("../relayConfig.cjs");
5
+ const { createRelayTrustStore, signRelayClaim } = require("../relayIdentity.cjs");
6
+ const {
7
+ DEFAULT_RELAY_ACTIVE_FANOUT,
8
+ DEFAULT_RELAY_MAX_CALL_SIGNAL_BYTES,
9
+ DEFAULT_RELAY_MAX_BYTES_PER_ROOM,
10
+ DEFAULT_RELAY_MAX_EVENT_BYTES,
11
+ DEFAULT_RELAY_MAX_EVENTS,
12
+ DEFAULT_RELAY_MAX_EVENTS_PER_PUBKEY_WINDOW,
13
+ DEFAULT_RELAY_MAX_EVENTS_PER_ROOM,
14
+ DEFAULT_RELAY_MAX_FUTURE_SECONDS,
15
+ DEFAULT_RELAY_MAX_HOST_IPC_MESSAGE_BYTES,
16
+ DEFAULT_RELAY_MAX_HOST_IPC_MESSAGES_PER_WINDOW,
17
+ DEFAULT_RELAY_HEARTBEAT_MS,
18
+ DEFAULT_RELAY_HEARTBEAT_TIMEOUT_MS,
19
+ DEFAULT_RELAY_FAILOVER_MS,
20
+ DEFAULT_RELAY_MAX_MESH_MESSAGE_BYTES,
21
+ DEFAULT_RELAY_MAX_MESH_MESSAGES_PER_WINDOW,
22
+ DEFAULT_RELAY_MAX_NOSTR_MESSAGE_BYTES,
23
+ DEFAULT_RELAY_MAX_ROOM_MESSAGE_BYTES,
24
+ DEFAULT_RELAY_MAX_ROOM_MESSAGES_PER_WINDOW,
25
+ DEFAULT_RELAY_MAX_UNAUTHENTICATED_MESSAGES_PER_WINDOW,
26
+ DEFAULT_RELAY_PUBKEY_QUOTA_WINDOW_SECONDS,
27
+ DEFAULT_RELAY_RATE_WINDOW_MS,
28
+ DEFAULT_RELAY_SFU_MAX_PARTICIPANTS,
29
+ defaultRelayStoragePath,
30
+ parseRelayArgs,
31
+ parseIceServers,
32
+ resolveStoragePath
33
+ } = require("@mh-gg/host-config");
34
+ const { relayAddressForPeerId, relayMeshAddressForPeerId, relayMeshPeerIdForRoomPeerId } = require("@mh-gg/relay-core");
35
+
36
+ function defaultStoragePath() {
37
+ return defaultRelayStoragePath(userDataDir());
38
+ }
39
+
40
+ function parseArgs(argv) {
41
+ return parseRelayArgs(argv, {
42
+ defaultIpcHost: DEFAULT_RELAY_IPC_HOST,
43
+ defaultIpcPort: DEFAULT_RELAY_IPC_PORT,
44
+ defaultStoragePath: defaultStoragePath()
45
+ });
46
+ }
47
+
48
+ function positiveInteger(value, fallback) {
49
+ return Number.isInteger(value) && value > 0 ? value : fallback;
50
+ }
51
+
52
+ function nonNegativeInteger(value, fallback) {
53
+ return Number.isInteger(value) && value >= 0 ? value : fallback;
54
+ }
55
+
56
+ function createRelayRuntimeConfig(options = {}) {
57
+ const peerJsSignaling = options.peerJsSignaling || peerJsSignalingConfig();
58
+ const dataDir = options.dataDir || userDataDir();
59
+ const relayConfig = ensureRelayConfig({
60
+ dataDir,
61
+ ipcHost: options.ipcHost || options.host,
62
+ ipcPort: options.ipcPort ?? options.port,
63
+ relayPeerId: options.relayPeerId,
64
+ relayName: options.relayName,
65
+ peerJsSignaling
66
+ });
67
+ const relayTrustStore = createRelayTrustStore({ dataDir, defaultSignaling: peerJsSignaling });
68
+ const relayPeerId = options.relayPeerId || relayConfig.relayPeerId;
69
+ const relayMeshPeerId = relayMeshPeerIdForRoomPeerId(relayPeerId);
70
+ const sfuPeerId = options.sfuPeerId || `${relayPeerId}-sfu`;
71
+ const resolvedRelayRefs = [];
72
+ for (const ref of options.relayRefs || []) {
73
+ const resolved = relayTrustStore.resolveReference(ref);
74
+ if (!resolved.ok) throw new Error(resolved.message);
75
+ resolvedRelayRefs.push(resolved.relayAddress);
76
+ }
77
+ const runtimeConfig = {
78
+ ipcHost: options.ipcHost || options.host || relayConfig.ipcHost,
79
+ ipcPort: Number.isInteger(options.ipcPort) ? options.ipcPort : Number.isInteger(options.port) ? options.port : relayConfig.ipcPort,
80
+ ipcSecret: relayConfig.ipcSecret,
81
+ storagePath: options.storagePath || defaultStoragePath(),
82
+ peerJsSignaling,
83
+ relayPeerId,
84
+ relayMeshPeerId,
85
+ relayAddress: relayMeshAddressForPeerId(relayPeerId, peerJsSignaling),
86
+ relayIdentity: relayConfig.relayIdentity,
87
+ relayTrustStore,
88
+ sfuEnabled: Boolean(options.sfuEnabled),
89
+ sfuPeerId,
90
+ sfuPeerAddress: relayAddressForPeerId(sfuPeerId, peerJsSignaling),
91
+ sfuMaxParticipants: positiveInteger(options.sfuMaxParticipants, DEFAULT_RELAY_SFU_MAX_PARTICIPANTS),
92
+ relayPeers: [...(options.relayPeers || []), ...resolvedRelayRefs],
93
+ activeRelayFanout: positiveInteger(options.activeRelayFanout, DEFAULT_RELAY_ACTIVE_FANOUT),
94
+ relayHeartbeatMs: positiveInteger(options.relayHeartbeatMs, DEFAULT_RELAY_HEARTBEAT_MS),
95
+ relayHeartbeatTimeoutMs: positiveInteger(options.relayHeartbeatTimeoutMs, DEFAULT_RELAY_HEARTBEAT_TIMEOUT_MS),
96
+ relayFailoverMs: positiveInteger(options.relayFailoverMs, DEFAULT_RELAY_FAILOVER_MS),
97
+ maxEvents: options.maxEvents || DEFAULT_RELAY_MAX_EVENTS,
98
+ maxEventsPerRoom: options.maxEventsPerRoom || DEFAULT_RELAY_MAX_EVENTS_PER_ROOM,
99
+ maxEventsPerStream: Number.isInteger(options.maxEventsPerStream) && options.maxEventsPerStream > 0 ? options.maxEventsPerStream : 10000,
100
+ maxBytesPerRoom: options.maxBytesPerRoom || DEFAULT_RELAY_MAX_BYTES_PER_ROOM,
101
+ maxEventsPerPubkeyWindow: options.maxEventsPerPubkeyWindow || DEFAULT_RELAY_MAX_EVENTS_PER_PUBKEY_WINDOW,
102
+ pubkeyQuotaWindowSeconds: options.pubkeyQuotaWindowSeconds || DEFAULT_RELAY_PUBKEY_QUOTA_WINDOW_SECONDS,
103
+ maxEventBytes: positiveInteger(options.maxEventBytes, DEFAULT_RELAY_MAX_EVENT_BYTES),
104
+ maxFutureSeconds: nonNegativeInteger(options.maxFutureSeconds, DEFAULT_RELAY_MAX_FUTURE_SECONDS),
105
+ maxHostIpcMessageBytes: positiveInteger(options.maxHostIpcMessageBytes, DEFAULT_RELAY_MAX_HOST_IPC_MESSAGE_BYTES),
106
+ maxRoomMessageBytes: positiveInteger(options.maxRoomMessageBytes, DEFAULT_RELAY_MAX_ROOM_MESSAGE_BYTES),
107
+ maxMeshMessageBytes: positiveInteger(options.maxMeshMessageBytes, DEFAULT_RELAY_MAX_MESH_MESSAGE_BYTES),
108
+ maxNostrMessageBytes: positiveInteger(options.maxNostrMessageBytes, DEFAULT_RELAY_MAX_NOSTR_MESSAGE_BYTES),
109
+ maxCallSignalBytes: positiveInteger(options.maxCallSignalBytes, DEFAULT_RELAY_MAX_CALL_SIGNAL_BYTES),
110
+ rateWindowMs: positiveInteger(options.rateWindowMs, DEFAULT_RELAY_RATE_WINDOW_MS),
111
+ maxUnauthenticatedMessagesPerWindow: positiveInteger(options.maxUnauthenticatedMessagesPerWindow, DEFAULT_RELAY_MAX_UNAUTHENTICATED_MESSAGES_PER_WINDOW),
112
+ maxRoomMessagesPerWindow: positiveInteger(options.maxRoomMessagesPerWindow, DEFAULT_RELAY_MAX_ROOM_MESSAGES_PER_WINDOW),
113
+ maxMeshMessagesPerWindow: positiveInteger(options.maxMeshMessagesPerWindow, DEFAULT_RELAY_MAX_MESH_MESSAGES_PER_WINDOW),
114
+ maxHostIpcMessagesPerWindow: positiveInteger(options.maxHostIpcMessagesPerWindow, DEFAULT_RELAY_MAX_HOST_IPC_MESSAGES_PER_WINDOW),
115
+ iceServers: options.iceServers || [{ urls: "stun:stun.l.google.com:19302" }],
116
+ startPeer: options.startPeer !== false,
117
+ createPeer: options.createPeer || createNodePeer
118
+ };
119
+ runtimeConfig.relayClaim = signRelayClaim(runtimeConfig.relayIdentity, {
120
+ relayAddress: runtimeConfig.relayAddress,
121
+ roomPeerId: runtimeConfig.relayPeerId,
122
+ relayMeshPeerId: runtimeConfig.relayMeshPeerId
123
+ });
124
+ runtimeConfig.rotateRelayTransportIdentity = (reason) => {
125
+ const previousRelayAddress = runtimeConfig.relayAddress;
126
+ const rotated = rotateRelayConfigPeerId(runtimeConfig, { dataDir, peerJsSignaling });
127
+ runtimeConfig.relayPeerId = rotated.relayPeerId;
128
+ runtimeConfig.relayMeshPeerId = rotated.relayMeshPeerId;
129
+ runtimeConfig.relayAddress = rotated.relayAddress;
130
+ runtimeConfig.relayIdentity = rotated.relayIdentity;
131
+ runtimeConfig.sfuPeerId = options.sfuPeerId || `${rotated.relayPeerId}-sfu`;
132
+ runtimeConfig.sfuPeerAddress = relayAddressForPeerId(runtimeConfig.sfuPeerId, peerJsSignaling);
133
+ runtimeConfig.relayClaim = signRelayClaim(runtimeConfig.relayIdentity, {
134
+ relayAddress: runtimeConfig.relayAddress,
135
+ roomPeerId: runtimeConfig.relayPeerId,
136
+ relayMeshPeerId: runtimeConfig.relayMeshPeerId,
137
+ previousRelayAddress
138
+ });
139
+ if (reason) runtimeConfig.lastRelayTransportRotationReason = reason?.message || String(reason);
140
+ return runtimeConfig;
141
+ };
142
+ return runtimeConfig;
143
+ }
144
+
145
+ function createRelayOptionsFromArgs(args) {
146
+ return {
147
+ ipcHost: args.ipcHost,
148
+ ipcPort: args.ipcPort,
149
+ storagePath: args.storagePath,
150
+ relayPeerId: args.relayPeerId,
151
+ relayName: args.relayName,
152
+ relayPeers: args.relayPeers,
153
+ relayRefs: args.relayRefs,
154
+ activeRelayFanout: args.activeRelayFanout,
155
+ relayHeartbeatMs: args.relayHeartbeatMs,
156
+ relayHeartbeatTimeoutMs: args.relayHeartbeatTimeoutMs,
157
+ relayFailoverMs: args.relayFailoverMs,
158
+ maxEvents: args.maxEvents,
159
+ maxEventsPerRoom: args.maxEventsPerRoom,
160
+ maxBytesPerRoom: args.maxBytesPerRoom,
161
+ maxEventsPerPubkeyWindow: args.maxEventsPerPubkeyWindow,
162
+ pubkeyQuotaWindowSeconds: args.pubkeyQuotaWindowSeconds,
163
+ maxEventBytes: args.maxEventBytes,
164
+ maxFutureSeconds: args.maxFutureSeconds,
165
+ sfuEnabled: args.sfuEnabled,
166
+ sfuPeerId: args.sfuPeerId,
167
+ sfuMaxParticipants: args.sfuMaxParticipants,
168
+ iceServers: parseIceServers(args.iceServers),
169
+ localPeerjs: args.localPeerjs
170
+ };
171
+ }
172
+
173
+ module.exports = {
174
+ createRelayOptionsFromArgs,
175
+ createRelayRuntimeConfig,
176
+ defaultStoragePath,
177
+ parseArgs,
178
+ resolveStoragePath
179
+ };
@@ -0,0 +1,34 @@
1
+ const { sendMessage } = require("@mh-gg/relay-core");
2
+
3
+ function createConnectionCleanup({
4
+ clientConnections,
5
+ clientRooms,
6
+ hostConnections,
7
+ incomingGate,
8
+ mesh,
9
+ relayClientRouting
10
+ }) {
11
+ return function cleanupConnection(conn) {
12
+ if (conn.MatterhornCleaned) return;
13
+ conn.MatterhornCleaned = true;
14
+ incomingGate.cleanup(conn);
15
+ const peerId = conn.MatterhornPeerId;
16
+ if (peerId) {
17
+ clientConnections.delete(peerId);
18
+ }
19
+ mesh.cleanupConnection(conn);
20
+ const roomName = clientRooms.get(conn);
21
+ relayClientRouting?.dropClientEphemeralTokens?.(conn);
22
+ clientRooms.delete(conn);
23
+ if (roomName && peerId && conn.MatterhornNotifyHostOnClose) {
24
+ const host = hostConnections.get(roomName);
25
+ if (host) sendMessage(host, { type: "relay/client-close", peerId });
26
+ else mesh.sendEnvelope({ type: "relay.client.close", roomName, peerId }, conn);
27
+ }
28
+ if (conn.hostRoomName && hostConnections.get(conn.hostRoomName) === conn) {
29
+ hostConnections.delete(conn.hostRoomName);
30
+ }
31
+ };
32
+ }
33
+
34
+ module.exports = { createConnectionCleanup };
@@ -0,0 +1,140 @@
1
+ const { verifyRelayControl } = require("../relayIdentity.cjs");
2
+ const { sendMessage } = require("@mh-gg/relay-core");
3
+
4
+ function createConnectionDataHandler({
5
+ hostAuth,
6
+ isNostrMessage,
7
+ isRelayEnvelopeMessage,
8
+ mesh,
9
+ promoteRelayConnection,
10
+ relayClientRouting,
11
+ relayHostMessages,
12
+ relayMeshEnvelopes,
13
+ relayStatus,
14
+ relayTrust,
15
+ validateRelayHints
16
+ }) {
17
+ const relayControlNonces = new Set();
18
+
19
+ function rememberRelayClaim(claim, options = {}) {
20
+ if (!claim || typeof relayTrust?.rememberClaim !== "function") return { ok: true };
21
+ const result = relayTrust.rememberClaim(claim, options);
22
+ if (result.ok && result.claim?.relayAddress) mesh.learnRelayAddress(result.claim.relayAddress);
23
+ return result;
24
+ }
25
+
26
+ function rememberRelayClaims(message) {
27
+ if (Array.isArray(message.relayClaims)) {
28
+ for (const claim of message.relayClaims) rememberRelayClaim(claim);
29
+ }
30
+ }
31
+
32
+ function rejectRelayClaim(conn, result) {
33
+ sendMessage(conn, {
34
+ type: "relay.error",
35
+ code: result.code || "invalid-relay-claim",
36
+ message: result.message || "Relay claim is invalid."
37
+ });
38
+ conn.close?.();
39
+ }
40
+
41
+ return function handleConnectionData(conn, message) {
42
+ if (isNostrMessage(message)) {
43
+ if (!mesh.hasRelayConnection(conn) && mesh.connectionHasRelayMetadata(conn) && conn.MatterhornRelayAddress) promoteRelayConnection(conn);
44
+ return;
45
+ }
46
+ if (!message || typeof message !== "object") return;
47
+ if (isRelayEnvelopeMessage(message)) {
48
+ relayMeshEnvelopes.handleRelayEnvelope(conn, message);
49
+ return;
50
+ }
51
+ if (message.type === "relay.status.ok") {
52
+ if (message.relayAddress && !message.relayClaim && relayTrust?.pinForAddress?.(message.relayAddress)) {
53
+ rejectRelayClaim(conn, { code: "relay-claim-required", message: "Pinned relay address did not present a signed relay claim." });
54
+ return;
55
+ }
56
+ const claimResult = rememberRelayClaim(message.relayClaim, { expectedRelayAddress: message.relayAddress });
57
+ if (!claimResult.ok) {
58
+ rejectRelayClaim(conn, claimResult);
59
+ return;
60
+ }
61
+ rememberRelayClaims(message);
62
+ if (message.load) {
63
+ conn.MatterhornRelayLoad = message.load;
64
+ }
65
+ if (message.relayAddress && mesh.bindConnectionAddress(conn, message.relayAddress)) promoteRelayConnection(conn);
66
+ mesh.recordRelayLiveness?.(conn, message.relayAddress);
67
+ if (!mesh.hasRelayConnection(conn)) mesh.learnRelayHints(message.relayHints);
68
+ return;
69
+ }
70
+ if (message.type === "relay.add") {
71
+ if (!hostAuth.requireHostIpcAuth(conn, message)) return;
72
+ let relayAddress = message.relayAddress;
73
+ if (message.relayRef && typeof relayTrust?.resolveReference === "function") {
74
+ const resolved = relayTrust.resolveReference(message.relayRef);
75
+ if (!resolved.ok) {
76
+ sendMessage(conn, { type: "relay.add.error", requestId: message.requestId, code: resolved.code, message: resolved.message });
77
+ return;
78
+ }
79
+ relayAddress = resolved.relayAddress;
80
+ if (resolved.relayClaim) rememberRelayClaim(resolved.relayClaim);
81
+ }
82
+ if (message.relayClaim) {
83
+ const claimResult = rememberRelayClaim(message.relayClaim, { expectedRelayAddress: relayAddress });
84
+ if (!claimResult.ok) {
85
+ sendMessage(conn, { type: "relay.add.error", requestId: message.requestId, code: claimResult.code, message: claimResult.message });
86
+ return;
87
+ }
88
+ relayAddress = claimResult.claim.relayAddress;
89
+ }
90
+ if (!relayAddress) {
91
+ sendMessage(conn, { type: "relay.add.error", requestId: message.requestId, code: "missing-relay-reference", message: "Relay address or reference is required." });
92
+ return;
93
+ }
94
+ mesh.learnRelayAddress(relayAddress);
95
+ sendMessage(conn, { type: "relay.add.ok", requestId: message.requestId, relayAddress: mesh.normalizeRelayAddress(relayAddress) });
96
+ return;
97
+ }
98
+ if (message.type === "relay.status") {
99
+ if (!hostAuth.requireHostIpcAuth(conn, message)) return;
100
+ sendMessage(conn, relayStatus.relayStatusMessage(message.requestId));
101
+ return;
102
+ }
103
+ if (message.type === "relay.disconnecting") {
104
+ if (!mesh.hasRelayConnection(conn) && !mesh.connectionHasRelayMetadata(conn)) return;
105
+ const expectedRelayAddress = conn.MatterhornRelayAddress || message.relayAddress;
106
+ const verification = verifyRelayControl(message.relayControl, message.relayClaim, { expectedRelayAddress });
107
+ if (!verification.ok) return;
108
+ const replayKey = `${verification.control.relayAddress}:${verification.control.nonce}`;
109
+ if (relayControlNonces.has(replayKey)) return;
110
+ relayControlNonces.add(replayKey);
111
+ const claimResult = rememberRelayClaim(message.relayClaim, { expectedRelayAddress: verification.control.relayAddress });
112
+ if (!claimResult.ok) return;
113
+ mesh.iceAddress(verification.control.relayAddress);
114
+ relayClientRouting.broadcastToRoom(message.roomName, relayClientRouting.relayHintMessage(message.roomName));
115
+ return;
116
+ }
117
+ if (typeof message.type === "string" && message.type.startsWith("relay.") && mesh.hasRelayConnection(conn)) {
118
+ relayMeshEnvelopes.handleRelayEnvelope(conn, message);
119
+ return;
120
+ }
121
+ if (message.type === "relay.hints") {
122
+ const validation = validateRelayHints(message);
123
+ if (!validation.ok) return;
124
+ mesh.recordRelayLiveness?.(conn);
125
+ mesh.learnRelayHints(message.relayHints);
126
+ rememberRelayClaim(message.relayClaim);
127
+ rememberRelayClaims(message);
128
+ if (typeof message.roomName === "string") relayClientRouting.broadcastToRoom(message.roomName, relayClientRouting.relayHintMessage(message.roomName));
129
+ return;
130
+ }
131
+ if (typeof message.type === "string" && message.type.startsWith("host.")) {
132
+ if (!hostAuth.requireHostIpcAuth(conn, message)) return;
133
+ relayHostMessages.handleHostMessage(conn, message);
134
+ return;
135
+ }
136
+ relayClientRouting.routeClientMessage(conn, message);
137
+ };
138
+ }
139
+
140
+ module.exports = { createConnectionDataHandler };
@@ -0,0 +1,100 @@
1
+ const {
2
+ decryptPartyPayload,
3
+ isEncryptedPayload,
4
+ nostrEventToPartyEvent,
5
+ MATTERHORN_OPERATION_NOSTR_KIND
6
+ } = require("@mh-gg/event");
7
+ const { safeValidate, validateRoomOperation } = require("@mh-gg/protocol");
8
+
9
+ function tagValue(tags, name) {
10
+ const tag = Array.isArray(tags) ? tags.find((item) => Array.isArray(item) && item[0] === name) : undefined;
11
+ return tag ? tag[1] : undefined;
12
+ }
13
+
14
+ function roomNameFromOperationEvent(nostrEvent) {
15
+ if (nostrEvent?.kind !== MATTERHORN_OPERATION_NOSTR_KIND) return undefined;
16
+ return tagValue(nostrEvent.tags, "d");
17
+ }
18
+
19
+ function parseOperationPayload(event, roomSecret) {
20
+ if (!isEncryptedPayload(event.payload)) return event.payload;
21
+ if (!roomSecret) return undefined;
22
+ return decryptPartyPayload({
23
+ roomSecret,
24
+ partyId: event.partyId,
25
+ kind: event.kind,
26
+ payload: event.payload,
27
+ header: event.header
28
+ });
29
+ }
30
+
31
+ function parseMatterhornOperationEvent(nostrEvent, options = {}) {
32
+ let event;
33
+ try {
34
+ event = nostrEventToPartyEvent(nostrEvent);
35
+ } catch {
36
+ return { ok: false, reason: "invalid-matterhorn-event" };
37
+ }
38
+ if (event.kind !== "room.operation") return { ok: false, reason: "not-room-operation" };
39
+
40
+ const header = event.header;
41
+ const isV2 = header && header.scheme === "matterhorn.operation.v2";
42
+
43
+ // v2 events carry a plaintext header in Nostr tags. Relays MUST NOT need a room secret
44
+ // to validate and store them. Decryption is a client-only concern.
45
+ if (isV2) {
46
+ return {
47
+ ok: true,
48
+ roomName: event.partyId,
49
+ header,
50
+ encryptedPayload: event.payload,
51
+ // Decrypted application payload only available when a roomSecret is explicitly supplied.
52
+ payload: options.roomSecret ? parseOperationPayload(event, options.roomSecret) : undefined
53
+ };
54
+ }
55
+
56
+ // Legacy plaintext/encrypted path (kept for transitional tooling). This path is only
57
+ // usable when the caller can supply a roomSecret for encrypted legacy payloads.
58
+ let payload;
59
+ try {
60
+ payload = parseOperationPayload(event, options.roomSecret);
61
+ } catch {
62
+ return { ok: false, reason: "operation-decrypt-failed" };
63
+ }
64
+
65
+ const operation = payload?.operation;
66
+ const validation = safeValidate(validateRoomOperation, operation);
67
+ if (!validation.ok) return { ok: false, reason: validation.message || "invalid-operation" };
68
+ if (operation.roomId !== event.partyId) return { ok: false, reason: "operation-room-mismatch" };
69
+ if (!isEncryptedPayload(event.payload) && operation.auth?.publicKey && nostrEvent?.pubkey && operation.auth.publicKey !== nostrEvent.pubkey) {
70
+ return { ok: false, reason: "operation-event-signer-mismatch" };
71
+ }
72
+ return { ok: true, roomName: event.partyId, operation };
73
+ }
74
+
75
+ function operationSortValue(operation) {
76
+ const createdAt = Number(operation?.createdAt || 0);
77
+ return Number.isFinite(createdAt) ? createdAt : 0;
78
+ }
79
+
80
+ function compareMatterhornOperations(left, right) {
81
+ const byCreatedAt = operationSortValue(left.operation) - operationSortValue(right.operation);
82
+ if (byCreatedAt !== 0) return byCreatedAt;
83
+ return String(left.operation.id).localeCompare(String(right.operation.id));
84
+ }
85
+
86
+ function parseMatterhornOperationEvents(events, options = {}) {
87
+ const parsed = [];
88
+ for (const event of events) {
89
+ const result = parseMatterhornOperationEvent(event, options);
90
+ if (result.ok) parsed.push(result);
91
+ }
92
+ return parsed.sort(compareMatterhornOperations);
93
+ }
94
+
95
+ module.exports = {
96
+ MATTERHORN_OPERATION_NOSTR_KIND,
97
+ parseMatterhornOperationEvent,
98
+ parseMatterhornOperationEvents,
99
+ roomNameFromOperationEvent
100
+ };
@@ -0,0 +1,182 @@
1
+ const { PROTOCOL } = require("@mh-gg/host-config");
2
+ const { generatePartyIdentity, MATTERHORN_INDEX_NGRAM_NOSTR_KIND } = require("@mh-gg/event");
3
+ const {
4
+ MATTERHORN_OPERATION_NOSTR_KIND,
5
+ parseMatterhornOperationEvent,
6
+ parseMatterhornOperationEvents,
7
+ roomNameFromOperationEvent
8
+ } = require("./matterhornOperationEvents.cjs");
9
+
10
+ function createMatterhornRuntimeEventBridge(options) {
11
+ const operationEventIdentity = options.operationEventIdentity || generatePartyIdentity(options.startedAt);
12
+ const runtimeEventOrigin = {};
13
+ const debug = options.debug || (() => {});
14
+ const encryptedRoomRuntimes = options.encryptedRoomRuntimes;
15
+
16
+ function relay() {
17
+ return options.relay();
18
+ }
19
+
20
+ function relayPluginRuntimes() {
21
+ return options.relayPluginRuntimes();
22
+ }
23
+
24
+ function relayClientRouting() {
25
+ return options.relayClientRouting();
26
+ }
27
+
28
+ function tagValue(tags, name) {
29
+ if (!Array.isArray(tags)) return undefined;
30
+ const tag = tags.find((item) => Array.isArray(item) && item[0] === name && typeof item[1] === "string");
31
+ return tag?.[1];
32
+ }
33
+
34
+ function roomNameFromControlPlaneEvent(event) {
35
+ return tagValue(event?.tags, "d") || tagValue(event?.tags, "room");
36
+ }
37
+
38
+ function handleRoomIndexNgramEvent(event, context = {}) {
39
+ if (context.originConn === runtimeEventOrigin) return;
40
+ if (event?.kind !== MATTERHORN_INDEX_NGRAM_NOSTR_KIND) return;
41
+ const roomName = roomNameFromControlPlaneEvent(event);
42
+ if (!roomName) return;
43
+ encryptedRoomRuntimes?.insertEvent?.(roomName, event);
44
+ }
45
+
46
+ // publishMatterhornOperation is a LEGACY path for plaintext relay-as-host rooms.
47
+ // Encrypted v2 rooms send pre-encrypted events directly; this function is not used for them.
48
+ // The bridge itself never holds or accesses room secrets.
49
+ function publishMatterhornOperation(roomName, operation) {
50
+ let event;
51
+ try {
52
+ // For legacy plaintext operations, wrap them in the simple envelope the relay can sign.
53
+ // This intentionally does not encrypt; durable encryption for encrypted rooms is done client-side.
54
+ event = {
55
+ kind: MATTERHORN_OPERATION_NOSTR_KIND,
56
+ created_at: Math.floor(operation.createdAt / 1000),
57
+ tags: [
58
+ ["protocol", PROTOCOL],
59
+ ["version", "1"],
60
+ ["d", roomName],
61
+ ["created-at-ms", String(operation.createdAt)]
62
+ ],
63
+ content: JSON.stringify({ payload: { operation } }),
64
+ pubkey: operationEventIdentity.pubkey
65
+ };
66
+ } catch (error) {
67
+ debug(`matterhorn operation event creation failed for ${roomName}: ${error?.message || String(error)}`);
68
+ return;
69
+ }
70
+ const result = relay().publish(event, runtimeEventOrigin);
71
+ if (!result.ok) debug(`matterhorn operation event publish failed for ${roomName}: ${result.message || "unknown error"}`);
72
+ }
73
+
74
+ async function applyMatterhornOperation(operation, options = {}) {
75
+ const runtimes = relayPluginRuntimes();
76
+ if (!runtimes?.hasRoom?.(operation.roomId)) return;
77
+ const result = await runtimes.handleRelayOperation({
78
+ roomName: operation.roomId,
79
+ operation
80
+ });
81
+ if (!options.broadcast || !result?.ok || !result.state) return;
82
+ relayClientRouting()?.broadcastToRoom(operation.roomId, {
83
+ type: "host/matterhorn-state",
84
+ protocol: PROTOCOL,
85
+ roomName: operation.roomId,
86
+ acceptedOperationId: operation.id,
87
+ state: result.state
88
+ });
89
+ }
90
+
91
+ function operationFromV2(header, payload, roomName) {
92
+ return {
93
+ id: header.opId,
94
+ hlc: header.hlc,
95
+ roomId: roomName,
96
+ pluginId: header.plugin,
97
+ type: header.type,
98
+ schemaAction: header.action,
99
+ actor: {
100
+ memberId: header.member,
101
+ deviceId: header.device,
102
+ role: header.role
103
+ },
104
+ grants: header.grants,
105
+ epoch: header.epoch,
106
+ stream: header.stream,
107
+ seq: header.seq,
108
+ appPackId: header.appPackId,
109
+ appPackHash: header.appPackHash,
110
+ payload
111
+ };
112
+ }
113
+
114
+ async function applyMatterhornOperationEvent(event, options = {}) {
115
+ const roomName = roomNameFromOperationEvent(event);
116
+ if (!roomName) return;
117
+
118
+ // Parse WITHOUT a room secret. Relays must never hold room secrets for the operation event path.
119
+ const parsed = parseMatterhornOperationEvent(event);
120
+ if (!parsed.ok) {
121
+ debug(`matterhorn operation event ignored for ${roomName}: ${parsed.reason}`);
122
+ return;
123
+ }
124
+
125
+ if (parsed.operation) {
126
+ // Legacy plaintext operation path. Only apply if the room has a plugin runtime.
127
+ const runtimes = relayPluginRuntimes();
128
+ if (!runtimes?.hasRoom?.(roomName)) return;
129
+ await applyMatterhornOperation(parsed.operation, options);
130
+ return;
131
+ }
132
+
133
+ if (parsed.header) {
134
+ // v2 encrypted event: verify-and-store only. The relay never decrypts the payload.
135
+ if (parsed.payload) {
136
+ // A payload here means this was called with a roomSecret (legacy/config error). Do not apply.
137
+ debug(`matterhorn v2 operation event ignored for ${roomName}: relay was given a room secret`);
138
+ } else {
139
+ debug(`matterhorn v2 operation event stored without decryption for ${roomName}: ${parsed.header.opId}`);
140
+ encryptedRoomRuntimes?.insertEvent?.(roomName, event);
141
+ }
142
+ }
143
+ }
144
+
145
+ function handleMatterhornOperationEvent(event, context = {}) {
146
+ if (context.originConn === runtimeEventOrigin) return;
147
+ if (event?.kind === MATTERHORN_INDEX_NGRAM_NOSTR_KIND) {
148
+ handleRoomIndexNgramEvent(event, context);
149
+ return;
150
+ }
151
+ void applyMatterhornOperationEvent(event, { broadcast: true }).catch((error) => {
152
+ debug(`matterhorn operation event apply failed: ${error?.message || String(error)}`);
153
+ });
154
+ }
155
+
156
+ async function catchUpMatterhornRuntime(roomName, roomSecret) {
157
+ const runtimes = relayPluginRuntimes();
158
+ if (!runtimes?.hasRoom?.(roomName)) return;
159
+ const events = relay().query([{ kinds: [MATTERHORN_OPERATION_NOSTR_KIND], "#d": [roomName] }]);
160
+
161
+ // Parse WITHOUT a room secret. v2 encrypted events are header-only; legacy events are plaintext.
162
+ for (const parsed of parseMatterhornOperationEvents(events)) {
163
+ if (parsed.operation) {
164
+ // Legacy plaintext operation path.
165
+ await applyMatterhornOperation(parsed.operation, { broadcast: false });
166
+ } else if (parsed.header) {
167
+ // v2 encrypted event: stored by the relay event policy. No replay needed.
168
+ debug(`matterhorn v2 operation event caught up without decryption for ${roomName}: ${parsed.header.opId}`);
169
+ }
170
+ }
171
+ }
172
+
173
+ return {
174
+ catchUpMatterhornRuntime,
175
+ handleMatterhornOperationEvent,
176
+ publishMatterhornOperation
177
+ };
178
+ }
179
+
180
+ module.exports = {
181
+ createMatterhornRuntimeEventBridge
182
+ };
@@ -0,0 +1,30 @@
1
+ const { MatterhornNostrRelay } = require("@mh-gg/relay-core");
2
+ const { createPartyEventPolicy } = require("@mh-gg/event");
3
+ const { createSqliteRelayStorage } = require("../sqliteRelayStorage.cjs");
4
+
5
+ function createRelayEventStore(config, options = {}) {
6
+ const storage = options.storage || createSqliteRelayStorage(config.storagePath);
7
+ const relay = new MatterhornNostrRelay({
8
+ eventPolicy: createPartyEventPolicy({
9
+ maxEventBytes: config.maxEventBytes,
10
+ maxFutureMs: config.maxFutureSeconds * 1000
11
+ }),
12
+ maxEventBytes: config.maxEventBytes,
13
+ maxEvents: config.maxEvents,
14
+ maxEventsPerRoom: config.maxEventsPerRoom,
15
+ maxBytesPerRoom: config.maxBytesPerRoom,
16
+ maxEventsPerPubkeyWindow: config.maxEventsPerPubkeyWindow,
17
+ pubkeyQuotaWindowSeconds: config.pubkeyQuotaWindowSeconds,
18
+ maxFutureSeconds: config.maxFutureSeconds,
19
+ maxMessageBytes: config.maxNostrMessageBytes,
20
+ onEvent: options.onEvent,
21
+ storage,
22
+ lazyStorage: options.lazyStorage !== false
23
+ });
24
+ return { relay, storage };
25
+ }
26
+
27
+ module.exports = {
28
+ createRelayEventStore,
29
+ createRelayEventPolicy: createPartyEventPolicy
30
+ };