@jingyi0605/codingns 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) 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-R-XZw7pd.js +3 -0
  4. package/dist/public/assets/App-DUAg5urj.css +1 -0
  5. package/dist/public/assets/App-DkvE5EyM.js +30 -0
  6. package/dist/public/assets/BootstrapPage-Vu5oEJ8z.js +1 -0
  7. package/dist/public/assets/ConversationPage-Cjpg6g0J.js +2 -0
  8. package/dist/public/assets/DesktopDetachPreviewPage-BgeEqbc5.js +1 -0
  9. package/dist/public/assets/DesktopWindowPage-1WelvxdH.js +2 -0
  10. package/dist/public/assets/FileContextPanel-D_ghXJuW.js +1 -0
  11. package/dist/public/assets/GitSidebar-D9f9Jxwr.js +6 -0
  12. package/dist/public/assets/MobileCreateSessionSheet-DLq5qPkx.js +1 -0
  13. package/dist/public/assets/MobileSheet-DLg-gX1t.js +1 -0
  14. package/dist/public/assets/MobileTopHeaderFrame-DArgZI7L.js +1 -0
  15. package/dist/public/assets/MobileWorkspaceSwitcherHeader-0ywJKfBQ.js +1 -0
  16. package/dist/public/assets/ServerSettingsModal-izoYMx9U.js +1 -0
  17. package/dist/public/assets/SessionIndexPage-C5aG8FIv.js +1 -0
  18. package/dist/public/assets/SettingsPage-HJIC-P-4.js +1 -0
  19. package/dist/public/assets/TerminalManagerPanel-DpyUTo9k.js +1 -0
  20. package/dist/public/assets/{TerminalPage-6jHZV9Mh.js → TerminalPage-CtKXIU0h.js} +19 -19
  21. package/dist/public/assets/TerminalRuntimeFallbackModal-CRhOQOsT.js +1 -0
  22. package/dist/public/assets/ToolFilesPage-DcYPsS-e.js +1 -0
  23. package/dist/public/assets/ToolGitPage-CsPl89ty.js +1 -0
  24. package/dist/public/assets/ToolProcessesPage-D0dvR8xK.js +1 -0
  25. package/dist/public/assets/ToolsHomePage-4fP-KRiv.js +1 -0
  26. package/dist/public/assets/WorkbenchLandingPage-kvlfyxRo.js +1 -0
  27. package/dist/public/assets/WorkbenchLayout-ByFw4eeu.js +3 -0
  28. package/dist/public/assets/WorkbenchModal-Ctob14VR.js +1 -0
  29. package/dist/public/assets/WorkbenchShellRoute-BUITtdAg.css +1 -0
  30. package/dist/public/assets/WorkbenchShellRoute-Kw7JEZI3.js +1 -0
  31. package/dist/public/assets/WorkspaceDebugDetailPage-Com5kEXJ.js +1 -0
  32. package/dist/public/assets/WorkspaceDetailPage-D0Lrx4Uz.js +1 -0
  33. package/dist/public/assets/WorkspaceHomePage-wR8d3aP9.js +1 -0
  34. package/dist/public/assets/butler-records-events-DgWCG364.js +1 -0
  35. package/dist/public/assets/default-session-permission-mode-CcGwR4Kk.js +1 -0
  36. package/dist/public/assets/event-DvH9tcej.js +1 -0
  37. package/dist/public/assets/file-tree-icon-UFVoVzhM.js +31 -0
  38. package/dist/public/assets/index-Byp9hJ0c.js +42 -0
  39. package/dist/public/assets/index-_52jxu4a.css +1 -0
  40. package/dist/public/assets/preferences-service-KIYeE2gk.js +1 -0
  41. package/dist/public/assets/session-runtime-machine-0KNSSPp5.js +17 -0
  42. package/dist/public/assets/styles-BWPBZvze.css +1 -0
  43. package/dist/public/assets/styles-CSUx5LGe.js +1 -0
  44. package/dist/public/assets/terminal-runtime-meta-AWXJpN4r.js +1 -0
  45. package/dist/public/assets/useRegisteredDebugTemplates-DBDRdptr.js +1 -0
  46. package/dist/public/assets/window-BWqRixxq.js +1 -0
  47. package/dist/public/index.html +2 -2
  48. package/dist/server/middlewares/auth-guard.d.ts +4 -0
  49. package/dist/server/middlewares/auth-guard.js +42 -4
  50. package/dist/server/middlewares/auth-guard.js.map +1 -1
  51. package/dist/server/modules/assistant-capability/assistant-capability-controller.d.ts +62 -1
  52. package/dist/server/modules/assistant-capability/assistant-capability-controller.js +58 -0
  53. package/dist/server/modules/assistant-capability/assistant-capability-controller.js.map +1 -1
  54. package/dist/server/modules/assistant-capability/assistant-capability-service.d.ts +66 -3
  55. package/dist/server/modules/assistant-capability/assistant-capability-service.js +173 -1
  56. package/dist/server/modules/assistant-capability/assistant-capability-service.js.map +1 -1
  57. package/dist/server/modules/auth/auth-controller.d.ts +11 -1
  58. package/dist/server/modules/auth/auth-controller.js +61 -2
  59. package/dist/server/modules/auth/auth-controller.js.map +1 -1
  60. package/dist/server/modules/auth/auth-device-display-name.d.ts +10 -0
  61. package/dist/server/modules/auth/auth-device-display-name.js +190 -0
  62. package/dist/server/modules/auth/auth-device-display-name.js.map +1 -0
  63. package/dist/server/modules/auth/auth-service.d.ts +80 -5
  64. package/dist/server/modules/auth/auth-service.js +333 -23
  65. package/dist/server/modules/auth/auth-service.js.map +1 -1
  66. package/dist/server/modules/butler/assistant-automation-service.d.ts +2 -0
  67. package/dist/server/modules/butler/assistant-automation-service.js +46 -0
  68. package/dist/server/modules/butler/assistant-automation-service.js.map +1 -1
  69. package/dist/server/modules/butler/assistant-sandbox-cleanup-scheduler.d.ts +32 -0
  70. package/dist/server/modules/butler/assistant-sandbox-cleanup-scheduler.js +93 -0
  71. package/dist/server/modules/butler/assistant-sandbox-cleanup-scheduler.js.map +1 -0
  72. package/dist/server/modules/butler/assistant-sandbox-service.d.ts +16 -2
  73. package/dist/server/modules/butler/assistant-sandbox-service.js +137 -4
  74. package/dist/server/modules/butler/assistant-sandbox-service.js.map +1 -1
  75. package/dist/server/modules/butler/butler-auth-service.js +7 -2
  76. package/dist/server/modules/butler/butler-auth-service.js.map +1 -1
  77. package/dist/server/modules/butler/butler-control-session-service.d.ts +4 -1
  78. package/dist/server/modules/butler/butler-control-session-service.js +20 -1
  79. package/dist/server/modules/butler/butler-control-session-service.js.map +1 -1
  80. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.d.ts +2 -1
  81. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.js +27 -25
  82. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.js.map +1 -1
  83. package/dist/server/modules/butler/butler-follow-up-service.d.ts +32 -4
  84. package/dist/server/modules/butler/butler-follow-up-service.js +436 -331
  85. package/dist/server/modules/butler/butler-follow-up-service.js.map +1 -1
  86. package/dist/server/modules/butler/butler-inbox-analysis-service.d.ts +1 -1
  87. package/dist/server/modules/butler/butler-inbox-analysis-service.js.map +1 -1
  88. package/dist/server/modules/butler/butler-inbox-service.js +1 -0
  89. package/dist/server/modules/butler/butler-inbox-service.js.map +1 -1
  90. package/dist/server/modules/butler/butler-session-service.d.ts +3 -1
  91. package/dist/server/modules/butler/butler-session-service.js +15 -1
  92. package/dist/server/modules/butler/butler-session-service.js.map +1 -1
  93. package/dist/server/modules/butler/butler-workspace-context.d.ts +1 -1
  94. package/dist/server/modules/butler/butler-workspace-context.js +54 -28
  95. package/dist/server/modules/butler/butler-workspace-context.js.map +1 -1
  96. package/dist/server/modules/provider/provider-controller.d.ts +1 -1
  97. package/dist/server/modules/provider/provider-controller.js.map +1 -1
  98. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-identity-service.d.ts +10 -0
  99. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-identity-service.js +48 -0
  100. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-identity-service.js.map +1 -0
  101. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.d.ts +48 -0
  102. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.js +11 -0
  103. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.js.map +1 -0
  104. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.d.ts +74 -0
  105. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.js +302 -0
  106. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.js.map +1 -0
  107. package/dist/server/modules/relay-tunnel/relay-tunnel-controller.d.ts +33 -0
  108. package/dist/server/modules/relay-tunnel/relay-tunnel-controller.js +57 -0
  109. package/dist/server/modules/relay-tunnel/relay-tunnel-controller.js.map +1 -0
  110. package/dist/server/modules/relay-tunnel/relay-tunnel-edge-proof.d.ts +9 -0
  111. package/dist/server/modules/relay-tunnel/relay-tunnel-edge-proof.js +25 -0
  112. package/dist/server/modules/relay-tunnel/relay-tunnel-edge-proof.js.map +1 -0
  113. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.d.ts +18 -0
  114. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.js +230 -0
  115. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.js.map +1 -0
  116. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.d.ts +41 -0
  117. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.js +443 -0
  118. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.js.map +1 -0
  119. package/dist/server/modules/relay-tunnel/relay-tunnel-service.d.ts +111 -0
  120. package/dist/server/modules/relay-tunnel/relay-tunnel-service.js +771 -0
  121. package/dist/server/modules/relay-tunnel/relay-tunnel-service.js.map +1 -0
  122. package/dist/server/modules/sessions/codex-app-server-helper-client.d.ts +2 -1
  123. package/dist/server/modules/sessions/codex-app-server-helper-client.js +78 -0
  124. package/dist/server/modules/sessions/codex-app-server-helper-client.js.map +1 -1
  125. package/dist/server/modules/sessions/codex-app-server-helper-process.js +84 -2
  126. package/dist/server/modules/sessions/codex-app-server-helper-process.js.map +1 -1
  127. package/dist/server/modules/sessions/provider-session-delete-cli.d.ts +15 -0
  128. package/dist/server/modules/sessions/provider-session-delete-cli.js +148 -0
  129. package/dist/server/modules/sessions/provider-session-delete-cli.js.map +1 -0
  130. package/dist/server/modules/sessions/session-controller.d.ts +4 -1
  131. package/dist/server/modules/sessions/session-controller.js +4 -0
  132. package/dist/server/modules/sessions/session-controller.js.map +1 -1
  133. package/dist/server/modules/sessions/session-history-service.d.ts +17 -0
  134. package/dist/server/modules/sessions/session-history-service.js +150 -1
  135. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  136. package/dist/server/modules/sessions/session-live-runtime-router-service.d.ts +25 -0
  137. package/dist/server/modules/sessions/session-live-runtime-router-service.js +42 -0
  138. package/dist/server/modules/sessions/session-live-runtime-router-service.js.map +1 -0
  139. package/dist/server/modules/sessions/session-live-runtime-service.js +34 -18
  140. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  141. package/dist/server/modules/sessions/session-message-attachment-service.d.ts +1 -0
  142. package/dist/server/modules/sessions/session-message-attachment-service.js +22 -0
  143. package/dist/server/modules/sessions/session-message-attachment-service.js.map +1 -1
  144. package/dist/server/modules/sessions/session-permission-request-service.d.ts +1 -0
  145. package/dist/server/modules/sessions/session-permission-request-service.js +200 -5
  146. package/dist/server/modules/sessions/session-permission-request-service.js.map +1 -1
  147. package/dist/server/modules/sessions/session-provider-error-mapper.js +32 -0
  148. package/dist/server/modules/sessions/session-provider-error-mapper.js.map +1 -1
  149. package/dist/server/modules/sessions/session-provider-usage-guard-service.d.ts +37 -0
  150. package/dist/server/modules/sessions/session-provider-usage-guard-service.js +179 -0
  151. package/dist/server/modules/sessions/session-provider-usage-guard-service.js.map +1 -0
  152. package/dist/server/modules/sessions/session-provider-usage-limit.d.ts +17 -0
  153. package/dist/server/modules/sessions/session-provider-usage-limit.js +465 -0
  154. package/dist/server/modules/sessions/session-provider-usage-limit.js.map +1 -0
  155. package/dist/server/modules/skills/assistant-runtime-skill-catalog.d.ts +8 -0
  156. package/dist/server/modules/skills/assistant-runtime-skill-catalog.js +26 -0
  157. package/dist/server/modules/skills/assistant-runtime-skill-catalog.js.map +1 -0
  158. package/dist/server/modules/skills/assistant-runtime-skill-cleanup.d.ts +9 -0
  159. package/dist/server/modules/skills/assistant-runtime-skill-cleanup.js +55 -0
  160. package/dist/server/modules/skills/assistant-runtime-skill-cleanup.js.map +1 -0
  161. package/dist/server/modules/skills/builtin-skill-service.js +1 -6
  162. package/dist/server/modules/skills/builtin-skill-service.js.map +1 -1
  163. package/dist/server/modules/skills/skill-controller.d.ts +2 -2
  164. package/dist/server/modules/skills/skill-controller.js +9 -1
  165. package/dist/server/modules/skills/skill-controller.js.map +1 -1
  166. package/dist/server/modules/skills/skill-manager-service.d.ts +26 -1
  167. package/dist/server/modules/skills/skill-manager-service.js +346 -90
  168. package/dist/server/modules/skills/skill-manager-service.js.map +1 -1
  169. package/dist/server/modules/skills/skill-name-policy.d.ts +2 -0
  170. package/dist/server/modules/skills/skill-name-policy.js +10 -0
  171. package/dist/server/modules/skills/skill-name-policy.js.map +1 -0
  172. package/dist/server/modules/tailscale/tailscale-service.d.ts +2 -0
  173. package/dist/server/modules/tailscale/tailscale-service.js +21 -8
  174. package/dist/server/modules/tailscale/tailscale-service.js.map +1 -1
  175. package/dist/server/modules/tasks/task-types.d.ts +3 -0
  176. package/dist/server/modules/tasks/task-types.js +3 -0
  177. package/dist/server/modules/tasks/task-types.js.map +1 -1
  178. package/dist/server/modules/terminal/template-reverse-proxy-service.js +71 -3
  179. package/dist/server/modules/terminal/template-reverse-proxy-service.js.map +1 -1
  180. package/dist/server/routes/assistant.js +30 -0
  181. package/dist/server/routes/assistant.js.map +1 -1
  182. package/dist/server/routes/auth.js +4 -0
  183. package/dist/server/routes/auth.js.map +1 -1
  184. package/dist/server/routes/sessions.js +1 -0
  185. package/dist/server/routes/sessions.js.map +1 -1
  186. package/dist/server/routes/system.d.ts +2 -1
  187. package/dist/server/routes/system.js +13 -1
  188. package/dist/server/routes/system.js.map +1 -1
  189. package/dist/server/server/create-server.d.ts +10 -0
  190. package/dist/server/server/create-server.js +82 -12
  191. package/dist/server/server/create-server.js.map +1 -1
  192. package/dist/server/shared/utils/tokens.d.ts +3 -1
  193. package/dist/server/shared/utils/tokens.js +9 -2
  194. package/dist/server/shared/utils/tokens.js.map +1 -1
  195. package/dist/server/storage/repositories/assistant-automation-task-repository.d.ts +2 -0
  196. package/dist/server/storage/repositories/assistant-automation-task-repository.js +8 -2
  197. package/dist/server/storage/repositories/assistant-automation-task-repository.js.map +1 -1
  198. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.d.ts +1 -0
  199. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js +27 -0
  200. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js.map +1 -1
  201. package/dist/server/storage/repositories/auth-device-repository.d.ts +22 -0
  202. package/dist/server/storage/repositories/auth-device-repository.js +97 -0
  203. package/dist/server/storage/repositories/auth-device-repository.js.map +1 -0
  204. package/dist/server/storage/repositories/auth-device-session-repository.d.ts +17 -0
  205. package/dist/server/storage/repositories/auth-device-session-repository.js +82 -0
  206. package/dist/server/storage/repositories/auth-device-session-repository.js.map +1 -0
  207. package/dist/server/storage/repositories/auth-login-event-repository.d.ts +9 -0
  208. package/dist/server/storage/repositories/auth-login-event-repository.js +53 -0
  209. package/dist/server/storage/repositories/auth-login-event-repository.js.map +1 -0
  210. package/dist/server/storage/repositories/auth-token-repository.d.ts +4 -0
  211. package/dist/server/storage/repositories/auth-token-repository.js +58 -5
  212. package/dist/server/storage/repositories/auth-token-repository.js.map +1 -1
  213. package/dist/server/storage/repositories/butler-follow-up-task-repository.js +21 -3
  214. package/dist/server/storage/repositories/butler-follow-up-task-repository.js.map +1 -1
  215. package/dist/server/storage/repositories/instance-relay-tunnel-identity-repository.d.ts +8 -0
  216. package/dist/server/storage/repositories/instance-relay-tunnel-identity-repository.js +52 -0
  217. package/dist/server/storage/repositories/instance-relay-tunnel-identity-repository.js.map +1 -0
  218. package/dist/server/storage/repositories/instance-relay-tunnel-repository.d.ts +10 -0
  219. package/dist/server/storage/repositories/instance-relay-tunnel-repository.js +153 -0
  220. package/dist/server/storage/repositories/instance-relay-tunnel-repository.js.map +1 -0
  221. package/dist/server/storage/repositories/instance-tailscale-repository.js +6 -3
  222. package/dist/server/storage/repositories/instance-tailscale-repository.js.map +1 -1
  223. package/dist/server/storage/repositories/managed-skill-repository.d.ts +2 -1
  224. package/dist/server/storage/repositories/managed-skill-repository.js +14 -4
  225. package/dist/server/storage/repositories/managed-skill-repository.js.map +1 -1
  226. package/dist/server/storage/repositories/session-message-attachment-repository.d.ts +2 -0
  227. package/dist/server/storage/repositories/session-message-attachment-repository.js +24 -0
  228. package/dist/server/storage/repositories/session-message-attachment-repository.js.map +1 -1
  229. package/dist/server/storage/sqlite/client.js +297 -2
  230. package/dist/server/storage/sqlite/client.js.map +1 -1
  231. package/dist/server/storage/sqlite/schema.sql +122 -4
  232. package/dist/server/types/domain.d.ts +82 -1
  233. package/dist/server/ws/workbench-ws-hub.js +99 -75
  234. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  235. package/dist/server/ws/ws-auth-guard.js +1 -4
  236. package/dist/server/ws/ws-auth-guard.js.map +1 -1
  237. package/dist/server/ws/ws-server.d.ts +1 -1
  238. package/dist/server/ws/ws-server.js.map +1 -1
  239. package/node_modules/@codingns/session-sync-core/dist/codex-resume-history.d.ts +1 -0
  240. package/node_modules/@codingns/session-sync-core/dist/codex-resume-history.js +80 -0
  241. package/node_modules/@codingns/session-sync-core/dist/codex-resume-history.js.map +1 -0
  242. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.d.ts +1 -0
  243. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js +11 -1
  244. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js.map +1 -1
  245. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +11 -0
  246. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +132 -21
  247. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  248. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.d.ts +2 -0
  249. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js +53 -1
  250. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js.map +1 -1
  251. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.d.ts +1 -0
  252. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js +10 -1
  253. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js.map +1 -1
  254. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.d.ts +1 -0
  255. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +30 -0
  256. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
  257. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.d.ts +5 -1
  258. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +145 -58
  259. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  260. package/node_modules/@codingns/session-sync-core/dist/services.d.ts +1 -0
  261. package/node_modules/@codingns/session-sync-core/dist/services.js +7 -0
  262. package/node_modules/@codingns/session-sync-core/dist/services.js.map +1 -1
  263. package/node_modules/@codingns/session-sync-core/dist/types.d.ts +2 -0
  264. package/package.json +1 -1
  265. package/scripts/postinstall.mjs +0 -33
  266. package/dist/public/assets/index-CSVhg7I8.js +0 -123
  267. package/dist/public/assets/index-Ce1VX19m.css +0 -1
