@xopcai/xopc 0.0.77 → 0.0.78

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 (126) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/telegram/xopc.extension.json +1 -1
  3. package/dist/gateway/static/root/assets/{agents-DN3vr8pb.js → agents-Bh_9-1KB.js} +2 -2
  4. package/dist/gateway/static/root/assets/{agents-DN3vr8pb.js.map → agents-Bh_9-1KB.js.map} +1 -1
  5. package/dist/gateway/static/root/assets/{apps-page-BUn41aPi.js → apps-page-CB5anZpc.js} +2 -2
  6. package/dist/gateway/static/root/assets/{apps-page-BUn41aPi.js.map → apps-page-CB5anZpc.js.map} +1 -1
  7. package/dist/gateway/static/root/assets/{channels-settings-CYMmWDtP.js → channels-settings-Bt1sprhC.js} +2 -2
  8. package/dist/gateway/static/root/assets/{channels-settings-CYMmWDtP.js.map → channels-settings-Bt1sprhC.js.map} +1 -1
  9. package/dist/gateway/static/root/assets/{channels-status-swr-sJj4ueTp.js → channels-status-swr-Crgak3fg.js} +2 -2
  10. package/dist/gateway/static/root/assets/{channels-status-swr-sJj4ueTp.js.map → channels-status-swr-Crgak3fg.js.map} +1 -1
  11. package/dist/gateway/static/root/assets/{cron-api-CLxnaHdq.js → cron-api-CzJGvQQ7.js} +2 -2
  12. package/dist/gateway/static/root/assets/{cron-api-CLxnaHdq.js.map → cron-api-CzJGvQQ7.js.map} +1 -1
  13. package/dist/gateway/static/root/assets/{cron-page-BAQ8xSnJ.js → cron-page-BoNRJNVV.js} +2 -2
  14. package/dist/gateway/static/root/assets/{cron-page-BAQ8xSnJ.js.map → cron-page-BoNRJNVV.js.map} +1 -1
  15. package/dist/gateway/static/root/assets/{dist-BfJYxiK5.js → dist-a-eaOUvs.js} +2 -2
  16. package/dist/gateway/static/root/assets/{dist-BfJYxiK5.js.map → dist-a-eaOUvs.js.map} +1 -1
  17. package/dist/gateway/static/root/assets/{extension-debug-page-bgvVs-Sy.js → extension-debug-page-D-O1XjAa.js} +2 -2
  18. package/dist/gateway/static/root/assets/{extension-debug-page-bgvVs-Sy.js.map → extension-debug-page-D-O1XjAa.js.map} +1 -1
  19. package/dist/gateway/static/root/assets/{extension-page-SG4TVv-u.js → extension-page-B2VpqBTH.js} +2 -2
  20. package/dist/gateway/static/root/assets/{extension-page-SG4TVv-u.js.map → extension-page-B2VpqBTH.js.map} +1 -1
  21. package/dist/gateway/static/root/assets/{extension-settings-page-CJZRTsjF.js → extension-settings-page-CmBcQfeO.js} +2 -2
  22. package/dist/gateway/static/root/assets/{extension-settings-page-CJZRTsjF.js.map → extension-settings-page-CmBcQfeO.js.map} +1 -1
  23. package/dist/gateway/static/root/assets/{fetch-K_0JRCXU.js → fetch-EGO9T3MN.js} +3 -3
  24. package/dist/gateway/static/root/assets/{fetch-K_0JRCXU.js.map → fetch-EGO9T3MN.js.map} +1 -1
  25. package/dist/gateway/static/root/assets/{field-primitives-Z76hyBYS.js → field-primitives-Bh7G1y4D.js} +2 -2
  26. package/dist/gateway/static/root/assets/{field-primitives-Z76hyBYS.js.map → field-primitives-Bh7G1y4D.js.map} +1 -1
  27. package/dist/gateway/static/root/assets/{heartbeat-config-api-BqfDabSI.js → heartbeat-config-api-DxpIEZNs.js} +2 -2
  28. package/dist/gateway/static/root/assets/{heartbeat-config-api-BqfDabSI.js.map → heartbeat-config-api-DxpIEZNs.js.map} +1 -1
  29. package/dist/gateway/static/root/assets/{index-ChiUhJAs.js → index-Dxy9ZCtC.js} +5 -5
  30. package/dist/gateway/static/root/assets/{index-ChiUhJAs.js.map → index-Dxy9ZCtC.js.map} +1 -1
  31. package/dist/gateway/static/root/assets/{logs-page-DrIMhDE2.js → logs-page-Dw58E2GE.js} +2 -2
  32. package/dist/gateway/static/root/assets/{logs-page-DrIMhDE2.js.map → logs-page-Dw58E2GE.js.map} +1 -1
  33. package/dist/gateway/static/root/assets/{sessions-page-B-RGO3N0.js → sessions-page-CPkhCy57.js} +2 -2
  34. package/dist/gateway/static/root/assets/{sessions-page-B-RGO3N0.js.map → sessions-page-CPkhCy57.js.map} +1 -1
  35. package/dist/gateway/static/root/assets/{settings-form-section-Csvl1iL6.js → settings-form-section-DLZDVMEf.js} +2 -2
  36. package/dist/gateway/static/root/assets/{settings-form-section-Csvl1iL6.js.map → settings-form-section-DLZDVMEf.js.map} +1 -1
  37. package/dist/gateway/static/root/assets/settings-page-CVPCa0PE.js +4 -0
  38. package/dist/gateway/static/root/assets/settings-page-CVPCa0PE.js.map +1 -0
  39. package/dist/gateway/static/root/assets/{skills-page-dHwx2vh0.js → skills-page-DueZ9Qfg.js} +2 -2
  40. package/dist/gateway/static/root/assets/{skills-page-dHwx2vh0.js.map → skills-page-DueZ9Qfg.js.map} +1 -1
  41. package/dist/gateway/static/root/assets/{theme-store-Bl5A2Fd_.js → theme-store-CWPq9gW1.js} +2 -2
  42. package/dist/gateway/static/root/assets/{theme-store-Bl5A2Fd_.js.map → theme-store-CWPq9gW1.js.map} +1 -1
  43. package/dist/gateway/static/root/assets/{utils-COYrNFF7.js → utils-Cnix55r9.js} +2 -2
  44. package/dist/gateway/static/root/assets/{utils-COYrNFF7.js.map → utils-Cnix55r9.js.map} +1 -1
  45. package/dist/gateway/static/root/assets/{voice-api-key-field-5WZZaxH3.js → voice-api-key-field-BR3Ut06g.js} +2 -2
  46. package/dist/gateway/static/root/assets/{voice-api-key-field-5WZZaxH3.js.map → voice-api-key-field-BR3Ut06g.js.map} +1 -1
  47. package/dist/gateway/static/root/index.html +3 -3
  48. package/dist/package.js +1 -1
  49. package/dist/src/browser/providers/browser-ext-install.js +23 -4
  50. package/dist/src/browser/providers/browser-ext-install.js.map +1 -1
  51. package/dist/src/cli/commands/tunnel.js +4 -5
  52. package/dist/src/cli/commands/tunnel.js.map +1 -1
  53. package/dist/src/config/index.js +2 -2
  54. package/dist/src/config/rules.js +0 -5
  55. package/dist/src/config/rules.js.map +1 -1
  56. package/dist/src/config/schema.d.ts +0 -27
  57. package/dist/src/config/schema.js +4 -18
  58. package/dist/src/config/schema.js.map +1 -1
  59. package/dist/src/gateway/auth-rate-limit.d.ts +2 -0
  60. package/dist/src/gateway/auth-rate-limit.js +9 -3
  61. package/dist/src/gateway/auth-rate-limit.js.map +1 -1
  62. package/dist/src/gateway/hono/app.js +19 -13
  63. package/dist/src/gateway/hono/app.js.map +1 -1
  64. package/dist/src/gateway/hono/lib/config-payload.d.ts +3 -1
  65. package/dist/src/gateway/hono/lib/config-payload.js +1 -2
  66. package/dist/src/gateway/hono/lib/config-payload.js.map +1 -1
  67. package/dist/src/gateway/hono/routes/tunnel.js +32 -30
  68. package/dist/src/gateway/hono/routes/tunnel.js.map +1 -1
  69. package/dist/src/gateway/host.d.ts +24 -0
  70. package/dist/src/gateway/host.js +33 -1
  71. package/dist/src/gateway/host.js.map +1 -1
  72. package/dist/src/gateway/index.d.ts +1 -1
  73. package/dist/src/gateway/index.js +2 -2
  74. package/dist/src/gateway/runtime-config.js +1 -8
  75. package/dist/src/gateway/runtime-config.js.map +1 -1
  76. package/dist/src/gateway/security/audit.js +4 -4
  77. package/dist/src/gateway/security/audit.js.map +1 -1
  78. package/dist/src/gateway/server.js +2 -3
  79. package/dist/src/gateway/server.js.map +1 -1
  80. package/dist/src/gateway/service/types.d.ts +2 -0
  81. package/dist/src/gateway/service.d.ts +2 -0
  82. package/dist/src/gateway/service.js +7 -2
  83. package/dist/src/gateway/service.js.map +1 -1
  84. package/dist/src/tunnel/frp-subdomain-host.d.ts +2 -0
  85. package/dist/src/tunnel/frp-subdomain-host.js +15 -0
  86. package/dist/src/tunnel/frp-subdomain-host.js.map +1 -0
  87. package/dist/src/tunnel/gateway-lifecycle.d.ts +0 -4
  88. package/dist/src/tunnel/gateway-lifecycle.js +9 -11
  89. package/dist/src/tunnel/gateway-lifecycle.js.map +1 -1
  90. package/dist/src/tunnel/index.d.ts +2 -4
  91. package/dist/src/tunnel/index.js +2 -4
  92. package/dist/src/tunnel/pair-url.js +7 -1
  93. package/dist/src/tunnel/pair-url.js.map +1 -1
  94. package/dist/src/tunnel/pairing.d.ts +13 -0
  95. package/dist/src/tunnel/pairing.js +48 -1
  96. package/dist/src/tunnel/pairing.js.map +1 -1
  97. package/dist/src/tunnel/tunnel-config.js +2 -16
  98. package/dist/src/tunnel/tunnel-config.js.map +1 -1
  99. package/dist/src/tunnel/tunnel-service.d.ts +1 -10
  100. package/dist/src/tunnel/tunnel-service.js +7 -60
  101. package/dist/src/tunnel/tunnel-service.js.map +1 -1
  102. package/dist/src/tunnel/tunnel-types.d.ts +3 -18
  103. package/dist/src/tunnel/well-known.d.ts +5 -0
  104. package/dist/src/tunnel/well-known.js +2 -1
  105. package/dist/src/tunnel/well-known.js.map +1 -1
  106. package/package.json +2 -2
  107. package/dist/gateway/static/root/assets/settings-page-nxAc0ta1.js +0 -4
  108. package/dist/gateway/static/root/assets/settings-page-nxAc0ta1.js.map +0 -1
  109. package/dist/src/tunnel/acme-cert-store.d.ts +0 -34
  110. package/dist/src/tunnel/acme-cert-store.js +0 -184
  111. package/dist/src/tunnel/acme-cert-store.js.map +0 -1
  112. package/dist/src/tunnel/acme-client.d.ts +0 -50
  113. package/dist/src/tunnel/acme-client.js +0 -473
  114. package/dist/src/tunnel/acme-client.js.map +0 -1
  115. package/dist/src/tunnel/acme-crypto.d.ts +0 -25
  116. package/dist/src/tunnel/acme-crypto.js +0 -58
  117. package/dist/src/tunnel/acme-crypto.js.map +0 -1
  118. package/dist/src/tunnel/acme-csr.d.ts +0 -5
  119. package/dist/src/tunnel/acme-csr.js +0 -48
  120. package/dist/src/tunnel/acme-csr.js.map +0 -1
  121. package/dist/src/tunnel/tls-server.d.ts +0 -14
  122. package/dist/src/tunnel/tls-server.js +0 -126
  123. package/dist/src/tunnel/tls-server.js.map +0 -1
  124. package/dist/src/tunnel/tunnel-e2e-config.d.ts +0 -11
  125. package/dist/src/tunnel/tunnel-e2e-config.js +0 -29
  126. package/dist/src/tunnel/tunnel-e2e-config.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"tunnel-service.js","names":[],"sources":["../../../src/tunnel/tunnel-service.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { EventEmitter } from 'node:events';\n\nimport { PACKAGE_VERSION } from '../package-version.js';\nimport { createLogger } from '../utils/logger.js';\nimport { TunnelBrokerClient, resolveBrokerApiBase } from './broker-client.js';\nimport { clearFrpcPathForProcess, ensureFrpcBinary, publishFrpcPathForProcess } from './frpc-binary.js';\nimport { writeFrpcConfig } from './frpc-config.js';\nimport { type FrpcProcessHandle, spawnFrpcProcess } from './frpc-process.js';\nimport { buildMobileConnectQrPayload, resolveLanGatewayUrl } from './tunnel-qr.js';\nimport { createPairingSecret } from './pairing.js';\nimport {\n canResumePersistedTunnel,\n persistedFromRegistration,\n registrationFromPersisted,\n} from './tunnel-persist.js';\nimport { clearTunnelState, loadTunnelState, saveTunnelState, updateTunnelState } from './tunnel-state.js';\nimport { logTunnelAudit } from './tunnel-audit.js';\nimport type { PersistedTunnelState, TunnelQrPayload, TunnelRegistration, TunnelStatus } from './tunnel-types.js';\nimport type {\n FrpcDownloadProgress,\n TunnelAcmeProgressStep,\n TunnelStartPhase,\n TunnelStartProgress,\n} from './tunnel-types.js';\nimport type { ResolvedTunnelE2eConfig } from './tunnel-e2e-config.js';\nimport { getCertStatusSummary } from './acme-cert-store.js';\nimport type { AcmeConfig } from './acme-client.js';\nimport { startTunnelTlsServer, stopTunnelTlsServer } from './tls-server.js';\n\nconst log = createLogger('Tunnel');\n\nexport type TunnelServiceConfig = {\n brokerUrl: string;\n registrationSecret: string;\n autoStart: boolean;\n gatewayHost: string;\n e2e: ResolvedTunnelE2eConfig;\n frpSubdomainHost: string;\n gatewayFetch?: typeof fetch;\n};\n\nexport function hashGatewayToken(token: string): string {\n return createHash('sha256').update(token, 'utf8').digest('hex');\n}\n\nfunction platformLabel(): string {\n return `${process.platform}-${process.arch}`;\n}\n\nlet singleton: TunnelService | null = null;\n\nexport function getTunnelService(): TunnelService {\n if (!singleton) singleton = new TunnelService();\n return singleton;\n}\n\nexport class TunnelService extends EventEmitter {\n private serviceConfig: TunnelServiceConfig | null = null;\n private pendingGatewayFetch: typeof fetch | undefined;\n private frpcHandle: FrpcProcessHandle | null = null;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private connectedSince: string | null = null;\n private lastHeartbeatAt: string | null = null;\n private lastError: string | null = null;\n private state: TunnelStatus['state'] = 'disconnected';\n private reconnectAttempt = 0;\n private stopping = false;\n private startContext: { gatewayPort: number; gatewayToken: string } | null = null;\n private frpcDownload: FrpcDownloadProgress | null = null;\n private startProgress: TunnelStartProgress | null = null;\n\n configure(cfg: TunnelServiceConfig): void {\n this.serviceConfig = {\n ...cfg,\n gatewayFetch: cfg.gatewayFetch ?? this.pendingGatewayFetch ?? this.serviceConfig?.gatewayFetch,\n };\n }\n\n setGatewayFetch(fetchFn: typeof fetch): void {\n this.pendingGatewayFetch = fetchFn;\n if (this.serviceConfig) {\n this.serviceConfig.gatewayFetch = fetchFn;\n }\n }\n\n getStatus(): TunnelStatus {\n const cfg = this.serviceConfig;\n const persisted = loadTunnelState();\n return {\n enabled: this.state === 'connected' || this.state === 'connecting' || this.state === 'reconnecting',\n state: this.state,\n subdomain: persisted?.subdomain ?? null,\n publicUrl: persisted?.publicUrl ?? null,\n connectedSince: this.connectedSince,\n frpcPid: this.frpcHandle?.pid ?? null,\n lastHeartbeatAt: this.lastHeartbeatAt,\n lastError: this.lastError,\n frpcDownload: this.frpcDownload,\n startProgress: this.startProgress,\n config: {\n autoStart: cfg?.autoStart ?? false,\n brokerUrl: cfg?.brokerUrl ?? 'https://frp.xopc.ai/api',\n e2e: {\n enabled: cfg?.e2e.enabled ?? true,\n tlsPort: cfg?.e2e.tlsPort ?? 18791,\n staging: cfg?.e2e.staging ?? false,\n },\n },\n cert: getCertStatusSummary(),\n };\n }\n\n private setStartPhase(\n phase: TunnelStartPhase,\n patch?: Partial<Pick<TunnelStartProgress, 'acmeStep' | 'publicUrl'>>,\n ): void {\n const prev = this.startProgress;\n const samePhase = prev?.phase === phase;\n this.startProgress = {\n phase,\n startedAt: samePhase && prev ? prev.startedAt : new Date().toISOString(),\n acmeStep:\n patch?.acmeStep !== undefined\n ? patch.acmeStep\n : samePhase\n ? (prev?.acmeStep ?? null)\n : null,\n publicUrl:\n patch?.publicUrl !== undefined\n ? patch.publicUrl\n : samePhase\n ? (prev?.publicUrl ?? null)\n : (prev?.publicUrl ?? null),\n };\n this.emit('tunnel:progress');\n }\n\n private clearStartProgress(): void {\n if (!this.startProgress) return;\n this.startProgress = null;\n this.emit('tunnel:progress');\n }\n\n buildQr(gatewayPort: number, gatewayHost: string): TunnelQrPayload {\n const persisted = loadTunnelState();\n const publicUrl = persisted?.publicUrl ?? null;\n if (!publicUrl) {\n return { qrPayload: '', publicUrl: null, lanUrl: null };\n }\n const { secret, expiresAt } = createPairingSecret();\n return buildMobileConnectQrPayload({\n publicUrl,\n lanUrl: resolveLanGatewayUrl(gatewayHost, gatewayPort),\n pairingSecret: secret,\n expiresAt: expiresAt.toISOString(),\n });\n }\n\n async start(gatewayPort: number, gatewayToken: string): Promise<TunnelQrPayload> {\n const cfg = this.serviceConfig;\n if (!cfg) throw new Error('Tunnel service not configured');\n\n this.stopping = false;\n this.startContext = { gatewayPort, gatewayToken };\n this.state = 'connecting';\n this.lastError = null;\n this.frpcDownload = null;\n this.startProgress = null;\n this.setStartPhase('preparing_frpc');\n this.emit('tunnel:connecting');\n\n let frpcBin: string;\n try {\n frpcBin = await ensureFrpcBinary({\n onProgress: (progress) => {\n this.frpcDownload = progress;\n this.setStartPhase('preparing_frpc');\n },\n });\n this.frpcDownload = null;\n this.emit('tunnel:progress');\n } catch (err) {\n this.frpcDownload = null;\n this.clearStartProgress();\n this.state = 'error';\n this.lastError = err instanceof Error ? err.message : String(err);\n this.emit('tunnel:error', this.lastError);\n throw err;\n }\n publishFrpcPathForProcess(frpcBin);\n const persisted = loadTunnelState();\n const broker = new TunnelBrokerClient(resolveBrokerApiBase(cfg.brokerUrl));\n\n let registration: TunnelRegistration;\n try {\n this.setStartPhase('registering');\n registration = await this.resolveRegistration(broker, cfg, gatewayToken, persisted);\n } catch (err) {\n this.clearStartProgress();\n this.state = 'error';\n this.lastError = err instanceof Error ? err.message : String(err);\n this.emit('tunnel:error', this.lastError);\n throw err;\n }\n\n const state = persistedFromRegistration(registration);\n saveTunnelState(state);\n this.setStartPhase('registering', { publicUrl: registration.publicUrl });\n\n try {\n const { frpcLocalPort, frpcMode } = await this.prepareFrpcTarget(\n broker,\n registration,\n cfg,\n gatewayPort,\n );\n const configPath = writeFrpcConfig(registration, frpcLocalPort, '127.0.0.1', frpcMode);\n this.setStartPhase('starting_frpc', { publicUrl: registration.publicUrl });\n await this.spawnAndWait(frpcBin, configPath, broker, state, registration.heartbeatIntervalMs);\n } catch (err) {\n this.clearStartProgress();\n this.state = 'error';\n this.lastError = err instanceof Error ? err.message : String(err);\n this.emit('tunnel:error', this.lastError);\n throw err;\n }\n\n this.clearStartProgress();\n this.state = 'connected';\n this.connectedSince = new Date().toISOString();\n this.reconnectAttempt = 0;\n this.emit('tunnel:connected');\n\n const qr = this.buildQr(gatewayPort, cfg.gatewayHost);\n logTunnelAudit(\n 'tunnel.start',\n {\n subdomain: registration.subdomain,\n publicUrl: registration.publicUrl,\n tunnelId: registration.tunnelId,\n gatewayTokenHash: hashGatewayToken(gatewayToken).slice(0, 12),\n },\n 'Remote access tunnel started',\n );\n return qr;\n }\n\n async stop(opts?: { release?: boolean }): Promise<{ released: boolean }> {\n const release = opts?.release === true;\n this.stopping = true;\n this.clearHeartbeat();\n stopTunnelTlsServer();\n if (this.frpcHandle) {\n await this.frpcHandle.kill();\n this.frpcHandle = null;\n }\n clearFrpcPathForProcess();\n\n let released = false;\n const persisted = loadTunnelState();\n const cfg = this.serviceConfig;\n if (release && persisted && cfg) {\n const broker = new TunnelBrokerClient(resolveBrokerApiBase(cfg.brokerUrl));\n try {\n await broker.deregister(persisted.tunnelId, persisted.tunnelToken);\n released = true;\n } catch (err) {\n const em = err instanceof Error ? err.message : String(err);\n log.warn({ err, tunnelId: persisted.tunnelId, phase: 'tunnel_release' }, `Tunnel deregister failed: ${em}`);\n }\n clearTunnelState();\n logTunnelAudit(\n 'tunnel.release',\n { tunnelId: persisted.tunnelId, subdomain: persisted.subdomain },\n 'Released tunnel registration and cleared local credentials',\n );\n } else {\n updateTunnelState({ enabled: false });\n logTunnelAudit(\n 'tunnel.stop',\n { tunnelId: persisted?.tunnelId ?? null, subdomain: persisted?.subdomain ?? null },\n 'Remote access tunnel stopped (registration retained)',\n );\n }\n\n this.state = 'disconnected';\n this.connectedSince = null;\n this.startContext = null;\n this.clearStartProgress();\n this.emit('tunnel:disconnected');\n return { released };\n }\n\n /**\n * Reuse Broker registration when possible so subdomain and URLs stay stable across stop/start.\n */\n private async resolveRegistration(\n broker: TunnelBrokerClient,\n cfg: TunnelServiceConfig,\n gatewayToken: string,\n persisted: PersistedTunnelState | null,\n ): Promise<TunnelRegistration> {\n if (canResumePersistedTunnel(persisted)) {\n try {\n await broker.heartbeat(persisted.tunnelId, persisted.tunnelToken);\n const resumed = registrationFromPersisted(persisted);\n if (resumed) {\n log.info(\n { tunnelId: persisted.tunnelId, subdomain: persisted.subdomain, phase: 'tunnel_resume' },\n 'Resumed tunnel from persisted credentials',\n );\n return resumed;\n }\n } catch (err) {\n log.info(\n { err, tunnelId: persisted.tunnelId, phase: 'tunnel_resume' },\n 'Persisted tunnel not resumable — registering again',\n );\n }\n }\n\n return broker.register({\n brokerUrl: resolveBrokerApiBase(cfg.brokerUrl),\n registrationSecret: cfg.registrationSecret,\n gatewayVersion: PACKAGE_VERSION,\n platform: platformLabel(),\n gatewayTokenHash: hashGatewayToken(gatewayToken),\n preferredSubdomain: persisted?.subdomain,\n });\n }\n\n private async spawnAndWait(\n frpcBin: string,\n configPath: string,\n broker: TunnelBrokerClient,\n state: PersistedTunnelState,\n heartbeatIntervalMs: number,\n ): Promise<void> {\n if (this.frpcHandle) {\n await this.frpcHandle.kill();\n this.frpcHandle = null;\n }\n\n const handle = spawnFrpcProcess(frpcBin, configPath);\n this.frpcHandle = handle;\n\n handle.onExit((code, signal) => {\n if (this.stopping) return;\n log.warn({ code, signal, pid: handle.pid }, 'frpc process exited');\n this.clearHeartbeat();\n this.frpcHandle = null;\n void this.scheduleReconnect(frpcBin, configPath, broker, state, heartbeatIntervalMs);\n });\n\n await handle.waitForLoginSuccess;\n this.startHeartbeat(broker, state, heartbeatIntervalMs);\n }\n\n private async scheduleReconnect(\n frpcBin: string,\n configPath: string,\n broker: TunnelBrokerClient,\n state: PersistedTunnelState,\n heartbeatIntervalMs: number,\n ): Promise<void> {\n if (this.stopping) return;\n const maxAttempts = 5;\n this.reconnectAttempt += 1;\n if (this.reconnectAttempt > maxAttempts) {\n this.state = 'error';\n this.lastError = 'frpc reconnect failed after maximum attempts';\n log.error({ attempts: maxAttempts }, this.lastError);\n this.emit('tunnel:error', this.lastError);\n return;\n }\n\n this.state = 'reconnecting';\n this.setStartPhase('reconnecting_frpc', { publicUrl: state.publicUrl ?? null });\n const delayMs = Math.min(16_000, 1000 * 2 ** (this.reconnectAttempt - 1));\n log.info({ attempt: this.reconnectAttempt, delayMs }, 'Scheduling frpc reconnect');\n await new Promise((r) => setTimeout(r, delayMs));\n if (this.stopping) return;\n\n try {\n await this.spawnAndWait(frpcBin, configPath, broker, state, heartbeatIntervalMs);\n this.clearStartProgress();\n this.state = 'connected';\n this.reconnectAttempt = 0;\n this.emit('tunnel:connected');\n } catch (err) {\n this.lastError = err instanceof Error ? err.message : String(err);\n void this.scheduleReconnect(frpcBin, configPath, broker, state, heartbeatIntervalMs);\n }\n }\n\n private startHeartbeat(\n broker: TunnelBrokerClient,\n state: PersistedTunnelState,\n intervalMs: number,\n ): void {\n this.clearHeartbeat();\n const tick = async () => {\n try {\n await broker.heartbeat(state.tunnelId, state.tunnelToken);\n this.lastHeartbeatAt = new Date().toISOString();\n } catch (err) {\n log.warn({ err, tunnelId: state.tunnelId }, 'Tunnel heartbeat failed');\n if (this.stopping) return;\n this.clearHeartbeat();\n const ctx = this.startContext;\n const cfg = this.serviceConfig;\n if (!ctx || !cfg) return;\n try {\n const registration = await broker.register({\n brokerUrl: resolveBrokerApiBase(cfg.brokerUrl),\n registrationSecret: cfg.registrationSecret,\n gatewayVersion: PACKAGE_VERSION,\n platform: platformLabel(),\n gatewayTokenHash: hashGatewayToken(ctx.gatewayToken),\n preferredSubdomain: state.subdomain,\n });\n const next = persistedFromRegistration(registration);\n saveTunnelState(next);\n const frpcBin = await ensureFrpcBinary();\n const { frpcLocalPort, frpcMode } = await this.prepareFrpcTarget(\n broker,\n registration,\n cfg,\n ctx.gatewayPort,\n );\n const configPath = writeFrpcConfig(registration, frpcLocalPort, '127.0.0.1', frpcMode);\n await this.spawnAndWait(frpcBin, configPath, broker, next, registration.heartbeatIntervalMs);\n } catch (reErr) {\n this.lastError = reErr instanceof Error ? reErr.message : String(reErr);\n this.state = 'error';\n this.emit('tunnel:error', this.lastError);\n }\n }\n };\n void tick();\n this.heartbeatTimer = setInterval(() => void tick(), intervalMs);\n }\n\n private clearHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n private async prepareFrpcTarget(\n broker: TunnelBrokerClient,\n registration: TunnelRegistration,\n cfg: TunnelServiceConfig,\n gatewayPort: number,\n ): Promise<{ frpcLocalPort: number; frpcMode: 'http' | 'https' }> {\n if (!cfg.e2e.enabled) {\n return { frpcLocalPort: gatewayPort, frpcMode: 'http' };\n }\n\n this.setStartPhase('provisioning_tls', { publicUrl: registration.publicUrl });\n\n const acmeConfig: AcmeConfig = {\n broker,\n tunnelId: registration.tunnelId,\n tunnelToken: registration.tunnelToken,\n subdomain: registration.subdomain,\n frpSubdomainHost: cfg.frpSubdomainHost,\n staging: cfg.e2e.staging,\n onProgress: (step: TunnelAcmeProgressStep) => {\n this.setStartPhase('provisioning_tls', {\n publicUrl: registration.publicUrl,\n acmeStep: step,\n });\n },\n };\n\n await startTunnelTlsServer({\n tlsPort: cfg.e2e.tlsPort,\n gatewayPort,\n acmeConfig,\n fetch: cfg.gatewayFetch,\n });\n\n return { frpcLocalPort: cfg.e2e.tlsPort, frpcMode: 'https' };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tunnel-service.js","names":[],"sources":["../../../src/tunnel/tunnel-service.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { EventEmitter } from 'node:events';\n\nimport { PACKAGE_VERSION } from '../package-version.js';\nimport { createLogger } from '../utils/logger.js';\nimport { TunnelBrokerClient, resolveBrokerApiBase } from './broker-client.js';\nimport { clearFrpcPathForProcess, ensureFrpcBinary, publishFrpcPathForProcess } from './frpc-binary.js';\nimport { writeFrpcConfig } from './frpc-config.js';\nimport { type FrpcProcessHandle, spawnFrpcProcess } from './frpc-process.js';\nimport { buildMobileConnectQrPayload, resolveLanGatewayUrl } from './tunnel-qr.js';\nimport { createPairingSecret } from './pairing.js';\nimport {\n canResumePersistedTunnel,\n persistedFromRegistration,\n registrationFromPersisted,\n} from './tunnel-persist.js';\nimport { clearTunnelState, loadTunnelState, saveTunnelState, updateTunnelState } from './tunnel-state.js';\nimport { logTunnelAudit } from './tunnel-audit.js';\nimport type { PersistedTunnelState, TunnelQrPayload, TunnelRegistration, TunnelStatus } from './tunnel-types.js';\nimport type { FrpcDownloadProgress, TunnelStartPhase, TunnelStartProgress } from './tunnel-types.js';\n\nconst log = createLogger('Tunnel');\n\nexport type TunnelServiceConfig = {\n brokerUrl: string;\n registrationSecret: string;\n autoStart: boolean;\n gatewayHost: string;\n frpSubdomainHost: string;\n};\n\nexport function hashGatewayToken(token: string): string {\n return createHash('sha256').update(token, 'utf8').digest('hex');\n}\n\nfunction platformLabel(): string {\n return `${process.platform}-${process.arch}`;\n}\n\nlet singleton: TunnelService | null = null;\n\nexport function getTunnelService(): TunnelService {\n if (!singleton) singleton = new TunnelService();\n return singleton;\n}\n\nexport class TunnelService extends EventEmitter {\n private serviceConfig: TunnelServiceConfig | null = null;\n private frpcHandle: FrpcProcessHandle | null = null;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private connectedSince: string | null = null;\n private lastHeartbeatAt: string | null = null;\n private lastError: string | null = null;\n private state: TunnelStatus['state'] = 'disconnected';\n private reconnectAttempt = 0;\n private stopping = false;\n private startContext: { gatewayPort: number; gatewayToken: string } | null = null;\n private frpcDownload: FrpcDownloadProgress | null = null;\n private startProgress: TunnelStartProgress | null = null;\n\n configure(cfg: TunnelServiceConfig): void {\n this.serviceConfig = cfg;\n }\n\n getStatus(): TunnelStatus {\n const cfg = this.serviceConfig;\n const persisted = loadTunnelState();\n return {\n enabled: this.state === 'connected' || this.state === 'connecting' || this.state === 'reconnecting',\n state: this.state,\n subdomain: persisted?.subdomain ?? null,\n publicUrl: persisted?.publicUrl ?? null,\n connectedSince: this.connectedSince,\n frpcPid: this.frpcHandle?.pid ?? null,\n lastHeartbeatAt: this.lastHeartbeatAt,\n lastError: this.lastError,\n frpcDownload: this.frpcDownload,\n startProgress: this.startProgress,\n config: {\n autoStart: cfg?.autoStart ?? false,\n brokerUrl: cfg?.brokerUrl ?? 'https://frp.xopc.ai/api',\n transport: { tls: 'broker_terminated' },\n },\n };\n }\n\n private setStartPhase(phase: TunnelStartPhase, patch?: Partial<Pick<TunnelStartProgress, 'publicUrl'>>): void {\n const prev = this.startProgress;\n const samePhase = prev?.phase === phase;\n this.startProgress = {\n phase,\n startedAt: samePhase && prev ? prev.startedAt : new Date().toISOString(),\n publicUrl:\n patch?.publicUrl !== undefined\n ? patch.publicUrl\n : samePhase\n ? (prev?.publicUrl ?? null)\n : (prev?.publicUrl ?? null),\n };\n this.emit('tunnel:progress');\n }\n\n private clearStartProgress(): void {\n if (!this.startProgress) return;\n this.startProgress = null;\n this.emit('tunnel:progress');\n }\n\n async buildQr(gatewayPort: number, gatewayHost: string): Promise<TunnelQrPayload> {\n const persisted = loadTunnelState();\n const publicUrl = persisted?.publicUrl ?? null;\n if (!publicUrl) {\n return { qrPayload: '', publicUrl: null, lanUrl: null };\n }\n const { secret, expiresAt } = createPairingSecret();\n return buildMobileConnectQrPayload({\n publicUrl,\n lanUrl: resolveLanGatewayUrl(gatewayHost, gatewayPort),\n pairingSecret: secret,\n expiresAt: expiresAt.toISOString(),\n });\n }\n\n async start(gatewayPort: number, gatewayToken: string): Promise<TunnelQrPayload> {\n const cfg = this.serviceConfig;\n if (!cfg) throw new Error('Tunnel service not configured');\n\n this.stopping = false;\n this.startContext = { gatewayPort, gatewayToken };\n this.state = 'connecting';\n this.lastError = null;\n this.frpcDownload = null;\n this.startProgress = null;\n this.setStartPhase('preparing_frpc');\n this.emit('tunnel:connecting');\n\n let frpcBin: string;\n try {\n frpcBin = await ensureFrpcBinary({\n onProgress: (progress) => {\n this.frpcDownload = progress;\n this.setStartPhase('preparing_frpc');\n },\n });\n this.frpcDownload = null;\n this.emit('tunnel:progress');\n } catch (err) {\n this.frpcDownload = null;\n this.clearStartProgress();\n this.state = 'error';\n this.lastError = err instanceof Error ? err.message : String(err);\n this.emit('tunnel:error', this.lastError);\n throw err;\n }\n publishFrpcPathForProcess(frpcBin);\n const persisted = loadTunnelState();\n const broker = new TunnelBrokerClient(resolveBrokerApiBase(cfg.brokerUrl));\n\n let registration: TunnelRegistration;\n try {\n this.setStartPhase('registering');\n registration = await this.resolveRegistration(broker, cfg, gatewayToken, persisted);\n } catch (err) {\n this.clearStartProgress();\n this.state = 'error';\n this.lastError = err instanceof Error ? err.message : String(err);\n this.emit('tunnel:error', this.lastError);\n throw err;\n }\n\n const state = persistedFromRegistration(registration);\n saveTunnelState(state);\n this.setStartPhase('registering', { publicUrl: registration.publicUrl });\n\n try {\n const configPath = writeFrpcConfig(registration, gatewayPort, '127.0.0.1', 'http');\n this.setStartPhase('starting_frpc', { publicUrl: registration.publicUrl });\n await this.spawnAndWait(frpcBin, configPath, broker, state, registration.heartbeatIntervalMs);\n } catch (err) {\n this.clearStartProgress();\n this.state = 'error';\n this.lastError = err instanceof Error ? err.message : String(err);\n this.emit('tunnel:error', this.lastError);\n throw err;\n }\n\n this.clearStartProgress();\n this.state = 'connected';\n this.connectedSince = new Date().toISOString();\n this.reconnectAttempt = 0;\n this.emit('tunnel:connected');\n\n const qr = await this.buildQr(gatewayPort, cfg.gatewayHost);\n logTunnelAudit(\n 'tunnel.start',\n {\n subdomain: registration.subdomain,\n publicUrl: registration.publicUrl,\n tunnelId: registration.tunnelId,\n gatewayTokenHash: hashGatewayToken(gatewayToken).slice(0, 12),\n },\n 'Remote access tunnel started',\n );\n return qr;\n }\n\n async stop(opts?: { release?: boolean }): Promise<{ released: boolean }> {\n const release = opts?.release === true;\n this.stopping = true;\n this.clearHeartbeat();\n if (this.frpcHandle) {\n await this.frpcHandle.kill();\n this.frpcHandle = null;\n }\n clearFrpcPathForProcess();\n\n let released = false;\n const persisted = loadTunnelState();\n const cfg = this.serviceConfig;\n if (release && persisted && cfg) {\n const broker = new TunnelBrokerClient(resolveBrokerApiBase(cfg.brokerUrl));\n try {\n await broker.deregister(persisted.tunnelId, persisted.tunnelToken);\n released = true;\n } catch (err) {\n const em = err instanceof Error ? err.message : String(err);\n log.warn({ err, tunnelId: persisted.tunnelId, phase: 'tunnel_release' }, `Tunnel deregister failed: ${em}`);\n }\n clearTunnelState();\n logTunnelAudit(\n 'tunnel.release',\n { tunnelId: persisted.tunnelId, subdomain: persisted.subdomain },\n 'Released tunnel registration and cleared local credentials',\n );\n } else {\n updateTunnelState({ enabled: false });\n logTunnelAudit(\n 'tunnel.stop',\n { tunnelId: persisted?.tunnelId ?? null, subdomain: persisted?.subdomain ?? null },\n 'Remote access tunnel stopped (registration retained)',\n );\n }\n\n this.state = 'disconnected';\n this.connectedSince = null;\n this.startContext = null;\n this.clearStartProgress();\n this.emit('tunnel:disconnected');\n return { released };\n }\n\n private async resolveRegistration(\n broker: TunnelBrokerClient,\n cfg: TunnelServiceConfig,\n gatewayToken: string,\n persisted: PersistedTunnelState | null,\n ): Promise<TunnelRegistration> {\n if (canResumePersistedTunnel(persisted)) {\n try {\n await broker.heartbeat(persisted.tunnelId, persisted.tunnelToken);\n const resumed = registrationFromPersisted(persisted);\n if (resumed) {\n log.info(\n { tunnelId: persisted.tunnelId, subdomain: persisted.subdomain, phase: 'tunnel_resume' },\n 'Resumed tunnel from persisted credentials',\n );\n return resumed;\n }\n } catch (err) {\n log.info(\n { err, tunnelId: persisted.tunnelId, phase: 'tunnel_resume' },\n 'Persisted tunnel not resumable — registering again',\n );\n }\n }\n\n return broker.register({\n brokerUrl: resolveBrokerApiBase(cfg.brokerUrl),\n registrationSecret: cfg.registrationSecret,\n gatewayVersion: PACKAGE_VERSION,\n platform: platformLabel(),\n gatewayTokenHash: hashGatewayToken(gatewayToken),\n preferredSubdomain: persisted?.subdomain,\n });\n }\n\n private async spawnAndWait(\n frpcBin: string,\n configPath: string,\n broker: TunnelBrokerClient,\n state: PersistedTunnelState,\n heartbeatIntervalMs: number,\n ): Promise<void> {\n if (this.frpcHandle) {\n await this.frpcHandle.kill();\n this.frpcHandle = null;\n }\n\n const handle = spawnFrpcProcess(frpcBin, configPath);\n this.frpcHandle = handle;\n\n handle.onExit((code, signal) => {\n if (this.stopping) return;\n log.warn({ code, signal, pid: handle.pid }, 'frpc process exited');\n this.clearHeartbeat();\n this.frpcHandle = null;\n void this.scheduleReconnect(frpcBin, configPath, broker, state, heartbeatIntervalMs);\n });\n\n await handle.waitForLoginSuccess;\n this.startHeartbeat(broker, state, heartbeatIntervalMs);\n }\n\n private async scheduleReconnect(\n frpcBin: string,\n configPath: string,\n broker: TunnelBrokerClient,\n state: PersistedTunnelState,\n heartbeatIntervalMs: number,\n ): Promise<void> {\n if (this.stopping) return;\n const maxAttempts = 5;\n this.reconnectAttempt += 1;\n if (this.reconnectAttempt > maxAttempts) {\n this.state = 'error';\n this.lastError = 'frpc reconnect failed after maximum attempts';\n log.error({ attempts: maxAttempts }, this.lastError);\n this.emit('tunnel:error', this.lastError);\n return;\n }\n\n this.state = 'reconnecting';\n this.setStartPhase('reconnecting_frpc', { publicUrl: state.publicUrl ?? null });\n const delayMs = Math.min(16_000, 1000 * 2 ** (this.reconnectAttempt - 1));\n log.info({ attempt: this.reconnectAttempt, delayMs }, 'Scheduling frpc reconnect');\n await new Promise((r) => setTimeout(r, delayMs));\n if (this.stopping) return;\n\n try {\n await this.spawnAndWait(frpcBin, configPath, broker, state, heartbeatIntervalMs);\n this.clearStartProgress();\n this.state = 'connected';\n this.reconnectAttempt = 0;\n this.emit('tunnel:connected');\n } catch (err) {\n this.lastError = err instanceof Error ? err.message : String(err);\n void this.scheduleReconnect(frpcBin, configPath, broker, state, heartbeatIntervalMs);\n }\n }\n\n private startHeartbeat(\n broker: TunnelBrokerClient,\n state: PersistedTunnelState,\n intervalMs: number,\n ): void {\n this.clearHeartbeat();\n const tick = async () => {\n try {\n await broker.heartbeat(state.tunnelId, state.tunnelToken);\n this.lastHeartbeatAt = new Date().toISOString();\n } catch (err) {\n log.warn({ err, tunnelId: state.tunnelId }, 'Tunnel heartbeat failed');\n if (this.stopping) return;\n this.clearHeartbeat();\n const ctx = this.startContext;\n const cfg = this.serviceConfig;\n if (!ctx || !cfg) return;\n try {\n const registration = await broker.register({\n brokerUrl: resolveBrokerApiBase(cfg.brokerUrl),\n registrationSecret: cfg.registrationSecret,\n gatewayVersion: PACKAGE_VERSION,\n platform: platformLabel(),\n gatewayTokenHash: hashGatewayToken(ctx.gatewayToken),\n preferredSubdomain: state.subdomain,\n });\n const next = persistedFromRegistration(registration);\n saveTunnelState(next);\n const frpcBin = await ensureFrpcBinary();\n const configPath = writeFrpcConfig(registration, ctx.gatewayPort, '127.0.0.1', 'http');\n await this.spawnAndWait(frpcBin, configPath, broker, next, registration.heartbeatIntervalMs);\n } catch (reErr) {\n this.lastError = reErr instanceof Error ? reErr.message : String(reErr);\n this.state = 'error';\n this.emit('tunnel:error', this.lastError);\n }\n }\n };\n void tick();\n this.heartbeatTimer = setInterval(() => void tick(), intervalMs);\n }\n\n private clearHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n}\n"],"mappings":"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"}
