@walkhi/code-relax 0.1.0-beta.10 → 0.1.0-beta.101

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