@swal/edge-mesh 0.0.0-stage → 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.
Files changed (464) hide show
  1. package/LICENSE +128 -0
  2. package/NOTICE +37 -0
  3. package/README.md +470 -2
  4. package/dist/adapters/gpu-agent/index.d.ts +61 -0
  5. package/dist/adapters/gpu-agent/index.d.ts.map +1 -0
  6. package/dist/adapters/gpu-agent/index.js +364 -0
  7. package/dist/adapters/gpu-agent/index.js.map +1 -0
  8. package/dist/adapters/maloca-salud/citas-distribuidas.d.ts +19 -0
  9. package/dist/adapters/maloca-salud/citas-distribuidas.d.ts.map +1 -0
  10. package/dist/adapters/maloca-salud/citas-distribuidas.js +51 -0
  11. package/dist/adapters/maloca-salud/citas-distribuidas.js.map +1 -0
  12. package/dist/adapters/maloca-salud/eps-bridge.d.ts +15 -0
  13. package/dist/adapters/maloca-salud/eps-bridge.d.ts.map +1 -0
  14. package/dist/adapters/maloca-salud/eps-bridge.js +27 -0
  15. package/dist/adapters/maloca-salud/eps-bridge.js.map +1 -0
  16. package/dist/adapters/maloca-salud/patient-profile.d.ts +20 -0
  17. package/dist/adapters/maloca-salud/patient-profile.d.ts.map +1 -0
  18. package/dist/adapters/maloca-salud/patient-profile.js +36 -0
  19. package/dist/adapters/maloca-salud/patient-profile.js.map +1 -0
  20. package/dist/adapters/maloca-salud/types.d.ts +40 -0
  21. package/dist/adapters/maloca-salud/types.d.ts.map +1 -0
  22. package/dist/adapters/maloca-salud/types.js +2 -0
  23. package/dist/adapters/maloca-salud/types.js.map +1 -0
  24. package/dist/adapters/maloca-veeduria/contract-bridge.d.ts +31 -0
  25. package/dist/adapters/maloca-veeduria/contract-bridge.d.ts.map +1 -0
  26. package/dist/adapters/maloca-veeduria/contract-bridge.js +103 -0
  27. package/dist/adapters/maloca-veeduria/contract-bridge.js.map +1 -0
  28. package/dist/adapters/maloca-veeduria/evidentia-veeduria.d.ts +14 -0
  29. package/dist/adapters/maloca-veeduria/evidentia-veeduria.d.ts.map +1 -0
  30. package/dist/adapters/maloca-veeduria/evidentia-veeduria.js +40 -0
  31. package/dist/adapters/maloca-veeduria/evidentia-veeduria.js.map +1 -0
  32. package/dist/adapters/maloca-veeduria/licitante-profile.d.ts +21 -0
  33. package/dist/adapters/maloca-veeduria/licitante-profile.d.ts.map +1 -0
  34. package/dist/adapters/maloca-veeduria/licitante-profile.js +58 -0
  35. package/dist/adapters/maloca-veeduria/licitante-profile.js.map +1 -0
  36. package/dist/adapters/maloca-veeduria/types.d.ts +30 -0
  37. package/dist/adapters/maloca-veeduria/types.d.ts.map +1 -0
  38. package/dist/adapters/maloca-veeduria/types.js +2 -0
  39. package/dist/adapters/maloca-veeduria/types.js.map +1 -0
  40. package/dist/adapters/maloca-xavier/agent-profile.d.ts +32 -0
  41. package/dist/adapters/maloca-xavier/agent-profile.d.ts.map +1 -0
  42. package/dist/adapters/maloca-xavier/agent-profile.js +56 -0
  43. package/dist/adapters/maloca-xavier/agent-profile.js.map +1 -0
  44. package/dist/adapters/maloca-xavier/codegraph-bridge.d.ts +27 -0
  45. package/dist/adapters/maloca-xavier/codegraph-bridge.d.ts.map +1 -0
  46. package/dist/adapters/maloca-xavier/codegraph-bridge.js +58 -0
  47. package/dist/adapters/maloca-xavier/codegraph-bridge.js.map +1 -0
  48. package/dist/adapters/maloca-xavier/llm-router.d.ts +25 -0
  49. package/dist/adapters/maloca-xavier/llm-router.d.ts.map +1 -0
  50. package/dist/adapters/maloca-xavier/llm-router.js +58 -0
  51. package/dist/adapters/maloca-xavier/llm-router.js.map +1 -0
  52. package/dist/authz/index.d.ts +81 -0
  53. package/dist/authz/index.d.ts.map +1 -0
  54. package/dist/authz/index.js +271 -0
  55. package/dist/authz/index.js.map +1 -0
  56. package/dist/chat/index.d.ts +138 -0
  57. package/dist/chat/index.d.ts.map +1 -0
  58. package/dist/chat/index.js +394 -0
  59. package/dist/chat/index.js.map +1 -0
  60. package/dist/chat/offline-queue.d.ts +32 -0
  61. package/dist/chat/offline-queue.d.ts.map +1 -0
  62. package/dist/chat/offline-queue.js +93 -0
  63. package/dist/chat/offline-queue.js.map +1 -0
  64. package/dist/core/DefaultEdgeMeshNode.d.ts +17 -0
  65. package/dist/core/DefaultEdgeMeshNode.d.ts.map +1 -0
  66. package/dist/core/DefaultEdgeMeshNode.js +57 -0
  67. package/dist/core/DefaultEdgeMeshNode.js.map +1 -0
  68. package/dist/core/node.d.ts +26 -0
  69. package/dist/core/node.d.ts.map +1 -0
  70. package/dist/core/node.js +16 -0
  71. package/dist/core/node.js.map +1 -0
  72. package/dist/edge-mesh.d.ts +139 -0
  73. package/dist/edge-mesh.d.ts.map +1 -0
  74. package/dist/edge-mesh.js +1146 -0
  75. package/dist/edge-mesh.js.map +1 -0
  76. package/dist/gos/GosBridge.d.ts +49 -0
  77. package/dist/gos/GosBridge.d.ts.map +1 -0
  78. package/dist/gos/GosBridge.js +133 -0
  79. package/dist/gos/GosBridge.js.map +1 -0
  80. package/dist/governance/authority.d.ts +44 -0
  81. package/dist/governance/authority.d.ts.map +1 -0
  82. package/dist/governance/authority.js +123 -0
  83. package/dist/governance/authority.js.map +1 -0
  84. package/dist/governance/index.d.ts +66 -0
  85. package/dist/governance/index.d.ts.map +1 -0
  86. package/dist/governance/index.js +210 -0
  87. package/dist/governance/index.js.map +1 -0
  88. package/dist/governance/merge.d.ts +31 -0
  89. package/dist/governance/merge.d.ts.map +1 -0
  90. package/dist/governance/merge.js +167 -0
  91. package/dist/governance/merge.js.map +1 -0
  92. package/dist/identity/index.d.ts +199 -0
  93. package/dist/identity/index.d.ts.map +1 -0
  94. package/dist/identity/index.js +415 -0
  95. package/dist/identity/index.js.map +1 -0
  96. package/dist/index.d.ts +71 -0
  97. package/dist/index.d.ts.map +1 -0
  98. package/dist/index.js +56 -0
  99. package/dist/index.js.map +1 -0
  100. package/dist/maloca/backoffice.d.ts +31 -0
  101. package/dist/maloca/backoffice.d.ts.map +1 -0
  102. package/dist/maloca/backoffice.js +40 -0
  103. package/dist/maloca/backoffice.js.map +1 -0
  104. package/dist/maloca/event-bus.d.ts +60 -0
  105. package/dist/maloca/event-bus.d.ts.map +1 -0
  106. package/dist/maloca/event-bus.js +175 -0
  107. package/dist/maloca/event-bus.js.map +1 -0
  108. package/dist/maloca/evidentia.d.ts +136 -0
  109. package/dist/maloca/evidentia.d.ts.map +1 -0
  110. package/dist/maloca/evidentia.js +447 -0
  111. package/dist/maloca/evidentia.js.map +1 -0
  112. package/dist/maloca/gateway/api.d.ts +146 -0
  113. package/dist/maloca/gateway/api.d.ts.map +1 -0
  114. package/dist/maloca/gateway/api.js +228 -0
  115. package/dist/maloca/gateway/api.js.map +1 -0
  116. package/dist/maloca/gateway/auth.d.ts +20 -0
  117. package/dist/maloca/gateway/auth.d.ts.map +1 -0
  118. package/dist/maloca/gateway/auth.js +115 -0
  119. package/dist/maloca/gateway/auth.js.map +1 -0
  120. package/dist/maloca/gateway/dashboard.d.ts +42 -0
  121. package/dist/maloca/gateway/dashboard.d.ts.map +1 -0
  122. package/dist/maloca/gateway/dashboard.js +67 -0
  123. package/dist/maloca/gateway/dashboard.js.map +1 -0
  124. package/dist/maloca/gateway/websocket.d.ts +34 -0
  125. package/dist/maloca/gateway/websocket.d.ts.map +1 -0
  126. package/dist/maloca/gateway/websocket.js +71 -0
  127. package/dist/maloca/gateway/websocket.js.map +1 -0
  128. package/dist/maloca/karma.d.ts +165 -0
  129. package/dist/maloca/karma.d.ts.map +1 -0
  130. package/dist/maloca/karma.js +307 -0
  131. package/dist/maloca/karma.js.map +1 -0
  132. package/dist/maloca/kernel.d.ts +20 -0
  133. package/dist/maloca/kernel.d.ts.map +1 -0
  134. package/dist/maloca/kernel.js +76 -0
  135. package/dist/maloca/kernel.js.map +1 -0
  136. package/dist/maloca/metadata.d.ts +30 -0
  137. package/dist/maloca/metadata.d.ts.map +1 -0
  138. package/dist/maloca/metadata.js +80 -0
  139. package/dist/maloca/metadata.js.map +1 -0
  140. package/dist/maloca/perfil.d.ts +57 -0
  141. package/dist/maloca/perfil.d.ts.map +1 -0
  142. package/dist/maloca/perfil.js +113 -0
  143. package/dist/maloca/perfil.js.map +1 -0
  144. package/dist/maloca/plugin-registry.d.ts +27 -0
  145. package/dist/maloca/plugin-registry.d.ts.map +1 -0
  146. package/dist/maloca/plugin-registry.js +76 -0
  147. package/dist/maloca/plugin-registry.js.map +1 -0
  148. package/dist/maloca/polygon-bridge.d.ts +63 -0
  149. package/dist/maloca/polygon-bridge.d.ts.map +1 -0
  150. package/dist/maloca/polygon-bridge.js +198 -0
  151. package/dist/maloca/polygon-bridge.js.map +1 -0
  152. package/dist/maloca/types.d.ts +44 -0
  153. package/dist/maloca/types.d.ts.map +1 -0
  154. package/dist/maloca/types.js +2 -0
  155. package/dist/maloca/types.js.map +1 -0
  156. package/dist/medico/aval.d.ts +149 -0
  157. package/dist/medico/aval.d.ts.map +1 -0
  158. package/dist/medico/aval.js +281 -0
  159. package/dist/medico/aval.js.map +1 -0
  160. package/dist/medico/canonica.d.ts +96 -0
  161. package/dist/medico/canonica.d.ts.map +1 -0
  162. package/dist/medico/canonica.js +148 -0
  163. package/dist/medico/canonica.js.map +1 -0
  164. package/dist/medico/credencial.d.ts +160 -0
  165. package/dist/medico/credencial.d.ts.map +1 -0
  166. package/dist/medico/credencial.js +240 -0
  167. package/dist/medico/credencial.js.map +1 -0
  168. package/dist/medico/index.d.ts +36 -0
  169. package/dist/medico/index.d.ts.map +1 -0
  170. package/dist/medico/index.js +39 -0
  171. package/dist/medico/index.js.map +1 -0
  172. package/dist/medico/malla.d.ts +218 -0
  173. package/dist/medico/malla.d.ts.map +1 -0
  174. package/dist/medico/malla.js +347 -0
  175. package/dist/medico/malla.js.map +1 -0
  176. package/dist/medico/registro-profesional.d.ts +84 -0
  177. package/dist/medico/registro-profesional.d.ts.map +1 -0
  178. package/dist/medico/registro-profesional.js +135 -0
  179. package/dist/medico/registro-profesional.js.map +1 -0
  180. package/dist/medico/registro.d.ts +149 -0
  181. package/dist/medico/registro.d.ts.map +1 -0
  182. package/dist/medico/registro.js +173 -0
  183. package/dist/medico/registro.js.map +1 -0
  184. package/dist/medico/types.d.ts +287 -0
  185. package/dist/medico/types.d.ts.map +1 -0
  186. package/dist/medico/types.js +135 -0
  187. package/dist/medico/types.js.map +1 -0
  188. package/dist/medico/verificacion-registro.d.ts +300 -0
  189. package/dist/medico/verificacion-registro.d.ts.map +1 -0
  190. package/dist/medico/verificacion-registro.js +319 -0
  191. package/dist/medico/verificacion-registro.js.map +1 -0
  192. package/dist/mesh/index.d.ts +457 -0
  193. package/dist/mesh/index.d.ts.map +1 -0
  194. package/dist/mesh/index.js +929 -0
  195. package/dist/mesh/index.js.map +1 -0
  196. package/dist/namespaces/encrypted-plugin.d.ts +56 -0
  197. package/dist/namespaces/encrypted-plugin.d.ts.map +1 -0
  198. package/dist/namespaces/encrypted-plugin.js +173 -0
  199. package/dist/namespaces/encrypted-plugin.js.map +1 -0
  200. package/dist/namespaces/index.d.ts +52 -0
  201. package/dist/namespaces/index.d.ts.map +1 -0
  202. package/dist/namespaces/index.js +134 -0
  203. package/dist/namespaces/index.js.map +1 -0
  204. package/dist/namespaces/ivn-proofs.d.ts +54 -0
  205. package/dist/namespaces/ivn-proofs.d.ts.map +1 -0
  206. package/dist/namespaces/ivn-proofs.js +162 -0
  207. package/dist/namespaces/ivn-proofs.js.map +1 -0
  208. package/dist/namespaces/offers-gossip.d.ts +95 -0
  209. package/dist/namespaces/offers-gossip.d.ts.map +1 -0
  210. package/dist/namespaces/offers-gossip.js +213 -0
  211. package/dist/namespaces/offers-gossip.js.map +1 -0
  212. package/dist/node-memory/idb-store.d.ts +22 -0
  213. package/dist/node-memory/idb-store.d.ts.map +1 -0
  214. package/dist/node-memory/idb-store.js +154 -0
  215. package/dist/node-memory/idb-store.js.map +1 -0
  216. package/dist/node-memory/index.d.ts +14 -0
  217. package/dist/node-memory/index.d.ts.map +1 -0
  218. package/dist/node-memory/index.js +159 -0
  219. package/dist/node-memory/index.js.map +1 -0
  220. package/dist/node-memory/sync-flush.d.ts +11 -0
  221. package/dist/node-memory/sync-flush.d.ts.map +1 -0
  222. package/dist/node-memory/sync-flush.js +37 -0
  223. package/dist/node-memory/sync-flush.js.map +1 -0
  224. package/dist/node-memory/types.d.ts +34 -0
  225. package/dist/node-memory/types.d.ts.map +1 -0
  226. package/dist/node-memory/types.js +2 -0
  227. package/dist/node-memory/types.js.map +1 -0
  228. package/dist/node-memory/xavier-store.d.ts +12 -0
  229. package/dist/node-memory/xavier-store.d.ts.map +1 -0
  230. package/dist/node-memory/xavier-store.js +75 -0
  231. package/dist/node-memory/xavier-store.js.map +1 -0
  232. package/dist/op-log/index.d.ts +49 -0
  233. package/dist/op-log/index.d.ts.map +1 -0
  234. package/dist/op-log/index.js +190 -0
  235. package/dist/op-log/index.js.map +1 -0
  236. package/dist/presence/health.d.ts +54 -0
  237. package/dist/presence/health.d.ts.map +1 -0
  238. package/dist/presence/health.js +139 -0
  239. package/dist/presence/health.js.map +1 -0
  240. package/dist/presence/index.d.ts +85 -0
  241. package/dist/presence/index.d.ts.map +1 -0
  242. package/dist/presence/index.js +255 -0
  243. package/dist/presence/index.js.map +1 -0
  244. package/dist/presence/peer-health.d.ts +40 -0
  245. package/dist/presence/peer-health.d.ts.map +1 -0
  246. package/dist/presence/peer-health.js +118 -0
  247. package/dist/presence/peer-health.js.map +1 -0
  248. package/dist/protocol/canonical.d.ts +10 -0
  249. package/dist/protocol/canonical.d.ts.map +1 -0
  250. package/dist/protocol/canonical.js +32 -0
  251. package/dist/protocol/canonical.js.map +1 -0
  252. package/dist/protocol/crypto.d.ts +299 -0
  253. package/dist/protocol/crypto.d.ts.map +1 -0
  254. package/dist/protocol/crypto.js +739 -0
  255. package/dist/protocol/crypto.js.map +1 -0
  256. package/dist/protocol/index.d.ts +30 -0
  257. package/dist/protocol/index.d.ts.map +1 -0
  258. package/dist/protocol/index.js +116 -0
  259. package/dist/protocol/index.js.map +1 -0
  260. package/dist/protocol/utils.d.ts +5 -0
  261. package/dist/protocol/utils.d.ts.map +1 -0
  262. package/dist/protocol/utils.js +27 -0
  263. package/dist/protocol/utils.js.map +1 -0
  264. package/dist/salones/manager.d.ts +100 -0
  265. package/dist/salones/manager.d.ts.map +1 -0
  266. package/dist/salones/manager.js +240 -0
  267. package/dist/salones/manager.js.map +1 -0
  268. package/dist/security/rate-limiter.d.ts +13 -0
  269. package/dist/security/rate-limiter.d.ts.map +1 -0
  270. package/dist/security/rate-limiter.js +32 -0
  271. package/dist/security/rate-limiter.js.map +1 -0
  272. package/dist/snapshot/index.d.ts +90 -0
  273. package/dist/snapshot/index.d.ts.map +1 -0
  274. package/dist/snapshot/index.js +189 -0
  275. package/dist/snapshot/index.js.map +1 -0
  276. package/dist/storage/index.d.ts +49 -0
  277. package/dist/storage/index.d.ts.map +1 -0
  278. package/dist/storage/index.js +176 -0
  279. package/dist/storage/index.js.map +1 -0
  280. package/dist/storage/optimizer.d.ts +28 -0
  281. package/dist/storage/optimizer.d.ts.map +1 -0
  282. package/dist/storage/optimizer.js +50 -0
  283. package/dist/storage/optimizer.js.map +1 -0
  284. package/dist/storage/ydoc-persistence.d.ts +27 -0
  285. package/dist/storage/ydoc-persistence.d.ts.map +1 -0
  286. package/dist/storage/ydoc-persistence.js +128 -0
  287. package/dist/storage/ydoc-persistence.js.map +1 -0
  288. package/dist/sync/ChunkSyncManager.d.ts +41 -0
  289. package/dist/sync/ChunkSyncManager.d.ts.map +1 -0
  290. package/dist/sync/ChunkSyncManager.js +115 -0
  291. package/dist/sync/ChunkSyncManager.js.map +1 -0
  292. package/dist/sync/conflict-resolver.d.ts +33 -0
  293. package/dist/sync/conflict-resolver.d.ts.map +1 -0
  294. package/dist/sync/conflict-resolver.js +54 -0
  295. package/dist/sync/conflict-resolver.js.map +1 -0
  296. package/dist/sync/engine.d.ts +168 -0
  297. package/dist/sync/engine.d.ts.map +1 -0
  298. package/dist/sync/engine.js +315 -0
  299. package/dist/sync/engine.js.map +1 -0
  300. package/dist/sync/index.d.ts +3 -0
  301. package/dist/sync/index.d.ts.map +1 -0
  302. package/dist/sync/index.js +3 -0
  303. package/dist/sync/index.js.map +1 -0
  304. package/dist/transport/TorTransportAdapter.d.ts +70 -0
  305. package/dist/transport/TorTransportAdapter.d.ts.map +1 -0
  306. package/dist/transport/TorTransportAdapter.js +192 -0
  307. package/dist/transport/TorTransportAdapter.js.map +1 -0
  308. package/dist/transport/memory.d.ts +31 -0
  309. package/dist/transport/memory.d.ts.map +1 -0
  310. package/dist/transport/memory.js +114 -0
  311. package/dist/transport/memory.js.map +1 -0
  312. package/dist/transport/peerjs.d.ts +45 -0
  313. package/dist/transport/peerjs.d.ts.map +1 -0
  314. package/dist/transport/peerjs.js +301 -0
  315. package/dist/transport/peerjs.js.map +1 -0
  316. package/dist/transport/pqc-handshake.d.ts +72 -0
  317. package/dist/transport/pqc-handshake.d.ts.map +1 -0
  318. package/dist/transport/pqc-handshake.js +129 -0
  319. package/dist/transport/pqc-handshake.js.map +1 -0
  320. package/dist/transport/relay-config.d.ts +11 -0
  321. package/dist/transport/relay-config.d.ts.map +1 -0
  322. package/dist/transport/relay-config.js +48 -0
  323. package/dist/transport/relay-config.js.map +1 -0
  324. package/dist/transport/relay-server.d.ts +37 -0
  325. package/dist/transport/relay-server.d.ts.map +1 -0
  326. package/dist/transport/relay-server.js +230 -0
  327. package/dist/transport/relay-server.js.map +1 -0
  328. package/dist/transport/tor.d.ts +47 -0
  329. package/dist/transport/tor.d.ts.map +1 -0
  330. package/dist/transport/tor.js +158 -0
  331. package/dist/transport/tor.js.map +1 -0
  332. package/dist/transport/types.d.ts +34 -0
  333. package/dist/transport/types.d.ts.map +1 -0
  334. package/dist/transport/types.js +2 -0
  335. package/dist/transport/types.js.map +1 -0
  336. package/dist/types/index.d.ts +293 -0
  337. package/dist/types/index.d.ts.map +1 -0
  338. package/dist/types/index.js +73 -0
  339. package/dist/types/index.js.map +1 -0
  340. package/dist/web/admission.d.ts +24 -0
  341. package/dist/web/admission.d.ts.map +1 -0
  342. package/dist/web/admission.js +41 -0
  343. package/dist/web/admission.js.map +1 -0
  344. package/dist/web/crypto.d.ts +18 -0
  345. package/dist/web/crypto.d.ts.map +1 -0
  346. package/dist/web/crypto.js +78 -0
  347. package/dist/web/crypto.js.map +1 -0
  348. package/dist/web/exchange.d.ts +23 -0
  349. package/dist/web/exchange.d.ts.map +1 -0
  350. package/dist/web/exchange.js +54 -0
  351. package/dist/web/exchange.js.map +1 -0
  352. package/dist/web/fragment.d.ts +58 -0
  353. package/dist/web/fragment.d.ts.map +1 -0
  354. package/dist/web/fragment.js +198 -0
  355. package/dist/web/fragment.js.map +1 -0
  356. package/dist/web/index.d.ts +25 -0
  357. package/dist/web/index.d.ts.map +1 -0
  358. package/dist/web/index.js +14 -0
  359. package/dist/web/index.js.map +1 -0
  360. package/dist/web/merge/index.d.ts +104 -0
  361. package/dist/web/merge/index.d.ts.map +1 -0
  362. package/dist/web/merge/index.js +236 -0
  363. package/dist/web/merge/index.js.map +1 -0
  364. package/dist/web/oplog/hlc.d.ts +19 -0
  365. package/dist/web/oplog/hlc.d.ts.map +1 -0
  366. package/dist/web/oplog/hlc.js +39 -0
  367. package/dist/web/oplog/hlc.js.map +1 -0
  368. package/dist/web/oplog/index.d.ts +10 -0
  369. package/dist/web/oplog/index.d.ts.map +1 -0
  370. package/dist/web/oplog/index.js +6 -0
  371. package/dist/web/oplog/index.js.map +1 -0
  372. package/dist/web/oplog/log.d.ts +182 -0
  373. package/dist/web/oplog/log.d.ts.map +1 -0
  374. package/dist/web/oplog/log.js +693 -0
  375. package/dist/web/oplog/log.js.map +1 -0
  376. package/dist/web/oplog/store.d.ts +40 -0
  377. package/dist/web/oplog/store.d.ts.map +1 -0
  378. package/dist/web/oplog/store.js +85 -0
  379. package/dist/web/oplog/store.js.map +1 -0
  380. package/dist/web/oplog/sync.d.ts +54 -0
  381. package/dist/web/oplog/sync.d.ts.map +1 -0
  382. package/dist/web/oplog/sync.js +162 -0
  383. package/dist/web/oplog/sync.js.map +1 -0
  384. package/dist/web/oplog/types.d.ts +119 -0
  385. package/dist/web/oplog/types.d.ts.map +1 -0
  386. package/dist/web/oplog/types.js +2 -0
  387. package/dist/web/oplog/types.js.map +1 -0
  388. package/dist/web/pairing.d.ts +186 -0
  389. package/dist/web/pairing.d.ts.map +1 -0
  390. package/dist/web/pairing.js +395 -0
  391. package/dist/web/pairing.js.map +1 -0
  392. package/dist/web/pq.d.ts +36 -0
  393. package/dist/web/pq.d.ts.map +1 -0
  394. package/dist/web/pq.js +59 -0
  395. package/dist/web/pq.js.map +1 -0
  396. package/dist/web/provider.d.ts +188 -0
  397. package/dist/web/provider.d.ts.map +1 -0
  398. package/dist/web/provider.js +2042 -0
  399. package/dist/web/provider.js.map +1 -0
  400. package/dist/web/rooms.d.ts +20 -0
  401. package/dist/web/rooms.d.ts.map +1 -0
  402. package/dist/web/rooms.js +51 -0
  403. package/dist/web/rooms.js.map +1 -0
  404. package/dist/web/rotation.d.ts +73 -0
  405. package/dist/web/rotation.d.ts.map +1 -0
  406. package/dist/web/rotation.js +130 -0
  407. package/dist/web/rotation.js.map +1 -0
  408. package/dist/web/secstate.d.ts +213 -0
  409. package/dist/web/secstate.d.ts.map +1 -0
  410. package/dist/web/secstate.js +789 -0
  411. package/dist/web/secstate.js.map +1 -0
  412. package/dist/web/store.d.ts +9 -0
  413. package/dist/web/store.d.ts.map +1 -0
  414. package/dist/web/store.js +32 -0
  415. package/dist/web/store.js.map +1 -0
  416. package/dist/web/transports/loopback.d.ts +11 -0
  417. package/dist/web/transports/loopback.d.ts.map +1 -0
  418. package/dist/web/transports/loopback.js +97 -0
  419. package/dist/web/transports/loopback.js.map +1 -0
  420. package/dist/web/transports/qr-sdp.d.ts +27 -0
  421. package/dist/web/transports/qr-sdp.d.ts.map +1 -0
  422. package/dist/web/transports/qr-sdp.js +131 -0
  423. package/dist/web/transports/qr-sdp.js.map +1 -0
  424. package/dist/web/transports/ws.d.ts +12 -0
  425. package/dist/web/transports/ws.d.ts.map +1 -0
  426. package/dist/web/transports/ws.js +139 -0
  427. package/dist/web/transports/ws.js.map +1 -0
  428. package/dist/web/trust/canonical.d.ts +8 -0
  429. package/dist/web/trust/canonical.d.ts.map +1 -0
  430. package/dist/web/trust/canonical.js +27 -0
  431. package/dist/web/trust/canonical.js.map +1 -0
  432. package/dist/web/trust/docs.d.ts +51 -0
  433. package/dist/web/trust/docs.d.ts.map +1 -0
  434. package/dist/web/trust/docs.js +170 -0
  435. package/dist/web/trust/docs.js.map +1 -0
  436. package/dist/web/trust/index.d.ts +10 -0
  437. package/dist/web/trust/index.d.ts.map +1 -0
  438. package/dist/web/trust/index.js +7 -0
  439. package/dist/web/trust/index.js.map +1 -0
  440. package/dist/web/trust/keys.d.ts +37 -0
  441. package/dist/web/trust/keys.d.ts.map +1 -0
  442. package/dist/web/trust/keys.js +75 -0
  443. package/dist/web/trust/keys.js.map +1 -0
  444. package/dist/web/trust/store.d.ts +150 -0
  445. package/dist/web/trust/store.d.ts.map +1 -0
  446. package/dist/web/trust/store.js +587 -0
  447. package/dist/web/trust/store.js.map +1 -0
  448. package/dist/web/trust/types.d.ts +134 -0
  449. package/dist/web/trust/types.d.ts.map +1 -0
  450. package/dist/web/trust/types.js +4 -0
  451. package/dist/web/trust/types.js.map +1 -0
  452. package/dist/web/types.d.ts +115 -0
  453. package/dist/web/types.d.ts.map +1 -0
  454. package/dist/web/types.js +2 -0
  455. package/dist/web/types.js.map +1 -0
  456. package/dist/web/util.d.ts +12 -0
  457. package/dist/web/util.d.ts.map +1 -0
  458. package/dist/web/util.js +54 -0
  459. package/dist/web/util.js.map +1 -0
  460. package/dist/web/webrtc.d.ts +35 -0
  461. package/dist/web/webrtc.d.ts.map +1 -0
  462. package/dist/web/webrtc.js +304 -0
  463. package/dist/web/webrtc.js.map +1 -0
  464. package/package.json +138 -4
