@walkhi/code-relax 0.1.0-beta.8 → 0.1.0-beta.81

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 (856) hide show
  1. package/README.md +67 -45
  2. package/dist/bin/cleanup-accounts.mjs +63 -0
  3. package/dist/bin/codex-remote.mjs +382 -262
  4. package/dist/bin/manage-relay-admin.mjs +20 -0
  5. package/dist/bin/migrate-relay-db.mjs +109 -0
  6. package/dist/bin/publish-android-release.mjs +63 -0
  7. package/dist/bin/publish-harmony-release.mjs +46 -0
  8. package/dist/bin/release-upload-server.mjs +190 -0
  9. package/dist/bin/self-relay-admin.mjs +8 -0
  10. package/dist/bin/self-relay-demo.mjs +2 -2
  11. package/dist/bin/self-relay-server.mjs +43 -30
  12. package/dist/bin/set-relay-admin-password.mjs +11 -0
  13. package/dist/build-platform.json +1 -0
  14. package/dist/get-shared-service-url.ps1 +12 -4
  15. package/dist/shared/app-server-events.cjs +457 -238
  16. package/dist/shared/code-browser.cjs +159 -0
  17. package/dist/shared/file-changes.cjs +31 -0
  18. package/dist/shared/file-citations.cjs +117 -0
  19. package/dist/shared/file-mentions.cjs +92 -0
  20. package/dist/shared/message-delivery.cjs +115 -0
  21. package/dist/shared/p2p-data-channel.cjs +22 -0
  22. package/dist/shared/p2p-diagnostics.cjs +1 -1
  23. package/dist/shared/p2p-file-channel.cjs +87 -0
  24. package/dist/shared/p2p-ice-policy.cjs +22 -0
  25. package/dist/shared/plan-pricing.cjs +32 -0
  26. package/dist/shared/question-attention.cjs +69 -0
  27. package/dist/shared/release-version.cjs +31 -0
  28. package/dist/shared/server-requests.cjs +311 -67
  29. package/dist/shared/service-endpoints.cjs +21 -0
  30. package/dist/shared/timeline-sync.cjs +173 -67
  31. package/dist/skills/code-relax/SKILL.md +22 -17
  32. package/dist/src/archived-threads.mjs +22 -0
  33. package/dist/src/bounded-process-log.mjs +63 -0
  34. package/dist/src/celsius-client.mjs +107 -0
  35. package/dist/src/code-browser.mjs +99 -0
  36. package/dist/src/desktop-launcher.mjs +62 -42
  37. package/dist/src/desktop-operation.mjs +41 -0
  38. package/dist/src/dots-approval-timeline.mjs +99 -0
  39. package/dist/src/dots-avatar.mjs +42 -0
  40. package/dist/src/dots-chat-projection.mjs +50 -0
  41. package/dist/src/dots-chat-server.mjs +531 -0
  42. package/dist/src/dots-message-images.mjs +318 -0
  43. package/dist/src/dots-profile.mjs +77 -0
  44. package/dist/src/dots-room-activity.mjs +199 -0
  45. package/dist/src/dots-room-client.mjs +163 -0
  46. package/dist/src/dots-room-projection.mjs +94 -0
  47. package/dist/src/dots-schedules.mjs +66 -0
  48. package/dist/src/dots-summary-tasks.mjs +32 -0
  49. package/dist/src/dots-task-catalog.mjs +433 -0
  50. package/dist/src/dots-task-files.mjs +52 -0
  51. package/dist/src/file-transfer-range.mjs +22 -0
  52. package/dist/src/file-upload-session.mjs +153 -0
  53. package/dist/src/hosted-history-app-server.mjs +103 -0
  54. package/dist/src/hosted-history-cache.mjs +113 -0
  55. package/dist/src/lan-socket-server.mjs +269 -37
  56. package/dist/src/managed-app-server.mjs +398 -216
  57. package/dist/src/managed-endpoint.mjs +25 -0
  58. package/dist/src/message-images.mjs +0 -18
  59. package/dist/src/message-upload-files.mjs +115 -0
  60. package/dist/src/onboarding.mjs +9 -28
  61. package/dist/src/plan-pricing.mjs +47 -0
  62. package/dist/src/platform/README.md +7 -12
  63. package/dist/src/platform/codex-protocol.mjs +51 -0
  64. package/dist/src/platform/codex-runtime.mjs +4 -0
  65. package/dist/src/platform/credential-store.mjs +19 -0
  66. package/dist/src/platform/desktop-host.mjs +3 -0
  67. package/dist/src/platform/desktop-permission-mode.mjs +17 -0
  68. package/dist/src/platform/desktop-project-registration.mjs +3 -0
  69. package/dist/src/platform/desktop-project-store.mjs +219 -0
  70. package/dist/src/platform/file-opener.mjs +9 -0
  71. package/dist/src/platform/linux/codex-runtime.mjs +56 -0
  72. package/dist/src/platform/linux/desktop-host.mjs +87 -0
  73. package/dist/src/platform/linux/desktop-installation.mjs +36 -0
  74. package/dist/src/platform/linux/desktop-project-registration.mjs +11 -0
  75. package/dist/src/platform/linux/network-info.mjs +11 -0
  76. package/dist/src/platform/linux/process.mjs +53 -0
  77. package/dist/src/platform/linux/project-locations.mjs +17 -0
  78. package/dist/src/platform/linux/resident-startup.mjs +52 -0
  79. package/dist/src/platform/linux/retained-runtime.mjs +76 -0
  80. package/dist/src/platform/linux/service-host.mjs +18 -0
  81. package/dist/src/platform/linux/system.mjs +50 -0
  82. package/dist/src/platform/linux/user-environment.mjs +31 -0
  83. package/dist/src/platform/macos/codex-runtime.mjs +121 -0
  84. package/dist/src/platform/macos/desktop-host.mjs +86 -0
  85. package/dist/src/platform/macos/desktop-project-registration.mjs +11 -0
  86. package/dist/src/platform/macos/network-info.mjs +8 -0
  87. package/dist/src/platform/macos/project-locations.mjs +8 -0
  88. package/dist/src/platform/macos/resident-startup.mjs +84 -0
  89. package/dist/src/platform/macos/retained-runtime.mjs +127 -0
  90. package/dist/src/platform/macos/service-host.mjs +18 -0
  91. package/dist/src/platform/macos/system.mjs +29 -0
  92. package/dist/src/platform/macos/user-environment.mjs +31 -0
  93. package/dist/src/platform/managed-user-environment.mjs +78 -0
  94. package/dist/src/platform/native-package.mjs +61 -0
  95. package/dist/src/platform/network-info.mjs +3 -0
  96. package/dist/src/platform/path-identity.mjs +5 -0
  97. package/dist/src/platform/posix/network-candidates.mjs +23 -0
  98. package/dist/src/platform/posix/runtime-paths.mjs +25 -0
  99. package/dist/src/platform/power-inhibitor.mjs +22 -0
  100. package/dist/src/platform/project-locations.mjs +3 -0
  101. package/dist/src/platform/runtime-paths.mjs +8 -0
  102. package/dist/src/platform/select.mjs +5 -0
  103. package/dist/src/platform/service-host.mjs +3 -0
  104. package/dist/src/platform/user-environment.mjs +3 -0
  105. package/dist/src/platform/windows/activate-desktop.ps1 +49 -0
  106. package/dist/src/platform/windows/codex-runtime.mjs +225 -0
  107. package/dist/src/platform/windows/desktop-helper.mjs +179 -0
  108. package/dist/src/platform/windows/desktop-host.mjs +45 -18
  109. package/dist/src/platform/windows/desktop-info.ps1 +27 -5
  110. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -0
  111. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -0
  112. package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -14
  113. package/dist/src/platform/windows/dots-profile.mjs +49 -0
  114. package/dist/src/platform/windows/keep-awake.ps1 +20 -0
  115. package/dist/src/platform/windows/launch-worker.mjs +41 -42
  116. package/dist/src/platform/windows/project-locations.mjs +16 -0
  117. package/dist/src/platform/windows/resident-start.js +20 -0
  118. package/dist/src/platform/windows/resident-startup.ps1 +27 -11
  119. package/dist/src/platform/windows/rustdesk-video-probe.mjs +168 -0
  120. package/dist/src/platform/windows/service-host.mjs +15 -23
  121. package/dist/src/platform/windows/start-hidden-console.ps1 +11 -2
  122. package/dist/src/platform/windows/user-environment.mjs +52 -50
  123. package/dist/src/project-create.mjs +121 -0
  124. package/dist/src/project-management.mjs +59 -0
  125. package/dist/src/project-remove.mjs +55 -0
  126. package/dist/src/project-update.mjs +59 -0
  127. package/dist/src/relay-config-import.mjs +45 -0
  128. package/dist/src/runtime-discovery.mjs +67 -0
  129. package/dist/src/self-relay/account-cleanup.mjs +30 -0
  130. package/dist/src/self-relay/account-connections.mjs +52 -0
  131. package/dist/src/self-relay/admin-auth.mjs +59 -0
  132. package/dist/src/self-relay/admin-cloud.mjs +101 -0
  133. package/dist/src/self-relay/admin-login-page.mjs +7 -0
  134. package/dist/src/self-relay/admin-page.mjs +87 -0
  135. package/dist/src/self-relay/admin-server.mjs +105 -0
  136. package/dist/src/self-relay/admin-state.mjs +85 -19
  137. package/dist/src/self-relay/admin-style.mjs +58 -0
  138. package/dist/src/self-relay/admin-workstations-page.mjs +95 -0
  139. package/dist/src/self-relay/admin-workstations.mjs +143 -0
  140. package/dist/src/self-relay/android-releases.mjs +61 -0
  141. package/dist/src/self-relay/client.mjs +45 -45
  142. package/dist/src/self-relay/connection-reporting.mjs +38 -0
  143. package/dist/src/self-relay/connector.mjs +456 -145
  144. package/dist/src/self-relay/demo.mjs +3 -4
  145. package/dist/src/self-relay/device-info.mjs +27 -0
  146. package/dist/src/self-relay/device-store.mjs +10 -2
  147. package/dist/src/self-relay/diagnostics.mjs +54 -0
  148. package/dist/src/self-relay/email-accounts.mjs +183 -0
  149. package/dist/src/self-relay/file-wire.mjs +76 -0
  150. package/dist/src/self-relay/lan-authorizations.mjs +44 -2
  151. package/dist/src/self-relay/lifecycle.mjs +255 -262
  152. package/dist/src/self-relay/long-pairing.mjs +40 -0
  153. package/dist/src/self-relay/mysql-state.mjs +118 -0
  154. package/dist/src/self-relay/p2p-probe.mjs +168 -123
  155. package/dist/src/self-relay/phone-connections.mjs +55 -0
  156. package/dist/src/self-relay/privacy-page.mjs +26 -0
  157. package/dist/src/self-relay/public-releases.mjs +67 -0
  158. package/dist/src/self-relay/rtc-file-bridge.mjs +46 -0
  159. package/dist/src/self-relay/server.mjs +1064 -428
  160. package/dist/src/self-relay/usage-daily.mjs +55 -0
  161. package/dist/src/self-relay/usage-meter.mjs +141 -0
  162. package/dist/src/server.mjs +1599 -1797
  163. package/dist/src/service-control.mjs +17 -7
  164. package/dist/src/service-doctor.mjs +150 -70
  165. package/dist/src/service-health.mjs +2 -2
  166. package/dist/src/service-install.mjs +16 -10
  167. package/dist/src/service-lifecycle.mjs +51 -46
  168. package/dist/src/service-power.mjs +58 -0
  169. package/dist/src/service-restart.mjs +45 -12
  170. package/dist/src/service-update-worker.mjs +42 -0
  171. package/dist/src/service-update.mjs +80 -0
  172. package/dist/src/shared-app-server.mjs +490 -305
  173. package/dist/src/shared-catalog.mjs +132 -50
  174. package/dist/src/shared-recovery.mjs +24 -14
  175. package/dist/src/shared-thread-stream.mjs +136 -51
  176. package/dist/src/skill-install.mjs +0 -13
  177. package/dist/src/turn-model-history.mjs +47 -0
  178. package/dist/src/usage-reset.mjs +17 -0
  179. package/dist/src/workstation-downloads.mjs +120 -0
  180. package/package.json +35 -37
  181. package/tools/postinstall.mjs +186 -146
  182. package/dist/src/app-server-client.mjs +0 -468
  183. package/dist/src/app-server-tasks.mjs +0 -358
  184. package/dist/src/debug-ui.mjs +0 -131
  185. package/dist/src/platform/windows/credential-store.mjs +0 -27
  186. package/dist/src/platform/windows/credential-store.ps1 +0 -49
  187. package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
  188. package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
  189. package/dist/src/platform/windows/desktop-tools.mjs +0 -48
  190. package/dist/src/thread-catalog.mjs +0 -47
  191. package/dist/tools/find-desktop-pipe.mjs +0 -10
  192. package/dist/web/activity-view.js +0 -283
  193. package/dist/web/approval-ui.js +0 -188
  194. package/dist/web/capabilities.js +0 -57
  195. package/dist/web/chat-transport.js +0 -202
  196. package/dist/web/chat.css +0 -782
  197. package/dist/web/chat.js +0 -3830
  198. package/dist/web/client-platform.js +0 -11
  199. package/dist/web/community-view.js +0 -20
  200. package/dist/web/community.css +0 -77
  201. package/dist/web/community.html +0 -37
  202. package/dist/web/composer-controller.js +0 -101
  203. package/dist/web/conversation-controller.js +0 -99
  204. package/dist/web/disclosure-state-controller.js +0 -95
  205. package/dist/web/draft-controller.js +0 -103
  206. package/dist/web/harmony-platform.js +0 -52
  207. package/dist/web/highlight-powershell.min.js +0 -39
  208. package/dist/web/highlight.min.js +0 -1262
  209. package/dist/web/history-cache.js +0 -240
  210. package/dist/web/history-controller.js +0 -167
  211. package/dist/web/index.html +0 -325
  212. package/dist/web/lan-session.js +0 -122
  213. package/dist/web/link-action-controller.js +0 -212
  214. package/dist/web/message-send-controller.js +0 -177
  215. package/dist/web/message-view.js +0 -98
  216. package/dist/web/p2p-data-channel.js +0 -50
  217. package/dist/web/p2p-diagnostics.js +0 -28
  218. package/dist/web/p2p-probe.js +0 -123
  219. package/dist/web/page-resume.js +0 -28
  220. package/dist/web/pending-message-store.js +0 -108
  221. package/dist/web/queue-controller.js +0 -82
  222. package/dist/web/resources.json +0 -1
  223. package/dist/web/self-relay-session.js +0 -129
  224. package/dist/web/server-requests.js +0 -67
  225. package/dist/web/station-connection-controller.js +0 -75
  226. package/dist/web/task-list-view.js +0 -296
  227. package/dist/web/thread-attention-controller.js +0 -124
  228. package/dist/web/thread-context-controller.js +0 -30
  229. package/dist/web/thread-list-controller.js +0 -61
  230. package/dist/web/thread-list-sync.js +0 -86
  231. package/dist/web/thread-title-controller.js +0 -57
  232. package/dist/web/timeline-formatters.js +0 -249
  233. package/dist/web/timeline-reducer.js +0 -81
  234. package/dist/web/timeline-renderer.js +0 -161
  235. package/dist/web/timeline-scroll-controller.js +0 -50
  236. package/dist/web/timeline-sync.js +0 -67
  237. package/dist/web/usage-controller.js +0 -258
  238. package/dist/web/vendor/README.md +0 -14
  239. package/dist/web/vendor/highlight-powershell.min.js +0 -39
  240. package/dist/web/vendor/highlight.LICENSE.txt +0 -29
  241. package/dist/web/vendor/highlight.min.js +0 -1262
  242. package/dist/web/vendor/lucide.LICENSE.txt +0 -17
  243. package/node_modules/@img/colour/LICENSE.md +0 -82
  244. package/node_modules/@img/colour/README.md +0 -15
  245. package/node_modules/@img/colour/color.cjs +0 -1596
  246. package/node_modules/@img/colour/index.cjs +0 -1
  247. package/node_modules/@img/colour/index.d.ts +0 -929
  248. package/node_modules/@img/colour/package.json +0 -58
  249. package/node_modules/@img/sharp-win32-x64/LICENSE +0 -191
  250. package/node_modules/@img/sharp-win32-x64/README.md +0 -59
  251. package/node_modules/@img/sharp-win32-x64/index.cjs +0 -1
  252. package/node_modules/@img/sharp-win32-x64/lib/libvips-42.dll +0 -0
  253. package/node_modules/@img/sharp-win32-x64/lib/libvips-cpp-8.18.6.dll +0 -0
  254. package/node_modules/@img/sharp-win32-x64/lib/sharp-win32-x64-0.35.4.node +0 -0
  255. package/node_modules/@img/sharp-win32-x64/package.json +0 -40
  256. package/node_modules/@img/sharp-win32-x64/versions.json +0 -30
  257. package/node_modules/@node-datachannel/win32-x64-msvc/node_datachannel.node +0 -0
  258. package/node_modules/@node-datachannel/win32-x64-msvc/package.json +0 -20
  259. package/node_modules/ansi-regex/index.d.ts +0 -37
  260. package/node_modules/ansi-regex/index.js +0 -10
  261. package/node_modules/ansi-regex/license +0 -9
  262. package/node_modules/ansi-regex/package.json +0 -55
  263. package/node_modules/ansi-regex/readme.md +0 -78
  264. package/node_modules/ansi-styles/index.d.ts +0 -345
  265. package/node_modules/ansi-styles/index.js +0 -163
  266. package/node_modules/ansi-styles/license +0 -9
  267. package/node_modules/ansi-styles/package.json +0 -56
  268. package/node_modules/ansi-styles/readme.md +0 -152
  269. package/node_modules/camelcase/index.d.ts +0 -63
  270. package/node_modules/camelcase/index.js +0 -76
  271. package/node_modules/camelcase/license +0 -9
  272. package/node_modules/camelcase/package.json +0 -43
  273. package/node_modules/camelcase/readme.md +0 -99
  274. package/node_modules/cliui/CHANGELOG.md +0 -76
  275. package/node_modules/cliui/LICENSE.txt +0 -14
  276. package/node_modules/cliui/README.md +0 -115
  277. package/node_modules/cliui/index.js +0 -354
  278. package/node_modules/cliui/package.json +0 -65
  279. package/node_modules/color-convert/CHANGELOG.md +0 -54
  280. package/node_modules/color-convert/LICENSE +0 -21
  281. package/node_modules/color-convert/README.md +0 -68
  282. package/node_modules/color-convert/conversions.js +0 -839
  283. package/node_modules/color-convert/index.js +0 -81
  284. package/node_modules/color-convert/package.json +0 -48
  285. package/node_modules/color-convert/route.js +0 -97
  286. package/node_modules/color-name/LICENSE +0 -8
  287. package/node_modules/color-name/README.md +0 -11
  288. package/node_modules/color-name/index.js +0 -152
  289. package/node_modules/color-name/package.json +0 -28
  290. package/node_modules/decamelize/index.js +0 -13
  291. package/node_modules/decamelize/license +0 -21
  292. package/node_modules/decamelize/package.json +0 -38
  293. package/node_modules/decamelize/readme.md +0 -48
  294. package/node_modules/detect-libc/LICENSE +0 -201
  295. package/node_modules/detect-libc/README.md +0 -163
  296. package/node_modules/detect-libc/index.d.ts +0 -14
  297. package/node_modules/detect-libc/lib/detect-libc.js +0 -313
  298. package/node_modules/detect-libc/lib/elf.js +0 -39
  299. package/node_modules/detect-libc/lib/filesystem.js +0 -51
  300. package/node_modules/detect-libc/lib/process.js +0 -24
  301. package/node_modules/detect-libc/package.json +0 -44
  302. package/node_modules/dijkstrajs/.travis.yml +0 -4
  303. package/node_modules/dijkstrajs/CONTRIBUTING.md +0 -8
  304. package/node_modules/dijkstrajs/LICENSE.md +0 -19
  305. package/node_modules/dijkstrajs/README.md +0 -18
  306. package/node_modules/dijkstrajs/dijkstra.js +0 -165
  307. package/node_modules/dijkstrajs/package.json +0 -60
  308. package/node_modules/dijkstrajs/test/dijkstra.test.js +0 -96
  309. package/node_modules/emoji-regex/LICENSE-MIT.txt +0 -20
  310. package/node_modules/emoji-regex/README.md +0 -73
  311. package/node_modules/emoji-regex/es2015/index.js +0 -6
  312. package/node_modules/emoji-regex/es2015/text.js +0 -6
  313. package/node_modules/emoji-regex/index.d.ts +0 -23
  314. package/node_modules/emoji-regex/index.js +0 -6
  315. package/node_modules/emoji-regex/package.json +0 -50
  316. package/node_modules/emoji-regex/text.js +0 -6
  317. package/node_modules/find-up/index.d.ts +0 -137
  318. package/node_modules/find-up/index.js +0 -89
  319. package/node_modules/find-up/license +0 -9
  320. package/node_modules/find-up/package.json +0 -53
  321. package/node_modules/find-up/readme.md +0 -156
  322. package/node_modules/get-caller-file/LICENSE.md +0 -6
  323. package/node_modules/get-caller-file/README.md +0 -41
  324. package/node_modules/get-caller-file/index.d.ts +0 -2
  325. package/node_modules/get-caller-file/index.js +0 -22
  326. package/node_modules/get-caller-file/index.js.map +0 -1
  327. package/node_modules/get-caller-file/package.json +0 -42
  328. package/node_modules/is-fullwidth-code-point/index.d.ts +0 -17
  329. package/node_modules/is-fullwidth-code-point/index.js +0 -50
  330. package/node_modules/is-fullwidth-code-point/license +0 -9
  331. package/node_modules/is-fullwidth-code-point/package.json +0 -42
  332. package/node_modules/is-fullwidth-code-point/readme.md +0 -39
  333. package/node_modules/locate-path/index.d.ts +0 -83
  334. package/node_modules/locate-path/index.js +0 -65
  335. package/node_modules/locate-path/license +0 -9
  336. package/node_modules/locate-path/package.json +0 -45
  337. package/node_modules/locate-path/readme.md +0 -122
  338. package/node_modules/node-datachannel/.clang-format +0 -17
  339. package/node_modules/node-datachannel/.editorconfig +0 -12
  340. package/node_modules/node-datachannel/.eslintignore +0 -8
  341. package/node_modules/node-datachannel/.eslintrc.json +0 -27
  342. package/node_modules/node-datachannel/.gitmodules +0 -3
  343. package/node_modules/node-datachannel/.prettierignore +0 -7
  344. package/node_modules/node-datachannel/.prettierrc +0 -13
  345. package/node_modules/node-datachannel/API.md +0 -247
  346. package/node_modules/node-datachannel/BULDING.md +0 -26
  347. package/node_modules/node-datachannel/CMakeLists.txt +0 -153
  348. package/node_modules/node-datachannel/LICENSE +0 -373
  349. package/node_modules/node-datachannel/README.md +0 -129
  350. package/node_modules/node-datachannel/dist/cjs/index.cjs +0 -11
  351. package/node_modules/node-datachannel/dist/cjs/index.cjs.map +0 -1
  352. package/node_modules/node-datachannel/dist/cjs/lib/datachannel-stream.cjs +0 -101
  353. package/node_modules/node-datachannel/dist/cjs/lib/datachannel-stream.cjs.map +0 -1
  354. package/node_modules/node-datachannel/dist/cjs/lib/index.cjs +0 -91
  355. package/node_modules/node-datachannel/dist/cjs/lib/index.cjs.map +0 -1
  356. package/node_modules/node-datachannel/dist/cjs/lib/node-datachannel.cjs +0 -94
  357. package/node_modules/node-datachannel/dist/cjs/lib/node-datachannel.cjs.map +0 -1
  358. package/node_modules/node-datachannel/dist/cjs/lib/websocket-server.cjs +0 -45
  359. package/node_modules/node-datachannel/dist/cjs/lib/websocket-server.cjs.map +0 -1
  360. package/node_modules/node-datachannel/dist/cjs/lib/websocket.cjs +0 -8
  361. package/node_modules/node-datachannel/dist/cjs/lib/websocket.cjs.map +0 -1
  362. package/node_modules/node-datachannel/dist/cjs/polyfill/Events.cjs +0 -86
  363. package/node_modules/node-datachannel/dist/cjs/polyfill/Events.cjs.map +0 -1
  364. package/node_modules/node-datachannel/dist/cjs/polyfill/Exception.cjs +0 -34
  365. package/node_modules/node-datachannel/dist/cjs/polyfill/Exception.cjs.map +0 -1
  366. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCCertificate.cjs +0 -34
  367. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCCertificate.cjs.map +0 -1
  368. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCDataChannel.cjs +0 -214
  369. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCDataChannel.cjs.map +0 -1
  370. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCDtlsTransport.cjs +0 -53
  371. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCDtlsTransport.cjs.map +0 -1
  372. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCError.cjs +0 -83
  373. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCError.cjs.map +0 -1
  374. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCIceCandidate.cjs +0 -132
  375. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCIceCandidate.cjs.map +0 -1
  376. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCIceTransport.cjs +0 -94
  377. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCIceTransport.cjs.map +0 -1
  378. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCPeerConnection.cjs +0 -434
  379. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCPeerConnection.cjs.map +0 -1
  380. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCSctpTransport.cjs +0 -59
  381. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCSctpTransport.cjs.map +0 -1
  382. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCSessionDescription.cjs +0 -45
  383. package/node_modules/node-datachannel/dist/cjs/polyfill/RTCSessionDescription.cjs.map +0 -1
  384. package/node_modules/node-datachannel/dist/cjs/polyfill/index.cjs +0 -42
  385. package/node_modules/node-datachannel/dist/cjs/polyfill/index.cjs.map +0 -1
  386. package/node_modules/node-datachannel/dist/esm/index.mjs +0 -8
  387. package/node_modules/node-datachannel/dist/esm/index.mjs.map +0 -1
  388. package/node_modules/node-datachannel/dist/esm/lib/datachannel-stream.mjs +0 -78
  389. package/node_modules/node-datachannel/dist/esm/lib/datachannel-stream.mjs.map +0 -1
  390. package/node_modules/node-datachannel/dist/esm/lib/index.mjs +0 -64
  391. package/node_modules/node-datachannel/dist/esm/lib/index.mjs.map +0 -1
  392. package/node_modules/node-datachannel/dist/esm/lib/node-datachannel.mjs +0 -78
  393. package/node_modules/node-datachannel/dist/esm/lib/node-datachannel.mjs.map +0 -1
  394. package/node_modules/node-datachannel/dist/esm/lib/websocket-server.mjs +0 -43
  395. package/node_modules/node-datachannel/dist/esm/lib/websocket-server.mjs.map +0 -1
  396. package/node_modules/node-datachannel/dist/esm/lib/websocket.mjs +0 -6
  397. package/node_modules/node-datachannel/dist/esm/lib/websocket.mjs.map +0 -1
  398. package/node_modules/node-datachannel/dist/esm/polyfill/Events.mjs +0 -82
  399. package/node_modules/node-datachannel/dist/esm/polyfill/Events.mjs.map +0 -1
  400. package/node_modules/node-datachannel/dist/esm/polyfill/Exception.mjs +0 -28
  401. package/node_modules/node-datachannel/dist/esm/polyfill/Exception.mjs.map +0 -1
  402. package/node_modules/node-datachannel/dist/esm/polyfill/RTCCertificate.mjs +0 -30
  403. package/node_modules/node-datachannel/dist/esm/polyfill/RTCCertificate.mjs.map +0 -1
  404. package/node_modules/node-datachannel/dist/esm/polyfill/RTCDataChannel.mjs +0 -210
  405. package/node_modules/node-datachannel/dist/esm/polyfill/RTCDataChannel.mjs.map +0 -1
  406. package/node_modules/node-datachannel/dist/esm/polyfill/RTCDtlsTransport.mjs +0 -49
  407. package/node_modules/node-datachannel/dist/esm/polyfill/RTCDtlsTransport.mjs.map +0 -1
  408. package/node_modules/node-datachannel/dist/esm/polyfill/RTCError.mjs +0 -79
  409. package/node_modules/node-datachannel/dist/esm/polyfill/RTCError.mjs.map +0 -1
  410. package/node_modules/node-datachannel/dist/esm/polyfill/RTCIceCandidate.mjs +0 -128
  411. package/node_modules/node-datachannel/dist/esm/polyfill/RTCIceCandidate.mjs.map +0 -1
  412. package/node_modules/node-datachannel/dist/esm/polyfill/RTCIceTransport.mjs +0 -89
  413. package/node_modules/node-datachannel/dist/esm/polyfill/RTCIceTransport.mjs.map +0 -1
  414. package/node_modules/node-datachannel/dist/esm/polyfill/RTCPeerConnection.mjs +0 -430
  415. package/node_modules/node-datachannel/dist/esm/polyfill/RTCPeerConnection.mjs.map +0 -1
  416. package/node_modules/node-datachannel/dist/esm/polyfill/RTCSctpTransport.mjs +0 -55
  417. package/node_modules/node-datachannel/dist/esm/polyfill/RTCSctpTransport.mjs.map +0 -1
  418. package/node_modules/node-datachannel/dist/esm/polyfill/RTCSessionDescription.mjs +0 -41
  419. package/node_modules/node-datachannel/dist/esm/polyfill/RTCSessionDescription.mjs.map +0 -1
  420. package/node_modules/node-datachannel/dist/esm/polyfill/index.mjs +0 -27
  421. package/node_modules/node-datachannel/dist/esm/polyfill/index.mjs.map +0 -1
  422. package/node_modules/node-datachannel/dist/types/lib/datachannel-stream.d.ts +0 -24
  423. package/node_modules/node-datachannel/dist/types/lib/index.d.ts +0 -241
  424. package/node_modules/node-datachannel/dist/types/lib/types.d.ts +0 -119
  425. package/node_modules/node-datachannel/dist/types/lib/websocket-server.d.ts +0 -13
  426. package/node_modules/node-datachannel/dist/types/lib/websocket.d.ts +0 -25
  427. package/node_modules/node-datachannel/dist/types/polyfill/Events.d.ts +0 -15
  428. package/node_modules/node-datachannel/dist/types/polyfill/RTCCertificate.d.ts +0 -9
  429. package/node_modules/node-datachannel/dist/types/polyfill/RTCDataChannel.d.ts +0 -29
  430. package/node_modules/node-datachannel/dist/types/polyfill/RTCDtlsTransport.d.ts +0 -15
  431. package/node_modules/node-datachannel/dist/types/polyfill/RTCError.d.ts +0 -17
  432. package/node_modules/node-datachannel/dist/types/polyfill/RTCIceCandidate.d.ts +0 -21
  433. package/node_modules/node-datachannel/dist/types/polyfill/RTCIceTransport.d.ts +0 -30
  434. package/node_modules/node-datachannel/dist/types/polyfill/RTCPeerConnection.d.ts +0 -64
  435. package/node_modules/node-datachannel/dist/types/polyfill/RTCSctpTransport.d.ts +0 -15
  436. package/node_modules/node-datachannel/dist/types/polyfill/RTCSessionDescription.d.ts +0 -10
  437. package/node_modules/node-datachannel/dist/types/polyfill/index.d.ts +0 -26
  438. package/node_modules/node-datachannel/jest.config.ts +0 -14
  439. package/node_modules/node-datachannel/package.json +0 -133
  440. package/node_modules/node-datachannel/rollup.config.mjs +0 -67
  441. package/node_modules/node-datachannel/src/cpp/data-channel-wrapper.cpp +0 -530
  442. package/node_modules/node-datachannel/src/cpp/data-channel-wrapper.h +0 -63
  443. package/node_modules/node-datachannel/src/cpp/ice-udp-mux-listener-wrapper.cpp +0 -172
  444. package/node_modules/node-datachannel/src/cpp/ice-udp-mux-listener-wrapper.h +0 -43
  445. package/node_modules/node-datachannel/src/cpp/main.cpp +0 -58
  446. package/node_modules/node-datachannel/src/cpp/media-audio-wrapper.cpp +0 -457
  447. package/node_modules/node-datachannel/src/cpp/media-audio-wrapper.h +0 -52
  448. package/node_modules/node-datachannel/src/cpp/media-av1packetization.cpp +0 -24
  449. package/node_modules/node-datachannel/src/cpp/media-av1packetization.h +0 -11
  450. package/node_modules/node-datachannel/src/cpp/media-av1rtppacketizer-wrapper.cpp +0 -126
  451. package/node_modules/node-datachannel/src/cpp/media-av1rtppacketizer-wrapper.h +0 -29
  452. package/node_modules/node-datachannel/src/cpp/media-direction.cpp +0 -43
  453. package/node_modules/node-datachannel/src/cpp/media-direction.h +0 -10
  454. package/node_modules/node-datachannel/src/cpp/media-h264rtppacketizer-wrapper.cpp +0 -126
  455. package/node_modules/node-datachannel/src/cpp/media-h264rtppacketizer-wrapper.h +0 -30
  456. package/node_modules/node-datachannel/src/cpp/media-h265rtppacketizer-wrapper.cpp +0 -126
  457. package/node_modules/node-datachannel/src/cpp/media-h265rtppacketizer-wrapper.h +0 -30
  458. package/node_modules/node-datachannel/src/cpp/media-h26xseparator.cpp +0 -32
  459. package/node_modules/node-datachannel/src/cpp/media-h26xseparator.h +0 -11
  460. package/node_modules/node-datachannel/src/cpp/media-mediahandler-helper.cpp +0 -31
  461. package/node_modules/node-datachannel/src/cpp/media-mediahandler-helper.h +0 -11
  462. package/node_modules/node-datachannel/src/cpp/media-pacinghandler-wrapper.cpp +0 -79
  463. package/node_modules/node-datachannel/src/cpp/media-pacinghandler-wrapper.h +0 -28
  464. package/node_modules/node-datachannel/src/cpp/media-rtcpnackresponder-wrapper.cpp +0 -68
  465. package/node_modules/node-datachannel/src/cpp/media-rtcpnackresponder-wrapper.h +0 -28
  466. package/node_modules/node-datachannel/src/cpp/media-rtcpreceivingsession-wrapper.cpp +0 -57
  467. package/node_modules/node-datachannel/src/cpp/media-rtcpreceivingsession-wrapper.h +0 -28
  468. package/node_modules/node-datachannel/src/cpp/media-rtcpsrreporter-wrapper.cpp +0 -93
  469. package/node_modules/node-datachannel/src/cpp/media-rtcpsrreporter-wrapper.h +0 -30
  470. package/node_modules/node-datachannel/src/cpp/media-rtppacketizationconfig-wrapper.cpp +0 -167
  471. package/node_modules/node-datachannel/src/cpp/media-rtppacketizationconfig-wrapper.h +0 -35
  472. package/node_modules/node-datachannel/src/cpp/media-rtppacketizer-wrapper.cpp +0 -95
  473. package/node_modules/node-datachannel/src/cpp/media-rtppacketizer-wrapper.h +0 -30
  474. package/node_modules/node-datachannel/src/cpp/media-track-wrapper.cpp +0 -458
  475. package/node_modules/node-datachannel/src/cpp/media-track-wrapper.h +0 -61
  476. package/node_modules/node-datachannel/src/cpp/media-video-wrapper.cpp +0 -526
  477. package/node_modules/node-datachannel/src/cpp/media-video-wrapper.h +0 -56
  478. package/node_modules/node-datachannel/src/cpp/peer-connection-wrapper.cpp +0 -1298
  479. package/node_modules/node-datachannel/src/cpp/peer-connection-wrapper.h +0 -89
  480. package/node_modules/node-datachannel/src/cpp/rtc-wrapper.cpp +0 -205
  481. package/node_modules/node-datachannel/src/cpp/rtc-wrapper.h +0 -24
  482. package/node_modules/node-datachannel/src/cpp/thread-safe-callback.cpp +0 -57
  483. package/node_modules/node-datachannel/src/cpp/thread-safe-callback.h +0 -47
  484. package/node_modules/node-datachannel/src/cpp/web-socket-server-wrapper.cpp +0 -275
  485. package/node_modules/node-datachannel/src/cpp/web-socket-server-wrapper.h +0 -41
  486. package/node_modules/node-datachannel/src/cpp/web-socket-wrapper.cpp +0 -796
  487. package/node_modules/node-datachannel/src/cpp/web-socket-wrapper.h +0 -63
  488. package/node_modules/node-datachannel/src/index.ts +0 -9
  489. package/node_modules/node-datachannel/src/lib/datachannel-stream.ts +0 -100
  490. package/node_modules/node-datachannel/src/lib/index.ts +0 -290
  491. package/node_modules/node-datachannel/src/lib/node-datachannel.ts +0 -84
  492. package/node_modules/node-datachannel/src/lib/types.ts +0 -169
  493. package/node_modules/node-datachannel/src/lib/websocket-server.ts +0 -37
  494. package/node_modules/node-datachannel/src/lib/websocket.ts +0 -26
  495. package/node_modules/node-datachannel/src/polyfill/Events.ts +0 -82
  496. package/node_modules/node-datachannel/src/polyfill/Exception.ts +0 -37
  497. package/node_modules/node-datachannel/src/polyfill/README.md +0 -41
  498. package/node_modules/node-datachannel/src/polyfill/RTCCertificate.ts +0 -21
  499. package/node_modules/node-datachannel/src/polyfill/RTCDataChannel.ts +0 -225
  500. package/node_modules/node-datachannel/src/polyfill/RTCDtlsTransport.ts +0 -46
  501. package/node_modules/node-datachannel/src/polyfill/RTCError.ts +0 -78
  502. package/node_modules/node-datachannel/src/polyfill/RTCIceCandidate.ts +0 -128
  503. package/node_modules/node-datachannel/src/polyfill/RTCIceTransport.ts +0 -90
  504. package/node_modules/node-datachannel/src/polyfill/RTCPeerConnection.ts +0 -527
  505. package/node_modules/node-datachannel/src/polyfill/RTCSctpTransport.ts +0 -51
  506. package/node_modules/node-datachannel/src/polyfill/RTCSessionDescription.ts +0 -41
  507. package/node_modules/node-datachannel/src/polyfill/index.ts +0 -38
  508. package/node_modules/node-datachannel/tsconfig.json +0 -21
  509. package/node_modules/p-limit/index.d.ts +0 -38
  510. package/node_modules/p-limit/index.js +0 -57
  511. package/node_modules/p-limit/license +0 -9
  512. package/node_modules/p-limit/package.json +0 -52
  513. package/node_modules/p-limit/readme.md +0 -101
  514. package/node_modules/p-locate/index.d.ts +0 -64
  515. package/node_modules/p-locate/index.js +0 -52
  516. package/node_modules/p-locate/license +0 -9
  517. package/node_modules/p-locate/package.json +0 -53
  518. package/node_modules/p-locate/readme.md +0 -90
  519. package/node_modules/p-try/index.d.ts +0 -39
  520. package/node_modules/p-try/index.js +0 -9
  521. package/node_modules/p-try/license +0 -9
  522. package/node_modules/p-try/package.json +0 -42
  523. package/node_modules/p-try/readme.md +0 -58
  524. package/node_modules/path-exists/index.d.ts +0 -28
  525. package/node_modules/path-exists/index.js +0 -23
  526. package/node_modules/path-exists/license +0 -9
  527. package/node_modules/path-exists/package.json +0 -39
  528. package/node_modules/path-exists/readme.md +0 -52
  529. package/node_modules/pngjs/.eslintignore +0 -2
  530. package/node_modules/pngjs/.eslintrc.json +0 -19
  531. package/node_modules/pngjs/.prettierignore +0 -3
  532. package/node_modules/pngjs/LICENSE +0 -20
  533. package/node_modules/pngjs/README.md +0 -429
  534. package/node_modules/pngjs/browser.js +0 -16096
  535. package/node_modules/pngjs/coverage/lcov-report/base.css +0 -354
  536. package/node_modules/pngjs/coverage/lcov-report/bitmapper.js.html +0 -881
  537. package/node_modules/pngjs/coverage/lcov-report/bitpacker.js.html +0 -554
  538. package/node_modules/pngjs/coverage/lcov-report/block-navigation.js +0 -77
  539. package/node_modules/pngjs/coverage/lcov-report/chunkstream.js.html +0 -647
  540. package/node_modules/pngjs/coverage/lcov-report/constants.js.html +0 -176
  541. package/node_modules/pngjs/coverage/lcov-report/crc.js.html +0 -200
  542. package/node_modules/pngjs/coverage/lcov-report/favicon.png +0 -0
  543. package/node_modules/pngjs/coverage/lcov-report/filter-pack.js.html +0 -593
  544. package/node_modules/pngjs/coverage/lcov-report/filter-parse-async.js.html +0 -152
  545. package/node_modules/pngjs/coverage/lcov-report/filter-parse-sync.js.html +0 -143
  546. package/node_modules/pngjs/coverage/lcov-report/filter-parse.js.html +0 -611
  547. package/node_modules/pngjs/coverage/lcov-report/format-normaliser.js.html +0 -359
  548. package/node_modules/pngjs/coverage/lcov-report/index.html +0 -426
  549. package/node_modules/pngjs/coverage/lcov-report/interlace.js.html +0 -365
  550. package/node_modules/pngjs/coverage/lcov-report/packer-async.js.html +0 -230
  551. package/node_modules/pngjs/coverage/lcov-report/packer-sync.js.html +0 -248
  552. package/node_modules/pngjs/coverage/lcov-report/packer.js.html +0 -467
  553. package/node_modules/pngjs/coverage/lcov-report/paeth-predictor.js.html +0 -128
  554. package/node_modules/pngjs/coverage/lcov-report/parser-async.js.html +0 -575
  555. package/node_modules/pngjs/coverage/lcov-report/parser-sync.js.html +0 -404
  556. package/node_modules/pngjs/coverage/lcov-report/parser.js.html +0 -950
  557. package/node_modules/pngjs/coverage/lcov-report/png-sync.js.html +0 -116
  558. package/node_modules/pngjs/coverage/lcov-report/png.js.html +0 -662
  559. package/node_modules/pngjs/coverage/lcov-report/prettify.css +0 -101
  560. package/node_modules/pngjs/coverage/lcov-report/prettify.js +0 -1007
  561. package/node_modules/pngjs/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  562. package/node_modules/pngjs/coverage/lcov-report/sorter.js +0 -169
  563. package/node_modules/pngjs/coverage/lcov-report/sync-inflate.js.html +0 -584
  564. package/node_modules/pngjs/coverage/lcov-report/sync-reader.js.html +0 -215
  565. package/node_modules/pngjs/coverage/lcov.info +0 -2177
  566. package/node_modules/pngjs/lib/bitmapper.js +0 -267
  567. package/node_modules/pngjs/lib/bitpacker.js +0 -158
  568. package/node_modules/pngjs/lib/chunkstream.js +0 -189
  569. package/node_modules/pngjs/lib/constants.js +0 -32
  570. package/node_modules/pngjs/lib/crc.js +0 -40
  571. package/node_modules/pngjs/lib/filter-pack.js +0 -171
  572. package/node_modules/pngjs/lib/filter-parse-async.js +0 -24
  573. package/node_modules/pngjs/lib/filter-parse-sync.js +0 -21
  574. package/node_modules/pngjs/lib/filter-parse.js +0 -177
  575. package/node_modules/pngjs/lib/format-normaliser.js +0 -93
  576. package/node_modules/pngjs/lib/interlace.js +0 -95
  577. package/node_modules/pngjs/lib/packer-async.js +0 -50
  578. package/node_modules/pngjs/lib/packer-sync.js +0 -56
  579. package/node_modules/pngjs/lib/packer.js +0 -129
  580. package/node_modules/pngjs/lib/paeth-predictor.js +0 -16
  581. package/node_modules/pngjs/lib/parser-async.js +0 -165
  582. package/node_modules/pngjs/lib/parser-sync.js +0 -108
  583. package/node_modules/pngjs/lib/parser.js +0 -290
  584. package/node_modules/pngjs/lib/png-sync.js +0 -12
  585. package/node_modules/pngjs/lib/png.js +0 -194
  586. package/node_modules/pngjs/lib/sync-inflate.js +0 -168
  587. package/node_modules/pngjs/lib/sync-reader.js +0 -45
  588. package/node_modules/pngjs/package.json +0 -73
  589. package/node_modules/qrcode/README.md +0 -770
  590. package/node_modules/qrcode/bin/qrcode +0 -159
  591. package/node_modules/qrcode/helper/to-sjis-browser.js +0 -2
  592. package/node_modules/qrcode/helper/to-sjis.js +0 -105
  593. package/node_modules/qrcode/lib/browser.js +0 -76
  594. package/node_modules/qrcode/lib/can-promise.js +0 -7
  595. package/node_modules/qrcode/lib/core/alignment-pattern.js +0 -83
  596. package/node_modules/qrcode/lib/core/alphanumeric-data.js +0 -59
  597. package/node_modules/qrcode/lib/core/bit-buffer.js +0 -37
  598. package/node_modules/qrcode/lib/core/bit-matrix.js +0 -65
  599. package/node_modules/qrcode/lib/core/byte-data.js +0 -30
  600. package/node_modules/qrcode/lib/core/error-correction-code.js +0 -135
  601. package/node_modules/qrcode/lib/core/error-correction-level.js +0 -50
  602. package/node_modules/qrcode/lib/core/finder-pattern.js +0 -22
  603. package/node_modules/qrcode/lib/core/format-info.js +0 -29
  604. package/node_modules/qrcode/lib/core/galois-field.js +0 -69
  605. package/node_modules/qrcode/lib/core/kanji-data.js +0 -54
  606. package/node_modules/qrcode/lib/core/mask-pattern.js +0 -234
  607. package/node_modules/qrcode/lib/core/mode.js +0 -167
  608. package/node_modules/qrcode/lib/core/numeric-data.js +0 -43
  609. package/node_modules/qrcode/lib/core/polynomial.js +0 -62
  610. package/node_modules/qrcode/lib/core/qrcode.js +0 -495
  611. package/node_modules/qrcode/lib/core/reed-solomon-encoder.js +0 -56
  612. package/node_modules/qrcode/lib/core/regex.js +0 -31
  613. package/node_modules/qrcode/lib/core/segments.js +0 -330
  614. package/node_modules/qrcode/lib/core/utils.js +0 -63
  615. package/node_modules/qrcode/lib/core/version-check.js +0 -9
  616. package/node_modules/qrcode/lib/core/version.js +0 -163
  617. package/node_modules/qrcode/lib/index.js +0 -12
  618. package/node_modules/qrcode/lib/renderer/canvas.js +0 -63
  619. package/node_modules/qrcode/lib/renderer/png.js +0 -78
  620. package/node_modules/qrcode/lib/renderer/svg-tag.js +0 -81
  621. package/node_modules/qrcode/lib/renderer/svg.js +0 -19
  622. package/node_modules/qrcode/lib/renderer/terminal/terminal-small.js +0 -85
  623. package/node_modules/qrcode/lib/renderer/terminal/terminal.js +0 -49
  624. package/node_modules/qrcode/lib/renderer/terminal.js +0 -9
  625. package/node_modules/qrcode/lib/renderer/utf8.js +0 -71
  626. package/node_modules/qrcode/lib/renderer/utils.js +0 -99
  627. package/node_modules/qrcode/lib/server.js +0 -138
  628. package/node_modules/qrcode/license +0 -10
  629. package/node_modules/qrcode/package.json +0 -77
  630. package/node_modules/require-directory/.jshintrc +0 -67
  631. package/node_modules/require-directory/.npmignore +0 -1
  632. package/node_modules/require-directory/.travis.yml +0 -3
  633. package/node_modules/require-directory/LICENSE +0 -22
  634. package/node_modules/require-directory/README.markdown +0 -184
  635. package/node_modules/require-directory/index.js +0 -86
  636. package/node_modules/require-directory/package.json +0 -40
  637. package/node_modules/require-main-filename/CHANGELOG.md +0 -26
  638. package/node_modules/require-main-filename/LICENSE.txt +0 -14
  639. package/node_modules/require-main-filename/README.md +0 -26
  640. package/node_modules/require-main-filename/index.js +0 -18
  641. package/node_modules/require-main-filename/package.json +0 -35
  642. package/node_modules/semver/LICENSE +0 -15
  643. package/node_modules/semver/README.md +0 -680
  644. package/node_modules/semver/bin/semver.js +0 -195
  645. package/node_modules/semver/classes/comparator.js +0 -143
  646. package/node_modules/semver/classes/index.js +0 -7
  647. package/node_modules/semver/classes/range.js +0 -577
  648. package/node_modules/semver/classes/semver.js +0 -350
  649. package/node_modules/semver/functions/clean.js +0 -8
  650. package/node_modules/semver/functions/cmp.js +0 -54
  651. package/node_modules/semver/functions/coerce.js +0 -62
  652. package/node_modules/semver/functions/compare-build.js +0 -9
  653. package/node_modules/semver/functions/compare-loose.js +0 -5
  654. package/node_modules/semver/functions/compare.js +0 -7
  655. package/node_modules/semver/functions/diff.js +0 -60
  656. package/node_modules/semver/functions/eq.js +0 -5
  657. package/node_modules/semver/functions/gt.js +0 -5
  658. package/node_modules/semver/functions/gte.js +0 -5
  659. package/node_modules/semver/functions/inc.js +0 -21
  660. package/node_modules/semver/functions/lt.js +0 -5
  661. package/node_modules/semver/functions/lte.js +0 -5
  662. package/node_modules/semver/functions/major.js +0 -5
  663. package/node_modules/semver/functions/minor.js +0 -5
  664. package/node_modules/semver/functions/neq.js +0 -5
  665. package/node_modules/semver/functions/parse.js +0 -18
  666. package/node_modules/semver/functions/patch.js +0 -5
  667. package/node_modules/semver/functions/prerelease.js +0 -8
  668. package/node_modules/semver/functions/rcompare.js +0 -5
  669. package/node_modules/semver/functions/rsort.js +0 -5
  670. package/node_modules/semver/functions/satisfies.js +0 -12
  671. package/node_modules/semver/functions/sort.js +0 -5
  672. package/node_modules/semver/functions/truncate.js +0 -48
  673. package/node_modules/semver/functions/valid.js +0 -8
  674. package/node_modules/semver/index.js +0 -93
  675. package/node_modules/semver/internal/constants.js +0 -37
  676. package/node_modules/semver/internal/debug.js +0 -11
  677. package/node_modules/semver/internal/identifiers.js +0 -29
  678. package/node_modules/semver/internal/lrucache.js +0 -42
  679. package/node_modules/semver/internal/parse-options.js +0 -17
  680. package/node_modules/semver/internal/re.js +0 -223
  681. package/node_modules/semver/package.json +0 -78
  682. package/node_modules/semver/preload.js +0 -4
  683. package/node_modules/semver/range.bnf +0 -17
  684. package/node_modules/semver/ranges/gtr.js +0 -6
  685. package/node_modules/semver/ranges/intersects.js +0 -9
  686. package/node_modules/semver/ranges/ltr.js +0 -6
  687. package/node_modules/semver/ranges/max-satisfying.js +0 -27
  688. package/node_modules/semver/ranges/min-satisfying.js +0 -26
  689. package/node_modules/semver/ranges/min-version.js +0 -63
  690. package/node_modules/semver/ranges/outside.js +0 -82
  691. package/node_modules/semver/ranges/simplify.js +0 -49
  692. package/node_modules/semver/ranges/subset.js +0 -249
  693. package/node_modules/semver/ranges/to-comparators.js +0 -10
  694. package/node_modules/semver/ranges/valid.js +0 -13
  695. package/node_modules/set-blocking/CHANGELOG.md +0 -26
  696. package/node_modules/set-blocking/LICENSE.txt +0 -14
  697. package/node_modules/set-blocking/README.md +0 -31
  698. package/node_modules/set-blocking/index.js +0 -7
  699. package/node_modules/set-blocking/package.json +0 -42
  700. package/node_modules/sharp/LICENSE +0 -191
  701. package/node_modules/sharp/README.md +0 -112
  702. package/node_modules/sharp/dist/channel.cjs +0 -177
  703. package/node_modules/sharp/dist/channel.mjs +0 -177
  704. package/node_modules/sharp/dist/colour.cjs +0 -199
  705. package/node_modules/sharp/dist/colour.mjs +0 -199
  706. package/node_modules/sharp/dist/composite.cjs +0 -213
  707. package/node_modules/sharp/dist/composite.mjs +0 -213
  708. package/node_modules/sharp/dist/constructor.cjs +0 -511
  709. package/node_modules/sharp/dist/constructor.mjs +0 -511
  710. package/node_modules/sharp/dist/index.cjs +0 -25
  711. package/node_modules/sharp/dist/index.d.cts +0 -1999
  712. package/node_modules/sharp/dist/index.d.mts +0 -2046
  713. package/node_modules/sharp/dist/index.mjs +0 -25
  714. package/node_modules/sharp/dist/input.cjs +0 -819
  715. package/node_modules/sharp/dist/input.mjs +0 -819
  716. package/node_modules/sharp/dist/is.cjs +0 -143
  717. package/node_modules/sharp/dist/is.mjs +0 -143
  718. package/node_modules/sharp/dist/libvips.cjs +0 -212
  719. package/node_modules/sharp/dist/libvips.mjs +0 -212
  720. package/node_modules/sharp/dist/operation.cjs +0 -1002
  721. package/node_modules/sharp/dist/operation.mjs +0 -1002
  722. package/node_modules/sharp/dist/output.cjs +0 -1785
  723. package/node_modules/sharp/dist/output.mjs +0 -1785
  724. package/node_modules/sharp/dist/resize.cjs +0 -617
  725. package/node_modules/sharp/dist/resize.mjs +0 -617
  726. package/node_modules/sharp/dist/sharp.cjs +0 -174
  727. package/node_modules/sharp/dist/sharp.mjs +0 -174
  728. package/node_modules/sharp/dist/utility.cjs +0 -301
  729. package/node_modules/sharp/dist/utility.mjs +0 -301
  730. package/node_modules/sharp/install/build.js +0 -38
  731. package/node_modules/sharp/lib/index.d.ts +0 -1999
  732. package/node_modules/sharp/package.json +0 -227
  733. package/node_modules/sharp/src/binding.gyp +0 -303
  734. package/node_modules/sharp/src/common.cc +0 -1186
  735. package/node_modules/sharp/src/common.h +0 -421
  736. package/node_modules/sharp/src/metadata.cc +0 -404
  737. package/node_modules/sharp/src/metadata.h +0 -95
  738. package/node_modules/sharp/src/operations.cc +0 -516
  739. package/node_modules/sharp/src/operations.h +0 -137
  740. package/node_modules/sharp/src/pipeline.cc +0 -1948
  741. package/node_modules/sharp/src/pipeline.h +0 -422
  742. package/node_modules/sharp/src/sharp.cc +0 -43
  743. package/node_modules/sharp/src/stats.cc +0 -186
  744. package/node_modules/sharp/src/stats.h +0 -62
  745. package/node_modules/sharp/src/utilities.cc +0 -289
  746. package/node_modules/sharp/src/utilities.h +0 -22
  747. package/node_modules/string-width/index.d.ts +0 -29
  748. package/node_modules/string-width/index.js +0 -47
  749. package/node_modules/string-width/license +0 -9
  750. package/node_modules/string-width/package.json +0 -56
  751. package/node_modules/string-width/readme.md +0 -50
  752. package/node_modules/strip-ansi/index.d.ts +0 -17
  753. package/node_modules/strip-ansi/index.js +0 -4
  754. package/node_modules/strip-ansi/license +0 -9
  755. package/node_modules/strip-ansi/package.json +0 -54
  756. package/node_modules/strip-ansi/readme.md +0 -46
  757. package/node_modules/which-module/LICENSE +0 -13
  758. package/node_modules/which-module/README.md +0 -58
  759. package/node_modules/which-module/index.js +0 -9
  760. package/node_modules/which-module/package.json +0 -41
  761. package/node_modules/wrap-ansi/index.js +0 -186
  762. package/node_modules/wrap-ansi/license +0 -9
  763. package/node_modules/wrap-ansi/package.json +0 -61
  764. package/node_modules/wrap-ansi/readme.md +0 -97
  765. package/node_modules/ws/LICENSE +0 -20
  766. package/node_modules/ws/README.md +0 -548
  767. package/node_modules/ws/browser.js +0 -8
  768. package/node_modules/ws/index.js +0 -22
  769. package/node_modules/ws/lib/buffer-util.js +0 -131
  770. package/node_modules/ws/lib/constants.js +0 -19
  771. package/node_modules/ws/lib/event-target.js +0 -292
  772. package/node_modules/ws/lib/extension.js +0 -203
  773. package/node_modules/ws/lib/limiter.js +0 -55
  774. package/node_modules/ws/lib/permessage-deflate.js +0 -530
  775. package/node_modules/ws/lib/receiver.js +0 -743
  776. package/node_modules/ws/lib/sender.js +0 -607
  777. package/node_modules/ws/lib/stream.js +0 -161
  778. package/node_modules/ws/lib/subprotocol.js +0 -62
  779. package/node_modules/ws/lib/validation.js +0 -152
  780. package/node_modules/ws/lib/websocket-server.js +0 -562
  781. package/node_modules/ws/lib/websocket.js +0 -1407
  782. package/node_modules/ws/package.json +0 -74
  783. package/node_modules/ws/wrapper.mjs +0 -21
  784. package/node_modules/y18n/CHANGELOG.md +0 -35
  785. package/node_modules/y18n/LICENSE +0 -13
  786. package/node_modules/y18n/README.md +0 -109
  787. package/node_modules/y18n/index.js +0 -188
  788. package/node_modules/y18n/package.json +0 -39
  789. package/node_modules/yargs/CHANGELOG.md +0 -420
  790. package/node_modules/yargs/LICENSE +0 -21
  791. package/node_modules/yargs/README.md +0 -140
  792. package/node_modules/yargs/build/lib/apply-extends.d.ts +0 -2
  793. package/node_modules/yargs/build/lib/apply-extends.js +0 -65
  794. package/node_modules/yargs/build/lib/argsert.d.ts +0 -2
  795. package/node_modules/yargs/build/lib/argsert.js +0 -65
  796. package/node_modules/yargs/build/lib/command.d.ts +0 -64
  797. package/node_modules/yargs/build/lib/command.js +0 -416
  798. package/node_modules/yargs/build/lib/common-types.d.ts +0 -36
  799. package/node_modules/yargs/build/lib/common-types.js +0 -25
  800. package/node_modules/yargs/build/lib/completion-templates.d.ts +0 -2
  801. package/node_modules/yargs/build/lib/completion-templates.js +0 -50
  802. package/node_modules/yargs/build/lib/completion.d.ts +0 -21
  803. package/node_modules/yargs/build/lib/completion.js +0 -135
  804. package/node_modules/yargs/build/lib/is-promise.d.ts +0 -1
  805. package/node_modules/yargs/build/lib/is-promise.js +0 -9
  806. package/node_modules/yargs/build/lib/levenshtein.d.ts +0 -1
  807. package/node_modules/yargs/build/lib/levenshtein.js +0 -58
  808. package/node_modules/yargs/build/lib/middleware.d.ts +0 -10
  809. package/node_modules/yargs/build/lib/middleware.js +0 -57
  810. package/node_modules/yargs/build/lib/obj-filter.d.ts +0 -1
  811. package/node_modules/yargs/build/lib/obj-filter.js +0 -14
  812. package/node_modules/yargs/build/lib/parse-command.d.ts +0 -11
  813. package/node_modules/yargs/build/lib/parse-command.js +0 -36
  814. package/node_modules/yargs/build/lib/process-argv.d.ts +0 -2
  815. package/node_modules/yargs/build/lib/process-argv.js +0 -31
  816. package/node_modules/yargs/build/lib/usage.d.ts +0 -49
  817. package/node_modules/yargs/build/lib/usage.js +0 -540
  818. package/node_modules/yargs/build/lib/validation.d.ts +0 -34
  819. package/node_modules/yargs/build/lib/validation.js +0 -330
  820. package/node_modules/yargs/build/lib/yargs.d.ts +0 -274
  821. package/node_modules/yargs/build/lib/yargs.js +0 -1190
  822. package/node_modules/yargs/build/lib/yerror.d.ts +0 -4
  823. package/node_modules/yargs/build/lib/yerror.js +0 -11
  824. package/node_modules/yargs/index.js +0 -40
  825. package/node_modules/yargs/locales/be.json +0 -46
  826. package/node_modules/yargs/locales/de.json +0 -46
  827. package/node_modules/yargs/locales/en.json +0 -51
  828. package/node_modules/yargs/locales/es.json +0 -46
  829. package/node_modules/yargs/locales/fi.json +0 -49
  830. package/node_modules/yargs/locales/fr.json +0 -53
  831. package/node_modules/yargs/locales/hi.json +0 -49
  832. package/node_modules/yargs/locales/hu.json +0 -46
  833. package/node_modules/yargs/locales/id.json +0 -50
  834. package/node_modules/yargs/locales/it.json +0 -46
  835. package/node_modules/yargs/locales/ja.json +0 -51
  836. package/node_modules/yargs/locales/ko.json +0 -49
  837. package/node_modules/yargs/locales/nb.json +0 -44
  838. package/node_modules/yargs/locales/nl.json +0 -49
  839. package/node_modules/yargs/locales/nn.json +0 -44
  840. package/node_modules/yargs/locales/pirate.json +0 -13
  841. package/node_modules/yargs/locales/pl.json +0 -49
  842. package/node_modules/yargs/locales/pt.json +0 -45
  843. package/node_modules/yargs/locales/pt_BR.json +0 -48
  844. package/node_modules/yargs/locales/ru.json +0 -46
  845. package/node_modules/yargs/locales/th.json +0 -46
  846. package/node_modules/yargs/locales/tr.json +0 -48
  847. package/node_modules/yargs/locales/zh_CN.json +0 -48
  848. package/node_modules/yargs/locales/zh_TW.json +0 -47
  849. package/node_modules/yargs/package.json +0 -92
  850. package/node_modules/yargs/yargs.js +0 -14
  851. package/node_modules/yargs-parser/CHANGELOG.md +0 -601
  852. package/node_modules/yargs-parser/LICENSE.txt +0 -14
  853. package/node_modules/yargs-parser/README.md +0 -449
  854. package/node_modules/yargs-parser/index.js +0 -1032
  855. package/node_modules/yargs-parser/lib/tokenize-arg-string.js +0 -40
  856. package/node_modules/yargs-parser/package.json +0 -46
