@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.
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{agents-DN3vr8pb.js → agents-Bh_9-1KB.js} +2 -2
- package/dist/gateway/static/root/assets/{agents-DN3vr8pb.js.map → agents-Bh_9-1KB.js.map} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-BUn41aPi.js → apps-page-CB5anZpc.js} +2 -2
- package/dist/gateway/static/root/assets/{apps-page-BUn41aPi.js.map → apps-page-CB5anZpc.js.map} +1 -1
- package/dist/gateway/static/root/assets/{channels-settings-CYMmWDtP.js → channels-settings-Bt1sprhC.js} +2 -2
- package/dist/gateway/static/root/assets/{channels-settings-CYMmWDtP.js.map → channels-settings-Bt1sprhC.js.map} +1 -1
- package/dist/gateway/static/root/assets/{channels-status-swr-sJj4ueTp.js → channels-status-swr-Crgak3fg.js} +2 -2
- package/dist/gateway/static/root/assets/{channels-status-swr-sJj4ueTp.js.map → channels-status-swr-Crgak3fg.js.map} +1 -1
- package/dist/gateway/static/root/assets/{cron-api-CLxnaHdq.js → cron-api-CzJGvQQ7.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-api-CLxnaHdq.js.map → cron-api-CzJGvQQ7.js.map} +1 -1
- package/dist/gateway/static/root/assets/{cron-page-BAQ8xSnJ.js → cron-page-BoNRJNVV.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-page-BAQ8xSnJ.js.map → cron-page-BoNRJNVV.js.map} +1 -1
- package/dist/gateway/static/root/assets/{dist-BfJYxiK5.js → dist-a-eaOUvs.js} +2 -2
- package/dist/gateway/static/root/assets/{dist-BfJYxiK5.js.map → dist-a-eaOUvs.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-bgvVs-Sy.js → extension-debug-page-D-O1XjAa.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-debug-page-bgvVs-Sy.js.map → extension-debug-page-D-O1XjAa.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-SG4TVv-u.js → extension-page-B2VpqBTH.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-page-SG4TVv-u.js.map → extension-page-B2VpqBTH.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-CJZRTsjF.js → extension-settings-page-CmBcQfeO.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-settings-page-CJZRTsjF.js.map → extension-settings-page-CmBcQfeO.js.map} +1 -1
- package/dist/gateway/static/root/assets/{fetch-K_0JRCXU.js → fetch-EGO9T3MN.js} +3 -3
- package/dist/gateway/static/root/assets/{fetch-K_0JRCXU.js.map → fetch-EGO9T3MN.js.map} +1 -1
- package/dist/gateway/static/root/assets/{field-primitives-Z76hyBYS.js → field-primitives-Bh7G1y4D.js} +2 -2
- package/dist/gateway/static/root/assets/{field-primitives-Z76hyBYS.js.map → field-primitives-Bh7G1y4D.js.map} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-config-api-BqfDabSI.js → heartbeat-config-api-DxpIEZNs.js} +2 -2
- package/dist/gateway/static/root/assets/{heartbeat-config-api-BqfDabSI.js.map → heartbeat-config-api-DxpIEZNs.js.map} +1 -1
- package/dist/gateway/static/root/assets/{index-ChiUhJAs.js → index-Dxy9ZCtC.js} +5 -5
- package/dist/gateway/static/root/assets/{index-ChiUhJAs.js.map → index-Dxy9ZCtC.js.map} +1 -1
- package/dist/gateway/static/root/assets/{logs-page-DrIMhDE2.js → logs-page-Dw58E2GE.js} +2 -2
- package/dist/gateway/static/root/assets/{logs-page-DrIMhDE2.js.map → logs-page-Dw58E2GE.js.map} +1 -1
- package/dist/gateway/static/root/assets/{sessions-page-B-RGO3N0.js → sessions-page-CPkhCy57.js} +2 -2
- package/dist/gateway/static/root/assets/{sessions-page-B-RGO3N0.js.map → sessions-page-CPkhCy57.js.map} +1 -1
- package/dist/gateway/static/root/assets/{settings-form-section-Csvl1iL6.js → settings-form-section-DLZDVMEf.js} +2 -2
- package/dist/gateway/static/root/assets/{settings-form-section-Csvl1iL6.js.map → settings-form-section-DLZDVMEf.js.map} +1 -1
- package/dist/gateway/static/root/assets/settings-page-CVPCa0PE.js +4 -0
- package/dist/gateway/static/root/assets/settings-page-CVPCa0PE.js.map +1 -0
- package/dist/gateway/static/root/assets/{skills-page-dHwx2vh0.js → skills-page-DueZ9Qfg.js} +2 -2
- package/dist/gateway/static/root/assets/{skills-page-dHwx2vh0.js.map → skills-page-DueZ9Qfg.js.map} +1 -1
- package/dist/gateway/static/root/assets/{theme-store-Bl5A2Fd_.js → theme-store-CWPq9gW1.js} +2 -2
- package/dist/gateway/static/root/assets/{theme-store-Bl5A2Fd_.js.map → theme-store-CWPq9gW1.js.map} +1 -1
- package/dist/gateway/static/root/assets/{utils-COYrNFF7.js → utils-Cnix55r9.js} +2 -2
- package/dist/gateway/static/root/assets/{utils-COYrNFF7.js.map → utils-Cnix55r9.js.map} +1 -1
- package/dist/gateway/static/root/assets/{voice-api-key-field-5WZZaxH3.js → voice-api-key-field-BR3Ut06g.js} +2 -2
- package/dist/gateway/static/root/assets/{voice-api-key-field-5WZZaxH3.js.map → voice-api-key-field-BR3Ut06g.js.map} +1 -1
- package/dist/gateway/static/root/index.html +3 -3
- package/dist/package.js +1 -1
- package/dist/src/browser/providers/browser-ext-install.js +23 -4
- package/dist/src/browser/providers/browser-ext-install.js.map +1 -1
- package/dist/src/cli/commands/tunnel.js +4 -5
- package/dist/src/cli/commands/tunnel.js.map +1 -1
- package/dist/src/config/index.js +2 -2
- package/dist/src/config/rules.js +0 -5
- package/dist/src/config/rules.js.map +1 -1
- package/dist/src/config/schema.d.ts +0 -27
- package/dist/src/config/schema.js +4 -18
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/gateway/auth-rate-limit.d.ts +2 -0
- package/dist/src/gateway/auth-rate-limit.js +9 -3
- package/dist/src/gateway/auth-rate-limit.js.map +1 -1
- package/dist/src/gateway/hono/app.js +19 -13
- package/dist/src/gateway/hono/app.js.map +1 -1
- package/dist/src/gateway/hono/lib/config-payload.d.ts +3 -1
- package/dist/src/gateway/hono/lib/config-payload.js +1 -2
- package/dist/src/gateway/hono/lib/config-payload.js.map +1 -1
- package/dist/src/gateway/hono/routes/tunnel.js +32 -30
- package/dist/src/gateway/hono/routes/tunnel.js.map +1 -1
- package/dist/src/gateway/host.d.ts +24 -0
- package/dist/src/gateway/host.js +33 -1
- package/dist/src/gateway/host.js.map +1 -1
- package/dist/src/gateway/index.d.ts +1 -1
- package/dist/src/gateway/index.js +2 -2
- package/dist/src/gateway/runtime-config.js +1 -8
- package/dist/src/gateway/runtime-config.js.map +1 -1
- package/dist/src/gateway/security/audit.js +4 -4
- package/dist/src/gateway/security/audit.js.map +1 -1
- package/dist/src/gateway/server.js +2 -3
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service/types.d.ts +2 -0
- package/dist/src/gateway/service.d.ts +2 -0
- package/dist/src/gateway/service.js +7 -2
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/tunnel/frp-subdomain-host.d.ts +2 -0
- package/dist/src/tunnel/frp-subdomain-host.js +15 -0
- package/dist/src/tunnel/frp-subdomain-host.js.map +1 -0
- package/dist/src/tunnel/gateway-lifecycle.d.ts +0 -4
- package/dist/src/tunnel/gateway-lifecycle.js +9 -11
- package/dist/src/tunnel/gateway-lifecycle.js.map +1 -1
- package/dist/src/tunnel/index.d.ts +2 -4
- package/dist/src/tunnel/index.js +2 -4
- package/dist/src/tunnel/pair-url.js +7 -1
- package/dist/src/tunnel/pair-url.js.map +1 -1
- package/dist/src/tunnel/pairing.d.ts +13 -0
- package/dist/src/tunnel/pairing.js +48 -1
- package/dist/src/tunnel/pairing.js.map +1 -1
- package/dist/src/tunnel/tunnel-config.js +2 -16
- package/dist/src/tunnel/tunnel-config.js.map +1 -1
- package/dist/src/tunnel/tunnel-service.d.ts +1 -10
- package/dist/src/tunnel/tunnel-service.js +7 -60
- package/dist/src/tunnel/tunnel-service.js.map +1 -1
- package/dist/src/tunnel/tunnel-types.d.ts +3 -18
- package/dist/src/tunnel/well-known.d.ts +5 -0
- package/dist/src/tunnel/well-known.js +2 -1
- package/dist/src/tunnel/well-known.js.map +1 -1
- package/package.json +2 -2
- package/dist/gateway/static/root/assets/settings-page-nxAc0ta1.js +0 -4
- package/dist/gateway/static/root/assets/settings-page-nxAc0ta1.js.map +0 -1
- package/dist/src/tunnel/acme-cert-store.d.ts +0 -34
- package/dist/src/tunnel/acme-cert-store.js +0 -184
- package/dist/src/tunnel/acme-cert-store.js.map +0 -1
- package/dist/src/tunnel/acme-client.d.ts +0 -50
- package/dist/src/tunnel/acme-client.js +0 -473
- package/dist/src/tunnel/acme-client.js.map +0 -1
- package/dist/src/tunnel/acme-crypto.d.ts +0 -25
- package/dist/src/tunnel/acme-crypto.js +0 -58
- package/dist/src/tunnel/acme-crypto.js.map +0 -1
- package/dist/src/tunnel/acme-csr.d.ts +0 -5
- package/dist/src/tunnel/acme-csr.js +0 -48
- package/dist/src/tunnel/acme-csr.js.map +0 -1
- package/dist/src/tunnel/tls-server.d.ts +0 -14
- package/dist/src/tunnel/tls-server.js +0 -126
- package/dist/src/tunnel/tls-server.js.map +0 -1
- package/dist/src/tunnel/tunnel-e2e-config.d.ts +0 -11
- package/dist/src/tunnel/tunnel-e2e-config.js +0 -29
- 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 {
|
|
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
|
|
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';
|
package/dist/src/tunnel/index.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
|
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;;
|
|
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,
|
|
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
|
|
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
|
-
|
|
74
|
-
|
|
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
|
|
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
|
|
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 };
|