@@ -0,0 +1,2042 @@
1
+ import { Awareness, applyAwarenessUpdate, encodeAwarenessUpdate, removeAwarenessStates, } from "y-protocols/awareness";
2
+ import * as Y from "yjs";
3
+ import { canIssue, idMatchesPub, isDeviceId, isEpoch, MAX_EPOCH, MAX_EPOCH_SKIP, } from "./admission.js";
4
+ import { deriveDocMaterial, deriveSenderKey, hkdf, importAesKey, openUpdate, sealUpdate, } from "./crypto.js";
5
+ import { DEFAULT_MAX_FRAME, DEFAULT_MAX_MESSAGE, F_FRAG, fragment, Reassembler, } from "./fragment.js";
6
+ import { createPairOffer, decodePairPayload, derivePairKey, GuestPairing, HostPairing, hostProofBytes, } from "./pairing.js";
7
+ import { identityVerify, kemKeygen, ML_DSA_PUBLIC_KEY_BYTES, ML_KEM_PUBLIC_KEY_BYTES, ML_KEM_SECRET_KEY_BYTES, } from "./pq.js";
8
+ import { derivePairRoomId, deriveRoomId, fingerprint, meshNamespace, } from "./rooms.js";
9
+ import { generateEcdhIdentity, MAX_ROT_MEMBERS, MAX_ROT_REFS, rotationId, rotationPreId, rotationSigBytes, unwrapMeshKey, wrapMeshKey, wrapsHash, } from "./rotation.js";
10
+ import { MAX_MEMBERS_PER_ADMIN, SecurityState, vaultSigner, } from "./secstate.js";
11
+ import { idbStore, memoryStore } from "./store.js";
12
+ import { b64uDecode, b64uEncode, concat, equalBytes, fromUtf8, randomBytes, utf8, } from "./util.js";
13
+ import { connectViaSignaling } from "./webrtc.js";
14
+ const F_DATA = 1; // legacy unsigned data frame (only with signFrames:false)
15
+ const F_PAIR = 2;
16
+ const F_SDATA = 3; // signed data frame: plaintext = kind | sess(8) | seq(u32) | sigLen(u16) | sig | body
17
+ const K_SV = 0;
18
+ const K_UPDATE = 1;
19
+ const K_AWARENESS = 2;
20
+ // kind 3 (pre-round-5 rotation frames) is retired: such frames are ignored
21
+ const K_CHANNEL = 4; // body = nsLen(u16) | namespace | payload
22
+ const K_HELLO = 5; // body = nonce(16): fresh challenge of this link (S1)
23
+ const K_AUTH = 6; // body = peer's nonce(16) | epoch(u32), signed: the sender is live on THIS link (S1)
24
+ /** round 5: security documents (JSON {t:"inv",ids} | {t:"want",ids} | {t:"docs",docs}), also under retired keys */
25
+ const K_TRUST = 7;
26
+ const NONCE_BYTES = 16;
27
+ /** finding 4: bytes of bulk data waiting for one link beyond the message in progress (then the link is closed) */
28
+ const MAX_OUTSTANDING = 16 * 1024 * 1024;
29
+ /** SF4: per link, at most this many challenges sent and answered */
30
+ const HANDSHAKE_MAX = 8;
31
+ /** frames kept per link while waiting for the peer's K_AUTH (also bounded by PRE_AUTH_MAX bytes) */
32
+ const EARLY_MAX_FRAMES = 32;
33
+ /**
34
+ * R4-S1: signature checks a link may cause before it authenticates (handshake frames only; a legit peer needs at most
35
+ * HANDSHAKE_MAX challenges under up to 1 + RETIRED_TRY keys plus HANDSHAKE_MAX answers = 48). Beyond: the link closes.
36
+ */
37
+ const HS_VERIFY_MAX = 64;
38
+ /**
39
+ * R5-S2: the same limits across links. Handshake frames already verified on ANY link are dropped unverified (a global
40
+ * window), one claimed identity may cost at most HS_VERIFY_PER_ID checks per minute over all links, a room holds at
41
+ * most MAX_UNAUTH_PER_ROOM links that have not authenticated (MAX_UNAUTH_PER_PEER per announced peer), frames kept
42
+ * before a peer's K_AUTH share EARLY_TOTAL bytes, and in a retired room a sender is answered at most
43
+ * LEGACY_ANSWERS_PER_MIN times per minute (and never challenged again: stragglers come back by themselves).
44
+ */
45
+ const HS_VERIFY_PER_ID = 128;
46
+ const MAX_UNAUTH_PER_ROOM = 32;
47
+ const MAX_UNAUTH_PER_PEER = 2;
48
+ const EARLY_TOTAL = 4 * 1024 * 1024;
49
+ const LEGACY_ANSWERS_PER_MIN = 2;
50
+ /** Replay window per sender session: seqs at most this far behind the highest one are still accepted once. */
51
+ const REPLAY_WINDOW = 1024;
52
+ const ORIGIN = Symbol("swal-mesh");
53
+ /** round 5: trust-channel limits (per message / per peer) */
54
+ const TRUST_INV_MAX = 20_000;
55
+ const TRUST_WANT_MAX = 2048;
56
+ const TRUST_DOCS_MAX = 256;
57
+ const TRUST_BATCH_BYTES = 512 * 1024;
58
+ /** documents served to one peer per minute, and failed documents accepted from one sender per minute */
59
+ const TRUST_SERVE_PER_MIN = 8192;
60
+ const TRUST_FAIL_PER_MIN = 64;
61
+ /** R4-S2: bounded caches drop their least recently used entry instead of being cleared all at once. */
62
+ class Lru {
63
+ cap;
64
+ m = new Map();
65
+ constructor(cap) {
66
+ this.cap = cap;
67
+ }
68
+ get(k) {
69
+ const v = this.m.get(k);
70
+ if (v !== undefined) {
71
+ this.m.delete(k);
72
+ this.m.set(k, v);
73
+ }
74
+ return v;
75
+ }
76
+ has(k) {
77
+ return this.get(k) !== undefined;
78
+ }
79
+ set(k, v) {
80
+ this.m.delete(k);
81
+ this.m.set(k, v);
82
+ if (this.m.size > this.cap)
83
+ this.m.delete(this.m.keys().next().value);
84
+ }
85
+ clear() {
86
+ this.m.clear();
87
+ }
88
+ }
89
+ /** SF5: retired keys this device itself held, kept in its LOCAL store (never read from the shared doc) */
90
+ const RETIRED_KEY = "retired";
91
+ const MAX_RETIRED = 16;
92
+ /** Retired keys tried on (and used to seal) rotation frames, so devices on another branch/epoch still get them. */
93
+ const RETIRED_TRY = 4;
94
+ const MAX_CANDIDATES = 16;
95
+ /** Total order of rotations (B4): higher epoch first, then the lower rotation id. Deterministic on every device. */
96
+ const betterRot = (a, b) => (a.epoch !== b.epoch ? a.epoch > b.epoch : a.id < b.id);
97
+ const HOLD_MAX_FRAMES = 64;
98
+ const HOLD_MAX_BYTES = 8 * 1024 * 1024;
99
+ /** S5: held frames of ALL links together */
100
+ const HOLD_TOTAL_BYTES = 16 * 1024 * 1024;
101
+ /** S5: largest message reassembled on a link before it is authenticated, and for all such links together */
102
+ const PRE_AUTH_MAX = 1024 * 1024;
103
+ const PRE_AUTH_TOTAL = 8 * 1024 * 1024;
104
+ /** S5: largest pairing message (JSON); the grant no longer carries the doc, which arrives by normal sync */
105
+ const PAIR_MAX = 256 * 1024;
106
+ const HOLD_MS = 30_000;
107
+ const u32 = (n) => {
108
+ const b = new Uint8Array(4);
109
+ new DataView(b.buffer).setUint32(0, n >>> 0, false);
110
+ return b;
111
+ };
112
+ /**
113
+ * What a device signs for a data frame: bound to the room (mesh key + epoch), the sender, the kind and a per-sender
114
+ * session + sequence number (S1: receivers drop duplicates and replays).
115
+ */
116
+ const frameSigBytes = (rid, sender, kind, sess, seq, body) => concat(utf8(`swal-frame/v2|${rid}|${sender}|`), new Uint8Array([kind]), sess, u32(seq), body);
117
+ export function createMesh(opts) {
118
+ const { appId, topic: topicName, doc, vault } = opts;
119
+ const transports = opts.signaling ?? [];
120
+ const now = opts.now ?? (() => Date.now());
121
+ const awareness = new Awareness(doc);
122
+ const listeners = new Map();
123
+ const emit = (e, d) => {
124
+ for (const cb of listeners.get(e) ?? []) {
125
+ try {
126
+ cb(d);
127
+ }
128
+ catch { }
129
+ }
130
+ };
131
+ let destroyed = false;
132
+ let running = false;
133
+ let meshKey = null;
134
+ let docMat = null;
135
+ let sigKey = null;
136
+ let dataRid = "";
137
+ let instanceId = opts.instance ?? "";
138
+ let epoch = 0;
139
+ const localNum = (x) => (isEpoch(x) ? x : 0); // SF1: an out-of-range local value is ignored
140
+ let status = "off";
141
+ const links = new Set();
142
+ // per-sender AES-GCM keys, cached PER KEY MATERIAL (never by epoch number: two meshes, or two concurrent
143
+ // rotations, can share an epoch number). Reset by loadKeys (B3).
144
+ let senderKeys = new WeakMap();
145
+ const store = opts.store ??
146
+ vault.store ??
147
+ (opts.persist === "idb" && typeof indexedDB !== "undefined"
148
+ ? idbStore(`swal-mesh-local/${appId}/${topicName}`)
149
+ : memoryStore());
150
+ let root = null;
151
+ /** round 5: the security state (signed documents, web/trust membership); null until a root is pinned */
152
+ let sec = null;
153
+ const selfPub = b64uEncode(vault.devicePublicKey);
154
+ const signer = vaultSigner(vault, selfPub);
155
+ let trusted = new Map(); // members other than this one (and the owner), from the security state
156
+ const legacy = new Map(); // retired data rid -> its epoch material
157
+ let curRot = null; // rotation that produced the current key (null: epoch 0 / unknown)
158
+ let curRotId = null; // its id, kept even before the document itself has arrived (pairing grant)
159
+ const cands = new Map(); // verified rotations not adopted (yet)
160
+ const unwrapFailed = new Set(); // rotations whose wrap for us did not open (reported once)
161
+ let rotChain = Promise.resolve();
162
+ /** Every key change (own rotation, adoption, conflict resolution) runs one at a time. */
163
+ function serialRot(f) {
164
+ const run = rotChain.then(f);
165
+ rotChain = run.catch(() => { });
166
+ return run;
167
+ }
168
+ let ecdhId = null;
169
+ let kemId = null;
170
+ const rooms = new Map(); // rid -> leave
171
+ const linkSubs = [];
172
+ let hostSession = null;
173
+ let guestSession = null;
174
+ let pairRids = new Set();
175
+ const maxFrame = opts.maxFrameBytes ?? DEFAULT_MAX_FRAME;
176
+ const signFrames = opts.signFrames !== false;
177
+ const peerIds = () => [
178
+ ...new Set([...links]
179
+ .filter((l) => l.deviceId &&
180
+ l.rid !== "pair" &&
181
+ !l.legacy &&
182
+ !l.closing &&
183
+ !isRevoked(l.deviceId))
184
+ .map((l) => l.deviceId)),
185
+ ];
186
+ const setStatus = () => {
187
+ const s = !running
188
+ ? "off"
189
+ : peerIds().length > 0
190
+ ? "online"
191
+ : "connecting";
192
+ if (s !== status) {
193
+ status = s;
194
+ emit("status", s);
195
+ }
196
+ emit("peers", peerIds());
197
+ };
198
+ const err = (e) => emit("error", e);
199
+ // ---- devices: members by signed web/trust grants (round 5). Nothing in the shared doc counts. ----
200
+ const selfRole = () => root?.deviceId === vault.deviceId
201
+ ? "owner"
202
+ : (sec?.roleOf(vault.deviceId) ?? null);
203
+ const selfDevice = () => {
204
+ const g = sec?.memberGrant(vault.deviceId);
205
+ return {
206
+ deviceId: vault.deviceId,
207
+ pub: selfPub,
208
+ name: g?.name ?? opts.deviceName ?? "device",
209
+ addedAt: g?.issuedAt ?? 0,
210
+ ...(selfRole() ? { role: selfRole() } : {}),
211
+ ...(g ? { admittedBy: g.issuer } : {}),
212
+ };
213
+ };
214
+ /** A device that had a grant and has none left that makes it a member (revoked, or cut by the owner). */
215
+ const isRevoked = (id) => sec?.isOut(id) ?? false;
216
+ // a device revoked a moment ago is never listed, even before the trust state is recomputed
217
+ const devices = () => [
218
+ selfDevice(),
219
+ ...[...trusted.values()].filter((d) => !isRevoked(d.deviceId)),
220
+ ].sort((a, b) => a.addedAt - b.addedAt);
221
+ const roleOf = (id) => id === vault.deviceId
222
+ ? selfRole()
223
+ : isRevoked(id)
224
+ ? null
225
+ : (trusted.get(id)?.role ?? null);
226
+ let trustChain = Promise.resolve();
227
+ /**
228
+ * R4-S2: one trust pass at most waits behind the running one. Every change of the security state asks for a pass;
229
+ * those that arrive while one is queued share it (it starts after them, so it sees their documents).
230
+ */
231
+ let trustQueued = null;
232
+ const refreshTrust = () => {
233
+ if (trustQueued)
234
+ return trustQueued;
235
+ const run = trustChain.then(() => {
236
+ trustQueued = null;
237
+ return computeTrust();
238
+ });
239
+ trustQueued = run.catch(err);
240
+ trustChain = trustQueued;
241
+ return trustQueued;
242
+ };
243
+ async function computeTrust() {
244
+ const next = new Map();
245
+ if (sec && root) {
246
+ if (root.deviceId !== vault.deviceId)
247
+ next.set(root.deviceId, {
248
+ deviceId: root.deviceId,
249
+ pub: root.pub,
250
+ name: ownerName ?? "owner",
251
+ addedAt: 0,
252
+ role: "owner",
253
+ });
254
+ for (const m of sec.members()) {
255
+ if (m.deviceId === vault.deviceId)
256
+ continue;
257
+ if (opts.authorizeDevice &&
258
+ !(await opts.authorizeDevice(m.deviceId, b64uDecode(m.pub))))
259
+ continue;
260
+ next.set(m.deviceId, {
261
+ deviceId: m.deviceId,
262
+ pub: m.pub,
263
+ name: m.grant.name || m.deviceId,
264
+ addedAt: m.grant.issuedAt,
265
+ role: m.role,
266
+ admittedBy: m.grant.issuer,
267
+ });
268
+ }
269
+ }
270
+ trusted = next;
271
+ for (const l of [...links])
272
+ if (l.deviceId && isRevoked(l.deviceId))
273
+ closeRec(l);
274
+ emit("devices", devices());
275
+ replayHeld();
276
+ if (sec && running) {
277
+ void serialRot(considerRotations).catch(err);
278
+ }
279
+ }
280
+ // ---- persistence ----
281
+ let persistence = null;
282
+ let ownerName = null;
283
+ /** Open (or switch to) the security state of the pinned root, and follow its changes. */
284
+ let secUnsub = null;
285
+ async function openSecurity(r) {
286
+ secUnsub?.();
287
+ sec = await SecurityState.open({
288
+ root: r,
289
+ store,
290
+ schema: opts.trustSchema,
291
+ now,
292
+ });
293
+ secUnsub = sec.onChange(() => void refreshTrust());
294
+ }
295
+ const ready = (async () => {
296
+ // B1: a deviceId IS the fingerprint of the identity key; every peer enforces it, so must we
297
+ if (vault.devicePublicKey.length !== ML_DSA_PUBLIC_KEY_BYTES)
298
+ throw new Error("vault.devicePublicKey must be an ML-DSA-65 public key (1952 bytes; ECDSA identities are no longer accepted)");
299
+ if (!(await idMatchesPub(vault.deviceId, selfPub))) {
300
+ throw new Error("vault.deviceId must be deviceIdOf(vault.devicePublicKey)");
301
+ }
302
+ const r = (await store.get("root"));
303
+ if (r &&
304
+ typeof r.deviceId === "string" &&
305
+ typeof r.pub === "string" &&
306
+ typeof r.mid === "string")
307
+ root = r;
308
+ const on = await store.get("ownerName");
309
+ if (typeof on === "string")
310
+ ownerName = on;
311
+ const cr = await store.get("rotId");
312
+ if (typeof cr === "string")
313
+ curRotId = cr;
314
+ if (root) {
315
+ await openSecurity(root);
316
+ const c = curRotId ? sec.rotation(curRotId) : undefined;
317
+ if (c)
318
+ curRot = c;
319
+ }
320
+ if (opts.persist === "idb" && typeof indexedDB !== "undefined") {
321
+ const { IndexeddbPersistence } = await import("y-indexeddb");
322
+ const p = new IndexeddbPersistence(`swal-mesh/${appId}/${topicName}`, doc);
323
+ persistence = p;
324
+ await p.whenSynced;
325
+ }
326
+ if (opts.persist === "idb" && typeof indexedDB === "undefined") {
327
+ throw new Error("persist:'idb' requested but IndexedDB is not available");
328
+ }
329
+ if (sec)
330
+ await publishKex();
331
+ await refreshTrust();
332
+ epoch = Math.max(localNum(await vault.getEpoch?.()), localNum(await store.get("epoch")), epoch); // known before start
333
+ // already paired: resume (unless the app is about to move this device to another mesh, see `resume`)
334
+ if (opts.resume !== false && root && trusted.size > 0 && !destroyed)
335
+ await start();
336
+ })();
337
+ ready.catch(err);
338
+ // ---- key-agreement identity (rotation wraps) ----
339
+ async function ecdhIdentity() {
340
+ if (!ecdhId) {
341
+ ecdhId =
342
+ (await vault.getEcdhIdentity?.()) ?? (await generateEcdhIdentity());
343
+ }
344
+ return ecdhId;
345
+ }
346
+ async function kemIdentity() {
347
+ if (!kemId) {
348
+ const k = (await vault.getKemIdentity?.()) ?? kemKeygen();
349
+ if (k.publicKey.length !== ML_KEM_PUBLIC_KEY_BYTES ||
350
+ k.secretKey.length !== ML_KEM_SECRET_KEY_BYTES)
351
+ throw new Error("vault.getKemIdentity must return an ML-KEM-768 key pair");
352
+ kemId = k;
353
+ }
354
+ return kemId;
355
+ }
356
+ /** This device's key-agreement record (a signed document; a new one only when the keys changed). */
357
+ async function publishKex() {
358
+ if (!sec)
359
+ return;
360
+ const ecdh = b64uEncode((await ecdhIdentity()).publicKey);
361
+ const kem = b64uEncode((await kemIdentity()).publicKey);
362
+ const cur = sec.keyAgreement(vault.deviceId);
363
+ if (cur?.ecdh === ecdh && cur.kem === kem)
364
+ return;
365
+ await addLocal([await sec.kexRecord(signer, ecdh, kem)]);
366
+ }
367
+ /** Key-agreement keys of a device, from its signed record (members only use those of members). */
368
+ function kexOf(deviceId) {
369
+ const k = sec?.keyAgreement(deviceId);
370
+ return k ? { pub: b64uDecode(k.ecdh), kem: b64uDecode(k.kem) } : null;
371
+ }
372
+ // ---- keys ----
373
+ function senderKey(material, deviceId) {
374
+ let m = senderKeys.get(material);
375
+ if (!m) {
376
+ m = new Map();
377
+ senderKeys.set(material, m);
378
+ }
379
+ let key = m.get(deviceId);
380
+ if (!key) {
381
+ if (m.size >= 4096)
382
+ m.clear();
383
+ key = deriveSenderKey(material, topicName, deviceId);
384
+ m.set(deviceId, key);
385
+ }
386
+ return key;
387
+ }
388
+ async function persistEpoch(n) {
389
+ await vault.setEpoch?.(n);
390
+ await store.set("epoch", n);
391
+ }
392
+ async function loadKeys(raw) {
393
+ senderKeys = new WeakMap(); // B3: nothing derived from a previous mesh key survives a key change
394
+ meshKey = raw ?? (await vault.getOrCreateMeshKey());
395
+ // B2: the epoch is device-local state (vault / local store), advanced only by a verified rotation or by the
396
+ // authenticated pairing grant. It is NEVER read from the shared doc, which every member can write.
397
+ epoch = Math.max(localNum(await vault.getEpoch?.()), localNum(await store.get("epoch")), epoch);
398
+ docMat = await deriveDocMaterial(meshKey, topicName);
399
+ sigKey = await importAesKey(await hkdf(meshKey, `swal-signal/v1|${topicName}`));
400
+ instanceId =
401
+ opts.instance ?? (root ? await fingerprint(b64uDecode(root.pub)) : "");
402
+ dataRid = await deriveRoomId(meshKey, appId, topicName, epoch, instanceId || undefined);
403
+ if (curRot && curRot.epoch !== epoch)
404
+ curRot = null;
405
+ }
406
+ // ---- link I/O: per-link ordered queue; messages above maxFrame are fragmented (H5) ----
407
+ const outQ = new WeakMap();
408
+ const outBytes = new WeakMap(); // finding 4: bytes accepted for a link, not handed over yet
409
+ function sendBytes(link, bytes, alive = () => true) {
410
+ const pending = outBytes.get(link) ?? 0;
411
+ // finding 4: a peer that drains far slower than we produce must not make us buffer without bound. A single
412
+ // message of any size (up to maxMessageBytes) may wait alone; beyond MAX_OUTSTANDING of backlog the link is
413
+ // closed (the peer resyncs from state vectors when it reconnects).
414
+ if (pending > 0 && pending + bytes.length > MAX_OUTSTANDING) {
415
+ err(new Error(`link ${link.id}: more than ${MAX_OUTSTANDING} bytes waiting for a slow peer: closing it`));
416
+ for (const r of [...links])
417
+ if (r.link === link)
418
+ closeRec(r);
419
+ try {
420
+ link.close();
421
+ }
422
+ catch { }
423
+ return Promise.resolve();
424
+ }
425
+ outBytes.set(link, pending + bytes.length);
426
+ const next = (outQ.get(link) ?? Promise.resolve())
427
+ .then(async () => {
428
+ try {
429
+ if (!alive())
430
+ return;
431
+ for (const f of await fragment(bytes, maxFrame)) {
432
+ if (link.drain)
433
+ await link.drain(); // BL3: paced by the peer instead of piling up in memory
434
+ if (!alive())
435
+ return;
436
+ link.send(f);
437
+ }
438
+ }
439
+ finally {
440
+ outBytes.set(link, Math.max(0, (outBytes.get(link) ?? 0) - bytes.length));
441
+ }
442
+ })
443
+ .catch(err);
444
+ outQ.set(link, next);
445
+ return next;
446
+ }
447
+ /**
448
+ * finding 4: control frames (rotations, link handshake) never wait behind bulk data nor on drain(): handed to the
449
+ * link at once (`sendPriority` puts them at the next message boundary of the link's own queue when it has one).
450
+ */
451
+ async function sendPriority(link, bytes) {
452
+ for (const f of await fragment(bytes, maxFrame))
453
+ (link.sendPriority ?? link.send).call(link, f);
454
+ }
455
+ const signCache = new WeakMap();
456
+ let selfSess = randomBytes(8); // this instance's sender session (a restart is a new session)
457
+ let selfSeq = 0;
458
+ function signFor(rid, kind, body) {
459
+ const c = signCache.get(body); // a broadcast signs once (same seq) for all links
460
+ if (c && c.rid === rid && c.kind === kind)
461
+ return c.p;
462
+ if (selfSeq >= 0xffffffff) {
463
+ selfSess = randomBytes(8);
464
+ selfSeq = 0;
465
+ }
466
+ const sess = selfSess;
467
+ const seq = ++selfSeq;
468
+ const p = vault
469
+ .sign(frameSigBytes(rid, vault.deviceId, kind, sess, seq, body))
470
+ .then((sig) => ({ sig, sess, seq }));
471
+ signCache.set(body, { rid, kind, p });
472
+ return p;
473
+ }
474
+ // receiver side of S1: highest seq + recently seen seqs per (sender, session)
475
+ const replay = new Map();
476
+ /** R4-S1: the same test as freshSeq, without recording: run BEFORE the signature check (recorded after it). */
477
+ function seqFresh(sender, sess, seq) {
478
+ const w = replay.get(`${sender}|${sess}`);
479
+ return !w || !(seq + REPLAY_WINDOW <= w.max || w.seen.has(seq));
480
+ }
481
+ function freshSeq(sender, sess, seq) {
482
+ const k = `${sender}|${sess}`;
483
+ let w = replay.get(k);
484
+ if (!w) {
485
+ if (replay.size >= 4096)
486
+ replay.delete(replay.keys().next().value);
487
+ w = { max: 0, seen: new Set() };
488
+ replay.set(k, w);
489
+ }
490
+ if (seq + REPLAY_WINDOW <= w.max || w.seen.has(seq))
491
+ return false;
492
+ w.seen.add(seq);
493
+ if (seq > w.max)
494
+ w.max = seq;
495
+ if (w.seen.size > 2 * REPLAY_WINDOW)
496
+ for (const x of w.seen)
497
+ if (x + REPLAY_WINDOW <= w.max)
498
+ w.seen.delete(x);
499
+ return true;
500
+ }
501
+ function sendFrame(rec, kind, body) {
502
+ const lg = rec.legacy;
503
+ return sendFrameWith(rec, kind, body, lg ? lg.material : docMat, lg ? lg.rid : dataRid);
504
+ }
505
+ /** Recent retired keys (newest first): rotation frames are also sealed under them, and receivers try them. */
506
+ const retiredKeys = (n = RETIRED_TRY) => [...legacy.values()].sort((x, y) => y.epoch - x.epoch).slice(0, n);
507
+ /**
508
+ * A rotation must reach peers still on the previous key or on a concurrent branch: its security-document frame is
509
+ * sealed under the current key, the recent retired ones and the key the peer is known to hold.
510
+ */
511
+ async function sendTrustEverywhere(rec, body) {
512
+ if (rec.legacy)
513
+ return sendFrameWith(rec, K_TRUST, body, rec.legacy.material, rec.legacy.rid, true);
514
+ await sendFrameWith(rec, K_TRUST, body, docMat, dataRid, true);
515
+ const recent = retiredKeys();
516
+ for (const r of recent)
517
+ await sendFrameWith(rec, K_TRUST, body, r.material, r.rid, true);
518
+ const c = rec.common;
519
+ if (c && c.rid !== dataRid && !recent.some((r) => r.rid === c.rid))
520
+ await sendFrameWith(rec, K_TRUST, body, c.material, c.rid, true);
521
+ }
522
+ async function sendFrameWith(rec, kind, body, mat, rid, priority = false) {
523
+ if (!mat || rec.closing)
524
+ return;
525
+ const key = await senderKey(mat, vault.deviceId);
526
+ const id = utf8(vault.deviceId);
527
+ let inner;
528
+ if (signFrames) {
529
+ const { sig, sess, seq } = await signFor(rid, kind, body);
530
+ inner = concat(new Uint8Array([kind]), sess, u32(seq), new Uint8Array([sig.length >> 8, sig.length & 0xff]), sig, body);
531
+ }
532
+ else
533
+ inner = concat(new Uint8Array([kind]), body);
534
+ const sealed = await sealUpdate(key, inner, `${rid}|${vault.deviceId}`);
535
+ if (rec.closing)
536
+ return;
537
+ const frame = concat(new Uint8Array([signFrames ? F_SDATA : F_DATA, id.length]), id, sealed);
538
+ if (priority)
539
+ return sendPriority(rec.link, frame);
540
+ await sendBytes(rec.link, frame, () => !rec.closing);
541
+ }
542
+ const established = () => [...links].filter((l) => !l.closing &&
543
+ !l.legacy &&
544
+ (!signFrames || l.authed) &&
545
+ (l.rid === dataRid || l.rid === "") &&
546
+ !(l.deviceId && isRevoked(l.deviceId)));
547
+ /** Synchronous: the link leaves `links` right now (the transport's onClose may fire much later). */
548
+ function closeRec(rec) {
549
+ rec.closing = true;
550
+ links.delete(rec);
551
+ try {
552
+ rec.link.close();
553
+ }
554
+ catch { }
555
+ setStatus();
556
+ }
557
+ const broadcast = (kind, body, except) => {
558
+ for (const l of established())
559
+ if (l !== except)
560
+ sendFrame(l, kind, body).catch(err);
561
+ };
562
+ // one stable sender per link: the pairing state machines identify the active link by it
563
+ const pairSenders = new WeakMap();
564
+ function sendPair(link) {
565
+ let f = pairSenders.get(link);
566
+ if (!f) {
567
+ f = (m) => void sendBytes(link, concat(new Uint8Array([F_PAIR]), utf8(JSON.stringify(m))));
568
+ pairSenders.set(link, f);
569
+ }
570
+ return f;
571
+ }
572
+ const reject = (reason, from) => emit("rejected", { reason, from });
573
+ const heldAt = new WeakMap(); // first time a frame was held (kept across replays)
574
+ let heldTotal = 0; // S5: bytes held over all links
575
+ function dropHeld(rec) {
576
+ if (rec.held)
577
+ heldTotal -= rec.held.bytes;
578
+ rec.held = undefined;
579
+ }
580
+ function hold(rec, data) {
581
+ if (heldTotal + data.length > HOLD_TOTAL_BYTES) {
582
+ rec.lostHeld = true; // S5: global cap (the sender resyncs later; SF4 re-challenges the link)
583
+ return;
584
+ }
585
+ if (!rec.held)
586
+ rec.held = { frames: [], bytes: 0 };
587
+ const h = rec.held;
588
+ const t = heldAt.get(data) ?? now();
589
+ heldAt.set(data, t);
590
+ h.frames.push({ d: data, t });
591
+ h.bytes += data.length;
592
+ heldTotal += data.length;
593
+ while (h.frames.length > HOLD_MAX_FRAMES || h.bytes > HOLD_MAX_BYTES) {
594
+ const n = h.frames.shift().d.length;
595
+ h.bytes -= n;
596
+ heldTotal -= n;
597
+ rec.lostHeld = true;
598
+ }
599
+ }
600
+ /** R5-S2: frames that arrived before the peer's K_AUTH, kept sealed; all links share EARLY_TOTAL bytes. */
601
+ let earlyTotal = 0;
602
+ function keepEarly(rec, data) {
603
+ if (!rec.early)
604
+ rec.early = { frames: [], bytes: 0 };
605
+ const e = rec.early;
606
+ if (e.frames.length < EARLY_MAX_FRAMES &&
607
+ e.bytes + data.length <= PRE_AUTH_MAX &&
608
+ earlyTotal + data.length <= EARLY_TOTAL) {
609
+ e.frames.push(data);
610
+ e.bytes += data.length;
611
+ earlyTotal += data.length;
612
+ }
613
+ }
614
+ function dropEarly(rec) {
615
+ if (rec.early)
616
+ earlyTotal -= rec.early.bytes;
617
+ rec.early = undefined;
618
+ }
619
+ /** R5-S2: handshake frames verified on any link (sender|session|seq) */
620
+ const hsGlobal = new Lru(8192);
621
+ /** Re-run held frames once the trust state changed (a pending admission may have arrived). */
622
+ function replayHeld() {
623
+ for (const rec of links) {
624
+ const h = rec.held;
625
+ if (!h || rec.closing)
626
+ continue;
627
+ dropHeld(rec);
628
+ for (const f of h.frames) {
629
+ if (now() - f.t > HOLD_MS) {
630
+ rec.lostHeld = true;
631
+ continue;
632
+ }
633
+ rec.chain = rec.chain.then(() => onData(rec, f.d)).catch(err);
634
+ }
635
+ }
636
+ // SF4: a link that lost held frames (expired or over the caps) may have lost its handshake: challenge again
637
+ if (signFrames) {
638
+ for (const rec of links) {
639
+ if (!rec.lostHeld || rec.authed || rec.rid === "pair")
640
+ continue;
641
+ rec.lostHeld = false;
642
+ resendHello(rec);
643
+ }
644
+ }
645
+ }
646
+ async function onData(rec, data) {
647
+ const lg = rec.legacy;
648
+ if (!running || rec.closing || data.length < 2)
649
+ return;
650
+ const signed = data[0] === F_SDATA;
651
+ if (!signed && signFrames)
652
+ return reject("unsigned frame");
653
+ const idLen = data[1];
654
+ if (data.length < 2 + idLen)
655
+ return;
656
+ const sender = fromUtf8(data.subarray(2, 2 + idLen));
657
+ if (!isDeviceId(sender))
658
+ return;
659
+ if (rec.deviceId && rec.deviceId !== sender)
660
+ return reject("frame sender does not match the link", sender);
661
+ const revoked = isRevoked(sender);
662
+ // signed frames from a sender whose admission has not reached us yet: hold them (still encrypted, no key derived)
663
+ if (signed && !revoked && !trusted.has(sender)) {
664
+ if (!lg)
665
+ hold(rec, data);
666
+ return;
667
+ }
668
+ // the link's key first; on a live link also the recent retired keys, which may only carry rotations (B4)
669
+ const tries = lg
670
+ ? [lg]
671
+ : [
672
+ { material: docMat, rid: dataRid, epoch },
673
+ ...retiredKeys(MAX_RETIRED),
674
+ ];
675
+ let plain = null;
676
+ let rid = "";
677
+ let retired = false;
678
+ let via = null; // the key this frame came under
679
+ for (let i = 0; i < tries.length && !plain; i++) {
680
+ const t = tries[i];
681
+ if (!t.material)
682
+ continue;
683
+ try {
684
+ plain = await openUpdate(await senderKey(t.material, sender), data.subarray(2 + idLen), `${t.rid}|${sender}`);
685
+ rid = t.rid;
686
+ retired = i > 0;
687
+ via = { material: t.material, rid: t.rid, epoch: t.epoch };
688
+ }
689
+ catch { } // wrong key / tampered / other epoch: try the next one, else drop silently
690
+ }
691
+ if (!plain || rec.closing || plain.length < 1)
692
+ return;
693
+ const kind = plain[0];
694
+ let body = plain.subarray(1);
695
+ let sess = "";
696
+ if (signed) {
697
+ if (plain.length < 15)
698
+ return reject("malformed signed frame", sender);
699
+ const sessBytes = plain.subarray(1, 9);
700
+ const seq = new DataView(plain.buffer, plain.byteOffset + 9, 4).getUint32(0, false);
701
+ const sigLen = (plain[13] << 8) | plain[14];
702
+ if (plain.length < 15 + sigLen)
703
+ return reject("malformed signed frame", sender);
704
+ const sig = plain.subarray(15, 15 + sigLen);
705
+ body = plain.subarray(15 + sigLen);
706
+ sess = b64uEncode(sessBytes);
707
+ if (revoked)
708
+ return; // a revoked device is never listened to
709
+ const pub = trusted.get(sender)?.pub;
710
+ if (!pub)
711
+ return;
712
+ // R4-S1: everything that can be decided without the (2 ms) signature check is decided first
713
+ const hsKey = `${sender}|${sess}|${seq}`;
714
+ if (rec.authed) {
715
+ if (sess !== rec.peerSess || !seqFresh(sender, sess, seq))
716
+ return; // replay / other link
717
+ }
718
+ else if (kind !== K_HELLO && kind !== K_AUTH) {
719
+ // the peer authenticated us first and already talks: keep a few frames (unverified, still sealed) until its
720
+ // K_AUTH reaches us; they are verified once, when they are run again after it
721
+ keepEarly(rec, data);
722
+ return;
723
+ }
724
+ else {
725
+ // a replayed handshake frame (on this link or any other): dropped unverified
726
+ if (rec.hsSeen?.has(hsKey) || hsGlobal.has(hsKey))
727
+ return;
728
+ rec.hsChecks = (rec.hsChecks ?? 0) + 1;
729
+ if (rec.hsChecks > HS_VERIFY_MAX ||
730
+ !budget(budgets.hs, sender, 1, HS_VERIFY_PER_ID)) {
731
+ reject("too many handshake frames", sender);
732
+ return closeRec(rec);
733
+ }
734
+ }
735
+ let ok = false;
736
+ try {
737
+ ok = identityVerify(b64uDecode(pub), frameSigBytes(rid, sender, kind, sessBytes, seq, body), sig);
738
+ }
739
+ catch { }
740
+ if (!ok)
741
+ return reject("bad frame signature", sender);
742
+ if (rec.closing)
743
+ return;
744
+ if (!rec.authed) {
745
+ if (!rec.hsSeen)
746
+ rec.hsSeen = new Set();
747
+ if (rec.hsSeen.size < 2 * HS_VERIFY_MAX)
748
+ rec.hsSeen.add(hsKey);
749
+ hsGlobal.set(hsKey, true);
750
+ }
751
+ else if (kind === K_HELLO || kind === K_AUTH)
752
+ freshSeq(sender, sess, seq); // (other kinds: recorded below)
753
+ // S1: a link carries nothing but its handshake until the peer signed OUR fresh challenge on it
754
+ // the handshake may arrive under one of our recent retired keys (a key switch raced with it, or the peer is on
755
+ // another branch): it is then bound to THAT key's room and epoch, and answered under the same key
756
+ if (kind === K_HELLO) {
757
+ if (body.length !== NONCE_BYTES || !via)
758
+ return;
759
+ rec.common = via;
760
+ rec.hellosIn = (rec.hellosIn ?? 0) + 1;
761
+ // R5-S2: in a retired room a sender is answered a couple of times per minute at most (whatever the links)
762
+ const mayAnswer = !rec.legacy ||
763
+ budget(budgets.legacyAnswer, `${rec.rid}|${sender}`, 1, LEGACY_ANSWERS_PER_MIN);
764
+ if ((rec.answers ?? 0) < HANDSHAKE_MAX && mayAnswer)
765
+ await answerHello(rec, body, sender, via);
766
+ // SF4: a repeated challenge means our earlier messages may have been lost (e.g. held and expired while
767
+ // the peer was not yet admitted here): challenge again too
768
+ if (rec.hellosIn > 1 && !rec.authed)
769
+ resendHello(rec);
770
+ return;
771
+ }
772
+ if (kind === K_AUTH) {
773
+ if (rec.authed || !via)
774
+ return;
775
+ const ep = via.epoch;
776
+ const okAuth = rec.nonce !== undefined &&
777
+ body.length > NONCE_BYTES + 4 &&
778
+ equalBytes(body.subarray(0, NONCE_BYTES), rec.nonce) &&
779
+ new DataView(body.buffer, body.byteOffset + NONCE_BYTES, 4).getUint32(0, false) === ep &&
780
+ fromUtf8(body.subarray(NONCE_BYTES + 4)) === vault.deviceId;
781
+ if (!okAuth)
782
+ return reject("bad link authentication", sender);
783
+ rec.common = via;
784
+ rec.deviceId = sender;
785
+ rec.authed = true;
786
+ rec.peerSess = sess;
787
+ rec.lift?.();
788
+ setStatus();
789
+ const early = rec.early?.frames ?? [];
790
+ dropEarly(rec);
791
+ for (const f of early)
792
+ rec.chain = rec.chain.then(() => onData(rec, f)).catch(err);
793
+ return maybeStart(rec);
794
+ }
795
+ if (!rec.authed) {
796
+ // the peer authenticated us first and already talks: keep a few frames until its K_AUTH reaches us
797
+ keepEarly(rec, data);
798
+ return;
799
+ }
800
+ if (sess !== rec.peerSess || !freshSeq(sender, sess, seq))
801
+ return; // replay / other link
802
+ }
803
+ else if (revoked)
804
+ return;
805
+ if (retired) {
806
+ // security documents travel under any key the peer may hold (a straggler, a peer on another branch)
807
+ if (kind === K_TRUST && via)
808
+ await handleTrust(rec, body, sender, via);
809
+ // (d) an authenticated peer still talks under one of our retired keys: it missed a rotation. Offer it the
810
+ // stored rotations from that key's epoch (once per link and current rotation).
811
+ else if (via && rec.authed)
812
+ offerRotations(rec, sender, via);
813
+ return;
814
+ }
815
+ if (!rec.deviceId) {
816
+ rec.deviceId = sender; // legacy unsigned wire only: the link is bound to its first sender
817
+ rec.lift?.();
818
+ setStatus();
819
+ }
820
+ if (lg) {
821
+ // retired room: only security documents (the peer catches up with the rotations and revocations it missed)
822
+ if (kind === K_TRUST)
823
+ await handleTrust(rec, body, sender, lg);
824
+ return;
825
+ }
826
+ if (kind === K_SV) {
827
+ await sendFrame(rec, K_UPDATE, Y.encodeStateAsUpdate(doc, body));
828
+ }
829
+ else if (kind === K_UPDATE) {
830
+ if (opts.authorizeUpdate && !(await opts.authorizeUpdate(sender, body)))
831
+ return reject("update not authorized", sender);
832
+ if (rec.closing)
833
+ return;
834
+ Y.applyUpdate(doc, body, ORIGIN);
835
+ }
836
+ else if (kind === K_AWARENESS) {
837
+ applyAwarenessUpdate(awareness, body, ORIGIN);
838
+ }
839
+ else if (kind === K_TRUST) {
840
+ await handleTrust(rec, body, sender, {
841
+ material: docMat,
842
+ rid: dataRid,
843
+ epoch,
844
+ });
845
+ }
846
+ else if (kind === K_CHANNEL) {
847
+ const n = body.length >= 2 ? (body[0] << 8) | body[1] : -1;
848
+ if (n < 0 || body.length < 2 + n)
849
+ return reject("malformed channel frame", sender);
850
+ const ns = fromUtf8(body.subarray(2, 2 + n));
851
+ const k = ns.slice(ns.lastIndexOf("/") + 1);
852
+ if (ns !== chanNs(k))
853
+ return reject("foreign channel", sender);
854
+ const payload = body.subarray(2 + n);
855
+ for (const cb of [...(chanSubs.get(k) ?? [])]) {
856
+ try {
857
+ cb(payload, sender);
858
+ }
859
+ catch (e) {
860
+ err(e);
861
+ }
862
+ }
863
+ }
864
+ }
865
+ // ---- channels: app/instance-namespaced message streams over the same encrypted + signed frames ----
866
+ const chanSubs = new Map();
867
+ const chanNs = (kind) => `${meshNamespace(appId, instanceId)}/${kind}`;
868
+ function channel(kind) {
869
+ if (!/^[A-Za-z0-9._-]{1,64}$/.test(kind))
870
+ throw new Error(`invalid channel kind "${kind}"`);
871
+ const mine = new Set();
872
+ return {
873
+ get namespace() {
874
+ return chanNs(kind);
875
+ },
876
+ async send(data, o = {}) {
877
+ if (!running)
878
+ throw new Error("mesh is not connected");
879
+ const ns = utf8(chanNs(kind));
880
+ const body = concat(new Uint8Array([ns.length >> 8, ns.length & 0xff]), ns, data);
881
+ const targets = established().filter((l) => l.deviceId && (o.to === undefined || l.deviceId === o.to));
882
+ await Promise.all(targets.map((l) => sendFrame(l, K_CHANNEL, body)));
883
+ },
884
+ onMessage(cb) {
885
+ const set = chanSubs.get(kind) ?? new Set();
886
+ chanSubs.set(kind, set);
887
+ set.add(cb);
888
+ mine.add(cb);
889
+ return () => {
890
+ set.delete(cb);
891
+ mine.delete(cb);
892
+ };
893
+ },
894
+ close() {
895
+ for (const cb of mine)
896
+ chanSubs.get(kind)?.delete(cb);
897
+ mine.clear();
898
+ },
899
+ };
900
+ }
901
+ const linkKey = (l) => l.legacy ?? { material: docMat, rid: dataRid, epoch };
902
+ const budgets = {
903
+ serve: new Map(),
904
+ fail: new Map(),
905
+ hs: new Map(),
906
+ legacyAnswer: new Map(),
907
+ };
908
+ /** Per-peer budget over a sliding minute; false when `n` more would exceed `max`. */
909
+ function budget(m, peer, n, max) {
910
+ const t = now();
911
+ let b = m.get(peer);
912
+ if (!b || t - b.t > 60_000) {
913
+ b = { t, n: 0 };
914
+ m.delete(peer);
915
+ m.set(peer, b);
916
+ if (m.size > 4096)
917
+ m.delete(m.keys().next().value);
918
+ }
919
+ if (b.n + n > max)
920
+ return false;
921
+ b.n += n;
922
+ return true;
923
+ }
924
+ function sendTrust(rec, msg, key) {
925
+ const body = utf8(JSON.stringify(msg));
926
+ if (rec.legacy)
927
+ return sendFrameWith(rec, K_TRUST, body, rec.legacy.material, rec.legacy.rid, true);
928
+ return sendFrameWith(rec, K_TRUST, body, key.material, key.rid, true);
929
+ }
930
+ /** Documents in batches (count and bytes bounded). */
931
+ async function sendDocs(rec, docs, key) {
932
+ let batch = [];
933
+ let bytes = 0;
934
+ for (const d of docs) {
935
+ const n = JSON.stringify(d).length;
936
+ if (batch.length &&
937
+ (batch.length >= TRUST_DOCS_MAX || bytes + n > TRUST_BATCH_BYTES)) {
938
+ await sendTrust(rec, { t: "docs", docs: batch }, key);
939
+ batch = [];
940
+ bytes = 0;
941
+ }
942
+ batch.push(d);
943
+ bytes += n;
944
+ }
945
+ if (batch.length)
946
+ await sendTrust(rec, { t: "docs", docs: batch }, key);
947
+ }
948
+ /** Tell every authenticated peer (but `except`) about new documents. */
949
+ function announce(keys, except) {
950
+ if (!keys.length)
951
+ return;
952
+ for (const l of [...links]) {
953
+ if (l === except ||
954
+ l.closing ||
955
+ l.rid === "pair" ||
956
+ !l.deviceId ||
957
+ isRevoked(l.deviceId))
958
+ continue;
959
+ if (signFrames && !l.authed)
960
+ continue;
961
+ void sendTrust(l, { t: "inv", ids: keys }, linkKey(l)).catch(err);
962
+ }
963
+ }
964
+ /** Add documents made or received here, and announce the new ones. */
965
+ async function addLocal(docs, except, from) {
966
+ if (!sec)
967
+ return [];
968
+ // R6-S2b: `from` is the peer that sent these documents, so the trust store can charge its waiting slots to it
969
+ const outs = await sec.addMany(docs, { from });
970
+ announce(outs
971
+ .filter((o) => o.status === "accepted" && o.id)
972
+ .map((o) => o.id), except);
973
+ return outs;
974
+ }
975
+ async function handleTrust(rec, body, sender, key) {
976
+ if (!sec)
977
+ return;
978
+ let m;
979
+ try {
980
+ m = JSON.parse(fromUtf8(body));
981
+ }
982
+ catch {
983
+ return reject("malformed trust message", sender);
984
+ }
985
+ if (!m || typeof m !== "object")
986
+ return;
987
+ if (m.t === "inv" &&
988
+ Array.isArray(m.ids) &&
989
+ m.ids.length <= TRUST_INV_MAX) {
990
+ const want = sec.missing(m.ids).slice(0, TRUST_WANT_MAX);
991
+ if (want.length)
992
+ await sendTrust(rec, { t: "want", ids: want }, key);
993
+ }
994
+ else if (m.t === "want" &&
995
+ Array.isArray(m.ids) &&
996
+ m.ids.length <= TRUST_WANT_MAX) {
997
+ const ids = m.ids.filter((x) => typeof x === "string");
998
+ if (!budget(budgets.serve, sender, ids.length, TRUST_SERVE_PER_MIN))
999
+ return reject("security documents asked too often", sender);
1000
+ await sendDocs(rec, await sec.get(ids), key);
1001
+ }
1002
+ else if (m.t === "docs" &&
1003
+ Array.isArray(m.docs) &&
1004
+ m.docs.length <= TRUST_DOCS_MAX) {
1005
+ // a sender whose documents keep failing (bad signatures, junk) is cut off for a while: each costs a check
1006
+ for (let i = 0; i < m.docs.length; i += 16) {
1007
+ if (!budget(budgets.fail, sender, 0, TRUST_FAIL_PER_MIN))
1008
+ return reject("too many bad security documents", sender);
1009
+ const outs = await addLocal(m.docs.slice(i, i + 16), rec, sender);
1010
+ for (const o of outs)
1011
+ if (o.status === "rejected")
1012
+ reject(o.reason ?? "invalid security document", sender);
1013
+ const bad = outs.filter((o) => o.status === "rejected").length;
1014
+ if (bad && !budget(budgets.fail, sender, bad, TRUST_FAIL_PER_MIN))
1015
+ return reject("too many bad security documents", sender);
1016
+ }
1017
+ }
1018
+ }
1019
+ /** Rotation documents `peer` (still on a key of `fromEpoch`) can adopt, best last (at most MAX_EPOCH_SKIP ahead). */
1020
+ function rotationsFor(peer, fromEpoch) {
1021
+ if (!sec)
1022
+ return [];
1023
+ return sec
1024
+ .rotations()
1025
+ .filter((r) => r.to.includes(peer) &&
1026
+ r.epoch > fromEpoch &&
1027
+ r.epoch <= fromEpoch + MAX_EPOCH_SKIP &&
1028
+ r.epoch <= epoch)
1029
+ .map(({ id: _id, ...d }) => d);
1030
+ }
1031
+ const offered = new Set();
1032
+ /** (d) a peer still talks under one of our retired keys: hand it what it missed, under that key. */
1033
+ function offerRotations(rec, peer, via) {
1034
+ const k = `${curRot?.id ?? epoch}|${peer}|${via.epoch}`;
1035
+ if (offered.has(k) || !sec)
1036
+ return;
1037
+ if (offered.size >= 4096)
1038
+ offered.clear();
1039
+ offered.add(k);
1040
+ void (async () => {
1041
+ await sendDocs(rec, rotationsFor(peer, via.epoch), via);
1042
+ if (sec)
1043
+ await sendTrust(rec, { t: "inv", ids: sec.inventory() }, via);
1044
+ })().catch(err);
1045
+ }
1046
+ // ---- rotations (owner only issues them; every device adopts the best one it can open) ----
1047
+ /**
1048
+ * Adopt the best rotation we are a recipient of (highest epoch, then lowest id), one window of MAX_EPOCH_SKIP at a
1049
+ * time. Only owner-identity devices issue rotations, so concurrency only exists between devices of the owner;
1050
+ * every device applies the same rule to the same set. Rotations are documents: whatever order they arrive in, and
1051
+ * whoever relays them, every device ends on the same one.
1052
+ */
1053
+ async function considerRotations() {
1054
+ if (!sec || !root || !running)
1055
+ return;
1056
+ const me = vault.deviceId;
1057
+ for (let guard = 0; guard < 64; guard++) {
1058
+ const ownerKex = kexOf(root.deviceId);
1059
+ for (const r of sec.rotations()) {
1060
+ if (!curRot && curRotId === r.id && r.epoch === epoch)
1061
+ curRot = r; // the rotation our pairing key came from
1062
+ if (r.epoch < epoch ||
1063
+ r.id === curRot?.id ||
1064
+ cands.has(r.id) ||
1065
+ unwrapFailed.has(r.id))
1066
+ continue;
1067
+ if (r.epoch > epoch + MAX_EPOCH_SKIP ||
1068
+ !r.to.includes(me) ||
1069
+ !r.wraps[me])
1070
+ continue;
1071
+ if (opts.canRotate) {
1072
+ let ok = true;
1073
+ for (const t of r.revoked)
1074
+ if (!(await opts.canRotate(r.from, t)))
1075
+ ok = false;
1076
+ if (!ok) {
1077
+ unwrapFailed.add(r.id);
1078
+ emit("rejected", {
1079
+ reason: "rotation not authorized",
1080
+ from: r.from,
1081
+ epoch: r.epoch,
1082
+ });
1083
+ continue;
1084
+ }
1085
+ }
1086
+ if (!ownerKex)
1087
+ continue; // the owner's key record has not arrived yet: tried again on the next change
1088
+ try {
1089
+ const key = await unwrapMeshKey((await ecdhIdentity()).privateKey, (await kemIdentity()).secretKey, ownerKex.pub, await rotationPreId(r), r.from, me, r.wraps[me]);
1090
+ cands.set(r.id, { rec: r, key });
1091
+ }
1092
+ catch {
1093
+ unwrapFailed.add(r.id);
1094
+ emit("rejected", {
1095
+ reason: "rotation wrap does not authenticate",
1096
+ from: r.from,
1097
+ epoch: r.epoch,
1098
+ });
1099
+ }
1100
+ }
1101
+ const cur = { epoch, id: curRot ? curRot.id : (curRotId ?? "\uffff") }; // genesis/unknown loses to any rotation of its epoch
1102
+ let best = null;
1103
+ for (const [id, c] of cands) {
1104
+ if (c.rec.epoch < epoch) {
1105
+ cands.delete(id);
1106
+ continue;
1107
+ }
1108
+ if (betterRot(c.rec, best ? best.rec : cur))
1109
+ best = c;
1110
+ }
1111
+ if (!best)
1112
+ break;
1113
+ await adopt(best);
1114
+ }
1115
+ while (cands.size > MAX_CANDIDATES)
1116
+ cands.delete(cands.keys().next().value);
1117
+ await ownerRekey();
1118
+ }
1119
+ async function adopt(c) {
1120
+ // (c) the rotation we leave stays a candidate (same epoch): nothing is lost if the choice has to be revisited
1121
+ if (curRot &&
1122
+ meshKey &&
1123
+ curRot.epoch === c.rec.epoch &&
1124
+ curRot.id !== c.rec.id)
1125
+ cands.set(curRot.id, { rec: curRot, key: meshKey });
1126
+ const { oldEpoch, oldKey, oldRid } = await switchEpoch(c.key, c.rec.epoch);
1127
+ curRot = c.rec;
1128
+ curRotId = c.rec.id;
1129
+ await store.set("rotId", curRotId);
1130
+ cands.delete(c.rec.id);
1131
+ for (const [id, x] of cands)
1132
+ if (x.rec.epoch < epoch)
1133
+ cands.delete(id);
1134
+ for (const l of [...links])
1135
+ if (l.deviceId && isRevoked(l.deviceId))
1136
+ closeRec(l);
1137
+ await rememberRetired(oldEpoch, oldKey, oldRid);
1138
+ void refreshTrust();
1139
+ for (const t of c.rec.revoked)
1140
+ emit("revoked", { deviceId: t, epoch });
1141
+ }
1142
+ /**
1143
+ * A known revocation has not been executed by an owner rotation yet: the devices it cut off may hold the current
1144
+ * key, whenever they were paired (R5-B2). The UI shows it as "pendiente de que el dueño se conecte".
1145
+ */
1146
+ const rekeyPending = () => sec ? sec.unexecuted().devices.length > 0 : false;
1147
+ /**
1148
+ * Members a rotation left out although they were admitted before it (e.g. their key record was not known yet), or
1149
+ * members whose grant handed over no key (re-anchored): the owner re-keys to include them.
1150
+ */
1151
+ function missed() {
1152
+ if (!sec || !curRot)
1153
+ return [];
1154
+ const cr = curRot;
1155
+ const out = [];
1156
+ for (const d of trusted.values()) {
1157
+ if (d.deviceId === root?.deviceId || cr.to.includes(d.deviceId))
1158
+ continue;
1159
+ const g = sec.memberGrant(d.deviceId);
1160
+ if (!g || (g.epoch !== undefined && g.epoch >= cr.epoch))
1161
+ continue;
1162
+ if (kexOf(d.deviceId))
1163
+ out.push(d.deviceId);
1164
+ }
1165
+ return out;
1166
+ }
1167
+ /**
1168
+ * Owner devices only: re-key at epoch + 1 without every device a revocation cut off that no owner rotation has
1169
+ * executed yet (an admin's revocation is executed here), and to include members a previous rotation missed. Runs
1170
+ * after every change of the security state (R5-S3: a re-key refused once is retried as soon as it can work).
1171
+ */
1172
+ async function ownerRekey() {
1173
+ if (!running ||
1174
+ !sec ||
1175
+ !root ||
1176
+ root.deviceId !== vault.deviceId ||
1177
+ destroyed)
1178
+ return;
1179
+ const u = sec.unexecuted();
1180
+ if (u.devices.length === 0 && missed().length === 0)
1181
+ return;
1182
+ // R4-S3: no rotation lists more than a receiver accepts: cut in several rotations if needed
1183
+ const chunks = [];
1184
+ let cur = [];
1185
+ let refs = 0;
1186
+ for (const dev of u.devices) {
1187
+ const n = sec.trust.grantsOf(dev).length;
1188
+ if (cur.length &&
1189
+ (cur.length >= MAX_ROT_MEMBERS || refs + n > MAX_ROT_REFS)) {
1190
+ chunks.push(cur);
1191
+ cur = [];
1192
+ refs = 0;
1193
+ }
1194
+ cur.push(dev);
1195
+ refs += n;
1196
+ }
1197
+ chunks.push(cur);
1198
+ for (const chunk of chunks) {
1199
+ if (!running || destroyed || !sec)
1200
+ return;
1201
+ try {
1202
+ await rotate(sec.unexecuted(new Set(chunk)));
1203
+ }
1204
+ catch (e) {
1205
+ err(e);
1206
+ return;
1207
+ }
1208
+ }
1209
+ }
1210
+ async function onPairFrame(rec, data) {
1211
+ if (data.length > PAIR_MAX)
1212
+ return reject("pairing message too large"); // S5
1213
+ let msg;
1214
+ try {
1215
+ msg = JSON.parse(fromUtf8(data.subarray(1)));
1216
+ }
1217
+ catch {
1218
+ return;
1219
+ }
1220
+ if (!msg || typeof msg !== "object")
1221
+ return;
1222
+ const send = sendPair(rec.link);
1223
+ const m = msg;
1224
+ if (hostSession && pairRids.has(rec.rid))
1225
+ await hostSession.s.handle(m, send);
1226
+ else if (guestSession && pairRids.has(rec.rid))
1227
+ await guestSession.s.handle(m, send);
1228
+ }
1229
+ const preAuthBudget = { used: 0, max: PRE_AUTH_TOTAL };
1230
+ /** S1: challenge the peer of a fresh data link; data flows once both sides answered each other's challenge. */
1231
+ function startHandshake(rec) {
1232
+ rec.nonce = randomBytes(NONCE_BYTES);
1233
+ // note 9: an unauthenticated link holds budgets (pre-auth reassembly, held frames): it does not stay forever
1234
+ const t = setTimeout(() => {
1235
+ if (!rec.authed && !rec.closing) {
1236
+ reject("link handshake timed out", rec.deviceId);
1237
+ closeRec(rec);
1238
+ }
1239
+ }, opts.handshakeTimeoutMs ?? 120_000);
1240
+ t.unref?.();
1241
+ rec.link.onClose(() => clearTimeout(t));
1242
+ rec.hellosOut = 1;
1243
+ const lg = rec.legacy;
1244
+ sendFrameWith(rec, K_HELLO, rec.nonce, lg ? lg.material : docMat, lg ? lg.rid : dataRid, true).catch(err);
1245
+ }
1246
+ /** SF4: same challenge again, at most HANDSHAKE_MAX times per link. */
1247
+ function resendHello(rec) {
1248
+ // R5-S2: never in a retired room (a straggler challenges us again by itself)
1249
+ if (!rec.nonce ||
1250
+ rec.authed ||
1251
+ rec.closing ||
1252
+ rec.legacy ||
1253
+ (rec.hellosOut ?? 0) >= HANDSHAKE_MAX)
1254
+ return;
1255
+ rec.hellosOut = (rec.hellosOut ?? 0) + 1;
1256
+ // a fresh copy: a new signature and sequence number, so the peer sees a new frame, not a replay (R4-S1)
1257
+ const nonce = rec.nonce.slice();
1258
+ // under the current AND the recent retired keys: the peer may still be on (or have switched from) any of them
1259
+ void (async () => {
1260
+ const lg = rec.legacy;
1261
+ await sendFrameWith(rec, K_HELLO, nonce, lg ? lg.material : docMat, lg ? lg.rid : dataRid, true);
1262
+ if (!lg)
1263
+ for (const r of retiredKeys())
1264
+ await sendFrameWith(rec, K_HELLO, nonce, r.material, r.rid, true);
1265
+ })().catch(err);
1266
+ }
1267
+ async function answerHello(rec, peerNonce, peer, via) {
1268
+ rec.answers = (rec.answers ?? 0) + 1;
1269
+ rec.authSent = true;
1270
+ // bound to the challenger and to the key the challenge came under (sealed under that same key)
1271
+ await sendFrameWith(rec, K_AUTH, concat(peerNonce, u32(via.epoch), utf8(peer)), via.material, via.rid, true);
1272
+ maybeStart(rec);
1273
+ }
1274
+ function maybeStart(rec) {
1275
+ if (!rec.authed || !rec.authSent || rec.started || rec.closing)
1276
+ return;
1277
+ rec.started = true;
1278
+ const peer = rec.deviceId;
1279
+ if (rec.legacy) {
1280
+ // retired room (BL1): the peer may be a straggler, or a partition that rotated on its own. Hand it the
1281
+ // rotations it can adopt and our inventory; it converges (and re-keys if needed) and does the same for us.
1282
+ const lg = rec.legacy;
1283
+ void (async () => {
1284
+ await sendDocs(rec, rotationsFor(peer, lg.epoch), lg);
1285
+ if (sec)
1286
+ await sendTrust(rec, { t: "inv", ids: sec.inventory() }, lg);
1287
+ })().catch(err);
1288
+ return;
1289
+ }
1290
+ sendFrame(rec, K_SV, Y.encodeStateVector(doc)).catch(err);
1291
+ if (awareness.getLocalState())
1292
+ sendFrame(rec, K_AWARENESS, encodeAwarenessUpdate(awareness, [doc.clientID])).catch(err);
1293
+ if (sec)
1294
+ void sendTrust(rec, { t: "inv", ids: sec.inventory() }, linkKey(rec)).catch(err);
1295
+ // the peer may be on another key of this epoch (its handshake crossed a key switch): push the rotation we are on
1296
+ // if it is a recipient, under every key it may hold
1297
+ const cur = curRot;
1298
+ if (cur && cur.to.includes(peer)) {
1299
+ const { id: _id, ...d } = cur;
1300
+ void sendTrustEverywhere(rec, utf8(JSON.stringify({ t: "docs", docs: [d] }))).catch(err);
1301
+ }
1302
+ }
1303
+ function wireLink(link, rid) {
1304
+ const isData = running && rid === dataRid;
1305
+ const lg = running && !isData ? legacy.get(rid) : undefined;
1306
+ const isPair = pairRids.has(rid);
1307
+ if (rid !== "" && !isData && !isPair && !lg)
1308
+ return link.close();
1309
+ const rec = {
1310
+ link,
1311
+ rid: isPair && !isData ? "pair" : rid,
1312
+ chain: Promise.resolve(),
1313
+ };
1314
+ if (lg)
1315
+ rec.legacy = lg;
1316
+ if (rid === "")
1317
+ rec.rid = ""; // room-less (qr-sdp): frames are self-describing
1318
+ // R5-S2: a bounded number of links still to authenticate per room, and per announced peer
1319
+ if (rec.rid !== "pair") {
1320
+ const pending = [...links].filter((l) => !l.authed && !l.closing && l.rid === rec.rid);
1321
+ if (pending.length >= MAX_UNAUTH_PER_ROOM ||
1322
+ pending.filter((l) => l.link.id === link.id).length >=
1323
+ MAX_UNAUTH_PER_PEER) {
1324
+ reject("too many links waiting to authenticate", link.id);
1325
+ return link.close();
1326
+ }
1327
+ }
1328
+ links.add(rec);
1329
+ // S5: until the link is authenticated it may reassemble at most 1 MiB, charged to a budget shared by all
1330
+ // unauthenticated links; afterwards the configured limit applies
1331
+ const reasm = new Reassembler({
1332
+ maxMessageBytes: PRE_AUTH_MAX,
1333
+ maxPendingBytes: PRE_AUTH_MAX,
1334
+ shared: preAuthBudget,
1335
+ });
1336
+ rec.lift = () => reasm.setLimits({
1337
+ maxMessageBytes: opts.maxMessageBytes ?? DEFAULT_MAX_MESSAGE,
1338
+ maxPendingBytes: opts.maxMessageBytes ?? DEFAULT_MAX_MESSAGE,
1339
+ shared: null,
1340
+ });
1341
+ const dispatch = (d) => d[0] === F_PAIR
1342
+ ? onPairFrame({ ...rec, rid: rid === "" ? ([...pairRids][0] ?? "") : rid }, d)
1343
+ : d[0] === F_DATA || d[0] === F_SDATA
1344
+ ? onData(rec, d)
1345
+ : undefined;
1346
+ link.onMessage((d) => {
1347
+ if (rec.closing)
1348
+ return;
1349
+ rec.chain = rec.chain
1350
+ .then(async () => {
1351
+ if (d[0] !== F_FRAG)
1352
+ return dispatch(d);
1353
+ const whole = await reasm.push(d);
1354
+ if (whole && whole[0] !== F_FRAG && !rec.closing)
1355
+ return dispatch(whole);
1356
+ })
1357
+ .catch(err);
1358
+ });
1359
+ link.onClose(() => {
1360
+ reasm.clear();
1361
+ dropHeld(rec);
1362
+ dropEarly(rec);
1363
+ links.delete(rec);
1364
+ setStatus();
1365
+ });
1366
+ if (isPair && guestSession)
1367
+ guestSession.s.attach(sendPair(link));
1368
+ if (signFrames && (isData || lg || (rid === "" && running)))
1369
+ startHandshake(rec);
1370
+ else if (isData || (rid === "" && running)) {
1371
+ sendFrame(rec, K_SV, Y.encodeStateVector(doc)).catch(err);
1372
+ const st = awareness.getLocalState();
1373
+ if (st)
1374
+ sendFrame(rec, K_AWARENESS, encodeAwarenessUpdate(awareness, [doc.clientID])).catch(err);
1375
+ }
1376
+ if (rid === "" && guestSession)
1377
+ guestSession.s.attach(sendPair(link));
1378
+ setStatus();
1379
+ }
1380
+ // ---- rooms ----
1381
+ let subscribed = false;
1382
+ async function joinRoom(rid, key) {
1383
+ if (rooms.has(rid))
1384
+ return;
1385
+ if (!subscribed) {
1386
+ subscribed = true;
1387
+ for (const t of transports)
1388
+ if (t.kind === "link")
1389
+ linkSubs.push(t.onLink((l, r) => !destroyed && wireLink(l, r)));
1390
+ }
1391
+ const stops = [];
1392
+ rooms.set(rid, () => {
1393
+ for (const s of stops)
1394
+ s();
1395
+ });
1396
+ for (const t of transports) {
1397
+ try {
1398
+ if (t.kind === "link") {
1399
+ await t.join(rid, vault.deviceId);
1400
+ stops.push(() => t.leave(rid));
1401
+ }
1402
+ else {
1403
+ const stop = await connectViaSignaling(t, {
1404
+ rid,
1405
+ selfId: vault.deviceId,
1406
+ key,
1407
+ rtc: opts.rtc,
1408
+ onLink: (l) => !destroyed && wireLink(l, rid),
1409
+ onError: err,
1410
+ });
1411
+ stops.push(stop);
1412
+ }
1413
+ }
1414
+ catch (e) {
1415
+ err(e); // a dead transport must not take the mesh down: others still run
1416
+ }
1417
+ }
1418
+ }
1419
+ function leaveRoom(rid) {
1420
+ rooms.get(rid)?.();
1421
+ rooms.delete(rid);
1422
+ }
1423
+ async function start() {
1424
+ if (running || destroyed)
1425
+ return;
1426
+ await loadKeys();
1427
+ running = true;
1428
+ setStatus();
1429
+ await joinRoom(dataRid, sigKey);
1430
+ await syncLegacy();
1431
+ // adopt what is pending (rotations already held) and, on an owner device, execute what is pending at once (a
1432
+ // revocation recorded before a restart, or a re-key interrupted by the app tearing the mesh down)
1433
+ void serialRot(considerRotations).catch(err);
1434
+ }
1435
+ function stopNetwork() {
1436
+ running = false;
1437
+ for (const rid of [...rooms.keys()])
1438
+ leaveRoom(rid);
1439
+ for (const l of [...links])
1440
+ closeRec(l);
1441
+ links.clear();
1442
+ legacy.clear();
1443
+ for (const u of linkSubs.splice(0))
1444
+ u();
1445
+ subscribed = false;
1446
+ pairRids = new Set();
1447
+ hostSession = null;
1448
+ guestSession = null;
1449
+ setStatus();
1450
+ }
1451
+ // ---- document / awareness propagation ----
1452
+ const onDocUpdate = (u, origin) => {
1453
+ if (origin !== ORIGIN && running)
1454
+ broadcast(K_UPDATE, u);
1455
+ };
1456
+ doc.on("update", onDocUpdate);
1457
+ const onAwareness = (c, origin) => {
1458
+ if (origin === ORIGIN || !running)
1459
+ return;
1460
+ broadcast(K_AWARENESS, encodeAwarenessUpdate(awareness, [
1461
+ ...c.added,
1462
+ ...c.updated,
1463
+ ...c.removed,
1464
+ ]));
1465
+ };
1466
+ awareness.on("update", onAwareness);
1467
+ // ---- rotation ----
1468
+ // The new mesh key never travels under the shared (old) key: it is wrapped per remaining device with a hybrid
1469
+ // key, HKDF(ML-KEM-768 secret || ECDH(owner static key, peer static key), swal-rotate/v4|preId|from|to) -> AES-GCM,
1470
+ // where preId hashes the rotation document (epoch, recipients, cuts, nonce); the owner signs its id. Every device
1471
+ // keeps the recent rotation documents in its local store and hands them to devices that were offline (through a
1472
+ // retired room, see `legacy`); a revoked device has no wrap and cannot unwrap anyone else's.
1473
+ async function switchEpoch(newKey, newEpoch) {
1474
+ // a re-key still in flight when the app destroyed the mesh must not overwrite the vault: the next instance on
1475
+ // the same vault would start from a key nobody else ever received
1476
+ if (destroyed)
1477
+ throw new Error("mesh destroyed during a key change");
1478
+ const oldRid = dataRid;
1479
+ const old = { epoch, rid: oldRid, material: docMat };
1480
+ const oldKey = meshKey;
1481
+ await vault.setMeshKey(newKey);
1482
+ await persistEpoch(newEpoch);
1483
+ epoch = newEpoch;
1484
+ await loadKeys(newKey);
1485
+ // links stay up across the rotation; the retired room stays joined ONLY to serve wraps to stragglers
1486
+ for (const l of links)
1487
+ if (l.rid === oldRid && !l.legacy)
1488
+ l.rid = dataRid;
1489
+ legacy.set(oldRid, old);
1490
+ legacy.delete(dataRid);
1491
+ // SF5: a bounded number of retired rooms (the oldest are left)
1492
+ while (legacy.size > MAX_RETIRED) {
1493
+ const oldest = [...legacy.values()].sort((x, y) => x.epoch - y.epoch)[0];
1494
+ legacy.delete(oldest.rid);
1495
+ leaveRoom(oldest.rid);
1496
+ }
1497
+ await joinRoom(dataRid, sigKey);
1498
+ return { oldEpoch: old.epoch, oldKey, oldRid };
1499
+ }
1500
+ /** SF5: persist a key this device retired, so a restart keeps serving stragglers from that room. */
1501
+ async function rememberRetired(e, key, rid) {
1502
+ const list = (await store.get(RETIRED_KEY)) ?? [];
1503
+ const next = [
1504
+ ...list.filter((x) => x.rid !== rid),
1505
+ { e, k: b64uEncode(key), rid },
1506
+ ].slice(-MAX_RETIRED);
1507
+ await store.set(RETIRED_KEY, next);
1508
+ }
1509
+ /** SF5: rejoin the rooms of keys this device retired (from its local store; the shared doc is never trusted). */
1510
+ async function syncLegacy() {
1511
+ if (!running)
1512
+ return;
1513
+ const list = (await store.get(RETIRED_KEY)) ?? [];
1514
+ for (const v of list.slice(-MAX_RETIRED)) {
1515
+ const e = v?.e;
1516
+ if (!isEpoch(e) || e > epoch || typeof v?.k !== "string")
1517
+ continue;
1518
+ let raw;
1519
+ try {
1520
+ raw = b64uDecode(v.k);
1521
+ }
1522
+ catch {
1523
+ continue;
1524
+ }
1525
+ if (raw.length !== 32)
1526
+ continue;
1527
+ const rid = await deriveRoomId(raw, appId, topicName, e, instanceId || undefined);
1528
+ if (rid === dataRid || (legacy.has(rid) && rooms.has(rid)))
1529
+ continue;
1530
+ legacy.set(rid, {
1531
+ epoch: e,
1532
+ rid,
1533
+ material: await deriveDocMaterial(raw, topicName),
1534
+ });
1535
+ await joinRoom(rid, await importAesKey(await hkdf(raw, `swal-signal/v1|${topicName}`)));
1536
+ }
1537
+ }
1538
+ /**
1539
+ * Owner only: re-key at epoch + 1 for every member (not revoked, key record known), cutting `part.devices` and
1540
+ * executing `part.cut` (their grants). The rotation is a signed document: recorded here first, then handed to every
1541
+ * connected device under every key it may hold, then adopted.
1542
+ */
1543
+ async function rotate(part) {
1544
+ const me = vault.deviceId;
1545
+ if (!sec || root?.deviceId !== me)
1546
+ throw new Error("only the owner re-keys the mesh");
1547
+ const newEpoch = epoch + 1;
1548
+ if (newEpoch > MAX_EPOCH)
1549
+ throw new Error("epoch limit reached: re-create the mesh");
1550
+ const cutDevs = new Set(part.devices);
1551
+ for (const l of [...links])
1552
+ if (l.deviceId && (cutDevs.has(l.deviceId) || isRevoked(l.deviceId)))
1553
+ closeRec(l);
1554
+ const pubs = new Map();
1555
+ for (const d of trusted.values()) {
1556
+ if (d.deviceId === me || cutDevs.has(d.deviceId) || isRevoked(d.deviceId))
1557
+ continue;
1558
+ const k = kexOf(d.deviceId);
1559
+ if (k)
1560
+ pubs.set(d.deviceId, k);
1561
+ else
1562
+ err(new Error(`no key record of ${d.deviceId} yet: it gets the key with a later rotation`));
1563
+ }
1564
+ // R4-S3/R5-S3: a rotation receivers would refuse must never be adopted here (the owner would end alone)
1565
+ if (pubs.size > MAX_ROT_MEMBERS ||
1566
+ cutDevs.size > MAX_ROT_MEMBERS ||
1567
+ part.cut.length > MAX_ROT_REFS)
1568
+ throw new Error(`re-key refused: ${pubs.size} recipients and ${cutDevs.size} cut devices; a rotation carries at most ${MAX_ROT_MEMBERS} of each`);
1569
+ const newKey = randomBytes(32);
1570
+ const base = {
1571
+ t: "rot",
1572
+ v: 2,
1573
+ inst: root.mid,
1574
+ epoch: newEpoch,
1575
+ from: me,
1576
+ to: [...pubs.keys()].sort(),
1577
+ revoked: [...cutDevs].sort(),
1578
+ cut: [...part.cut].sort(),
1579
+ revs: [...part.revs].sort().slice(0, MAX_ROT_REFS),
1580
+ n: b64uEncode(randomBytes(16)),
1581
+ };
1582
+ // wraps are bound to the pre-id; the id also commits to the whole wrap set, and the owner signs the id
1583
+ const pid = await rotationPreId(base);
1584
+ const priv = (await ecdhIdentity()).privateKey;
1585
+ const wraps = {};
1586
+ for (const [to, k] of pubs)
1587
+ wraps[to] = await wrapMeshKey(priv, k.pub, k.kem, pid, me, to, newKey);
1588
+ const wh = await wrapsHash(wraps);
1589
+ const id = await rotationId({ ...base, wh });
1590
+ const sig = b64uEncode(await vault.sign(rotationSigBytes(id)));
1591
+ if (destroyed)
1592
+ return; // torn down while signing: publish nothing, keep the vault as it was
1593
+ const docR = { ...base, wh, wraps, sig };
1594
+ const out = await sec.add(docR); // verified like any received rotation; records its cuts for good
1595
+ if (out.status !== "accepted" && out.status !== "duplicate")
1596
+ throw new Error(`re-key refused: ${out.reason}`);
1597
+ // 1) every connected device gets the rotation under every key it may hold, 2) switch
1598
+ const body = utf8(JSON.stringify({ t: "docs", docs: [docR] }));
1599
+ await Promise.all([...links]
1600
+ .filter((l) => !l.closing &&
1601
+ l.deviceId &&
1602
+ l.rid !== "pair" &&
1603
+ (!signFrames || l.authed))
1604
+ .map((l) => sendTrustEverywhere(l, body).catch(err)));
1605
+ await adopt({ rec: { ...docR, id }, key: newKey });
1606
+ announce([`R:${id}`]);
1607
+ }
1608
+ /**
1609
+ * Revoke a device: signed web/trust revocations of its grants, cut here at once and handed to every device. On the
1610
+ * owner the mesh is re-keyed right away; on an admin it is a REQUEST that the next owner device online executes
1611
+ * (until then `rekeyPending` is true: the revoked device still holds the current key).
1612
+ */
1613
+ async function revoke(deviceId, o = {}) {
1614
+ await ready;
1615
+ if (deviceId === vault.deviceId)
1616
+ throw new Error("cannot revoke the current device");
1617
+ if (!sec || !root)
1618
+ throw new Error("unknown device");
1619
+ await refreshTrust();
1620
+ if (deviceId === root.deviceId)
1621
+ throw new Error(`not authorized to revoke ${deviceId}`);
1622
+ if (sec.trust.grantsOf(deviceId).length === 0)
1623
+ throw new Error("unknown device");
1624
+ if (opts.canRotate && !(await opts.canRotate(vault.deviceId, deviceId)))
1625
+ throw new Error(`not authorized to revoke ${deviceId}`);
1626
+ const docs = await sec.revocationsFor(signer, deviceId, {
1627
+ heads: o.heads,
1628
+ reason: o.reason,
1629
+ now: now(),
1630
+ });
1631
+ if (docs.length === 0 && !isRevoked(deviceId))
1632
+ throw new Error(`not authorized to revoke ${deviceId}`);
1633
+ if (!running)
1634
+ await start();
1635
+ await addLocal(docs);
1636
+ // cut the revoked device off SYNCHRONOUSLY: its link must not be reachable by anything below
1637
+ for (const l of [...links])
1638
+ if (l.deviceId === deviceId || (l.deviceId && isRevoked(l.deviceId)))
1639
+ closeRec(l);
1640
+ await refreshTrust();
1641
+ emit("revoked", {
1642
+ deviceId,
1643
+ epoch,
1644
+ pending: root.deviceId !== vault.deviceId,
1645
+ });
1646
+ await serialRot(ownerRekey).catch(err);
1647
+ }
1648
+ /** Owner only: own grants for devices that lost membership through a revoked admin (cascade). */
1649
+ async function reanchor(deviceIds) {
1650
+ await ready;
1651
+ if (!sec || root?.deviceId !== vault.deviceId)
1652
+ throw new Error("only the owner re-anchors devices");
1653
+ const grants = [];
1654
+ for (const id of deviceIds) {
1655
+ if (sec.memberGrant(id))
1656
+ continue;
1657
+ const old = sec.trust.grantsOf(id)[0];
1658
+ if (!old)
1659
+ throw new Error(`unknown device ${id}`);
1660
+ grants.push(await sec.issueGrant(signer, old.subject.pub, {
1661
+ role: old.delegate >= 1 ? "admin" : "member",
1662
+ name: old.name,
1663
+ now: now(),
1664
+ }));
1665
+ }
1666
+ await addLocal(grants);
1667
+ await refreshTrust();
1668
+ await serialRot(ownerRekey).catch(err);
1669
+ }
1670
+ // ---- pairing ----
1671
+ /** First pairing of a fresh mesh: this device becomes its owner (the pinned trust root). */
1672
+ async function ensureRoot() {
1673
+ if (root && sec)
1674
+ return root;
1675
+ // a NEW mesh id, never one read from anywhere else (anybody could copy another mesh's id)
1676
+ const mid = b64uEncode(randomBytes(16));
1677
+ root = { mid, deviceId: vault.deviceId, pub: selfPub };
1678
+ await store.set("root", root);
1679
+ await openSecurity(root);
1680
+ await publishKex();
1681
+ return root;
1682
+ }
1683
+ /** Grant chain of this device (its member grant, then its issuers' up to the root): what a guest needs to verify. */
1684
+ function ownChain() {
1685
+ const out = [];
1686
+ let g = sec?.memberGrant(vault.deviceId) ?? null;
1687
+ for (let i = 0; g && i < 8; i++) {
1688
+ out.push(g);
1689
+ g = g.parent ? (sec?.trust.getGrant(g.parent) ?? null) : null;
1690
+ }
1691
+ return out;
1692
+ }
1693
+ async function pairHost(o = {}) {
1694
+ await ready;
1695
+ const guestRole = o.role ?? "member";
1696
+ const r = await ensureRoot();
1697
+ await refreshTrust();
1698
+ const mine = selfRole();
1699
+ if (!mine || !canIssue(mine, guestRole))
1700
+ throw new Error(`this device (${mine ?? "not admitted"}) cannot admit a ${guestRole}`);
1701
+ // R5-S3: refuse an admission the next re-key could not carry (receivers accept at most MAX_ROT_MEMBERS recipients)
1702
+ if (trusted.size + 1 >= MAX_ROT_MEMBERS)
1703
+ throw new Error(`the mesh is full (${MAX_ROT_MEMBERS} devices)`);
1704
+ if (mine === "admin" &&
1705
+ (sec?.trust
1706
+ .docs()
1707
+ .filter((d) => d.t === "grant" && d.issuer === vault.deviceId).length ??
1708
+ 0) >= MAX_MEMBERS_PER_ADMIN)
1709
+ throw new Error(`this admin already admitted ${MAX_MEMBERS_PER_ADMIN} devices`);
1710
+ await vault.getOrCreateMeshKey();
1711
+ const offer = await createPairOffer(vault, {
1712
+ mid: r.mid,
1713
+ root: r.deviceId,
1714
+ appId,
1715
+ topic: topicName,
1716
+ now: now(),
1717
+ });
1718
+ const rid = await derivePairRoomId(offer.pairSecret);
1719
+ const pairKey = await derivePairKey(offer.pairSecret);
1720
+ const host = new HostPairing(offer, {
1721
+ now,
1722
+ verify: identityVerify,
1723
+ prove: async (transcript) => ({
1724
+ pub: selfPub,
1725
+ sig: b64uEncode(await vault.sign(hostProofBytes(transcript, vault.deviceId, selfPub))),
1726
+ }),
1727
+ onSas: (p) => emit("sas", { role: "host", ...p }),
1728
+ buildGrant: async (guest) => {
1729
+ // B1: the ack already proved possession of guest.pub and deviceId = fingerprint(pub)
1730
+ if (guest.deviceId === vault.deviceId || guest.deviceId === r.deviceId)
1731
+ throw new Error("guest claims the host/root identity");
1732
+ if (!sec)
1733
+ throw new Error("no security state");
1734
+ if (opts.authorizeDevice &&
1735
+ !(await opts.authorizeDevice(guest.deviceId, b64uDecode(guest.pub)))) {
1736
+ throw new Error("device not authorized by the mesh policy");
1737
+ }
1738
+ // the key, its epoch and its rotation are read together, at the very moment the grant is made (a re-key may
1739
+ // have happened since the offer was shown)
1740
+ const keyNow = meshKey ?? (await vault.getOrCreateMeshKey());
1741
+ const epochNow = epoch;
1742
+ const rotNow = curRot;
1743
+ // a grant of its own (re-admission included: a new grant id, untouched by earlier revocations), handed the
1744
+ // key of this epoch
1745
+ const grant = await sec.issueGrant(signer, guest.pub, {
1746
+ role: guestRole === "admin" ? "admin" : "member",
1747
+ name: guest.name,
1748
+ epoch: epochNow,
1749
+ now: now(),
1750
+ });
1751
+ const dev = {
1752
+ ...guest,
1753
+ addedAt: grant.issuedAt,
1754
+ role: guestRole,
1755
+ admittedBy: vault.deviceId,
1756
+ };
1757
+ const extra = o.extra ? await o.extra(dev) : undefined;
1758
+ await addLocal([grant]);
1759
+ await refreshTrust();
1760
+ const docs = [grant, ...ownChain()];
1761
+ for (const k of [sec.kexDoc(r.deviceId), sec.kexDoc(vault.deviceId)])
1762
+ if (k && !docs.includes(k))
1763
+ docs.push(k);
1764
+ if (rotNow && JSON.stringify(rotNow).length < 128 * 1024) {
1765
+ const { id: _id, ...d } = rotNow;
1766
+ docs.push(d);
1767
+ }
1768
+ return {
1769
+ meshKey: b64uEncode(keyNow),
1770
+ epoch: epochNow,
1771
+ mid: r.mid,
1772
+ hostDevice: selfDevice(),
1773
+ root: r,
1774
+ docs,
1775
+ ...(rotNow ? { rotId: rotNow.id } : {}),
1776
+ ...(extra !== undefined ? { extra } : {}),
1777
+ };
1778
+ },
1779
+ onPaired: (d) => {
1780
+ emit("paired", trusted.get(d.deviceId) ?? d);
1781
+ endPairing(rid);
1782
+ void start().catch(err);
1783
+ },
1784
+ onFail: (reason) => {
1785
+ emit("error", new Error(`pairing failed: ${reason}`));
1786
+ endPairing(rid);
1787
+ },
1788
+ });
1789
+ hostSession = { s: host, rid };
1790
+ pairRids.add(rid);
1791
+ await joinRoom(rid, pairKey);
1792
+ const timer = setTimeout(() => endPairing(rid), offer.payload.exp - now() + 1000);
1793
+ const endPairingOnce = () => clearTimeout(timer);
1794
+ host._end = endPairingOnce;
1795
+ return {
1796
+ payload: offer.encoded,
1797
+ expiresAt: offer.payload.exp,
1798
+ cancel: () => {
1799
+ host.cancel();
1800
+ endPairing(rid);
1801
+ },
1802
+ };
1803
+ }
1804
+ function endPairing(rid) {
1805
+ hostSession?.s?._end?.();
1806
+ leaveRoom(rid);
1807
+ pairRids.delete(rid);
1808
+ // let the last frame (grant, possibly fragmented) flush before dropping the pairing links
1809
+ const pairLinks = [...links].filter((l) => l.rid === "pair");
1810
+ for (const l of pairLinks) {
1811
+ const drained = Promise.race([
1812
+ outQ.get(l.link) ?? Promise.resolve(),
1813
+ new Promise((r) => setTimeout(r, 10_000)),
1814
+ ]);
1815
+ void drained.then(() => setTimeout(() => l.link.close(), 500));
1816
+ }
1817
+ if (hostSession?.rid === rid)
1818
+ hostSession = null;
1819
+ if (guestSession?.rid === rid)
1820
+ guestSession = null;
1821
+ setStatus();
1822
+ }
1823
+ const ROOT_MISMATCH_ERR = "pairing refused: this mesh id is pinned to another owner key";
1824
+ const MOVE_MESH_ERR = "this device belongs to another mesh: to join a different one, create a new Mesh with a fresh Y.Doc";
1825
+ /** No shared content at all (the mesh itself never writes to the doc). */
1826
+ function docIsFresh() {
1827
+ for (const [, t] of doc.share) {
1828
+ const ty = t;
1829
+ if (ty._start !== null || ty._map.size > 0)
1830
+ return false;
1831
+ }
1832
+ return true;
1833
+ }
1834
+ async function pairJoin(encoded, o = {}) {
1835
+ await ready;
1836
+ const p = decodePairPayload(encoded);
1837
+ if (p.appId !== appId || p.topic !== topicName)
1838
+ throw new Error("pairing payload is for a different app/topic");
1839
+ // B3: a device moving to ANOTHER mesh would merge this mesh's doc into it (and keep serving it). Only a fresh
1840
+ // doc may change meshes: create a new Mesh with a new Y.Doc for that.
1841
+ // S6: trust on first use. The QR names the root; a device never re-pins another key for a mesh id it already knows
1842
+ if (root && root.mid === p.mid && root.deviceId !== p.root)
1843
+ throw new Error(ROOT_MISMATCH_ERR);
1844
+ const moving = root !== null && root.mid !== p.mid;
1845
+ if (moving && !docIsFresh())
1846
+ throw new Error(MOVE_MESH_ERR);
1847
+ // ...and while moving, the old mesh must not fill the fresh doc: go offline from it (resumed on failure)
1848
+ const resumeOld = moving && running;
1849
+ if (resumeOld)
1850
+ stopNetwork();
1851
+ const confirm = o.confirmSas ??
1852
+ ((code) => new Promise((resolve) => emit("sas", {
1853
+ role: "guest",
1854
+ code,
1855
+ confirm: () => resolve(true),
1856
+ reject: () => resolve(false),
1857
+ })));
1858
+ const guest = await GuestPairing.create(p, vault, {
1859
+ name: opts.deviceName ?? "device",
1860
+ onSas: async (code) => Boolean(await confirm(code)),
1861
+ now: now(),
1862
+ });
1863
+ const rid = await derivePairRoomId(b64uDecode(p.pairSecret));
1864
+ guestSession = { s: guest, rid };
1865
+ pairRids.add(rid);
1866
+ await joinRoom(rid, await derivePairKey(b64uDecode(p.pairSecret)));
1867
+ const timeout = setTimeout(() => guest.fail(new Error("pairing timed out")), Math.max(1000, p.exp - now()));
1868
+ try {
1869
+ const g = await guest.result;
1870
+ clearTimeout(timeout);
1871
+ // the trust root and our grant arrive over the SAS-authenticated session; the grant chain must be valid and
1872
+ // issued by the very host that proved the identity named by the QR
1873
+ if (!g.root || !Array.isArray(g.docs))
1874
+ throw new Error("pairing grant carries no grant (host too old?)");
1875
+ if (!isEpoch(g.epoch))
1876
+ throw new Error("pairing grant: bad epoch");
1877
+ if (g.root.mid !== p.mid || g.mid !== p.mid)
1878
+ throw new Error("pairing grant: mesh id does not match the pairing code");
1879
+ if (g.root.deviceId !== p.root)
1880
+ throw new Error("pairing grant: trust root does not match the pairing code");
1881
+ if (!(await idMatchesPub(g.root.deviceId, g.root.pub)))
1882
+ throw new Error("pairing grant: invalid trust root");
1883
+ if (root &&
1884
+ root.mid === g.root.mid &&
1885
+ (root.deviceId !== g.root.deviceId || root.pub !== g.root.pub))
1886
+ throw new Error(ROOT_MISMATCH_ERR);
1887
+ // verify in a scratch state first: nothing is pinned or stored before the grant checks out
1888
+ const probe = await SecurityState.open({
1889
+ root: g.root,
1890
+ store: memoryStore(),
1891
+ schema: opts.trustSchema,
1892
+ now,
1893
+ });
1894
+ await probe.addMany(g.docs);
1895
+ const mine = probe.memberGrant(vault.deviceId);
1896
+ if (!mine || mine.subject.pub !== selfPub)
1897
+ throw new Error("pairing grant: invalid grant for this device");
1898
+ // the host proved (inside the session) that it holds hostId's ML-DSA key; the grant must come from it
1899
+ if (mine.issuer !== p.hostId)
1900
+ throw new Error("pairing grant: grant not issued by the paired host");
1901
+ if (probe.identityOf(p.hostId) !== g.hostProof?.pub)
1902
+ throw new Error("pairing grant: grant not issued by the paired host");
1903
+ const switching = root?.mid !== g.root.mid;
1904
+ if (switching && root && !docIsFresh())
1905
+ throw new Error(MOVE_MESH_ERR);
1906
+ // same mesh: never step back to an older epoch (and its older key) because the host lags behind us
1907
+ if (!switching && root && g.epoch < epoch)
1908
+ throw new Error(`pairing refused: the host is at epoch ${g.epoch}, this device at ${epoch}`);
1909
+ // leave the current network BEFORE touching keys or the doc: nothing of one mesh may reach the other
1910
+ if (running)
1911
+ stopNetwork();
1912
+ if (switching) {
1913
+ // nothing of the old mesh's security state survives a move
1914
+ secUnsub?.();
1915
+ sec = null;
1916
+ trusted = new Map();
1917
+ legacy.clear();
1918
+ replay.clear();
1919
+ cands.clear();
1920
+ unwrapFailed.clear();
1921
+ curRot = null;
1922
+ curRotId = null;
1923
+ for (const k of [
1924
+ "sec/trust",
1925
+ "sec/kex",
1926
+ "sec/rots",
1927
+ "sec/cuts",
1928
+ "sec/gone",
1929
+ ])
1930
+ await store.set(k, []);
1931
+ await store.set(RETIRED_KEY, []);
1932
+ await store.set("rotId", null);
1933
+ }
1934
+ root = g.root;
1935
+ await store.set("root", root);
1936
+ if (g.hostDevice &&
1937
+ g.hostDevice.deviceId === g.root.deviceId &&
1938
+ typeof g.hostDevice.name === "string") {
1939
+ ownerName = g.hostDevice.name;
1940
+ await store.set("ownerName", ownerName);
1941
+ }
1942
+ if (!sec)
1943
+ await openSecurity(root);
1944
+ await sec.addMany(g.docs);
1945
+ const key = b64uDecode(g.meshKey);
1946
+ await vault.setMeshKey(key);
1947
+ await persistEpoch(g.epoch);
1948
+ epoch = g.epoch;
1949
+ // the rotation that produced this key (if any): lets this device take part in tie-breaks of its epoch
1950
+ curRotId = typeof g.rotId === "string" ? g.rotId : null;
1951
+ const cr = curRotId ? sec?.rotation(curRotId) : undefined;
1952
+ curRot = cr && cr.epoch === g.epoch ? cr : null;
1953
+ cands.clear();
1954
+ await store.set("rotId", curRotId);
1955
+ await publishKex();
1956
+ await refreshTrust();
1957
+ endPairing(rid);
1958
+ emit("paired", g.hostDevice);
1959
+ await start();
1960
+ return { host: g.hostDevice, extra: g.extra };
1961
+ }
1962
+ catch (e) {
1963
+ clearTimeout(timeout);
1964
+ endPairing(rid);
1965
+ if (resumeOld && !running && root?.mid !== p.mid)
1966
+ void start().catch(err);
1967
+ throw e;
1968
+ }
1969
+ }
1970
+ return {
1971
+ get status() {
1972
+ return status;
1973
+ },
1974
+ get peers() {
1975
+ return peerIds();
1976
+ },
1977
+ get epoch() {
1978
+ return epoch;
1979
+ },
1980
+ get root() {
1981
+ return root;
1982
+ },
1983
+ get rekeyPending() {
1984
+ return rekeyPending();
1985
+ },
1986
+ get namespace() {
1987
+ return meshNamespace(appId, instanceId);
1988
+ },
1989
+ channel,
1990
+ role(deviceId = vault.deviceId) {
1991
+ return roleOf(deviceId);
1992
+ },
1993
+ awareness,
1994
+ ready,
1995
+ pairHost,
1996
+ pairJoin,
1997
+ devices,
1998
+ revoke,
1999
+ reanchor,
2000
+ get security() {
2001
+ const st = sec;
2002
+ if (!st)
2003
+ return null;
2004
+ return {
2005
+ trust: st.trust,
2006
+ docs: async () => st.get(st.inventory()),
2007
+ rotations: () => st.rotations(),
2008
+ keyAgreement: (id) => st.keyAgreement(id),
2009
+ executedCuts: () => st.executedCuts(),
2010
+ add: async (d) => (await addLocal([d]))[0] ?? { status: "rejected" },
2011
+ addMany: (ds) => addLocal(ds),
2012
+ issueGrant: (subjectPub, o2) => st.issueGrant(signer, subjectPub, { ...o2, now: now() }),
2013
+ };
2014
+ },
2015
+ leave() {
2016
+ stopNetwork();
2017
+ },
2018
+ destroy() {
2019
+ if (destroyed)
2020
+ return;
2021
+ destroyed = true;
2022
+ stopNetwork();
2023
+ doc.off("update", onDocUpdate);
2024
+ removeAwarenessStates(awareness, [doc.clientID], ORIGIN);
2025
+ awareness.destroy();
2026
+ for (const t of transports)
2027
+ t.close();
2028
+ void persistence?.destroy();
2029
+ listeners.clear();
2030
+ },
2031
+ on(event, cb) {
2032
+ let set = listeners.get(event);
2033
+ if (!set) {
2034
+ set = new Set();
2035
+ listeners.set(event, set);
2036
+ }
2037
+ set.add(cb);
2038
+ return () => void set.delete(cb);
2039
+ },
2040
+ };
2041
+ }
2042
+ //# sourceMappingURL=provider.js.map