@nerxa/socketon 0.0.0-stage → 1.21.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.
Files changed (289) hide show
  1. package/EXPERIMENTAL.md +350 -0
  2. package/LICENSE +24 -0
  3. package/README.md +6317 -2
  4. package/WAProto/index.d.ts +17301 -0
  5. package/WAProto/index.js +125088 -0
  6. package/engine-requirements.js +14 -0
  7. package/package.json +100 -4
  8. package/src/Defaults/index.d.ts +152 -0
  9. package/src/Defaults/index.js +164 -0
  10. package/src/MessageBuilder/bot-signature.d.ts +22 -0
  11. package/src/MessageBuilder/bot-signature.js +122 -0
  12. package/src/MessageBuilder/extras.d.ts +449 -0
  13. package/src/MessageBuilder/extras.js +1356 -0
  14. package/src/MessageBuilder/index.d.ts +210 -0
  15. package/src/MessageBuilder/index.js +3594 -0
  16. package/src/MessageBuilder/metaai.d.ts +438 -0
  17. package/src/MessageBuilder/metaai.js +890 -0
  18. package/src/Signal/Group/ciphertext-message.d.ts +9 -0
  19. package/src/Signal/Group/ciphertext-message.js +11 -0
  20. package/src/Signal/Group/group-session-builder.d.ts +7 -0
  21. package/src/Signal/Group/group-session-builder.js +27 -0
  22. package/src/Signal/Group/group_cipher.d.ts +11 -0
  23. package/src/Signal/Group/group_cipher.js +78 -0
  24. package/src/Signal/Group/index.d.ts +11 -0
  25. package/src/Signal/Group/index.js +11 -0
  26. package/src/Signal/Group/keyhelper.d.ts +6 -0
  27. package/src/Signal/Group/keyhelper.js +17 -0
  28. package/src/Signal/Group/sender-chain-key.d.ts +13 -0
  29. package/src/Signal/Group/sender-chain-key.js +25 -0
  30. package/src/Signal/Group/sender-key-distribution-message.d.ts +16 -0
  31. package/src/Signal/Group/sender-key-distribution-message.js +62 -0
  32. package/src/Signal/Group/sender-key-message.d.ts +18 -0
  33. package/src/Signal/Group/sender-key-message.js +65 -0
  34. package/src/Signal/Group/sender-key-name.d.ts +11 -0
  35. package/src/Signal/Group/sender-key-name.js +47 -0
  36. package/src/Signal/Group/sender-key-record.d.ts +12 -0
  37. package/src/Signal/Group/sender-key-record.js +40 -0
  38. package/src/Signal/Group/sender-key-state.d.ts +16 -0
  39. package/src/Signal/Group/sender-key-state.js +83 -0
  40. package/src/Signal/Group/sender-message-key.d.ts +11 -0
  41. package/src/Signal/Group/sender-message-key.js +25 -0
  42. package/src/Signal/libsignal.d.ts +54 -0
  43. package/src/Signal/libsignal.js +427 -0
  44. package/src/Signal/lid-mapping.d.ts +20 -0
  45. package/src/Signal/lid-mapping.js +274 -0
  46. package/src/Socket/Client/index.d.ts +2 -0
  47. package/src/Socket/Client/index.js +3 -0
  48. package/src/Socket/Client/tcp.js +177 -0
  49. package/src/Socket/Client/types.d.ts +5 -0
  50. package/src/Socket/Client/types.js +9 -0
  51. package/src/Socket/Client/websocket.d.ts +12 -0
  52. package/src/Socket/Client/websocket.js +53 -0
  53. package/src/Socket/business.d.ts +431 -0
  54. package/src/Socket/business.js +379 -0
  55. package/src/Socket/chats.d.ts +247 -0
  56. package/src/Socket/chats.js +1484 -0
  57. package/src/Socket/communities.d.ts +598 -0
  58. package/src/Socket/communities.js +421 -0
  59. package/src/Socket/email.js +145 -0
  60. package/src/Socket/groups.d.ts +298 -0
  61. package/src/Socket/groups.js +339 -0
  62. package/src/Socket/index.d.ts +581 -0
  63. package/src/Socket/index.js +37 -0
  64. package/src/Socket/messages-recv.d.ts +370 -0
  65. package/src/Socket/messages-recv.js +1864 -0
  66. package/src/Socket/messages-send.d.ts +390 -0
  67. package/src/Socket/messages-send.js +1574 -0
  68. package/src/Socket/mex.d.ts +1 -0
  69. package/src/Socket/mex.js +44 -0
  70. package/src/Socket/mobile-builders.js +72 -0
  71. package/src/Socket/mobile.js +424 -0
  72. package/src/Socket/newsletter.d.ts +468 -0
  73. package/src/Socket/newsletter.js +671 -0
  74. package/src/Socket/socket.d.ts +77 -0
  75. package/src/Socket/socket.js +1106 -0
  76. package/src/Store/index.d.ts +3 -0
  77. package/src/Store/index.js +3 -0
  78. package/src/Store/make-in-memory-store.d.ts +62 -0
  79. package/src/Store/make-in-memory-store.js +396 -0
  80. package/src/Store/make-ordered-dictionary.d.ts +12 -0
  81. package/src/Store/make-ordered-dictionary.js +78 -0
  82. package/src/Store/object-repository.d.ts +10 -0
  83. package/src/Store/object-repository.js +23 -0
  84. package/src/Types/Auth.d.ts +1 -0
  85. package/src/Types/Auth.js +1 -0
  86. package/src/Types/Bussines.d.ts +1 -0
  87. package/src/Types/Bussines.js +1 -0
  88. package/src/Types/Call.d.ts +1 -0
  89. package/src/Types/Call.js +1 -0
  90. package/src/Types/Chat.d.ts +2 -0
  91. package/src/Types/Chat.js +24 -0
  92. package/src/Types/Contact.d.ts +1 -0
  93. package/src/Types/Contact.js +1 -0
  94. package/src/Types/Events.d.ts +1 -0
  95. package/src/Types/Events.js +0 -0
  96. package/src/Types/GroupMetadata.d.ts +5 -0
  97. package/src/Types/GroupMetadata.js +6 -0
  98. package/src/Types/Label.d.ts +2 -0
  99. package/src/Types/Label.js +24 -0
  100. package/src/Types/LabelAssociation.d.ts +2 -0
  101. package/src/Types/LabelAssociation.js +6 -0
  102. package/src/Types/Message.d.ts +13 -0
  103. package/src/Types/Message.js +23 -0
  104. package/src/Types/Mex.d.ts +2 -0
  105. package/src/Types/Mex.js +95 -0
  106. package/src/Types/Product.d.ts +1 -0
  107. package/src/Types/Product.js +1 -0
  108. package/src/Types/Signal.d.ts +1 -0
  109. package/src/Types/Signal.js +0 -0
  110. package/src/Types/Socket.d.ts +1 -0
  111. package/src/Types/Socket.js +0 -0
  112. package/src/Types/State.d.ts +5 -0
  113. package/src/Types/State.js +54 -0
  114. package/src/Types/USync.d.ts +1 -0
  115. package/src/Types/USync.js +0 -0
  116. package/src/Types/index.d.ts +13 -0
  117. package/src/Types/index.js +25 -0
  118. package/src/Utils/auth-store.d.ts +29 -0
  119. package/src/Utils/auth-store.js +76 -0
  120. package/src/Utils/auth-utils.d.ts +58 -0
  121. package/src/Utils/auth-utils.js +273 -0
  122. package/src/Utils/browser-utils.d.ts +9 -0
  123. package/src/Utils/browser-utils.js +28 -0
  124. package/src/Utils/business.d.ts +49 -0
  125. package/src/Utils/business.js +226 -0
  126. package/src/Utils/chat-utils.d.ts +408 -0
  127. package/src/Utils/chat-utils.js +846 -0
  128. package/src/Utils/companion-reg-client-utils.d.ts +4 -0
  129. package/src/Utils/companion-reg-client-utils.js +39 -0
  130. package/src/Utils/crypto.d.ts +39 -0
  131. package/src/Utils/crypto.js +110 -0
  132. package/src/Utils/decode-wa-message.d.ts +99 -0
  133. package/src/Utils/decode-wa-message.js +373 -0
  134. package/src/Utils/event-buffer.d.ts +22 -0
  135. package/src/Utils/event-buffer.js +624 -0
  136. package/src/Utils/generics.d.ts +63 -0
  137. package/src/Utils/generics.js +375 -0
  138. package/src/Utils/group-metadata-cache.d.ts +4 -0
  139. package/src/Utils/group-metadata-cache.js +25 -0
  140. package/src/Utils/history.d.ts +75 -0
  141. package/src/Utils/history.js +131 -0
  142. package/src/Utils/html-app.d.ts +19 -0
  143. package/src/Utils/html-app.js +96 -0
  144. package/src/Utils/html-multiplayer.d.ts +10 -0
  145. package/src/Utils/html-multiplayer.js +94 -0
  146. package/src/Utils/identity-change-handler.d.ts +13 -0
  147. package/src/Utils/identity-change-handler.js +48 -0
  148. package/src/Utils/index.d.ts +44 -0
  149. package/src/Utils/index.js +44 -0
  150. package/src/Utils/link-preview-metadata.d.ts +47 -0
  151. package/src/Utils/link-preview-metadata.js +88 -0
  152. package/src/Utils/link-preview.d.ts +12 -0
  153. package/src/Utils/link-preview.js +79 -0
  154. package/src/Utils/logger.d.ts +2 -0
  155. package/src/Utils/logger.js +2 -0
  156. package/src/Utils/lt-hash.d.ts +6 -0
  157. package/src/Utils/lt-hash.js +3 -0
  158. package/src/Utils/make-mutex.d.ts +6 -0
  159. package/src/Utils/make-mutex.js +32 -0
  160. package/src/Utils/message-retry-manager.d.ts +79 -0
  161. package/src/Utils/message-retry-manager.js +255 -0
  162. package/src/Utils/messages-media.d.ts +136 -0
  163. package/src/Utils/messages-media.js +814 -0
  164. package/src/Utils/messages.d.ts +62 -0
  165. package/src/Utils/messages.js +2223 -0
  166. package/src/Utils/mobile-companion-crypto.js +191 -0
  167. package/src/Utils/modern-messages.d.ts +150 -0
  168. package/src/Utils/modern-messages.js +496 -0
  169. package/src/Utils/native-flow.d.ts +11 -0
  170. package/src/Utils/native-flow.js +115 -0
  171. package/src/Utils/newsletter-status.d.ts +158 -0
  172. package/src/Utils/newsletter-status.js +651 -0
  173. package/src/Utils/noise-handler.d.ts +20 -0
  174. package/src/Utils/noise-handler.js +200 -0
  175. package/src/Utils/offline-node-processor.d.ts +9 -0
  176. package/src/Utils/offline-node-processor.js +33 -0
  177. package/src/Utils/optional-media.d.ts +3 -0
  178. package/src/Utils/optional-media.js +45 -0
  179. package/src/Utils/payment-messages.d.ts +66 -0
  180. package/src/Utils/payment-messages.js +160 -0
  181. package/src/Utils/pre-key-manager.d.ts +25 -0
  182. package/src/Utils/pre-key-manager.js +95 -0
  183. package/src/Utils/process-message.d.ts +60 -0
  184. package/src/Utils/process-message.js +688 -0
  185. package/src/Utils/reporting-utils.d.ts +12 -0
  186. package/src/Utils/reporting-utils.js +256 -0
  187. package/src/Utils/scheduled-message.d.ts +51 -0
  188. package/src/Utils/scheduled-message.js +112 -0
  189. package/src/Utils/sender-key-memory.d.ts +19 -0
  190. package/src/Utils/sender-key-memory.js +51 -0
  191. package/src/Utils/signal.d.ts +82 -0
  192. package/src/Utils/signal.js +193 -0
  193. package/src/Utils/stanza-ack.d.ts +16 -0
  194. package/src/Utils/stanza-ack.js +30 -0
  195. package/src/Utils/status-stickers.d.ts +127 -0
  196. package/src/Utils/status-stickers.js +260 -0
  197. package/src/Utils/sync-action-utils.d.ts +2 -0
  198. package/src/Utils/sync-action-utils.js +44 -0
  199. package/src/Utils/tc-token-utils.d.ts +29 -0
  200. package/src/Utils/tc-token-utils.js +155 -0
  201. package/src/Utils/ttl-cache.d.ts +28 -0
  202. package/src/Utils/ttl-cache.js +120 -0
  203. package/src/Utils/use-mongo-auth-state.d.ts +21 -0
  204. package/src/Utils/use-mongo-auth-state.js +67 -0
  205. package/src/Utils/use-multi-file-auth-state.d.ts +11 -0
  206. package/src/Utils/use-multi-file-auth-state.js +106 -0
  207. package/src/Utils/use-mysql-auth-state.d.ts +19 -0
  208. package/src/Utils/use-mysql-auth-state.js +79 -0
  209. package/src/Utils/use-postgres-auth-state.d.ts +19 -0
  210. package/src/Utils/use-postgres-auth-state.js +79 -0
  211. package/src/Utils/use-redis-auth-state.d.ts +19 -0
  212. package/src/Utils/use-redis-auth-state.js +89 -0
  213. package/src/Utils/use-single-file-auth-state.d.ts +10 -0
  214. package/src/Utils/use-single-file-auth-state.js +108 -0
  215. package/src/Utils/use-sqlite-auth-state.d.ts +12 -0
  216. package/src/Utils/use-sqlite-auth-state.js +120 -0
  217. package/src/Utils/useNekoDBAuth.d.ts +19 -0
  218. package/src/Utils/useNekoDBAuth.js +111 -0
  219. package/src/Utils/username.d.ts +12 -0
  220. package/src/Utils/username.js +58 -0
  221. package/src/Utils/validate-connection.d.ts +43 -0
  222. package/src/Utils/validate-connection.js +277 -0
  223. package/src/Utils/voice-recognition.d.ts +53 -0
  224. package/src/Utils/voice-recognition.js +219 -0
  225. package/src/Voip/audio-feeder.d.ts +24 -0
  226. package/src/Voip/audio-feeder.js +268 -0
  227. package/src/Voip/index.d.ts +88 -0
  228. package/src/Voip/index.js +936 -0
  229. package/src/Voip/relay-transport.d.ts +5 -0
  230. package/src/Voip/relay-transport.js +824 -0
  231. package/src/Voip/signaling.d.ts +11 -0
  232. package/src/Voip/signaling.js +1099 -0
  233. package/src/Voip/types.d.ts +2 -0
  234. package/src/Voip/types.js +12 -0
  235. package/src/Voip/video-feeder.d.ts +24 -0
  236. package/src/Voip/video-feeder.js +268 -0
  237. package/src/Voip/wasm-engine.d.ts +26 -0
  238. package/src/Voip/wasm-engine.js +1307 -0
  239. package/src/Voip/worker-bootstrap.js +1023 -0
  240. package/src/WABinary/constants.d.ts +22 -0
  241. package/src/WABinary/constants.js +1300 -0
  242. package/src/WABinary/decode.d.ts +9 -0
  243. package/src/WABinary/decode.js +261 -0
  244. package/src/WABinary/encode.d.ts +2 -0
  245. package/src/WABinary/encode.js +219 -0
  246. package/src/WABinary/generic-utils.d.ts +76 -0
  247. package/src/WABinary/generic-utils.js +214 -0
  248. package/src/WABinary/index.d.ts +5 -0
  249. package/src/WABinary/index.js +5 -0
  250. package/src/WABinary/jid-utils.d.ts +30 -0
  251. package/src/WABinary/jid-utils.js +97 -0
  252. package/src/WABinary/types.d.ts +1 -0
  253. package/src/WABinary/types.js +0 -0
  254. package/src/WAM/BinaryInfo.d.ts +7 -0
  255. package/src/WAM/BinaryInfo.js +9 -0
  256. package/src/WAM/constants.d.ts +34926 -0
  257. package/src/WAM/constants.js +22852 -0
  258. package/src/WAM/encode.d.ts +1 -0
  259. package/src/WAM/encode.js +148 -0
  260. package/src/WAM/index.d.ts +3 -0
  261. package/src/WAM/index.js +3 -0
  262. package/src/WAUSync/Protocols/USyncContactProtocol.d.ts +36 -0
  263. package/src/WAUSync/Protocols/USyncContactProtocol.js +50 -0
  264. package/src/WAUSync/Protocols/USyncDeviceProtocol.d.ts +22 -0
  265. package/src/WAUSync/Protocols/USyncDeviceProtocol.js +51 -0
  266. package/src/WAUSync/Protocols/USyncDisappearingModeProtocol.d.ts +12 -0
  267. package/src/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -0
  268. package/src/WAUSync/Protocols/USyncStatusProtocol.d.ts +12 -0
  269. package/src/WAUSync/Protocols/USyncStatusProtocol.js +37 -0
  270. package/src/WAUSync/Protocols/USyncUsernameProtocol.d.ts +9 -0
  271. package/src/WAUSync/Protocols/USyncUsernameProtocol.js +23 -0
  272. package/src/WAUSync/Protocols/UsyncBotProfileProtocol.d.ts +38 -0
  273. package/src/WAUSync/Protocols/UsyncBotProfileProtocol.js +49 -0
  274. package/src/WAUSync/Protocols/UsyncLIDProtocol.d.ts +14 -0
  275. package/src/WAUSync/Protocols/UsyncLIDProtocol.js +28 -0
  276. package/src/WAUSync/Protocols/index.d.ts +5 -0
  277. package/src/WAUSync/Protocols/index.js +5 -0
  278. package/src/WAUSync/USyncQuery.d.ts +20 -0
  279. package/src/WAUSync/USyncQuery.js +93 -0
  280. package/src/WAUSync/USyncUser.d.ts +16 -0
  281. package/src/WAUSync/USyncUser.js +30 -0
  282. package/src/WAUSync/index.d.ts +3 -0
  283. package/src/WAUSync/index.js +3 -0
  284. package/src/assets/wasm/README.md +76 -0
  285. package/src/assets/wasm/loader.js +5 -0
  286. package/src/assets/wasm/whatsapp.wasm +0 -0
  287. package/src/assets/wasm/worker-modules.js +273 -0
  288. package/src/index.d.ts +43 -0
  289. package/src/index.js +48 -0