@@ -1,468 +1,1104 @@
1
+ import { privacyPage } from './privacy-page.mjs';
2
+ import { createAndroidReleaseHandler } from './android-releases.mjs';
3
+ import { androidReleaseStorage } from '../../relay-admin/android-release-storage.mjs';
4
+ import { longPairing } from './long-pairing.mjs';
1
5
  import http from 'node:http';
6
+ import fs from 'node:fs';
7
+ import { isIP } from 'node:net';
2
8
  import { deviceStore, validId } from './device-store.mjs';
3
- import { randomUUID } from 'node:crypto';
9
+ import { randomUUID, timingSafeEqual } from 'node:crypto';
4
10
  import { WebSocketServer } from 'ws';
5
- import { secret, digest, matches, validSecret, send, decode } from './wire.mjs';
6
- import { readDebugAllowlist, debugAllowed } from './debug-allowlist.mjs';
7
- import { validProbeSignal } from './p2p-probe.mjs';
8
- import { relayAdminState } from './admin-state.mjs';
9
-
11
+ import { secret, digest, matches, validSecret, send, decode } from './wire.mjs';
12
+ import { routedFileBytes, parseFileBytes, sendFileFrame, closeFileSocket, discardFileSession } from './file-wire.mjs';
13
+ import { readDebugAllowlist, debugAllowed } from './debug-allowlist.mjs';
14
+ import { validProbeSignal } from './p2p-probe.mjs';
15
+ import { MAX_ADMIN_REQUEST_BYTES, relayAdminState } from './admin-state.mjs';
16
+ import { openRelayMysql } from './mysql-state.mjs';
17
+ import { openEmailAccounts } from './email-accounts.mjs';
18
+ import { accountConnections } from './account-connections.mjs';
19
+ import { usageMeter, usageReport } from './usage-meter.mjs';
20
+ import { relayDiagnostics } from './diagnostics.mjs';
21
+ import { connectionInfo, deviceInfo } from './device-info.mjs';
22
+ import { listWorkstations, workstationDetail, workstationSummary, resolveWorkstationAccounts, resolveWorkstationConnections } from './admin-workstations.mjs';
23
+ import { phoneConnectionEvents, validPhoneConnection } from './phone-connections.mjs';
24
+
10
25
  // Always loopback; a TLS reverse proxy supplies the private-trial Internet endpoint.