@@ -0,0 +1,771 @@
1
+ import { AppError } from "../../shared/errors/app-error.js";
2
+ import { decryptSecret, encryptSecret } from "../../shared/utils/secret-box.js";
3
+ import { nowIso } from "../../shared/utils/time.js";
4
+ import { RelayTunnelIdentityService } from "./crypto/relay-tunnel-identity-service.js";
5
+ import { createTaskManager } from "../tasks/task-manager.js";
6
+ import { HOST_TASK_TYPES } from "../tasks/task-types.js";
7
+ export class RelayTunnelService {
8
+ db;
9
+ bootstrapStateRepository;
10
+ repository;
11
+ defaultLocalTargetBaseUrl;
12
+ legacyLocalTargetBaseUrl;
13
+ controlSessionSecret;
14
+ fetchFn;
15
+ taskManager;
16
+ runtimeAdapter;
17
+ identityService;
18
+ constructor(db, bootstrapStateRepository, identityRepository, repository, options, taskManager = createTaskManager(), runtimeAdapter = new NoopRelayTunnelRuntimeAdapter()) {
19
+ this.db = db;
20
+ this.bootstrapStateRepository = bootstrapStateRepository;
21
+ this.repository = repository;
22
+ this.taskManager = taskManager;
23
+ this.runtimeAdapter = runtimeAdapter;
24
+ this.identityService = new RelayTunnelIdentityService(identityRepository);
25
+ this.controlSessionSecret = normalizeRequiredText(options.controlSessionSecret, "controlSessionSecret");
26
+ this.fetchFn = options.fetchFn ?? fetch;
27
+ this.defaultLocalTargetBaseUrl = normalizeHttpBaseUrl(options.defaultLocalTargetBaseUrl, "defaultLocalTargetBaseUrl");
28
+ this.legacyLocalTargetBaseUrl = options.legacyLocalTargetBaseUrl
29
+ ? normalizeHttpBaseUrl(options.legacyLocalTargetBaseUrl, "legacyLocalTargetBaseUrl")
30
+ : null;
31
+ this.registerBackgroundTasks();
32
+ }
33
+ async restoreOnStartup() {
34
+ const snapshot = this.readStateSnapshot();
35
+ if (!snapshot.hasPersistedConfig
36
+ || !snapshot.config.activated
37
+ || !snapshot.config.enabled
38
+ || !isBound(snapshot.config)) {
39
+ return;
40
+ }
41
+ const effectiveConfig = this.syncIdentityIntoConfig(snapshot.config);
42
+ if (!this.isBootstrapInitialized()) {
43
+ this.repository.upsertStatus(buildSkeletonStatus("blocked_uninitialized", effectiveConfig, {
44
+ observedAt: nowIso()
45
+ }));
46
+ return;
47
+ }
48
+ this.requestReconnect("relay_tunnel.startup_restore");
49
+ }
50
+ async getStatus() {
51
+ const snapshot = this.readStateSnapshot();
52
+ const effectiveConfig = this.resolveConfigWithIdentity(snapshot.config);
53
+ return this.buildStatusDto(snapshot, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
54
+ }
55
+ async ensureIdentity() {
56
+ const snapshot = this.readStateSnapshot();
57
+ const nextConfig = this.syncIdentityIntoConfig(snapshot.config);
58
+ return this.buildStatusDto({
59
+ config: nextConfig,
60
+ hasPersistedConfig: snapshot.hasPersistedConfig || nextConfig !== snapshot.config
61
+ }, nextConfig, this.resolveEffectiveStatus(nextConfig));
62
+ }
63
+ async updateConfig(input) {
64
+ const snapshot = this.readStateSnapshot();
65
+ const nextConfig = {
66
+ ...snapshot.config,
67
+ activated: input.activated !== undefined
68
+ ? input.activated
69
+ : snapshot.config.activated,
70
+ relayBaseUrl: input.relayBaseUrl !== undefined
71
+ ? normalizeWebsocketBaseUrl(input.relayBaseUrl, "relayBaseUrl")
72
+ : snapshot.config.relayBaseUrl,
73
+ controlBaseUrl: input.controlBaseUrl !== undefined
74
+ ? normalizeHttpBaseUrl(input.controlBaseUrl, "controlBaseUrl")
75
+ : snapshot.config.controlBaseUrl,
76
+ localTargetBaseUrl: input.localTargetBaseUrl !== undefined
77
+ ? normalizeHttpBaseUrl(input.localTargetBaseUrl, "localTargetBaseUrl")
78
+ : snapshot.config.localTargetBaseUrl,
79
+ localTargetBaseUrlSource: input.localTargetBaseUrl !== undefined
80
+ ? "custom"
81
+ : (snapshot.config.localTargetBaseUrlSource ?? "default"),
82
+ enabled: input.activated === false
83
+ ? false
84
+ : snapshot.config.enabled,
85
+ updatedAt: nowIso()
86
+ };
87
+ this.repository.upsertConfig(nextConfig);
88
+ const effectiveConfig = this.resolveConfigWithIdentity(nextConfig);
89
+ if (!effectiveConfig.activated) {
90
+ const nextStatus = buildSkeletonStatus("disabled", effectiveConfig, {
91
+ observedAt: nowIso()
92
+ });
93
+ this.repository.upsertStatus(nextStatus);
94
+ this.taskManager.cancel(HOST_TASK_TYPES.relayTunnelConnect, "default", "relay_tunnel_deactivated");
95
+ await this.runtimeAdapter.disconnect?.("relay_tunnel_deactivated");
96
+ return this.buildStatusDto({
97
+ config: effectiveConfig,
98
+ hasPersistedConfig: true
99
+ }, effectiveConfig, nextStatus);
100
+ }
101
+ if (effectiveConfig.enabled && isBound(effectiveConfig) && this.isBootstrapInitialized()) {
102
+ this.requestReconnect("relay_tunnel.config_update");
103
+ }
104
+ return this.buildStatusDto({
105
+ config: effectiveConfig,
106
+ hasPersistedConfig: true
107
+ }, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
108
+ }
109
+ async loginControl(input) {
110
+ const snapshot = this.readStateSnapshot();
111
+ const controlBaseUrl = requireConfiguredControlBaseUrl(snapshot.config);
112
+ const email = normalizeRequiredText(input.email, "email");
113
+ const password = normalizeRequiredText(input.password, "password");
114
+ const response = await this.requestControlApi({
115
+ controlBaseUrl,
116
+ path: "/api/public/auth/login",
117
+ method: "POST",
118
+ headers: {
119
+ "Content-Type": "application/json"
120
+ },
121
+ body: JSON.stringify({
122
+ email,
123
+ password
124
+ }),
125
+ failurePrefix: "控制站登录失败"
126
+ });
127
+ const timestamp = nowIso();
128
+ const nextConfig = {
129
+ ...snapshot.config,
130
+ accountId: response.account.accountId,
131
+ controlAccessTokenCiphertext: encryptSecret(this.controlSessionSecret, response.accessToken),
132
+ controlAccountEmail: response.account.email.trim(),
133
+ controlSessionExpiresAt: normalizeOptionalText(response.expiresAt),
134
+ updatedAt: timestamp
135
+ };
136
+ this.repository.upsertConfig(nextConfig);
137
+ return this.buildStatusDto({
138
+ config: nextConfig,
139
+ hasPersistedConfig: true
140
+ }, this.resolveConfigWithIdentity(nextConfig), this.resolveEffectiveStatus(nextConfig));
141
+ }
142
+ async logoutControl() {
143
+ const snapshot = this.readStateSnapshot();
144
+ const nextConfig = clearRelayTunnelControlSession(snapshot.config, {
145
+ clearAccountId: !snapshot.config.bindingId,
146
+ updatedAt: nowIso()
147
+ });
148
+ this.repository.upsertConfig(nextConfig);
149
+ return this.buildStatusDto({
150
+ config: nextConfig,
151
+ hasPersistedConfig: true
152
+ }, this.resolveConfigWithIdentity(nextConfig), this.resolveEffectiveStatus(nextConfig));
153
+ }
154
+ async checkHostLabelAvailability(hostLabel) {
155
+ const snapshot = this.readStateSnapshot();
156
+ const normalizedHostLabel = normalizeRequiredText(hostLabel, "hostLabel");
157
+ const { controlBaseUrl, accessToken } = this.requireControlSession(snapshot.config);
158
+ const path = `/api/v1/hosts/availability?hostLabel=${encodeURIComponent(normalizedHostLabel)}`;
159
+ return await this.requestControlApi({
160
+ controlBaseUrl,
161
+ path,
162
+ method: "GET",
163
+ headers: {
164
+ Authorization: `Bearer ${accessToken}`
165
+ },
166
+ failurePrefix: "检查 Host 名称失败"
167
+ });
168
+ }
169
+ async bindControlHost(hostLabel) {
170
+ const snapshot = this.readStateSnapshot();
171
+ if (snapshot.config.bindingId && snapshot.config.tunnelDomain) {
172
+ const effectiveConfig = this.resolveConfigWithIdentity(snapshot.config);
173
+ return this.buildStatusDto(snapshot, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
174
+ }
175
+ const normalizedHostLabel = normalizeRequiredText(hostLabel, "hostLabel");
176
+ const { controlBaseUrl, accessToken, accountId } = this.requireControlSession(snapshot.config);
177
+ const identity = this.identityService.ensureIdentity();
178
+ const bindResponse = await this.requestControlApi({
179
+ controlBaseUrl,
180
+ path: "/api/v1/hosts/bind",
181
+ method: "POST",
182
+ headers: {
183
+ "Content-Type": "application/json",
184
+ Authorization: `Bearer ${accessToken}`
185
+ },
186
+ body: JSON.stringify({
187
+ hostLabel: normalizedHostLabel,
188
+ hostPublicKey: identity.publicKeyPem,
189
+ hostFingerprint: identity.keyFingerprint
190
+ }),
191
+ failurePrefix: "绑定 Host 失败"
192
+ });
193
+ return await this.bind({
194
+ accountId,
195
+ bindingId: bindResponse.binding.bindingId,
196
+ tunnelDomain: bindResponse.binding.tunnelDomain,
197
+ relayBaseUrl: bindResponse.binding.relayBaseUrl,
198
+ controlBaseUrl
199
+ });
200
+ }
201
+ async getTrafficWallet() {
202
+ const snapshot = this.readStateSnapshot();
203
+ const { controlBaseUrl, accessToken } = this.requireControlSession(snapshot.config);
204
+ const response = await this.requestControlApi({
205
+ controlBaseUrl,
206
+ path: "/api/v1/traffic-wallet/me",
207
+ method: "GET",
208
+ headers: {
209
+ Authorization: `Bearer ${accessToken}`
210
+ },
211
+ failurePrefix: "读取控制站流量信息失败"
212
+ });
213
+ return response.wallet;
214
+ }
215
+ async bind(input) {
216
+ const snapshot = this.readStateSnapshot();
217
+ const accountId = normalizeRequiredText(input.accountId, "accountId");
218
+ const bindingId = normalizeRequiredText(input.bindingId, "bindingId");
219
+ const tunnelDomain = normalizeTunnelDomain(input.tunnelDomain, "tunnelDomain");
220
+ const identity = this.identityService.ensureIdentity();
221
+ const relayBaseUrl = input.relayBaseUrl !== undefined
222
+ ? normalizeWebsocketBaseUrl(input.relayBaseUrl, "relayBaseUrl")
223
+ : snapshot.config.relayBaseUrl;
224
+ const controlBaseUrl = input.controlBaseUrl !== undefined
225
+ ? normalizeHttpBaseUrl(input.controlBaseUrl, "controlBaseUrl")
226
+ : snapshot.config.controlBaseUrl;
227
+ if (!relayBaseUrl || !controlBaseUrl) {
228
+ throw new AppError({
229
+ statusCode: 400,
230
+ errorCode: "INVALID_INPUT",
231
+ detail: "绑定前必须提供 relayBaseUrl 和 controlBaseUrl"
232
+ });
233
+ }
234
+ const timestamp = nowIso();
235
+ const nextConfig = {
236
+ ...snapshot.config,
237
+ relayBaseUrl,
238
+ controlBaseUrl,
239
+ accountId,
240
+ tunnelDomain,
241
+ bindingId,
242
+ hostPublicKey: identity.publicKeyPem,
243
+ hostKeyFingerprint: identity.keyFingerprint,
244
+ updatedAt: timestamp
245
+ };
246
+ const nextStatus = buildSkeletonStatus(nextConfig.enabled
247
+ ? (this.isBootstrapInitialized() ? "connecting" : "blocked_uninitialized")
248
+ : "disabled", nextConfig, {
249
+ observedAt: timestamp
250
+ });
251
+ this.db.transaction(() => {
252
+ this.repository.upsertConfig(nextConfig);
253
+ this.repository.upsertStatus(nextStatus);
254
+ })();
255
+ if (nextConfig.enabled && this.isBootstrapInitialized()) {
256
+ this.requestReconnect("relay_tunnel.bind");
257
+ }
258
+ return this.buildStatusDto({
259
+ config: nextConfig,
260
+ hasPersistedConfig: true
261
+ }, nextConfig, this.resolveEffectiveStatus(nextConfig));
262
+ }
263
+ async unbind() {
264
+ const snapshot = this.readStateSnapshot();
265
+ const timestamp = nowIso();
266
+ const nextConfig = clearRelayTunnelControlSession({
267
+ ...snapshot.config,
268
+ enabled: false,
269
+ bindingId: null,
270
+ tunnelDomain: null,
271
+ updatedAt: timestamp
272
+ }, {
273
+ clearAccountId: true,
274
+ updatedAt: timestamp
275
+ });
276
+ const nextStatus = buildSkeletonStatus("disabled", nextConfig, {
277
+ observedAt: timestamp
278
+ });
279
+ this.taskManager.cancel(HOST_TASK_TYPES.relayTunnelConnect, "default", "relay_tunnel_unbound");
280
+ await this.runtimeAdapter.disconnect?.("relay_tunnel_unbound");
281
+ this.db.transaction(() => {
282
+ this.repository.upsertConfig(nextConfig);
283
+ this.repository.upsertStatus(nextStatus);
284
+ })();
285
+ return this.buildStatusDto({
286
+ config: nextConfig,
287
+ hasPersistedConfig: true
288
+ }, nextConfig, nextStatus);
289
+ }
290
+ async enable() {
291
+ const snapshot = this.readStateSnapshot();
292
+ if (!isBound(snapshot.config)) {
293
+ throw new AppError({
294
+ statusCode: 409,
295
+ errorCode: "RELAY_TUNNEL_NOT_BOUND",
296
+ detail: "当前实例还没有绑定公共隧道"
297
+ });
298
+ }
299
+ const timestamp = nowIso();
300
+ const nextConfig = {
301
+ ...snapshot.config,
302
+ activated: true,
303
+ enabled: true,
304
+ updatedAt: timestamp
305
+ };
306
+ const configWithIdentity = this.syncIdentityIntoConfig(nextConfig);
307
+ const nextStatus = buildSkeletonStatus(this.isBootstrapInitialized() ? "connecting" : "blocked_uninitialized", configWithIdentity, {
308
+ observedAt: timestamp
309
+ });
310
+ this.db.transaction(() => {
311
+ this.repository.upsertConfig(configWithIdentity);
312
+ this.repository.upsertStatus(nextStatus);
313
+ })();
314
+ if (this.isBootstrapInitialized()) {
315
+ this.requestReconnect("relay_tunnel.enable");
316
+ }
317
+ return this.buildStatusDto({
318
+ config: configWithIdentity,
319
+ hasPersistedConfig: true
320
+ }, configWithIdentity, nextStatus);
321
+ }
322
+ async disable() {
323
+ const snapshot = this.readStateSnapshot();
324
+ const timestamp = nowIso();
325
+ const nextConfig = {
326
+ ...snapshot.config,
327
+ enabled: false,
328
+ updatedAt: timestamp
329
+ };
330
+ const nextStatus = buildSkeletonStatus("disabled", nextConfig, {
331
+ observedAt: timestamp
332
+ });
333
+ this.db.transaction(() => {
334
+ this.repository.upsertConfig(nextConfig);
335
+ this.repository.upsertStatus(nextStatus);
336
+ })();
337
+ this.taskManager.cancel(HOST_TASK_TYPES.relayTunnelConnect, "default", "relay_tunnel_disabled");
338
+ await this.runtimeAdapter.disconnect?.("relay_tunnel_disabled");
339
+ return this.buildStatusDto({
340
+ config: nextConfig,
341
+ hasPersistedConfig: true
342
+ }, nextConfig, nextStatus);
343
+ }
344
+ requestReconnect(source = "relay_tunnel.reconnect") {
345
+ return this.taskManager.enqueue(HOST_TASK_TYPES.relayTunnelConnect, {
346
+ key: "default",
347
+ source,
348
+ input: {
349
+ source
350
+ }
351
+ });
352
+ }
353
+ readStateSnapshot() {
354
+ const persistedConfig = this.reconcileLegacyLocalTargetBaseUrl(this.repository.findConfig());
355
+ return {
356
+ config: persistedConfig
357
+ ?? {
358
+ activated: false,
359
+ enabled: false,
360
+ provider: "codingns_relay",
361
+ relayBaseUrl: null,
362
+ controlBaseUrl: null,
363
+ controlAccessTokenCiphertext: null,
364
+ controlAccountEmail: null,
365
+ controlSessionExpiresAt: null,
366
+ accountId: null,
367
+ tunnelDomain: null,
368
+ bindingId: null,
369
+ hostPublicKey: null,
370
+ hostKeyFingerprint: null,
371
+ localTargetBaseUrl: this.defaultLocalTargetBaseUrl,
372
+ localTargetBaseUrlSource: "default",
373
+ updatedAt: nowIso()
374
+ },
375
+ hasPersistedConfig: persistedConfig !== null
376
+ };
377
+ }
378
+ reconcileLegacyLocalTargetBaseUrl(config) {
379
+ if (!config) {
380
+ return config;
381
+ }
382
+ if ((config.localTargetBaseUrlSource ?? "default") !== "default") {
383
+ return config;
384
+ }
385
+ if (config.localTargetBaseUrl === this.defaultLocalTargetBaseUrl) {
386
+ return config;
387
+ }
388
+ // `default` 源的目标地址由当前运行模式决定,不应该把历史默认值永久粘在库里。
389
+ // 只要默认入口变化了,就在启动时自动收敛到新的默认值;用户显式写入的 custom 配置不动。
390
+ const migratedConfig = {
391
+ ...config,
392
+ localTargetBaseUrl: this.defaultLocalTargetBaseUrl,
393
+ localTargetBaseUrlSource: "default",
394
+ updatedAt: nowIso()
395
+ };
396
+ this.repository.upsertConfig(migratedConfig);
397
+ return migratedConfig;
398
+ }
399
+ resolveEffectiveStatus(config) {
400
+ const persisted = this.repository.findStatus();
401
+ if (!config.activated || !config.enabled) {
402
+ return buildSkeletonStatus("disabled", config, {
403
+ observedAt: persisted?.observedAt ?? null
404
+ });
405
+ }
406
+ if (!isBound(config)) {
407
+ return buildSkeletonStatus("unbound", config, {
408
+ observedAt: persisted?.observedAt ?? null
409
+ });
410
+ }
411
+ if (!this.isBootstrapInitialized()) {
412
+ return buildSkeletonStatus("blocked_uninitialized", config, {
413
+ observedAt: persisted?.observedAt ?? null
414
+ });
415
+ }
416
+ if (!persisted
417
+ || persisted.phase === "disabled"
418
+ || persisted.phase === "unbound"
419
+ || persisted.phase === "blocked_uninitialized") {
420
+ return buildSkeletonStatus("connecting", config, {
421
+ observedAt: persisted?.observedAt ?? null
422
+ });
423
+ }
424
+ return {
425
+ ...persisted,
426
+ bindingId: config.bindingId,
427
+ tunnelDomain: config.tunnelDomain,
428
+ hostFingerprint: config.hostKeyFingerprint
429
+ };
430
+ }
431
+ registerBackgroundTasks() {
432
+ if (this.taskManager.has(HOST_TASK_TYPES.relayTunnelConnect)) {
433
+ return;
434
+ }
435
+ this.taskManager.register({
436
+ taskType: HOST_TASK_TYPES.relayTunnelConnect,
437
+ executionLane: "host_background",
438
+ timeoutMs: 15_000,
439
+ run: async (_input, context) => await this.runConnectTask(context.signal)
440
+ });
441
+ }
442
+ async runConnectTask(signal) {
443
+ const snapshot = this.readStateSnapshot();
444
+ if (!snapshot.config.enabled || !isBound(snapshot.config)) {
445
+ const effectiveConfig = this.resolveConfigWithIdentity(snapshot.config);
446
+ return this.buildStatusDto(snapshot, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
447
+ }
448
+ const effectiveConfig = this.syncIdentityIntoConfig(snapshot.config);
449
+ if (!this.isBootstrapInitialized()) {
450
+ const blockedStatus = buildSkeletonStatus("blocked_uninitialized", effectiveConfig, {
451
+ observedAt: nowIso()
452
+ });
453
+ this.repository.upsertStatus(blockedStatus);
454
+ return this.buildStatusDto(snapshot, effectiveConfig, blockedStatus);
455
+ }
456
+ try {
457
+ const nextStatus = await this.runtimeAdapter.connect(effectiveConfig, signal);
458
+ if (signal.aborted) {
459
+ const latestSnapshot = this.readStateSnapshot();
460
+ const effectiveConfig = this.resolveConfigWithIdentity(latestSnapshot.config);
461
+ return this.buildStatusDto(latestSnapshot, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
462
+ }
463
+ this.repository.upsertStatus(nextStatus);
464
+ return this.buildStatusDto(snapshot, effectiveConfig, nextStatus);
465
+ }
466
+ catch (error) {
467
+ if (signal.aborted) {
468
+ const latestSnapshot = this.readStateSnapshot();
469
+ const effectiveConfig = this.resolveConfigWithIdentity(latestSnapshot.config);
470
+ return this.buildStatusDto(latestSnapshot, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
471
+ }
472
+ const failedStatus = {
473
+ ...buildSkeletonStatus("error", snapshot.config, {
474
+ observedAt: nowIso()
475
+ }),
476
+ lastError: error instanceof Error ? error.message : String(error)
477
+ };
478
+ this.repository.upsertStatus(failedStatus);
479
+ return this.buildStatusDto(snapshot, effectiveConfig, failedStatus);
480
+ }
481
+ }
482
+ buildStatusDto(snapshot, effectiveConfig, effectiveStatus) {
483
+ return {
484
+ activated: effectiveConfig.activated,
485
+ enabled: effectiveConfig.enabled,
486
+ provider: effectiveConfig.provider,
487
+ relayBaseUrl: effectiveConfig.relayBaseUrl,
488
+ controlBaseUrl: effectiveConfig.controlBaseUrl,
489
+ controlAccountEmail: effectiveConfig.controlAccountEmail,
490
+ controlSessionExpiresAt: effectiveConfig.controlSessionExpiresAt,
491
+ accountId: effectiveConfig.accountId,
492
+ tunnelDomain: effectiveConfig.tunnelDomain,
493
+ bindingId: effectiveConfig.bindingId,
494
+ hostPublicKey: effectiveConfig.hostPublicKey,
495
+ hostKeyFingerprint: effectiveConfig.hostKeyFingerprint,
496
+ localTargetBaseUrl: effectiveConfig.localTargetBaseUrl,
497
+ phase: effectiveStatus.phase,
498
+ connected: effectiveStatus.connected,
499
+ hostFingerprint: effectiveStatus.hostFingerprint,
500
+ trafficUsedBytes: effectiveStatus.trafficUsedBytes,
501
+ trafficRemainingBytes: effectiveStatus.trafficRemainingBytes,
502
+ quotaResetAt: effectiveStatus.quotaResetAt,
503
+ lastError: effectiveStatus.lastError,
504
+ observedAt: effectiveStatus.observedAt,
505
+ updatedAt: snapshot.hasPersistedConfig ? snapshot.config.updatedAt : null
506
+ };
507
+ }
508
+ resolveConfigWithIdentity(config) {
509
+ const identity = this.identityService.getIdentity();
510
+ if (!identity) {
511
+ return config;
512
+ }
513
+ return {
514
+ ...config,
515
+ hostPublicKey: identity.publicKeyPem,
516
+ hostKeyFingerprint: identity.keyFingerprint
517
+ };
518
+ }
519
+ syncIdentityIntoConfig(config) {
520
+ const identity = this.identityService.ensureIdentity();
521
+ if (config.hostPublicKey === identity.publicKeyPem
522
+ && config.hostKeyFingerprint === identity.keyFingerprint) {
523
+ return config;
524
+ }
525
+ const nextConfig = {
526
+ ...config,
527
+ hostPublicKey: identity.publicKeyPem,
528
+ hostKeyFingerprint: identity.keyFingerprint
529
+ };
530
+ this.repository.upsertConfig(nextConfig);
531
+ return nextConfig;
532
+ }
533
+ isBootstrapInitialized() {
534
+ return this.bootstrapStateRepository.getState().initialized;
535
+ }
536
+ requireControlSession(config) {
537
+ const controlBaseUrl = requireConfiguredControlBaseUrl(config);
538
+ const encryptedAccessToken = normalizeOptionalText(config.controlAccessTokenCiphertext);
539
+ const accountId = normalizeOptionalText(config.accountId);
540
+ if (!encryptedAccessToken || !accountId) {
541
+ throw new AppError({
542
+ statusCode: 409,
543
+ errorCode: "RELAY_TUNNEL_CONTROL_SESSION_REQUIRED",
544
+ detail: "当前还没有登录控制站账号"
545
+ });
546
+ }
547
+ try {
548
+ return {
549
+ controlBaseUrl,
550
+ accessToken: decryptSecret(this.controlSessionSecret, encryptedAccessToken),
551
+ accountId
552
+ };
553
+ }
554
+ catch {
555
+ const nextConfig = clearRelayTunnelControlSession(config, {
556
+ clearAccountId: !config.bindingId,
557
+ updatedAt: nowIso()
558
+ });
559
+ this.repository.upsertConfig(nextConfig);
560
+ throw new AppError({
561
+ statusCode: 409,
562
+ errorCode: "RELAY_TUNNEL_CONTROL_SESSION_REQUIRED",
563
+ detail: "控制站登录态已失效,请重新登录"
564
+ });
565
+ }
566
+ }
567
+ async requestControlApi(input) {
568
+ const response = await this.fetchFn(new URL(input.path, ensureTrailingSlash(input.controlBaseUrl)), {
569
+ method: input.method,
570
+ headers: input.headers,
571
+ body: input.body
572
+ });
573
+ if (!response.ok) {
574
+ throw await buildControlApiError(response, input.failurePrefix);
575
+ }
576
+ return await response.json();
577
+ }
578
+ }
579
+ class NoopRelayTunnelRuntimeAdapter {
580
+ async connect(config, _signal) {
581
+ return buildSkeletonStatus("connecting", config, {
582
+ observedAt: nowIso()
583
+ });
584
+ }
585
+ }
586
+ function buildSkeletonStatus(phase, config, overrides) {
587
+ return {
588
+ phase,
589
+ connected: false,
590
+ bindingId: config.bindingId,
591
+ tunnelDomain: config.tunnelDomain,
592
+ hostFingerprint: config.hostKeyFingerprint,
593
+ trafficUsedBytes: null,
594
+ trafficRemainingBytes: null,
595
+ quotaResetAt: null,
596
+ lastError: null,
597
+ observedAt: overrides?.observedAt ?? null
598
+ };
599
+ }
600
+ function isBound(config) {
601
+ return Boolean(config.bindingId && config.tunnelDomain);
602
+ }
603
+ function normalizeRequiredText(value, field) {
604
+ const normalized = value?.trim();
605
+ if (!normalized) {
606
+ throw new AppError({
607
+ statusCode: 400,
608
+ errorCode: "INVALID_INPUT",
609
+ detail: `${field} 不能为空`,
610
+ field
611
+ });
612
+ }
613
+ return normalized;
614
+ }
615
+ function normalizeOptionalText(value) {
616
+ const normalized = value?.trim();
617
+ return normalized ? normalized : null;
618
+ }
619
+ function normalizeTunnelDomain(value, field) {
620
+ const normalized = normalizeRequiredText(value, field).toLowerCase();
621
+ if (!/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(normalized)) {
622
+ throw new AppError({
623
+ statusCode: 400,
624
+ errorCode: "INVALID_INPUT",
625
+ detail: "tunnelDomain 必须是合法域名",
626
+ field
627
+ });
628
+ }
629
+ return normalized;
630
+ }
631
+ function normalizeHttpBaseUrl(value, field) {
632
+ if (value === null || value === undefined) {
633
+ return value ?? null;
634
+ }
635
+ const normalized = value.trim();
636
+ if (normalized.length === 0) {
637
+ return null;
638
+ }
639
+ let parsed;
640
+ try {
641
+ parsed = new URL(normalized);
642
+ }
643
+ catch {
644
+ throw new AppError({
645
+ statusCode: 400,
646
+ errorCode: "INVALID_INPUT",
647
+ detail: `${field} 必须是合法的 http 或 https 地址`,
648
+ field
649
+ });
650
+ }
651
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
652
+ throw new AppError({
653
+ statusCode: 400,
654
+ errorCode: "INVALID_INPUT",
655
+ detail: `${field} 只允许使用 http 或 https 协议`,
656
+ field
657
+ });
658
+ }
659
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
660
+ throw new AppError({
661
+ statusCode: 400,
662
+ errorCode: "INVALID_INPUT",
663
+ detail: `${field} 不能包含账号、查询参数或 hash`,
664
+ field
665
+ });
666
+ }
667
+ const pathname = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "");
668
+ return `${parsed.protocol}//${parsed.host}${pathname}`;
669
+ }
670
+ function normalizeWebsocketBaseUrl(value, field) {
671
+ if (value === null || value === undefined) {
672
+ return value ?? null;
673
+ }
674
+ const normalized = value.trim();
675
+ if (normalized.length === 0) {
676
+ return null;
677
+ }
678
+ let parsed;
679
+ try {
680
+ parsed = new URL(normalized);
681
+ }
682
+ catch {
683
+ throw new AppError({
684
+ statusCode: 400,
685
+ errorCode: "INVALID_INPUT",
686
+ detail: `${field} 必须是合法的 ws、wss、http 或 https 地址`,
687
+ field
688
+ });
689
+ }
690
+ if (parsed.protocol !== "ws:"
691
+ && parsed.protocol !== "wss:"
692
+ && parsed.protocol !== "http:"
693
+ && parsed.protocol !== "https:") {
694
+ throw new AppError({
695
+ statusCode: 400,
696
+ errorCode: "INVALID_INPUT",
697
+ detail: `${field} 只允许使用 ws、wss、http 或 https 协议`,
698
+ field
699
+ });
700
+ }
701
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
702
+ throw new AppError({
703
+ statusCode: 400,
704
+ errorCode: "INVALID_INPUT",
705
+ detail: `${field} 不能包含账号、查询参数或 hash`,
706
+ field
707
+ });
708
+ }
709
+ const pathname = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "");
710
+ const normalizedProtocol = parsed.protocol === "https:"
711
+ ? "wss:"
712
+ : parsed.protocol === "http:"
713
+ ? "ws:"
714
+ : parsed.protocol;
715
+ return `${normalizedProtocol}//${parsed.host}${pathname}`;
716
+ }
717
+ function requireConfiguredControlBaseUrl(config) {
718
+ const controlBaseUrl = normalizeOptionalText(config.controlBaseUrl);
719
+ if (!controlBaseUrl) {
720
+ throw new AppError({
721
+ statusCode: 409,
722
+ errorCode: "RELAY_TUNNEL_CONTROL_BASE_URL_REQUIRED",
723
+ detail: "当前还没有配置控制站点地址"
724
+ });
725
+ }
726
+ return controlBaseUrl;
727
+ }
728
+ function ensureTrailingSlash(value) {
729
+ return value.endsWith("/") ? value : `${value}/`;
730
+ }
731
+ function clearRelayTunnelControlSession(config, options) {
732
+ return {
733
+ ...config,
734
+ controlAccessTokenCiphertext: null,
735
+ controlAccountEmail: null,
736
+ controlSessionExpiresAt: null,
737
+ accountId: options.clearAccountId ? null : config.accountId,
738
+ updatedAt: options.updatedAt
739
+ };
740
+ }
741
+ async function buildControlApiError(response, failurePrefix) {
742
+ const detail = await readControlApiErrorDetail(response);
743
+ return new AppError({
744
+ statusCode: response.status,
745
+ errorCode: "RELAY_TUNNEL_CONTROL_API_ERROR",
746
+ detail: `${failurePrefix}:${detail}`
747
+ });
748
+ }
749
+ async function readControlApiErrorDetail(response) {
750
+ const contentType = response.headers.get("content-type") ?? "";
751
+ if (contentType.includes("application/json")) {
752
+ try {
753
+ const payload = await response.json();
754
+ const detail = readJsonErrorText(payload.detail)
755
+ ?? readJsonErrorText(payload.message)
756
+ ?? readJsonErrorText(payload.error);
757
+ if (detail) {
758
+ return detail;
759
+ }
760
+ }
761
+ catch {
762
+ // 忽略 JSON 解析失败,回退到纯文本。
763
+ }
764
+ }
765
+ const text = normalizeOptionalText(await response.text());
766
+ return text ?? `HTTP ${response.status}`;
767
+ }
768
+ function readJsonErrorText(value) {
769
+ return typeof value === "string" ? normalizeOptionalText(value) : null;
770
+ }
771
+ //# sourceMappingURL=relay-tunnel-service.js.map