@jingyi0605/codingns 0.4.0 → 0.5.1

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 (282) hide show
  1. package/bin/codingns.mjs +425 -1
  2. package/dist/public/assets/AdaptiveButlerPage-B153lk5H.css +1 -0
  3. package/dist/public/assets/AdaptiveButlerPage-SffCV4Vb.js +3 -0
  4. package/dist/public/assets/App-DUAg5urj.css +1 -0
  5. package/dist/public/assets/App-WOLwMld_.js +30 -0
  6. package/dist/public/assets/BootstrapPage--zExdgfM.js +1 -0
  7. package/dist/public/assets/ConversationPage-D9pzRmOg.js +2 -0
  8. package/dist/public/assets/DesktopDetachPreviewPage-DvI9CIKi.js +1 -0
  9. package/dist/public/assets/DesktopWindowPage-D8FpOSLE.js +2 -0
  10. package/dist/public/assets/FileContextPanel-C8T7oqRN.js +1 -0
  11. package/dist/public/assets/GitSidebar-Bze7DNnc.js +6 -0
  12. package/dist/public/assets/MobileCreateSessionSheet-CXSKMnYn.js +1 -0
  13. package/dist/public/assets/MobileSheet-Gzc14EpR.js +1 -0
  14. package/dist/public/assets/MobileTopHeaderFrame-BWorAJ1C.js +1 -0
  15. package/dist/public/assets/MobileWorkspaceSwitcherHeader-DOr4pTUq.js +1 -0
  16. package/dist/public/assets/ServerSettingsModal-BYB0GvTl.js +1 -0
  17. package/dist/public/assets/SessionIndexPage-CR3IARXX.js +1 -0
  18. package/dist/public/assets/SettingsPage-B_BQtnwE.js +1 -0
  19. package/dist/public/assets/TerminalManagerPanel-PQ-EM64j.js +1 -0
  20. package/dist/public/assets/{TerminalPage-6jHZV9Mh.js → TerminalPage-CvnHXBhw.js} +19 -19
  21. package/dist/public/assets/TerminalRuntimeFallbackModal-D7Aq186N.js +1 -0
  22. package/dist/public/assets/ToolFilesPage-Qzkc6K2I.js +1 -0
  23. package/dist/public/assets/ToolGitPage-BdNDN-cV.js +1 -0
  24. package/dist/public/assets/ToolProcessesPage-EXJ9DHWI.js +1 -0
  25. package/dist/public/assets/ToolsHomePage-CjF3CWzR.js +1 -0
  26. package/dist/public/assets/WorkbenchLandingPage-DZPk4SmX.js +1 -0
  27. package/dist/public/assets/WorkbenchLayout-rwQib5In.js +3 -0
  28. package/dist/public/assets/WorkbenchModal-B09hC9b5.js +1 -0
  29. package/dist/public/assets/WorkbenchShellRoute-Cerk5uK7.js +1 -0
  30. package/dist/public/assets/WorkbenchShellRoute-DsW4mBTX.css +1 -0
  31. package/dist/public/assets/WorkspaceDebugDetailPage-Bcq8s-Ma.js +1 -0
  32. package/dist/public/assets/WorkspaceDetailPage-DNAa8pKr.js +1 -0
  33. package/dist/public/assets/WorkspaceHomePage-BoiLuACV.js +1 -0
  34. package/dist/public/assets/butler-records-events-DgWCG364.js +1 -0
  35. package/dist/public/assets/client-runtime-manager-CRQ-F5d2.js +1 -0
  36. package/dist/public/assets/default-session-permission-mode-D0wZ9Jek.js +1 -0
  37. package/dist/public/assets/event-DvH9tcej.js +1 -0
  38. package/dist/public/assets/file-tree-icon-Dp_xhVfD.js +31 -0
  39. package/dist/public/assets/index-C2G8Gmf1.js +42 -0
  40. package/dist/public/assets/index-CpPTUeA3.css +1 -0
  41. package/dist/public/assets/preferences-service-gOt2ZjKZ.js +1 -0
  42. package/dist/public/assets/session-runtime-machine-Dq3pW-UF.js +17 -0
  43. package/dist/public/assets/styles-BWPBZvze.css +1 -0
  44. package/dist/public/assets/styles-CSUx5LGe.js +1 -0
  45. package/dist/public/assets/terminal-runtime-meta-BMT-rSEe.js +1 -0
  46. package/dist/public/assets/useRegisteredDebugTemplates-zMcEOGca.js +1 -0
  47. package/dist/public/assets/window-BWqRixxq.js +1 -0
  48. package/dist/public/index.html +2 -2
  49. package/dist/server/middlewares/auth-guard.d.ts +4 -0
  50. package/dist/server/middlewares/auth-guard.js +42 -4
  51. package/dist/server/middlewares/auth-guard.js.map +1 -1
  52. package/dist/server/modules/assistant-capability/assistant-capability-controller.d.ts +62 -1
  53. package/dist/server/modules/assistant-capability/assistant-capability-controller.js +58 -0
  54. package/dist/server/modules/assistant-capability/assistant-capability-controller.js.map +1 -1
  55. package/dist/server/modules/assistant-capability/assistant-capability-service.d.ts +66 -3
  56. package/dist/server/modules/assistant-capability/assistant-capability-service.js +173 -1
  57. package/dist/server/modules/assistant-capability/assistant-capability-service.js.map +1 -1
  58. package/dist/server/modules/auth/auth-controller.d.ts +11 -1
  59. package/dist/server/modules/auth/auth-controller.js +61 -2
  60. package/dist/server/modules/auth/auth-controller.js.map +1 -1
  61. package/dist/server/modules/auth/auth-device-display-name.d.ts +10 -0
  62. package/dist/server/modules/auth/auth-device-display-name.js +190 -0
  63. package/dist/server/modules/auth/auth-device-display-name.js.map +1 -0
  64. package/dist/server/modules/auth/auth-service.d.ts +80 -5
  65. package/dist/server/modules/auth/auth-service.js +333 -23
  66. package/dist/server/modules/auth/auth-service.js.map +1 -1
  67. package/dist/server/modules/butler/assistant-automation-service.d.ts +2 -0
  68. package/dist/server/modules/butler/assistant-automation-service.js +46 -0
  69. package/dist/server/modules/butler/assistant-automation-service.js.map +1 -1
  70. package/dist/server/modules/butler/assistant-sandbox-cleanup-scheduler.d.ts +32 -0
  71. package/dist/server/modules/butler/assistant-sandbox-cleanup-scheduler.js +93 -0
  72. package/dist/server/modules/butler/assistant-sandbox-cleanup-scheduler.js.map +1 -0
  73. package/dist/server/modules/butler/assistant-sandbox-service.d.ts +16 -2
  74. package/dist/server/modules/butler/assistant-sandbox-service.js +137 -4
  75. package/dist/server/modules/butler/assistant-sandbox-service.js.map +1 -1
  76. package/dist/server/modules/butler/butler-auth-service.js +7 -2
  77. package/dist/server/modules/butler/butler-auth-service.js.map +1 -1
  78. package/dist/server/modules/butler/butler-control-session-service.d.ts +4 -1
  79. package/dist/server/modules/butler/butler-control-session-service.js +20 -1
  80. package/dist/server/modules/butler/butler-control-session-service.js.map +1 -1
  81. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.d.ts +2 -1
  82. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.js +27 -25
  83. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.js.map +1 -1
  84. package/dist/server/modules/butler/butler-follow-up-service.d.ts +32 -4
  85. package/dist/server/modules/butler/butler-follow-up-service.js +436 -331
  86. package/dist/server/modules/butler/butler-follow-up-service.js.map +1 -1
  87. package/dist/server/modules/butler/butler-inbox-analysis-service.d.ts +1 -1
  88. package/dist/server/modules/butler/butler-inbox-analysis-service.js.map +1 -1
  89. package/dist/server/modules/butler/butler-inbox-service.js +1 -0
  90. package/dist/server/modules/butler/butler-inbox-service.js.map +1 -1
  91. package/dist/server/modules/butler/butler-session-service.d.ts +3 -1
  92. package/dist/server/modules/butler/butler-session-service.js +15 -1
  93. package/dist/server/modules/butler/butler-session-service.js.map +1 -1
  94. package/dist/server/modules/butler/butler-workspace-context.d.ts +1 -1
  95. package/dist/server/modules/butler/butler-workspace-context.js +54 -28
  96. package/dist/server/modules/butler/butler-workspace-context.js.map +1 -1
  97. package/dist/server/modules/client/client-controller.js +1 -1
  98. package/dist/server/modules/client/client-controller.js.map +1 -1
  99. package/dist/server/modules/client/client-service.d.ts +16 -2
  100. package/dist/server/modules/client/client-service.js +21 -3
  101. package/dist/server/modules/client/client-service.js.map +1 -1
  102. package/dist/server/modules/provider/provider-controller.d.ts +1 -1
  103. package/dist/server/modules/provider/provider-controller.js.map +1 -1
  104. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-identity-service.d.ts +10 -0
  105. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-identity-service.js +48 -0
  106. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-identity-service.js.map +1 -0
  107. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.d.ts +48 -0
  108. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.js +11 -0
  109. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.js.map +1 -0
  110. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.d.ts +74 -0
  111. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.js +302 -0
  112. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.js.map +1 -0
  113. package/dist/server/modules/relay-tunnel/relay-tunnel-controller.d.ts +33 -0
  114. package/dist/server/modules/relay-tunnel/relay-tunnel-controller.js +57 -0
  115. package/dist/server/modules/relay-tunnel/relay-tunnel-controller.js.map +1 -0
  116. package/dist/server/modules/relay-tunnel/relay-tunnel-edge-proof.d.ts +9 -0
  117. package/dist/server/modules/relay-tunnel/relay-tunnel-edge-proof.js +25 -0
  118. package/dist/server/modules/relay-tunnel/relay-tunnel-edge-proof.js.map +1 -0
  119. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.d.ts +18 -0
  120. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.js +230 -0
  121. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.js.map +1 -0
  122. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.d.ts +41 -0
  123. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.js +443 -0
  124. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.js.map +1 -0
  125. package/dist/server/modules/relay-tunnel/relay-tunnel-service.d.ts +112 -0
  126. package/dist/server/modules/relay-tunnel/relay-tunnel-service.js +966 -0
  127. package/dist/server/modules/relay-tunnel/relay-tunnel-service.js.map +1 -0
  128. package/dist/server/modules/sessions/codex-app-server-helper-client.d.ts +2 -1
  129. package/dist/server/modules/sessions/codex-app-server-helper-client.js +78 -0
  130. package/dist/server/modules/sessions/codex-app-server-helper-client.js.map +1 -1
  131. package/dist/server/modules/sessions/codex-app-server-helper-process.js +84 -2
  132. package/dist/server/modules/sessions/codex-app-server-helper-process.js.map +1 -1
  133. package/dist/server/modules/sessions/provider-session-delete-cli.d.ts +15 -0
  134. package/dist/server/modules/sessions/provider-session-delete-cli.js +148 -0
  135. package/dist/server/modules/sessions/provider-session-delete-cli.js.map +1 -0
  136. package/dist/server/modules/sessions/session-controller.d.ts +4 -1
  137. package/dist/server/modules/sessions/session-controller.js +4 -0
  138. package/dist/server/modules/sessions/session-controller.js.map +1 -1
  139. package/dist/server/modules/sessions/session-history-service.d.ts +17 -0
  140. package/dist/server/modules/sessions/session-history-service.js +150 -1
  141. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  142. package/dist/server/modules/sessions/session-live-runtime-router-service.d.ts +25 -0
  143. package/dist/server/modules/sessions/session-live-runtime-router-service.js +42 -0
  144. package/dist/server/modules/sessions/session-live-runtime-router-service.js.map +1 -0
  145. package/dist/server/modules/sessions/session-live-runtime-service.js +34 -18
  146. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  147. package/dist/server/modules/sessions/session-message-attachment-service.d.ts +1 -0
  148. package/dist/server/modules/sessions/session-message-attachment-service.js +22 -0
  149. package/dist/server/modules/sessions/session-message-attachment-service.js.map +1 -1
  150. package/dist/server/modules/sessions/session-permission-request-service.d.ts +1 -0
  151. package/dist/server/modules/sessions/session-permission-request-service.js +200 -5
  152. package/dist/server/modules/sessions/session-permission-request-service.js.map +1 -1
  153. package/dist/server/modules/sessions/session-provider-error-mapper.js +32 -0
  154. package/dist/server/modules/sessions/session-provider-error-mapper.js.map +1 -1
  155. package/dist/server/modules/sessions/session-provider-usage-guard-service.d.ts +37 -0
  156. package/dist/server/modules/sessions/session-provider-usage-guard-service.js +179 -0
  157. package/dist/server/modules/sessions/session-provider-usage-guard-service.js.map +1 -0
  158. package/dist/server/modules/sessions/session-provider-usage-limit.d.ts +17 -0
  159. package/dist/server/modules/sessions/session-provider-usage-limit.js +465 -0
  160. package/dist/server/modules/sessions/session-provider-usage-limit.js.map +1 -0
  161. package/dist/server/modules/skills/assistant-runtime-skill-catalog.d.ts +8 -0
  162. package/dist/server/modules/skills/assistant-runtime-skill-catalog.js +26 -0
  163. package/dist/server/modules/skills/assistant-runtime-skill-catalog.js.map +1 -0
  164. package/dist/server/modules/skills/assistant-runtime-skill-cleanup.d.ts +9 -0
  165. package/dist/server/modules/skills/assistant-runtime-skill-cleanup.js +55 -0
  166. package/dist/server/modules/skills/assistant-runtime-skill-cleanup.js.map +1 -0
  167. package/dist/server/modules/skills/builtin-skill-service.js +1 -6
  168. package/dist/server/modules/skills/builtin-skill-service.js.map +1 -1
  169. package/dist/server/modules/skills/skill-controller.d.ts +2 -2
  170. package/dist/server/modules/skills/skill-controller.js +9 -1
  171. package/dist/server/modules/skills/skill-controller.js.map +1 -1
  172. package/dist/server/modules/skills/skill-manager-service.d.ts +26 -1
  173. package/dist/server/modules/skills/skill-manager-service.js +346 -90
  174. package/dist/server/modules/skills/skill-manager-service.js.map +1 -1
  175. package/dist/server/modules/skills/skill-name-policy.d.ts +2 -0
  176. package/dist/server/modules/skills/skill-name-policy.js +10 -0
  177. package/dist/server/modules/skills/skill-name-policy.js.map +1 -0
  178. package/dist/server/modules/tailscale/tailscale-service.d.ts +2 -0
  179. package/dist/server/modules/tailscale/tailscale-service.js +21 -8
  180. package/dist/server/modules/tailscale/tailscale-service.js.map +1 -1
  181. package/dist/server/modules/tasks/task-types.d.ts +3 -0
  182. package/dist/server/modules/tasks/task-types.js +3 -0
  183. package/dist/server/modules/tasks/task-types.js.map +1 -1
  184. package/dist/server/modules/terminal/template-reverse-proxy-service.js +71 -3
  185. package/dist/server/modules/terminal/template-reverse-proxy-service.js.map +1 -1
  186. package/dist/server/modules/workbench/snapshot-revision.d.ts +4 -0
  187. package/dist/server/modules/workbench/snapshot-revision.js +13 -0
  188. package/dist/server/modules/workbench/snapshot-revision.js.map +1 -0
  189. package/dist/server/modules/workbench/workbench-service.d.ts +1 -0
  190. package/dist/server/modules/workbench/workbench-service.js +3 -2
  191. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  192. package/dist/server/modules/workbench/workspace-panel-snapshot-service.d.ts +6 -1
  193. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js +10 -8
  194. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js.map +1 -1
  195. package/dist/server/routes/assistant.js +30 -0
  196. package/dist/server/routes/assistant.js.map +1 -1
  197. package/dist/server/routes/auth.js +4 -0
  198. package/dist/server/routes/auth.js.map +1 -1
  199. package/dist/server/routes/sessions.js +1 -0
  200. package/dist/server/routes/sessions.js.map +1 -1
  201. package/dist/server/routes/system.d.ts +2 -1
  202. package/dist/server/routes/system.js +13 -1
  203. package/dist/server/routes/system.js.map +1 -1
  204. package/dist/server/server/create-server.d.ts +10 -0
  205. package/dist/server/server/create-server.js +83 -13
  206. package/dist/server/server/create-server.js.map +1 -1
  207. package/dist/server/shared/utils/tokens.d.ts +3 -1
  208. package/dist/server/shared/utils/tokens.js +9 -2
  209. package/dist/server/shared/utils/tokens.js.map +1 -1
  210. package/dist/server/storage/repositories/assistant-automation-task-repository.d.ts +2 -0
  211. package/dist/server/storage/repositories/assistant-automation-task-repository.js +8 -2
  212. package/dist/server/storage/repositories/assistant-automation-task-repository.js.map +1 -1
  213. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.d.ts +1 -0
  214. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js +27 -0
  215. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js.map +1 -1
  216. package/dist/server/storage/repositories/auth-device-repository.d.ts +22 -0
  217. package/dist/server/storage/repositories/auth-device-repository.js +97 -0
  218. package/dist/server/storage/repositories/auth-device-repository.js.map +1 -0
  219. package/dist/server/storage/repositories/auth-device-session-repository.d.ts +17 -0
  220. package/dist/server/storage/repositories/auth-device-session-repository.js +82 -0
  221. package/dist/server/storage/repositories/auth-device-session-repository.js.map +1 -0
  222. package/dist/server/storage/repositories/auth-login-event-repository.d.ts +9 -0
  223. package/dist/server/storage/repositories/auth-login-event-repository.js +53 -0
  224. package/dist/server/storage/repositories/auth-login-event-repository.js.map +1 -0
  225. package/dist/server/storage/repositories/auth-token-repository.d.ts +4 -0
  226. package/dist/server/storage/repositories/auth-token-repository.js +58 -5
  227. package/dist/server/storage/repositories/auth-token-repository.js.map +1 -1
  228. package/dist/server/storage/repositories/butler-follow-up-task-repository.js +21 -3
  229. package/dist/server/storage/repositories/butler-follow-up-task-repository.js.map +1 -1
  230. package/dist/server/storage/repositories/instance-relay-tunnel-identity-repository.d.ts +8 -0
  231. package/dist/server/storage/repositories/instance-relay-tunnel-identity-repository.js +52 -0
  232. package/dist/server/storage/repositories/instance-relay-tunnel-identity-repository.js.map +1 -0
  233. package/dist/server/storage/repositories/instance-relay-tunnel-repository.d.ts +10 -0
  234. package/dist/server/storage/repositories/instance-relay-tunnel-repository.js +153 -0
  235. package/dist/server/storage/repositories/instance-relay-tunnel-repository.js.map +1 -0
  236. package/dist/server/storage/repositories/instance-tailscale-repository.js +6 -3
  237. package/dist/server/storage/repositories/instance-tailscale-repository.js.map +1 -1
  238. package/dist/server/storage/repositories/managed-skill-repository.d.ts +2 -1
  239. package/dist/server/storage/repositories/managed-skill-repository.js +14 -4
  240. package/dist/server/storage/repositories/managed-skill-repository.js.map +1 -1
  241. package/dist/server/storage/repositories/session-message-attachment-repository.d.ts +2 -0
  242. package/dist/server/storage/repositories/session-message-attachment-repository.js +24 -0
  243. package/dist/server/storage/repositories/session-message-attachment-repository.js.map +1 -1
  244. package/dist/server/storage/sqlite/client.js +297 -2
  245. package/dist/server/storage/sqlite/client.js.map +1 -1
  246. package/dist/server/storage/sqlite/schema.sql +122 -4
  247. package/dist/server/types/domain.d.ts +91 -1
  248. package/dist/server/ws/workbench-ws-hub.js +225 -99
  249. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  250. package/dist/server/ws/ws-auth-guard.js +1 -4
  251. package/dist/server/ws/ws-auth-guard.js.map +1 -1
  252. package/dist/server/ws/ws-server.d.ts +1 -1
  253. package/dist/server/ws/ws-server.js.map +1 -1
  254. package/node_modules/@codingns/session-sync-core/dist/codex-resume-history.d.ts +1 -0
  255. package/node_modules/@codingns/session-sync-core/dist/codex-resume-history.js +80 -0
  256. package/node_modules/@codingns/session-sync-core/dist/codex-resume-history.js.map +1 -0
  257. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.d.ts +1 -0
  258. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js +11 -1
  259. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js.map +1 -1
  260. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +11 -0
  261. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +132 -21
  262. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  263. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.d.ts +2 -0
  264. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js +53 -1
  265. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js.map +1 -1
  266. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.d.ts +1 -0
  267. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js +10 -1
  268. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js.map +1 -1
  269. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.d.ts +1 -0
  270. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +30 -0
  271. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
  272. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.d.ts +5 -1
  273. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +145 -58
  274. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  275. package/node_modules/@codingns/session-sync-core/dist/services.d.ts +1 -0
  276. package/node_modules/@codingns/session-sync-core/dist/services.js +7 -0
  277. package/node_modules/@codingns/session-sync-core/dist/services.js.map +1 -1
  278. package/node_modules/@codingns/session-sync-core/dist/types.d.ts +2 -0
  279. package/package.json +1 -1
  280. package/scripts/postinstall.mjs +0 -33
  281. package/dist/public/assets/index-CSVhg7I8.js +0 -123
  282. package/dist/public/assets/index-Ce1VX19m.css +0 -1