11
- export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlMs = 120000,
12
- maxConnections = 32, maxClientsPerHost = 16, maxPayload = 8 * 1024 * 1024, stateFile,
13
- maxRelaySessions = 8, adminStateFile, publicRelayUrl = '', appDownloadUrl = '', debugAllowlistFile, log = () => {} } = {}) {
14
- if (!validSecret(hostKey)) throw new Error('Invalid host credential');
15
- if (!Number.isSafeInteger(maxClientsPerHost) || maxClientsPerHost < 1 || maxClientsPerHost > 64) throw new Error('Invalid mobile device limit');
16
- if (!Number.isSafeInteger(maxRelaySessions) || maxRelaySessions < 1 || maxRelaySessions > 256) throw new Error('Invalid relay session limit');
17
- if (publicRelayUrl) relayPublicAddress(publicRelayUrl);
18
- // Harmony's native WebSocket supplies the endpoint Origin automatically.
19
- // libwebsockets can use http even for WSS; trust only the configured endpoint.
20
- const nativeOrigins = publicRelayUrl ? ['http:', 'https:'].map(protocol => `${protocol}//${new URL(publicRelayUrl).host}`) : [];
21
- if (appDownloadUrl && !/^https:\/\/[^\s]+$/.test(appDownloadUrl)) throw new Error('Invalid app download URL');
22
- const hostHash = digest(hostKey), rooms = new Map();
23
- const store = deviceStore(stateFile);
24
- const debugAllowlist = readDebugAllowlist(debugAllowlistFile);
25
- const adminState = relayAdminState(adminStateFile, maxRelaySessions);
26
- let activeRelaySessions = 0;
27
- for (const record of store.entries()) rooms.set(record.id, { ...record, durable: true, connections: new Map(), observers: new Map() });
28
- function commit(room, changes) {
29
- const next = { ...room, ...changes, updatedAt: new Date().toISOString() };
30
- if (room.durable) {
31
- const { id, hostHash, clients, revision, status, createdAt, updatedAt, officialComputer } = next;
32
- store.save({ id, hostHash, clients, revision, status, createdAt, updatedAt, ...(officialComputer ? { officialComputer } : {}) });
33
- }
34
- Object.assign(room, changes, { updatedAt: next.updatedAt });
26
+ export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlMs = 120000,
27
+ maxConnections = 128, maxPayload = 8 * 1024 * 1024, stateFile,
28
+ maxRelaySessions = 8, adminStateFile, mysqlConfigFile, accountConfigFile, usageDiagnosticKeyFile,
29
+ publicRelayUrl = '', appDownloadUrl = '', debugAllowlistFile, longPairingFile,
30
+ androidReleaseConfigFile, androidReleaseCredentialFile, log = () => {} } = {}) {
31
+ const androidReleases = createAndroidReleaseHandler({
32
+ ...(androidReleaseConfigFile ? androidReleaseStorage(androidReleaseConfigFile, androidReleaseCredentialFile) : {}), log });
33
+ const longInvitations = longPairing(longPairingFile);
34
+ longInvitations.list();
35
+ function longInvitation(code) {
36
+ try { return longInvitations.find(code); }
37
+ catch (error) { log({ event: 'long-pairing-error', message: error.message }); return undefined; }
38
+ }
39
+ if (!validSecret(hostKey)) throw new Error('Invalid host credential');
40
+ if (!Number.isSafeInteger(maxRelaySessions) || maxRelaySessions < 1 || maxRelaySessions > 256) throw new Error('Invalid relay session limit');
41
+ if (publicRelayUrl) relayPublicAddress(publicRelayUrl);
42
+ // Harmony's native WebSocket supplies the endpoint Origin automatically.
43
+ // libwebsockets can use http even for WSS; trust only the configured endpoint.
44
+ const nativeOrigins = publicRelayUrl ? ['http:', 'https:'].map(protocol => `${protocol}//${new URL(publicRelayUrl).host}`) : [];
45
+ if (appDownloadUrl && !/^https:\/\/[^\s]+$/.test(appDownloadUrl)) throw new Error('Invalid app download URL');
46
+ const hostHash = digest(hostKey), rooms = new Map();
47
+ const database = mysqlConfigFile ? await openRelayMysql(mysqlConfigFile, maxRelaySessions) : null;
48
+ if (accountConfigFile && !database?.schemaVersions.includes('003_relay_usage')) throw new Error('Relay account and usage schema is required');
49
+ const accounts = accountConfigFile ? openEmailAccounts(database.pool, accountConfigFile) : null;
50
+ const accountSockets = accountConnections();
51
+ const meter = accounts ? usageMeter(database.pool) : null;
52
+ const cancelledAccountIds = new Set();
53
+ const accountCancellationWork = new Map();
54
+ const diagnostics = database?.schemaVersions.includes('006_relay_diagnostics')
55
+ ? relayDiagnostics(database.pool, error => log({ event: 'relay-diagnostic-error', message: error.message })) : null;
56
+ const adminAccessFile = '/etc/codex-relay/admin-access.json';
57
+ const adminAccessKey = fs.existsSync(adminAccessFile) ? JSON.parse(fs.readFileSync(adminAccessFile, 'utf8')).key : '';
58
+ if (adminAccessKey && !validSecret(adminAccessKey)) throw new Error('Invalid admin access key');
59
+ const diagnosticKey = usageDiagnosticKeyFile ? fs.readFileSync(usageDiagnosticKeyFile, 'utf8').trim() : '';
60
+ if (diagnosticKey && !/^[A-Za-z0-9_-]{43}$/.test(diagnosticKey)) throw new Error('Invalid usage diagnostic key');
61
+ const store = database?.devices || deviceStore(stateFile);
62
+ const phoneEvents = phoneConnectionEvents(database?.pool);
63
+ const debugAllowlist = readDebugAllowlist(debugAllowlistFile);
64
+ const adminState = database?.admin || relayAdminState(adminStateFile, maxRelaySessions);
65
+ let activeRelaySessions = 0;
66
+ for (const record of store.entries()) rooms.set(record.id, { ...record, durable: true, connections: new Map(), observers: new Map(), uploadClients: new Map() });
67
+ let commitTail = Promise.resolve();
68
+ function queueStore(operation) {
69
+ const pending = commitTail.then(operation);
70
+ commitTail = pending.catch(() => {});
71
+ return pending;
72
+ }
73
+ function commit(room, changes) {
74
+ return queueStore(async () => {
75
+ changes = typeof changes === 'function' ? changes(room) : changes;
76
+ const next = { ...room, ...changes, updatedAt: new Date().toISOString() };
77
+ if (room.durable) {
78
+ const { id, hostHash, clients, revision, status, createdAt, updatedAt, officialComputer } = next;
79
+ await store.save({ id, hostHash, clients, revision, status, createdAt, updatedAt, ...(officialComputer ? { officialComputer } : {}) });
80
+ }
81
+ Object.assign(room, changes, { updatedAt: next.updatedAt });
82
+ });
83
+ }
84
+ function registerRoom(id, durable, credential) {
85
+ return queueStore(async () => {
86
+ // Resolve concurrent first connections before writing the device identity.
87
+ if (durable && rooms.has(id)) return rooms.get(id);
88
+ const now = new Date().toISOString();
89
+ const record = { id, revision: 0, status: 'active', clients: [], createdAt: now, updatedAt: now,
90
+ ...(durable ? { hostHash: digest(credential) } : {}) };
91
+ if (durable) await store.save(record);
92
+ const room = { ...record, durable, connections: new Map(), observers: new Map(), uploadClients: new Map() };
93
+ rooms.set(id, room);
94
+ return room;
95
+ });
96
+ }
97
+ function recordConnection(room, clientId, at, info) {
98
+ return queueStore(async () => {
99
+ const client = room.clients.find(item => item.id === clientId);
100
+ if (!client) return false;
101
+ const updated = room.durable ? await store.recordConnection(room.id, clientId, at, info) : connectionInfo(client, at, info);
102
+ if (!updated) return false;
103
+ room.clients = room.clients.map(item => item.id === clientId ? updated : item);
104
+ return true;
105
+ });
106
+ }
107
+ async function manageDevices(room, ws, message, currentClientId) {
108
+ try {
109
+ if (!validId(message.commandId)) throw new Error('Invalid device management request');
110
+ if (message.type === 'devices-list') {
111
+ await commitTail;
112
+ if (currentClientId && !room.clients.some(item => item.id === currentClientId)) throw new Error('配对授权已撤销');
113
+ const clients = room.clients.map(({ id, createdAt, lastConnectedAt, platform, deviceName }) => ({
114
+ id, createdAt, lastConnectedAt: lastConnectedAt || null, platform: platform || null, deviceName: deviceName || null,
115
+ current: id === currentClientId,
116
+ })).sort((a, b) => (b.lastConnectedAt || b.createdAt).localeCompare(a.lastConnectedAt || a.createdAt));
117
+ send(ws, { type: message.type, commandId: message.commandId, clients, workstationOnline: Boolean(room.host) });
118
+ return;
119
+ }
120
+ if (!validId(message.id) || message.confirmed !== true) throw new Error('请确认要撤销的配对授权');
121
+ await commit(room, current => {
122
+ if (currentClientId && !current.clients.some(item => item.id === currentClientId)) throw new Error('配对授权已撤销');
123
+ return { clients: current.clients.filter(item => item.id !== message.id), revision: current.revision + 1 };
124
+ });
125
+ if (room.host) send(room.host, { type: 'authorization-changed', clients: room.clients, revision: room.revision });
126
+ // Acknowledge self-revocation before closing this connection.
127
+ send(ws, { type: message.type, commandId: message.commandId, id: message.id, revoked: true, workstationOnline: Boolean(room.host) });
128
+ const connection = room.connections.get(message.id);
129
+ if (connection) {
130
+ releaseRelay(connection);
131
+ if (room.host) send(room.host, { type: 'detach', session: connection.session });
132
+ room.connections.delete(message.id);
133
+ connection.ws.close(4401, 'Device authorization revoked');
134
+ }
135
+ const observer = room.observers?.get(message.id);
136
+ room.observers?.delete(message.id);
137
+ observer?.ws.close(4401, 'Device authorization revoked');
138
+ for (const [key, upload] of room.uploadClients || []) if (upload.clientId === message.id) {
139
+ room.uploadClients.delete(key);
140
+ if (room.uploadHost) {
141
+ discardFileSession(room.uploadHost, upload.session);
142
+ send(room.uploadHost, { type: 'file-close', session: upload.session });
143
+ }
144
+ upload.ws.close(4401, 'Device authorization revoked');
145
+ }
146
+ for (const [socket, id] of room.deviceManagers || []) if (id === message.id) {
147
+ room.deviceManagers.delete(socket); socket.close(4401, 'Device authorization revoked');
148
+ }
149
+ } catch (error) { send(ws, { type: message.type, commandId: message.commandId, error: error.message }); }
35
150
  }
36
151
  function invitation(room) {
37
152
  const code = secret(); room.pairingHash = digest(code); room.expires = Date.now() + pairingTtlMs;
38
153
  return { code, expires: room.expires };
39
154
  }
40
- let listenUrl = '';
41
- function relayUrl(request) {
42
- if (publicRelayUrl) return relayPublicAddress(publicRelayUrl);
43
- const host = request.headers.host || new URL(listenUrl).host;
44
- const secure = request.socket.encrypted || request.headers['x-forwarded-proto'] === 'https';
45
- return `${secure ? 'wss' : 'ws'}://${host}/relay`;
46
- }
47
- function findInvitation(code) {
48
- if (!validSecret(code)) return undefined;
49
- return [...rooms.values()].find(room => room.status === 'active' && room.pairingHash && Date.now() < room.expires && matches(code, room.pairingHash));
50
- }
51
- const server = http.createServer((request, response) => {
52
- const target = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
53
- if (target.pathname === '/admin' || target.pathname.startsWith('/admin/')) {
54
- void handleAdmin(request, response, target); return;
55
- }
56
- if (request.method === 'GET' && target.pathname === '/announcement') {
57
- const body = JSON.stringify({ announcement: adminState.read().announcement });
58
- response.writeHead(200, pairingHeaders('application/json; charset=utf-8', body)); response.end(body); return;
59
- }
60
- if (request.method !== 'GET' || !['/pair', '/pair/resolve'].includes(target.pathname)) {
61
- response.writeHead(404, { 'Cache-Control': 'no-store' }); response.end(); return;
62
- }
63
- const code = target.searchParams.get('invite') || '';
64
- const room = findInvitation(code);
65
- if (!room) {
66
- const json = target.pathname === '/pair/resolve' || /application\/json/i.test(request.headers.accept || '');
67
- const body = json ? JSON.stringify({ error: '配对邀请已过期或已使用' }) : pairingPage({ expired: true });
68
- response.writeHead(410, pairingHeaders(json ? 'application/json; charset=utf-8' : 'text/html; charset=utf-8', body)); response.end(body); return;
69
- }
70
- if (target.pathname === '/pair/resolve' || /application\/json/i.test(request.headers.accept || '')) {
71
- const body = JSON.stringify({ version: 6, relayUrl: relayUrl(request), room: room.id, expires: room.expires });
72
- response.writeHead(200, pairingHeaders('application/json; charset=utf-8', body)); response.end(body); return;
73
- }
74
- const secure = request.socket.encrypted || request.headers['x-forwarded-proto'] === 'https';
75
- const current = new URL(target.pathname + target.search, `${secure ? 'https' : 'http'}://${request.headers.host || new URL(listenUrl).host}`);
76
- const body = pairingPage({ deepLink: `codex-remote://pair?url=${encodeURIComponent(current.href)}`, expires: room.expires, appDownloadUrl });
77
- response.writeHead(200, pairingHeaders('text/html; charset=utf-8', body)); response.end(body);
78
- });
79
- function adminReply(response, status, body, type = 'application/json; charset=utf-8') {
80
- const value = typeof body === 'string' ? body : JSON.stringify(body);
81
- response.writeHead(status, { 'Content-Type': type, 'Content-Length': Buffer.byteLength(value), 'Cache-Control': 'no-store',
82
- 'X-Content-Type-Options': 'nosniff', 'Content-Security-Policy': "default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'" });
83
- response.end(value);
84
- }
85
- async function handleAdmin(request, response, target) {
86
- if (request.method === 'GET' && target.pathname === '/admin') {
87
- adminReply(response, 200, adminPage(), 'text/html; charset=utf-8'); return;
88
- }
89
- if (request.method === 'GET' && target.pathname === '/admin/api/status') {
90
- const state = adminState.read();
91
- adminReply(response, 200, { maxRelaySessions: state.maxRelaySessions, activeRelaySessions,
92
- activeExemptRelaySessions: [...rooms.values()].reduce((sum, room) => sum + [...room.connections.values()].filter(connection => connection.relayExempt).length, 0),
93
- websocketConnections: wss.clients.size, onlineComputers: [...rooms.values()].filter(room => room.host).length,
94
- activePhones: [...rooms.values()].reduce((sum, room) => sum + room.connections.size, 0), announcement: state.announcement }); return;
95
- }
96
- if (!['PUT', 'DELETE'].includes(request.method || '') || request.headers['content-type'] !== 'application/json' ||
97
- request.headers['x-code-relax-admin'] !== '1') { adminReply(response, 405, { error: 'Unsupported admin request' }); return; }
98
- try {
99
- const body = request.method === 'DELETE' ? {} : JSON.parse(await readBody(request, 4096));
100
- if (target.pathname === '/admin/api/settings' && request.method === 'PUT') {
101
- const state = adminState.setMaxRelaySessions(body.maxRelaySessions);
102
- log({ event: 'admin-relay-limit-updated', maxRelaySessions: state.maxRelaySessions });
103
- } else if (target.pathname === '/admin/api/announcement' && request.method === 'PUT') {
104
- const state = adminState.publish(body.title, body.message); broadcastAnnouncement(state.announcement);
105
- log({ event: 'admin-announcement-published', announcementId: state.announcement.id });
106
- } else if (target.pathname === '/admin/api/announcement' && request.method === 'DELETE') {
107
- const state = adminState.withdraw(); broadcastAnnouncement(state.announcement);
108
- log({ event: 'admin-announcement-withdrawn' });
109
- } else { adminReply(response, 404, { error: 'Admin route not found' }); return; }
110
- adminReply(response, 200, { ok: true });
111
- } catch (error) { adminReply(response, 400, { error: error.message }); }
112
- }
113
- function broadcastAnnouncement(announcement) {
114
- for (const room of rooms.values()) {
115
- for (const observer of room.observers.values()) send(observer.ws, { type: 'announcement', announcement });
116
- for (const connection of room.connections.values()) send(connection.ws, { type: 'announcement', announcement });
117
- }
118
- }
119
- function releaseRelay(connection) {
120
- if (!connection?.relayAdmitted) return;
121
- connection.relayAdmitted = false; activeRelaySessions = Math.max(0, activeRelaySessions - 1);
122
- }
123
- function admitRelay(connection, room) {
124
- if (connection.relayAdmitted || connection.relayExempt) return true;
125
- if (connection.capacityExempt) {
126
- connection.relayExempt = true;
127
- log({ event: 'relay-capacity-exempt', device: room.id }); return true;
128
- }
129
- const limit = adminState.read().maxRelaySessions;
130
- if (activeRelaySessions >= limit) {
131
- log({ event: 'relay-capacity-rejected', device: room.id, activeRelaySessions, maxRelaySessions: limit }); return false;
132
- }
133
- connection.relayAdmitted = true; activeRelaySessions++;
134
- log({ event: 'relay-capacity-admitted', device: room.id, activeRelaySessions, maxRelaySessions: limit }); return true;
135
- }
136
- const wss = new WebSocketServer({ noServer: true, maxPayload, perMessageDeflate: {
137
- serverNoContextTakeover: true, clientNoContextTakeover: true,
138
- threshold: 1024, concurrencyLimit: 4, zlibDeflateOptions: { level: 3 },
139
- } });
155
+ let listenUrl = '';
156
+ let adminTail = Promise.resolve();
157
+ function relayUrl(request) {
158
+ if (publicRelayUrl) return relayPublicAddress(publicRelayUrl);
159
+ const host = request.headers.host || new URL(listenUrl).host;
160
+ const secure = request.socket.encrypted || request.headers['x-forwarded-proto'] === 'https';
161
+ return `${secure ? 'wss' : 'ws'}://${host}/relay`;
162
+ }
163
+ function findInvitation(code) {
164
+ if (!validSecret(code)) return undefined;
165
+ const room = [...rooms.values()].find(room => room.status === 'active' && room.pairingHash && Date.now() < room.expires && matches(code, room.pairingHash));
166
+ if (room) return { room, expires: room.expires };
167
+ const invite = longInvitation(code), target = invite && rooms.get(invite.room);
168
+ return target?.status === 'active' ? { room: target, expires: invite.expires } : undefined;
169
+ }
170
+ const server = http.createServer((request, response) => {
171
+ const target = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
172
+ if (target.pathname.startsWith('/code-relax/android/')) {
173
+ void androidReleases(request, response, target); return;
174
+ }
175
+ if (target.pathname === '/admin' || target.pathname.startsWith('/admin/')) {
176
+ adminTail = adminTail.then(() => handleAdmin(request, response, target)).catch(() => {
177
+ if (!response.headersSent) adminReply(response, 500, { error: 'Admin operation failed' });
178
+ });
179
+ return;
180
+ }
181
+ if (target.pathname === '/account/privacy' && request.method === 'GET') {
182
+ response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache',
183
+ 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
184
+ 'X-Content-Type-Options': 'nosniff' });
185
+ response.end(privacyPage); return;
186
+ }
187
+ if (accounts && target.pathname.startsWith('/account/')) {
188
+ void handleAccount(request, response, target).catch(() => accountReply(response, 500, { error: 'Account operation failed' }));
189
+ return;
190
+ }
191
+ if (request.method === 'GET' && target.pathname === '/announcement') {
192
+ const body = JSON.stringify({ announcement: adminState.read().announcement });
193
+ response.writeHead(200, pairingHeaders('application/json; charset=utf-8', body)); response.end(body); return;
194
+ }
195
+ if (request.method !== 'GET' || !['/pair', '/pair/resolve'].includes(target.pathname)) {
196
+ response.writeHead(404, { 'Cache-Control': 'no-store' }); response.end(); return;
197
+ }
198
+ const code = target.searchParams.get('invite') || '';
199
+ const invitation = findInvitation(code);
200
+ if (!invitation) {
201
+ const json = target.pathname === '/pair/resolve' || /application\/json/i.test(request.headers.accept || '');
202
+ const body = json ? JSON.stringify({ error: '配对邀请已过期或已使用' }) : pairingPage({ expired: true });
203
+ response.writeHead(410, pairingHeaders(json ? 'application/json; charset=utf-8' : 'text/html; charset=utf-8', body)); response.end(body); return;
204
+ }
205
+ if (target.pathname === '/pair/resolve' || /application\/json/i.test(request.headers.accept || '')) {
206
+ const body = JSON.stringify({ version: 6, relayUrl: relayUrl(request), room: invitation.room.id, expires: invitation.expires });
207
+ response.writeHead(200, pairingHeaders('application/json; charset=utf-8', body)); response.end(body); return;
208
+ }
209
+ const secure = request.socket.encrypted || request.headers['x-forwarded-proto'] === 'https';
210
+ const current = new URL(target.pathname + target.search, `${secure ? 'https' : 'http'}://${request.headers.host || new URL(listenUrl).host}`);
211
+ const body = pairingPage({ deepLink: `codex-remote://pair?url=${encodeURIComponent(current.href)}`, expires: invitation.expires, appDownloadUrl });
212
+ response.writeHead(200, pairingHeaders('text/html; charset=utf-8', body)); response.end(body);
213
+ });
214
+ function adminReply(response, status, body, type = 'application/json; charset=utf-8') {
215
+ const value = typeof body === 'string' ? body : JSON.stringify(body);
216
+ response.writeHead(status, { 'Content-Type': type, 'Content-Length': Buffer.byteLength(value), 'Cache-Control': 'no-store',
217
+ 'X-Content-Type-Options': 'nosniff', 'Content-Security-Policy': "default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'" });
218
+ response.end(value);
219
+ }
220
+ function accountReply(response, status, body) {
221
+ if (response.headersSent || response.destroyed) return;
222
+ const value = JSON.stringify(body);
223
+ response.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(value),
224
+ 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' });
225
+ response.end(value);
226
+ }
227
+ function cancelAccountConnections(userId, token) {
228
+ const pending = accountCancellationWork.get(userId);
229
+ if (pending) return pending;
230
+ if (cancelledAccountIds.has(userId)) return Promise.resolve();
231
+ cancelledAccountIds.add(userId);
232
+ const work = (async () => {
233
+ try { await accounts.cancelAccount(token); }
234
+ catch (failure) { cancelledAccountIds.delete(userId); throw failure; }
235
+ for (const room of rooms.values()) {
236
+ for (const [key, arm] of room.usageArms || []) if (arm.userId === userId) room.usageArms.delete(key);
237
+ for (const [key, task] of room.usageTasks || []) if (task.userId === userId) room.usageTasks.delete(key);
238
+ for (const [clientId, connection] of room.connections) {
239
+ if (connection.userId !== userId) continue;
240
+ releaseRelay(connection); room.connections.delete(clientId);
241
+ if (room.host) send(room.host, { type: 'detach', session: connection.session });
242
+ connection.ws.close(4431, 'Account cancelled');
243
+ }
244
+ for (const [clientId, observer] of room.observers) {
245
+ if (observer.userId !== userId) continue;
246
+ room.observers.delete(clientId); observer.ws.close(4431, 'Account cancelled');
247
+ }
248
+ for (const [clientId, upload] of room.uploadClients) {
249
+ if (upload.userId !== userId) continue;
250
+ room.uploadClients.delete(clientId);
251
+ if (room.uploadHost) {
252
+ discardFileSession(room.uploadHost, upload.session);
253
+ send(room.uploadHost, { type: 'file-close', session: upload.session });
254
+ }
255
+ upload.ws.close(4431, 'Account cancelled');
256
+ }
257
+ }
258
+ })().finally(() => accountCancellationWork.delete(userId));
259
+ accountCancellationWork.set(userId, work);
260
+ return work;
261
+ }
262
+ async function handleAccount(request, response, target) {
263
+ try {
264
+ if (target.pathname === '/account/config' && request.method === 'GET') {
265
+ accountReply(response, 200, { captchaAppId: accounts.captchaAppId }); return;
266
+ }
267
+ const bearer = /^Bearer ([A-Za-z0-9_-]{43})$/.exec(request.headers.authorization || '')?.[1];
268
+ if (target.pathname === '/account/me' && request.method === 'GET') {
269
+ const user = await accounts.current(bearer);
270
+ accountReply(response, 200, { user, usage: await accounts.usage(user.id) }); return;
271
+ }
272
+ if (target.pathname === '/account/logout' && request.method === 'POST') {
273
+ const result = await accounts.logout(bearer);
274
+ accountSockets.revoke(bearer);
275
+ accountReply(response, 200, result); return;
276
+ }
277
+ if (target.pathname === '/account/me' && request.method === 'DELETE') {
278
+ const user = await accounts.current(bearer);
279
+ await cancelAccountConnections(user.id, bearer);
280
+ accountReply(response, 200, { ok: true }); return;
281
+ }
282
+ if (request.method !== 'POST' || request.headers['content-type'] !== 'application/json') {
283
+ accountReply(response, 404, { error: 'Account route not found' }); return;
284
+ }
285
+ const body = JSON.parse(await readBody(request, 4096));
286
+ if (!body || typeof body !== 'object' || Array.isArray(body)) { accountReply(response, 400, { error: 'Invalid request body' }); return; }
287
+ if (target.pathname === '/account/email/request') {
288
+ const ip = request.headers['x-real-ip'] || request.socket.remoteAddress || '';
289
+ if (typeof ip !== 'string' || !isIP(ip)) { accountReply(response, 400, { error: 'Invalid client address' }); return; }
290
+ accountReply(response, 202, await accounts.requestCode(body, ip));
291
+ } else if (target.pathname === '/account/email/verify') {
292
+ accountReply(response, 200, await accounts.verifyCode(body));
293
+ } else accountReply(response, 404, { error: 'Account route not found' });
294
+ } catch (failure) {
295
+ accountReply(response, failure.status || 503, { error: failure.status ? failure.message : 'Account service unavailable' });
296
+ }
297
+ }
298
+ async function handleAdmin(request, response, target) {
299
+ const accessKey = request.headers['x-code-relax-admin-key'];
300
+ const internalAccess = adminAccessKey && validSecret(accessKey) &&
301
+ timingSafeEqual(Buffer.from(accessKey), Buffer.from(adminAccessKey));
302
+ const token = /^Bearer ([A-Za-z0-9_-]{43})$/.exec(request.headers.authorization || '')?.[1];
303
+ const diagnosticRoute = ['/admin/api/usage-diagnostics', '/admin/api/usage-diagnostics/drain'].includes(target.pathname);
304
+ const diagnosticAccess = diagnosticRoute && diagnosticKey && token &&
305
+ timingSafeEqual(Buffer.from(token), Buffer.from(diagnosticKey));
306
+ if (!internalAccess && !diagnosticAccess) { adminReply(response, 404, { error: 'Not found' }); return; }
307
+ if (request.method === 'GET' && target.pathname === '/admin/api/workstations') {
308
+ const workstations = await resolveWorkstationAccounts(listWorkstations(rooms, { details: true }), database?.pool);
309
+ await resolveWorkstationConnections(workstations, phoneEvents);
310
+ adminReply(response, 200, { sampledAt: new Date().toISOString(), workstations: workstations.map(workstationSummary) }); return;
311
+ }
312
+ if (request.method === 'GET' && target.pathname.startsWith('/admin/api/workstations/')) {
313
+ const id = target.pathname.slice('/admin/api/workstations/'.length);
314
+ const room = validId(id) && rooms.get(id);
315
+ if (!room) { adminReply(response, 404, { error: '未找到工作站' }); return; }
316
+ const [workstation] = await resolveWorkstationAccounts([
317
+ workstationDetail(room, debugAllowlist.devices.some(entry => entry.deviceId === room.id))], database?.pool);
318
+ await resolveWorkstationConnections([workstation], phoneEvents);
319
+ workstation.connectionEvents = await phoneEvents.list(room.id);
320
+ adminReply(response, 200, { sampledAt: new Date().toISOString(), workstation }); return;
321
+ }
322
+ if (request.method === 'GET' && target.pathname === '/admin/api/status') {
323
+ const state = adminState.read();
324
+ adminReply(response, 200, { maxRelaySessions: state.maxRelaySessions, activeRelaySessions,
325
+ activeExemptRelaySessions: [...rooms.values()].reduce((sum, room) => sum + [...room.connections.values()].filter(connection => connection.relayExempt).length, 0),
326
+ websocketConnections: wss.clients.size, onlineComputers: [...rooms.values()].filter(room => room.host).length,
327
+ activePhones: [...rooms.values()].reduce((sum, room) => sum + room.connections.size, 0),
328
+ processRssBytes: process.memoryUsage().rss, processUptimeSeconds: process.uptime(),
329
+ announcement: state.announcement, announcements: state.announcements }); return;
330
+ }
331
+ if (request.method === 'GET' && target.pathname === '/admin/api/usage' && meter) {
332
+ const page = Number(target.searchParams.get('page') || '1');
333
+ const view = target.searchParams.get('view') || 'user';
334
+ if (!Number.isSafeInteger(page) || page < 1 || page > 100000 ||
335
+ !['user', 'computer'].includes(view) || target.searchParams.has('user') ||
336
+ target.searchParams.has('computer')) {
337
+ adminReply(response, 400, { error: 'Invalid usage query' }); return;
338
+ }
339
+ adminReply(response, 200, await usageReport(database.pool, page, view)); return;
340
+ }
341
+ const diagnosticSource = target.searchParams.get('source') || 'usage';
342
+ if (target.pathname.startsWith('/admin/api/usage-diagnostics') && !['usage', 'account-cleanup'].includes(diagnosticSource)) {
343
+ adminReply(response, 400, { error: 'Invalid diagnostic source' }); return;
344
+ }
345
+ if (diagnostics && target.pathname === '/admin/api/usage-diagnostics') {
346
+ if (request.method === 'GET') adminReply(response, 200, await diagnostics.read(diagnosticSource));
347
+ else if (request.method === 'DELETE') adminReply(response, 200, await diagnostics.clear(diagnosticSource));
348
+ else adminReply(response, 405, { error: 'Unsupported diagnostic request' });
349
+ return;
350
+ }
351
+ if (diagnostics && target.pathname === '/admin/api/usage-diagnostics/drain') {
352
+ if (request.method === 'POST') adminReply(response, 200, await diagnostics.drain(diagnosticSource));
353
+ else adminReply(response, 405, { error: 'Unsupported diagnostic request' });
354
+ return;
355
+ }
356
+ if (!['PUT', 'DELETE'].includes(request.method || '') || request.headers['content-type'] !== 'application/json' ||
357
+ request.headers['x-code-relax-admin'] !== '1') { adminReply(response, 405, { error: 'Unsupported admin request' }); return; }
358
+ try {
359
+ const body = request.method === 'DELETE' ? {} : JSON.parse(await readBody(request, MAX_ADMIN_REQUEST_BYTES));
360
+ if (target.pathname === '/admin/api/settings' && request.method === 'PUT') {
361
+ const state = await adminState.setMaxRelaySessions(body.maxRelaySessions);
362
+ log({ event: 'admin-relay-limit-updated', maxRelaySessions: state.maxRelaySessions });
363
+ } else if (target.pathname === '/admin/api/announcements' && request.method === 'PUT') {
364
+ const state = await adminState.createAnnouncement(body.title, body.message, body.publish === true);
365
+ if (body.publish === true) {
366
+ broadcastAnnouncement(state.announcement);
367
+ log({ event: 'admin-announcement-published', announcementId: state.announcement.id });
368
+ } else log({ event: 'admin-announcement-draft-created', announcementId: state.announcements[0].id });
369
+ } else if (target.pathname.startsWith('/admin/api/announcements/')) {
370
+ const parts = target.pathname.slice('/admin/api/announcements/'.length).split('/');
371
+ const id = parts[0];
372
+ if (!/^[\w-]{16,128}$/.test(id)) { adminReply(response, 404, { error: 'Admin route not found' }); return; }
373
+ if (parts.length === 1 && request.method === 'PUT') {
374
+ await adminState.updateDraft(id, body.title, body.message);
375
+ log({ event: 'admin-announcement-draft-updated', announcementId: id });
376
+ } else if (parts.length === 1 && request.method === 'DELETE') {
377
+ await adminState.deleteAnnouncement(id);
378
+ log({ event: 'admin-announcement-deleted', announcementId: id });
379
+ } else if (parts.length === 2 && parts[1] === 'publish' && request.method === 'PUT') {
380
+ const state = await adminState.publishDraft(id, body.title, body.message);
381
+ broadcastAnnouncement(state.announcement);
382
+ log({ event: 'admin-announcement-published', announcementId: id });
383
+ } else if (parts.length === 2 && parts[1] === 'withdraw' && request.method === 'PUT') {
384
+ const state = await adminState.withdraw(id);
385
+ broadcastAnnouncement(state.announcement);
386
+ log({ event: 'admin-announcement-withdrawn', announcementId: id });
387
+ } else { adminReply(response, 404, { error: 'Admin route not found' }); return; }
388
+ } else { adminReply(response, 404, { error: 'Admin route not found' }); return; }
389
+ adminReply(response, 200, { ok: true });
390
+ } catch (error) { adminReply(response, 400, { error: error.message }); }
391
+ }
392
+ function broadcastAnnouncement(announcement) {
393
+ for (const room of rooms.values()) {
394
+ for (const observer of room.observers.values()) send(observer.ws, { type: 'announcement', announcement });
395
+ for (const connection of room.connections.values()) send(connection.ws, { type: 'announcement', announcement });
396
+ }
397
+ }
398
+ function releaseRelay(connection) {
399
+ if (!connection?.relayAdmitted) return;
400
+ connection.relayAdmitted = false; activeRelaySessions = Math.max(0, activeRelaySessions - 1);
401
+ }
402
+ function admitRelay(connection, room) {
403
+ if (connection.relayAdmitted || connection.relayExempt) return true;
404
+ if (connection.capacityExempt) {
405
+ connection.relayExempt = true;
406
+ log({ event: 'relay-capacity-exempt', device: room.id }); return true;
407
+ }
408
+ const limit = adminState.read().maxRelaySessions;
409
+ if (activeRelaySessions >= limit) {
410
+ log({ event: 'relay-capacity-rejected', device: room.id, activeRelaySessions, maxRelaySessions: limit }); return false;
411
+ }
412
+ connection.relayAdmitted = true; activeRelaySessions++;
413
+ log({ event: 'relay-capacity-admitted', device: room.id, activeRelaySessions, maxRelaySessions: limit }); return true;
414
+ }
415
+ const wss = new WebSocketServer({ noServer: true, maxPayload, perMessageDeflate: {
416
+ serverNoContextTakeover: true, clientNoContextTakeover: true,
417
+ threshold: 1024, concurrencyLimit: 4, zlibDeflateOptions: { level: 3 },
418
+ } });
140
419
  let admissionStart = Date.now(), admissions = 0;
