@openlv/signaling 0.0.2 → 0.1.0

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/dist/index.js CHANGED
@@ -2,7 +2,9 @@ import { make } from "@openlv/core";
2
2
  import { parseEncryptionKey, validatePublicKeyHash } from "@openlv/core/encryption";
3
3
  import { EventEmitter } from "eventemitter3";
4
4
  import { match } from "ts-pattern";
5
+ import { parseSignalMessage, } from "./messages.js";
5
6
  import { log } from "./utils/log.js";
7
+ export * from "./messages.js";
6
8
  export * from "./protocol.js";
7
9
  export const SIGNAL_STATE = {
8
10
  STANDBY: "standby",
@@ -15,19 +17,44 @@ export const SIGNAL_STATE = {
15
17
  };
16
18
  export const XR_PREFIX = "x";
17
19
  export const XR_H_PREFIX = "h";
20
+ /**
21
+ * Relays are lossy (MQTT QoS 0, ntfy best-effort), so every handshake step
22
+ * is re-sent on an interval until the state machine observes progress.
23
+ * Receivers treat duplicates as no-ops, which keeps re-sends wire-compatible.
24
+ */
25
+ const HANDSHAKE_RESEND_INTERVAL_MS = 2000;
26
+ const HANDSHAKE_TIMEOUT_MS = 30000;
18
27
  /**
19
28
  * Base Signaling Layer implementation
20
29
  *
21
30
  * https://openlv.sh/api/signaling
22
31
  */
23
- export const createSignalingLayer = (init) => async ({ canEncrypt, encrypt, decrypt, rpDiscovered, h, k, publicKey, isHost, }) => {
32
+ export const createSignalingLayer = (init) => async ({ canEncrypt, encrypt, decrypt, rpDiscovered, capabilities, peerCapabilities, h, k, publicKey, isHost, }) => {
24
33
  const emitter = new EventEmitter();
25
34
  let state = SIGNAL_STATE.STANDBY;
35
+ let peerKeyRecorded = false;
36
+ let peerCapabilitiesRecorded = false;
37
+ let resendTimer;
38
+ let deadlineTimer;
39
+ const handshakeKey = k || undefined;
40
+ const stopResend = () => {
41
+ clearInterval(resendTimer);
42
+ resendTimer = undefined;
43
+ };
44
+ const stopTimers = () => {
45
+ stopResend();
46
+ clearTimeout(deadlineTimer);
47
+ deadlineTimer = undefined;
48
+ };
26
49
  const setState = (_state) => {
50
+ if (state === _state)
51
+ return;
27
52
  state = _state;
53
+ if (_state === SIGNAL_STATE.ENCRYPTED || _state === SIGNAL_STATE.ERROR) {
54
+ stopTimers();
55
+ }
28
56
  emitter.emit("state_change", _state);
29
57
  };
30
- const handshakeKey = k || undefined;
31
58
  const send = async (method, recipient, payload) => {
32
59
  const prefix = match(method)
33
60
  .with("handshake", () => XR_H_PREFIX)
@@ -45,102 +72,151 @@ export const createSignalingLayer = (init) => async ({ canEncrypt, encrypt, decr
45
72
  return encrypt(JSON.stringify(payload));
46
73
  })
47
74
  .exhaustive();
75
+ if (message === undefined) {
76
+ throw new Error(`Cannot encrypt ${method} frame: key not available`);
77
+ }
48
78
  await init.publish(prefix + recipient + message);
49
79
  };
50
- const handleReceive = async (payload) => {
51
- const prefix = payload.slice(0, XR_H_PREFIX.length);
52
- const recipient = payload.slice(XR_H_PREFIX.length, XR_H_PREFIX.length + 1);
53
- const body = payload.slice(XR_PREFIX.length + 1);
54
- const isRecipient = (isHost ? "h" : "c") === recipient;
55
- log("isRecipient", isRecipient);
56
- if (!isRecipient)
80
+ /**
81
+ * Send a handshake step now, then keep re-sending it until the next step
82
+ * (or teardown) supersedes it. Errors are logged and swallowed — the next
83
+ * tick retries.
84
+ */
85
+ const sendRepeating = (method, recipient, payload) => {
86
+ stopResend();
87
+ const attempt = () => send(method, recipient, payload)
88
+ .catch(error => log("handshake send failed, will retry", error));
89
+ resendTimer = setInterval(attempt, HANDSHAKE_RESEND_INTERVAL_MS);
90
+ return attempt();
91
+ };
92
+ const startHandshakeDeadline = () => {
93
+ if (deadlineTimer)
57
94
  return;
58
- await match({ prefix, body })
59
- .with({ prefix: XR_H_PREFIX }, async () => {
60
- if (!handshakeKey)
95
+ deadlineTimer = setTimeout(() => {
96
+ if (state === SIGNAL_STATE.ENCRYPTED)
61
97
  return;
62
- const message = await handshakeKey.decrypt(body);
63
- const msg = JSON.parse(message);
64
- await match({ msg, state, isHost })
65
- .with({ msg: { type: "flash" }, state: SIGNAL_STATE.READY, isHost: true }, async () => {
66
- setState(SIGNAL_STATE.HANDSHAKE);
67
- await send("handshake", "c", {
68
- type: "pubkey",
69
- payload: {
70
- publicKey: publicKey.toString(),
71
- },
72
- timestamp: Date.now(),
73
- });
74
- })
75
- .with({ msg: { type: "pubkey" }, isHost: false, state: SIGNAL_STATE.HANDSHAKE }, async ({ msg: { payload: msgPayload } }) => {
76
- try {
77
- const receivedKey = await parseEncryptionKey(msgPayload.publicKey);
78
- if (!await validatePublicKeyHash(receivedKey, h)) {
79
- setState(SIGNAL_STATE.ERROR);
80
- log("Received host public key does not match expected hash — possible tampering");
81
- return;
82
- }
83
- }
84
- catch {
98
+ log("handshake timed out");
99
+ setState(SIGNAL_STATE.ERROR);
100
+ }, HANDSHAKE_TIMEOUT_MS);
101
+ };
102
+ const recordPeerKey = async (key) => {
103
+ // Never overwrite an established peer key — a second, different pubkey
104
+ // mid-handshake is either a duplicate delivery or an injection attempt.
105
+ if (peerKeyRecorded)
106
+ return false;
107
+ peerKeyRecorded = true;
108
+ await rpDiscovered(key);
109
+ return true;
110
+ };
111
+ const recordPeerCapabilities = async (peerCaps) => {
112
+ // Same overwrite guard as the peer key: only the first delivery counts,
113
+ // re-sent duplicates are no-ops.
114
+ if (peerCapabilitiesRecorded)
115
+ return;
116
+ peerCapabilitiesRecorded = true;
117
+ await peerCapabilities(peerCaps);
118
+ };
119
+ const capabilitiesMessage = () => ({
120
+ type: "capabilities",
121
+ payload: capabilities,
122
+ timestamp: Date.now(),
123
+ });
124
+ const pubkeyMessage = () => ({
125
+ type: "pubkey",
126
+ payload: { publicKey: publicKey.toString() },
127
+ timestamp: Date.now(),
128
+ });
129
+ const handleHandshakeFrame = async (msg) => {
130
+ await match({ msg, state, isHost })
131
+ // Host: a client wants to connect. Re-entered on duplicate `flash`
132
+ // deliveries (client re-sends until it hears our pubkey).
133
+ .with({ msg: { type: "flash" }, state: SIGNAL_STATE.READY, isHost: true }, async () => {
134
+ setState(SIGNAL_STATE.HANDSHAKE);
135
+ startHandshakeDeadline();
136
+ await sendRepeating("handshake", "c", pubkeyMessage());
137
+ })
138
+ // Client: host announced its public key.
139
+ .with({ msg: { type: "pubkey" }, isHost: false, state: SIGNAL_STATE.HANDSHAKE }, async ({ msg: { payload: msgPayload } }) => {
140
+ try {
141
+ const receivedKey = await parseEncryptionKey(msgPayload.publicKey);
142
+ if (!await validatePublicKeyHash(receivedKey, h)) {
85
143
  setState(SIGNAL_STATE.ERROR);
86
- log("Failed to parse received host public key");
144
+ log("Received host public key does not match expected hash — possible tampering");
87
145
  return;
88
146
  }
89
- await rpDiscovered(msgPayload.publicKey);
90
- setState(SIGNAL_STATE.HANDSHAKE_PARTIAL);
91
- return await send("encrypted", "h", {
92
- type: "pubkey",
93
- payload: {
94
- publicKey: publicKey.toString(),
95
- },
96
- timestamp: Date.now(),
97
- });
98
- })
99
- .otherwise(() => {
100
- log("Received invalid message H", msg);
101
- });
147
+ }
148
+ catch {
149
+ setState(SIGNAL_STATE.ERROR);
150
+ log("Failed to parse received host public key");
151
+ return;
152
+ }
153
+ if (!await recordPeerKey(msgPayload.publicKey))
154
+ return;
155
+ setState(SIGNAL_STATE.HANDSHAKE_PARTIAL);
156
+ return await sendRepeating("encrypted", "h", pubkeyMessage());
102
157
  })
103
- .with({ prefix: XR_PREFIX }, async () => {
104
- const message = await decrypt(body);
105
- const msg = JSON.parse(message);
106
- await match({ msg, state, isHost })
107
- .with({
108
- msg: { type: "pubkey" },
109
- isHost: true,
110
- state: SIGNAL_STATE.HANDSHAKE,
111
- }, async ({ msg: { payload: msgPayload } }) => {
112
- await rpDiscovered(msgPayload.publicKey);
113
- setState(SIGNAL_STATE.HANDSHAKE_PARTIAL);
114
- return await send("encrypted", "c", {
115
- type: "ack",
116
- payload: undefined,
117
- timestamp: Date.now(),
118
- });
119
- })
120
- .with({
121
- msg: { type: "ack" },
122
- state: SIGNAL_STATE.HANDSHAKE_PARTIAL,
123
- }, async () => {
124
- setState(SIGNAL_STATE.ENCRYPTED);
125
- if (isHost)
126
- return;
127
- return await send("encrypted", "h", {
128
- type: "ack",
129
- payload: undefined,
130
- timestamp: Date.now(),
131
- });
132
- })
133
- .with({ msg: { type: "data" }, state: SIGNAL_STATE.ENCRYPTED }, async () => {
134
- emitter.emit("message", msg.payload);
135
- })
136
- .otherwise(() => {
137
- log("Received invalid message X", msg);
138
- });
158
+ .otherwise(() => {
159
+ log("Ignoring handshake frame", msg.type, "in state", state);
160
+ });
161
+ };
162
+ const handleEncryptedFrame = async (msg) => {
163
+ await match({ msg, state, isHost })
164
+ // Host: client responded with its public key.
165
+ .with({ msg: { type: "pubkey" }, isHost: true, state: SIGNAL_STATE.HANDSHAKE }, async ({ msg: { payload: msgPayload } }) => {
166
+ if (!await recordPeerKey(msgPayload.publicKey))
167
+ return;
168
+ setState(SIGNAL_STATE.HANDSHAKE_PARTIAL);
169
+ return await sendRepeating("encrypted", "c", capabilitiesMessage());
170
+ })
171
+ .with({ msg: { type: "capabilities" }, state: SIGNAL_STATE.HANDSHAKE_PARTIAL }, async ({ msg: { payload: msgPayload } }) => {
172
+ await recordPeerCapabilities(msgPayload);
173
+ setState(SIGNAL_STATE.ENCRYPTED);
174
+ if (isHost)
175
+ return;
176
+ return await send("encrypted", "h", capabilitiesMessage());
177
+ })
178
+ // Client already encrypted, but the host is still re-sending its
179
+ // capabilities (our final packet was lost): answer again so the host
180
+ // can finish.
181
+ .with({ msg: { type: "capabilities" }, state: SIGNAL_STATE.ENCRYPTED, isHost: false }, async () => await send("encrypted", "h", capabilitiesMessage()))
182
+ .with({ msg: { type: "data" }, state: SIGNAL_STATE.ENCRYPTED }, async () => {
183
+ emitter.emit("message", msg.payload);
139
184
  })
140
185
  .otherwise(() => {
141
- log("Received invalid message", payload);
186
+ log("Ignoring encrypted frame", msg.type, "in state", state);
142
187
  });
143
188
  };
189
+ const handleReceive = async (payload) => {
190
+ // The topic is public: anyone can publish garbage. Nothing in this
191
+ // handler may throw past this boundary, otherwise a single malformed
192
+ // frame becomes an unhandled rejection.
193
+ try {
194
+ const prefix = payload.slice(0, 1);
195
+ const recipient = payload.slice(1, 2);
196
+ const body = payload.slice(2);
197
+ const isRecipient = (isHost ? "h" : "c") === recipient;
198
+ if (!isRecipient)
199
+ return;
200
+ if (prefix === XR_H_PREFIX) {
201
+ if (!handshakeKey)
202
+ return;
203
+ const msg = parseSignalMessage(await handshakeKey.decrypt(body));
204
+ if (msg)
205
+ await handleHandshakeFrame(msg);
206
+ }
207
+ else if (prefix === XR_PREFIX) {
208
+ const msg = parseSignalMessage(await decrypt(body));
209
+ if (msg)
210
+ await handleEncryptedFrame(msg);
211
+ }
212
+ else {
213
+ log("Dropping frame with unknown prefix");
214
+ }
215
+ }
216
+ catch (error) {
217
+ log("Dropping undecryptable or malformed frame", error);
218
+ }
219
+ };
144
220
  return make(emitter, {
145
221
  type: init.type,
146
222
  async setup() {
@@ -149,24 +225,24 @@ export const createSignalingLayer = (init) => async ({ canEncrypt, encrypt, decr
149
225
  await init.subscribe(handleReceive);
150
226
  if (canEncrypt()) {
151
227
  setState(SIGNAL_STATE.ENCRYPTED);
228
+ return;
152
229
  }
153
- else {
154
- if (isHost) {
155
- setState(SIGNAL_STATE.READY);
156
- }
157
- else {
158
- setState(SIGNAL_STATE.READY);
159
- await send("handshake", "h", {
160
- type: "flash",
161
- payload: {},
162
- timestamp: Date.now(),
163
- });
164
- setState(SIGNAL_STATE.HANDSHAKE);
165
- }
230
+ setState(SIGNAL_STATE.READY);
231
+ if (!isHost) {
232
+ // Enter HANDSHAKE before publishing: the host's pubkey reply can
233
+ // arrive while the publish is still in flight.
234
+ setState(SIGNAL_STATE.HANDSHAKE);
235
+ startHandshakeDeadline();
236
+ await sendRepeating("handshake", "h", {
237
+ type: "flash",
238
+ payload: {},
239
+ timestamp: Date.now(),
240
+ });
166
241
  }
167
242
  },
168
243
  async teardown() {
169
244
  log("teardown");
245
+ stopTimers();
170
246
  await init.teardown?.();
171
247
  },
172
248
  send(message) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { make } from \"@openlv/core\";\nimport type { EncryptionKey, SymmetricKey } from \"@openlv/core/encryption\";\nimport { parseEncryptionKey, validatePublicKeyHash } from \"@openlv/core/encryption\";\nimport { EventEmitter } from \"eventemitter3\";\nimport { match } from \"ts-pattern\";\nimport type { MaybePromise } from \"viem\";\n\nimport type { SignalMessage } from \"./messages.js\";\nimport type { SignalingChannel } from \"./protocol.js\";\nimport { log } from \"./utils/log.js\";\n\nexport * from \"./protocol.js\";\n\nexport const SIGNAL_STATE = {\n STANDBY: \"standby\",\n CONNECTING: \"connecting\",\n READY: \"ready\",\n HANDSHAKE: \"handshake\",\n HANDSHAKE_PARTIAL: \"handshake-partial\",\n ENCRYPTED: \"encrypted\",\n ERROR: \"error\",\n} as const;\nexport type SignalState = (typeof SIGNAL_STATE)[keyof typeof SIGNAL_STATE];\n\nexport type SignalEventMap = {\n state_change: (state: SignalState) => void;\n message: (message: object) => void;\n};\n\nexport type SignalingProperties = {\n isHost: boolean;\n h: string;\n k?: SymmetricKey;\n rpDiscovered: (rpKey: string) => MaybePromise<void>;\n // Decrypt using our private key\n decrypt: (message: string) => MaybePromise<string>;\n // Encrypt to relying party\n encrypt: (message: string) => MaybePromise<string>;\n // our public key\n publicKey: EncryptionKey;\n canEncrypt: () => boolean;\n};\n\nexport type SignalingContext = {\n type: string;\n // waitForConnection: () => Promise<void>;\n // reconnect: () => void;\n\n // Sending only works once keys are exchanged\n send: (message: object) => MaybePromise<void>;\n setup: () => MaybePromise<void>;\n teardown: () => MaybePromise<void>;\n\n getState: () => {\n state: SignalState;\n };\n};\nexport type SignalingLayer = EventEmitter<SignalEventMap> & SignalingContext;\nexport type SignalingLayerFn = (\n properties: SignalingProperties,\n) => Promise<SignalingLayer>;\n\nexport const XR_PREFIX = \"x\";\nexport const XR_H_PREFIX = \"h\";\n\n/**\n * Base Signaling Layer implementation\n *\n * https://openlv.sh/api/signaling\n */\nexport const createSignalingLayer = (\n init: SignalingChannel,\n): SignalingLayerFn => async ({\n canEncrypt,\n encrypt,\n decrypt,\n rpDiscovered,\n h,\n k,\n publicKey,\n isHost,\n}: SignalingProperties) => {\n const emitter = new EventEmitter<SignalEventMap>();\n let state: SignalState = SIGNAL_STATE.STANDBY;\n const setState = (_state: SignalState) => {\n state = _state;\n emitter.emit(\"state_change\", _state);\n };\n const handshakeKey = k || undefined;\n\n const send = async (\n method: \"handshake\" | \"encrypted\",\n recipient: \"h\" | \"c\",\n payload: SignalMessage,\n ) => {\n const prefix = match(method)\n .with(\"handshake\", () => XR_H_PREFIX)\n .with(\"encrypted\", () => XR_PREFIX)\n .exhaustive();\n\n const message = await match(method)\n .with(\"handshake\", () => {\n if (!handshakeKey) return;\n\n return handshakeKey.encrypt(JSON.stringify(payload));\n })\n .with(\"encrypted\", () => {\n if (!canEncrypt()) return;\n\n return encrypt(JSON.stringify(payload));\n })\n .exhaustive();\n\n await init.publish(prefix + recipient + message);\n };\n\n const handleReceive = async (payload: string) => {\n const prefix = payload.slice(0, XR_H_PREFIX.length);\n const recipient = payload.slice(\n XR_H_PREFIX.length,\n XR_H_PREFIX.length + 1,\n );\n const body = payload.slice(XR_PREFIX.length + 1);\n const isRecipient = (isHost ? \"h\" : \"c\") === recipient;\n\n log(\"isRecipient\", isRecipient);\n\n if (!isRecipient) return;\n\n await match({ prefix, body })\n .with({ prefix: XR_H_PREFIX }, async () => {\n if (!handshakeKey) return;\n\n const message = await handshakeKey.decrypt(body);\n const msg = JSON.parse(message) as SignalMessage;\n\n await match({ msg, state, isHost })\n .with({ msg: { type: \"flash\" }, state: SIGNAL_STATE.READY, isHost: true }, async () => {\n setState(SIGNAL_STATE.HANDSHAKE);\n await send(\"handshake\", \"c\", {\n type: \"pubkey\",\n payload: {\n publicKey: publicKey.toString(),\n },\n timestamp: Date.now(),\n });\n })\n .with({ msg: { type: \"pubkey\" }, isHost: false, state: SIGNAL_STATE.HANDSHAKE }, async ({ msg: { payload: msgPayload } }) => {\n try {\n const receivedKey = await parseEncryptionKey(msgPayload.publicKey);\n\n if (!await validatePublicKeyHash(receivedKey, h)) {\n setState(SIGNAL_STATE.ERROR);\n log(\"Received host public key does not match expected hash — possible tampering\");\n\n return;\n }\n }\n catch {\n setState(SIGNAL_STATE.ERROR);\n log(\"Failed to parse received host public key\");\n\n return;\n }\n\n await rpDiscovered(msgPayload.publicKey);\n setState(SIGNAL_STATE.HANDSHAKE_PARTIAL);\n\n return await send(\"encrypted\", \"h\", {\n type: \"pubkey\",\n payload: {\n publicKey: publicKey.toString(),\n },\n timestamp: Date.now(),\n });\n })\n .otherwise(() => {\n log(\"Received invalid message H\", msg);\n });\n })\n .with({ prefix: XR_PREFIX }, async () => {\n const message = await decrypt(body);\n const msg = JSON.parse(message) as SignalMessage;\n\n await match({ msg, state, isHost })\n .with(\n {\n msg: { type: \"pubkey\" },\n isHost: true,\n state: SIGNAL_STATE.HANDSHAKE,\n },\n async ({ msg: { payload: msgPayload } }) => {\n await rpDiscovered(msgPayload.publicKey);\n setState(SIGNAL_STATE.HANDSHAKE_PARTIAL);\n\n return await send(\"encrypted\", \"c\", {\n type: \"ack\",\n payload: undefined,\n timestamp: Date.now(),\n });\n },\n )\n .with(\n {\n msg: { type: \"ack\" },\n state: SIGNAL_STATE.HANDSHAKE_PARTIAL,\n },\n async () => {\n setState(SIGNAL_STATE.ENCRYPTED);\n\n if (isHost) return;\n\n return await send(\"encrypted\", \"h\", {\n type: \"ack\",\n payload: undefined,\n timestamp: Date.now(),\n });\n },\n )\n .with({ msg: { type: \"data\" }, state: SIGNAL_STATE.ENCRYPTED }, async () => {\n emitter.emit(\"message\", msg.payload as object);\n })\n .otherwise(() => {\n log(\"Received invalid message X\", msg);\n });\n })\n .otherwise(() => {\n log(\"Received invalid message\", payload);\n });\n };\n\n return make(emitter, {\n type: init.type,\n async setup() {\n setState(SIGNAL_STATE.CONNECTING);\n await init.setup();\n await init.subscribe(handleReceive);\n\n if (canEncrypt()) {\n setState(SIGNAL_STATE.ENCRYPTED);\n }\n else {\n if (isHost) {\n setState(SIGNAL_STATE.READY);\n }\n else {\n setState(SIGNAL_STATE.READY);\n await send(\"handshake\", \"h\", {\n type: \"flash\",\n payload: {},\n timestamp: Date.now(),\n });\n setState(SIGNAL_STATE.HANDSHAKE);\n }\n }\n },\n async teardown() {\n log(\"teardown\");\n await init.teardown?.();\n },\n send(message: object) {\n if (!canEncrypt()) {\n return Promise.reject(\n new Error(\"Cannot encrypt message before keys are exchanged\"),\n );\n }\n\n const them = isHost ? \"c\" : \"h\";\n\n return send(\"encrypted\", them, {\n type: \"data\",\n payload: message,\n timestamp: Date.now(),\n });\n },\n getState() {\n return { state: state };\n },\n });\n};\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { make } from \"@openlv/core\";\nimport type { EncryptionKey, SymmetricKey } from \"@openlv/core/encryption\";\nimport { parseEncryptionKey, validatePublicKeyHash } from \"@openlv/core/encryption\";\nimport { EventEmitter } from \"eventemitter3\";\nimport { match } from \"ts-pattern\";\nimport type { MaybePromise } from \"viem\";\n\nimport {\n parseSignalMessage,\n type PeerCapabilities,\n type SignalMessage,\n} from \"./messages.js\";\nimport type { SignalingChannel } from \"./protocol.js\";\nimport { log } from \"./utils/log.js\";\n\nexport * from \"./messages.js\";\nexport * from \"./protocol.js\";\n\nexport const SIGNAL_STATE = {\n STANDBY: \"standby\",\n CONNECTING: \"connecting\",\n READY: \"ready\",\n HANDSHAKE: \"handshake\",\n HANDSHAKE_PARTIAL: \"handshake-partial\",\n ENCRYPTED: \"encrypted\",\n ERROR: \"error\",\n} as const;\nexport type SignalState = (typeof SIGNAL_STATE)[keyof typeof SIGNAL_STATE];\n\nexport type SignalEventMap = {\n state_change: (state: SignalState) => void;\n message: (message: object) => void;\n};\n\nexport type SignalingProperties = {\n isHost: boolean;\n h: string;\n k?: SymmetricKey;\n rpDiscovered: (rpKey: string) => MaybePromise<void>;\n // Our capabilities, advertised to the peer during the handshake\n capabilities: PeerCapabilities;\n peerCapabilities: (capabilities: PeerCapabilities) => MaybePromise<void>;\n // Decrypt using our private key\n decrypt: (message: string) => MaybePromise<string>;\n // Encrypt to relying party\n encrypt: (message: string) => MaybePromise<string>;\n // our public key\n publicKey: EncryptionKey;\n canEncrypt: () => boolean;\n};\n\nexport type SignalingContext = {\n type: string;\n\n // Sending only works once keys are exchanged\n send: (message: object) => MaybePromise<void>;\n setup: () => MaybePromise<void>;\n teardown: () => MaybePromise<void>;\n\n getState: () => {\n state: SignalState;\n };\n};\nexport type SignalingLayer = EventEmitter<SignalEventMap> & SignalingContext;\nexport type SignalingLayerFn = (\n properties: SignalingProperties,\n) => Promise<SignalingLayer>;\n\nexport const XR_PREFIX = \"x\";\nexport const XR_H_PREFIX = \"h\";\n\n/**\n * Relays are lossy (MQTT QoS 0, ntfy best-effort), so every handshake step\n * is re-sent on an interval until the state machine observes progress.\n * Receivers treat duplicates as no-ops, which keeps re-sends wire-compatible.\n */\nconst HANDSHAKE_RESEND_INTERVAL_MS = 2000;\nconst HANDSHAKE_TIMEOUT_MS = 30_000;\n\n/**\n * Base Signaling Layer implementation\n *\n * https://openlv.sh/api/signaling\n */\nexport const createSignalingLayer = (\n init: SignalingChannel,\n): SignalingLayerFn => async ({\n canEncrypt,\n encrypt,\n decrypt,\n rpDiscovered,\n capabilities,\n peerCapabilities,\n h,\n k,\n publicKey,\n isHost,\n}: SignalingProperties) => {\n const emitter = new EventEmitter<SignalEventMap>();\n let state: SignalState = SIGNAL_STATE.STANDBY;\n let peerKeyRecorded = false;\n let peerCapabilitiesRecorded = false;\n let resendTimer: ReturnType<typeof setInterval> | undefined;\n let deadlineTimer: ReturnType<typeof setTimeout> | undefined;\n const handshakeKey = k || undefined;\n\n const stopResend = () => {\n clearInterval(resendTimer);\n resendTimer = undefined;\n };\n const stopTimers = () => {\n stopResend();\n clearTimeout(deadlineTimer);\n deadlineTimer = undefined;\n };\n\n const setState = (_state: SignalState) => {\n if (state === _state) return;\n\n state = _state;\n\n if (_state === SIGNAL_STATE.ENCRYPTED || _state === SIGNAL_STATE.ERROR) {\n stopTimers();\n }\n\n emitter.emit(\"state_change\", _state);\n };\n\n const send = async (\n method: \"handshake\" | \"encrypted\",\n recipient: \"h\" | \"c\",\n payload: SignalMessage,\n ) => {\n const prefix = match(method)\n .with(\"handshake\", () => XR_H_PREFIX)\n .with(\"encrypted\", () => XR_PREFIX)\n .exhaustive();\n\n const message = await match(method)\n .with(\"handshake\", () => {\n if (!handshakeKey) return;\n\n return handshakeKey.encrypt(JSON.stringify(payload));\n })\n .with(\"encrypted\", () => {\n if (!canEncrypt()) return;\n\n return encrypt(JSON.stringify(payload));\n })\n .exhaustive();\n\n if (message === undefined) {\n throw new Error(`Cannot encrypt ${method} frame: key not available`);\n }\n\n await init.publish(prefix + recipient + message);\n };\n\n /**\n * Send a handshake step now, then keep re-sending it until the next step\n * (or teardown) supersedes it. Errors are logged and swallowed — the next\n * tick retries.\n */\n const sendRepeating = (\n method: \"handshake\" | \"encrypted\",\n recipient: \"h\" | \"c\",\n payload: SignalMessage,\n ) => {\n stopResend();\n\n const attempt = () => send(method, recipient, payload)\n .catch(error => log(\"handshake send failed, will retry\", error));\n\n resendTimer = setInterval(attempt, HANDSHAKE_RESEND_INTERVAL_MS);\n\n return attempt();\n };\n\n const startHandshakeDeadline = () => {\n if (deadlineTimer) return;\n\n deadlineTimer = setTimeout(() => {\n if (state === SIGNAL_STATE.ENCRYPTED) return;\n\n log(\"handshake timed out\");\n setState(SIGNAL_STATE.ERROR);\n }, HANDSHAKE_TIMEOUT_MS);\n };\n\n const recordPeerKey = async (key: string): Promise<boolean> => {\n // Never overwrite an established peer key — a second, different pubkey\n // mid-handshake is either a duplicate delivery or an injection attempt.\n if (peerKeyRecorded) return false;\n\n peerKeyRecorded = true;\n await rpDiscovered(key);\n\n return true;\n };\n\n const recordPeerCapabilities = async (\n peerCaps: PeerCapabilities,\n ): Promise<void> => {\n // Same overwrite guard as the peer key: only the first delivery counts,\n // re-sent duplicates are no-ops.\n if (peerCapabilitiesRecorded) return;\n\n peerCapabilitiesRecorded = true;\n await peerCapabilities(peerCaps);\n };\n\n const capabilitiesMessage = (): SignalMessage => ({\n type: \"capabilities\",\n payload: capabilities,\n timestamp: Date.now(),\n });\n const pubkeyMessage = (): SignalMessage => ({\n type: \"pubkey\",\n payload: { publicKey: publicKey.toString() },\n timestamp: Date.now(),\n });\n\n const handleHandshakeFrame = async (msg: SignalMessage) => {\n await match({ msg, state, isHost })\n // Host: a client wants to connect. Re-entered on duplicate `flash`\n // deliveries (client re-sends until it hears our pubkey).\n .with({ msg: { type: \"flash\" }, state: SIGNAL_STATE.READY, isHost: true }, async () => {\n setState(SIGNAL_STATE.HANDSHAKE);\n startHandshakeDeadline();\n await sendRepeating(\"handshake\", \"c\", pubkeyMessage());\n })\n // Client: host announced its public key.\n .with({ msg: { type: \"pubkey\" }, isHost: false, state: SIGNAL_STATE.HANDSHAKE }, async ({ msg: { payload: msgPayload } }) => {\n try {\n const receivedKey = await parseEncryptionKey(msgPayload.publicKey);\n\n if (!await validatePublicKeyHash(receivedKey, h)) {\n setState(SIGNAL_STATE.ERROR);\n log(\"Received host public key does not match expected hash — possible tampering\");\n\n return;\n }\n }\n catch {\n setState(SIGNAL_STATE.ERROR);\n log(\"Failed to parse received host public key\");\n\n return;\n }\n\n if (!await recordPeerKey(msgPayload.publicKey)) return;\n\n setState(SIGNAL_STATE.HANDSHAKE_PARTIAL);\n\n return await sendRepeating(\"encrypted\", \"h\", pubkeyMessage());\n })\n .otherwise(() => {\n log(\"Ignoring handshake frame\", msg.type, \"in state\", state);\n });\n };\n\n const handleEncryptedFrame = async (msg: SignalMessage) => {\n await match({ msg, state, isHost })\n // Host: client responded with its public key.\n .with(\n { msg: { type: \"pubkey\" }, isHost: true, state: SIGNAL_STATE.HANDSHAKE },\n async ({ msg: { payload: msgPayload } }) => {\n if (!await recordPeerKey(msgPayload.publicKey)) return;\n\n setState(SIGNAL_STATE.HANDSHAKE_PARTIAL);\n\n return await sendRepeating(\"encrypted\", \"c\", capabilitiesMessage());\n },\n )\n .with(\n { msg: { type: \"capabilities\" }, state: SIGNAL_STATE.HANDSHAKE_PARTIAL },\n async ({ msg: { payload: msgPayload } }) => {\n await recordPeerCapabilities(msgPayload);\n setState(SIGNAL_STATE.ENCRYPTED);\n\n if (isHost) return;\n\n return await send(\"encrypted\", \"h\", capabilitiesMessage());\n },\n )\n // Client already encrypted, but the host is still re-sending its\n // capabilities (our final packet was lost): answer again so the host\n // can finish.\n .with(\n { msg: { type: \"capabilities\" }, state: SIGNAL_STATE.ENCRYPTED, isHost: false },\n async () => await send(\"encrypted\", \"h\", capabilitiesMessage()),\n )\n .with({ msg: { type: \"data\" }, state: SIGNAL_STATE.ENCRYPTED }, async () => {\n emitter.emit(\"message\", msg.payload as object);\n })\n .otherwise(() => {\n log(\"Ignoring encrypted frame\", msg.type, \"in state\", state);\n });\n };\n\n const handleReceive = async (payload: string) => {\n // The topic is public: anyone can publish garbage. Nothing in this\n // handler may throw past this boundary, otherwise a single malformed\n // frame becomes an unhandled rejection.\n try {\n const prefix = payload.slice(0, 1);\n const recipient = payload.slice(1, 2);\n const body = payload.slice(2);\n const isRecipient = (isHost ? \"h\" : \"c\") === recipient;\n\n if (!isRecipient) return;\n\n if (prefix === XR_H_PREFIX) {\n if (!handshakeKey) return;\n\n const msg = parseSignalMessage(await handshakeKey.decrypt(body));\n\n if (msg) await handleHandshakeFrame(msg);\n }\n else if (prefix === XR_PREFIX) {\n const msg = parseSignalMessage(await decrypt(body));\n\n if (msg) await handleEncryptedFrame(msg);\n }\n else {\n log(\"Dropping frame with unknown prefix\");\n }\n }\n catch (error) {\n log(\"Dropping undecryptable or malformed frame\", error);\n }\n };\n\n return make(emitter, {\n type: init.type,\n async setup() {\n setState(SIGNAL_STATE.CONNECTING);\n await init.setup();\n await init.subscribe(handleReceive);\n\n if (canEncrypt()) {\n setState(SIGNAL_STATE.ENCRYPTED);\n\n return;\n }\n\n setState(SIGNAL_STATE.READY);\n\n if (!isHost) {\n // Enter HANDSHAKE before publishing: the host's pubkey reply can\n // arrive while the publish is still in flight.\n setState(SIGNAL_STATE.HANDSHAKE);\n startHandshakeDeadline();\n await sendRepeating(\"handshake\", \"h\", {\n type: \"flash\",\n payload: {},\n timestamp: Date.now(),\n });\n }\n },\n async teardown() {\n log(\"teardown\");\n stopTimers();\n await init.teardown?.();\n },\n send(message: object) {\n if (!canEncrypt()) {\n return Promise.reject(\n new Error(\"Cannot encrypt message before keys are exchanged\"),\n );\n }\n\n const them = isHost ? \"c\" : \"h\";\n\n return send(\"encrypted\", them, {\n type: \"data\",\n payload: message,\n timestamp: Date.now(),\n });\n },\n getState() {\n return { state: state };\n },\n });\n};\n"]}
@@ -76,6 +76,8 @@ const testSignalingLayer = async (layer, props) => {
76
76
  const signalA = await signalingLayer({
77
77
  h,
78
78
  rpDiscovered: v => log("rpKey", v),
79
+ capabilities: { transports: ["wrtc"] },
80
+ peerCapabilities: v => log("peerCapabilities", v),
79
81
  canEncrypt: () => true,
80
82
  encrypt: publicKey.encrypt,
81
83
  decrypt: decryptionKey.decrypt,
@@ -85,6 +87,8 @@ const testSignalingLayer = async (layer, props) => {
85
87
  const signalB = await signalingLayer({
86
88
  h,
87
89
  rpDiscovered: v => log("rpKey", v),
90
+ capabilities: { transports: ["wrtc"] },
91
+ peerCapabilities: v => log("peerCapabilities", v),
88
92
  canEncrypt: () => true,
89
93
  encrypt: publicKey.encrypt,
90
94
  decrypt: decryptionKey.decrypt,
@@ -1 +1 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { generateKeyPair } from \"@openlv/core/encryption\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { mqtt } from \"./mqtt/index.js\";\nimport { ntfy } from \"./ntfy/index.js\";\nimport type { SignalingProtocol, SignalingProtocolOptions } from \"./protocol.js\";\nimport { log } from \"./utils/log.js\";\n\nconst hKey = \"test\";\n\nconst providersByType: readonly [\n string,\n readonly [string, SignalingProtocol, SignalingProtocolOptions][],\n][] = [\n [\n \"mqtt\",\n [\n [\n \"mqtt-dashboard\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"wss://mqtt-dashboard.com:8884/mqtt\",\n },\n ],\n [\n \"broker.emqx.io\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"ws://broker.emqx.io:8083/mqtt\",\n },\n ],\n [\n \"test.mosquitto.org\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"ws://test.mosquitto.org:8080/mqtt\",\n },\n ],\n [\n \"broker.itdata.nu\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"wss://broker.itdata.nu/mqtt\",\n },\n ],\n ],\n ],\n [\n \"ntfy\",\n [\n [\n \"ntfy.sh\",\n ntfy,\n { topic: \"mytesttopic1111\", url: \"https://ntfy.sh/\" },\n ],\n [\n \"ntfy.envs.net\",\n ntfy,\n { topic: \"mytesttopic1111\", url: \"https://ntfy.envs.net/\" },\n ],\n ],\n ],\n // TODO: GunDB is disabled\n // [\n // \"gundb\",\n // [\n // [\n // \"try.axe.eco\",\n // gundb,\n // { topic: \"mytesttopic1111\", url: \"wss://try.axe.eco/gun\" },\n // ],\n // ],\n // ],\n] as const;\n\nconst testSignalingLayer = async (\n layer: SignalingProtocol,\n props: SignalingProtocolOptions,\n): Promise<void> => {\n const { encryptionKey: publicKey, decryptionKey } = await generateKeyPair();\n const h = hKey;\n\n const signalingLayer = await layer(props);\n const signalA = await signalingLayer({\n h,\n rpDiscovered: v => log(\"rpKey\", v),\n canEncrypt: () => true,\n encrypt: publicKey.encrypt,\n decrypt: decryptionKey.decrypt,\n publicKey,\n isHost: true,\n });\n const signalB = await signalingLayer({\n h,\n rpDiscovered: v => log(\"rpKey\", v),\n canEncrypt: () => true,\n encrypt: publicKey.encrypt,\n decrypt: decryptionKey.decrypt,\n publicKey,\n isHost: false,\n });\n\n expect(signalA).toBeDefined();\n expect(signalB).toBeDefined();\n\n await Promise.all([signalA.setup(), signalB.setup()]);\n\n const messageReceivedA = new Promise<void>((resolve) => {\n signalA.on(\"message\", (message) => {\n log(\"messageReceivedA\", message);\n expect(message).toEqual({ data: \"test2\" });\n resolve();\n });\n });\n const messageReceivedB = new Promise<void>((resolve) => {\n signalB.on(\"message\", (message) => {\n log(\"messageReceivedB\", message);\n expect(message).toEqual({ data: \"test1\" });\n resolve();\n });\n });\n\n await signalA.send({ data: \"test1\" });\n\n await messageReceivedB;\n\n await signalB.send({ data: \"test2\" });\n\n await messageReceivedA;\n\n await signalA.teardown();\n await signalB.teardown();\n};\n\nconst PROVIDER_TIMEOUT_MS = 5000;\n\ntype ProviderResult =\n | { url: string; ok: true; duration: number; }\n | { url: string; ok: false; error: string; };\n\nconst testProvider = async (\n layer: SignalingProtocol,\n props: SignalingProtocolOptions,\n): Promise<ProviderResult> => {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(\n () => reject(new Error(`Timeout after ${PROVIDER_TIMEOUT_MS}ms`)),\n PROVIDER_TIMEOUT_MS,\n ),\n );\n\n const startTime = performance.now();\n\n try {\n await Promise.race([testSignalingLayer(layer, props), timeout]);\n const duration = Math.round(performance.now() - startTime);\n\n return { url: props.url, ok: true, duration };\n }\n catch (error) {\n return {\n url: props.url,\n ok: false,\n error: (error as Error).message,\n };\n }\n};\n\nconst formatResults = (typeName: string, results: ProviderResult[]): string => {\n const passed = results.filter(r => r.ok).length;\n const total = results.length;\n\n return [\n `${typeName.toUpperCase()}: ${passed}/${total}`,\n ...results.map(r =>\n (r.ok ? `✓ ${r.url} ${r.duration}ms` : `✗ ${r.url} - ${r.error}`),\n ),\n ].join(\"\\n\");\n};\n\ndescribe.for(providersByType)(\"signaling: %s\", ([typeName, providers]) => {\n it(\n \"should pass signaling messages between peers\",\n { timeout: PROVIDER_TIMEOUT_MS * providers.length + 5000 },\n async () => {\n const results = await Promise.all(\n providers.map(([, layer, props]) => testProvider(layer, props)),\n );\n\n const summary = formatResults(typeName, results);\n\n console.log(`\\n${summary}`);\n\n const passed = results.filter(r => r.ok).length;\n\n expect(passed).toBeGreaterThan(0);\n },\n );\n});\n"]}
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { generateKeyPair } from \"@openlv/core/encryption\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { mqtt } from \"./mqtt/index.js\";\nimport { ntfy } from \"./ntfy/index.js\";\nimport type { SignalingProtocol, SignalingProtocolOptions } from \"./protocol.js\";\nimport { log } from \"./utils/log.js\";\n\nconst hKey = \"test\";\n\nconst providersByType: readonly [\n string,\n readonly [string, SignalingProtocol, SignalingProtocolOptions][],\n][] = [\n [\n \"mqtt\",\n [\n [\n \"mqtt-dashboard\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"wss://mqtt-dashboard.com:8884/mqtt\",\n },\n ],\n [\n \"broker.emqx.io\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"ws://broker.emqx.io:8083/mqtt\",\n },\n ],\n [\n \"test.mosquitto.org\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"ws://test.mosquitto.org:8080/mqtt\",\n },\n ],\n [\n \"broker.itdata.nu\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"wss://broker.itdata.nu/mqtt\",\n },\n ],\n ],\n ],\n [\n \"ntfy\",\n [\n [\n \"ntfy.sh\",\n ntfy,\n { topic: \"mytesttopic1111\", url: \"https://ntfy.sh/\" },\n ],\n [\n \"ntfy.envs.net\",\n ntfy,\n { topic: \"mytesttopic1111\", url: \"https://ntfy.envs.net/\" },\n ],\n ],\n ],\n // TODO: GunDB is disabled\n // [\n // \"gundb\",\n // [\n // [\n // \"try.axe.eco\",\n // gundb,\n // { topic: \"mytesttopic1111\", url: \"wss://try.axe.eco/gun\" },\n // ],\n // ],\n // ],\n] as const;\n\nconst testSignalingLayer = async (\n layer: SignalingProtocol,\n props: SignalingProtocolOptions,\n): Promise<void> => {\n const { encryptionKey: publicKey, decryptionKey } = await generateKeyPair();\n const h = hKey;\n\n const signalingLayer = await layer(props);\n const signalA = await signalingLayer({\n h,\n rpDiscovered: v => log(\"rpKey\", v),\n capabilities: { transports: [\"wrtc\"] },\n peerCapabilities: v => log(\"peerCapabilities\", v),\n canEncrypt: () => true,\n encrypt: publicKey.encrypt,\n decrypt: decryptionKey.decrypt,\n publicKey,\n isHost: true,\n });\n const signalB = await signalingLayer({\n h,\n rpDiscovered: v => log(\"rpKey\", v),\n capabilities: { transports: [\"wrtc\"] },\n peerCapabilities: v => log(\"peerCapabilities\", v),\n canEncrypt: () => true,\n encrypt: publicKey.encrypt,\n decrypt: decryptionKey.decrypt,\n publicKey,\n isHost: false,\n });\n\n expect(signalA).toBeDefined();\n expect(signalB).toBeDefined();\n\n await Promise.all([signalA.setup(), signalB.setup()]);\n\n const messageReceivedA = new Promise<void>((resolve) => {\n signalA.on(\"message\", (message) => {\n log(\"messageReceivedA\", message);\n expect(message).toEqual({ data: \"test2\" });\n resolve();\n });\n });\n const messageReceivedB = new Promise<void>((resolve) => {\n signalB.on(\"message\", (message) => {\n log(\"messageReceivedB\", message);\n expect(message).toEqual({ data: \"test1\" });\n resolve();\n });\n });\n\n await signalA.send({ data: \"test1\" });\n\n await messageReceivedB;\n\n await signalB.send({ data: \"test2\" });\n\n await messageReceivedA;\n\n await signalA.teardown();\n await signalB.teardown();\n};\n\nconst PROVIDER_TIMEOUT_MS = 5000;\n\ntype ProviderResult =\n | { url: string; ok: true; duration: number; }\n | { url: string; ok: false; error: string; };\n\nconst testProvider = async (\n layer: SignalingProtocol,\n props: SignalingProtocolOptions,\n): Promise<ProviderResult> => {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(\n () => reject(new Error(`Timeout after ${PROVIDER_TIMEOUT_MS}ms`)),\n PROVIDER_TIMEOUT_MS,\n ),\n );\n\n const startTime = performance.now();\n\n try {\n await Promise.race([testSignalingLayer(layer, props), timeout]);\n const duration = Math.round(performance.now() - startTime);\n\n return { url: props.url, ok: true, duration };\n }\n catch (error) {\n return {\n url: props.url,\n ok: false,\n error: (error as Error).message,\n };\n }\n};\n\nconst formatResults = (typeName: string, results: ProviderResult[]): string => {\n const passed = results.filter(r => r.ok).length;\n const total = results.length;\n\n return [\n `${typeName.toUpperCase()}: ${passed}/${total}`,\n ...results.map(r =>\n (r.ok ? `✓ ${r.url} ${r.duration}ms` : `✗ ${r.url} - ${r.error}`),\n ),\n ].join(\"\\n\");\n};\n\ndescribe.for(providersByType)(\"signaling: %s\", ([typeName, providers]) => {\n it(\n \"should pass signaling messages between peers\",\n { timeout: PROVIDER_TIMEOUT_MS * providers.length + 5000 },\n async () => {\n const results = await Promise.all(\n providers.map(([, layer, props]) => testProvider(layer, props)),\n );\n\n const summary = formatResults(typeName, results);\n\n console.log(`\\n${summary}`);\n\n const passed = results.filter(r => r.ok).length;\n\n expect(passed).toBeGreaterThan(0);\n },\n );\n});\n"]}
@@ -10,12 +10,42 @@ export type SignalMessageBase<T extends string, P> = {
10
10
  export type SignalMessageFlash = SignalMessageBase<"flash", object>;
11
11
  export type SignalMessagePubkey = SignalMessageBase<"pubkey", {
12
12
  publicKey: string;
13
- dAppInfo?: {
14
- name: string;
15
- url: string;
16
- icon: string;
17
- };
18
13
  }>;
19
- export type SignalMessageAck = SignalMessageBase<"ack", undefined>;
14
+ /** Optional self-description shown in the other peer's UI. */
15
+ export type PeerInfo = {
16
+ /** Reverse-DNS application identifier, e.g. "com.example.wallet". */
17
+ identity: string;
18
+ name: string;
19
+ /**
20
+ * A data URI or an image URL. Only size is enforced on the wire — anyone
21
+ * rendering or fetching it is responsible for sanity-checking it first.
22
+ */
23
+ icon?: string;
24
+ };
25
+ export type PeerCapabilities = {
26
+ /** Supported transport identifiers, in preference order. */
27
+ transports: string[];
28
+ info?: PeerInfo;
29
+ };
30
+ /**
31
+ * Completes the handshake: acknowledges the previous step, negotiates the
32
+ * transport, and shares peer metadata in a single packet.
33
+ */
34
+ export type SignalMessageCapabilities = SignalMessageBase<"capabilities", PeerCapabilities>;
20
35
  export type SignalMessageData = SignalMessageBase<"data", object>;
21
- export type SignalMessage = SignalMessageFlash | SignalMessagePubkey | SignalMessageAck | SignalMessageData;
36
+ export type SignalMessage = SignalMessageFlash | SignalMessagePubkey | SignalMessageCapabilities | SignalMessageData;
37
+ export declare const MAX_ICON_LENGTH = 8192;
38
+ /**
39
+ * Check outgoing peer info against the wire limits enforced by receivers.
40
+ * Returns a human-readable problem, or undefined when the info is sendable.
41
+ * Senders must use this: a receiver silently drops an oversized packet, which
42
+ * would otherwise surface only as a generic handshake timeout.
43
+ */
44
+ export declare const validatePeerInfo: (info: PeerInfo) => string | undefined;
45
+ export declare const parseCapabilities: (raw: unknown) => PeerCapabilities | undefined;
46
+ /**
47
+ * Parse and shape-check a decrypted signaling payload. The relay topic is
48
+ * public, so decrypted JSON is still untrusted input: anything that does not
49
+ * match one of the four message shapes is dropped (returns undefined).
50
+ */
51
+ export declare const parseSignalMessage: (raw: string) => SignalMessage | undefined;