@xopcai/xopc 0.0.66 → 0.0.67

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 (78) hide show
  1. package/dist/extensions/telegram/xopc.extension.json +1 -1
  2. package/dist/gateway/static/root/assets/{agents-JiJR38n1.js → agents-CmhOU0fB.js} +2 -2
  3. package/dist/gateway/static/root/assets/{agents-JiJR38n1.js.map → agents-CmhOU0fB.js.map} +1 -1
  4. package/dist/gateway/static/root/assets/{apps-page-aBDyr7Im.js → apps-page-CzS9A_6L.js} +2 -2
  5. package/dist/gateway/static/root/assets/{apps-page-aBDyr7Im.js.map → apps-page-CzS9A_6L.js.map} +1 -1
  6. package/dist/gateway/static/root/assets/{channels-settings-DXxfgG4N.js → channels-settings-Bn0YSztA.js} +2 -2
  7. package/dist/gateway/static/root/assets/{channels-settings-DXxfgG4N.js.map → channels-settings-Bn0YSztA.js.map} +1 -1
  8. package/dist/gateway/static/root/assets/{cron-dreaming-jobs-3--QJSP5.js → cron-dreaming-jobs-bAYsiHdF.js} +2 -2
  9. package/dist/gateway/static/root/assets/{cron-dreaming-jobs-3--QJSP5.js.map → cron-dreaming-jobs-bAYsiHdF.js.map} +1 -1
  10. package/dist/gateway/static/root/assets/{cron-page-CCn-JwBy.js → cron-page-CKqDtScu.js} +2 -2
  11. package/dist/gateway/static/root/assets/{cron-page-CCn-JwBy.js.map → cron-page-CKqDtScu.js.map} +1 -1
  12. package/dist/gateway/static/root/assets/{dist-BS-cwfHQ.js → dist-CUMhnSuX.js} +2 -2
  13. package/dist/gateway/static/root/assets/{dist-BS-cwfHQ.js.map → dist-CUMhnSuX.js.map} +1 -1
  14. package/dist/gateway/static/root/assets/{extension-debug-page-M8JS4Jid.js → extension-debug-page-BDls5xp3.js} +2 -2
  15. package/dist/gateway/static/root/assets/{extension-debug-page-M8JS4Jid.js.map → extension-debug-page-BDls5xp3.js.map} +1 -1
  16. package/dist/gateway/static/root/assets/{extension-page-C8gjVE_t.js → extension-page-BSfv53OO.js} +2 -2
  17. package/dist/gateway/static/root/assets/{extension-page-C8gjVE_t.js.map → extension-page-BSfv53OO.js.map} +1 -1
  18. package/dist/gateway/static/root/assets/{extension-settings-page-DeY2M2di.js → extension-settings-page-B0nkVsLi.js} +2 -2
  19. package/dist/gateway/static/root/assets/{extension-settings-page-DeY2M2di.js.map → extension-settings-page-B0nkVsLi.js.map} +1 -1
  20. package/dist/gateway/static/root/assets/{heartbeat-config-api-DWmeyjyR.js → heartbeat-config-api-P5zdbIBw.js} +2 -2
  21. package/dist/gateway/static/root/assets/{heartbeat-config-api-DWmeyjyR.js.map → heartbeat-config-api-P5zdbIBw.js.map} +1 -1
  22. package/dist/gateway/static/root/assets/{index-Cc57jhuG.js → index-DAglRwh4.js} +5 -5
  23. package/dist/gateway/static/root/assets/{index-Cc57jhuG.js.map → index-DAglRwh4.js.map} +1 -1
  24. package/dist/gateway/static/root/assets/index-DHj3Cf9B.css +1 -0
  25. package/dist/gateway/static/root/assets/{logs-page-kIsSDMqb.js → logs-page-CfGDv4k7.js} +2 -2
  26. package/dist/gateway/static/root/assets/{logs-page-kIsSDMqb.js.map → logs-page-CfGDv4k7.js.map} +1 -1
  27. package/dist/gateway/static/root/assets/{sessions-page-Da9gjMh7.js → sessions-page-DoGE8N-O.js} +2 -2
  28. package/dist/gateway/static/root/assets/{sessions-page-Da9gjMh7.js.map → sessions-page-DoGE8N-O.js.map} +1 -1
  29. package/dist/gateway/static/root/assets/settings-page-C5VrwDoO.js +3 -0
  30. package/dist/gateway/static/root/assets/settings-page-C5VrwDoO.js.map +1 -0
  31. package/dist/gateway/static/root/assets/{skills-page-B_msZcLx.js → skills-page-ADUi69Fa.js} +2 -2
  32. package/dist/gateway/static/root/assets/{skills-page-B_msZcLx.js.map → skills-page-ADUi69Fa.js.map} +1 -1
  33. package/dist/gateway/static/root/assets/{use-image-provider-credentials-Bs8xl2E3.js → use-image-provider-credentials-C4x4dNv2.js} +2 -2
  34. package/dist/gateway/static/root/assets/{use-image-provider-credentials-Bs8xl2E3.js.map → use-image-provider-credentials-C4x4dNv2.js.map} +1 -1
  35. package/dist/gateway/static/root/index.html +2 -2
  36. package/dist/package.js +1 -1
  37. package/dist/src/cli/commands/tunnel.js +69 -8
  38. package/dist/src/cli/commands/tunnel.js.map +1 -1
  39. package/dist/src/config/schema.d.ts +2 -0
  40. package/dist/src/config/schema.js +2 -0
  41. package/dist/src/config/schema.js.map +1 -1
  42. package/dist/src/gateway/hono/lib/config-payload.d.ts +2 -5
  43. package/dist/src/gateway/hono/lib/config-payload.js +3 -5
  44. package/dist/src/gateway/hono/lib/config-payload.js.map +1 -1
  45. package/dist/src/gateway/hono/routes/tunnel.js +12 -10
  46. package/dist/src/gateway/hono/routes/tunnel.js.map +1 -1
  47. package/dist/src/tunnel/acme-client.d.ts +11 -0
  48. package/dist/src/tunnel/acme-client.js +54 -10
  49. package/dist/src/tunnel/acme-client.js.map +1 -1
  50. package/dist/src/tunnel/env.d.ts +14 -3
  51. package/dist/src/tunnel/env.js +34 -5
  52. package/dist/src/tunnel/env.js.map +1 -1
  53. package/dist/src/tunnel/frpc-binary.d.ts +6 -1
  54. package/dist/src/tunnel/frpc-binary.js +66 -40
  55. package/dist/src/tunnel/frpc-binary.js.map +1 -1
  56. package/dist/src/tunnel/frpc-extract.d.ts +10 -0
  57. package/dist/src/tunnel/frpc-extract.js +129 -0
  58. package/dist/src/tunnel/frpc-extract.js.map +1 -0
  59. package/dist/src/tunnel/gateway-lifecycle.d.ts +3 -1
  60. package/dist/src/tunnel/gateway-lifecycle.js +6 -4
  61. package/dist/src/tunnel/gateway-lifecycle.js.map +1 -1
  62. package/dist/src/tunnel/index.d.ts +3 -1
  63. package/dist/src/tunnel/index.js +2 -2
  64. package/dist/src/tunnel/tls-server.js +5 -0
  65. package/dist/src/tunnel/tls-server.js.map +1 -1
  66. package/dist/src/tunnel/tunnel-config.js +11 -1
  67. package/dist/src/tunnel/tunnel-config.js.map +1 -1
  68. package/dist/src/tunnel/tunnel-e2e-config.d.ts +4 -1
  69. package/dist/src/tunnel/tunnel-e2e-config.js +10 -3
  70. package/dist/src/tunnel/tunnel-e2e-config.js.map +1 -1
  71. package/dist/src/tunnel/tunnel-service.d.ts +4 -0
  72. package/dist/src/tunnel/tunnel-service.js +66 -5
  73. package/dist/src/tunnel/tunnel-service.js.map +1 -1
  74. package/dist/src/tunnel/tunnel-types.d.ts +16 -0
  75. package/package.json +3 -3
  76. package/dist/gateway/static/root/assets/index-B5gp15_q.css +0 -1
  77. package/dist/gateway/static/root/assets/settings-page-B0spIRVS.js +0 -3
  78. package/dist/gateway/static/root/assets/settings-page-B0spIRVS.js.map +0 -1