141
420
  server.on('upgrade', (request, socket, head) => {
142
- if (request.url !== '/relay' || (request.headers.origin && request.headers.origin !== 'null' && !nativeOrigins.includes(request.headers.origin))) {
143
- log({ event: 'upgrade-rejected', reason: request.url !== '/relay' ? 'path' : 'origin' });
421
+ if (request.url !== '/relay' || (request.headers.origin && request.headers.origin !== 'null' && !nativeOrigins.includes(request.headers.origin))) {
422
+ log({ event: 'upgrade-rejected', reason: request.url !== '/relay' ? 'path' : 'origin' });
144
423
  socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n'); return;
145
424
  }
146
425
  if (Date.now() - admissionStart >= 60000) { admissionStart = Date.now(); admissions = 0; }
147
- if (wss.clients.size >= maxConnections || ++admissions > 60) {
426
+ if (wss.clients.size >= maxConnections || ++admissions > 240) {
148
427
  socket.end('HTTP/1.1 429 Too Many Requests\r\nConnection: close\r\n\r\n'); return;
149
428
  }
150
- wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws));
429
+ wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws, request));
151
430
  });
152
- wss.on('connection', ws => {
153
- let role, room, session, clientId, alive = true;
431
+ wss.on('connection', (ws, request) => {
432
+ const address = request.headers['x-real-ip'];
433
+ ws.adminInfo = { connectedAt: new Date().toISOString(),
434
+ address: typeof address === 'string' && isIP(address) ? address : request.socket.remoteAddress };
435
+ let role, room, session, clientId, alive = true;
436
+ const phoneEventIds = new Set();
437
+ let accountConnection;
438
+ function invalidateAccountConnection() {
439
+ if (role === 'client' && room?.connections.get(clientId)?.ws === ws) {
440
+ releaseRelay(room.connections.get(clientId));
441
+ room.connections.delete(clientId);
442
+ // Detach also closes the workstation RTC peer; closing only the
443
+ // phone's signaling socket would leave the direct data path alive.
444
+ if (room.host) send(room.host, { type: 'detach', session });
445
+ } else if (role === 'file-client' && room?.uploadClients.get(session)?.ws === ws) {
446
+ room.uploadClients.delete(session);
447
+ if (room.uploadHost) {
448
+ discardFileSession(room.uploadHost, session);
449
+ send(room.uploadHost, { type: 'file-close', session });
450
+ }
451
+ }
452
+ ws.close(4431, 'Account session invalid');
453
+ }
154
454
  const authTimer = setTimeout(() => ws.close(1008, 'Authentication timeout'), 5000);
155
455
  ws.on('pong', () => { alive = true; });
156
- const heartbeat = setInterval(() => { if (!alive) ws.terminate(); else { alive = false; ws.ping(); } }, 30000);
157
- const fail = (code = 4401, reason = 'Device authorization rejected') => ws.close(code, reason);
158
- const queueClientMessage = (connection, message, pause = 0) => {
159
- const bytes = Buffer.byteLength(JSON.stringify(message));
160
- connection.queuedBytes = (connection.queuedBytes || 0) + bytes;
161
- if (connection.queuedBytes > 40 * 1024 * 1024) {
162
- connection.ws.close(1013, 'Receiver too slow'); return;
163
- }
164
- connection.outbound = (connection.outbound || Promise.resolve()).then(async () => {
165
- if (pause > 0) await new Promise(resolve => setTimeout(resolve, pause));
166
- connection.queuedBytes -= bytes;
167
- send(connection.ws, message);
168
- }).catch(() => connection.ws.close(1011, 'Relay delivery failed'));
169
- };
456
+ const heartbeat = setInterval(() => {
457
+ sampleInboundQueue();
458
+ if (!alive) ws.terminate(); else { alive = false; ws.ping(); }
459
+ }, 30000);
460
+ const fail = (code = 4401, reason = 'Device authorization rejected') => ws.close(code, reason);
461
+ const queueClientMessage = (connection, message, pause = 0) => {
462
+ const bytes = Buffer.byteLength(JSON.stringify(message));
463
+ connection.queuedBytes = (connection.queuedBytes || 0) + bytes;
464
+ if (connection.queuedBytes > 40 * 1024 * 1024) {
465
+ connection.ws.close(1013, 'Receiver too slow'); return;
466
+ }
467
+ connection.outbound = (connection.outbound || Promise.resolve()).then(async () => {
468
+ if (pause > 0) await new Promise(resolve => setTimeout(resolve, pause));
469
+ connection.queuedBytes -= bytes;
470
+ send(connection.ws, message);
471
+ }).catch(() => connection.ws.close(1011, 'Relay delivery failed'));
472
+ };
170
473
  ws.on('error', () => {}); // close owns routing cleanup.
171
474
  ws.on('close', (code, reason) => {
172
475
  log({ event: 'closed', role: role || 'unauthenticated', device: room?.id, code, reason: reason.toString() });
173
476
  clearTimeout(authTimer); clearInterval(heartbeat);
174
- if (role === 'host' && room?.host === ws) {
175
- room.host = undefined; room.hostName = undefined; room.lanCandidates = [];
176
- for (const observer of room.observers.values()) send(observer.ws, { type: 'observing', online: false });
177
- if (!room.durable) rooms.delete(room.id);
178
- for (const connection of room.connections.values()) connection.ws.close(4410, 'Computer offline; authorization retained');
179
- } else if (role === 'client' && room?.connections.get(clientId)?.ws === ws) {
180
- releaseRelay(room.connections.get(clientId));
181
- room.connections.delete(clientId);
182
- if (room.host) send(room.host, { type: 'detach', session });
183
- } else if (role === 'observer' && room?.observers.get(clientId)?.ws === ws) {
184
- room.observers.delete(clientId);
477
+ accountConnection?.dispose();
478
+ if (role === 'host' && room?.host === ws) {
479
+ for (const task of room.usageTasks?.values() || []) {
480
+ void meter?.lost(task).catch(error => log({ event: 'usage-error', message: error.message }));
481
+ }
482
+ room.usageTasks?.clear(); room.usageArms?.clear();
483
+ room.host = undefined; room.hostName = undefined; room.lanCandidates = []; room.health = null;
484
+ room.uploadHost?.close(4410, 'Computer offline');
485
+ for (const client of room.uploadClients?.values() || []) client.ws.close(code === 4432 ? 4432 : 4410, code === 4432 ? 'Computer inbound queue limit exceeded' : 'Computer offline');
486
+ for (const observer of room.observers.values()) send(observer.ws, { type: 'observing', online: false });
487
+ if (!room.durable) rooms.delete(room.id);
488
+ for (const connection of room.connections.values()) connection.ws.close(
489
+ code === 4432 ? 4432 : 4410,
490
+ code === 4432 ? 'Computer inbound queue limit exceeded' : 'Computer offline; authorization retained');
491
+ } else if (role === 'client' && room?.connections.get(clientId)?.ws === ws) {
492
+ const connection = room.connections.get(clientId);
493
+ releaseRelay(connection);
494
+ room.connections.delete(clientId);
495
+ if (room.host) send(room.host, { type: 'detach', session });
496
+ } else if (role === 'observer' && room?.observers.get(clientId)?.ws === ws) {
497
+ room.observers.delete(clientId);
498
+ } else if (role === 'file-host' && room?.uploadHost === ws) {
499
+ room.uploadHost = undefined;
500
+ for (const client of room.uploadClients?.values() || []) client.ws.close(code === 4432 ? 4432 : 4410, code === 4432 ? 'File receiver inbound queue limit exceeded' : 'File receiver offline');
501
+ } else if (role === 'file-client' && room?.uploadClients?.get(session)?.ws === ws) {
502
+ room.uploadClients.delete(session);
503
+ if (room.uploadHost) sendFileFrame(room.uploadHost, { type: 'file-close', session }, { bytesPerSecond: 4 * 1024 * 1024 });
185
504
  }
186
505
  });
506
+ let messageTail = Promise.resolve();
507
+ // Include the in-flight message: async database work must not permit an
508
+ // unbounded chain of closures retaining incoming payloads.
509
+ let pendingMessages = 0, pendingBytes = 0;
510
+ const maxPendingMessages = 128, maxPendingBytes = 16 * 1024 * 1024;
511
+ let sampleStarted = Date.now(), peakMessages = 0, peakBytes = 0, maxWaitMs = 0, receivedMessages = 0;
512
+ function sampleInboundQueue() {
513
+ const now = Date.now();
514
+ if (now - sampleStarted < 60000) return;
515
+ // Summarize only intervals with an actual backlog; never log payloads or credentials.
516
+ if (peakMessages > 1) log({ event: 'inbound-queue-sample', role: role || 'unauthenticated',
517
+ windowMs: now - sampleStarted, receivedMessages, peakMessages, peakBytes, maxWaitMs,
518
+ pendingMessages, pendingBytes });
519
+ sampleStarted = now; peakMessages = pendingMessages; peakBytes = pendingBytes;
520
+ maxWaitMs = 0; receivedMessages = 0;
521
+ }
187
522
  ws.on('message', (bytes, binary) => {
523
+ if (ws.readyState !== ws.OPEN) return;
524
+ const size = bytes.length;
525
+ if (pendingMessages >= maxPendingMessages || pendingBytes + size > maxPendingBytes) {
526
+ log({ event: 'inbound-queue-limit', role: role || 'unauthenticated',
527
+ pendingMessages, pendingBytes, incomingBytes: size, maxWaitMs });
528
+ ws.close(4432, 'Inbound queue limit exceeded');
529
+ return;
530
+ }
531
+ pendingMessages++; pendingBytes += size;
532
+ receivedMessages++;
533
+ peakMessages = Math.max(peakMessages, pendingMessages); peakBytes = Math.max(peakBytes, pendingBytes);
534
+ const enqueuedAt = Date.now();
535
+ messageTail = messageTail.then(async () => {
188
536
  try {
537
+ maxWaitMs = Math.max(maxWaitMs, Date.now() - enqueuedAt);
538
+ if (ws.readyState !== ws.OPEN) return;
539
+ if (accountConnection && !accountConnection.valid()) return;
540
+ if (binary) {
541
+ if (role === 'file-host' && room.uploadHost === ws) {
542
+ const frame = parseFileBytes(bytes);
543
+ const client = room.uploadClients.get(frame.session);
544
+ if (!client || client.ws.readyState !== ws.OPEN) return;
545
+ sendFileFrame(client.ws, frame.data, { bytesPerSecond: 1024 * 1024 }); return;
546
+ }
547
+ if (role === 'file-client' && room.uploadClients?.get(session)?.ws === ws) {
548
+ if (room.uploadHost?.readyState !== ws.OPEN) return fail(4410, 'File receiver offline');
549
+ sendFileFrame(room.uploadHost, routedFileBytes(session, bytes), { bytesPerSecond: 4 * 1024 * 1024, session }); return;
550
+ }
551
+ return fail(1008, 'Invalid binary frame');
552
+ }
189
553
  const message = decode(bytes, binary);
190
554
  if (!role) {
191
- if (message.type === 'host') {
192
- const durable = message.deviceId !== undefined;
193
- if (durable && (!validId(message.deviceId) || !validSecret(message.deviceSecret))) return fail();
194
- if (!durable && stateFile) return fail(4406, 'Device protocol upgrade required');
195
- let lanCandidates, hostName;
196
- try {
197
- lanCandidates = normalizeLanCandidates(message.lanCandidates);
198
- hostName = normalizeHostName(message.hostName);
199
- }
200
- catch { return fail(1008, 'Invalid LAN candidates'); }
201
- room = durable ? rooms.get(message.deviceId) : undefined;
202
- if (room && (room.status !== 'active' || !matches(message.deviceSecret, room.hostHash))) return fail();
203
- if (!room) {
204
- // Durable computers prove future ownership with their generated device secret.
205
- // The legacy deployment key remains accepted, but is no longer required for first registration.
206
- if (!durable && !matches(message.key, hostHash)) return fail();
207
- if (rooms.size >= 256) return fail(4429, 'Device limit reached');
208
- const now = new Date().toISOString();
209
- room = { id: durable ? message.deviceId : randomUUID(), durable,
210
- revision: 0, status: 'active', clients: [], connections: new Map(), observers: new Map(), createdAt: now, updatedAt: now,
211
- ...(durable ? { hostHash: digest(message.deviceSecret) } : {}) };
212
- commit(room, {}); rooms.set(room.id, room);
555
+ let userId;
556
+ if (!['host', 'file-host', 'observe', 'device-management'].includes(message.type) &&
557
+ !(['pair', 'resume'].includes(message.type) && message.management === true)) {
558
+ if (!['pair', 'resume', 'observe', 'file-client'].includes(message.type)) return fail();
559
+ if (!accounts || !validSecret(message.accountToken)) return fail(4431, 'Account login required');
560
+ accountConnection = accountSockets.track(message.accountToken, invalidateAccountConnection);
561
+ try {
562
+ const user = await accounts.current(message.accountToken);
563
+ if (!accountConnection.activate(user.expiresAt)) return;
564
+ userId = user.id;
565
+ ws.adminInfo.account = { id: user.id, email: user.email };
566
+ }
567
+ catch { return fail(4431, 'Account session invalid'); }
568
+ if (ws.readyState !== ws.OPEN) return;
569
+ if (cancelledAccountIds.has(userId)) return fail(4431, 'Account cancelled');
570
+ }
571
+ if (message.type === 'file-host') {
572
+ room = rooms.get(message.deviceId);
573
+ if (!room?.host || room.status !== 'active' || !matches(message.deviceSecret, room.hostHash)) return fail();
574
+ role = 'file-host'; clearTimeout(authTimer);
575
+ room.uploadClients ||= new Map();
576
+ room.uploadHost?.close(4409, 'File receiver replaced');
577
+ room.uploadHost = ws;
578
+ send(ws, { type: 'file-host-ready' }); return;
579
+ }
580
+ if (message.type === 'file-client') {
581
+ room = rooms.get(message.room);
582
+ const client = room?.clients.find(item => item.id === message.clientId);
583
+ if (!room?.host || !room.uploadHost || room.status !== 'active' || !client ||
584
+ !matches(message.credential, client.hash)) return fail();
585
+ room.uploadClients ||= new Map();
586
+ if (!['upload', 'download'].includes(message.purpose)) return fail(1008, 'Invalid file purpose');
587
+ if (message.purpose === 'upload' && (typeof message.uploadSessionId !== 'string' ||
588
+ !/^[a-f0-9-]{36}$/.test(message.uploadSessionId) || typeof message.resumeUpload !== 'boolean')) {
589
+ return fail(1008, 'Invalid upload session');
590
+ }
591
+ if (!await recordConnection(room, message.clientId, new Date().toISOString(), deviceInfo(message))) return fail(4401, 'Device authorization revoked');
592
+ if (ws.readyState !== ws.OPEN || !accountConnection.valid()) return;
593
+ if (room.host?.readyState !== ws.OPEN || room.uploadHost?.readyState !== ws.OPEN) return fail(4410, 'File receiver offline');
594
+ if (room.status !== 'active' || !room.clients.some(item => item.id === message.clientId && matches(message.credential, item.hash))) return fail(4401, 'Device authorization revoked');
595
+ const replacing = message.purpose === 'upload' ? [...room.uploadClients.values()].filter(item =>
596
+ item.clientId === message.clientId && item.uploadSessionId === message.uploadSessionId) : [];
597
+ const same = [...room.uploadClients.values()].filter(item => item.clientId === message.clientId && item.purpose === message.purpose && !replacing.includes(item));
598
+ const all = [...rooms.values()].flatMap(item => [...(item.uploadClients?.values() || [])]).filter(item => !replacing.includes(item));
599
+ if (same.length >= (message.purpose === 'upload' ? 1 : 3) ||
600
+ all.length >= 8 || (message.purpose === 'upload' && all.filter(item => item.purpose === 'upload').length >= 2)) {
601
+ return fail(4429, 'Too many file transfers');
602
+ }
603
+ for (const old of replacing) {
604
+ room.uploadClients.delete(old.session);
605
+ sendFileFrame(room.uploadHost, { type: 'file-close', session: old.session }, { bytesPerSecond: 4 * 1024 * 1024 });
606
+ old.ws.close(4409, 'Upload connection replaced');
607
+ }
608
+ role = 'file-client'; clientId = message.clientId; session = randomUUID(); clearTimeout(authTimer);
609
+ room.uploadClients.set(session, { ws, session, clientId, userId, purpose: message.purpose, uploadSessionId: message.uploadSessionId });
610
+ sendFileFrame(room.uploadHost, { type: 'file-open', session, purpose: message.purpose, clientId,
611
+ uploadSessionId: message.uploadSessionId, resumeUpload: message.resumeUpload }, { bytesPerSecond: 4 * 1024 * 1024 }); return;
612
+ }
613
+ if (message.type === 'host') {
614
+ const durable = message.deviceId !== undefined;
615
+ if (durable && (!validId(message.deviceId) || !validSecret(message.deviceSecret))) return fail();
616
+ if (!durable && stateFile) return fail(4406, 'Device protocol upgrade required');
617
+ let lanCandidates, hostName;
618
+ try {
619
+ lanCandidates = normalizeLanCandidates(message.lanCandidates);
620
+ hostName = normalizeHostName(message.hostName);
213
621
  }
214
- const oldHost = room.host;
215
- room.observers ||= new Map();
216
- room.host = ws; room.hostName = hostName; room.lanCandidates = lanCandidates;
217
- room.p2pProbe = message.p2pProbe === 1; room.p2pProbeRepeat = message.p2pProbeRepeat === true;
218
- role = 'host'; clearTimeout(authTimer);
219
- if (oldHost && oldHost !== ws) {
220
- oldHost.close(4409, 'Computer connection replaced');
221
- for (const connection of room.connections.values()) connection.ws.close(4410, 'Computer reconnecting');
222
- }
223
- for (const observer of room.observers.values()) {
224
- send(observer.ws, { type: 'observing', online: true });
225
- send(observer.ws, { type: 'computer-info', hostName: room.hostName });
226
- send(observer.ws, { type: 'lan-candidates', candidates: room.lanCandidates });
227
- }
228
- const invite = room.clients.length ? {} : invitation(room);
229
- send(ws, { type: 'registered', protocolVersion: 7, room: room.id, paired: room.clients.length > 0,
230
- clientCount: room.clients.length, clients: room.clients, revision: room.revision, ...invite }); return;
231
- }
232
- room = rooms.get(message.room);
233
- if (!room || room.status !== 'active') return fail();
234
- let credential;
235
- if (message.type === 'pair' && room.pairingHash && Date.now() < room.expires && matches(message.code, room.pairingHash)) {
236
- if (!room.host) return fail(4410, 'Computer offline; authorization retained');
237
- credential = message.credential || secret();
238
- clientId = message.clientId || (!room.durable ? randomUUID() : undefined);
239
- if (!validSecret(credential) || !validId(clientId)) return fail();
240
- const existing = room.clients.find(client => client.id === clientId);
241
- if (!existing && room.clients.length >= maxClientsPerHost) return fail(4429, 'Mobile device limit reached');
242
- const now = new Date().toISOString();
243
- const client = { id: clientId, hash: digest(credential), createdAt: existing?.createdAt || now, updatedAt: now };
244
- commit(room, { clients: [...room.clients.filter(item => item.id !== clientId), client], revision: room.revision + 1 });
245
- room.pairingHash = undefined;
246
- send(room.host, { type: 'authorization-changed', clients: room.clients, revision: room.revision });
247
- } else {
248
- clientId = message.clientId;
249
- const client = room.clients.find(item => item.id === clientId) ||
250
- (!room.durable && !clientId ? room.clients.find(item => matches(message.credential, item.hash)) : undefined);
251
- if (client) clientId = client.id;
252
- if (!['resume', 'pair', 'observe'].includes(message.type) || !client || !matches(message.credential, client.hash)) {
253
- return fail(message.type === 'pair' ? 4408 : 4401, message.type === 'pair' ? 'Pairing code expired or used' : 'Device authorization rejected');
254
- }
255
- }
256
- if (message.type === 'observe') {
257
- role = 'observer'; clearTimeout(authTimer); room.observers ||= new Map();
258
- const oldObserver = room.observers.get(clientId);
259
- if (oldObserver?.ws && oldObserver.ws !== ws) oldObserver.ws.close(4409, 'Metadata connection replaced by same identity');
260
- room.observers.set(clientId, { ws });
261
- send(ws, { type: 'observing', online: Boolean(room.host) });
262
- send(ws, { type: 'debug-permissions', enabled: debugAllowed(debugAllowlist, room.id, room.clients.find(client => client.id === clientId)) });
263
- const announcement = adminState.read().announcement;
264
- if (announcement) send(ws, { type: 'announcement', announcement });
265
- if (room.hostName) send(ws, { type: 'computer-info', hostName: room.hostName });
266
- if (room.lanCandidates?.length) send(ws, { type: 'lan-candidates', candidates: room.lanCandidates });
267
- if (room.officialComputer) send(ws, { type: 'official-computer', computer: room.officialComputer });
268
- return;
269
- }
270
- if (!room.host) return fail(4410, 'Computer offline; authorization retained');
271
- const oldConnection = room.connections.get(clientId);
272
- role = 'client'; clearTimeout(authTimer); session = randomUUID();
273
- if (oldConnection?.ws && oldConnection.ws !== ws) {
274
- releaseRelay(oldConnection);
275
- send(room.host, { type: 'detach', session: oldConnection.session });
276
- oldConnection.ws.close(4409, 'Phone connection replaced by same identity');
277
- }
278
- room.connections.set(clientId, { ws, session, p2pProbe: room.p2pProbe && message.p2pProbe === 1,
279
- repeat: room.p2pProbeRepeat, signals: 0, probeStarted: 0, outbound: Promise.resolve(), queuedBytes: 0,
280
- relayAdmitted: false, relayExempt: false,
281
- capacityExempt: debugAllowed(debugAllowlist, room.id, room.clients.find(client => client.id === clientId)) });
282
- send(ws, { type: 'paired', protocolVersion: 7, room: room.id, clientId, revision: room.revision, ...(credential ? { credential } : {}) });
283
- send(ws, { type: 'computer-info', hostName: room.hostName });
284
- if (room.lanCandidates?.length) send(ws, { type: 'lan-candidates', candidates: room.lanCandidates });
285
- const announcement = adminState.read().announcement;
286
- if (announcement) send(ws, { type: 'announcement', announcement });
287
- send(room.host, { type: 'attach', session, clientId, p2pProbe: room.connections.get(clientId).p2pProbe ? 1 : 0 }); return;
622
+ catch { return fail(1008, 'Invalid LAN candidates'); }
623
+ room = durable ? rooms.get(message.deviceId) : undefined;
624
+ if (!room) {
625
+ if (!durable && !matches(message.key, hostHash)) return fail();
626
+ room = await registerRoom(durable ? message.deviceId : randomUUID(), durable, message.deviceSecret);
627
+ if (ws.readyState !== ws.OPEN) return;
628
+ }
629
+ if (durable && (room.status !== 'active' || !matches(message.deviceSecret, room.hostHash))) return fail();
630
+ const oldHost = room.host;
631
+ room.observers ||= new Map();
632
+ if (oldHost && oldHost !== ws) {
633
+ for (const task of room.usageTasks?.values() || []) {
634
+ try { await meter?.lost(task); } catch (error) { log({ event: 'usage-error', message: error.message }); }
635
+ }
636
+ room.usageTasks?.clear(); room.usageArms?.clear();
637
+ }
638
+ room.host = ws; room.hostName = hostName; room.lanCandidates = lanCandidates; room.health = null;
639
+ room.adminPhoneConnections = undefined;
640
+ room.adminLastOnlineAt = ws.adminInfo.connectedAt; room.adminLastHostName = hostName;
641
+ room.usageTasks ||= new Map(); room.usageArms ||= new Map();
642
+ if (oldHost && oldHost !== ws) room.uploadHost?.close(4409, 'Computer connection replaced');
643
+ room.p2pProbe = message.p2pProbe === 1; room.p2pProbeRepeat = message.p2pProbeRepeat === true;
644
+ role = 'host'; clearTimeout(authTimer);
645
+ if (oldHost && oldHost !== ws) {
646
+ oldHost.close(4409, 'Computer connection replaced');
647
+ for (const connection of room.connections.values()) connection.ws.close(4410, 'Computer reconnecting');
648
+ }
649
+ for (const observer of room.observers.values()) {
650
+ send(observer.ws, { type: 'observing', online: true });
651
+ send(observer.ws, { type: 'computer-info', hostName: room.hostName });
652
+ send(observer.ws, { type: 'lan-candidates', candidates: room.lanCandidates });
653
+ }
654
+ const invite = room.clients.length ? {} : invitation(room);
655
+ send(ws, { type: 'registered', protocolVersion: 7, room: room.id, paired: room.clients.length > 0,
656
+ clientCount: room.clients.length, clients: room.clients, revision: room.revision, ...invite }); return;
657
+ }
658
+ room = rooms.get(message.room);
659
+ if (!room || room.status !== 'active') return fail();
660
+ let credential;
661
+ const longInvite = message.type === 'pair' ? longInvitation(message.code) : undefined;
662
+ const reusableInvite = longInvite?.room === room.id;
663
+ if (message.type === 'pair' && !room.pairingInProgress && (reusableInvite ||
664
+ (room.pairingHash && Date.now() < room.expires && matches(message.code, room.pairingHash)))) {
665
+ if (!room.host) return fail(4410, 'Computer offline; authorization retained');
666
+ credential = message.credential || secret();
667
+ clientId = message.clientId || (!room.durable ? randomUUID() : undefined);
668
+ if (!validSecret(credential) || !validId(clientId)) return fail();
669
+ const existing = room.clients.find(client => client.id === clientId);
670
+ const now = new Date().toISOString();
671
+ const client = { ...existing, id: clientId, hash: digest(credential), createdAt: existing?.createdAt || now, updatedAt: now,
672
+ ...deviceInfo(message), lastConnectedAt: now };
673
+ room.pairingInProgress = true;
674
+ try {
675
+ await commit(room, current => ({ clients: [...current.clients.filter(item => item.id !== clientId), client], revision: current.revision + 1 }));
676
+ if (!reusableInvite) room.pairingHash = undefined;
677
+ } finally { room.pairingInProgress = false; }
678
+ if (!room.host) return fail(4410, 'Computer offline; authorization retained');
679
+ send(room.host, { type: 'authorization-changed', clients: room.clients, revision: room.revision });
680
+ } else {
681
+ clientId = message.clientId;
682
+ const client = room.clients.find(item => item.id === clientId) ||
683
+ (!room.durable && !clientId ? room.clients.find(item => matches(message.credential, item.hash)) : undefined);
684
+ if (client) clientId = client.id;
685
+ if (!['resume', 'pair', 'observe', 'device-management'].includes(message.type) || !client || !matches(message.credential, client.hash)) {
686
+ return fail(message.type === 'pair' ? 4408 : 4401, message.type === 'pair' ? 'Pairing code expired or used' : 'Device authorization rejected');
687
+ }
688
+ }
689
+ if (ws.readyState !== ws.OPEN) return;
690
+ if (accountConnection && !accountConnection.valid()) return;
691
+ if (cancelledAccountIds.has(userId)) return fail(4431, 'Account cancelled');
692
+ if (!await recordConnection(room, clientId, new Date().toISOString(), deviceInfo(message))) return fail(4401, 'Device authorization revoked');
693
+ if (ws.readyState !== ws.OPEN) return;
694
+ if (message.type === 'device-management') {
695
+ role = 'device-manager'; clearTimeout(authTimer); room.deviceManagers ||= new Map();
696
+ room.deviceManagers.set(ws, clientId);
697
+ ws.once('close', () => room.deviceManagers.delete(ws));
698
+ send(ws, { type: 'device-management-ready' }); return;
699
+ }
700
+ if (message.type === 'observe') {
701
+ role = 'observer'; clearTimeout(authTimer); room.observers ||= new Map();
702
+ const oldObserver = room.observers.get(clientId);
703
+ if (oldObserver?.ws && oldObserver.ws !== ws) oldObserver.ws.close(4409, 'Metadata connection replaced by same identity');
704
+ room.observers.set(clientId, { ws, userId });
705
+ send(ws, { type: 'observing', online: Boolean(room.host), health: room.host ? room.health || null : null });
706
+ send(ws, { type: 'debug-permissions', enabled: debugAllowed(debugAllowlist, room.id, room.clients.find(client => client.id === clientId)) });
707
+ const announcement = adminState.read().announcement;
708
+ if (announcement) send(ws, { type: 'announcement', announcement });
709
+ if (room.hostName) send(ws, { type: 'computer-info', hostName: room.hostName });
710
+ if (room.lanCandidates?.length) send(ws, { type: 'lan-candidates', candidates: room.lanCandidates });
711
+ if (room.officialComputer) send(ws, { type: 'official-computer', computer: room.officialComputer });
712
+ return;
713
+ }
714
+ if (!room.host) return fail(4410, 'Computer offline; authorization retained');
715
+ const oldConnection = room.connections.get(clientId);
716
+ role = 'client'; clearTimeout(authTimer); session = randomUUID();
717
+ if (oldConnection?.ws && oldConnection.ws !== ws) {
718
+ releaseRelay(oldConnection);
719
+ send(room.host, { type: 'detach', session: oldConnection.session });
720
+ oldConnection.ws.close(4409, 'Phone connection replaced by same identity');
721
+ }
722
+ room.connections.set(clientId, { ws, session, p2pProbe: Boolean(userId) && room.p2pProbe && message.p2pProbe === 1,
723
+ userId,
724
+ repeat: room.p2pProbeRepeat, signals: 0, probeStarted: 0, outbound: Promise.resolve(), queuedBytes: 0,
725
+ relayAdmitted: false, relayExempt: false,
726
+ capacityExempt: debugAllowed(debugAllowlist, room.id, room.clients.find(client => client.id === clientId)) });
727
+ send(ws, { type: 'paired', protocolVersion: 7, room: room.id, clientId, revision: room.revision, ...(credential ? { credential } : {}) });
728
+ send(ws, { type: 'computer-info', hostName: room.hostName });
729
+ if (room.lanCandidates?.length) send(ws, { type: 'lan-candidates', candidates: room.lanCandidates });
730
+ const announcement = adminState.read().announcement;
731
+ if (announcement) send(ws, { type: 'announcement', announcement });
732
+ send(room.host, { type: 'attach', session, clientId, chat: Boolean(userId),
733
+ p2pProbe: room.connections.get(clientId).p2pProbe ? 1 : 0 }); return;
734
+ }
735
+ if (role === 'device-manager') {
736
+ if (!room.clients.some(item => item.id === clientId)) return fail(4401, 'Device authorization revoked');
737
+ if (!['devices-list', 'device-revoke'].includes(message.type)) return fail(1008, 'Invalid device management command');
738
+ await manageDevices(room, ws, message, clientId); return;
288
739
  }
289
- if (role === 'host') {
290
- if (room.host !== ws) return;
291
- if (message.type === 'lan-candidates') {
292
- room.lanCandidates = normalizeLanCandidates(message.candidates);
293
- for (const connection of room.connections.values()) send(connection.ws, { type: 'lan-candidates', candidates: room.lanCandidates });
294
- for (const observer of room.observers.values()) send(observer.ws, { type: 'lan-candidates', candidates: room.lanCandidates });
295
- return;
296
- }
297
- if (message.type === 'status') {
298
- send(ws, { type: 'status', commandId: message.commandId, paired: room.clients.length > 0, clientCount: room.clients.length, revision: room.revision }); return;
299
- }
300
- if (message.type === 'pair-code') {
301
- send(ws, { type: 'pair-code', commandId: message.commandId, room: room.id, ...invitation(room) }); return;
302
- }
303
- if (message.type === 'revoke') {
304
- commit(room, { clients: [], revision: room.revision + 1 });
305
- room.pairingHash = undefined;
306
- for (const connection of room.connections.values()) {
307
- releaseRelay(connection);
308
- send(ws, { type: 'detach', session: connection.session });
309
- connection.ws.close(4401, 'Device authorization revoked');
310
- }
311
- room.connections.clear();
312
- for (const observer of room.observers.values()) observer.ws.close(4401, 'Device authorization revoked');
313
- room.observers.clear();
314
- send(ws, { type: 'authorization-changed', clients: [], revision: room.revision });
315
- send(ws, { type: 'revoked', commandId: message.commandId, room: room.id }); return;
316
- }
317
- const connection = [...room.connections.values()].find(item => item.session === message.session);
318
- if (!connection) return; // Late frames from an ended session.
319
- if (message.type === 'data' && typeof message.payload === 'string') queueClientMessage(connection, { type: 'data', payload: message.payload });
320
- else if (message.type === 'ready') queueClientMessage(connection, { type: 'ready', p2pProbe: connection.p2pProbe ? 1 : 0,
321
- p2pProbeRepeat: connection.p2pProbe && connection.repeat });
322
- else if (message.type === 'p2p-probe' && connection.p2pProbe && validProbeSignal(message)) {
323
- if (connection.probeId !== message.probeId) return;
324
- if (++connection.signals > 150) return fail(1008, 'Probe signal limit');
325
- send(connection.ws, { ...message, session: undefined });
326
- }
327
- else if (message.type === 'detach') connection.ws.close(1011, 'Local Bridge unavailable');
328
- else fail();
329
- } else if (role === 'observer' && room.observers.get(clientId)?.ws === ws && message.type === 'official-computer') {
330
- const computer = normalizeOfficialComputer(message);
331
- commit(room, { officialComputer: computer });
332
- send(ws, { type: 'official-computer-saved', computer });
333
- } else if (role === 'client' && room.connections.get(clientId)?.ws === ws && message.type === 'p2p-probe') {
334
- const connection = room.connections.get(clientId);
335
- if (!connection.p2pProbe || !validProbeSignal(message)) return fail(1008, 'Invalid probe signal');
336
- if (message.kind === 'start') {
337
- if (connection.probeId && (!connection.repeat || Date.now() - connection.probeStarted < 2000)) {
338
- send(ws, { type: 'p2p-probe', version: 1, probeId: message.probeId, kind: 'close' }); return;
339
- }
340
- connection.probeId = message.probeId; connection.signals = 0; connection.probeStarted = Date.now();
341
- }
342
- if (connection.probeId !== message.probeId) return;
343
- if (++connection.signals > 150) return fail(1008, 'Probe signal limit');
344
- send(room.host, { ...message, session });
345
- } else if (room.connections.get(clientId)?.ws === ws && message.type === 'p2p-route' && message.direct === 1) {
346
- releaseRelay(room.connections.get(clientId));
347
- } else if (room.connections.get(clientId)?.ws === ws && message.type === 'data' && typeof message.payload === 'string') {
348
- const connection = room.connections.get(clientId);
349
- if (!controlPayload(message.payload) && !admitRelay(connection, room)) {
350
- ws.close(4428, 'Server busy'); return;
351
- }
352
- send(room.host, { type: 'data', session, payload: message.payload });
740
+ if (role === 'host') {
741
+ if (message.type === 'workstation-health') {
742
+ if (room.host !== ws) return;
743
+ const health = message.health;
744
+ if (health !== null && (!health || Array.isArray(health) || typeof health !== 'object' ||
745
+ health.status !== 'ok' || typeof health.serviceInstanceId !== 'string' || JSON.stringify(health).length > 65536))
746
+ return fail(1008, 'Invalid workstation health');
747
+ room.health = health;
748
+ for (const observer of room.observers.values()) send(observer.ws, { type: 'workstation-health', health });
749
+ return;
750
+ }
751
+
752
+ if (['phone-connection', 'phone-connections'].includes(message.type)) {
753
+ if (room.host !== ws) return;
754
+ if (message.type === 'phone-connections') {
755
+ if (!Array.isArray(message.active) || message.active.length > 128 ||
756
+ message.active.some(item => !validPhoneConnection(item))) return fail(1008, 'Invalid phone connection snapshot');
757
+ room.adminPhoneConnections = new Map(message.active.filter(item => room.clients.some(client => client.id === item.clientId))
758
+ .map(item => [item.connectionId, { connectionId: item.connectionId, clientId: item.clientId, mode: item.mode }]));
759
+ return;
760
+ }
761
+ const event = message.event;
762
+ if (!validPhoneConnection(event)) return fail(1008, 'Invalid phone connection event');
763
+ // Revocation can overtake the workstation's final close event.
764
+ if (!room.clients.some(client => client.id === event.clientId)) {
765
+ send(ws, { type: 'phone-connection-ack', id: event.id }); return;
766
+ }
767
+ try { await phoneEvents.record(room.id, event); }
768
+ catch (error) { log({ event: 'phone-connection-error', message: error.message }); return; }
769
+ if (room.host !== ws) return;
770
+ if (phoneEventIds.has(event.id)) { send(ws, { type: 'phone-connection-ack', id: event.id }); return; }
771
+ phoneEventIds.add(event.id);
772
+ if (phoneEventIds.size > 256) phoneEventIds.delete(phoneEventIds.values().next().value);
773
+ room.adminPhoneConnections ||= new Map();
774
+ if (event.state === 'connected') room.adminPhoneConnections.set(event.connectionId,
775
+ { connectionId: event.connectionId, clientId: event.clientId, mode: event.mode });
776
+ else room.adminPhoneConnections.delete(event.connectionId);
777
+ send(ws, { type: 'phone-connection-ack', id: event.id }); return;
778
+ }
779
+ if (room.host !== ws) return;
780
+ if (['devices-list', 'device-revoke'].includes(message.type)) { await manageDevices(room, ws, message); return; }
781
+ if (message.type === 'device-connected') {
782
+ const at = new Date(Math.min(Date.parse(message.at), Date.now())).toISOString();
783
+ if (!validId(message.clientId)) return fail(1008, 'Invalid mobile identity');
784
+ await recordConnection(room, message.clientId, at, deviceInfo(message));
785
+ room.adminLanConnections ||= new Map();
786
+ if (room.clients.some(client => client.id === message.clientId) &&
787
+ (!room.adminLanConnections.has(message.clientId) || room.adminLanConnections.get(message.clientId) < at)) {
788
+ room.adminLanConnections.set(message.clientId, at);
789
+ }
790
+ return;
791
+ }
792
+ if (meter && ['usage-arm', 'usage-message', 'usage-cancel', 'usage-turn', 'usage-observer-lost'].includes(message.type)) {
793
+ if (message.type === 'usage-observer-lost') {
794
+ for (const task of room.usageTasks.values()) {
795
+ try { await meter.lost(task); } catch (error) { log({ event: 'usage-error', message: error.message }); }
796
+ }
797
+ room.usageTasks.clear();
798
+ return;
799
+ }
800
+ const armKey = `${message.session}:${message.requestId}`;
801
+ const connection = [...room.connections.values()].find(item => item.session === message.session);
802
+ if (message.type === 'usage-arm') {
803
+ if (!connection?.userId || !Number.isSafeInteger(message.requestId) || message.requestId < 1 ||
804
+ (message.threadId !== null && (!plainText(message.threadId, 128, false)))) {
805
+ diagnostics?.record('usage', 'arm-rejected', !connection?.userId ? 'account-missing' : 'invalid-request');
806
+ return;
807
+ }
808
+ for (const [key, arm] of room.usageArms) if (Date.now() - arm.at > 120000) room.usageArms.delete(key);
809
+ if (room.usageArms.size >= 256) return;
810
+ room.usageArms.set(armKey, { userId: connection.userId,
811
+ clientId: [...room.connections.entries()].find(([, item]) => item === connection)?.[0],
812
+ session: message.session, at: Date.now() });
813
+ return;
814
+ }
815
+ if (message.type === 'usage-cancel') {
816
+ room.usageArms.delete(armKey); return;
817
+ }
818
+ if (message.type === 'usage-message') {
819
+ const arm = room.usageArms.get(armKey);
820
+ room.usageArms.delete(armKey);
821
+ if (!arm || Date.now() - arm.at > 120000 || !plainText(message.messageKey, 128, false)) {
822
+ diagnostics?.record('usage', 'message-rejected', !arm ? 'arm-missing' : 'expired-or-invalid');
823
+ return;
824
+ }
825
+ try {
826
+ await meter.action(arm, room.id, arm.clientId, 'message', message.messageKey);
827
+ if (['start', 'steer'].includes(message.turnKind) &&
828
+ plainText(message.threadId, 80, false) && plainText(message.turnId, 80, false)) {
829
+ const key = `${message.threadId}:${message.turnId}`;
830
+ if (key.length <= 128) {
831
+ const counted = await meter.action(arm, room.id, arm.clientId, 'turn', key);
832
+ if (counted && message.turnKind === 'steer') diagnostics?.record('usage', 'turn-recovered', 'steer');
833
+ if (!room.usageTasks.has(key) && message.observerReady === true && message.completed !== true &&
834
+ (message.turnKind === 'start' || message.active === true)) {
835
+ const task = await meter.turnOrigin(room.id, key);
836
+ if (task) {
837
+ await meter.pulse(task, true, room.id, task.clientId, counted ? key : randomUUID());
838
+ room.usageTasks.set(key, task);
839
+ }
840
+ }
841
+ }
842
+ }
843
+ } catch (error) { diagnostics?.record('usage', 'message-error', 'database'); log({ event: 'usage-error', message: error.message }); }
844
+ return;
845
+ }
846
+ if (!['turn/started', 'turn/completed', 'heartbeat', 'resume', 'inactive'].includes(message.method) ||
847
+ !plainText(message.threadId, 80, false) || !plainText(message.turnId, 80, false)) return;
848
+ const key = `${message.threadId}:${message.turnId}`;
849
+ if (key.length > 128) return;
850
+ let task = room.usageTasks.get(key);
851
+ try {
852
+ if (message.method === 'resume' && !task) {
853
+ task = await meter.turnOrigin(room.id, key);
854
+ if (!task) { diagnostics?.record('usage', 'turn-resume', 'origin-missing'); return; }
855
+ await meter.pulse(task, true, room.id, task.clientId, `${key}:${randomUUID()}`);
856
+ diagnostics?.record('usage', 'turn-resume', 'confirmed');
857
+ room.usageTasks.set(key, task);
858
+ } else if (task && message.method === 'heartbeat') await meter.pulse(task, true, room.id, task.clientId);
859
+ else if (task && message.method === 'inactive') {
860
+ await meter.lost(task);
861
+ room.usageTasks.delete(key);
862
+ }
863
+ else if (task && message.method === 'turn/completed') {
864
+ await meter.pulse(task, false, room.id, task.clientId);
865
+ room.usageTasks.delete(key);
866
+ }
867
+ } catch (error) { log({ event: 'usage-error', message: error.message }); }
868
+ return;
869
+ }
870
+ if (message.type === 'lan-candidates') {
871
+ room.lanCandidates = normalizeLanCandidates(message.candidates);
872
+ for (const connection of room.connections.values()) send(connection.ws, { type: 'lan-candidates', candidates: room.lanCandidates });
873
+ for (const observer of room.observers.values()) send(observer.ws, { type: 'lan-candidates', candidates: room.lanCandidates });
874
+ return;
875
+ }
876
+ if (message.type === 'status') {
877
+ send(ws, { type: 'status', commandId: message.commandId, paired: room.clients.length > 0, clientCount: room.clients.length, revision: room.revision }); return;
878
+ }
879
+ if (message.type === 'pair-code') {
880
+ send(ws, { type: 'pair-code', commandId: message.commandId, room: room.id, ...invitation(room) }); return;
881
+ }
882
+ if (['invite-create', 'invite-list', 'invite-revoke'].includes(message.type)) {
883
+ try {
884
+ if (!room.durable) throw new Error('长期邀请需要持久工作站身份');
885
+ let result;
886
+ if (message.type === 'invite-create') result = longInvitations.create(room.id, message.expires);
887
+ else if (message.type === 'invite-list') result = { invitations: longInvitations.list().filter(item => item.room === room.id) };
888
+ else {
889
+ if (!longInvitations.list().some(item => item.id === message.id && item.room === room.id)) throw new Error('未找到本工作站的邀请');
890
+ longInvitations.revoke(message.id);
891
+ result = { id: message.id, revoked: true };
892
+ }
893
+ send(ws, { type: message.type, commandId: message.commandId, ...result });
894
+ } catch (error) { send(ws, { type: message.type, commandId: message.commandId, error: error.message }); }
895
+ return;
896
+ }
897
+ if (message.type === 'revoke') {
898
+ await commit(room, current => ({ clients: [], revision: current.revision + 1 }));
899
+ room.pairingHash = undefined;
900
+ for (const connection of room.connections.values()) {
901
+ releaseRelay(connection);
902
+ send(ws, { type: 'detach', session: connection.session });
903
+ connection.ws.close(4401, 'Device authorization revoked');
904
+ }
905
+ room.connections.clear();
906
+ for (const client of room.uploadClients?.values() || []) {
907
+ if (room.uploadHost) {
908
+ discardFileSession(room.uploadHost, client.session);
909
+ send(room.uploadHost, { type: 'file-close', session: client.session });
910
+ }
911
+ client.ws.close(4401, 'Device authorization revoked');
912
+ }
913
+ room.uploadClients?.clear();
914
+ for (const observer of room.observers.values()) observer.ws.close(4401, 'Device authorization revoked');
915
+ room.observers.clear();
916
+ for (const socket of room.deviceManagers?.keys() || []) socket.close(4401, 'Device authorization revoked');
917
+ room.deviceManagers?.clear();
918
+ send(ws, { type: 'authorization-changed', clients: [], revision: room.revision });
919
+ send(ws, { type: 'revoked', commandId: message.commandId, room: room.id }); return;
920
+ }
921
+ const connection = [...room.connections.values()].find(item => item.session === message.session);
922
+ if (!connection) return; // Late frames from an ended session.
923
+ if (message.type === 'data' && typeof message.payload === 'string') queueClientMessage(connection, { type: 'data', payload: message.payload });
924
+ else if (message.type === 'ready') queueClientMessage(connection, { type: 'ready', p2pProbe: connection.p2pProbe ? 1 : 0,
925
+ p2pProbeRepeat: connection.p2pProbe && connection.repeat });
926
+ else if (message.type === 'p2p-probe' && connection.p2pProbe && validProbeSignal(message)) {
927
+ const diagnostic = message.purpose === 'diagnostic';
928
+ const file = message.purpose === 'file';
929
+ if (message.purpose && !diagnostic && !file) return fail(1008, 'Invalid probe purpose');
930
+ if (connection[file ? 'fileProbeId' : diagnostic ? 'diagnosticProbeId' : 'probeId'] !== message.probeId) return;
931
+ if (++connection[file ? 'fileSignals' : diagnostic ? 'diagnosticSignals' : 'signals'] > 150) return fail(1008, 'Probe signal limit');
932
+ send(connection.ws, { ...message, session: undefined });
933
+ }
934
+ else if (message.type === 'detach') connection.ws.close(1011, 'Local Bridge unavailable');
935
+ else fail();
936
+ } else if (role === 'file-host' && room.uploadHost === ws) {
937
+ const client = room.uploadClients.get(message.session);
938
+ if (!client) return;
939
+ if (message.type === 'file-frame' && typeof message.payload === 'string' && message.payload.length <= 512 * 1024) {
940
+ sendFileFrame(client.ws, { type: 'file-frame', payload: message.payload }, { bytesPerSecond: 1024 * 1024 });
941
+ } else if (message.type === 'file-close') closeFileSocket(client.ws, 1011, 'File receiver unavailable');
942
+ else fail(1008, 'Invalid file frame');
943
+ } else if (role === 'file-client' && room.uploadClients?.get(session)?.ws === ws) {
944
+ if (message.type !== 'file-frame' || typeof message.payload !== 'string' || message.payload.length > 512 * 1024) return fail(1008, 'Invalid file frame');
945
+ if (room.uploadHost?.readyState !== ws.OPEN) return fail(4410, 'File receiver offline');
946
+ sendFileFrame(room.uploadHost, { type: 'file-frame', session, payload: message.payload }, { bytesPerSecond: 4 * 1024 * 1024 });
947
+ } else if (role === 'observer' && room.observers.get(clientId)?.ws === ws && message.type === 'official-computer') {
948
+ const computer = normalizeOfficialComputer(message);
949
+ await commit(room, { officialComputer: computer });
950
+ send(ws, { type: 'official-computer-saved', computer });
951
+ } else if (role === 'client' && room.connections.get(clientId)?.ws === ws && message.type === 'p2p-probe') {
952
+ const connection = room.connections.get(clientId);
953
+ if (!connection.p2pProbe || !validProbeSignal(message)) return fail(1008, 'Invalid probe signal');
954
+ const diagnostic = message.purpose === 'diagnostic';
955
+ const file = message.purpose === 'file';
956
+ if (message.purpose && !diagnostic && !file) return fail(1008, 'Invalid probe purpose');
957
+ const idField = file ? 'fileProbeId' : diagnostic ? 'diagnosticProbeId' : 'probeId';
958
+ const signalsField = file ? 'fileSignals' : diagnostic ? 'diagnosticSignals' : 'signals';
959
+ const startedField = file ? 'fileProbeStarted' : diagnostic ? 'diagnosticProbeStarted' : 'probeStarted';
960
+ if (message.kind === 'start') {
961
+ if (connection[idField] && (!connection.repeat || Date.now() - connection[startedField] < 2000)) {
962
+ send(ws, { type: 'p2p-probe', version: 1, probeId: message.probeId,
963
+ ...(message.purpose ? { purpose: message.purpose } : {}), kind: 'close' }); return;
964
+ }
965
+ connection[idField] = message.probeId; connection[signalsField] = 0; connection[startedField] = Date.now();
966
+ }
967
+ if (connection[idField] !== message.probeId) return;
968
+ if (++connection[signalsField] > 150) return fail(1008, 'Probe signal limit');
969
+ send(room.host, { ...message, session });
970
+ if (diagnostic && message.kind === 'close') connection.diagnosticProbeId = undefined;
971
+ } else if (room.connections.get(clientId)?.ws === ws && message.type === 'p2p-route' && message.direct === 1) {
972
+ releaseRelay(room.connections.get(clientId));
973
+ room.connections.get(clientId).adminRoute = 'direct';
974
+ } else if (room.connections.get(clientId)?.ws === ws && message.type === 'data' && typeof message.payload === 'string') {
975
+ const connection = room.connections.get(clientId);
976
+ if (!connection.userId && !managementPayload(message.payload)) return fail(4431, 'Account login required');
977
+ if (connection.userId && !controlPayload(message.payload) && !admitRelay(connection, room)) {
978
+ ws.close(4428, 'Server busy'); return;
979
+ }
980
+ if (connection.userId && !controlPayload(message.payload)) connection.adminRoute = 'relay';
981
+ send(room.host, { type: 'data', session, payload: message.payload });
353
982
  } else fail();
354
983
  } catch (error) { log({ event: 'request-error', role, message: 'Relay operation failed' }); ws.close(1011, 'Relay operation failed'); }
984
+ finally { pendingMessages--; pendingBytes -= size; }
985
+ }).catch(() => ws.close(1011, 'Relay operation failed'));
355
986
  });
356
987
  });
357
- await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, '127.0.0.1', resolve); });
358
- listenUrl = `ws://127.0.0.1:${server.address().port}/relay`;
359
- return {
360
- url: listenUrl, hostKey,
988
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, '127.0.0.1', resolve); });
989
+ listenUrl = `ws://127.0.0.1:${server.address().port}/relay`;
990
+ return {
991
+ url: listenUrl, hostKey,
361
992
  async close() {
362
993
  for (const ws of wss.clients) ws.terminate();
363
994
  await new Promise(resolve => wss.close(resolve));
364
995
  await new Promise(resolve => server.close(resolve));
996
+ if (database) await database.close();
365
997
  },
366
- };
367
- }
368
-
369
- function relayPublicAddress(value) {
370
- const parsed = new URL(value);
371
- if (!['ws:', 'wss:'].includes(parsed.protocol) || parsed.pathname !== '/relay' || parsed.search || parsed.hash || parsed.username || parsed.password) {
372
- throw new Error('Invalid public relay URL');
373
- }
374
- return parsed.href;
375
- }
376
-
377
- function pairingHeaders(type, body) {
378
- return { 'Content-Type': type, 'Content-Length': Buffer.byteLength(body), 'Cache-Control': 'no-store',
379
- 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer',
380
- 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'" };
381
- }
382
-
383
- function pairingPage({ deepLink = '', expires = 0, appDownloadUrl = '', expired = false }) {
384
- const open = deepLink ? `<a class="primary" href="${escapeHtml(deepLink)}">打开 Code Relax</a>` : '';
385
- const download = appDownloadUrl ? `<a href="${escapeHtml(appDownloadUrl)}">安装 Code Relax</a>` : '<p>尚未安装时,请通过当前私测渠道安装 App,再重新打开此页面。</p>';
386
- const text = expired ? '这条设备配对邀请已过期或已经使用,请在电脑重新生成。'
387
- : `这是一次性设备配对邀请,有效期至 ${escapeHtml(new Date(expires).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }))}。每台移动设备只需配对一次。`;
388
- return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Code Relax 配对</title><style>body{margin:0;background:#0d1210;color:#eef4f0;font:16px system-ui,sans-serif}main{max-width:520px;margin:12vh auto;padding:32px}h1{font-size:28px}p{color:#aab7af;line-height:1.7}a{display:block;margin-top:16px;padding:14px 18px;border:1px solid #496257;border-radius:12px;color:#dce9e1;text-align:center;text-decoration:none}.primary{background:#1f5a40;border-color:#367c5b}</style><main><h1>Code Relax</h1><p>${text}</p>${expired ? '' : open + download}</main></html>`;
389
- }
390
-
391
- function escapeHtml(value) {
392
- return String(value).replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character]);
393
- }
394
-
395
- function normalizeLanCandidates(value) {
396
- if (value === undefined) return [];
397
- if (!Array.isArray(value) || value.length > 8) throw new Error('Invalid LAN candidates');
398
- return value.map(candidate => {
399
- const endpoint = new URL(candidate?.address || '');
400
- if (endpoint.protocol !== 'http:' || !endpoint.port || endpoint.pathname !== '/' || endpoint.search || endpoint.hash ||
401
- endpoint.username || endpoint.password || !privateIpv4(endpoint.hostname) ||
402
- !['wifi', 'ethernet'].includes(candidate.networkType) || !plainLabel(candidate.networkName) || !plainLabel(candidate.interfaceName)) {
403
- throw new Error('Invalid LAN candidate');
404
- }
405
- return { address: endpoint.href.replace(/\/$/, ''), networkType: candidate.networkType,
406
- networkName: candidate.networkName.trim(), interfaceName: candidate.interfaceName.trim() };
407
- });
408
- }
409
-
410
- function normalizeHostName(value) {
411
- if (!plainLabel(value)) throw new Error('Invalid host name');
412
- return value.trim();
413
- }
414
-
415
- function normalizeOfficialComputer(value) {
416
- if (!plainText(value.environmentId, 256, false) || !plainText(value.name ?? '', 80, true) ||
417
- !plainText(value.hostname ?? '', 80, true)) throw new Error('Invalid official computer');
418
- return { environmentId: value.environmentId.trim(), name: (value.name || '').trim(),
419
- hostname: (value.hostname || '').trim(), updatedAt: new Date().toISOString() };
420
- }
421
-
422
- function privateIpv4(value) {
423
- const parts = value.split('.').map(Number);
424
- if (parts.length !== 4 || parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) return false;
425
- return parts[0] === 10 || (parts[0] === 192 && parts[1] === 168) ||
426
- (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
427
- (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127);
428
- }
429
-
430
- function plainLabel(value) {
431
- return typeof value === 'string' && value.trim().length > 0 && value.trim().length <= 80 && !/[\u0000-\u001f\u007f]/.test(value);
432
- }
433
-
434
-
435
- function plainText(value, limit, empty) {
436
- return typeof value === 'string' && (empty || value.trim().length > 0) && value.trim().length <= limit && !/[\u0000-\u001f\u007f]/.test(value);
437
- }
438
-
439
- function controlPayload(value) {
440
- try {
441
- const message = JSON.parse(value);
442
- if (message.type === 'unsubscribe') return true;
443
- return ['request', 'subscribe'].includes(message.type) && typeof message.path === 'string' &&
444
- (message.path === '/api/health' || message.path.startsWith('/api/shared-recovery'));
445
- } catch { return false; }
446
- }
447
-
448
- function readBody(request, limit) {
449
- return new Promise((resolve, reject) => {
450
- let value = '';
451
- request.setEncoding('utf8');
452
- request.on('data', chunk => {
453
- value += chunk;
454
- if (Buffer.byteLength(value) > limit) { reject(new Error('Admin request too large')); request.destroy(); }
455
- });
456
- request.on('end', () => resolve(value)); request.on('error', reject);
457
- });
458
- }
459
-
460
- function adminPage() {
461
- return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Code Relax 管理</title><style>
462
- body{margin:0;background:#0d1210;color:#eef4f0;font:15px system-ui,sans-serif}main{max-width:760px;margin:40px auto;padding:0 20px}section{background:#151d19;border:1px solid #2a3931;border-radius:14px;padding:18px;margin:16px 0}h1{font-size:24px}h2{font-size:17px}label{display:block;margin:12px 0 6px;color:#aab7af}input,textarea{box-sizing:border-box;width:100%;padding:11px;border:1px solid #43564b;border-radius:9px;background:#0d1210;color:#eef4f0}textarea{min-height:120px;resize:vertical}button{margin:12px 8px 0 0;padding:10px 16px;border:0;border-radius:9px;background:#2c7653;color:white}button.secondary{background:#39443e}pre{white-space:pre-wrap;color:#b9c8bf}.row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.metric{padding:12px;background:#0d1210;border-radius:9px}.metric b{display:block;font-size:22px}</style><main><h1>Code Relax 管理</h1><section><h2>运行状态</h2><div id="metrics" class="row"></div><button onclick="load()">刷新</button></section><section><h2>服务器转发容量</h2><label>最大并发 WSS 转发会话</label><input id="limit" type="number" min="1" max="256"><button onclick="saveLimit()">保存</button></section><section><h2>系统公告</h2><label>标题</label><input id="title" maxlength="80"><label>正文</label><textarea id="message" maxlength="2000"></textarea><button onclick="publish()">发布</button><button class="secondary" onclick="withdraw()">撤下</button><pre id="current"></pre></section><pre id="result"></pre><script>
463
- const request=async(path,options={})=>{const response=await fetch(path,options);const body=await response.json();if(!response.ok)throw new Error(body.error||('HTTP '+response.status));return body};
464
- async function load(){try{const s=await request('/admin/api/status');limit.value=s.maxRelaySessions;metrics.innerHTML='<div class="metric"><b>'+s.activeRelaySessions+'</b>受限转发 / '+s.maxRelaySessions+'</div><div class="metric"><b>'+s.activeExemptRelaySessions+'</b>白名单转发</div><div class="metric"><b>'+s.websocketConnections+'</b>WebSocket</div><div class="metric"><b>'+s.onlineComputers+'</b>在线工作站</div><div class="metric"><b>'+s.activePhones+'</b>活动手机</div>';current.textContent=s.announcement?s.announcement.title+'\\n'+s.announcement.message+'\\n'+s.announcement.publishedAt:'当前无公告';}catch(e){result.textContent=e.message}}
465
- const write=(method,path,body)=>request(path,{method,headers:{'Content-Type':'application/json','X-Code-Relax-Admin':'1'},body:JSON.stringify(body)}).then(()=>{result.textContent='已保存';return load()}).catch(e=>result.textContent=e.message);
466
- function saveLimit(){return write('PUT','/admin/api/settings',{maxRelaySessions:Number(limit.value)})}function publish(){return write('PUT','/admin/api/announcement',{title:title.value,message:message.value})}function withdraw(){return write('DELETE','/admin/api/announcement',{})}load();
467
- </script></main></html>`;
468
- }
998
+ };
999
+ }
1000
+
1001
+ function relayPublicAddress(value) {
1002
+ const parsed = new URL(value);
1003
+ if (!['ws:', 'wss:'].includes(parsed.protocol) || parsed.pathname !== '/relay' || parsed.search || parsed.hash || parsed.username || parsed.password) {
1004
+ throw new Error('Invalid public relay URL');
1005
+ }
1006
+ return parsed.href;
1007
+ }
1008
+
1009
+ function pairingHeaders(type, body) {
1010
+ return { 'Content-Type': type, 'Content-Length': Buffer.byteLength(body), 'Cache-Control': 'no-store',
1011
+ 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer',
1012
+ 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'" };
1013
+ }
1014
+
1015
+ function pairingPage({ deepLink = '', expires = 0, appDownloadUrl = '', expired = false }) {
1016
+ const open = deepLink ? `<a class="primary" href="${escapeHtml(deepLink)}">打开 Code Relax</a>` : '';
1017
+ const download = appDownloadUrl ? `<a href="${escapeHtml(appDownloadUrl)}">安装 Code Relax</a>` : '<p>尚未安装时,请通过当前私测渠道安装 App,再重新打开此页面。</p>';
1018
+ const text = expired ? '这条设备配对邀请已过期或已经使用,请在电脑重新生成。'
1019
+ : `这是一次性设备配对邀请,有效期至 ${escapeHtml(new Date(expires).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }))}。每台移动设备只需配对一次。`;
1020
+ return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Code Relax 配对</title><style>body{margin:0;background:#0d1210;color:#eef4f0;font:16px system-ui,sans-serif}main{max-width:520px;margin:12vh auto;padding:32px}h1{font-size:28px}p{color:#aab7af;line-height:1.7}a{display:block;margin-top:16px;padding:14px 18px;border:1px solid #496257;border-radius:12px;color:#dce9e1;text-align:center;text-decoration:none}.primary{background:#1f5a40;border-color:#367c5b}</style><main><h1>Code Relax</h1><p>${text}</p>${expired ? '' : open + download}</main></html>`;
1021
+ }
1022
+
1023
+ function escapeHtml(value) {
1024
+ return String(value).replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character]);
1025
+ }
1026
+
1027
+ function normalizeLanCandidates(value) {
1028
+ if (value === undefined) return [];
1029
+ if (!Array.isArray(value) || value.length > 8) throw new Error('Invalid LAN candidates');
1030
+ return value.map(candidate => {
1031
+ const endpoint = new URL(candidate?.address || '');
1032
+ if (endpoint.protocol !== 'http:' || !endpoint.port || endpoint.pathname !== '/' || endpoint.search || endpoint.hash ||
1033
+ endpoint.username || endpoint.password || !privateIpv4(endpoint.hostname) ||
1034
+ !['wifi', 'ethernet'].includes(candidate.networkType) || !plainLabel(candidate.networkName) || !plainLabel(candidate.interfaceName)) {
1035
+ throw new Error('Invalid LAN candidate');
1036
+ }
1037
+ return { address: endpoint.href.replace(/\/$/, ''), networkType: candidate.networkType,
1038
+ networkName: candidate.networkName.trim(), interfaceName: candidate.interfaceName.trim() };
1039
+ });
1040
+ }
1041
+
1042
+ function normalizeHostName(value) {
1043
+ if (!plainLabel(value)) throw new Error('Invalid host name');
1044
+ return value.trim();
1045
+ }
1046
+
1047
+ function normalizeOfficialComputer(value) {
1048
+ if (!plainText(value.environmentId, 256, false) || !plainText(value.name ?? '', 80, true) ||
1049
+ !plainText(value.hostname ?? '', 80, true)) throw new Error('Invalid official computer');
1050
+ return { environmentId: value.environmentId.trim(), name: (value.name || '').trim(),
1051
+ hostname: (value.hostname || '').trim(), updatedAt: new Date().toISOString() };
1052
+ }
1053
+
1054
+ function privateIpv4(value) {
1055
+ const parts = value.split('.').map(Number);
1056
+ if (parts.length !== 4 || parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) return false;
1057
+ return parts[0] === 10 || (parts[0] === 192 && parts[1] === 168) ||
1058
+ (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
1059
+ (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127);
1060
+ }
1061
+
1062
+ function plainLabel(value) {
1063
+ return typeof value === 'string' && value.trim().length > 0 && value.trim().length <= 80 && !/[\u0000-\u001f\u007f]/.test(value);
1064
+ }
1065
+
1066
+
1067
+ function plainText(value, limit, empty) {
1068
+ return typeof value === 'string' && (empty || value.trim().length > 0) && value.trim().length <= limit && !/[\u0000-\u001f\u007f]/.test(value);
1069
+ }
1070
+
1071
+ // Device-authorized management must not become an anonymous chat tunnel.
1072
+ function managementPayload(value) {
1073
+ try {
1074
+ const message = JSON.parse(value);
1075
+ if (['unsubscribe', 'cancel', 'response-ack'].includes(message.type)) return true;
1076
+ if (message.type === 'subscribe') return message.path === '/api/shared-recovery/events';
1077
+ if (message.type !== 'request') return false;
1078
+ const method = message.method || 'GET';
1079
+ if (method === 'GET') return ['/api/health', '/api/official-remote/status'].includes(message.path) ||
1080
+ /^\/api\/threads\/[0-9a-f-]{28,64}\/model-changes(?:\?|$)/i.test(message.path);
1081
+ return method === 'POST' && message.path === '/api/official-remote/pairing';
1082
+ } catch { return false; }
1083
+ }
1084
+
1085
+ function controlPayload(value) {
1086
+ try {
1087
+ const message = JSON.parse(value);
1088
+ if (message.type === 'unsubscribe') return true;
1089
+ return ['request', 'subscribe'].includes(message.type) && typeof message.path === 'string' &&
1090
+ (message.path === '/api/health' || ['/api/desktop/start', '/api/desktop/restart'].includes(message.path) || message.path.startsWith('/api/shared-recovery'));
1091
+ } catch { return false; }
1092
+ }
1093
+
1094
+ function readBody(request, limit) {
1095
+ return new Promise((resolve, reject) => {
1096
+ let value = '';
1097
+ request.setEncoding('utf8');
1098
+ request.on('data', chunk => {
1099
+ value += chunk;
1100
+ if (Buffer.byteLength(value) > limit) { reject(new Error('Admin request too large')); request.destroy(); }
1101
+ });
1102
+ request.on('end', () => resolve(value)); request.on('error', reject);
1103
+ });
1104
+ }