@walkhi/code-relax 0.1.0-beta.3 → 0.1.0-beta.30

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