@@ -0,0 +1,824 @@
1
+ import { appendFileSync } from 'node:fs';
2
+
3
+ const RELAY_PROTO_UDP = 0;
4
+ const FAUX_WEB_CLIENT_RELAY_PORT = 3478;
5
+ const TRUE_WEB_CLIENT_RELAY_PORT = 3480;
6
+ const CONNECTION_TIMEOUT_MS = 20000;
7
+ const ICE_RESTART_IDLE_THRESHOLD_MS = 10000;
8
+ const ICE_RTT_POLL_MS = 1000;
9
+ const MAX_BUFFER_SIZE = 256 * 1024;
10
+
11
+ const RELAY_PACKET_LOG_PATH = process.env.CALL_DUMP_RELAY_PACKETS_PATH ?? "";
12
+ const DISABLE_IPV6 = process.env.CALL_DISABLE_IPV6 !== "0";
13
+ const RELAY_PORT_MODE = process.env.CALL_RELAY_PORT_MODE === "web" ? "web" : "original";
14
+ const USE_ORIGINAL_RELAY_PORTS = RELAY_PORT_MODE === "original";
15
+
16
+ const RELAY_DTLS_FINGERPRINT = "F9:CA:0C:98:A3:CC:71:D6:42:CE:5A:E2:53:D2:15:20:D3:1B:BA:D8:57:A4:F0:AF:BE:0B:FB:F3:6B:0C:A0:68";
17
+
18
+ const getConnectionIdentifier = (ip, port) => ip.includes(":") ? `[${ip}]:${port}` : `${ip}:${port}`;
19
+
20
+ const createEmptyStats = () => ({
21
+ sentPackets: 0,
22
+ receivedPackets: 0,
23
+ sentBytes: 0,
24
+ receivedBytes: 0,
25
+ droppedPackets: 0,
26
+ openConnections: 0
27
+ });
28
+
29
+ const getRelayLookupId = (ip, port) => getConnectionIdentifier(
30
+ ip,
31
+ port === TRUE_WEB_CLIENT_RELAY_PORT ? TRUE_WEB_CLIENT_RELAY_PORT : port
32
+ );
33
+
34
+ const getRtcConnectPort = info => info.ip === "157.240.24.133" ? FAUX_WEB_CLIENT_RELAY_PORT : info.port;
35
+
36
+ const getVoipStackPort = info => {
37
+ if (USE_ORIGINAL_RELAY_PORTS) {
38
+ return info.originalPort || info.port;
39
+ }
40
+
41
+ const sourcePort = info.originalPort || info.port;
42
+
43
+ return sourcePort === TRUE_WEB_CLIENT_RELAY_PORT
44
+ ? TRUE_WEB_CLIENT_RELAY_PORT
45
+ : FAUX_WEB_CLIENT_RELAY_PORT;
46
+ };
47
+
48
+ const bufferPacket = (connection, packet) => {
49
+ if (packet.byteLength > MAX_BUFFER_SIZE) {
50
+ connection.stats.droppedPackets += 1;
51
+ return false;
52
+ }
53
+
54
+ while (
55
+ connection.packetBuffer.length > 0 &&
56
+ connection.bufferedBytes + packet.byteLength > MAX_BUFFER_SIZE
57
+ ) {
58
+ const dropped = connection.packetBuffer.shift();
59
+
60
+ if (dropped) {
61
+ connection.bufferedBytes -= dropped.byteLength;
62
+ connection.stats.droppedPackets += 1;
63
+ }
64
+ }
65
+
66
+ connection.packetBuffer.push(packet);
67
+ connection.bufferedBytes += packet.byteLength;
68
+
69
+ return true;
70
+ };
71
+
72
+ const shiftPacket = connection => {
73
+ const packet = connection.packetBuffer.shift() ?? null;
74
+
75
+ if (packet) {
76
+ connection.bufferedBytes -= packet.byteLength;
77
+ }
78
+
79
+ return packet;
80
+ };
81
+
82
+ const replaceIceCredentials = (sdp, ufrag, pwd) => sdp
83
+ .replace(/a=ice-ufrag:[^\r\n]+/g, `a=ice-ufrag:${ufrag}`)
84
+ .replace(/a=ice-pwd:[^\r\n]+/g, `a=ice-pwd:${pwd}`);
85
+
86
+ const replaceDtlsFingerprint = (sdp, algorithm, fingerprint) => sdp
87
+ .replace(/a=fingerprint:[^\r\n]+/g, `a=fingerprint:${algorithm} ${fingerprint}`);
88
+
89
+ const removeIceCandidates = sdp => sdp
90
+ .replace(/a=candidate:[^\r\n]+\r?\n/g, "")
91
+ .replace(/a=end-of-candidates\r?\n?/g, "");
92
+
93
+ const appendRelayCandidate = (sdp, ip, port) => {
94
+ const candidate = `a=candidate:2 1 udp 2122262783 ${ip} ${port} typ host generation 0 network-cost 5`;
95
+
96
+ return `${removeIceCandidates(sdp)}${candidate}\r\na=end-of-candidates\r\n`;
97
+ };
98
+
99
+ const buildRemoteRelayAnswer = (offerSdp, info) => {
100
+ const setupLine = info.enableEdgerayDtlsActiveMode ? "a=setup:active" : "a=setup:passive";
101
+
102
+ let answerSdp = offerSdp.replace(/a=setup:actpass/g, setupLine);
103
+
104
+ answerSdp = replaceIceCredentials(answerSdp, info.authToken ?? info.token, info.key);
105
+ answerSdp = replaceDtlsFingerprint(answerSdp, "sha-256", RELAY_DTLS_FINGERPRINT);
106
+ answerSdp = answerSdp.replace(/a=ice-options:[^\r\n]+\r\n/g, "");
107
+ answerSdp = answerSdp.replace(/a=max-message-size:[^\r\n]+/g, "a=max-message-size:1200");
108
+
109
+ return appendRelayCandidate(answerSdp, info.ip, info.port);
110
+ };
111
+
112
+ const toArrayBuffer = data => {
113
+ const copy = new Uint8Array(data.byteLength);
114
+ copy.set(data);
115
+
116
+ return copy.buffer;
117
+ };
118
+
119
+ const toUint8Array = data => {
120
+ if (data instanceof Uint8Array) {
121
+ return new Uint8Array(data);
122
+ }
123
+
124
+ if (Buffer.isBuffer(data)) {
125
+ return new Uint8Array(data);
126
+ }
127
+
128
+ if (data instanceof ArrayBuffer) {
129
+ return new Uint8Array(data);
130
+ }
131
+
132
+ if (data && typeof data === "object" && "byteLength" in data && "buffer" in data) {
133
+ const view = data;
134
+
135
+ return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
136
+ }
137
+
138
+ return null;
139
+ };
140
+
141
+ const classifyRelayPacket = packet => {
142
+ if (packet.byteLength < 2) {
143
+ return "non_stun";
144
+ }
145
+
146
+ const bytes = new Uint8Array(packet);
147
+ const first = bytes[0];
148
+ const second = bytes[1];
149
+
150
+ if ((first & 0xc0) !== 0) {
151
+ return "non_stun";
152
+ }
153
+
154
+ const stunType = ((first & 0x3f) << 8) | second;
155
+
156
+ if (stunType === 0x0001) {
157
+ return "stun_bind";
158
+ }
159
+
160
+ if (stunType === 0x0003) {
161
+ return "stun_alloc";
162
+ }
163
+
164
+ return "stun_unknown";
165
+ };
166
+
167
+ const appendRelayPacketLog = (direction, connection, packet) => {
168
+ if (!RELAY_PACKET_LOG_PATH) {
169
+ return;
170
+ }
171
+
172
+ const buffer = packet instanceof ArrayBuffer
173
+ ? Buffer.from(new Uint8Array(packet))
174
+ : Buffer.from(packet.buffer, packet.byteOffset, packet.byteLength);
175
+
176
+ appendFileSync(
177
+ RELAY_PACKET_LOG_PATH,
178
+ JSON.stringify({
179
+ ts: new Date().toISOString(),
180
+ direction,
181
+ id: connection.info.id,
182
+ relayId: connection.info.relayId,
183
+ relayName: connection.info.name,
184
+ ip: connection.info.ip,
185
+ rtcPort: connection.info.port,
186
+ voipPort: getVoipStackPort(connection.info),
187
+ size: buffer.byteLength,
188
+ hex: buffer.toString("hex")
189
+ }) + "\n"
190
+ );
191
+ };
192
+
193
+ export class RelayRtcTransport {
194
+ constructor(config) {
195
+ this.config = config;
196
+ this._relayInfoById = new Map();
197
+ this._connections = new Map();
198
+ this._totals = createEmptyStats();
199
+ this._wrtcPromise = null;
200
+
201
+ this.updateRelayList = update => {
202
+ const nextInfoById = new Map();
203
+
204
+ for (const relay of update.relays ?? []) {
205
+ if (
206
+ relay.token_id == null ||
207
+ relay.token_id < 0 ||
208
+ relay.token_id >= (update.relay_tokens ?? []).length
209
+ ) {
210
+ continue;
211
+ }
212
+
213
+ const authToken = update.auth_tokens && relay.auth_token_id != null && relay.auth_token_id >= 0
214
+ ? update.auth_tokens[relay.auth_token_id]
215
+ : undefined;
216
+
217
+ for (const address of relay.addresses ?? []) {
218
+ if (address.protocol !== RELAY_PROTO_UDP) {
219
+ continue;
220
+ }
221
+
222
+ if (address.ipv4 && address.port != null) {
223
+ const clientPort = USE_ORIGINAL_RELAY_PORTS ? address.port : TRUE_WEB_CLIENT_RELAY_PORT;
224
+ const id = getConnectionIdentifier(address.ipv4, clientPort);
225
+
226
+ nextInfoById.set(id, {
227
+ id,
228
+ relayId: relay.relay_id,
229
+ ip: address.ipv4,
230
+ port: clientPort,
231
+ originalPort: address.port,
232
+ isIPv6: false,
233
+ token: update.relay_tokens[relay.token_id],
234
+ authToken,
235
+ key: update.relay_key,
236
+ name: relay.relay_name,
237
+ enableEdgerayDtlsActiveMode: update.enable_edgeray_dtls_active_mode === true
238
+ });
239
+ }
240
+
241
+ if (!DISABLE_IPV6 && address.ipv6 && address.port_v6 != null) {
242
+ const clientPort = USE_ORIGINAL_RELAY_PORTS ? address.port_v6 : TRUE_WEB_CLIENT_RELAY_PORT;
243
+ const id = getConnectionIdentifier(address.ipv6, clientPort);
244
+
245
+ nextInfoById.set(id, {
246
+ id,
247
+ relayId: relay.relay_id,
248
+ ip: address.ipv6,
249
+ port: clientPort,
250
+ originalPort: address.port_v6,
251
+ isIPv6: true,
252
+ token: update.relay_tokens[relay.token_id],
253
+ authToken,
254
+ key: update.relay_key,
255
+ name: relay.relay_name,
256
+ enableEdgerayDtlsActiveMode: update.enable_edgeray_dtls_active_mode === true
257
+ });
258
+ }
259
+ }
260
+ }
261
+
262
+ for (const id of this._relayInfoById.keys()) {
263
+ if (!nextInfoById.has(id)) {
264
+ this._closeConnection(id);
265
+ }
266
+ }
267
+
268
+ this._relayInfoById.clear();
269
+
270
+ for (const [id, info] of nextInfoById) {
271
+ this._relayInfoById.set(id, info);
272
+ void this._ensureConnection(info);
273
+ }
274
+ };
275
+
276
+ this.send = (packet, ip, port) => {
277
+ const requestedId = getConnectionIdentifier(ip, port);
278
+ const preferredPort = USE_ORIGINAL_RELAY_PORTS ? port : TRUE_WEB_CLIENT_RELAY_PORT;
279
+
280
+ const candidateIds = [
281
+ requestedId,
282
+ getConnectionIdentifier(ip, preferredPort),
283
+ getRelayLookupId(ip, TRUE_WEB_CLIENT_RELAY_PORT),
284
+ getRelayLookupId(ip, FAUX_WEB_CLIENT_RELAY_PORT)
285
+ ];
286
+
287
+ let info;
288
+
289
+ for (const candidateId of candidateIds) {
290
+ info = this._relayInfoById.get(candidateId);
291
+
292
+ if (info) {
293
+ break;
294
+ }
295
+ }
296
+
297
+ if (!info) {
298
+ if (DISABLE_IPV6 && ip.includes(":")) {
299
+ return 0;
300
+ }
301
+
302
+ const earlyConnection = this._getOrCreateEarlyConnection(ip, port);
303
+ bufferPacket(earlyConnection, toArrayBuffer(packet));
304
+
305
+ return packet.byteLength;
306
+ }
307
+
308
+ const connection = this._getOrCreateConnection(info);
309
+ const arrayBuffer = toArrayBuffer(packet);
310
+
311
+ if (
312
+ classifyRelayPacket(arrayBuffer) === "stun_alloc" &&
313
+ connection.state === "open" &&
314
+ connection.sentMedia &&
315
+ Date.now() - connection.lastRxPacketTime > ICE_RESTART_IDLE_THRESHOLD_MS
316
+ ) {
317
+ connection.packetBuffer = [];
318
+ connection.bufferedBytes = 0;
319
+ bufferPacket(connection, arrayBuffer);
320
+ void this._restartIce(connection);
321
+
322
+ return packet.byteLength;
323
+ }
324
+
325
+ if (connection.state === "open" && connection.dataChannel?.readyState === "open") {
326
+ if (!this._sendBufferedPacket(connection, arrayBuffer)) {
327
+ connection.stats.droppedPackets += 1;
328
+ }
329
+
330
+ return packet.byteLength;
331
+ }
332
+
333
+ bufferPacket(connection, arrayBuffer);
334
+ void this._ensureConnection(info);
335
+
336
+ return packet.byteLength;
337
+ };
338
+
339
+ this.getStats = () => {
340
+ let openConnections = 0;
341
+
342
+ for (const connection of this._connections.values()) {
343
+ if (connection.state === "open") {
344
+ openConnections += 1;
345
+ }
346
+ }
347
+
348
+ return {
349
+ ...this._totals,
350
+ openConnections
351
+ };
352
+ };
353
+
354
+ this.closeAll = async () => {
355
+ for (const id of [...this._connections.keys()]) {
356
+ this._closeConnection(id);
357
+ }
358
+ };
359
+
360
+ this._getOrCreateConnection = info => {
361
+ const existing = this._connections.get(info.id);
362
+
363
+ if (existing) {
364
+ existing.info = info;
365
+ return existing;
366
+ }
367
+
368
+ const created = {
369
+ info,
370
+ state: "none",
371
+ peerConnection: null,
372
+ dataChannel: null,
373
+ iceCandidate: null,
374
+ packetBuffer: [],
375
+ bufferedBytes: 0,
376
+ connectPromise: null,
377
+ connectionTimeout: null,
378
+ iceStatsInterval: null,
379
+ lastIceRttMs: null,
380
+ hasReceivedFirstPacket: false,
381
+ hasNonStunPacketSent: false,
382
+ sentMedia: false,
383
+ lastRxPacketTime: 0,
384
+ isReconnecting: false,
385
+ stats: createEmptyStats()
386
+ };
387
+
388
+ this._connections.set(info.id, created);
389
+
390
+ return created;
391
+ };
392
+
393
+ this._getOrCreateEarlyConnection = (ip, port) => {
394
+ const id = getConnectionIdentifier(ip, port);
395
+ const existing = this._connections.get(id);
396
+
397
+ if (existing) {
398
+ return existing;
399
+ }
400
+
401
+ const placeholderInfo = {
402
+ id,
403
+ relayId: 0,
404
+ ip,
405
+ port,
406
+ originalPort: port,
407
+ isIPv6: ip.includes(":"),
408
+ token: "",
409
+ key: "",
410
+ name: "early-packet",
411
+ enableEdgerayDtlsActiveMode: false
412
+ };
413
+
414
+ const created = {
415
+ info: placeholderInfo,
416
+ state: "none",
417
+ peerConnection: null,
418
+ dataChannel: null,
419
+ iceCandidate: null,
420
+ packetBuffer: [],
421
+ bufferedBytes: 0,
422
+ connectPromise: null,
423
+ connectionTimeout: null,
424
+ iceStatsInterval: null,
425
+ lastIceRttMs: null,
426
+ hasReceivedFirstPacket: false,
427
+ hasNonStunPacketSent: false,
428
+ sentMedia: false,
429
+ lastRxPacketTime: 0,
430
+ isReconnecting: false,
431
+ stats: createEmptyStats()
432
+ };
433
+
434
+ this._connections.set(id, created);
435
+
436
+ return created;
437
+ };
438
+
439
+ this._ensureConnection = async info => {
440
+ const connection = this._getOrCreateConnection(info);
441
+
442
+ if (connection.state === "open" || connection.state === "connecting") {
443
+ return connection.connectPromise ?? Promise.resolve();
444
+ }
445
+
446
+ const promise = this._connect(connection);
447
+ connection.connectPromise = promise;
448
+
449
+ try {
450
+ await promise;
451
+ } finally {
452
+ connection.connectPromise = null;
453
+ }
454
+ };
455
+
456
+ this._connect = async connection => {
457
+ const wrtcModule = await this._loadWrtc();
458
+ const { RTCPeerConnection } = wrtcModule;
459
+
460
+ if (typeof RTCPeerConnection !== "function") {
461
+ throw new Error("RTCPeerConnection unavailable from @roamhq/wrtc");
462
+ }
463
+
464
+ this._closePeerObjects(connection);
465
+
466
+ connection.state = "connecting";
467
+
468
+ const pc = new RTCPeerConnection();
469
+ const dc = pc.createDataChannel("pre-negotiated", {
470
+ negotiated: true,
471
+ id: 0,
472
+ ordered: false,
473
+ maxRetransmits: 0,
474
+ priority: "high"
475
+ });
476
+
477
+ connection.peerConnection = pc;
478
+ connection.dataChannel = dc;
479
+
480
+ dc.binaryType = "arraybuffer";
481
+
482
+ dc.onopen = () => {
483
+ connection.state = "open";
484
+ connection.isReconnecting = false;
485
+
486
+ if (connection.connectionTimeout) {
487
+ clearTimeout(connection.connectionTimeout);
488
+ connection.connectionTimeout = null;
489
+ }
490
+
491
+ this._flushBufferedPackets(connection);
492
+ this._startIceRttPolling(connection);
493
+ };
494
+
495
+ dc.onclose = () => {
496
+ if (connection.state !== "failed") {
497
+ connection.state = "closed";
498
+ }
499
+ };
500
+
501
+ dc.onerror = () => {
502
+ connection.state = "failed";
503
+ };
504
+
505
+ dc.onmessage = event => {
506
+ this._handleIncomingPacket(connection, event.data);
507
+ };
508
+
509
+ pc.onicecandidate = event => {
510
+ if (event.candidate?.candidate && !connection.iceCandidate) {
511
+ connection.iceCandidate = event.candidate.candidate;
512
+ }
513
+ };
514
+
515
+ pc.oniceconnectionstatechange = () => {
516
+ if (pc.iceConnectionState === "failed" || pc.iceConnectionState === "closed") {
517
+ connection.state = "failed";
518
+ }
519
+ };
520
+
521
+ connection.connectionTimeout = setTimeout(() => {
522
+ if (connection.state === "connecting") {
523
+ connection.state = "failed";
524
+ this._closePeerObjects(connection);
525
+ }
526
+ }, CONNECTION_TIMEOUT_MS);
527
+
528
+ const offer = await pc.createOffer();
529
+
530
+ await pc.setLocalDescription(offer);
531
+
532
+ const remoteSdp = buildRemoteRelayAnswer(offer.sdp ?? "", {
533
+ ...connection.info,
534
+ port: getRtcConnectPort(connection.info)
535
+ });
536
+
537
+ await pc.setRemoteDescription({
538
+ type: "answer",
539
+ sdp: remoteSdp
540
+ });
541
+ };
542
+
543
+ this._restartIce = async connection => {
544
+ if (connection.isReconnecting || !connection.hasNonStunPacketSent) {
545
+ return;
546
+ }
547
+
548
+ const { RTCPeerConnection } = await this._loadWrtc();
549
+
550
+ if (typeof RTCPeerConnection !== "function") {
551
+ return;
552
+ }
553
+
554
+ connection.isReconnecting = true;
555
+
556
+ try {
557
+ this._closePeerObjects(connection);
558
+ connection.state = "connecting";
559
+
560
+ const pc = new RTCPeerConnection();
561
+ const dc = pc.createDataChannel("pre-negotiated", {
562
+ negotiated: true,
563
+ id: 0,
564
+ ordered: false,
565
+ maxRetransmits: 0,
566
+ priority: "high"
567
+ });
568
+
569
+ connection.peerConnection = pc;
570
+ connection.dataChannel = dc;
571
+
572
+ dc.binaryType = "arraybuffer";
573
+
574
+ dc.onopen = () => {
575
+ connection.state = "open";
576
+ connection.isReconnecting = false;
577
+ this._flushBufferedPackets(connection);
578
+ this._startIceRttPolling(connection);
579
+ };
580
+
581
+ dc.onclose = () => {
582
+ if (connection.state !== "failed") {
583
+ connection.state = "closed";
584
+ }
585
+ };
586
+
587
+ dc.onerror = () => {
588
+ connection.state = "failed";
589
+ };
590
+
591
+ dc.onmessage = event => {
592
+ this._handleIncomingPacket(connection, event.data);
593
+ };
594
+
595
+ pc.onicecandidate = event => {
596
+ if (event.candidate?.candidate && !connection.iceCandidate) {
597
+ connection.iceCandidate = event.candidate.candidate;
598
+ }
599
+ };
600
+
601
+ const offer = await pc.createOffer({
602
+ iceRestart: false
603
+ });
604
+
605
+ let localSdp = offer.sdp ?? "";
606
+
607
+ if (connection.iceCandidate) {
608
+ localSdp = `${removeIceCandidates(localSdp)}a=${connection.iceCandidate}\r\na=end-of-candidates\r\n`;
609
+ }
610
+
611
+ await pc.setLocalDescription({
612
+ type: "offer",
613
+ sdp: localSdp
614
+ });
615
+
616
+ const remoteSdp = buildRemoteRelayAnswer(localSdp, {
617
+ ...connection.info,
618
+ port: getRtcConnectPort(connection.info)
619
+ });
620
+
621
+ await pc.setRemoteDescription({
622
+ type: "answer",
623
+ sdp: remoteSdp
624
+ });
625
+ } catch {
626
+ connection.state = "failed";
627
+ } finally {
628
+ connection.isReconnecting = false;
629
+ }
630
+ };
631
+
632
+ this._handleIncomingPacket = (connection, raw) => {
633
+ const packet = toUint8Array(raw);
634
+
635
+ if (!packet) {
636
+ return;
637
+ }
638
+
639
+ connection.stats.receivedPackets += 1;
640
+ connection.stats.receivedBytes += packet.byteLength;
641
+ connection.hasReceivedFirstPacket = true;
642
+ connection.lastRxPacketTime = Date.now();
643
+
644
+ this._totals.receivedPackets += 1;
645
+ this._totals.receivedBytes += packet.byteLength;
646
+
647
+ appendRelayPacketLog("recv", connection, packet);
648
+
649
+ this.config.onTransportMessage(packet, connection.info.ip, getVoipStackPort(connection.info));
650
+ };
651
+
652
+ this._flushBufferedPackets = connection => {
653
+ while (
654
+ connection.state === "open" &&
655
+ connection.dataChannel?.readyState === "open" &&
656
+ connection.packetBuffer.length > 0
657
+ ) {
658
+ const packet = shiftPacket(connection);
659
+
660
+ if (!packet) {
661
+ break;
662
+ }
663
+
664
+ if (!this._sendBufferedPacket(connection, packet)) {
665
+ connection.stats.droppedPackets += 1;
666
+ break;
667
+ }
668
+ }
669
+ };
670
+
671
+ this._sendBufferedPacket = (connection, packet) => {
672
+ try {
673
+ connection.dataChannel?.send(packet);
674
+
675
+ if (classifyRelayPacket(packet) === "non_stun") {
676
+ connection.hasNonStunPacketSent = true;
677
+ connection.sentMedia = true;
678
+ }
679
+
680
+ connection.stats.sentPackets += 1;
681
+ connection.stats.sentBytes += packet.byteLength;
682
+
683
+ this._totals.sentPackets += 1;
684
+ this._totals.sentBytes += packet.byteLength;
685
+
686
+ appendRelayPacketLog("send", connection, packet);
687
+
688
+ return true;
689
+ } catch {
690
+ return false;
691
+ }
692
+ };
693
+
694
+ this._startIceRttPolling = connection => {
695
+ this._stopIceRttPolling(connection);
696
+
697
+ const pc = connection.peerConnection;
698
+
699
+ if (!pc || typeof pc.getStats !== "function" || !this.config.onIceRtt) {
700
+ return;
701
+ }
702
+
703
+ const poll = async () => {
704
+ const rttMs = await this._readCurrentRoundTripTimeMs(pc);
705
+
706
+ if (rttMs == null || connection.lastIceRttMs === rttMs) {
707
+ return;
708
+ }
709
+
710
+ connection.lastIceRttMs = rttMs;
711
+ this.config.onIceRtt?.(rttMs, connection.info.ip, connection.info.port);
712
+ };
713
+
714
+ void poll();
715
+
716
+ connection.iceStatsInterval = setInterval(() => {
717
+ void poll();
718
+ }, ICE_RTT_POLL_MS);
719
+
720
+ connection.iceStatsInterval.unref?.();
721
+ };
722
+
723
+ this._stopIceRttPolling = connection => {
724
+ if (connection.iceStatsInterval) {
725
+ clearInterval(connection.iceStatsInterval);
726
+ connection.iceStatsInterval = null;
727
+ }
728
+
729
+ connection.lastIceRttMs = null;
730
+ };
731
+
732
+ this._readCurrentRoundTripTimeMs = async pc => {
733
+ try {
734
+ const stats = await pc.getStats();
735
+
736
+ const reports = stats && typeof stats.values === "function"
737
+ ? Array.from(stats.values())
738
+ : Array.isArray(stats)
739
+ ? stats
740
+ : Object.values(stats ?? {});
741
+
742
+ if (!reports.length) {
743
+ return null;
744
+ }
745
+
746
+ let selectedCandidatePairId = "";
747
+
748
+ for (const report of reports) {
749
+ if (report?.type === "transport" && typeof report.selectedCandidatePairId === "string") {
750
+ selectedCandidatePairId = report.selectedCandidatePairId;
751
+ break;
752
+ }
753
+ }
754
+
755
+ const candidatePairs = reports.filter(report => report?.type === "candidate-pair");
756
+
757
+ const selectedPair =
758
+ selectedCandidatePairId && candidatePairs.find(report => report?.id === selectedCandidatePairId) ||
759
+ candidatePairs.find(report => report?.selected || report?.nominated) ||
760
+ candidatePairs[0];
761
+
762
+ const rttSeconds = typeof selectedPair?.currentRoundTripTime === "number"
763
+ ? selectedPair.currentRoundTripTime
764
+ : typeof selectedPair?.totalRoundTripTime === "number" &&
765
+ typeof selectedPair?.responsesReceived === "number" &&
766
+ selectedPair.responsesReceived > 0
767
+ ? selectedPair.totalRoundTripTime / selectedPair.responsesReceived
768
+ : null;
769
+
770
+ if (rttSeconds == null || !Number.isFinite(rttSeconds) || rttSeconds <= 0) {
771
+ return null;
772
+ }
773
+
774
+ return Math.max(1, Math.round(rttSeconds * 1000));
775
+ } catch {
776
+ return null;
777
+ }
778
+ };
779
+
780
+ this._closeConnection = id => {
781
+ const connection = this._connections.get(id);
782
+
783
+ if (!connection) {
784
+ return;
785
+ }
786
+
787
+ if (connection.connectionTimeout) {
788
+ clearTimeout(connection.connectionTimeout);
789
+ connection.connectionTimeout = null;
790
+ }
791
+
792
+ this._closePeerObjects(connection);
793
+
794
+ connection.state = "closed";
795
+ connection.packetBuffer = [];
796
+ connection.bufferedBytes = 0;
797
+
798
+ this._connections.delete(id);
799
+ };
800
+
801
+ this._closePeerObjects = connection => {
802
+ this._stopIceRttPolling(connection);
803
+
804
+ try {
805
+ connection.dataChannel?.close?.();
806
+ } catch { }
807
+
808
+ try {
809
+ connection.peerConnection?.close?.();
810
+ } catch { }
811
+
812
+ connection.dataChannel = null;
813
+ connection.peerConnection = null;
814
+ };
815
+
816
+ this._loadWrtc = () => {
817
+ this._wrtcPromise ??= import("@roamhq/wrtc").then(module => module.default ?? module);
818
+
819
+ return this._wrtcPromise;
820
+ };
821
+ }
822
+ }
823
+
824
+ export default RelayRtcTransport;