@@ -5,12 +5,10 @@ export type FrpcDownloadProgress = {
5
5
  totalBytes?: number | null;
6
6
  percent?: number | null;
7
7
  };
8
- export type TunnelStartPhase = 'preparing_frpc' | 'registering' | 'provisioning_tls' | 'starting_frpc' | 'reconnecting_frpc';
9
- export type TunnelAcmeProgressStep = 'checking' | 'dns_challenge' | 'dns_propagation' | 'ca_validation' | 'issuing';
8
+ export type TunnelStartPhase = 'preparing_frpc' | 'registering' | 'starting_frpc' | 'reconnecting_frpc';
10
9
  export type TunnelStartProgress = {
11
10
  phase: TunnelStartPhase;
12
11
  startedAt: string;
13
- acmeStep?: TunnelAcmeProgressStep | null;
14
12
  publicUrl?: string | null;
15
13
  };
16
14
  export type TunnelRegistration = {
@@ -55,23 +53,10 @@ export type TunnelStatus = {
55
53
  config: {
56
54
  autoStart: boolean;
57
55
  brokerUrl: string;
58
- e2e: {
59
- enabled: boolean;
60
- tlsPort: number;
61
- staging: boolean;
56
+ transport: {
57
+ tls: 'broker_terminated';
62
58
  };
63
59
  };
64
- cert: {
65
- status: 'no_cert' | 'healthy' | 'expiring_soon' | 'critical' | 'renewal_failed';
66
- domain: string | null;
67
- issuedAt: string | null;
68
- expiresAt: string | null;
69
- daysUntilExpiry: number | null;
70
- autoRenewal: boolean;
71
- renewalFailed: boolean;
72
- lastRenewalError: string | null;
73
- lastRenewalErrorAt: string | null;
74
- };
75
60
  };
76
61
  export type TunnelQrPayload = {
77
62
  qrPayload: string;
@@ -1,3 +1,7 @@
1
+ export type TunnelWellKnownTransport = {
2
+ tls: 'broker_terminated' | string;
3
+ publicScheme?: string;
4
+ };
1
5
  export type TunnelWellKnownConfig = {
2
6
  brokerUrl: string;
3
7
  frp: {
@@ -7,6 +11,7 @@ export type TunnelWellKnownConfig = {
7
11
  };
8
12
  frpcVersion: string;
9
13
  heartbeatIntervalMs: number;
14
+ transport?: TunnelWellKnownTransport;
10
15
  };
11
16
  /**
12
17
  * Fetch `/.well-known/tunnel-config` (broker URL, frp endpoints, frpc version).
@@ -26,7 +26,8 @@ async function fetchTunnelWellKnown(brokerUrl) {
26
26
  };
27
27
  log.debug({
28
28
  url,
29
- brokerUrl: body.brokerUrl
29
+ brokerUrl: body.brokerUrl,
30
+ transport: body.transport?.tls
30
31
  }, "Loaded tunnel well-known config");
31
32
  return body;
32
33
  }
@@ -1 +1 @@
1
- {"version":3,"file":"well-known.js","names":[],"sources":["../../../src/tunnel/well-known.ts"],"sourcesContent":["import { createLogger } from '../utils/logger.js';\n\nconst log = createLogger('Tunnel');\n\nexport type TunnelWellKnownConfig = {\n brokerUrl: string;\n frp: {\n serverAddr: string;\n serverPort: number;\n subdomainHost: string;\n };\n frpcVersion: string;\n heartbeatIntervalMs: number;\n};\n\nlet cached: { origin: string; config: TunnelWellKnownConfig; fetchedAt: number } | null = null;\nconst CACHE_TTL_MS = 5 * 60_000;\n\nfunction brokerOrigin(brokerUrl: string): string {\n const u = new URL(brokerUrl.replace(/\\/+$/, '').replace(/\\/api\\/?$/, '') || brokerUrl);\n return u.origin;\n}\n\n/**\n * Fetch `/.well-known/tunnel-config` (broker URL, frp endpoints, frpc version).\n * Registration secret is never published here — use `XOPC_TUNNEL_REGISTRATION_SECRET`.\n */\nexport async function fetchTunnelWellKnown(brokerUrl: string): Promise<TunnelWellKnownConfig> {\n const origin = brokerOrigin(brokerUrl);\n if (cached && cached.origin === origin && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {\n return cached.config;\n }\n\n const url = `${origin}/.well-known/tunnel-config`;\n const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });\n if (!res.ok) {\n throw new Error(`Tunnel well-known fetch failed: ${res.status} ${res.statusText}`);\n }\n const body = (await res.json()) as TunnelWellKnownConfig;\n cached = { origin, config: body, fetchedAt: Date.now() };\n log.debug({ url, brokerUrl: body.brokerUrl }, 'Loaded tunnel well-known config');\n return body;\n}\n\nexport async function pingTunnelBroker(brokerUrl: string): Promise<boolean> {\n try {\n await fetchTunnelWellKnown(brokerUrl);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function clearTunnelWellKnownCache(): void {\n cached = null;\n}\n"],"mappings":";;;aAAkD;AAElD,MAAM,MAAM,aAAa,SAAS;AAalC,IAAI,SAAsF;AAC1F,MAAM,eAAe,IAAI;AAEzB,SAAS,aAAa,WAA2B;AAE/C,QAAO,IADO,IAAI,UAAU,QAAQ,QAAQ,GAAG,CAAC,QAAQ,aAAa,GAAG,IAAI,UACpE,CAAC;;;;;;AAOX,eAAsB,qBAAqB,WAAmD;CAC5F,MAAM,SAAS,aAAa,UAAU;AACtC,KAAI,UAAU,OAAO,WAAW,UAAU,KAAK,KAAK,GAAG,OAAO,YAAY,aACxE,QAAO,OAAO;CAGhB,MAAM,MAAM,GAAG,OAAO;CACtB,MAAM,MAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,YAAY,QAAQ,IAAO,EAAE,CAAC;AACrE,KAAI,CAAC,IAAI,GACP,OAAM,IAAI,MAAM,mCAAmC,IAAI,OAAO,GAAG,IAAI,aAAa;CAEpF,MAAM,OAAQ,MAAM,IAAI,MAAM;AAC9B,UAAS;EAAE;EAAQ,QAAQ;EAAM,WAAW,KAAK,KAAK;EAAE;AACxD,KAAI,MAAM;EAAE;EAAK,WAAW,KAAK;EAAW,EAAE,kCAAkC;AAChF,QAAO;;AAGT,eAAsB,iBAAiB,WAAqC;AAC1E,KAAI;AACF,QAAM,qBAAqB,UAAU;AACrC,SAAO;SACD;AACN,SAAO;;;AAIX,SAAgB,4BAAkC;AAChD,UAAS"}
1
+ {"version":3,"file":"well-known.js","names":[],"sources":["../../../src/tunnel/well-known.ts"],"sourcesContent":["import { createLogger } from '../utils/logger.js';\n\nconst log = createLogger('Tunnel');\n\nexport type TunnelWellKnownTransport = {\n tls: 'broker_terminated' | string;\n publicScheme?: string;\n};\n\nexport type TunnelWellKnownConfig = {\n brokerUrl: string;\n frp: {\n serverAddr: string;\n serverPort: number;\n subdomainHost: string;\n };\n frpcVersion: string;\n heartbeatIntervalMs: number;\n transport?: TunnelWellKnownTransport;\n};\n\nlet cached: { origin: string; config: TunnelWellKnownConfig; fetchedAt: number } | null = null;\nconst CACHE_TTL_MS = 5 * 60_000;\n\nfunction brokerOrigin(brokerUrl: string): string {\n const u = new URL(brokerUrl.replace(/\\/+$/, '').replace(/\\/api\\/?$/, '') || brokerUrl);\n return u.origin;\n}\n\n/**\n * Fetch `/.well-known/tunnel-config` (broker URL, frp endpoints, frpc version).\n * Registration secret is never published here — use `XOPC_TUNNEL_REGISTRATION_SECRET`.\n */\nexport async function fetchTunnelWellKnown(brokerUrl: string): Promise<TunnelWellKnownConfig> {\n const origin = brokerOrigin(brokerUrl);\n if (cached && cached.origin === origin && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {\n return cached.config;\n }\n\n const url = `${origin}/.well-known/tunnel-config`;\n const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });\n if (!res.ok) {\n throw new Error(`Tunnel well-known fetch failed: ${res.status} ${res.statusText}`);\n }\n const body = (await res.json()) as TunnelWellKnownConfig;\n cached = { origin, config: body, fetchedAt: Date.now() };\n log.debug({ url, brokerUrl: body.brokerUrl, transport: body.transport?.tls }, 'Loaded tunnel well-known config');\n return body;\n}\n\nexport async function pingTunnelBroker(brokerUrl: string): Promise<boolean> {\n try {\n await fetchTunnelWellKnown(brokerUrl);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function clearTunnelWellKnownCache(): void {\n cached = null;\n}\n"],"mappings":";;;aAAkD;AAElD,MAAM,MAAM,aAAa,SAAS;AAmBlC,IAAI,SAAsF;AAC1F,MAAM,eAAe,IAAI;AAEzB,SAAS,aAAa,WAA2B;AAE/C,QAAO,IADO,IAAI,UAAU,QAAQ,QAAQ,GAAG,CAAC,QAAQ,aAAa,GAAG,IAAI,UACpE,CAAC;;;;;;AAOX,eAAsB,qBAAqB,WAAmD;CAC5F,MAAM,SAAS,aAAa,UAAU;AACtC,KAAI,UAAU,OAAO,WAAW,UAAU,KAAK,KAAK,GAAG,OAAO,YAAY,aACxE,QAAO,OAAO;CAGhB,MAAM,MAAM,GAAG,OAAO;CACtB,MAAM,MAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,YAAY,QAAQ,IAAO,EAAE,CAAC;AACrE,KAAI,CAAC,IAAI,GACP,OAAM,IAAI,MAAM,mCAAmC,IAAI,OAAO,GAAG,IAAI,aAAa;CAEpF,MAAM,OAAQ,MAAM,IAAI,MAAM;AAC9B,UAAS;EAAE;EAAQ,QAAQ;EAAM,WAAW,KAAK,KAAK;EAAE;AACxD,KAAI,MAAM;EAAE;EAAK,WAAW,KAAK;EAAW,WAAW,KAAK,WAAW;EAAK,EAAE,kCAAkC;AAChH,QAAO;;AAGT,eAAsB,iBAAiB,WAAqC;AAC1E,KAAI;AACF,QAAM,qBAAqB,UAAU;AACrC,SAAO;SACD;AACN,SAAO;;;AAIX,SAAgB,4BAAkC;AAChD,UAAS"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xopcai/xopc",
3
- "version": "0.0.77",
3
+ "version": "0.0.78",
4
4
  "description": "The OPC workstation that grows with you: AI assistant for One Person Companies — CLI, gateway, multi-channel (Telegram/WeChat), 20+ LLM providers via pi-ai, extensions and skills.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -150,7 +150,7 @@
150
150
  "test:startup:bench": "node --import tsx scripts/bench-cli-startup.ts",
151
151
  "test:startup:bench:check": "node --import tsx scripts/bench-cli-startup.ts --runs 3 --warmup 1",
152
152
  "test:startup:imports": "node scripts/check-cli-bootstrap-imports.mjs",
153
- "tunnel:phase5:verify": "node scripts/tunnel-phase5-staging-verify.mjs",
153
+ "tunnel:phase6:verify": "node scripts/tunnel-phase6-broker-tls-verify.mjs",
154
154
  "release:patch": "bash scripts/release-patch.sh",
155
155
  "docs:dev": "vitepress dev docs",
156
156
  "docs:build": "vitepress build docs",