@@ -0,0 +1,129 @@
1
+ import { createLogger } from "../utils/logger/index.js";
2
+ import { init_logger } from "../utils/logger.js";
3
+ import { dirname, join } from "node:path";
4
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { spawn } from "node:child_process";
6
+ import { gunzipSync } from "node:zlib";
7
+ //#region src/tunnel/frpc-extract.ts
8
+ init_logger();
9
+ const log = createLogger("TunnelFrpcExtract");
10
+ function buildFrpcArchiveMemberPath(folder, platform) {
11
+ return `${folder}/frpc${platform === "win32" ? ".exe" : ""}`.replace(/\\/g, "/");
12
+ }
13
+ /** Resolve on-disk path after system tar extracts a POSIX member path. */
14
+ function resolveExtractedMemberPath(extractDir, memberPath) {
15
+ return join(extractDir, ...memberPath.replace(/\\/g, "/").split("/"));
16
+ }
17
+ function readTarField(header, offset, length) {
18
+ return header.subarray(offset, offset + length).toString("utf8").replace(/\0.*$/, "").trim();
19
+ }
20
+ function parseTarEntryName(header) {
21
+ let name = readTarField(header, 0, 100);
22
+ const prefix = readTarField(header, 345, 155);
23
+ if (prefix) name = `${prefix}/${name}`;
24
+ return name.replace(/\\/g, "/").replace(/^\.\//, "");
25
+ }
26
+ function parseTarSize(header) {
27
+ const raw = readTarField(header, 124, 12);
28
+ if (!raw) return 0;
29
+ return parseInt(raw, 8) || 0;
30
+ }
31
+ /** Pure Node tar.gz member extract — no system `tar` (Windows / minimal Linux). */
32
+ function extractTarGzMemberNode(archivePath, memberPath, destPath) {
33
+ const target = memberPath.replace(/\\/g, "/").replace(/^\.\//, "");
34
+ const tar = gunzipSync(readFileSync(archivePath));
35
+ let offset = 0;
36
+ while (offset + 512 <= tar.length) {
37
+ const header = tar.subarray(offset, offset + 512);
38
+ if (header.every((b) => b === 0)) break;
39
+ const name = parseTarEntryName(header);
40
+ const size = parseTarSize(header);
41
+ offset += 512;
42
+ if (size < 0 || offset + size > tar.length) throw new Error("Corrupt tar archive");
43
+ const content = tar.subarray(offset, offset + size);
44
+ offset += size;
45
+ offset += (512 - size % 512) % 512;
46
+ if (name === target) {
47
+ mkdirSync(dirname(destPath), { recursive: true });
48
+ writeFileSync(destPath, content);
49
+ return;
50
+ }
51
+ }
52
+ throw new Error(`Archive missing expected path: ${target}`);
53
+ }
54
+ async function extractTarGzMemberSystemTar(archivePath, memberPath, destPath, extractDir) {
55
+ mkdirSync(extractDir, { recursive: true });
56
+ await new Promise((resolve, reject) => {
57
+ let stderr = "";
58
+ const child = spawn("tar", [
59
+ "xzf",
60
+ archivePath,
61
+ "-C",
62
+ extractDir,
63
+ memberPath
64
+ ], { stdio: [
65
+ "ignore",
66
+ "ignore",
67
+ "pipe"
68
+ ] });
69
+ child.stderr?.on("data", (chunk) => {
70
+ stderr += chunk.toString("utf8");
71
+ });
72
+ child.on("error", reject);
73
+ child.on("exit", (code) => {
74
+ if (code === 0) resolve();
75
+ else {
76
+ const detail = stderr.trim();
77
+ reject(/* @__PURE__ */ new Error(`tar exited with code ${code ?? "unknown"}${detail ? `: ${detail}` : ""}`));
78
+ }
79
+ });
80
+ });
81
+ const extracted = resolveExtractedMemberPath(extractDir, memberPath);
82
+ if (!existsSync(extracted)) throw new Error(`Archive missing expected path: ${memberPath}`);
83
+ mkdirSync(dirname(destPath), { recursive: true });
84
+ writeFileSync(destPath, readFileSync(extracted));
85
+ }
86
+ /**
87
+ * Extract frpc from a release tarball.
88
+ * Tries system `tar` first; falls back to built-in Node parser (macOS BSD tar quirks, Windows without tar, etc.).
89
+ */
90
+ async function extractFrpcFromTarGzArchive(archivePath, destBin, folder, platform = process.platform) {
91
+ const memberPath = buildFrpcArchiveMemberPath(folder, platform);
92
+ const extractDir = join(dirname(destBin), `_frpc-extract-${process.pid}-${Date.now()}`);
93
+ let systemErr;
94
+ try {
95
+ await extractTarGzMemberSystemTar(archivePath, memberPath, destBin, extractDir);
96
+ log.debug({
97
+ memberPath,
98
+ method: "system-tar"
99
+ }, "Extracted frpc from archive");
100
+ return;
101
+ } catch (err) {
102
+ systemErr = err;
103
+ log.debug({
104
+ err,
105
+ memberPath,
106
+ phase: "frpc_extract_system_tar"
107
+ }, "System tar extract failed — trying Node fallback");
108
+ } finally {
109
+ rmSync(extractDir, {
110
+ recursive: true,
111
+ force: true
112
+ });
113
+ }
114
+ try {
115
+ extractTarGzMemberNode(archivePath, memberPath, destBin);
116
+ log.debug({
117
+ memberPath,
118
+ method: "node-tar"
119
+ }, "Extracted frpc from archive");
120
+ } catch (nodeErr) {
121
+ const systemEm = systemErr instanceof Error ? systemErr.message : String(systemErr);
122
+ const nodeEm = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);
123
+ throw new Error(`Failed to extract ${memberPath}: ${nodeEm} (system tar: ${systemEm})`);
124
+ }
125
+ }
126
+ //#endregion
127
+ export { buildFrpcArchiveMemberPath, extractFrpcFromTarGzArchive, extractTarGzMemberNode, resolveExtractedMemberPath };
128
+
129
+ //# sourceMappingURL=frpc-extract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frpc-extract.js","names":[],"sources":["../../../src/tunnel/frpc-extract.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { gunzipSync } from 'node:zlib';\n\nimport { createLogger } from '../utils/logger.js';\n\nconst log = createLogger('TunnelFrpcExtract');\n\nexport function buildFrpcArchiveMemberPath(folder: string, platform: NodeJS.Platform): string {\n const ext = platform === 'win32' ? '.exe' : '';\n return `${folder}/frpc${ext}`.replace(/\\\\/g, '/');\n}\n\n/** Resolve on-disk path after system tar extracts a POSIX member path. */\nexport function resolveExtractedMemberPath(extractDir: string, memberPath: string): string {\n return join(extractDir, ...memberPath.replace(/\\\\/g, '/').split('/'));\n}\n\nfunction readTarField(header: Buffer, offset: number, length: number): string {\n return header.subarray(offset, offset + length).toString('utf8').replace(/\\0.*$/, '').trim();\n}\n\nfunction parseTarEntryName(header: Buffer): string {\n let name = readTarField(header, 0, 100);\n const prefix = readTarField(header, 345, 155);\n if (prefix) name = `${prefix}/${name}`;\n return name.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n}\n\nfunction parseTarSize(header: Buffer): number {\n const raw = readTarField(header, 124, 12);\n if (!raw) return 0;\n return parseInt(raw, 8) || 0;\n}\n\n/** Pure Node tar.gz member extract — no system `tar` (Windows / minimal Linux). */\nexport function extractTarGzMemberNode(\n archivePath: string,\n memberPath: string,\n destPath: string,\n): void {\n const target = memberPath.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n const tar = gunzipSync(readFileSync(archivePath));\n\n let offset = 0;\n while (offset + 512 <= tar.length) {\n const header = tar.subarray(offset, offset + 512);\n if (header.every((b) => b === 0)) break;\n\n const name = parseTarEntryName(header);\n const size = parseTarSize(header);\n offset += 512;\n\n if (size < 0 || offset + size > tar.length) {\n throw new Error('Corrupt tar archive');\n }\n\n const content = tar.subarray(offset, offset + size);\n offset += size;\n offset += (512 - (size % 512)) % 512;\n\n if (name === target) {\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, content);\n return;\n }\n }\n\n throw new Error(`Archive missing expected path: ${target}`);\n}\n\nasync function extractTarGzMemberSystemTar(\n archivePath: string,\n memberPath: string,\n destPath: string,\n extractDir: string,\n): Promise<void> {\n mkdirSync(extractDir, { recursive: true });\n await new Promise<void>((resolve, reject) => {\n let stderr = '';\n const child = spawn('tar', ['xzf', archivePath, '-C', extractDir, memberPath], {\n stdio: ['ignore', 'ignore', 'pipe'],\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n stderr += chunk.toString('utf8');\n });\n child.on('error', reject);\n child.on('exit', (code) => {\n if (code === 0) resolve();\n else {\n const detail = stderr.trim();\n reject(\n new Error(`tar exited with code ${code ?? 'unknown'}${detail ? `: ${detail}` : ''}`),\n );\n }\n });\n });\n\n const extracted = resolveExtractedMemberPath(extractDir, memberPath);\n if (!existsSync(extracted)) {\n throw new Error(`Archive missing expected path: ${memberPath}`);\n }\n\n mkdirSync(dirname(destPath), { recursive: true });\n writeFileSync(destPath, readFileSync(extracted));\n}\n\n/**\n * Extract frpc from a release tarball.\n * Tries system `tar` first; falls back to built-in Node parser (macOS BSD tar quirks, Windows without tar, etc.).\n */\nexport async function extractFrpcFromTarGzArchive(\n archivePath: string,\n destBin: string,\n folder: string,\n platform: NodeJS.Platform = process.platform,\n): Promise<void> {\n const memberPath = buildFrpcArchiveMemberPath(folder, platform);\n const extractDir = join(dirname(destBin), `_frpc-extract-${process.pid}-${Date.now()}`);\n let systemErr: unknown;\n\n try {\n await extractTarGzMemberSystemTar(archivePath, memberPath, destBin, extractDir);\n log.debug({ memberPath, method: 'system-tar' }, 'Extracted frpc from archive');\n return;\n } catch (err) {\n systemErr = err;\n log.debug(\n { err, memberPath, phase: 'frpc_extract_system_tar' },\n 'System tar extract failed — trying Node fallback',\n );\n } finally {\n rmSync(extractDir, { recursive: true, force: true });\n }\n\n try {\n extractTarGzMemberNode(archivePath, memberPath, destBin);\n log.debug({ memberPath, method: 'node-tar' }, 'Extracted frpc from archive');\n } catch (nodeErr) {\n const systemEm = systemErr instanceof Error ? systemErr.message : String(systemErr);\n const nodeEm = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);\n throw new Error(`Failed to extract ${memberPath}: ${nodeEm} (system tar: ${systemEm})`);\n }\n}\n"],"mappings":"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"}
@@ -4,7 +4,9 @@ type TunnelEventSink = {
4
4
  };
5
5
  /** Publish `tunnel.status` on gateway SSE when tunnel lifecycle changes. */
6
6
  export declare function wireTunnelEventsToGateway(service: TunnelEventSink): void;
7
- export declare function configureTunnelFromGatewayConfig(config: Config): Promise<void>;
7
+ export declare function configureTunnelFromGatewayConfig(config: Config, opts?: {
8
+ force?: boolean;
9
+ }): Promise<void>;
8
10
  /**
9
11
  * Start FRP tunnel when `tunnel.autoStart` is set (CLI gateway / GatewayServer after HTTP listen).
10
12
  */
@@ -22,14 +22,16 @@ function wireTunnelEventsToGateway(service) {
22
22
  tunnel.on("tunnel:connected", publish);
23
23
  tunnel.on("tunnel:disconnected", publish);
24
24
  tunnel.on("tunnel:error", publish);
25
+ tunnel.on("tunnel:progress", publish);
25
26
  subscribeCertStatus((cert) => {
26
27
  service.emit("tunnel.cert.status", cert);
27
28
  publish();
28
29
  });
29
30
  }
30
- async function configureTunnelFromGatewayConfig(config) {
31
- if (!config.tunnel?.enabled) return;
31
+ async function configureTunnelFromGatewayConfig(config, opts) {
32
+ if (!opts?.force && !config.tunnel?.enabled) return;
32
33
  const gateway = config.gateway ?? {};
34
+ const gatewayPort = gateway.port ?? 18790;
33
35
  let brokerUrl = resolveTunnelBrokerUrl(config.tunnel?.brokerUrl);
34
36
  try {
35
37
  const wellKnown = await fetchTunnelWellKnown(brokerUrl);
@@ -43,7 +45,7 @@ async function configureTunnelFromGatewayConfig(config) {
43
45
  }
44
46
  let registrationSecret;
45
47
  try {
46
- registrationSecret = resolveTunnelRegistrationSecret(process.env, brokerUrl);
48
+ registrationSecret = resolveTunnelRegistrationSecret(process.env, brokerUrl, config.tunnel?.registrationSecret);
47
49
  } catch (err) {
48
50
  const em = err instanceof Error ? err.message : String(err);
49
51
  log.warn({
@@ -57,7 +59,7 @@ async function configureTunnelFromGatewayConfig(config) {
57
59
  registrationSecret,
58
60
  autoStart: config.tunnel?.autoStart ?? false,
59
61
  gatewayHost: gateway.host ?? "127.0.0.1",
60
- e2e: resolveTunnelE2eConfig(config.tunnel),
62
+ e2e: resolveTunnelE2eConfig(config.tunnel, gatewayPort),
61
63
  frpSubdomainHost: resolveFrpSubdomainHost(brokerUrl)
62
64
  });
63
65
  }
@@ -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 { 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\n subscribeCertStatus((cert) => {\n service.emit('tunnel.cert.status', cert);\n publish();\n });\n}\n\nexport async function configureTunnelFromGatewayConfig(config: Config): Promise<void> {\n if (!config.tunnel?.enabled) return;\n\n const gateway = config.gateway ?? {};\n let brokerUrl = resolveTunnelBrokerUrl(config.tunnel?.brokerUrl);\n\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\n let registrationSecret: string;\n try {\n registrationSecret = resolveTunnelRegistrationSecret(process.env, brokerUrl);\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: gateway.host ?? '127.0.0.1',\n e2e: resolveTunnelE2eConfig(config.tunnel),\n frpSubdomainHost: resolveFrpSubdomainHost(brokerUrl),\n });\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 = gateway.host ?? '127.0.0.1';\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":";;;;;;;;;aACkD;AAQlD,MAAM,MAAM,aAAa,SAAS;AAIlC,IAAI,iBAAiB;;AAGrB,SAAgB,0BAA0B,SAAgC;AACxE,KAAI,eAAgB;AACpB,kBAAiB;CAEjB,MAAM,SAAS,kBAAkB;CACjC,MAAM,gBAAgB;AACpB,UAAQ,KAAK,iBAAiB,OAAO,WAAW,CAAC;;AAGnD,QAAO,GAAG,qBAAqB,QAAQ;AACvC,QAAO,GAAG,oBAAoB,QAAQ;AACtC,QAAO,GAAG,uBAAuB,QAAQ;AACzC,QAAO,GAAG,gBAAgB,QAAQ;AAElC,sBAAqB,SAAS;AAC5B,UAAQ,KAAK,sBAAsB,KAAK;AACxC,WAAS;GACT;;AAGJ,eAAsB,iCAAiC,QAA+B;AACpF,KAAI,CAAC,OAAO,QAAQ,QAAS;CAE7B,MAAM,UAAU,OAAO,WAAW,EAAE;CACpC,IAAI,YAAY,uBAAuB,OAAO,QAAQ,UAAU;AAEhE,KAAI;EACF,MAAM,YAAY,MAAM,qBAAqB,UAAU;AACvD,MAAI,UAAU,WAAW,MAAM,CAC7B,aAAY,UAAU,UAAU,MAAM;UAEjC,KAAK;AACZ,MAAI,MACF;GAAE;GAAK;GAAW,OAAO;GAAqB,EAC9C,gEACD;;CAGH,IAAI;AACJ,KAAI;AACF,uBAAqB,gCAAgC,QAAQ,KAAK,UAAU;UACrE,KAAK;EACZ,MAAM,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC3D,MAAI,KAAK;GAAE,OAAO;GAAoB,cAAc;GAAI,EAAE,GAAG;AAC7D,QAAM;;AAGR,mBAAkB,CAAC,UAAU;EAC3B;EACA;EACA,WAAW,OAAO,QAAQ,aAAa;EACvC,aAAa,QAAQ,QAAQ;EAC7B,KAAK,uBAAuB,OAAO,OAAO;EAC1C,kBAAkB,wBAAwB,UAAU;EACrD,CAAC;;;;;AAMJ,eAAsB,+BACpB,QACA,cACe;AACf,KAAI,CAAC,OAAO,QAAQ,UAAW;AAE/B,KAAI,CAAC,sBAAsB,OAAO,EAAE;AAClC,MAAI,KACF;GAAE,OAAO;GAAoB,gBAAgB,OAAO,QAAQ,SAAS,WAAW;GAAM,EACtF,kEACD;AACD;;AAGF,KAAI,OAAO,OAAO,YAAY,MAAM;AAClC,MAAI,MACF,EAAE,OAAO,oBAAoB,EAC7B,+EACD;AACD;;CAGF,MAAM,UAAU,OAAO,WAAW,EAAE;CACpC,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,OAAO,QAAQ,QAAQ;AAE7B,OAAM,iCAAiC,OAAO;AAE9C,KAAI,CAAC,cAAc;AACjB,MAAI,KACF,EAAE,OAAO,oBAAoB,EAC7B,4FACD;AACD;;CAGF,MAAM,SAAS,kBAAkB;CACjC,MAAM,EAAE,UAAU,OAAO,WAAW;AACpC,KAAI,UAAU,eAAe,UAAU,gBAAgB,UAAU,gBAAgB;AAC/E,MAAI,MAAM;GAAE,OAAO;GAAoB;GAAO,EAAE,yCAAyC;AACzF;;AAGF,KAAI;AACF,QAAM,OAAO,MAAM,MAAM,aAAa;AACtC,MAAI,KACF;GAAE,OAAO;GAAoB;GAAM;GAAM,EACzC,gDACD;UACM,KAAK;EACZ,MAAM,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC3D,MAAI,MAAM;GAAE;GAAK,OAAO;GAAoB,cAAc;GAAI,EAAE,4BAA4B,KAAK"}
1
+ {"version":3,"file":"gateway-lifecycle.js","names":[],"sources":["../../../src/tunnel/gateway-lifecycle.ts"],"sourcesContent":["import type { Config } from '../config/schema.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 async function configureTunnelFromGatewayConfig(\n config: Config,\n opts?: { force?: boolean },\n): Promise<void> {\n if (!opts?.force && !config.tunnel?.enabled) return;\n\n const gateway = config.gateway ?? {};\n const gatewayPort = gateway.port ?? 18790;\n let brokerUrl = resolveTunnelBrokerUrl(config.tunnel?.brokerUrl);\n\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\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: gateway.host ?? '127.0.0.1',\n e2e: resolveTunnelE2eConfig(config.tunnel, gatewayPort),\n frpSubdomainHost: resolveFrpSubdomainHost(brokerUrl),\n });\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 = gateway.host ?? '127.0.0.1';\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":";;;;;;;;;aACkD;AAQlD,MAAM,MAAM,aAAa,SAAS;AAIlC,IAAI,iBAAiB;;AAGrB,SAAgB,0BAA0B,SAAgC;AACxE,KAAI,eAAgB;AACpB,kBAAiB;CAEjB,MAAM,SAAS,kBAAkB;CACjC,MAAM,gBAAgB;AACpB,UAAQ,KAAK,iBAAiB,OAAO,WAAW,CAAC;;AAGnD,QAAO,GAAG,qBAAqB,QAAQ;AACvC,QAAO,GAAG,oBAAoB,QAAQ;AACtC,QAAO,GAAG,uBAAuB,QAAQ;AACzC,QAAO,GAAG,gBAAgB,QAAQ;AAClC,QAAO,GAAG,mBAAmB,QAAQ;AAErC,sBAAqB,SAAS;AAC5B,UAAQ,KAAK,sBAAsB,KAAK;AACxC,WAAS;GACT;;AAGJ,eAAsB,iCACpB,QACA,MACe;AACf,KAAI,CAAC,MAAM,SAAS,CAAC,OAAO,QAAQ,QAAS;CAE7C,MAAM,UAAU,OAAO,WAAW,EAAE;CACpC,MAAM,cAAc,QAAQ,QAAQ;CACpC,IAAI,YAAY,uBAAuB,OAAO,QAAQ,UAAU;AAEhE,KAAI;EACF,MAAM,YAAY,MAAM,qBAAqB,UAAU;AACvD,MAAI,UAAU,WAAW,MAAM,CAC7B,aAAY,UAAU,UAAU,MAAM;UAEjC,KAAK;AACZ,MAAI,MACF;GAAE;GAAK;GAAW,OAAO;GAAqB,EAC9C,gEACD;;CAGH,IAAI;AACJ,KAAI;AACF,uBAAqB,gCACnB,QAAQ,KACR,WACA,OAAO,QAAQ,mBAChB;UACM,KAAK;EACZ,MAAM,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC3D,MAAI,KAAK;GAAE,OAAO;GAAoB,cAAc;GAAI,EAAE,GAAG;AAC7D,QAAM;;AAGR,mBAAkB,CAAC,UAAU;EAC3B;EACA;EACA,WAAW,OAAO,QAAQ,aAAa;EACvC,aAAa,QAAQ,QAAQ;EAC7B,KAAK,uBAAuB,OAAO,QAAQ,YAAY;EACvD,kBAAkB,wBAAwB,UAAU;EACrD,CAAC;;;;;AAMJ,eAAsB,+BACpB,QACA,cACe;AACf,KAAI,CAAC,OAAO,QAAQ,UAAW;AAE/B,KAAI,CAAC,sBAAsB,OAAO,EAAE;AAClC,MAAI,KACF;GAAE,OAAO;GAAoB,gBAAgB,OAAO,QAAQ,SAAS,WAAW;GAAM,EACtF,kEACD;AACD;;AAGF,KAAI,OAAO,OAAO,YAAY,MAAM;AAClC,MAAI,MACF,EAAE,OAAO,oBAAoB,EAC7B,+EACD;AACD;;CAGF,MAAM,UAAU,OAAO,WAAW,EAAE;CACpC,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,OAAO,QAAQ,QAAQ;AAE7B,OAAM,iCAAiC,OAAO;AAE9C,KAAI,CAAC,cAAc;AACjB,MAAI,KACF,EAAE,OAAO,oBAAoB,EAC7B,4FACD;AACD;;CAGF,MAAM,SAAS,kBAAkB;CACjC,MAAM,EAAE,UAAU,OAAO,WAAW;AACpC,KAAI,UAAU,eAAe,UAAU,gBAAgB,UAAU,gBAAgB;AAC/E,MAAI,MAAM;GAAE,OAAO;GAAoB;GAAO,EAAE,yCAAyC;AACzF;;AAGF,KAAI;AACF,QAAM,OAAO,MAAM,MAAM,aAAa;AACtC,MAAI,KACF;GAAE,OAAO;GAAoB;GAAM;GAAM,EACzC,gDACD;UACM,KAAK;EACZ,MAAM,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC3D,MAAI,MAAM;GAAE;GAAK,OAAO;GAAoB,cAAc;GAAI,EAAE,4BAA4B,KAAK"}
@@ -2,9 +2,11 @@ export { assertTunnelMayStart, CURRENT_TUNNEL_CONSENT_VERSION, getTunnelConsentS
2
2
  export { TunnelBrokerClient, resolveBrokerApiBase } from './broker-client.js';
3
3
  export { applyTunnelConsentToConfig, mergeTunnelConfigPatch, sanitizeTunnelConfig, setTunnelEnabledInConfig, } from './tunnel-config.js';
4
4
  export { clearFrpcPathForProcess, ensureFrpcBinary, FRPC_VERSION, publishFrpcPathForProcess, } from './frpc-binary.js';
5
+ export type { EnsureFrpcBinaryOptions, FrpcDownloadProgress } from './frpc-binary.js';
5
6
  export { configureTunnelFromGatewayConfig, maybeAutoStartTunnelFromConfig, wireTunnelEventsToGateway, } from './gateway-lifecycle.js';
6
7
  export { fetchTunnelWellKnown, clearTunnelWellKnownCache } from './well-known.js';
7
- export { isProductionTunnelBroker, resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret, } from './env.js';
8
+ export { getTunnelRegistrationSecretMeta, isProductionTunnelBroker, isMaskedTunnelSecretPatchValue, resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret, } from './env.js';
9
+ export type { TunnelRegistrationSecretMeta, TunnelRegistrationSecretSource } from './env.js';
8
10
  export { logTunnelAudit } from './tunnel-audit.js';
9
11
  export type { TunnelAuditEvent } from './tunnel-audit.js';
10
12
  export { consumeTunnelMutationLimit, resetTunnelMutationLimitsForTests } from './tunnel-rate-limit.js';
@@ -1,6 +1,6 @@
1
1
  import { CURRENT_TUNNEL_CONSENT_VERSION, TUNNEL_CONSENT_REQUIRED_CODE, TUNNEL_RISK_SUMMARY_LINES, TunnelConsentError, assertTunnelMayStart, getTunnelConsentState, hasValidTunnelConsent } from "./consent.js";
2
2
  import { getCertStatusSummary, recordRenewalFailure, subscribeCertStatus } from "./acme-cert-store.js";
3
- import { isProductionTunnelBroker, resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret } from "./env.js";
3
+ import { getTunnelRegistrationSecretMeta, isMaskedTunnelSecretPatchValue, isProductionTunnelBroker, resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret } from "./env.js";
4
4
  import { TunnelBrokerClient, resolveBrokerApiBase } from "./broker-client.js";
5
5
  import { FRPC_VERSION, clearFrpcPathForProcess, ensureFrpcBinary, publishFrpcPathForProcess } from "./frpc-binary.js";
6
6
  import { buildMobileConnectQrPayload, resolveLanGatewayUrl } from "./tunnel-qr.js";
@@ -14,4 +14,4 @@ import { clearTunnelWellKnownCache, fetchTunnelWellKnown } from "./well-known.js
14
14
  import { configureTunnelFromGatewayConfig, maybeAutoStartTunnelFromConfig, wireTunnelEventsToGateway } from "./gateway-lifecycle.js";
15
15
  import { applyTunnelConsentToConfig, mergeTunnelConfigPatch, sanitizeTunnelConfig, setTunnelEnabledInConfig } from "./tunnel-config.js";
16
16
  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, getTunnelService, hasValidTunnelConsent, hashGatewayToken, isProductionTunnelBroker, loadTunnelState, logTunnelAudit, maybeAutoStartTunnelFromConfig, mergeTunnelConfigPatch, publishFrpcPathForProcess, recordRenewalFailure, resetPairingSessionsForTests, resetTunnelMutationLimitsForTests, resolveBrokerApiBase, resolveFrpSubdomainHost, resolveLanGatewayUrl, resolveTunnelBrokerUrl, resolveTunnelE2eConfig, resolveTunnelRegistrationSecret, resolveTunnelStatePath, sanitizeTunnelConfig, saveTunnelState, setTunnelEnabledInConfig, stopTunnelTlsServer, subscribeCertStatus, wireTunnelEventsToGateway };
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 };
@@ -79,6 +79,11 @@ async function startTunnelTlsServer(config) {
79
79
  await new Promise((resolve, reject) => {
80
80
  httpsNodeServer.once("error", reject);
81
81
  httpsNodeServer.listen(config.tlsPort, "127.0.0.1", () => resolve());
82
+ }).catch((err) => {
83
+ httpsNodeServer?.close();
84
+ httpsNodeServer = null;
85
+ if ((err instanceof Error ? err.message : String(err)).includes("EADDRINUSE")) throw new Error(`Tunnel TLS port ${config.tlsPort} is already in use. Stop other xopc tunnel instances or set tunnel.e2e.tlsPort (try ${config.gatewayPort + 1} for gateway port ${config.gatewayPort}).`);
86
+ throw err;
82
87
  });
83
88
  log.info({
84
89
  port: config.tlsPort,
@@ -1 +1 @@
1
- {"version":3,"file":"tls-server.js","names":["createHttpsServer"],"sources":["../../../src/tunnel/tls-server.ts"],"sourcesContent":["import { createServer as createHttpsServer, type Server } from 'node:https';\nimport { Readable } from 'node:stream';\n\nimport { createLogger } from '../utils/logger.js';\nimport {\n ensureValidCert,\n loadStoredCert,\n startRenewalScheduler,\n stopRenewalScheduler,\n type StoredCert,\n} from './acme-cert-store.js';\nimport type { AcmeConfig } from './acme-client.js';\n\nconst log = createLogger('TunnelTLS');\n\nlet httpsNodeServer: Server | null = null;\n\nexport type TunnelTlsServerConfig = {\n tlsPort: number;\n gatewayPort: number;\n acmeConfig: AcmeConfig;\n fetch?: typeof fetch;\n};\n\nfunction resolveTlsFetch(\n gatewayPort: number,\n customFetch?: typeof fetch,\n): (req: Request) => Response | Promise<Response> {\n if (customFetch) {\n return (req: Request) => customFetch(req);\n }\n return async (req: Request) => {\n const url = new URL(req.url);\n url.protocol = 'http:';\n url.hostname = '127.0.0.1';\n url.port = String(gatewayPort);\n return fetch(url, {\n method: req.method,\n headers: req.headers,\n body: req.body,\n redirect: 'manual',\n duplex: 'half',\n } as RequestInit);\n };\n}\n\nasync function nodeRequestToFetch(req: import('node:http').IncomingMessage, tlsPort: number): Promise<Request> {\n const url = new URL(req.url ?? '/', `https://127.0.0.1:${tlsPort}`);\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const part of value) headers.append(key, part);\n } else {\n headers.set(key, value);\n }\n }\n\n const hasBody = req.method !== 'GET' && req.method !== 'HEAD';\n const init: RequestInit = {\n method: req.method,\n headers,\n };\n if (hasBody) {\n init.body = Readable.toWeb(req) as RequestInit['body'];\n (init as RequestInit & { duplex?: 'half' }).duplex = 'half';\n }\n return new Request(url, init);\n}\n\nasync function sendFetchResponse(res: import('node:http').ServerResponse, response: Response): Promise<void> {\n res.writeHead(response.status, Object.fromEntries(response.headers.entries()));\n if (response.body) {\n const reader = response.body.getReader();\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n res.write(value);\n }\n }\n res.end();\n}\n\nexport async function startTunnelTlsServer(config: TunnelTlsServerConfig): Promise<Server> {\n if (httpsNodeServer) return httpsNodeServer;\n\n const cert = await ensureValidCert(config.acmeConfig);\n const handler = resolveTlsFetch(config.gatewayPort, config.fetch);\n\n httpsNodeServer = createHttpsServer(\n {\n key: cert.keyPem,\n cert: cert.certPem,\n minVersion: 'TLSv1.2',\n },\n (req, res) => {\n void (async () => {\n try {\n const request = await nodeRequestToFetch(req, config.tlsPort);\n const response = await handler(request);\n await sendFetchResponse(res, response);\n } catch (err) {\n log.warn({ err, phase: 'tls_proxy' }, 'TLS proxy request failed');\n if (!res.headersSent) res.writeHead(502);\n res.end('Bad Gateway');\n }\n })();\n },\n );\n\n await new Promise<void>((resolve, reject) => {\n httpsNodeServer!.once('error', reject);\n httpsNodeServer!.listen(config.tlsPort, '127.0.0.1', () => resolve());\n });\n\n log.info(\n { port: config.tlsPort, domain: cert.domain, expiresAt: cert.expiresAt },\n \"Tunnel TLS server listening (Let's Encrypt cert)\",\n );\n\n startRenewalScheduler(config.acmeConfig, () => reloadTlsCert());\n\n return httpsNodeServer;\n}\n\nfunction reloadTlsCert(): void {\n const cert = loadStoredCert();\n if (!cert || !httpsNodeServer) return;\n httpsNodeServer.setSecureContext({ key: cert.keyPem, cert: cert.certPem });\n log.info({ domain: cert.domain, expiresAt: cert.expiresAt }, 'TLS cert hot-reloaded');\n}\n\nexport function stopTunnelTlsServer(): void {\n stopRenewalScheduler();\n if (httpsNodeServer) {\n httpsNodeServer.close();\n httpsNodeServer = null;\n log.info('TLS server stopped');\n }\n}\n\nexport function getActiveTlsCert(): StoredCert | null {\n return loadStoredCert();\n}\n\n/** @internal */\nexport function resetTunnelTlsServerForTests(): void {\n stopTunnelTlsServer();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tls-server.js","names":["createHttpsServer"],"sources":["../../../src/tunnel/tls-server.ts"],"sourcesContent":["import { createServer as createHttpsServer, type Server } from 'node:https';\nimport { Readable } from 'node:stream';\n\nimport { createLogger } from '../utils/logger.js';\nimport {\n ensureValidCert,\n loadStoredCert,\n startRenewalScheduler,\n stopRenewalScheduler,\n type StoredCert,\n} from './acme-cert-store.js';\nimport type { AcmeConfig } from './acme-client.js';\n\nconst log = createLogger('TunnelTLS');\n\nlet httpsNodeServer: Server | null = null;\n\nexport type TunnelTlsServerConfig = {\n tlsPort: number;\n gatewayPort: number;\n acmeConfig: AcmeConfig;\n fetch?: typeof fetch;\n};\n\nfunction resolveTlsFetch(\n gatewayPort: number,\n customFetch?: typeof fetch,\n): (req: Request) => Response | Promise<Response> {\n if (customFetch) {\n return (req: Request) => customFetch(req);\n }\n return async (req: Request) => {\n const url = new URL(req.url);\n url.protocol = 'http:';\n url.hostname = '127.0.0.1';\n url.port = String(gatewayPort);\n return fetch(url, {\n method: req.method,\n headers: req.headers,\n body: req.body,\n redirect: 'manual',\n duplex: 'half',\n } as RequestInit);\n };\n}\n\nasync function nodeRequestToFetch(req: import('node:http').IncomingMessage, tlsPort: number): Promise<Request> {\n const url = new URL(req.url ?? '/', `https://127.0.0.1:${tlsPort}`);\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const part of value) headers.append(key, part);\n } else {\n headers.set(key, value);\n }\n }\n\n const hasBody = req.method !== 'GET' && req.method !== 'HEAD';\n const init: RequestInit = {\n method: req.method,\n headers,\n };\n if (hasBody) {\n init.body = Readable.toWeb(req) as RequestInit['body'];\n (init as RequestInit & { duplex?: 'half' }).duplex = 'half';\n }\n return new Request(url, init);\n}\n\nasync function sendFetchResponse(res: import('node:http').ServerResponse, response: Response): Promise<void> {\n res.writeHead(response.status, Object.fromEntries(response.headers.entries()));\n if (response.body) {\n const reader = response.body.getReader();\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n res.write(value);\n }\n }\n res.end();\n}\n\nexport async function startTunnelTlsServer(config: TunnelTlsServerConfig): Promise<Server> {\n if (httpsNodeServer) return httpsNodeServer;\n\n const cert = await ensureValidCert(config.acmeConfig);\n const handler = resolveTlsFetch(config.gatewayPort, config.fetch);\n\n httpsNodeServer = createHttpsServer(\n {\n key: cert.keyPem,\n cert: cert.certPem,\n minVersion: 'TLSv1.2',\n },\n (req, res) => {\n void (async () => {\n try {\n const request = await nodeRequestToFetch(req, config.tlsPort);\n const response = await handler(request);\n await sendFetchResponse(res, response);\n } catch (err) {\n log.warn({ err, phase: 'tls_proxy' }, 'TLS proxy request failed');\n if (!res.headersSent) res.writeHead(502);\n res.end('Bad Gateway');\n }\n })();\n },\n );\n\n await new Promise<void>((resolve, reject) => {\n httpsNodeServer!.once('error', reject);\n httpsNodeServer!.listen(config.tlsPort, '127.0.0.1', () => resolve());\n }).catch((err: unknown) => {\n httpsNodeServer?.close();\n httpsNodeServer = null;\n const em = err instanceof Error ? err.message : String(err);\n if (em.includes('EADDRINUSE')) {\n throw new Error(\n `Tunnel TLS port ${config.tlsPort} is already in use. ` +\n `Stop other xopc tunnel instances or set tunnel.e2e.tlsPort (try ${config.gatewayPort + 1} for gateway port ${config.gatewayPort}).`,\n );\n }\n throw err;\n });\n\n log.info(\n { port: config.tlsPort, domain: cert.domain, expiresAt: cert.expiresAt },\n \"Tunnel TLS server listening (Let's Encrypt cert)\",\n );\n\n startRenewalScheduler(config.acmeConfig, () => reloadTlsCert());\n\n return httpsNodeServer;\n}\n\nfunction reloadTlsCert(): void {\n const cert = loadStoredCert();\n if (!cert || !httpsNodeServer) return;\n httpsNodeServer.setSecureContext({ key: cert.keyPem, cert: cert.certPem });\n log.info({ domain: cert.domain, expiresAt: cert.expiresAt }, 'TLS cert hot-reloaded');\n}\n\nexport function stopTunnelTlsServer(): void {\n stopRenewalScheduler();\n if (httpsNodeServer) {\n httpsNodeServer.close();\n httpsNodeServer = null;\n log.info('TLS server stopped');\n }\n}\n\nexport function getActiveTlsCert(): StoredCert | null {\n return loadStoredCert();\n}\n\n/** @internal */\nexport function resetTunnelTlsServerForTests(): void {\n stopTunnelTlsServer();\n}\n"],"mappings":"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"}
@@ -1,11 +1,13 @@
1
1
  import { TunnelConfigSchema, TunnelConsentSchema, init_schema } from "../config/schema.js";
2
2
  import { assertTunnelAutoStartAllowed, buildTunnelConsentRecord, hasValidTunnelConsent } from "./consent.js";
3
+ import { isMaskedTunnelSecretPatchValue } from "./env.js";
3
4
  import { z } from "zod";
4
5
  //#region src/tunnel/tunnel-config.ts
5
6
  init_schema();
6
7
  const TunnelConfigPatchSchema = z.object({
7
8
  enabled: z.boolean().optional(),
8
9
  brokerUrl: z.string().url().optional(),
10
+ registrationSecret: z.union([z.string(), z.null()]).optional(),
9
11
  autoStart: z.boolean().optional(),
10
12
  subdomain: z.string().optional(),
11
13
  consent: TunnelConsentSchema.optional()
@@ -16,10 +18,18 @@ function mergeTunnelConfigPatch(config, patch) {
16
18
  ok: false,
17
19
  message: parsed.error.issues.map((i) => i.message).join("; ")
18
20
  };
21
+ const patchFields = { ...parsed.data };
22
+ if (parsed.data.registrationSecret !== void 0) if (parsed.data.registrationSecret === null) delete patchFields.registrationSecret;
23
+ else {
24
+ const trimmed = parsed.data.registrationSecret.trim();
25
+ if (!trimmed || isMaskedTunnelSecretPatchValue(trimmed)) delete patchFields.registrationSecret;
26
+ else patchFields.registrationSecret = trimmed;
27
+ }
19
28
  const next = {
20
29
  ...config.tunnel ?? TunnelConfigSchema.parse({}),
21
- ...parsed.data
30
+ ...patchFields
22
31
  };
32
+ if (parsed.data.registrationSecret === null) delete next.registrationSecret;
23
33
  if (parsed.data.autoStart === true) {
24
34
  const probe = {
25
35
  ...config,
@@ -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 } from '../config/schema.js';\n\nimport {\n assertTunnelAutoStartAllowed,\n buildTunnelConsentRecord,\n hasValidTunnelConsent,\n} from './consent.js';\n\nconst TunnelConfigPatchSchema = z.object({\n enabled: z.boolean().optional(),\n brokerUrl: z.string().url().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 next = {\n ...(config.tunnel ?? TunnelConfigSchema.parse({})),\n ...parsed.data,\n };\n\n if (parsed.data.autoStart === true) {\n const probe: Config = { ...config, tunnel: { ...next, autoStart: true } };\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 (parsed.data.enabled === true && !hasValidTunnelConsent({ ...config, tunnel: next })) {\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 = 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;AAQ9E,MAAM,0BAA0B,EAAE,OAAO;CACvC,SAAS,EAAE,SAAS,CAAC,UAAU;CAC/B,WAAW,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU;CACtC,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,OAAO;EACX,GAAI,OAAO,UAAU,mBAAmB,MAAM,EAAE,CAAC;EACjD,GAAG,OAAO;EACX;AAED,KAAI,OAAO,KAAK,cAAc,MAAM;EAClC,MAAM,QAAgB;GAAE,GAAG;GAAQ,QAAQ;IAAE,GAAG;IAAM,WAAW;IAAM;GAAE;AACzE,MAAI;AACF,gCAA6B,MAAM;WAC5B,KAAK;AAEZ,UAAO;IAAE,IAAI;IAAO,SADT,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;IAC1B;;;AAIrC,KAAI,OAAO,KAAK,YAAY,QAAQ,CAAC,sBAAsB;EAAE,GAAG;EAAQ,QAAQ;EAAM,CAAC,CACrF,QAAO;EACL,IAAI;EACJ,SACE;EACH;AAGH,QAAO,SAAS;AAChB,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
+ {"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 probe: Config = { ...config, tunnel: { ...next, autoStart: true } };\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 (parsed.data.enabled === true && !hasValidTunnelConsent({ ...config, tunnel: next })) {\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 = 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,QAAgB;GAAE,GAAG;GAAQ,QAAQ;IAAE,GAAG;IAAM,WAAW;IAAM;GAAE;AACzE,MAAI;AACF,gCAA6B,MAAM;WAC5B,KAAK;AAEZ,UAAO;IAAE,IAAI;IAAO,SADT,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;IAC1B;;;AAIrC,KAAI,OAAO,KAAK,YAAY,QAAQ,CAAC,sBAAsB;EAAE,GAAG;EAAQ,QAAQ;EAAM,CAAC,CACrF,QAAO;EACL,IAAI;EACJ,SACE;EACH;AAGH,QAAO,SAAS;AAChB,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"}
@@ -4,5 +4,8 @@ export type ResolvedTunnelE2eConfig = {
4
4
  tlsPort: number;
5
5
  staging: boolean;
6
6
  };
7
- export declare function resolveTunnelE2eConfig(tunnel?: TunnelConfig): ResolvedTunnelE2eConfig;
7
+ /** Historical schema default — not suitable when gateway.port ≠ 18790 (e.g. Electron 28790). */
8
+ export declare const LEGACY_DEFAULT_TUNNEL_TLS_PORT = 18791;
9
+ export declare function resolveTunnelTlsPort(e2eTlsPort: number | undefined, gatewayPort: number): number;
10
+ export declare function resolveTunnelE2eConfig(tunnel?: TunnelConfig, gatewayPort?: number): ResolvedTunnelE2eConfig;
8
11
  export declare function resolveFrpSubdomainHost(brokerUrl: string, override?: string): string;
@@ -1,9 +1,16 @@
1
1
  //#region src/tunnel/tunnel-e2e-config.ts
2
- function resolveTunnelE2eConfig(tunnel) {
2
+ /** Historical schema default — not suitable when gateway.port ≠ 18790 (e.g. Electron 28790). */
3
+ const LEGACY_DEFAULT_TUNNEL_TLS_PORT = 18791;
4
+ function resolveTunnelTlsPort(e2eTlsPort, gatewayPort) {
5
+ if (e2eTlsPort === void 0) return gatewayPort + 1;
6
+ if (e2eTlsPort === 18791 && gatewayPort !== 18790) return gatewayPort + 1;
7
+ return e2eTlsPort;
8
+ }
9
+ function resolveTunnelE2eConfig(tunnel, gatewayPort = 18790) {
3
10
  const e2e = tunnel?.e2e;
4
11
  return {
5
12
  enabled: e2e?.enabled ?? true,
6
- tlsPort: e2e?.tlsPort ?? 18791,
13
+ tlsPort: resolveTunnelTlsPort(e2e?.tlsPort, gatewayPort),
7
14
  staging: e2e?.staging ?? false
8
15
  };
9
16
  }
@@ -17,6 +24,6 @@ function resolveFrpSubdomainHost(brokerUrl, override) {
17
24
  return "frp.xopc.ai";
18
25
  }
19
26
  //#endregion
20
- export { resolveFrpSubdomainHost, resolveTunnelE2eConfig };
27
+ export { LEGACY_DEFAULT_TUNNEL_TLS_PORT, resolveFrpSubdomainHost, resolveTunnelE2eConfig, resolveTunnelTlsPort };
21
28
 
22
29
  //# sourceMappingURL=tunnel-e2e-config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tunnel-e2e-config.js","names":[],"sources":["../../../src/tunnel/tunnel-e2e-config.ts"],"sourcesContent":["import type { TunnelConfig } from '../config/schema.js';\n\nexport type ResolvedTunnelE2eConfig = {\n enabled: boolean;\n tlsPort: number;\n staging: boolean;\n};\n\nexport function resolveTunnelE2eConfig(tunnel?: TunnelConfig): ResolvedTunnelE2eConfig {\n const e2e = tunnel?.e2e;\n return {\n enabled: e2e?.enabled ?? true,\n tlsPort: e2e?.tlsPort ?? 18791,\n staging: e2e?.staging ?? false,\n };\n}\n\nexport function resolveFrpSubdomainHost(brokerUrl: string, override?: string): string {\n if (override?.trim()) return override.trim();\n try {\n const host = new URL(brokerUrl.replace(/\\/api\\/?$/, '')).hostname;\n if (host === 'frp.xopc.ai' || host.endsWith('.frp.xopc.ai')) return 'frp.xopc.ai';\n if (host.includes('.')) return host;\n } catch {\n /* fall through */\n }\n return 'frp.xopc.ai';\n}\n"],"mappings":";AAQA,SAAgB,uBAAuB,QAAgD;CACrF,MAAM,MAAM,QAAQ;AACpB,QAAO;EACL,SAAS,KAAK,WAAW;EACzB,SAAS,KAAK,WAAW;EACzB,SAAS,KAAK,WAAW;EAC1B;;AAGH,SAAgB,wBAAwB,WAAmB,UAA2B;AACpF,KAAI,UAAU,MAAM,CAAE,QAAO,SAAS,MAAM;AAC5C,KAAI;EACF,MAAM,OAAO,IAAI,IAAI,UAAU,QAAQ,aAAa,GAAG,CAAC,CAAC;AACzD,MAAI,SAAS,iBAAiB,KAAK,SAAS,eAAe,CAAE,QAAO;AACpE,MAAI,KAAK,SAAS,IAAI,CAAE,QAAO;SACzB;AAGR,QAAO"}
1
+ {"version":3,"file":"tunnel-e2e-config.js","names":[],"sources":["../../../src/tunnel/tunnel-e2e-config.ts"],"sourcesContent":["import type { TunnelConfig } from '../config/schema.js';\n\nexport type ResolvedTunnelE2eConfig = {\n enabled: boolean;\n tlsPort: number;\n staging: boolean;\n};\n\n/** Historical schema default — not suitable when gateway.port ≠ 18790 (e.g. Electron 28790). */\nexport const LEGACY_DEFAULT_TUNNEL_TLS_PORT = 18791;\n\nexport function resolveTunnelTlsPort(e2eTlsPort: number | undefined, gatewayPort: number): number {\n if (e2eTlsPort === undefined) {\n return gatewayPort + 1;\n }\n if (e2eTlsPort === LEGACY_DEFAULT_TUNNEL_TLS_PORT && gatewayPort !== 18790) {\n return gatewayPort + 1;\n }\n return e2eTlsPort;\n}\n\nexport function resolveTunnelE2eConfig(\n tunnel?: TunnelConfig,\n gatewayPort = 18790,\n): ResolvedTunnelE2eConfig {\n const e2e = tunnel?.e2e;\n return {\n enabled: e2e?.enabled ?? true,\n tlsPort: resolveTunnelTlsPort(e2e?.tlsPort, gatewayPort),\n staging: e2e?.staging ?? false,\n };\n}\n\nexport function resolveFrpSubdomainHost(brokerUrl: string, override?: string): string {\n if (override?.trim()) return override.trim();\n try {\n const host = new URL(brokerUrl.replace(/\\/api\\/?$/, '')).hostname;\n if (host === 'frp.xopc.ai' || host.endsWith('.frp.xopc.ai')) return 'frp.xopc.ai';\n if (host.includes('.')) return host;\n } catch {\n /* fall through */\n }\n return 'frp.xopc.ai';\n}\n"],"mappings":";;AASA,MAAa,iCAAiC;AAE9C,SAAgB,qBAAqB,YAAgC,aAA6B;AAChG,KAAI,eAAe,KAAA,EACjB,QAAO,cAAc;AAEvB,KAAI,eAAA,SAAiD,gBAAgB,MACnE,QAAO,cAAc;AAEvB,QAAO;;AAGT,SAAgB,uBACd,QACA,cAAc,OACW;CACzB,MAAM,MAAM,QAAQ;AACpB,QAAO;EACL,SAAS,KAAK,WAAW;EACzB,SAAS,qBAAqB,KAAK,SAAS,YAAY;EACxD,SAAS,KAAK,WAAW;EAC1B;;AAGH,SAAgB,wBAAwB,WAAmB,UAA2B;AACpF,KAAI,UAAU,MAAM,CAAE,QAAO,SAAS,MAAM;AAC5C,KAAI;EACF,MAAM,OAAO,IAAI,IAAI,UAAU,QAAQ,aAAa,GAAG,CAAC,CAAC;AACzD,MAAI,SAAS,iBAAiB,KAAK,SAAS,eAAe,CAAE,QAAO;AACpE,MAAI,KAAK,SAAS,IAAI,CAAE,QAAO;SACzB;AAGR,QAAO"}
@@ -24,9 +24,13 @@ export declare class TunnelService extends EventEmitter {
24
24
  private reconnectAttempt;
25
25
  private stopping;
26
26
  private startContext;
27
+ private frpcDownload;
28
+ private startProgress;
27
29
  configure(cfg: TunnelServiceConfig): void;
28
30
  setGatewayFetch(fetchFn: typeof fetch): void;
29
31
  getStatus(): TunnelStatus;
32
+ private setStartPhase;
33
+ private clearStartProgress;
30
34
  buildQr(gatewayPort: number, gatewayHost: string): TunnelQrPayload;
31
35
  start(gatewayPort: number, gatewayToken: string): Promise<TunnelQrPayload>;
32
36
  stop(opts?: {
@@ -41,6 +41,8 @@ var TunnelService = class extends EventEmitter {
41
41
  reconnectAttempt = 0;
42
42
  stopping = false;
43
43
  startContext = null;
44
+ frpcDownload = null;
45
+ startProgress = null;
44
46
  configure(cfg) {
45
47
  this.serviceConfig = {
46
48
  ...cfg,
@@ -63,6 +65,8 @@ var TunnelService = class extends EventEmitter {
63
65
  frpcPid: this.frpcHandle?.pid ?? null,
64
66
  lastHeartbeatAt: this.lastHeartbeatAt,
65
67
  lastError: this.lastError,
68
+ frpcDownload: this.frpcDownload,
69
+ startProgress: this.startProgress,
66
70
  config: {
67
71
  autoStart: cfg?.autoStart ?? false,
68
72
  brokerUrl: cfg?.brokerUrl ?? "https://frp.xopc.ai/api",
@@ -75,6 +79,22 @@ var TunnelService = class extends EventEmitter {
75
79
  cert: getCertStatusSummary()
76
80
  };
77
81
  }
82
+ setStartPhase(phase, patch) {
83
+ const prev = this.startProgress;
84
+ const samePhase = prev?.phase === phase;
85
+ this.startProgress = {
86
+ phase,
87
+ startedAt: samePhase && prev ? prev.startedAt : (/* @__PURE__ */ new Date()).toISOString(),
88
+ acmeStep: patch?.acmeStep !== void 0 ? patch.acmeStep : samePhase ? prev?.acmeStep ?? null : null,
89
+ publicUrl: patch?.publicUrl !== void 0 ? patch.publicUrl : samePhase ? prev?.publicUrl ?? null : prev?.publicUrl ?? null
90
+ };
91
+ this.emit("tunnel:progress");
92
+ }
93
+ clearStartProgress() {
94
+ if (!this.startProgress) return;
95
+ this.startProgress = null;
96
+ this.emit("tunnel:progress");
97
+ }
78
98
  buildQr(gatewayPort, gatewayHost) {
79
99
  const publicUrl = loadTunnelState()?.publicUrl ?? null;
80
100
  if (!publicUrl) return {
@@ -100,15 +120,35 @@ var TunnelService = class extends EventEmitter {
100
120
  };
101
121
  this.state = "connecting";
102
122
  this.lastError = null;
123
+ this.frpcDownload = null;
124
+ this.startProgress = null;
125
+ this.setStartPhase("preparing_frpc");
103
126
  this.emit("tunnel:connecting");
104
- const frpcBin = await ensureFrpcBinary();
127
+ let frpcBin;
128
+ try {
129
+ frpcBin = await ensureFrpcBinary({ onProgress: (progress) => {
130
+ this.frpcDownload = progress;
131
+ this.setStartPhase("preparing_frpc");
132
+ } });
133
+ this.frpcDownload = null;
134
+ this.emit("tunnel:progress");
135
+ } catch (err) {
136
+ this.frpcDownload = null;
137
+ this.clearStartProgress();
138
+ this.state = "error";
139
+ this.lastError = err instanceof Error ? err.message : String(err);
140
+ this.emit("tunnel:error", this.lastError);
141
+ throw err;
142
+ }
105
143
  publishFrpcPathForProcess(frpcBin);
106
144
  const persisted = loadTunnelState();
107
145
  const broker = new TunnelBrokerClient(resolveBrokerApiBase(cfg.brokerUrl));
108
146
  let registration;
109
147
  try {
148
+ this.setStartPhase("registering");
110
149
  registration = await this.resolveRegistration(broker, cfg, gatewayToken, persisted);
111
150
  } catch (err) {
151
+ this.clearStartProgress();
112
152
  this.state = "error";
113
153
  this.lastError = err instanceof Error ? err.message : String(err);
114
154
  this.emit("tunnel:error", this.lastError);
@@ -116,9 +156,20 @@ var TunnelService = class extends EventEmitter {
116
156
  }
117
157
  const state = persistedFromRegistration(registration);
118
158
  saveTunnelState(state);
119
- const { frpcLocalPort, frpcMode } = await this.prepareFrpcTarget(broker, registration, cfg, gatewayPort);
120
- const configPath = writeFrpcConfig(registration, frpcLocalPort, "127.0.0.1", frpcMode);
121
- await this.spawnAndWait(frpcBin, configPath, broker, state, registration.heartbeatIntervalMs);
159
+ this.setStartPhase("registering", { publicUrl: registration.publicUrl });
160
+ try {
161
+ const { frpcLocalPort, frpcMode } = await this.prepareFrpcTarget(broker, registration, cfg, gatewayPort);
162
+ const configPath = writeFrpcConfig(registration, frpcLocalPort, "127.0.0.1", frpcMode);
163
+ this.setStartPhase("starting_frpc", { publicUrl: registration.publicUrl });
164
+ await this.spawnAndWait(frpcBin, configPath, broker, state, registration.heartbeatIntervalMs);
165
+ } catch (err) {
166
+ this.clearStartProgress();
167
+ this.state = "error";
168
+ this.lastError = err instanceof Error ? err.message : String(err);
169
+ this.emit("tunnel:error", this.lastError);
170
+ throw err;
171
+ }
172
+ this.clearStartProgress();
122
173
  this.state = "connected";
123
174
  this.connectedSince = (/* @__PURE__ */ new Date()).toISOString();
124
175
  this.reconnectAttempt = 0;
@@ -173,6 +224,7 @@ var TunnelService = class extends EventEmitter {
173
224
  this.state = "disconnected";
174
225
  this.connectedSince = null;
175
226
  this.startContext = null;
227
+ this.clearStartProgress();
176
228
  this.emit("tunnel:disconnected");
177
229
  return { released };
178
230
  }
@@ -240,6 +292,7 @@ var TunnelService = class extends EventEmitter {
240
292
  return;
241
293
  }
242
294
  this.state = "reconnecting";
295
+ this.setStartPhase("reconnecting_frpc", { publicUrl: state.publicUrl ?? null });
243
296
  const delayMs = Math.min(16e3, 1e3 * 2 ** (this.reconnectAttempt - 1));
244
297
  log.info({
245
298
  attempt: this.reconnectAttempt,
@@ -249,6 +302,7 @@ var TunnelService = class extends EventEmitter {
249
302
  if (this.stopping) return;
250
303
  try {
251
304
  await this.spawnAndWait(frpcBin, configPath, broker, state, heartbeatIntervalMs);
305
+ this.clearStartProgress();
252
306
  this.state = "connected";
253
307
  this.reconnectAttempt = 0;
254
308
  this.emit("tunnel:connected");
@@ -309,13 +363,20 @@ var TunnelService = class extends EventEmitter {
309
363
  frpcLocalPort: gatewayPort,
310
364
  frpcMode: "http"
311
365
  };
366
+ this.setStartPhase("provisioning_tls", { publicUrl: registration.publicUrl });
312
367
  const acmeConfig = {
313
368
  broker,
314
369
  tunnelId: registration.tunnelId,
315
370
  tunnelToken: registration.tunnelToken,
316
371
  subdomain: registration.subdomain,
317
372
  frpSubdomainHost: cfg.frpSubdomainHost,
318
- staging: cfg.e2e.staging
373
+ staging: cfg.e2e.staging,
374
+ onProgress: (step) => {
375
+ this.setStartPhase("provisioning_tls", {
376
+ publicUrl: registration.publicUrl,
377
+ acmeStep: step
378
+ });
379
+ }
319
380
  };
320
381
  await startTunnelTlsServer({
321
382
  tlsPort: cfg.e2e.tlsPort,