@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":"gateway-lifecycle.js","names":[],"sources":["../../../src/tunnel/gateway-lifecycle.ts"],"sourcesContent":["import type { Config } from '../config/schema.js';\nimport { resolveGatewayEffectiveHost } from '../config/gateway-bind.js';\nimport { createLogger } from '../utils/logger.js';\nimport { hasValidTunnelConsent } from './consent.js';\nimport { subscribeCertStatus } from './acme-cert-store.js';\nimport { resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret } from './env.js';\nimport { getTunnelService } from './tunnel-service.js';\nimport { resolveFrpSubdomainHost, resolveTunnelE2eConfig } from './tunnel-e2e-config.js';\nimport { fetchTunnelWellKnown } from './well-known.js';\n\nconst log = createLogger('Tunnel');\n\ntype TunnelEventSink = { emit(type: string, payload: unknown): void };\n\nlet tunnelSseWired = false;\n\n/** Publish `tunnel.status` on gateway SSE when tunnel lifecycle changes. */\nexport function wireTunnelEventsToGateway(service: TunnelEventSink): void {\n if (tunnelSseWired) return;\n tunnelSseWired = true;\n\n const tunnel = getTunnelService();\n const publish = () => {\n service.emit('tunnel.status', tunnel.getStatus());\n };\n\n tunnel.on('tunnel:connecting', publish);\n tunnel.on('tunnel:connected', publish);\n tunnel.on('tunnel:disconnected', publish);\n tunnel.on('tunnel:error', publish);\n tunnel.on('tunnel:progress', publish);\n\n subscribeCertStatus((cert) => {\n service.emit('tunnel.cert.status', cert);\n publish();\n });\n}\n\nexport type ConfigureTunnelFromGatewayConfigOptions = {\n force?: boolean;\n /** Apply config/env broker URL immediately; refresh from well-known in the background. */\n deferWellKnownFetch?: boolean;\n};\n\nfunction applyTunnelServiceFromGatewayConfig(config: Config, brokerUrl: string): void {\n const gateway = config.gateway ?? {};\n const gatewayPort = gateway.port ?? 18790;\n\n let registrationSecret: string;\n try {\n registrationSecret = resolveTunnelRegistrationSecret(\n process.env,\n brokerUrl,\n config.tunnel?.registrationSecret,\n );\n } catch (err) {\n const em = err instanceof Error ? err.message : String(err);\n log.warn({ phase: 'tunnel_configure', errorMessage: em }, em);\n throw err;\n }\n\n getTunnelService().configure({\n brokerUrl,\n registrationSecret,\n autoStart: config.tunnel?.autoStart ?? false,\n gatewayHost: resolveGatewayEffectiveHost(config),\n e2e: resolveTunnelE2eConfig(config.tunnel, gatewayPort),\n frpSubdomainHost: resolveFrpSubdomainHost(brokerUrl),\n });\n}\n\nasync function resolveBrokerUrlFromWellKnown(initialBrokerUrl: string): Promise<string> {\n let brokerUrl = initialBrokerUrl;\n try {\n const wellKnown = await fetchTunnelWellKnown(brokerUrl);\n if (wellKnown.brokerUrl?.trim()) {\n brokerUrl = wellKnown.brokerUrl.trim();\n }\n } catch (err) {\n log.debug(\n { err, brokerUrl, phase: 'tunnel_well_known' },\n 'Tunnel well-known fetch skipped (using config/env broker URL)',\n );\n }\n return brokerUrl;\n}\n\nlet deferredWellKnownRefresh: Promise<void> | null = null;\n\nfunction scheduleDeferredWellKnownRefresh(config: Config, initialBrokerUrl: string): void {\n if (deferredWellKnownRefresh) return;\n\n deferredWellKnownRefresh = resolveBrokerUrlFromWellKnown(initialBrokerUrl)\n .then((resolvedBrokerUrl) => {\n if (resolvedBrokerUrl === initialBrokerUrl) return;\n applyTunnelServiceFromGatewayConfig(config, resolvedBrokerUrl);\n })\n .catch((err) => {\n const em = err instanceof Error ? err.message : String(err);\n log.warn(\n { err, phase: 'tunnel_well_known_deferred', errorMessage: em },\n `Deferred tunnel well-known refresh failed: ${em}`,\n );\n })\n .finally(() => {\n deferredWellKnownRefresh = null;\n });\n}\n\nexport async function configureTunnelFromGatewayConfig(\n config: Config,\n opts?: ConfigureTunnelFromGatewayConfigOptions,\n): Promise<void> {\n if (!opts?.force && !config.tunnel?.enabled) return;\n\n const initialBrokerUrl = resolveTunnelBrokerUrl(config.tunnel?.brokerUrl);\n\n if (opts?.deferWellKnownFetch) {\n applyTunnelServiceFromGatewayConfig(config, initialBrokerUrl);\n scheduleDeferredWellKnownRefresh(config, initialBrokerUrl);\n return;\n }\n\n if (deferredWellKnownRefresh) {\n await deferredWellKnownRefresh;\n }\n\n const brokerUrl = await resolveBrokerUrlFromWellKnown(initialBrokerUrl);\n applyTunnelServiceFromGatewayConfig(config, brokerUrl);\n}\n\n/**\n * Start FRP tunnel when `tunnel.autoStart` is set (CLI gateway / GatewayServer after HTTP listen).\n */\nexport async function maybeAutoStartTunnelFromConfig(\n config: Config,\n gatewayToken: string | undefined,\n): Promise<void> {\n if (!config.tunnel?.autoStart) return;\n\n if (!hasValidTunnelConsent(config)) {\n log.warn(\n { phase: 'tunnel_autostart', consentVersion: config.tunnel?.consent?.version ?? null },\n 'tunnel.autoStart skipped: security consent required or outdated',\n );\n return;\n }\n\n if (config.tunnel.enabled !== true) {\n log.debug(\n { phase: 'tunnel_autostart' },\n 'tunnel.autoStart skipped: tunnel.enabled is false (start remote access once)',\n );\n return;\n }\n\n const gateway = config.gateway ?? {};\n const port = gateway.port ?? 18790;\n const host = resolveGatewayEffectiveHost(config);\n\n await configureTunnelFromGatewayConfig(config);\n\n if (!gatewayToken) {\n log.warn(\n { phase: 'tunnel_autostart' },\n 'tunnel.autoStart is enabled but gateway auth token is unavailable (auth mode may be none)',\n );\n return;\n }\n\n const tunnel = getTunnelService();\n const { state } = tunnel.getStatus();\n if (state === 'connected' || state === 'connecting' || state === 'reconnecting') {\n log.debug({ phase: 'tunnel_autostart', state }, 'Tunnel already active — skip autostart');\n return;\n }\n\n try {\n await tunnel.start(port, gatewayToken);\n log.info(\n { phase: 'tunnel_autostart', host, port },\n 'Tunnel auto-started after gateway HTTP listen',\n );\n } catch (err) {\n const em = err instanceof Error ? err.message : String(err);\n log.error({ err, phase: 'tunnel_autostart', errorMessage: em }, `Tunnel autostart failed: ${em}`);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"gateway-lifecycle.js","names":[],"sources":["../../../src/tunnel/gateway-lifecycle.ts"],"sourcesContent":["import type { Config } from '../config/schema.js';\nimport { resolveGatewayEffectiveHost } from '../config/gateway-bind.js';\nimport { createLogger } from '../utils/logger.js';\nimport { hasValidTunnelConsent } from './consent.js';\nimport { resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret } from './env.js';\nimport { getTunnelService } from './tunnel-service.js';\nimport { resolveFrpSubdomainHost } from './frp-subdomain-host.js';\nimport { fetchTunnelWellKnown } from './well-known.js';\n\nconst log = createLogger('Tunnel');\n\ntype TunnelEventSink = { emit(type: string, payload: unknown): void };\n\nlet tunnelSseWired = false;\n\n/** Publish `tunnel.status` on gateway SSE when tunnel lifecycle changes. */\nexport function wireTunnelEventsToGateway(service: TunnelEventSink): void {\n if (tunnelSseWired) return;\n tunnelSseWired = true;\n\n const tunnel = getTunnelService();\n const publish = () => {\n service.emit('tunnel.status', tunnel.getStatus());\n };\n\n tunnel.on('tunnel:connecting', publish);\n tunnel.on('tunnel:connected', publish);\n tunnel.on('tunnel:disconnected', publish);\n tunnel.on('tunnel:error', publish);\n tunnel.on('tunnel:progress', publish);\n}\n\nexport type ConfigureTunnelFromGatewayConfigOptions = {\n force?: boolean;\n deferWellKnownFetch?: boolean;\n};\n\nfunction applyTunnelServiceFromGatewayConfig(config: Config, brokerUrl: string): void {\n let registrationSecret: string;\n try {\n registrationSecret = resolveTunnelRegistrationSecret(\n process.env,\n brokerUrl,\n config.tunnel?.registrationSecret,\n );\n } catch (err) {\n const em = err instanceof Error ? err.message : String(err);\n log.warn({ phase: 'tunnel_configure', errorMessage: em }, em);\n throw err;\n }\n\n getTunnelService().configure({\n brokerUrl,\n registrationSecret,\n autoStart: config.tunnel?.autoStart ?? false,\n gatewayHost: resolveGatewayEffectiveHost(config),\n frpSubdomainHost: resolveFrpSubdomainHost(brokerUrl),\n });\n}\n\nasync function resolveBrokerUrlFromWellKnown(initialBrokerUrl: string): Promise<string> {\n let brokerUrl = initialBrokerUrl;\n try {\n const wellKnown = await fetchTunnelWellKnown(brokerUrl);\n if (wellKnown.brokerUrl?.trim()) {\n brokerUrl = wellKnown.brokerUrl.trim();\n }\n if (wellKnown.transport?.tls === 'broker_terminated') {\n log.debug({ brokerUrl, phase: 'tunnel_well_known' }, 'Broker uses wildcard TLS termination');\n } else if (wellKnown.transport?.tls) {\n log.warn(\n { tls: wellKnown.transport.tls, phase: 'tunnel_well_known' },\n 'Unexpected broker transport mode — expect broker_terminated',\n );\n }\n } catch (err) {\n log.debug(\n { err, brokerUrl, phase: 'tunnel_well_known' },\n 'Tunnel well-known fetch skipped (using config/env broker URL)',\n );\n }\n return brokerUrl;\n}\n\nlet deferredWellKnownRefresh: Promise<void> | null = null;\n\nfunction scheduleDeferredWellKnownRefresh(config: Config, initialBrokerUrl: string): void {\n if (deferredWellKnownRefresh) return;\n\n deferredWellKnownRefresh = resolveBrokerUrlFromWellKnown(initialBrokerUrl)\n .then((resolvedBrokerUrl) => {\n if (resolvedBrokerUrl === initialBrokerUrl) return;\n applyTunnelServiceFromGatewayConfig(config, resolvedBrokerUrl);\n })\n .catch((err) => {\n const em = err instanceof Error ? err.message : String(err);\n log.warn(\n { err, phase: 'tunnel_well_known_deferred', errorMessage: em },\n `Deferred tunnel well-known refresh failed: ${em}`,\n );\n })\n .finally(() => {\n deferredWellKnownRefresh = null;\n });\n}\n\nexport async function configureTunnelFromGatewayConfig(\n config: Config,\n opts?: ConfigureTunnelFromGatewayConfigOptions,\n): Promise<void> {\n if (!opts?.force && !config.tunnel?.enabled) return;\n\n const initialBrokerUrl = resolveTunnelBrokerUrl(config.tunnel?.brokerUrl);\n\n if (opts?.deferWellKnownFetch) {\n applyTunnelServiceFromGatewayConfig(config, initialBrokerUrl);\n scheduleDeferredWellKnownRefresh(config, initialBrokerUrl);\n return;\n }\n\n if (deferredWellKnownRefresh) {\n await deferredWellKnownRefresh;\n }\n\n const brokerUrl = await resolveBrokerUrlFromWellKnown(initialBrokerUrl);\n applyTunnelServiceFromGatewayConfig(config, brokerUrl);\n}\n\nexport async function maybeAutoStartTunnelFromConfig(\n config: Config,\n gatewayToken: string | undefined,\n): Promise<void> {\n if (!config.tunnel?.autoStart) return;\n\n if (!hasValidTunnelConsent(config)) {\n log.warn(\n { phase: 'tunnel_autostart', consentVersion: config.tunnel?.consent?.version ?? null },\n 'tunnel.autoStart skipped: security consent required or outdated',\n );\n return;\n }\n\n if (config.tunnel.enabled !== true) {\n log.debug(\n { phase: 'tunnel_autostart' },\n 'tunnel.autoStart skipped: tunnel.enabled is false (start remote access once)',\n );\n return;\n }\n\n const gateway = config.gateway ?? {};\n const port = gateway.port ?? 18790;\n const host = resolveGatewayEffectiveHost(config);\n\n await configureTunnelFromGatewayConfig(config);\n\n if (!gatewayToken) {\n log.warn(\n { phase: 'tunnel_autostart' },\n 'tunnel.autoStart is enabled but gateway auth token is unavailable (auth mode may be none)',\n );\n return;\n }\n\n const tunnel = getTunnelService();\n const { state } = tunnel.getStatus();\n if (state === 'connected' || state === 'connecting' || state === 'reconnecting') {\n log.debug({ phase: 'tunnel_autostart', state }, 'Tunnel already active — skip autostart');\n return;\n }\n\n try {\n await tunnel.start(port, gatewayToken);\n log.info(\n { phase: 'tunnel_autostart', host, port },\n 'Tunnel auto-started after gateway HTTP listen',\n );\n } catch (err) {\n const em = err instanceof Error ? err.message : String(err);\n log.error({ err, phase: 'tunnel_autostart', errorMessage: em }, `Tunnel autostart failed: ${em}`);\n }\n}\n"],"mappings":"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"}
@@ -5,6 +5,7 @@ export { clearFrpcPathForProcess, ensureFrpcBinary, FRPC_VERSION, publishFrpcPat
5
5
  export type { EnsureFrpcBinaryOptions, FrpcDownloadProgress } from './frpc-binary.js';
6
6
  export { configureTunnelFromGatewayConfig, maybeAutoStartTunnelFromConfig, wireTunnelEventsToGateway, } from './gateway-lifecycle.js';
7
7
  export { fetchTunnelWellKnown, clearTunnelWellKnownCache } from './well-known.js';
8
+ export type { TunnelWellKnownConfig, TunnelWellKnownTransport } from './well-known.js';
8
9
  export { getTunnelRegistrationSecretMeta, isProductionTunnelBroker, isMaskedTunnelSecretPatchValue, resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret, } from './env.js';
9
10
  export type { TunnelRegistrationSecretMeta, TunnelRegistrationSecretSource } from './env.js';
10
11
  export { logTunnelAudit } from './tunnel-audit.js';
@@ -12,10 +13,7 @@ export type { TunnelAuditEvent } from './tunnel-audit.js';
12
13
  export { consumeTunnelMutationLimit, resetTunnelMutationLimitsForTests } from './tunnel-rate-limit.js';
13
14
  export { getTunnelService, hashGatewayToken, TunnelService } from './tunnel-service.js';
14
15
  export type { TunnelServiceConfig } from './tunnel-service.js';
15
- export { resolveFrpSubdomainHost, resolveTunnelE2eConfig } from './tunnel-e2e-config.js';
16
- export type { ResolvedTunnelE2eConfig } from './tunnel-e2e-config.js';
17
- export { getCertStatusSummary, subscribeCertStatus, recordRenewalFailure } from './acme-cert-store.js';
18
- export { getActiveTlsCert, stopTunnelTlsServer } from './tls-server.js';
16
+ export { resolveFrpSubdomainHost } from './frp-subdomain-host.js';
19
17
  export { createPairingSecret, consumePairingSecret, resetPairingSessionsForTests } from './pairing.js';
20
18
  export type { PairingSecretResult } from './pairing.js';
21
19
  export { buildMobileConnectQrPayload, resolveLanGatewayUrl } from './tunnel-qr.js';
@@ -1,5 +1,4 @@
1
1
  import { CURRENT_TUNNEL_CONSENT_VERSION, TUNNEL_CONSENT_REQUIRED_CODE, TUNNEL_RISK_SUMMARY_LINES, TunnelConsentError, assertTunnelMayStart, getTunnelConsentState, hasValidTunnelConsent } from "./consent.js";
2
- import { getCertStatusSummary, recordRenewalFailure, subscribeCertStatus } from "./acme-cert-store.js";
3
2
  import { getTunnelRegistrationSecretMeta, isMaskedTunnelSecretPatchValue, isProductionTunnelBroker, resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret } from "./env.js";
4
3
  import { TunnelBrokerClient, resolveBrokerApiBase } from "./broker-client.js";
5
4
  import { FRPC_VERSION, clearFrpcPathForProcess, ensureFrpcBinary, publishFrpcPathForProcess } from "./frpc-binary.js";
@@ -7,11 +6,10 @@ import { buildMobileConnectQrPayload, resolveLanGatewayUrl } from "./tunnel-qr.j
7
6
  import { consumePairingSecret, createPairingSecret, resetPairingSessionsForTests } from "./pairing.js";
8
7
  import { loadTunnelState, resolveTunnelStatePath, saveTunnelState } from "./tunnel-state.js";
9
8
  import { logTunnelAudit } from "./tunnel-audit.js";
10
- import { getActiveTlsCert, stopTunnelTlsServer } from "./tls-server.js";
11
9
  import { TunnelService, getTunnelService, hashGatewayToken } from "./tunnel-service.js";
12
- import { resolveFrpSubdomainHost, resolveTunnelE2eConfig } from "./tunnel-e2e-config.js";
10
+ import { resolveFrpSubdomainHost } from "./frp-subdomain-host.js";
13
11
  import { clearTunnelWellKnownCache, fetchTunnelWellKnown } from "./well-known.js";
14
12
  import { configureTunnelFromGatewayConfig, maybeAutoStartTunnelFromConfig, wireTunnelEventsToGateway } from "./gateway-lifecycle.js";
15
13
  import { applyTunnelConsentToConfig, mergeTunnelConfigPatch, sanitizeTunnelConfig, setTunnelEnabledInConfig } from "./tunnel-config.js";
16
14
  import { consumeTunnelMutationLimit, resetTunnelMutationLimitsForTests } from "./tunnel-rate-limit.js";
17
- export { CURRENT_TUNNEL_CONSENT_VERSION, FRPC_VERSION, TUNNEL_CONSENT_REQUIRED_CODE, TUNNEL_RISK_SUMMARY_LINES, TunnelBrokerClient, TunnelConsentError, TunnelService, applyTunnelConsentToConfig, assertTunnelMayStart, buildMobileConnectQrPayload, clearFrpcPathForProcess, clearTunnelWellKnownCache, configureTunnelFromGatewayConfig, consumePairingSecret, consumeTunnelMutationLimit, createPairingSecret, ensureFrpcBinary, fetchTunnelWellKnown, getActiveTlsCert, getCertStatusSummary, getTunnelConsentState, getTunnelRegistrationSecretMeta, getTunnelService, hasValidTunnelConsent, hashGatewayToken, isMaskedTunnelSecretPatchValue, isProductionTunnelBroker, loadTunnelState, logTunnelAudit, maybeAutoStartTunnelFromConfig, mergeTunnelConfigPatch, publishFrpcPathForProcess, recordRenewalFailure, resetPairingSessionsForTests, resetTunnelMutationLimitsForTests, resolveBrokerApiBase, resolveFrpSubdomainHost, resolveLanGatewayUrl, resolveTunnelBrokerUrl, resolveTunnelE2eConfig, resolveTunnelRegistrationSecret, resolveTunnelStatePath, sanitizeTunnelConfig, saveTunnelState, setTunnelEnabledInConfig, stopTunnelTlsServer, subscribeCertStatus, wireTunnelEventsToGateway };
15
+ export { CURRENT_TUNNEL_CONSENT_VERSION, FRPC_VERSION, TUNNEL_CONSENT_REQUIRED_CODE, TUNNEL_RISK_SUMMARY_LINES, TunnelBrokerClient, TunnelConsentError, TunnelService, applyTunnelConsentToConfig, assertTunnelMayStart, buildMobileConnectQrPayload, clearFrpcPathForProcess, clearTunnelWellKnownCache, configureTunnelFromGatewayConfig, consumePairingSecret, consumeTunnelMutationLimit, createPairingSecret, ensureFrpcBinary, fetchTunnelWellKnown, getTunnelConsentState, getTunnelRegistrationSecretMeta, getTunnelService, hasValidTunnelConsent, hashGatewayToken, isMaskedTunnelSecretPatchValue, isProductionTunnelBroker, loadTunnelState, logTunnelAudit, maybeAutoStartTunnelFromConfig, mergeTunnelConfigPatch, publishFrpcPathForProcess, resetPairingSessionsForTests, resetTunnelMutationLimitsForTests, resolveBrokerApiBase, resolveFrpSubdomainHost, resolveLanGatewayUrl, resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret, resolveTunnelStatePath, sanitizeTunnelConfig, saveTunnelState, setTunnelEnabledInConfig, wireTunnelEventsToGateway };
@@ -60,7 +60,13 @@ function parseMobileConnectDeepLink(raw) {
60
60
  const u = new URL(trimmed);
61
61
  if (u.protocol !== "xopc:" || u.hostname !== "gateway" || u.pathname !== "/mobile-connect") return null;
62
62
  const baseUrl = u.searchParams.get("baseUrl")?.trim() ?? "";
63
- const pairingSecret = u.searchParams.get("ps")?.trim() ?? "";
63
+ const psRaw = u.searchParams.get("ps")?.trim() ?? "";
64
+ let pairingSecret = psRaw;
65
+ if (psRaw) try {
66
+ pairingSecret = decodeURIComponent(psRaw);
67
+ } catch {
68
+ pairingSecret = psRaw;
69
+ }
64
70
  if (!baseUrl || !pairingSecret) return null;
65
71
  const lanRaw = u.searchParams.get("lanUrl")?.trim() ?? "";
66
72
  const lanUrl = lanRaw ? normalizeGatewayBaseUrl(lanRaw) : null;
@@ -1 +1 @@
1
- {"version":3,"file":"pair-url.js","names":[],"sources":["../../../src/tunnel/pair-url.ts"],"sourcesContent":["import net from 'node:net';\n\nimport { isNetworkAccessibleBindHost, resolveGatewayEffectiveHost } from '../config/gateway-bind.js';\nimport type { Config } from '../config/schema.js';\nimport { enumerateLanGatewayCandidates } from './tunnel-qr.js';\n\nexport type MobilePairUrlValidationCode = 'INVALID_URL' | 'LOOPBACK_NOT_REACHABLE';\n\nexport type MobilePairUrlValidationResult =\n | { ok: true; url: string; loopback: false }\n | { ok: false; code: MobilePairUrlValidationCode; message: string };\n\n/** Normalize a gateway root URL (no trailing slash, no path). */\nexport function normalizeGatewayBaseUrl(raw: string): string | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n try {\n const u = new URL(trimmed);\n if (u.protocol !== 'http:' && u.protocol !== 'https:') {\n return null;\n }\n if (u.username || u.password) return null;\n if (u.pathname && u.pathname !== '/') return null;\n if (u.search || u.hash) return null;\n const portSuffix = u.port ? `:${u.port}` : '';\n return `${u.protocol}//${u.hostname}${portSuffix}`;\n } catch {\n return null;\n }\n}\n\n/** True when a phone on the network cannot reach this gateway root URL. */\nexport function isLoopbackGatewayBaseUrl(raw: string): boolean {\n const normalized = normalizeGatewayBaseUrl(raw);\n if (!normalized) return false;\n try {\n const u = new URL(normalized);\n const host = u.hostname.trim().toLowerCase();\n if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]') {\n return true;\n }\n const ipVersion = net.isIP(host);\n if (ipVersion === 4) {\n const parts = host.split('.').map((p) => Number(p));\n if (parts.length === 4 && parts.every((n) => Number.isInteger(n)) && parts[0] === 127) {\n return true;\n }\n }\n return false;\n } catch {\n return false;\n }\n}\n\nexport function validateMobilePairBaseUrl(raw: string): MobilePairUrlValidationResult {\n const url = normalizeGatewayBaseUrl(raw);\n if (!url) {\n return {\n ok: false,\n code: 'INVALID_URL',\n message: 'Enter an absolute gateway URL starting with http:// or https:// (no path).',\n };\n }\n if (isLoopbackGatewayBaseUrl(url)) {\n return {\n ok: false,\n code: 'LOOPBACK_NOT_REACHABLE',\n message:\n '127.0.0.1 and localhost only work on the gateway machine. Use a LAN IP or tunnel URL instead.',\n };\n }\n return { ok: true, url, loopback: false };\n}\n\nexport type ParsedMobileConnectDeepLink = {\n baseUrl: string;\n lanUrl: string | null;\n pairingSecret: string;\n};\n\n/** Parse `xopc://gateway/mobile-connect?...` payloads from QR codes. */\nexport function parseMobileConnectDeepLink(raw: string): ParsedMobileConnectDeepLink | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n try {\n const u = new URL(trimmed);\n if (u.protocol !== 'xopc:' || u.hostname !== 'gateway' || u.pathname !== '/mobile-connect') {\n return null;\n }\n const baseUrl = u.searchParams.get('baseUrl')?.trim() ?? '';\n const pairingSecret = u.searchParams.get('ps')?.trim() ?? '';\n if (!baseUrl || !pairingSecret) return null;\n const lanRaw = u.searchParams.get('lanUrl')?.trim() ?? '';\n const lanUrl = lanRaw ? normalizeGatewayBaseUrl(lanRaw) : null;\n const normalizedBase = normalizeGatewayBaseUrl(baseUrl);\n if (!normalizedBase) return null;\n return {\n baseUrl: normalizedBase,\n lanUrl,\n pairingSecret,\n };\n } catch {\n return null;\n }\n}\n\n/** Ordered URLs for mobile clients: prefer LAN on the same network, then tunnel/base. */\nexport function buildMobileConnectUrlOrder(params: {\n baseUrl: string | null | undefined;\n lanUrl: string | null | undefined;\n}): string[] {\n const ordered: string[] = [];\n const seen = new Set<string>();\n\n const push = (raw: string | null | undefined) => {\n const url = raw ? normalizeGatewayBaseUrl(raw) : null;\n if (!url || isLoopbackGatewayBaseUrl(url) || seen.has(url)) return;\n seen.add(url);\n ordered.push(url);\n };\n\n push(params.lanUrl);\n push(params.baseUrl);\n return ordered;\n}\n\n/** First LAN URL when the gateway listens on a network-accessible bind host. */\nexport function resolveMobilePairLanUrl(config: Config): string | null {\n const listenHost = resolveGatewayEffectiveHost(config);\n if (!isNetworkAccessibleBindHost(listenHost)) return null;\n const port = config.gateway?.port ?? 18790;\n return enumerateLanGatewayCandidates(port)[0]?.url ?? null;\n}\n"],"mappings":";;;;;AAaA,SAAgB,wBAAwB,KAA4B;CAClE,MAAM,UAAU,IAAI,MAAM;AAC1B,KAAI,CAAC,QAAS,QAAO;AACrB,KAAI;EACF,MAAM,IAAI,IAAI,IAAI,QAAQ;AAC1B,MAAI,EAAE,aAAa,WAAW,EAAE,aAAa,SAC3C,QAAO;AAET,MAAI,EAAE,YAAY,EAAE,SAAU,QAAO;AACrC,MAAI,EAAE,YAAY,EAAE,aAAa,IAAK,QAAO;AAC7C,MAAI,EAAE,UAAU,EAAE,KAAM,QAAO;EAC/B,MAAM,aAAa,EAAE,OAAO,IAAI,EAAE,SAAS;AAC3C,SAAO,GAAG,EAAE,SAAS,IAAI,EAAE,WAAW;SAChC;AACN,SAAO;;;;AAKX,SAAgB,yBAAyB,KAAsB;CAC7D,MAAM,aAAa,wBAAwB,IAAI;AAC/C,KAAI,CAAC,WAAY,QAAO;AACxB,KAAI;EAEF,MAAM,OAAO,IADC,IAAI,WACJ,CAAC,SAAS,MAAM,CAAC,aAAa;AAC5C,MAAI,SAAS,eAAe,SAAS,eAAe,SAAS,SAAS,SAAS,QAC7E,QAAO;AAGT,MADkB,IAAI,KAAK,KACd,KAAK,GAAG;GACnB,MAAM,QAAQ,KAAK,MAAM,IAAI,CAAC,KAAK,MAAM,OAAO,EAAE,CAAC;AACnD,OAAI,MAAM,WAAW,KAAK,MAAM,OAAO,MAAM,OAAO,UAAU,EAAE,CAAC,IAAI,MAAM,OAAO,IAChF,QAAO;;AAGX,SAAO;SACD;AACN,SAAO;;;AAIX,SAAgB,0BAA0B,KAA4C;CACpF,MAAM,MAAM,wBAAwB,IAAI;AACxC,KAAI,CAAC,IACH,QAAO;EACL,IAAI;EACJ,MAAM;EACN,SAAS;EACV;AAEH,KAAI,yBAAyB,IAAI,CAC/B,QAAO;EACL,IAAI;EACJ,MAAM;EACN,SACE;EACH;AAEH,QAAO;EAAE,IAAI;EAAM;EAAK,UAAU;EAAO;;;AAU3C,SAAgB,2BAA2B,KAAiD;CAC1F,MAAM,UAAU,IAAI,MAAM;AAC1B,KAAI,CAAC,QAAS,QAAO;AACrB,KAAI;EACF,MAAM,IAAI,IAAI,IAAI,QAAQ;AAC1B,MAAI,EAAE,aAAa,WAAW,EAAE,aAAa,aAAa,EAAE,aAAa,kBACvE,QAAO;EAET,MAAM,UAAU,EAAE,aAAa,IAAI,UAAU,EAAE,MAAM,IAAI;EACzD,MAAM,gBAAgB,EAAE,aAAa,IAAI,KAAK,EAAE,MAAM,IAAI;AAC1D,MAAI,CAAC,WAAW,CAAC,cAAe,QAAO;EACvC,MAAM,SAAS,EAAE,aAAa,IAAI,SAAS,EAAE,MAAM,IAAI;EACvD,MAAM,SAAS,SAAS,wBAAwB,OAAO,GAAG;EAC1D,MAAM,iBAAiB,wBAAwB,QAAQ;AACvD,MAAI,CAAC,eAAgB,QAAO;AAC5B,SAAO;GACL,SAAS;GACT;GACA;GACD;SACK;AACN,SAAO;;;;AAKX,SAAgB,2BAA2B,QAG9B;CACX,MAAM,UAAoB,EAAE;CAC5B,MAAM,uBAAO,IAAI,KAAa;CAE9B,MAAM,QAAQ,QAAmC;EAC/C,MAAM,MAAM,MAAM,wBAAwB,IAAI,GAAG;AACjD,MAAI,CAAC,OAAO,yBAAyB,IAAI,IAAI,KAAK,IAAI,IAAI,CAAE;AAC5D,OAAK,IAAI,IAAI;AACb,UAAQ,KAAK,IAAI;;AAGnB,MAAK,OAAO,OAAO;AACnB,MAAK,OAAO,QAAQ;AACpB,QAAO;;;AAIT,SAAgB,wBAAwB,QAA+B;AAErE,KAAI,CAAC,4BADc,4BAA4B,OACJ,CAAC,CAAE,QAAO;AAErD,QAAO,8BADM,OAAO,SAAS,QAAQ,MACK,CAAC,IAAI,OAAO"}
1
+ {"version":3,"file":"pair-url.js","names":[],"sources":["../../../src/tunnel/pair-url.ts"],"sourcesContent":["import net from 'node:net';\n\nimport { isNetworkAccessibleBindHost, resolveGatewayEffectiveHost } from '../config/gateway-bind.js';\nimport type { Config } from '../config/schema.js';\nimport { enumerateLanGatewayCandidates } from './tunnel-qr.js';\n\nexport type MobilePairUrlValidationCode = 'INVALID_URL' | 'LOOPBACK_NOT_REACHABLE';\n\nexport type MobilePairUrlValidationResult =\n | { ok: true; url: string; loopback: false }\n | { ok: false; code: MobilePairUrlValidationCode; message: string };\n\n/** Normalize a gateway root URL (no trailing slash, no path). */\nexport function normalizeGatewayBaseUrl(raw: string): string | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n try {\n const u = new URL(trimmed);\n if (u.protocol !== 'http:' && u.protocol !== 'https:') {\n return null;\n }\n if (u.username || u.password) return null;\n if (u.pathname && u.pathname !== '/') return null;\n if (u.search || u.hash) return null;\n const portSuffix = u.port ? `:${u.port}` : '';\n return `${u.protocol}//${u.hostname}${portSuffix}`;\n } catch {\n return null;\n }\n}\n\n/** True when a phone on the network cannot reach this gateway root URL. */\nexport function isLoopbackGatewayBaseUrl(raw: string): boolean {\n const normalized = normalizeGatewayBaseUrl(raw);\n if (!normalized) return false;\n try {\n const u = new URL(normalized);\n const host = u.hostname.trim().toLowerCase();\n if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]') {\n return true;\n }\n const ipVersion = net.isIP(host);\n if (ipVersion === 4) {\n const parts = host.split('.').map((p) => Number(p));\n if (parts.length === 4 && parts.every((n) => Number.isInteger(n)) && parts[0] === 127) {\n return true;\n }\n }\n return false;\n } catch {\n return false;\n }\n}\n\nexport function validateMobilePairBaseUrl(raw: string): MobilePairUrlValidationResult {\n const url = normalizeGatewayBaseUrl(raw);\n if (!url) {\n return {\n ok: false,\n code: 'INVALID_URL',\n message: 'Enter an absolute gateway URL starting with http:// or https:// (no path).',\n };\n }\n if (isLoopbackGatewayBaseUrl(url)) {\n return {\n ok: false,\n code: 'LOOPBACK_NOT_REACHABLE',\n message:\n '127.0.0.1 and localhost only work on the gateway machine. Use a LAN IP or tunnel URL instead.',\n };\n }\n return { ok: true, url, loopback: false };\n}\n\nexport type ParsedMobileConnectDeepLink = {\n baseUrl: string;\n lanUrl: string | null;\n pairingSecret: string;\n};\n\n/** Parse `xopc://gateway/mobile-connect?...` payloads from QR codes. */\nexport function parseMobileConnectDeepLink(raw: string): ParsedMobileConnectDeepLink | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n try {\n const u = new URL(trimmed);\n if (u.protocol !== 'xopc:' || u.hostname !== 'gateway' || u.pathname !== '/mobile-connect') {\n return null;\n }\n const baseUrl = u.searchParams.get('baseUrl')?.trim() ?? '';\n const psRaw = u.searchParams.get('ps')?.trim() ?? '';\n let pairingSecret = psRaw;\n if (psRaw) {\n try {\n pairingSecret = decodeURIComponent(psRaw);\n } catch {\n pairingSecret = psRaw;\n }\n }\n if (!baseUrl || !pairingSecret) return null;\n const lanRaw = u.searchParams.get('lanUrl')?.trim() ?? '';\n const lanUrl = lanRaw ? normalizeGatewayBaseUrl(lanRaw) : null;\n const normalizedBase = normalizeGatewayBaseUrl(baseUrl);\n if (!normalizedBase) return null;\n return {\n baseUrl: normalizedBase,\n lanUrl,\n pairingSecret,\n };\n } catch {\n return null;\n }\n}\n\n/** Ordered URLs for mobile clients: prefer LAN on the same network, then tunnel/base. */\nexport function buildMobileConnectUrlOrder(params: {\n baseUrl: string | null | undefined;\n lanUrl: string | null | undefined;\n}): string[] {\n const ordered: string[] = [];\n const seen = new Set<string>();\n\n const push = (raw: string | null | undefined) => {\n const url = raw ? normalizeGatewayBaseUrl(raw) : null;\n if (!url || isLoopbackGatewayBaseUrl(url) || seen.has(url)) return;\n seen.add(url);\n ordered.push(url);\n };\n\n push(params.lanUrl);\n push(params.baseUrl);\n return ordered;\n}\n\n/** First LAN URL when the gateway listens on a network-accessible bind host. */\nexport function resolveMobilePairLanUrl(config: Config): string | null {\n const listenHost = resolveGatewayEffectiveHost(config);\n if (!isNetworkAccessibleBindHost(listenHost)) return null;\n const port = config.gateway?.port ?? 18790;\n return enumerateLanGatewayCandidates(port)[0]?.url ?? null;\n}\n"],"mappings":"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"}
@@ -2,6 +2,19 @@ export type PairingSecretResult = {
2
2
  secret: string;
3
3
  expiresAt: Date;
4
4
  };
5
+ export type PairingExchangePayload = {
6
+ token: string;
7
+ baseUrl: string | null;
8
+ lanUrl: string | null;
9
+ connectUrls: string[];
10
+ };
11
+ export declare function cachePairingExchange(secret: string, payload: PairingExchangePayload): void;
12
+ export declare function getCachedPairingExchange(secret: string): PairingExchangePayload | null;
13
+ /**
14
+ * Consume a pairing secret once and build the exchange payload.
15
+ * Concurrent requests for the same secret share one in-flight exchange (mobile cold-start deeplink).
16
+ */
17
+ export declare function exchangePairingSecretOnce(secret: string, buildPayload: () => PairingExchangePayload): Promise<PairingExchangePayload | null>;
5
18
  export declare function createPairingSecret(): PairingSecretResult;
6
19
  export declare function consumePairingSecret(secret: string): boolean;
7
20
  /** @internal */
@@ -16,6 +16,51 @@ function ensureCleanupTimer() {
16
16
  }, CLEANUP_INTERVAL_MS);
17
17
  cleanupTimer.unref?.();
18
18
  }
19
+ const EXCHANGE_REPLAY_MS = 6e4;
20
+ const exchangeReplayCache = /* @__PURE__ */ new Map();
21
+ function cachePairingExchange(secret, payload) {
22
+ exchangeReplayCache.set(secret, {
23
+ expiresAt: Date.now() + EXCHANGE_REPLAY_MS,
24
+ payload
25
+ });
26
+ }
27
+ function getCachedPairingExchange(secret) {
28
+ const entry = exchangeReplayCache.get(secret);
29
+ if (!entry) return null;
30
+ if (Date.now() > entry.expiresAt) {
31
+ exchangeReplayCache.delete(secret);
32
+ return null;
33
+ }
34
+ return entry.payload;
35
+ }
36
+ const inflightExchanges = /* @__PURE__ */ new Map();
37
+ /**
38
+ * Consume a pairing secret once and build the exchange payload.
39
+ * Concurrent requests for the same secret share one in-flight exchange (mobile cold-start deeplink).
40
+ */
41
+ async function exchangePairingSecretOnce(secret, buildPayload) {
42
+ const key = secret.trim();
43
+ if (!key) return null;
44
+ const replay = getCachedPairingExchange(key);
45
+ if (replay) return replay;
46
+ let inflight = inflightExchanges.get(key);
47
+ if (!inflight) {
48
+ inflight = (async () => {
49
+ const cached = getCachedPairingExchange(key);
50
+ if (cached) return cached;
51
+ if (!consumePairingSecret(key)) return getCachedPairingExchange(key);
52
+ const payload = buildPayload();
53
+ cachePairingExchange(key, payload);
54
+ return payload;
55
+ })();
56
+ inflightExchanges.set(key, inflight);
57
+ }
58
+ try {
59
+ return await inflight;
60
+ } finally {
61
+ if (inflightExchanges.get(key) === inflight) inflightExchanges.delete(key);
62
+ }
63
+ }
19
64
  function createPairingSecret() {
20
65
  ensureCleanupTimer();
21
66
  const secret = randomBytes(32).toString("base64url");
@@ -47,12 +92,14 @@ function consumePairingSecret(secret) {
47
92
  /** @internal */
48
93
  function resetPairingSessionsForTests() {
49
94
  sessions.clear();
95
+ exchangeReplayCache.clear();
96
+ inflightExchanges.clear();
50
97
  if (cleanupTimer) {
51
98
  clearInterval(cleanupTimer);
52
99
  cleanupTimer = null;
53
100
  }
54
101
  }
55
102
  //#endregion
56
- export { consumePairingSecret, createPairingSecret, resetPairingSessionsForTests };
103
+ export { cachePairingExchange, consumePairingSecret, createPairingSecret, exchangePairingSecretOnce, getCachedPairingExchange, resetPairingSessionsForTests };
57
104
 
58
105
  //# sourceMappingURL=pairing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pairing.js","names":[],"sources":["../../../src/tunnel/pairing.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\n\nimport { createLogger } from '../utils/logger.js';\n\nconst log = createLogger('TunnelPairing');\n\nconst PAIRING_TTL_MS = 5 * 60_000;\nconst CLEANUP_INTERVAL_MS = 60_000;\n\ntype PairingSession = {\n expiresAt: Date;\n consumed: boolean;\n};\n\nconst sessions = new Map<string, PairingSession>();\n\nlet cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\nfunction ensureCleanupTimer(): void {\n if (cleanupTimer) return;\n cleanupTimer = setInterval(() => {\n const now = Date.now();\n for (const [key, session] of sessions) {\n if (now > session.expiresAt.getTime()) sessions.delete(key);\n }\n }, CLEANUP_INTERVAL_MS);\n cleanupTimer.unref?.();\n}\n\nexport type PairingSecretResult = {\n secret: string;\n expiresAt: Date;\n};\n\nexport function createPairingSecret(): PairingSecretResult {\n ensureCleanupTimer();\n const secret = randomBytes(32).toString('base64url');\n const expiresAt = new Date(Date.now() + PAIRING_TTL_MS);\n sessions.set(secret, { expiresAt, consumed: false });\n log.info({ expiresAt: expiresAt.toISOString() }, 'Pairing session created');\n return { secret, expiresAt };\n}\n\nexport function consumePairingSecret(secret: string): boolean {\n if (!secret.trim()) return false;\n const session = sessions.get(secret);\n if (!session) return false;\n if (session.consumed) return false;\n if (Date.now() > session.expiresAt.getTime()) {\n sessions.delete(secret);\n return false;\n }\n session.consumed = true;\n sessions.delete(secret);\n log.info('Pairing session consumed');\n return true;\n}\n\n/** @internal */\nexport function resetPairingSessionsForTests(): void {\n sessions.clear();\n if (cleanupTimer) {\n clearInterval(cleanupTimer);\n cleanupTimer = null;\n }\n}\n"],"mappings":";;;;aAEkD;AAElD,MAAM,MAAM,aAAa,gBAAgB;AAEzC,MAAM,iBAAiB,IAAI;AAC3B,MAAM,sBAAsB;AAO5B,MAAM,2BAAW,IAAI,KAA6B;AAElD,IAAI,eAAsD;AAE1D,SAAS,qBAA2B;AAClC,KAAI,aAAc;AAClB,gBAAe,kBAAkB;EAC/B,MAAM,MAAM,KAAK,KAAK;AACtB,OAAK,MAAM,CAAC,KAAK,YAAY,SAC3B,KAAI,MAAM,QAAQ,UAAU,SAAS,CAAE,UAAS,OAAO,IAAI;IAE5D,oBAAoB;AACvB,cAAa,SAAS;;AAQxB,SAAgB,sBAA2C;AACzD,qBAAoB;CACpB,MAAM,SAAS,YAAY,GAAG,CAAC,SAAS,YAAY;CACpD,MAAM,YAAY,IAAI,KAAK,KAAK,KAAK,GAAG,eAAe;AACvD,UAAS,IAAI,QAAQ;EAAE;EAAW,UAAU;EAAO,CAAC;AACpD,KAAI,KAAK,EAAE,WAAW,UAAU,aAAa,EAAE,EAAE,0BAA0B;AAC3E,QAAO;EAAE;EAAQ;EAAW;;AAG9B,SAAgB,qBAAqB,QAAyB;AAC5D,KAAI,CAAC,OAAO,MAAM,CAAE,QAAO;CAC3B,MAAM,UAAU,SAAS,IAAI,OAAO;AACpC,KAAI,CAAC,QAAS,QAAO;AACrB,KAAI,QAAQ,SAAU,QAAO;AAC7B,KAAI,KAAK,KAAK,GAAG,QAAQ,UAAU,SAAS,EAAE;AAC5C,WAAS,OAAO,OAAO;AACvB,SAAO;;AAET,SAAQ,WAAW;AACnB,UAAS,OAAO,OAAO;AACvB,KAAI,KAAK,2BAA2B;AACpC,QAAO;;;AAIT,SAAgB,+BAAqC;AACnD,UAAS,OAAO;AAChB,KAAI,cAAc;AAChB,gBAAc,aAAa;AAC3B,iBAAe"}
1
+ {"version":3,"file":"pairing.js","names":[],"sources":["../../../src/tunnel/pairing.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\n\nimport { createLogger } from '../utils/logger.js';\n\nconst log = createLogger('TunnelPairing');\n\nconst PAIRING_TTL_MS = 5 * 60_000;\nconst CLEANUP_INTERVAL_MS = 60_000;\n\ntype PairingSession = {\n expiresAt: Date;\n consumed: boolean;\n};\n\nconst sessions = new Map<string, PairingSession>();\n\nlet cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\nfunction ensureCleanupTimer(): void {\n if (cleanupTimer) return;\n cleanupTimer = setInterval(() => {\n const now = Date.now();\n for (const [key, session] of sessions) {\n if (now > session.expiresAt.getTime()) sessions.delete(key);\n }\n }, CLEANUP_INTERVAL_MS);\n cleanupTimer.unref?.();\n}\n\nexport type PairingSecretResult = {\n secret: string;\n expiresAt: Date;\n};\n\nexport type PairingExchangePayload = {\n token: string;\n baseUrl: string | null;\n lanUrl: string | null;\n connectUrls: string[];\n};\n\nconst EXCHANGE_REPLAY_MS = 60_000;\nconst exchangeReplayCache = new Map<string, { expiresAt: number; payload: PairingExchangePayload }>();\n\nexport function cachePairingExchange(secret: string, payload: PairingExchangePayload): void {\n exchangeReplayCache.set(secret, { expiresAt: Date.now() + EXCHANGE_REPLAY_MS, payload });\n}\n\nexport function getCachedPairingExchange(secret: string): PairingExchangePayload | null {\n const entry = exchangeReplayCache.get(secret);\n if (!entry) return null;\n if (Date.now() > entry.expiresAt) {\n exchangeReplayCache.delete(secret);\n return null;\n }\n return entry.payload;\n}\n\nconst inflightExchanges = new Map<string, Promise<PairingExchangePayload | null>>();\n\n/**\n * Consume a pairing secret once and build the exchange payload.\n * Concurrent requests for the same secret share one in-flight exchange (mobile cold-start deeplink).\n */\nexport async function exchangePairingSecretOnce(\n secret: string,\n buildPayload: () => PairingExchangePayload,\n): Promise<PairingExchangePayload | null> {\n const key = secret.trim();\n if (!key) return null;\n\n const replay = getCachedPairingExchange(key);\n if (replay) return replay;\n\n let inflight = inflightExchanges.get(key);\n if (!inflight) {\n inflight = (async (): Promise<PairingExchangePayload | null> => {\n const cached = getCachedPairingExchange(key);\n if (cached) return cached;\n if (!consumePairingSecret(key)) {\n return getCachedPairingExchange(key);\n }\n const payload = buildPayload();\n cachePairingExchange(key, payload);\n return payload;\n })();\n inflightExchanges.set(key, inflight);\n }\n\n try {\n return await inflight;\n } finally {\n if (inflightExchanges.get(key) === inflight) inflightExchanges.delete(key);\n }\n}\n\nexport function createPairingSecret(): PairingSecretResult {\n ensureCleanupTimer();\n const secret = randomBytes(32).toString('base64url');\n const expiresAt = new Date(Date.now() + PAIRING_TTL_MS);\n sessions.set(secret, { expiresAt, consumed: false });\n log.info({ expiresAt: expiresAt.toISOString() }, 'Pairing session created');\n return { secret, expiresAt };\n}\n\nexport function consumePairingSecret(secret: string): boolean {\n if (!secret.trim()) return false;\n const session = sessions.get(secret);\n if (!session) return false;\n if (session.consumed) return false;\n if (Date.now() > session.expiresAt.getTime()) {\n sessions.delete(secret);\n return false;\n }\n session.consumed = true;\n sessions.delete(secret);\n log.info('Pairing session consumed');\n return true;\n}\n\n/** @internal */\nexport function resetPairingSessionsForTests(): void {\n sessions.clear();\n exchangeReplayCache.clear();\n inflightExchanges.clear();\n if (cleanupTimer) {\n clearInterval(cleanupTimer);\n cleanupTimer = null;\n }\n}\n"],"mappings":";;;;aAEkD;AAElD,MAAM,MAAM,aAAa,gBAAgB;AAEzC,MAAM,iBAAiB,IAAI;AAC3B,MAAM,sBAAsB;AAO5B,MAAM,2BAAW,IAAI,KAA6B;AAElD,IAAI,eAAsD;AAE1D,SAAS,qBAA2B;AAClC,KAAI,aAAc;AAClB,gBAAe,kBAAkB;EAC/B,MAAM,MAAM,KAAK,KAAK;AACtB,OAAK,MAAM,CAAC,KAAK,YAAY,SAC3B,KAAI,MAAM,QAAQ,UAAU,SAAS,CAAE,UAAS,OAAO,IAAI;IAE5D,oBAAoB;AACvB,cAAa,SAAS;;AAexB,MAAM,qBAAqB;AAC3B,MAAM,sCAAsB,IAAI,KAAqE;AAErG,SAAgB,qBAAqB,QAAgB,SAAuC;AAC1F,qBAAoB,IAAI,QAAQ;EAAE,WAAW,KAAK,KAAK,GAAG;EAAoB;EAAS,CAAC;;AAG1F,SAAgB,yBAAyB,QAA+C;CACtF,MAAM,QAAQ,oBAAoB,IAAI,OAAO;AAC7C,KAAI,CAAC,MAAO,QAAO;AACnB,KAAI,KAAK,KAAK,GAAG,MAAM,WAAW;AAChC,sBAAoB,OAAO,OAAO;AAClC,SAAO;;AAET,QAAO,MAAM;;AAGf,MAAM,oCAAoB,IAAI,KAAqD;;;;;AAMnF,eAAsB,0BACpB,QACA,cACwC;CACxC,MAAM,MAAM,OAAO,MAAM;AACzB,KAAI,CAAC,IAAK,QAAO;CAEjB,MAAM,SAAS,yBAAyB,IAAI;AAC5C,KAAI,OAAQ,QAAO;CAEnB,IAAI,WAAW,kBAAkB,IAAI,IAAI;AACzC,KAAI,CAAC,UAAU;AACb,cAAY,YAAoD;GAC9D,MAAM,SAAS,yBAAyB,IAAI;AAC5C,OAAI,OAAQ,QAAO;AACnB,OAAI,CAAC,qBAAqB,IAAI,CAC5B,QAAO,yBAAyB,IAAI;GAEtC,MAAM,UAAU,cAAc;AAC9B,wBAAqB,KAAK,QAAQ;AAClC,UAAO;MACL;AACJ,oBAAkB,IAAI,KAAK,SAAS;;AAGtC,KAAI;AACF,SAAO,MAAM;WACL;AACR,MAAI,kBAAkB,IAAI,IAAI,KAAK,SAAU,mBAAkB,OAAO,IAAI;;;AAI9E,SAAgB,sBAA2C;AACzD,qBAAoB;CACpB,MAAM,SAAS,YAAY,GAAG,CAAC,SAAS,YAAY;CACpD,MAAM,YAAY,IAAI,KAAK,KAAK,KAAK,GAAG,eAAe;AACvD,UAAS,IAAI,QAAQ;EAAE;EAAW,UAAU;EAAO,CAAC;AACpD,KAAI,KAAK,EAAE,WAAW,UAAU,aAAa,EAAE,EAAE,0BAA0B;AAC3E,QAAO;EAAE;EAAQ;EAAW;;AAG9B,SAAgB,qBAAqB,QAAyB;AAC5D,KAAI,CAAC,OAAO,MAAM,CAAE,QAAO;CAC3B,MAAM,UAAU,SAAS,IAAI,OAAO;AACpC,KAAI,CAAC,QAAS,QAAO;AACrB,KAAI,QAAQ,SAAU,QAAO;AAC7B,KAAI,KAAK,KAAK,GAAG,QAAQ,UAAU,SAAS,EAAE;AAC5C,WAAS,OAAO,OAAO;AACvB,SAAO;;AAET,SAAQ,WAAW;AACnB,UAAS,OAAO,OAAO;AACvB,KAAI,KAAK,2BAA2B;AACpC,QAAO;;;AAIT,SAAgB,+BAAqC;AACnD,UAAS,OAAO;AAChB,qBAAoB,OAAO;AAC3B,mBAAkB,OAAO;AACzB,KAAI,cAAc;AAChB,gBAAc,aAAa;AAC3B,iBAAe"}
@@ -1,22 +1,16 @@
1
- import { TunnelConfigSchema, TunnelConsentSchema, TunnelE2eSchema, init_schema } from "../config/schema.js";
1
+ import { TunnelConfigSchema, TunnelConsentSchema, init_schema } from "../config/schema.js";
2
2
  import { assertTunnelAutoStartAllowed, buildTunnelConsentRecord, hasValidTunnelConsent } from "./consent.js";
3
3
  import { isMaskedTunnelSecretPatchValue } from "./env.js";
4
4
  import { z } from "zod";
5
5
  //#region src/tunnel/tunnel-config.ts
6
6
  init_schema();
7
- const TunnelE2ePatchSchema = z.object({
8
- enabled: z.boolean().optional(),
9
- tlsPort: z.number().int().min(1024).max(65535).optional(),
10
- staging: z.boolean().optional()
11
- });
12
7
  const TunnelConfigPatchSchema = z.object({
13
8
  enabled: z.boolean().optional(),
14
9
  brokerUrl: z.string().url().optional(),
15
10
  registrationSecret: z.union([z.string(), z.null()]).optional(),
16
11
  autoStart: z.boolean().optional(),
17
12
  subdomain: z.string().optional(),
18
- consent: TunnelConsentSchema.optional(),
19
- e2e: TunnelE2ePatchSchema.optional()
13
+ consent: TunnelConsentSchema.optional()
20
14
  });
21
15
  function mergeTunnelConfigPatch(config, patch) {
22
16
  const parsed = TunnelConfigPatchSchema.safeParse(patch);
@@ -61,14 +55,6 @@ function mergeTunnelConfigPatch(config, patch) {
61
55
  ok: false,
62
56
  message: "Cannot enable tunnel without accepting the security notice. Start remote access from settings or record consent first."
63
57
  };
64
- if (parsed.data.e2e !== void 0) next.e2e = TunnelE2eSchema.parse({
65
- ...next.e2e ?? {
66
- enabled: true,
67
- tlsPort: 18791,
68
- staging: false
69
- },
70
- ...parsed.data.e2e
71
- });
72
58
  config.tunnel = TunnelConfigSchema.parse(next);
73
59
  return { ok: true };
74
60
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tunnel-config.js","names":[],"sources":["../../../src/tunnel/tunnel-config.ts"],"sourcesContent":["import { z } from 'zod';\n\nimport type { Config } from '../config/schema.js';\nimport { TunnelConfigSchema, TunnelConsentSchema, TunnelE2eSchema } from '../config/schema.js';\n\nimport {\n assertTunnelAutoStartAllowed,\n buildTunnelConsentRecord,\n hasValidTunnelConsent,\n} from './consent.js';\nimport { isMaskedTunnelSecretPatchValue } from './env.js';\n\nconst TunnelE2ePatchSchema = z.object({\n enabled: z.boolean().optional(),\n tlsPort: z.number().int().min(1024).max(65535).optional(),\n staging: z.boolean().optional(),\n});\n\nconst TunnelConfigPatchSchema = z.object({\n enabled: z.boolean().optional(),\n brokerUrl: z.string().url().optional(),\n registrationSecret: z.union([z.string(), z.null()]).optional(),\n autoStart: z.boolean().optional(),\n subdomain: z.string().optional(),\n consent: TunnelConsentSchema.optional(),\n e2e: TunnelE2ePatchSchema.optional(),\n});\n\nexport function mergeTunnelConfigPatch(\n config: Config,\n patch: Record<string, unknown>,\n): { ok: true } | { ok: false; message: string } {\n const parsed = TunnelConfigPatchSchema.safeParse(patch);\n if (!parsed.success) {\n return { ok: false, message: parsed.error.issues.map((i) => i.message).join('; ') };\n }\n\n const patchFields = { ...parsed.data };\n if (parsed.data.registrationSecret !== undefined) {\n if (parsed.data.registrationSecret === null) {\n delete patchFields.registrationSecret;\n } else {\n const trimmed = parsed.data.registrationSecret.trim();\n if (!trimmed || isMaskedTunnelSecretPatchValue(trimmed)) {\n delete patchFields.registrationSecret;\n } else {\n patchFields.registrationSecret = trimmed;\n }\n }\n }\n\n const next = {\n ...(config.tunnel ?? TunnelConfigSchema.parse({})),\n ...patchFields,\n };\n\n if (parsed.data.registrationSecret === null) {\n delete next.registrationSecret;\n }\n\n if (parsed.data.autoStart === true) {\n const probeTunnel = TunnelConfigSchema.parse({ ...next, autoStart: true });\n const probe: Config = { ...config, tunnel: probeTunnel };\n try {\n assertTunnelAutoStartAllowed(probe);\n } catch (err) {\n const em = err instanceof Error ? err.message : String(err);\n return { ok: false, message: em };\n }\n }\n\n if (\n parsed.data.enabled === true &&\n !hasValidTunnelConsent({ ...config, tunnel: TunnelConfigSchema.parse(next) })\n ) {\n return {\n ok: false,\n message:\n 'Cannot enable tunnel without accepting the security notice. Start remote access from settings or record consent first.',\n };\n }\n\n if (parsed.data.e2e !== undefined) {\n next.e2e = TunnelE2eSchema.parse({\n ...(next.e2e ?? { enabled: true, tlsPort: 18791, staging: false }),\n ...parsed.data.e2e,\n });\n }\n\n config.tunnel = TunnelConfigSchema.parse(next);\n return { ok: true };\n}\n\nexport function applyTunnelConsentToConfig(config: Config): void {\n if (!config.tunnel) {\n config.tunnel = TunnelConfigSchema.parse({});\n }\n config.tunnel.consent = buildTunnelConsentRecord();\n}\n\nexport function setTunnelEnabledInConfig(config: Config, enabled: boolean): void {\n if (!config.tunnel) {\n config.tunnel = TunnelConfigSchema.parse({});\n }\n config.tunnel.enabled = enabled;\n}\n\n/**\n * Clear stale tunnel flags when consent is missing or outdated (Phase 2: config/runtime alignment).\n * Returns true when `config.tunnel` was modified.\n */\nexport function sanitizeTunnelConfig(config: Config): boolean {\n const tunnel = config.tunnel;\n if (!tunnel) return false;\n if (hasValidTunnelConsent(config)) return false;\n\n let changed = false;\n if (tunnel.enabled) {\n tunnel.enabled = false;\n changed = true;\n }\n if (tunnel.autoStart) {\n tunnel.autoStart = false;\n changed = true;\n }\n return changed;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tunnel-config.js","names":[],"sources":["../../../src/tunnel/tunnel-config.ts"],"sourcesContent":["import { z } from 'zod';\n\nimport type { Config } from '../config/schema.js';\nimport { TunnelConfigSchema, TunnelConsentSchema } from '../config/schema.js';\n\nimport {\n assertTunnelAutoStartAllowed,\n buildTunnelConsentRecord,\n hasValidTunnelConsent,\n} from './consent.js';\nimport { isMaskedTunnelSecretPatchValue } from './env.js';\n\nconst TunnelConfigPatchSchema = z.object({\n enabled: z.boolean().optional(),\n brokerUrl: z.string().url().optional(),\n registrationSecret: z.union([z.string(), z.null()]).optional(),\n autoStart: z.boolean().optional(),\n subdomain: z.string().optional(),\n consent: TunnelConsentSchema.optional(),\n});\n\nexport function mergeTunnelConfigPatch(\n config: Config,\n patch: Record<string, unknown>,\n): { ok: true } | { ok: false; message: string } {\n const parsed = TunnelConfigPatchSchema.safeParse(patch);\n if (!parsed.success) {\n return { ok: false, message: parsed.error.issues.map((i) => i.message).join('; ') };\n }\n\n const patchFields = { ...parsed.data };\n if (parsed.data.registrationSecret !== undefined) {\n if (parsed.data.registrationSecret === null) {\n delete patchFields.registrationSecret;\n } else {\n const trimmed = parsed.data.registrationSecret.trim();\n if (!trimmed || isMaskedTunnelSecretPatchValue(trimmed)) {\n delete patchFields.registrationSecret;\n } else {\n patchFields.registrationSecret = trimmed;\n }\n }\n }\n\n const next = {\n ...(config.tunnel ?? TunnelConfigSchema.parse({})),\n ...patchFields,\n };\n\n if (parsed.data.registrationSecret === null) {\n delete next.registrationSecret;\n }\n\n if (parsed.data.autoStart === true) {\n const probeTunnel = TunnelConfigSchema.parse({ ...next, autoStart: true });\n const probe: Config = { ...config, tunnel: probeTunnel };\n try {\n assertTunnelAutoStartAllowed(probe);\n } catch (err) {\n const em = err instanceof Error ? err.message : String(err);\n return { ok: false, message: em };\n }\n }\n\n if (\n parsed.data.enabled === true &&\n !hasValidTunnelConsent({ ...config, tunnel: TunnelConfigSchema.parse(next) })\n ) {\n return {\n ok: false,\n message:\n 'Cannot enable tunnel without accepting the security notice. Start remote access from settings or record consent first.',\n };\n }\n\n config.tunnel = TunnelConfigSchema.parse(next);\n return { ok: true };\n}\n\nexport function applyTunnelConsentToConfig(config: Config): void {\n if (!config.tunnel) {\n config.tunnel = TunnelConfigSchema.parse({});\n }\n config.tunnel.consent = buildTunnelConsentRecord();\n}\n\nexport function setTunnelEnabledInConfig(config: Config, enabled: boolean): void {\n if (!config.tunnel) {\n config.tunnel = TunnelConfigSchema.parse({});\n }\n config.tunnel.enabled = enabled;\n}\n\n/**\n * Clear stale tunnel flags when consent is missing or outdated (Phase 2: config/runtime alignment).\n * Returns true when `config.tunnel` was modified.\n */\nexport function sanitizeTunnelConfig(config: Config): boolean {\n const tunnel = config.tunnel;\n if (!tunnel) return false;\n if (hasValidTunnelConsent(config)) return false;\n\n let changed = false;\n if (tunnel.enabled) {\n tunnel.enabled = false;\n changed = true;\n }\n if (tunnel.autoStart) {\n tunnel.autoStart = false;\n changed = true;\n }\n return changed;\n}\n"],"mappings":";;;;;aAG8E;AAS9E,MAAM,0BAA0B,EAAE,OAAO;CACvC,SAAS,EAAE,SAAS,CAAC,UAAU;CAC/B,WAAW,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU;CACtC,oBAAoB,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU;CAC9D,WAAW,EAAE,SAAS,CAAC,UAAU;CACjC,WAAW,EAAE,QAAQ,CAAC,UAAU;CAChC,SAAS,oBAAoB,UAAU;CACxC,CAAC;AAEF,SAAgB,uBACd,QACA,OAC+C;CAC/C,MAAM,SAAS,wBAAwB,UAAU,MAAM;AACvD,KAAI,CAAC,OAAO,QACV,QAAO;EAAE,IAAI;EAAO,SAAS,OAAO,MAAM,OAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,KAAK;EAAE;CAGrF,MAAM,cAAc,EAAE,GAAG,OAAO,MAAM;AACtC,KAAI,OAAO,KAAK,uBAAuB,KAAA,EACrC,KAAI,OAAO,KAAK,uBAAuB,KACrC,QAAO,YAAY;MACd;EACL,MAAM,UAAU,OAAO,KAAK,mBAAmB,MAAM;AACrD,MAAI,CAAC,WAAW,+BAA+B,QAAQ,CACrD,QAAO,YAAY;MAEnB,aAAY,qBAAqB;;CAKvC,MAAM,OAAO;EACX,GAAI,OAAO,UAAU,mBAAmB,MAAM,EAAE,CAAC;EACjD,GAAG;EACJ;AAED,KAAI,OAAO,KAAK,uBAAuB,KACrC,QAAO,KAAK;AAGd,KAAI,OAAO,KAAK,cAAc,MAAM;EAClC,MAAM,cAAc,mBAAmB,MAAM;GAAE,GAAG;GAAM,WAAW;GAAM,CAAC;EAC1E,MAAM,QAAgB;GAAE,GAAG;GAAQ,QAAQ;GAAa;AACxD,MAAI;AACF,gCAA6B,MAAM;WAC5B,KAAK;AAEZ,UAAO;IAAE,IAAI;IAAO,SADT,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;IAC1B;;;AAIrC,KACE,OAAO,KAAK,YAAY,QACxB,CAAC,sBAAsB;EAAE,GAAG;EAAQ,QAAQ,mBAAmB,MAAM,KAAK;EAAE,CAAC,CAE7E,QAAO;EACL,IAAI;EACJ,SACE;EACH;AAGH,QAAO,SAAS,mBAAmB,MAAM,KAAK;AAC9C,QAAO,EAAE,IAAI,MAAM;;AAGrB,SAAgB,2BAA2B,QAAsB;AAC/D,KAAI,CAAC,OAAO,OACV,QAAO,SAAS,mBAAmB,MAAM,EAAE,CAAC;AAE9C,QAAO,OAAO,UAAU,0BAA0B;;AAGpD,SAAgB,yBAAyB,QAAgB,SAAwB;AAC/E,KAAI,CAAC,OAAO,OACV,QAAO,SAAS,mBAAmB,MAAM,EAAE,CAAC;AAE9C,QAAO,OAAO,UAAU;;;;;;AAO1B,SAAgB,qBAAqB,QAAyB;CAC5D,MAAM,SAAS,OAAO;AACtB,KAAI,CAAC,OAAQ,QAAO;AACpB,KAAI,sBAAsB,OAAO,CAAE,QAAO;CAE1C,IAAI,UAAU;AACd,KAAI,OAAO,SAAS;AAClB,SAAO,UAAU;AACjB,YAAU;;AAEZ,KAAI,OAAO,WAAW;AACpB,SAAO,YAAY;AACnB,YAAU;;AAEZ,QAAO"}
@@ -1,20 +1,16 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import type { TunnelQrPayload, TunnelStatus } from './tunnel-types.js';
3
- import type { ResolvedTunnelE2eConfig } from './tunnel-e2e-config.js';
4
3
  export type TunnelServiceConfig = {
5
4
  brokerUrl: string;
6
5
  registrationSecret: string;
7
6
  autoStart: boolean;
8
7
  gatewayHost: string;
9
- e2e: ResolvedTunnelE2eConfig;
10
8
  frpSubdomainHost: string;
11
- gatewayFetch?: typeof fetch;
12
9
  };
13
10
  export declare function hashGatewayToken(token: string): string;
14
11
  export declare function getTunnelService(): TunnelService;
15
12
  export declare class TunnelService extends EventEmitter {
16
13
  private serviceConfig;
17
- private pendingGatewayFetch;
18
14
  private frpcHandle;
19
15
  private heartbeatTimer;
20
16
  private connectedSince;
@@ -27,24 +23,19 @@ export declare class TunnelService extends EventEmitter {
27
23
  private frpcDownload;
28
24
  private startProgress;
29
25
  configure(cfg: TunnelServiceConfig): void;
30
- setGatewayFetch(fetchFn: typeof fetch): void;
31
26
  getStatus(): TunnelStatus;
32
27
  private setStartPhase;
33
28
  private clearStartProgress;
34
- buildQr(gatewayPort: number, gatewayHost: string): TunnelQrPayload;
29
+ buildQr(gatewayPort: number, gatewayHost: string): Promise<TunnelQrPayload>;
35
30
  start(gatewayPort: number, gatewayToken: string): Promise<TunnelQrPayload>;
36
31
  stop(opts?: {
37
32
  release?: boolean;
38
33
  }): Promise<{
39
34
  released: boolean;
40
35
  }>;
41
- /**
42
- * Reuse Broker registration when possible so subdomain and URLs stay stable across stop/start.
43
- */
44
36
  private resolveRegistration;
45
37
  private spawnAndWait;
46
38
  private scheduleReconnect;
47
39
  private startHeartbeat;
48
40
  private clearHeartbeat;
49
- private prepareFrpcTarget;
50
41
  }
@@ -1,7 +1,6 @@
1
1
  import { PACKAGE_VERSION, init_package_version } from "../package-version.js";
2
2
  import { createLogger } from "../utils/logger/index.js";
3
3
  import { init_logger } from "../utils/logger.js";
4
- import { getCertStatusSummary } from "./acme-cert-store.js";
5
4
  import { TunnelBrokerClient, resolveBrokerApiBase } from "./broker-client.js";
6
5
  import { clearFrpcPathForProcess, ensureFrpcBinary, publishFrpcPathForProcess } from "./frpc-binary.js";
7
6
  import { writeFrpcConfig } from "./frpc-config.js";
@@ -11,7 +10,6 @@ import { createPairingSecret } from "./pairing.js";
11
10
  import { canResumePersistedTunnel, persistedFromRegistration, registrationFromPersisted } from "./tunnel-persist.js";
12
11
  import { clearTunnelState, loadTunnelState, saveTunnelState, updateTunnelState } from "./tunnel-state.js";
13
12
  import { logTunnelAudit } from "./tunnel-audit.js";
14
- import { startTunnelTlsServer, stopTunnelTlsServer } from "./tls-server.js";
15
13
  import { createHash } from "node:crypto";
16
14
  import { EventEmitter } from "node:events";
17
15
  //#region src/tunnel/tunnel-service.ts
@@ -31,7 +29,6 @@ function getTunnelService() {
31
29
  }
32
30
  var TunnelService = class extends EventEmitter {
33
31
  serviceConfig = null;
34
- pendingGatewayFetch;
35
32
  frpcHandle = null;
36
33
  heartbeatTimer = null;
37
34
  connectedSince = null;
@@ -44,14 +41,7 @@ var TunnelService = class extends EventEmitter {
44
41
  frpcDownload = null;
45
42
  startProgress = null;
46
43
  configure(cfg) {
47
- this.serviceConfig = {
48
- ...cfg,
49
- gatewayFetch: cfg.gatewayFetch ?? this.pendingGatewayFetch ?? this.serviceConfig?.gatewayFetch
50
- };
51
- }
52
- setGatewayFetch(fetchFn) {
53
- this.pendingGatewayFetch = fetchFn;
54
- if (this.serviceConfig) this.serviceConfig.gatewayFetch = fetchFn;
44
+ this.serviceConfig = cfg;
55
45
  }
56
46
  getStatus() {
57
47
  const cfg = this.serviceConfig;
@@ -70,13 +60,8 @@ var TunnelService = class extends EventEmitter {
70
60
  config: {
71
61
  autoStart: cfg?.autoStart ?? false,
72
62
  brokerUrl: cfg?.brokerUrl ?? "https://frp.xopc.ai/api",
73
- e2e: {
74
- enabled: cfg?.e2e.enabled ?? true,
75
- tlsPort: cfg?.e2e.tlsPort ?? 18791,
76
- staging: cfg?.e2e.staging ?? false
77
- }
78
- },
79
- cert: getCertStatusSummary()
63
+ transport: { tls: "broker_terminated" }
64
+ }
80
65
  };
81
66
  }
82
67
  setStartPhase(phase, patch) {
@@ -85,7 +70,6 @@ var TunnelService = class extends EventEmitter {
85
70
  this.startProgress = {
86
71
  phase,
87
72
  startedAt: samePhase && prev ? prev.startedAt : (/* @__PURE__ */ new Date()).toISOString(),
88
- acmeStep: patch?.acmeStep !== void 0 ? patch.acmeStep : samePhase ? prev?.acmeStep ?? null : null,
89
73
  publicUrl: patch?.publicUrl !== void 0 ? patch.publicUrl : samePhase ? prev?.publicUrl ?? null : prev?.publicUrl ?? null
90
74
  };
91
75
  this.emit("tunnel:progress");
@@ -95,7 +79,7 @@ var TunnelService = class extends EventEmitter {
95
79
  this.startProgress = null;
96
80
  this.emit("tunnel:progress");
97
81
  }
98
- buildQr(gatewayPort, gatewayHost) {
82
+ async buildQr(gatewayPort, gatewayHost) {
99
83
  const publicUrl = loadTunnelState()?.publicUrl ?? null;
100
84
  if (!publicUrl) return {
101
85
  qrPayload: "",
@@ -158,8 +142,7 @@ var TunnelService = class extends EventEmitter {
158
142
  saveTunnelState(state);
159
143
  this.setStartPhase("registering", { publicUrl: registration.publicUrl });
160
144
  try {
161
- const { frpcLocalPort, frpcMode } = await this.prepareFrpcTarget(broker, registration, cfg, gatewayPort);
162
- const configPath = writeFrpcConfig(registration, frpcLocalPort, "127.0.0.1", frpcMode);
145
+ const configPath = writeFrpcConfig(registration, gatewayPort, "127.0.0.1", "http");
163
146
  this.setStartPhase("starting_frpc", { publicUrl: registration.publicUrl });
164
147
  await this.spawnAndWait(frpcBin, configPath, broker, state, registration.heartbeatIntervalMs);
165
148
  } catch (err) {
@@ -174,7 +157,7 @@ var TunnelService = class extends EventEmitter {
174
157
  this.connectedSince = (/* @__PURE__ */ new Date()).toISOString();
175
158
  this.reconnectAttempt = 0;
176
159
  this.emit("tunnel:connected");
177
- const qr = this.buildQr(gatewayPort, cfg.gatewayHost);
160
+ const qr = await this.buildQr(gatewayPort, cfg.gatewayHost);
178
161
  logTunnelAudit("tunnel.start", {
179
162
  subdomain: registration.subdomain,
180
163
  publicUrl: registration.publicUrl,
@@ -187,7 +170,6 @@ var TunnelService = class extends EventEmitter {
187
170
  const release = opts?.release === true;
188
171
  this.stopping = true;
189
172
  this.clearHeartbeat();
190
- stopTunnelTlsServer();
191
173
  if (this.frpcHandle) {
192
174
  await this.frpcHandle.kill();
193
175
  this.frpcHandle = null;
@@ -228,9 +210,6 @@ var TunnelService = class extends EventEmitter {
228
210
  this.emit("tunnel:disconnected");
229
211
  return { released };
230
212
  }
231
- /**
232
- * Reuse Broker registration when possible so subdomain and URLs stay stable across stop/start.
233
- */
234
213
  async resolveRegistration(broker, cfg, gatewayToken, persisted) {
235
214
  if (canResumePersistedTunnel(persisted)) try {
236
215
  await broker.heartbeat(persisted.tunnelId, persisted.tunnelToken);
@@ -339,8 +318,7 @@ var TunnelService = class extends EventEmitter {
339
318
  const next = persistedFromRegistration(registration);
340
319
  saveTunnelState(next);
341
320
  const frpcBin = await ensureFrpcBinary();
342
- const { frpcLocalPort, frpcMode } = await this.prepareFrpcTarget(broker, registration, cfg, ctx.gatewayPort);
343
- const configPath = writeFrpcConfig(registration, frpcLocalPort, "127.0.0.1", frpcMode);
321
+ const configPath = writeFrpcConfig(registration, ctx.gatewayPort, "127.0.0.1", "http");
344
322
  await this.spawnAndWait(frpcBin, configPath, broker, next, registration.heartbeatIntervalMs);
345
323
  } catch (reErr) {
346
324
  this.lastError = reErr instanceof Error ? reErr.message : String(reErr);
@@ -358,37 +336,6 @@ var TunnelService = class extends EventEmitter {
358
336
  this.heartbeatTimer = null;
359
337
  }
360
338
  }
361
- async prepareFrpcTarget(broker, registration, cfg, gatewayPort) {
362
- if (!cfg.e2e.enabled) return {
363
- frpcLocalPort: gatewayPort,
364
- frpcMode: "http"
365
- };
366
- this.setStartPhase("provisioning_tls", { publicUrl: registration.publicUrl });
367
- const acmeConfig = {
368
- broker,
369
- tunnelId: registration.tunnelId,
370
- tunnelToken: registration.tunnelToken,
371
- subdomain: registration.subdomain,
372
- frpSubdomainHost: cfg.frpSubdomainHost,
373
- staging: cfg.e2e.staging,
374
- onProgress: (step) => {
375
- this.setStartPhase("provisioning_tls", {
376
- publicUrl: registration.publicUrl,
377
- acmeStep: step
378
- });
379
- }
380
- };
381
- await startTunnelTlsServer({
382
- tlsPort: cfg.e2e.tlsPort,
383
- gatewayPort,
384
- acmeConfig,
385
- fetch: cfg.gatewayFetch
386
- });
387
- return {
388
- frpcLocalPort: cfg.e2e.tlsPort,
389
- frpcMode: "https"
390
- };
391
- }
392
339
  };
393
340
  //#endregion
394
341
  export { TunnelService, getTunnelService, hashGatewayToken };