@@ -0,0 +1,966 @@
1
+ import os from "node:os";
2
+ import { AppError } from "../../shared/errors/app-error.js";
3
+ import { decryptSecret, encryptSecret } from "../../shared/utils/secret-box.js";
4
+ import { nowIso } from "../../shared/utils/time.js";
5
+ import { RelayTunnelIdentityService } from "./crypto/relay-tunnel-identity-service.js";
6
+ import { createTaskManager } from "../tasks/task-manager.js";
7
+ import { HOST_TASK_TYPES } from "../tasks/task-types.js";
8
+ export class RelayTunnelService {
9
+ db;
10
+ bootstrapStateRepository;
11
+ repository;
12
+ defaultLocalTargetBaseUrl;
13
+ legacyLocalTargetBaseUrl;
14
+ controlSessionSecret;
15
+ fetchFn;
16
+ taskManager;
17
+ runtimeAdapter;
18
+ identityService;
19
+ constructor(db, bootstrapStateRepository, identityRepository, repository, options, taskManager = createTaskManager(), runtimeAdapter = new NoopRelayTunnelRuntimeAdapter()) {
20
+ this.db = db;
21
+ this.bootstrapStateRepository = bootstrapStateRepository;
22
+ this.repository = repository;
23
+ this.taskManager = taskManager;
24
+ this.runtimeAdapter = runtimeAdapter;
25
+ this.identityService = new RelayTunnelIdentityService(identityRepository);
26
+ this.controlSessionSecret = normalizeRequiredText(options.controlSessionSecret, "controlSessionSecret");
27
+ this.fetchFn = options.fetchFn ?? fetch;
28
+ this.defaultLocalTargetBaseUrl = normalizeHttpBaseUrl(options.defaultLocalTargetBaseUrl, "defaultLocalTargetBaseUrl");
29
+ this.legacyLocalTargetBaseUrl = options.legacyLocalTargetBaseUrl
30
+ ? normalizeHttpBaseUrl(options.legacyLocalTargetBaseUrl, "legacyLocalTargetBaseUrl")
31
+ : null;
32
+ this.registerBackgroundTasks();
33
+ }
34
+ async restoreOnStartup() {
35
+ const snapshot = this.readStateSnapshot();
36
+ if (!snapshot.hasPersistedConfig
37
+ || !snapshot.config.activated
38
+ || !snapshot.config.enabled
39
+ || !isBound(snapshot.config)) {
40
+ return;
41
+ }
42
+ const effectiveConfig = this.syncIdentityIntoConfig(snapshot.config);
43
+ if (!this.isBootstrapInitialized()) {
44
+ this.repository.upsertStatus(buildSkeletonStatus("blocked_uninitialized", effectiveConfig, {
45
+ observedAt: nowIso()
46
+ }));
47
+ return;
48
+ }
49
+ this.requestReconnect("relay_tunnel.startup_restore");
50
+ }
51
+ async getStatus() {
52
+ const snapshot = this.readStateSnapshot();
53
+ const effectiveConfig = this.resolveConfigWithIdentity(snapshot.config);
54
+ return this.buildStatusDto(snapshot, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
55
+ }
56
+ async ensureIdentity() {
57
+ const snapshot = this.readStateSnapshot();
58
+ const nextConfig = this.syncIdentityIntoConfig(snapshot.config);
59
+ return this.buildStatusDto({
60
+ config: nextConfig,
61
+ hasPersistedConfig: snapshot.hasPersistedConfig || nextConfig !== snapshot.config
62
+ }, nextConfig, this.resolveEffectiveStatus(nextConfig));
63
+ }
64
+ async updateConfig(input) {
65
+ const snapshot = this.readStateSnapshot();
66
+ const nextConfig = {
67
+ ...snapshot.config,
68
+ activated: input.activated !== undefined
69
+ ? input.activated
70
+ : snapshot.config.activated,
71
+ relayBaseUrl: input.relayBaseUrl !== undefined
72
+ ? normalizeWebsocketBaseUrl(input.relayBaseUrl, "relayBaseUrl")
73
+ : snapshot.config.relayBaseUrl,
74
+ controlBaseUrl: input.controlBaseUrl !== undefined
75
+ ? normalizeHttpBaseUrl(input.controlBaseUrl, "controlBaseUrl")
76
+ : snapshot.config.controlBaseUrl,
77
+ localTargetBaseUrl: input.localTargetBaseUrl !== undefined
78
+ ? normalizeHttpBaseUrl(input.localTargetBaseUrl, "localTargetBaseUrl")
79
+ : snapshot.config.localTargetBaseUrl,
80
+ localTargetBaseUrlSource: input.localTargetBaseUrl !== undefined
81
+ ? "custom"
82
+ : (snapshot.config.localTargetBaseUrlSource ?? "default"),
83
+ enabled: input.activated === false
84
+ ? false
85
+ : snapshot.config.enabled,
86
+ updatedAt: nowIso()
87
+ };
88
+ this.repository.upsertConfig(nextConfig);
89
+ const effectiveConfig = this.resolveConfigWithIdentity(nextConfig);
90
+ if (!effectiveConfig.activated) {
91
+ const nextStatus = buildSkeletonStatus("disabled", effectiveConfig, {
92
+ observedAt: nowIso()
93
+ });
94
+ this.repository.upsertStatus(nextStatus);
95
+ this.taskManager.cancel(HOST_TASK_TYPES.relayTunnelConnect, "default", "relay_tunnel_deactivated");
96
+ await this.runtimeAdapter.disconnect?.("relay_tunnel_deactivated");
97
+ return this.buildStatusDto({
98
+ config: effectiveConfig,
99
+ hasPersistedConfig: true
100
+ }, effectiveConfig, nextStatus);
101
+ }
102
+ if (effectiveConfig.enabled && isBound(effectiveConfig) && this.isBootstrapInitialized()) {
103
+ this.requestReconnect("relay_tunnel.config_update");
104
+ }
105
+ return this.buildStatusDto({
106
+ config: effectiveConfig,
107
+ hasPersistedConfig: true
108
+ }, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
109
+ }
110
+ async loginControl(input) {
111
+ const snapshot = this.readStateSnapshot();
112
+ const controlBaseUrl = requireConfiguredControlBaseUrl(snapshot.config);
113
+ const email = normalizeRequiredText(input.email, "email");
114
+ const password = normalizeRequiredText(input.password, "password");
115
+ const response = await this.requestControlApi({
116
+ controlBaseUrl,
117
+ path: "/api/public/auth/login",
118
+ method: "POST",
119
+ headers: {
120
+ "Content-Type": "application/json"
121
+ },
122
+ body: JSON.stringify({
123
+ email,
124
+ password
125
+ }),
126
+ failurePrefix: "控制站登录失败"
127
+ });
128
+ const timestamp = nowIso();
129
+ const nextConfig = {
130
+ ...snapshot.config,
131
+ accountId: response.account.accountId,
132
+ controlAccessTokenCiphertext: encryptSecret(this.controlSessionSecret, response.accessToken),
133
+ controlAccountEmail: response.account.email.trim(),
134
+ controlSessionExpiresAt: normalizeOptionalText(response.expiresAt),
135
+ updatedAt: timestamp
136
+ };
137
+ this.repository.upsertConfig(nextConfig);
138
+ return this.buildStatusDto({
139
+ config: nextConfig,
140
+ hasPersistedConfig: true
141
+ }, this.resolveConfigWithIdentity(nextConfig), this.resolveEffectiveStatus(nextConfig));
142
+ }
143
+ async logoutControl() {
144
+ const snapshot = this.readStateSnapshot();
145
+ const nextConfig = clearRelayTunnelControlSession(snapshot.config, {
146
+ clearAccountId: !snapshot.config.bindingId,
147
+ updatedAt: nowIso()
148
+ });
149
+ this.repository.upsertConfig(nextConfig);
150
+ return this.buildStatusDto({
151
+ config: nextConfig,
152
+ hasPersistedConfig: true
153
+ }, this.resolveConfigWithIdentity(nextConfig), this.resolveEffectiveStatus(nextConfig));
154
+ }
155
+ async checkHostLabelAvailability(hostLabel) {
156
+ const snapshot = this.readStateSnapshot();
157
+ const normalizedHostLabel = normalizeRequiredText(hostLabel, "hostLabel");
158
+ const { controlBaseUrl, accessToken } = this.requireControlSession(snapshot.config);
159
+ const path = `/api/v1/hosts/availability?hostLabel=${encodeURIComponent(normalizedHostLabel)}`;
160
+ return await this.requestControlApi({
161
+ controlBaseUrl,
162
+ path,
163
+ method: "GET",
164
+ headers: {
165
+ Authorization: `Bearer ${accessToken}`
166
+ },
167
+ failurePrefix: "检查 Host 名称失败"
168
+ });
169
+ }
170
+ async bindControlHost(hostLabel) {
171
+ const snapshot = this.readStateSnapshot();
172
+ if (snapshot.config.bindingId && snapshot.config.tunnelDomain) {
173
+ const effectiveConfig = this.resolveConfigWithIdentity(snapshot.config);
174
+ return this.buildStatusDto(snapshot, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
175
+ }
176
+ const normalizedHostLabel = normalizeRequiredText(hostLabel, "hostLabel");
177
+ const { controlBaseUrl, accessToken, accountId } = this.requireControlSession(snapshot.config);
178
+ const identity = this.identityService.ensureIdentity();
179
+ const bindResponse = await this.requestControlApi({
180
+ controlBaseUrl,
181
+ path: "/api/v1/hosts/bind",
182
+ method: "POST",
183
+ headers: {
184
+ "Content-Type": "application/json",
185
+ Authorization: `Bearer ${accessToken}`
186
+ },
187
+ body: JSON.stringify({
188
+ hostLabel: normalizedHostLabel,
189
+ hostPublicKey: identity.publicKeyPem,
190
+ hostFingerprint: identity.keyFingerprint
191
+ }),
192
+ failurePrefix: "绑定 Host 失败"
193
+ });
194
+ return await this.bind({
195
+ accountId,
196
+ bindingId: bindResponse.binding.bindingId,
197
+ tunnelDomain: bindResponse.binding.tunnelDomain,
198
+ relayBaseUrl: bindResponse.binding.relayBaseUrl,
199
+ controlBaseUrl
200
+ });
201
+ }
202
+ async getTrafficWallet() {
203
+ const snapshot = this.readStateSnapshot();
204
+ const { controlBaseUrl, accessToken } = this.requireControlSession(snapshot.config);
205
+ const response = await this.requestControlApi({
206
+ controlBaseUrl,
207
+ path: "/api/v1/traffic-wallet/me",
208
+ method: "GET",
209
+ headers: {
210
+ Authorization: `Bearer ${accessToken}`
211
+ },
212
+ failurePrefix: "读取控制站流量信息失败"
213
+ });
214
+ return response.wallet;
215
+ }
216
+ async bind(input) {
217
+ const snapshot = this.readStateSnapshot();
218
+ const accountId = normalizeRequiredText(input.accountId, "accountId");
219
+ const bindingId = normalizeRequiredText(input.bindingId, "bindingId");
220
+ const tunnelDomain = normalizeTunnelDomain(input.tunnelDomain, "tunnelDomain");
221
+ const identity = this.identityService.ensureIdentity();
222
+ const relayBaseUrl = input.relayBaseUrl !== undefined
223
+ ? normalizeWebsocketBaseUrl(input.relayBaseUrl, "relayBaseUrl")
224
+ : snapshot.config.relayBaseUrl;
225
+ const controlBaseUrl = input.controlBaseUrl !== undefined
226
+ ? normalizeHttpBaseUrl(input.controlBaseUrl, "controlBaseUrl")
227
+ : snapshot.config.controlBaseUrl;
228
+ if (!relayBaseUrl || !controlBaseUrl) {
229
+ throw new AppError({
230
+ statusCode: 400,
231
+ errorCode: "INVALID_INPUT",
232
+ detail: "绑定前必须提供 relayBaseUrl 和 controlBaseUrl"
233
+ });
234
+ }
235
+ const timestamp = nowIso();
236
+ const nextConfig = {
237
+ ...snapshot.config,
238
+ relayBaseUrl,
239
+ controlBaseUrl,
240
+ accountId,
241
+ tunnelDomain,
242
+ bindingId,
243
+ hostPublicKey: identity.publicKeyPem,
244
+ hostKeyFingerprint: identity.keyFingerprint,
245
+ updatedAt: timestamp
246
+ };
247
+ const nextStatus = buildSkeletonStatus(nextConfig.enabled
248
+ ? (this.isBootstrapInitialized() ? "connecting" : "blocked_uninitialized")
249
+ : "disabled", nextConfig, {
250
+ observedAt: timestamp
251
+ });
252
+ this.db.transaction(() => {
253
+ this.repository.upsertConfig(nextConfig);
254
+ this.repository.upsertStatus(nextStatus);
255
+ })();
256
+ if (nextConfig.enabled && this.isBootstrapInitialized()) {
257
+ this.requestReconnect("relay_tunnel.bind");
258
+ }
259
+ return this.buildStatusDto({
260
+ config: nextConfig,
261
+ hasPersistedConfig: true
262
+ }, nextConfig, this.resolveEffectiveStatus(nextConfig));
263
+ }
264
+ async unbind() {
265
+ const snapshot = this.readStateSnapshot();
266
+ const timestamp = nowIso();
267
+ const nextConfig = clearRelayTunnelControlSession({
268
+ ...snapshot.config,
269
+ enabled: false,
270
+ bindingId: null,
271
+ tunnelDomain: null,
272
+ updatedAt: timestamp
273
+ }, {
274
+ clearAccountId: true,
275
+ updatedAt: timestamp
276
+ });
277
+ const nextStatus = buildSkeletonStatus("disabled", nextConfig, {
278
+ observedAt: timestamp
279
+ });
280
+ this.taskManager.cancel(HOST_TASK_TYPES.relayTunnelConnect, "default", "relay_tunnel_unbound");
281
+ await this.runtimeAdapter.disconnect?.("relay_tunnel_unbound");
282
+ this.db.transaction(() => {
283
+ this.repository.upsertConfig(nextConfig);
284
+ this.repository.upsertStatus(nextStatus);
285
+ })();
286
+ return this.buildStatusDto({
287
+ config: nextConfig,
288
+ hasPersistedConfig: true
289
+ }, nextConfig, nextStatus);
290
+ }
291
+ async enable() {
292
+ const snapshot = this.readStateSnapshot();
293
+ if (!isBound(snapshot.config)) {
294
+ throw new AppError({
295
+ statusCode: 409,
296
+ errorCode: "RELAY_TUNNEL_NOT_BOUND",
297
+ detail: "当前实例还没有绑定公共隧道"
298
+ });
299
+ }
300
+ const timestamp = nowIso();
301
+ const nextConfig = {
302
+ ...snapshot.config,
303
+ activated: true,
304
+ enabled: true,
305
+ updatedAt: timestamp
306
+ };
307
+ const configWithIdentity = this.syncIdentityIntoConfig(nextConfig);
308
+ const nextStatus = buildSkeletonStatus(this.isBootstrapInitialized() ? "connecting" : "blocked_uninitialized", configWithIdentity, {
309
+ observedAt: timestamp
310
+ });
311
+ this.db.transaction(() => {
312
+ this.repository.upsertConfig(configWithIdentity);
313
+ this.repository.upsertStatus(nextStatus);
314
+ })();
315
+ if (this.isBootstrapInitialized()) {
316
+ this.requestReconnect("relay_tunnel.enable");
317
+ }
318
+ return this.buildStatusDto({
319
+ config: configWithIdentity,
320
+ hasPersistedConfig: true
321
+ }, configWithIdentity, nextStatus);
322
+ }
323
+ async disable() {
324
+ const snapshot = this.readStateSnapshot();
325
+ const timestamp = nowIso();
326
+ const nextConfig = {
327
+ ...snapshot.config,
328
+ enabled: false,
329
+ updatedAt: timestamp
330
+ };
331
+ const nextStatus = buildSkeletonStatus("disabled", nextConfig, {
332
+ observedAt: timestamp
333
+ });
334
+ this.db.transaction(() => {
335
+ this.repository.upsertConfig(nextConfig);
336
+ this.repository.upsertStatus(nextStatus);
337
+ })();
338
+ this.taskManager.cancel(HOST_TASK_TYPES.relayTunnelConnect, "default", "relay_tunnel_disabled");
339
+ await this.runtimeAdapter.disconnect?.("relay_tunnel_disabled");
340
+ return this.buildStatusDto({
341
+ config: nextConfig,
342
+ hasPersistedConfig: true
343
+ }, nextConfig, nextStatus);
344
+ }
345
+ requestReconnect(source = "relay_tunnel.reconnect") {
346
+ return this.taskManager.enqueue(HOST_TASK_TYPES.relayTunnelConnect, {
347
+ key: "default",
348
+ source,
349
+ input: {
350
+ source
351
+ }
352
+ });
353
+ }
354
+ readStateSnapshot() {
355
+ const persistedConfig = this.reconcileLegacyLocalTargetBaseUrl(this.repository.findConfig());
356
+ return {
357
+ config: persistedConfig
358
+ ?? {
359
+ activated: false,
360
+ enabled: false,
361
+ provider: "codingns_relay",
362
+ relayBaseUrl: null,
363
+ controlBaseUrl: null,
364
+ controlAccessTokenCiphertext: null,
365
+ controlAccountEmail: null,
366
+ controlSessionExpiresAt: null,
367
+ accountId: null,
368
+ tunnelDomain: null,
369
+ bindingId: null,
370
+ hostPublicKey: null,
371
+ hostKeyFingerprint: null,
372
+ localTargetBaseUrl: this.defaultLocalTargetBaseUrl,
373
+ localTargetBaseUrlSource: "default",
374
+ updatedAt: nowIso()
375
+ },
376
+ hasPersistedConfig: persistedConfig !== null
377
+ };
378
+ }
379
+ reconcileLegacyLocalTargetBaseUrl(config) {
380
+ if (!config) {
381
+ return config;
382
+ }
383
+ if ((config.localTargetBaseUrlSource ?? "default") !== "default") {
384
+ return config;
385
+ }
386
+ if (config.localTargetBaseUrl === this.defaultLocalTargetBaseUrl) {
387
+ return config;
388
+ }
389
+ // `default` 源的目标地址由当前运行模式决定,不应该把历史默认值永久粘在库里。
390
+ // 只要默认入口变化了,就在启动时自动收敛到新的默认值;用户显式写入的 custom 配置不动。
391
+ const migratedConfig = {
392
+ ...config,
393
+ localTargetBaseUrl: this.defaultLocalTargetBaseUrl,
394
+ localTargetBaseUrlSource: "default",
395
+ updatedAt: nowIso()
396
+ };
397
+ this.repository.upsertConfig(migratedConfig);
398
+ return migratedConfig;
399
+ }
400
+ resolveEffectiveStatus(config) {
401
+ const persisted = this.repository.findStatus();
402
+ if (!config.activated || !config.enabled) {
403
+ return buildSkeletonStatus("disabled", config, {
404
+ observedAt: persisted?.observedAt ?? null
405
+ });
406
+ }
407
+ if (!isBound(config)) {
408
+ return buildSkeletonStatus("unbound", config, {
409
+ observedAt: persisted?.observedAt ?? null
410
+ });
411
+ }
412
+ if (!this.isBootstrapInitialized()) {
413
+ return buildSkeletonStatus("blocked_uninitialized", config, {
414
+ observedAt: persisted?.observedAt ?? null
415
+ });
416
+ }
417
+ if (!persisted
418
+ || persisted.phase === "disabled"
419
+ || persisted.phase === "unbound"
420
+ || persisted.phase === "blocked_uninitialized") {
421
+ return buildSkeletonStatus("connecting", config, {
422
+ observedAt: persisted?.observedAt ?? null
423
+ });
424
+ }
425
+ return {
426
+ ...persisted,
427
+ bindingId: config.bindingId,
428
+ tunnelDomain: config.tunnelDomain,
429
+ hostFingerprint: config.hostKeyFingerprint
430
+ };
431
+ }
432
+ registerBackgroundTasks() {
433
+ if (this.taskManager.has(HOST_TASK_TYPES.relayTunnelConnect)) {
434
+ return;
435
+ }
436
+ this.taskManager.register({
437
+ taskType: HOST_TASK_TYPES.relayTunnelConnect,
438
+ executionLane: "host_background",
439
+ timeoutMs: 15_000,
440
+ run: async (_input, context) => await this.runConnectTask(context.signal)
441
+ });
442
+ }
443
+ async runConnectTask(signal) {
444
+ const snapshot = this.readStateSnapshot();
445
+ if (!snapshot.config.enabled || !isBound(snapshot.config)) {
446
+ const effectiveConfig = this.resolveConfigWithIdentity(snapshot.config);
447
+ return this.buildStatusDto(snapshot, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
448
+ }
449
+ const effectiveConfig = this.syncIdentityIntoConfig(snapshot.config);
450
+ if (!this.isBootstrapInitialized()) {
451
+ const blockedStatus = buildSkeletonStatus("blocked_uninitialized", effectiveConfig, {
452
+ observedAt: nowIso()
453
+ });
454
+ this.repository.upsertStatus(blockedStatus);
455
+ return this.buildStatusDto(snapshot, effectiveConfig, blockedStatus);
456
+ }
457
+ try {
458
+ const nextStatus = await this.runtimeAdapter.connect(effectiveConfig, signal);
459
+ if (signal.aborted) {
460
+ const latestSnapshot = this.readStateSnapshot();
461
+ const effectiveConfig = this.resolveConfigWithIdentity(latestSnapshot.config);
462
+ return this.buildStatusDto(latestSnapshot, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
463
+ }
464
+ this.repository.upsertStatus(nextStatus);
465
+ return this.buildStatusDto(snapshot, effectiveConfig, nextStatus);
466
+ }
467
+ catch (error) {
468
+ if (signal.aborted) {
469
+ const latestSnapshot = this.readStateSnapshot();
470
+ const effectiveConfig = this.resolveConfigWithIdentity(latestSnapshot.config);
471
+ return this.buildStatusDto(latestSnapshot, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
472
+ }
473
+ const failedStatus = {
474
+ ...buildSkeletonStatus("error", snapshot.config, {
475
+ observedAt: nowIso()
476
+ }),
477
+ lastError: error instanceof Error ? error.message : String(error)
478
+ };
479
+ this.repository.upsertStatus(failedStatus);
480
+ return this.buildStatusDto(snapshot, effectiveConfig, failedStatus);
481
+ }
482
+ }
483
+ buildStatusDto(snapshot, effectiveConfig, effectiveStatus) {
484
+ return {
485
+ activated: effectiveConfig.activated,
486
+ enabled: effectiveConfig.enabled,
487
+ provider: effectiveConfig.provider,
488
+ relayBaseUrl: effectiveConfig.relayBaseUrl,
489
+ controlBaseUrl: effectiveConfig.controlBaseUrl,
490
+ controlAccountEmail: effectiveConfig.controlAccountEmail,
491
+ controlSessionExpiresAt: effectiveConfig.controlSessionExpiresAt,
492
+ accountId: effectiveConfig.accountId,
493
+ tunnelDomain: effectiveConfig.tunnelDomain,
494
+ bindingId: effectiveConfig.bindingId,
495
+ hostPublicKey: effectiveConfig.hostPublicKey,
496
+ hostKeyFingerprint: effectiveConfig.hostKeyFingerprint,
497
+ localTargetBaseUrl: effectiveConfig.localTargetBaseUrl,
498
+ candidateEndpoints: buildHostCandidateEndpoints(effectiveConfig),
499
+ phase: effectiveStatus.phase,
500
+ connected: effectiveStatus.connected,
501
+ hostFingerprint: effectiveStatus.hostFingerprint,
502
+ trafficUsedBytes: effectiveStatus.trafficUsedBytes,
503
+ trafficRemainingBytes: effectiveStatus.trafficRemainingBytes,
504
+ quotaResetAt: effectiveStatus.quotaResetAt,
505
+ lastError: effectiveStatus.lastError,
506
+ observedAt: effectiveStatus.observedAt,
507
+ updatedAt: snapshot.hasPersistedConfig ? snapshot.config.updatedAt : null
508
+ };
509
+ }
510
+ resolveConfigWithIdentity(config) {
511
+ const identity = this.identityService.getIdentity();
512
+ if (!identity) {
513
+ return config;
514
+ }
515
+ return {
516
+ ...config,
517
+ hostPublicKey: identity.publicKeyPem,
518
+ hostKeyFingerprint: identity.keyFingerprint
519
+ };
520
+ }
521
+ syncIdentityIntoConfig(config) {
522
+ const identity = this.identityService.ensureIdentity();
523
+ if (config.hostPublicKey === identity.publicKeyPem
524
+ && config.hostKeyFingerprint === identity.keyFingerprint) {
525
+ return config;
526
+ }
527
+ const nextConfig = {
528
+ ...config,
529
+ hostPublicKey: identity.publicKeyPem,
530
+ hostKeyFingerprint: identity.keyFingerprint
531
+ };
532
+ this.repository.upsertConfig(nextConfig);
533
+ return nextConfig;
534
+ }
535
+ isBootstrapInitialized() {
536
+ return this.bootstrapStateRepository.getState().initialized;
537
+ }
538
+ requireControlSession(config) {
539
+ const controlBaseUrl = requireConfiguredControlBaseUrl(config);
540
+ const encryptedAccessToken = normalizeOptionalText(config.controlAccessTokenCiphertext);
541
+ const accountId = normalizeOptionalText(config.accountId);
542
+ if (!encryptedAccessToken || !accountId) {
543
+ throw new AppError({
544
+ statusCode: 409,
545
+ errorCode: "RELAY_TUNNEL_CONTROL_SESSION_REQUIRED",
546
+ detail: "当前还没有登录控制站账号"
547
+ });
548
+ }
549
+ try {
550
+ return {
551
+ controlBaseUrl,
552
+ accessToken: decryptSecret(this.controlSessionSecret, encryptedAccessToken),
553
+ accountId
554
+ };
555
+ }
556
+ catch {
557
+ const nextConfig = clearRelayTunnelControlSession(config, {
558
+ clearAccountId: !config.bindingId,
559
+ updatedAt: nowIso()
560
+ });
561
+ this.repository.upsertConfig(nextConfig);
562
+ throw new AppError({
563
+ statusCode: 409,
564
+ errorCode: "RELAY_TUNNEL_CONTROL_SESSION_REQUIRED",
565
+ detail: "控制站登录态已失效,请重新登录"
566
+ });
567
+ }
568
+ }
569
+ async requestControlApi(input) {
570
+ let response;
571
+ try {
572
+ response = await this.fetchFn(new URL(input.path, ensureTrailingSlash(input.controlBaseUrl)), {
573
+ method: input.method,
574
+ headers: input.headers,
575
+ body: input.body
576
+ });
577
+ }
578
+ catch (error) {
579
+ throw buildControlFetchError(error, input.controlBaseUrl, input.failurePrefix);
580
+ }
581
+ if (!response.ok) {
582
+ throw await buildControlApiError(response, input.controlBaseUrl, input.failurePrefix);
583
+ }
584
+ return await response.json();
585
+ }
586
+ }
587
+ class NoopRelayTunnelRuntimeAdapter {
588
+ async connect(config, _signal) {
589
+ return buildSkeletonStatus("connecting", config, {
590
+ observedAt: nowIso()
591
+ });
592
+ }
593
+ }
594
+ function buildSkeletonStatus(phase, config, overrides) {
595
+ return {
596
+ phase,
597
+ connected: false,
598
+ bindingId: config.bindingId,
599
+ tunnelDomain: config.tunnelDomain,
600
+ hostFingerprint: config.hostKeyFingerprint,
601
+ trafficUsedBytes: null,
602
+ trafficRemainingBytes: null,
603
+ quotaResetAt: null,
604
+ lastError: null,
605
+ observedAt: overrides?.observedAt ?? null
606
+ };
607
+ }
608
+ function buildHostCandidateEndpoints(config) {
609
+ const endpoints = new Map();
610
+ const relayEndpoint = buildRelayPublicUrl(config);
611
+ if (relayEndpoint) {
612
+ endpoints.set(relayEndpoint, {
613
+ endpointId: `relay:${relayEndpoint}`,
614
+ kind: "relay",
615
+ url: relayEndpoint,
616
+ priority: 400,
617
+ expiresAt: null,
618
+ source: "host_reported"
619
+ });
620
+ }
621
+ for (const localCandidateUrl of buildLocalCandidateUrls(config.localTargetBaseUrl)) {
622
+ endpoints.set(localCandidateUrl, {
623
+ endpointId: `host_reported:${localCandidateUrl}`,
624
+ kind: classifyCandidateEndpointKind(localCandidateUrl),
625
+ url: localCandidateUrl,
626
+ priority: resolveCandidateEndpointPriority(localCandidateUrl),
627
+ expiresAt: null,
628
+ source: "host_reported"
629
+ });
630
+ }
631
+ return Array.from(endpoints.values()).sort((left, right) => {
632
+ if (left.priority !== right.priority) {
633
+ return left.priority - right.priority;
634
+ }
635
+ return left.url.localeCompare(right.url);
636
+ });
637
+ }
638
+ function buildRelayPublicUrl(config) {
639
+ if (!config.tunnelDomain || !config.controlBaseUrl) {
640
+ return null;
641
+ }
642
+ try {
643
+ const controlUrl = new URL(config.controlBaseUrl);
644
+ controlUrl.hostname = config.tunnelDomain.trim().toLowerCase();
645
+ controlUrl.pathname = "/";
646
+ controlUrl.search = "";
647
+ controlUrl.hash = "";
648
+ return controlUrl.toString().replace(/\/$/, "");
649
+ }
650
+ catch {
651
+ return null;
652
+ }
653
+ }
654
+ function buildLocalCandidateUrls(localTargetBaseUrl) {
655
+ let parsed;
656
+ try {
657
+ parsed = new URL(localTargetBaseUrl);
658
+ }
659
+ catch {
660
+ return [];
661
+ }
662
+ const candidates = new Set();
663
+ const hostname = parsed.hostname.trim().toLowerCase();
664
+ candidates.add(normalizeUrlWithoutTrailingSlash(parsed.toString()));
665
+ if (hostname === "0.0.0.0" || hostname === "::" || hostname === "::0") {
666
+ for (const networkAddress of listPrivateIpv4Addresses()) {
667
+ const candidateUrl = new URL(parsed.toString());
668
+ candidateUrl.hostname = networkAddress;
669
+ candidates.add(normalizeUrlWithoutTrailingSlash(candidateUrl.toString()));
670
+ }
671
+ }
672
+ if (hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1") {
673
+ for (const networkAddress of listPrivateIpv4Addresses()) {
674
+ const candidateUrl = new URL(parsed.toString());
675
+ candidateUrl.hostname = networkAddress;
676
+ candidates.add(normalizeUrlWithoutTrailingSlash(candidateUrl.toString()));
677
+ }
678
+ }
679
+ return Array.from(candidates);
680
+ }
681
+ function listPrivateIpv4Addresses() {
682
+ const interfaces = os.networkInterfaces();
683
+ const candidates = new Set();
684
+ for (const entries of Object.values(interfaces)) {
685
+ for (const entry of entries ?? []) {
686
+ if (!entry || entry.family !== "IPv4" || entry.internal) {
687
+ continue;
688
+ }
689
+ if (!isPrivateIpv4Address(entry.address)) {
690
+ continue;
691
+ }
692
+ candidates.add(entry.address);
693
+ }
694
+ }
695
+ return Array.from(candidates).sort();
696
+ }
697
+ function isPrivateIpv4Address(address) {
698
+ return (/^10\./.test(address)
699
+ || /^192\.168\./.test(address)
700
+ || /^172\.(1[6-9]|2\d|3[0-1])\./.test(address));
701
+ }
702
+ function classifyCandidateEndpointKind(candidateUrl) {
703
+ try {
704
+ const hostname = new URL(candidateUrl).hostname.toLowerCase();
705
+ if (hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1") {
706
+ return "loopback";
707
+ }
708
+ if (isPrivateIpv4Address(hostname)) {
709
+ return "lan";
710
+ }
711
+ return "custom";
712
+ }
713
+ catch {
714
+ return "custom";
715
+ }
716
+ }
717
+ function resolveCandidateEndpointPriority(candidateUrl) {
718
+ const kind = classifyCandidateEndpointKind(candidateUrl);
719
+ switch (kind) {
720
+ case "loopback":
721
+ return 100;
722
+ case "lan":
723
+ return 200;
724
+ case "tailscale":
725
+ return 300;
726
+ case "relay":
727
+ return 400;
728
+ default:
729
+ return 500;
730
+ }
731
+ }
732
+ function normalizeUrlWithoutTrailingSlash(value) {
733
+ return value.endsWith("/") ? value.slice(0, -1) : value;
734
+ }
735
+ function isBound(config) {
736
+ return Boolean(config.bindingId && config.tunnelDomain);
737
+ }
738
+ function normalizeRequiredText(value, field) {
739
+ const normalized = value?.trim();
740
+ if (!normalized) {
741
+ throw new AppError({
742
+ statusCode: 400,
743
+ errorCode: "INVALID_INPUT",
744
+ detail: `${field} 不能为空`,
745
+ field
746
+ });
747
+ }
748
+ return normalized;
749
+ }
750
+ function normalizeOptionalText(value) {
751
+ const normalized = value?.trim();
752
+ return normalized ? normalized : null;
753
+ }
754
+ function normalizeTunnelDomain(value, field) {
755
+ const normalized = normalizeRequiredText(value, field).toLowerCase();
756
+ if (!/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(normalized)) {
757
+ throw new AppError({
758
+ statusCode: 400,
759
+ errorCode: "INVALID_INPUT",
760
+ detail: "tunnelDomain 必须是合法域名",
761
+ field
762
+ });
763
+ }
764
+ return normalized;
765
+ }
766
+ function normalizeHttpBaseUrl(value, field) {
767
+ if (value === null || value === undefined) {
768
+ return value ?? null;
769
+ }
770
+ const normalized = value.trim();
771
+ if (normalized.length === 0) {
772
+ return null;
773
+ }
774
+ let parsed;
775
+ try {
776
+ parsed = new URL(normalized);
777
+ }
778
+ catch {
779
+ throw new AppError({
780
+ statusCode: 400,
781
+ errorCode: "INVALID_INPUT",
782
+ detail: `${field} 必须是合法的 http 或 https 地址`,
783
+ field
784
+ });
785
+ }
786
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
787
+ throw new AppError({
788
+ statusCode: 400,
789
+ errorCode: "INVALID_INPUT",
790
+ detail: `${field} 只允许使用 http 或 https 协议`,
791
+ field
792
+ });
793
+ }
794
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
795
+ throw new AppError({
796
+ statusCode: 400,
797
+ errorCode: "INVALID_INPUT",
798
+ detail: `${field} 不能包含账号、查询参数或 hash`,
799
+ field
800
+ });
801
+ }
802
+ const pathname = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "");
803
+ return `${parsed.protocol}//${parsed.host}${pathname}`;
804
+ }
805
+ function normalizeWebsocketBaseUrl(value, field) {
806
+ if (value === null || value === undefined) {
807
+ return value ?? null;
808
+ }
809
+ const normalized = value.trim();
810
+ if (normalized.length === 0) {
811
+ return null;
812
+ }
813
+ let parsed;
814
+ try {
815
+ parsed = new URL(normalized);
816
+ }
817
+ catch {
818
+ throw new AppError({
819
+ statusCode: 400,
820
+ errorCode: "INVALID_INPUT",
821
+ detail: `${field} 必须是合法的 ws、wss、http 或 https 地址`,
822
+ field
823
+ });
824
+ }
825
+ if (parsed.protocol !== "ws:"
826
+ && parsed.protocol !== "wss:"
827
+ && parsed.protocol !== "http:"
828
+ && parsed.protocol !== "https:") {
829
+ throw new AppError({
830
+ statusCode: 400,
831
+ errorCode: "INVALID_INPUT",
832
+ detail: `${field} 只允许使用 ws、wss、http 或 https 协议`,
833
+ field
834
+ });
835
+ }
836
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
837
+ throw new AppError({
838
+ statusCode: 400,
839
+ errorCode: "INVALID_INPUT",
840
+ detail: `${field} 不能包含账号、查询参数或 hash`,
841
+ field
842
+ });
843
+ }
844
+ const pathname = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "");
845
+ const normalizedProtocol = parsed.protocol === "https:"
846
+ ? "wss:"
847
+ : parsed.protocol === "http:"
848
+ ? "ws:"
849
+ : parsed.protocol;
850
+ return `${normalizedProtocol}//${parsed.host}${pathname}`;
851
+ }
852
+ function requireConfiguredControlBaseUrl(config) {
853
+ const controlBaseUrl = normalizeOptionalText(config.controlBaseUrl);
854
+ if (!controlBaseUrl) {
855
+ throw new AppError({
856
+ statusCode: 409,
857
+ errorCode: "RELAY_TUNNEL_CONTROL_BASE_URL_REQUIRED",
858
+ detail: "当前还没有配置控制站点地址"
859
+ });
860
+ }
861
+ return controlBaseUrl;
862
+ }
863
+ function ensureTrailingSlash(value) {
864
+ return value.endsWith("/") ? value : `${value}/`;
865
+ }
866
+ function clearRelayTunnelControlSession(config, options) {
867
+ return {
868
+ ...config,
869
+ controlAccessTokenCiphertext: null,
870
+ controlAccountEmail: null,
871
+ controlSessionExpiresAt: null,
872
+ accountId: options.clearAccountId ? null : config.accountId,
873
+ updatedAt: options.updatedAt
874
+ };
875
+ }
876
+ async function buildControlApiError(response, controlBaseUrl, failurePrefix) {
877
+ const detail = await readControlApiErrorDetail(response);
878
+ if (response.status === 401 || response.status === 403) {
879
+ return new AppError({
880
+ statusCode: response.status,
881
+ errorCode: "RELAY_TUNNEL_CONTROL_ACCESS_DENIED",
882
+ detail: `${failurePrefix}:控制站 ${controlBaseUrl} 拒绝了这次请求(HTTP ${response.status})。`
883
+ + ` 请确认这是正确的控制站地址,并检查账号、密码或访问权限。`
884
+ + appendControlApiDetail(detail)
885
+ });
886
+ }
887
+ if (response.status === 404) {
888
+ return new AppError({
889
+ statusCode: 404,
890
+ errorCode: "RELAY_TUNNEL_CONTROL_ENDPOINT_NOT_FOUND",
891
+ detail: `${failurePrefix}:控制站 ${controlBaseUrl} 上没有这个接口(HTTP 404)。`
892
+ + " 这通常说明地址写错了,或者目标服务不是 CodingNS 控制站。"
893
+ + appendControlApiDetail(detail)
894
+ });
895
+ }
896
+ return new AppError({
897
+ statusCode: response.status,
898
+ errorCode: "RELAY_TUNNEL_CONTROL_API_ERROR",
899
+ detail: `${failurePrefix}:控制站 ${controlBaseUrl} 返回了异常响应(HTTP ${response.status})。`
900
+ + appendControlApiDetail(detail)
901
+ });
902
+ }
903
+ function buildControlFetchError(error, controlBaseUrl, failurePrefix) {
904
+ return new AppError({
905
+ statusCode: 502,
906
+ errorCode: "RELAY_TUNNEL_CONTROL_UNREACHABLE",
907
+ detail: `${failurePrefix}:无法连接到控制站 ${controlBaseUrl}。`
908
+ + " 请确认服务地址、端口和网络连接是否正确。"
909
+ + appendControlApiDetail(resolveFetchErrorDetail(error))
910
+ });
911
+ }
912
+ async function readControlApiErrorDetail(response) {
913
+ const contentType = response.headers.get("content-type") ?? "";
914
+ if (contentType.includes("application/json")) {
915
+ try {
916
+ const payload = await response.json();
917
+ const detail = readJsonErrorText(payload.detail)
918
+ ?? readJsonErrorText(payload.message)
919
+ ?? readJsonErrorText(payload.error);
920
+ if (detail) {
921
+ return detail;
922
+ }
923
+ }
924
+ catch {
925
+ // 忽略 JSON 解析失败,回退到纯文本。
926
+ }
927
+ }
928
+ const text = normalizeOptionalText(await response.text());
929
+ return text ?? `HTTP ${response.status}`;
930
+ }
931
+ function readJsonErrorText(value) {
932
+ return typeof value === "string" ? normalizeOptionalText(value) : null;
933
+ }
934
+ function resolveFetchErrorDetail(error) {
935
+ if (error instanceof Error) {
936
+ const code = readFetchErrorCode(error);
937
+ if (code === "ECONNREFUSED") {
938
+ return "连接被目标服务器拒绝。";
939
+ }
940
+ if (code === "ENOTFOUND" || code === "EAI_AGAIN") {
941
+ return "域名无法解析。";
942
+ }
943
+ if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT") {
944
+ return "连接超时。";
945
+ }
946
+ if (code === "CERT_HAS_EXPIRED" || code === "DEPTH_ZERO_SELF_SIGNED_CERT") {
947
+ return "TLS 证书无效。";
948
+ }
949
+ return normalizeOptionalText(error.message);
950
+ }
951
+ return null;
952
+ }
953
+ function readFetchErrorCode(error) {
954
+ const cause = "cause" in error
955
+ ? error.cause
956
+ : undefined;
957
+ return typeof cause?.code === "string" ? cause.code : null;
958
+ }
959
+ function appendControlApiDetail(detail) {
960
+ const normalized = normalizeOptionalText(detail);
961
+ if (!normalized) {
962
+ return "";
963
+ }
964
+ return ` 详情:${normalized}`;
965
+ }
966
+ //# sourceMappingURL=relay-tunnel-service.js.map