@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
@@ -1,4 +1,5 @@
1
1
  import type { TunnelBrokerClient } from './broker-client.js';
2
+ import type { TunnelAcmeProgressStep } from './tunnel-types.js';
2
3
  export type AcmeConfig = {
3
4
  broker: TunnelBrokerClient;
4
5
  tunnelId: string;
@@ -6,6 +7,7 @@ export type AcmeConfig = {
6
7
  subdomain: string;
7
8
  frpSubdomainHost: string;
8
9
  staging?: boolean;
10
+ onProgress?: (step: TunnelAcmeProgressStep) => void;
9
11
  };
10
12
  export type AcmeCertResult = {
11
13
  certPem: string;
@@ -14,4 +16,13 @@ export type AcmeCertResult = {
14
16
  expiresAt: Date;
15
17
  issuedAt: Date;
16
18
  };
19
+ /** LE always validates `_acme-challenge.{domain}` (RFC 8555 §8.4). */
20
+ export declare function resolveAcmeChallengeFqdn(domain: string): string;
21
+ export declare function formatAcmeDnsChallengeInvalidError(fqdn: string, data: {
22
+ error?: {
23
+ detail?: string;
24
+ type?: string;
25
+ };
26
+ validationRecord?: string[];
27
+ }): string;
17
28
  export declare function requestCertificate(config: AcmeConfig): Promise<AcmeCertResult>;
@@ -40,17 +40,34 @@ async function resolveAcmeDnsTxt(fqdn) {
40
40
  function sleep(ms) {
41
41
  return new Promise((r) => setTimeout(r, ms));
42
42
  }
43
- async function waitForDnsTxt(fqdn, expectedValue, timeoutMs = 18e4) {
43
+ function normalizeFqdn(fqdn) {
44
+ return fqdn.trim().replace(/\.$/, "").toLowerCase();
45
+ }
46
+ /** LE always validates `_acme-challenge.{domain}` (RFC 8555 §8.4). */
47
+ function resolveAcmeChallengeFqdn(domain) {
48
+ return `_acme-challenge.${domain}`;
49
+ }
50
+ function formatAcmeDnsChallengeInvalidError(fqdn, data) {
51
+ const parts = [`ACME DNS-01 challenge invalid for ${fqdn}`];
52
+ if (data.error?.detail) parts.push(data.error.detail);
53
+ else if (data.error?.type) parts.push(data.error.type);
54
+ if (data.validationRecord?.length) parts.push(`validation: ${data.validationRecord.join("; ")}`);
55
+ return parts.join(" — ");
56
+ }
57
+ async function waitForDnsTxt(fqdn, expectedValue, opts) {
58
+ const initialDelayMs = opts?.initialDelayMs ?? 45e3;
59
+ const timeoutMs = opts?.timeoutMs ?? 18e4;
44
60
  const deadline = Date.now() + timeoutMs;
45
61
  let lastSeen = [];
46
- await sleep(45e3);
62
+ if (initialDelayMs > 0) await sleep(initialDelayMs);
47
63
  while (Date.now() < deadline) {
48
64
  try {
49
65
  lastSeen = await resolveAcmeDnsTxt(fqdn);
50
66
  if (lastSeen.some((value) => value === expectedValue)) {
51
67
  log.info({
52
68
  fqdn,
53
- resolvers: ACME_DNS_RESOLVERS
69
+ resolvers: ACME_DNS_RESOLVERS,
70
+ txtCount: lastSeen.length
54
71
  }, "DNS-01 TXT record visible");
55
72
  return;
56
73
  }
@@ -210,14 +227,22 @@ async function ensureAccount(directory, keyPem, staging) {
210
227
  keyPem
211
228
  };
212
229
  }
213
- async function pollChallengeReady(challengeUrl, account, directory) {
230
+ async function pollChallengeReady(challengeUrl, account, directory, challengeFqdn) {
214
231
  for (let i = 0; i < 30; i++) {
215
232
  await sleep(i === 0 ? 5e3 : 2e3);
216
233
  const { data } = await acmeSignedPost(challengeUrl, account, await getNonce(directory), null);
217
234
  if (data.status === "valid") return;
218
- if (data.status === "invalid") throw new Error("ACME DNS-01 challenge invalid");
235
+ if (data.status === "invalid") {
236
+ const message = formatAcmeDnsChallengeInvalidError(challengeFqdn, data);
237
+ log.error({
238
+ challengeFqdn,
239
+ error: data.error,
240
+ validationRecord: data.validationRecord
241
+ }, message);
242
+ throw new Error(message);
243
+ }
219
244
  }
220
- throw new Error("ACME DNS-01 challenge timed out");
245
+ throw new Error(`ACME DNS-01 challenge timed out for ${challengeFqdn}`);
221
246
  }
222
247
  async function pollOrderValid(orderUrl, account, directory) {
223
248
  for (let i = 0; i < 30; i++) {
@@ -235,6 +260,7 @@ async function requestCertificate(config) {
235
260
  domain,
236
261
  staging
237
262
  }, "Starting ACME certificate request");
263
+ config.onProgress?.("checking");
238
264
  const directory = await getDirectory(staging);
239
265
  const account = await ensureAccount(directory, loadAccountKeyPem(), staging);
240
266
  let nonce = await getNonce(directory);
@@ -252,21 +278,39 @@ async function requestCertificate(config) {
252
278
  const thumbprint = jwkThumbprint(account.jwk);
253
279
  const keyAuth = `${challenge.token}.${thumbprint}`;
254
280
  const txtValue = base64url(createHash("sha256").update(keyAuth).digest());
255
- log.info({ fqdn: `_acme-challenge.${domain}` }, "Setting DNS-01 challenge via Broker");
281
+ const challengeFqdn = resolveAcmeChallengeFqdn(domain);
282
+ log.info({
283
+ fqdn: challengeFqdn,
284
+ txtPreview: `${txtValue.slice(0, 8)}…`
285
+ }, "Setting DNS-01 challenge via Broker");
286
+ config.onProgress?.("dns_challenge");
256
287
  const { recordId, fqdn } = await config.broker.setDnsChallenge({
257
288
  tunnelId: config.tunnelId,
258
289
  tunnelToken: config.tunnelToken,
259
290
  subdomain: config.subdomain,
260
291
  txtValue
261
292
  });
293
+ if (normalizeFqdn(fqdn) !== normalizeFqdn(challengeFqdn)) log.warn({
294
+ brokerFqdn: fqdn,
295
+ challengeFqdn,
296
+ phase: "acme_dns_fqdn_mismatch"
297
+ }, "Broker returned unexpected ACME challenge FQDN — polling canonical name for Let's Encrypt");
262
298
  try {
263
- await waitForDnsTxt(fqdn, txtValue);
299
+ config.onProgress?.("dns_propagation");
300
+ await waitForDnsTxt(challengeFqdn, txtValue);
301
+ await sleep(15e3);
302
+ await waitForDnsTxt(challengeFqdn, txtValue, {
303
+ initialDelayMs: 0,
304
+ timeoutMs: 6e4
305
+ });
264
306
  nonce = await getNonce(directory);
265
307
  await acmeSignedPost(challenge.url, account, nonce, {});
266
- await pollChallengeReady(challenge.url, account, directory);
308
+ config.onProgress?.("ca_validation");
309
+ await pollChallengeReady(challenge.url, account, directory, challengeFqdn);
267
310
  const { csrDer, keyPem } = generateDomainCsr(domain);
268
311
  const finalizeUrl = orderResult.data.finalize;
269
312
  if (!finalizeUrl) throw new Error("ACME order missing finalize URL");
313
+ config.onProgress?.("issuing");
270
314
  nonce = await getNonce(directory);
271
315
  await acmeSignedPost(finalizeUrl, account, nonce, { csr: base64url(csrDer) });
272
316
  const certPem = await downloadCertificate(await pollOrderValid(orderUrl, account, directory), account, directory);
@@ -298,6 +342,6 @@ async function requestCertificate(config) {
298
342
  }
299
343
  }
300
344
  //#endregion
301
- export { requestCertificate };
345
+ export { formatAcmeDnsChallengeInvalidError, requestCertificate, resolveAcmeChallengeFqdn };
302
346
 
303
347
  //# sourceMappingURL=acme-client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"acme-client.js","names":["undiciFetch"],"sources":["../../../src/tunnel/acme-client.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { Resolver } from 'node:dns/promises';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { Agent, fetch as undiciFetch, type RequestInit } from 'undici';\n\nimport { resolveStateDir } from '../config/paths.js';\nimport { createLogger } from '../utils/logger.js';\nimport type { TunnelBrokerClient } from './broker-client.js';\nimport {\n base64url,\n ensureEcAccountKeyPem,\n exportJwkFromPrivateKeyPem,\n getCertExpiryFromPem,\n jwkThumbprint,\n signAcmeJws,\n} from './acme-crypto.js';\nimport { generateDomainCsr } from './acme-csr.js';\n\nconst log = createLogger('TunnelACME');\n\nconst ACME_DIRECTORY = {\n production: 'https://acme-v02.api.letsencrypt.org/directory',\n staging: 'https://acme-staging-v02.api.letsencrypt.org/directory',\n} as const;\n\nconst ACME_FETCH_TIMEOUT_MS = 30_000;\nconst ACME_FETCH_RETRIES = 4;\n\nconst acmeDispatcher = new Agent({\n connect: { timeout: ACME_FETCH_TIMEOUT_MS },\n});\n\n/** Public resolvers — LE validators use global DNS, not the host's stale cache. */\nconst ACME_DNS_RESOLVERS = ['8.8.8.8', '1.1.1.1', '9.9.9.9'];\n\nlet acmeDnsResolver: Resolver | null = null;\n\nfunction getAcmeDnsResolver(): Resolver {\n if (!acmeDnsResolver) {\n acmeDnsResolver = new Resolver();\n acmeDnsResolver.setServers(ACME_DNS_RESOLVERS);\n }\n return acmeDnsResolver;\n}\n\nasync function resolveAcmeDnsTxt(fqdn: string): Promise<string[]> {\n const records = await getAcmeDnsResolver().resolveTxt(fqdn);\n return records.map((parts) => parts.join(''));\n}\n\nexport type AcmeConfig = {\n broker: TunnelBrokerClient;\n tunnelId: string;\n tunnelToken: string;\n subdomain: string;\n frpSubdomainHost: string;\n staging?: boolean;\n};\n\nexport type AcmeCertResult = {\n certPem: string;\n keyPem: string;\n domain: string;\n expiresAt: Date;\n issuedAt: Date;\n};\n\ntype AcmeDirectory = {\n newNonce: string;\n newAccount: string;\n newOrder: string;\n};\n\ntype AcmeAccount = {\n url: string;\n jwk: ReturnType<typeof exportJwkFromPrivateKeyPem>;\n keyPem: string;\n};\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\nasync function waitForDnsTxt(fqdn: string, expectedValue: string, timeoutMs = 180_000): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n let lastSeen: string[] = [];\n // Dynadot authoritative DNS often needs ~60s after set_dns2 before TXT is queryable.\n await sleep(45_000);\n while (Date.now() < deadline) {\n try {\n lastSeen = await resolveAcmeDnsTxt(fqdn);\n if (lastSeen.some((value) => value === expectedValue)) {\n log.info({ fqdn, resolvers: ACME_DNS_RESOLVERS }, 'DNS-01 TXT record visible');\n return;\n }\n } catch {\n /* NXDOMAIN / timeout — keep polling until deadline */\n }\n await sleep(5_000);\n }\n throw new Error(\n `DNS TXT not visible for ${fqdn} (expected ${expectedValue}; last seen: ${lastSeen.join(', ') || 'none'})`,\n );\n}\n\nfunction getAcmeDir(): string {\n const dir = join(resolveStateDir(), 'tunnel', 'acme');\n mkdirSync(dir, { recursive: true });\n return dir;\n}\n\nfunction loadAccountKeyPem(): string {\n const keyPath = join(getAcmeDir(), 'account-key.pem');\n if (existsSync(keyPath)) {\n return readFileSync(keyPath, 'utf8');\n }\n const pem = ensureEcAccountKeyPem();\n writeFileSync(keyPath, pem, { mode: 0o600 });\n return pem;\n}\n\nfunction accountUrlPath(staging: boolean): string {\n return join(getAcmeDir(), staging ? 'account-url-staging.txt' : 'account-url-production.txt');\n}\n\nfunction loadAccountUrl(staging: boolean): string {\n const path = accountUrlPath(staging);\n if (existsSync(path)) {\n return readFileSync(path, 'utf8').trim();\n }\n // Legacy single file — treat as production only.\n const legacyPath = join(getAcmeDir(), 'account-url.txt');\n if (!staging && existsSync(legacyPath)) {\n const legacyUrl = readFileSync(legacyPath, 'utf8').trim();\n if (legacyUrl.startsWith('http')) {\n writeFileSync(path, legacyUrl, 'utf8');\n return legacyUrl;\n }\n }\n return '';\n}\n\nfunction saveAccountUrl(staging: boolean, url: string): void {\n writeFileSync(accountUrlPath(staging), url, 'utf8');\n}\n\nfunction accountUrlMatchesCa(accountUrl: string, staging: boolean): boolean {\n const host = staging ? 'acme-staging-v02.api.letsencrypt.org' : 'acme-v02.api.letsencrypt.org';\n try {\n return new URL(accountUrl).host === host;\n } catch {\n return false;\n }\n}\n\nfunction resolveCertDomain(subdomain: string, frpSubdomainHost: string): string {\n return `${subdomain}.${frpSubdomainHost}`;\n}\n\nasync function acmeFetch(url: string, init?: RequestInit): Promise<Response> {\n let lastErr: unknown;\n for (let attempt = 1; attempt <= ACME_FETCH_RETRIES; attempt++) {\n try {\n return await undiciFetch(url, {\n ...init,\n dispatcher: acmeDispatcher,\n signal: AbortSignal.timeout(ACME_FETCH_TIMEOUT_MS + 5_000),\n });\n } catch (err) {\n lastErr = err;\n if (attempt < ACME_FETCH_RETRIES) {\n log.warn({ url, attempt, err }, 'ACME fetch failed, retrying');\n await sleep(2_000 * attempt);\n }\n }\n }\n throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));\n}\n\nasync function acmeFetchJson<T>(\n url: string,\n init?: RequestInit,\n): Promise<{ data: T; nonce?: string; location?: string | null }> {\n const res = await acmeFetch(url, init);\n const replayNonce = res.headers.get('replay-nonce') ?? undefined;\n const location = res.headers.get('location');\n\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n throw new Error(`ACME HTTP ${res.status} ${url}: ${body.slice(0, 300)}`);\n }\n\n if (res.status === 204) {\n return { data: {} as T, nonce: replayNonce, location };\n }\n\n const data = (await res.json()) as T;\n return { data, nonce: replayNonce, location };\n}\n\nasync function acmeSignedPost<T>(\n url: string,\n account: AcmeAccount,\n nonce: string,\n payload: unknown,\n): Promise<{ data: T; nonce?: string; location?: string | null }> {\n const useKid = account.url.startsWith('http');\n const jws = signAcmeJws({\n privateKeyPem: account.keyPem,\n url,\n nonce,\n payload,\n kid: useKid ? account.url : undefined,\n jwk: useKid ? undefined : account.jwk,\n });\n\n return acmeFetchJson<T>(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/jose+json' },\n body: JSON.stringify(jws),\n });\n}\n\nasync function downloadCertificate(certUrl: string, account: AcmeAccount, directory: AcmeDirectory): Promise<string> {\n const nonce = await getNonce(directory);\n const jws = signAcmeJws({\n privateKeyPem: account.keyPem,\n url: certUrl,\n nonce,\n payload: null,\n kid: account.url,\n });\n const res = await acmeFetch(certUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/jose+json', Accept: 'application/pem-certificate-chain' },\n body: JSON.stringify(jws),\n });\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n throw new Error(`ACME cert download failed: ${res.status} ${body.slice(0, 200)}`);\n }\n return res.text();\n}\n\nasync function getDirectory(staging: boolean): Promise<AcmeDirectory> {\n const url = staging ? ACME_DIRECTORY.staging : ACME_DIRECTORY.production;\n const { data } = await acmeFetchJson<AcmeDirectory>(url);\n return data;\n}\n\nasync function getNonce(directory: AcmeDirectory): Promise<string> {\n const res = await acmeFetch(directory.newNonce, { method: 'HEAD' });\n const nonce = res.headers.get('replay-nonce');\n if (!nonce) throw new Error('ACME CA did not return replay-nonce');\n return nonce;\n}\n\nasync function ensureAccount(\n directory: AcmeDirectory,\n keyPem: string,\n staging: boolean,\n): Promise<AcmeAccount> {\n const jwk = exportJwkFromPrivateKeyPem(keyPem);\n let accountUrl = loadAccountUrl(staging);\n if (accountUrl && !accountUrlMatchesCa(accountUrl, staging)) {\n accountUrl = '';\n }\n\n // new-acct MUST use embedded jwk (RFC 8555 §7.3.1). LE returns an existing account when the JWK matches.\n const nonce = await getNonce(directory);\n const result = await acmeSignedPost<{ status?: string }>(\n directory.newAccount,\n { url: 'new', jwk, keyPem },\n nonce,\n { termsOfServiceAgreed: true },\n );\n\n if (result.location) {\n accountUrl = result.location;\n saveAccountUrl(staging, accountUrl);\n }\n if (!accountUrl) throw new Error('ACME account registration failed (no account URL)');\n\n return { url: accountUrl, jwk, keyPem };\n}\n\nasync function pollChallengeReady(\n challengeUrl: string,\n account: AcmeAccount,\n directory: AcmeDirectory,\n): Promise<void> {\n for (let i = 0; i < 30; i++) {\n await sleep(i === 0 ? 5_000 : 2_000);\n const nonce = await getNonce(directory);\n // POST-as-GET (payload null) — must not POST `{}` again; that re-submits the challenge response.\n const { data } = await acmeSignedPost<{ status?: string }>(challengeUrl, account, nonce, null);\n if (data.status === 'valid') return;\n if (data.status === 'invalid') throw new Error('ACME DNS-01 challenge invalid');\n }\n throw new Error('ACME DNS-01 challenge timed out');\n}\n\nasync function pollOrderValid(\n orderUrl: string,\n account: AcmeAccount,\n directory: AcmeDirectory,\n): Promise<string> {\n for (let i = 0; i < 30; i++) {\n const nonce = await getNonce(directory);\n const { data } = await acmeSignedPost<{ status?: string; certificate?: string }>(\n orderUrl,\n account,\n nonce,\n null,\n );\n if (data.status === 'valid' && data.certificate) return data.certificate;\n if (data.status === 'invalid') throw new Error('ACME order invalid');\n await sleep(2_000);\n }\n throw new Error('ACME order finalize timed out');\n}\n\nexport async function requestCertificate(config: AcmeConfig): Promise<AcmeCertResult> {\n const staging = config.staging ?? false;\n const domain = resolveCertDomain(config.subdomain, config.frpSubdomainHost);\n\n log.info({ domain, staging }, 'Starting ACME certificate request');\n\n const directory = await getDirectory(staging);\n const accountKeyPem = loadAccountKeyPem();\n const account = await ensureAccount(directory, accountKeyPem, staging);\n\n let nonce = await getNonce(directory);\n const orderResult = await acmeSignedPost<{ authorizations?: string[]; finalize?: string }>(\n directory.newOrder,\n account,\n nonce,\n { identifiers: [{ type: 'dns', value: domain }] },\n );\n const orderUrl = orderResult.location;\n if (!orderUrl) throw new Error('ACME newOrder missing order URL');\n\n const authzUrl = orderResult.data.authorizations?.[0];\n if (!authzUrl) throw new Error('ACME order missing authorization');\n\n nonce = await getNonce(directory);\n const authz = await acmeSignedPost<{\n challenges?: Array<{ type: string; url: string; token: string }>;\n }>(authzUrl, account, nonce, null);\n const challenge = authz.data.challenges?.find((c) => c.type === 'dns-01');\n if (!challenge) throw new Error('No DNS-01 challenge offered by CA');\n\n const thumbprint = jwkThumbprint(account.jwk);\n const keyAuth = `${challenge.token}.${thumbprint}`;\n const txtValue = base64url(createHash('sha256').update(keyAuth).digest());\n\n log.info({ fqdn: `_acme-challenge.${domain}` }, 'Setting DNS-01 challenge via Broker');\n const { recordId, fqdn } = await config.broker.setDnsChallenge({\n tunnelId: config.tunnelId,\n tunnelToken: config.tunnelToken,\n subdomain: config.subdomain,\n txtValue,\n });\n\n try {\n await waitForDnsTxt(fqdn, txtValue);\n\n nonce = await getNonce(directory);\n await acmeSignedPost(challenge.url, account, nonce, {});\n\n await pollChallengeReady(challenge.url, account, directory);\n\n const { csrDer, keyPem } = generateDomainCsr(domain);\n const finalizeUrl = orderResult.data.finalize;\n if (!finalizeUrl) throw new Error('ACME order missing finalize URL');\n\n nonce = await getNonce(directory);\n await acmeSignedPost(finalizeUrl, account, nonce, { csr: base64url(csrDer) });\n\n const certUrl = await pollOrderValid(orderUrl, account, directory);\n const certPem = await downloadCertificate(certUrl, account, directory);\n\n const firstCert = certPem.match(/-----BEGIN CERTIFICATE-----[\\s\\S]+?-----END CERTIFICATE-----/)?.[0];\n if (!firstCert) throw new Error('ACME certificate PEM parse failed');\n\n const expiresAt = getCertExpiryFromPem(firstCert);\n log.info({ domain, expiresAt: expiresAt.toISOString() }, 'Certificate issued');\n\n return { certPem: certPem.trim(), keyPem, domain, expiresAt, issuedAt: new Date() };\n } finally {\n await config.broker\n .cleanupDnsChallenge({\n tunnelId: config.tunnelId,\n tunnelToken: config.tunnelToken,\n recordId,\n })\n .catch((err) => {\n log.warn({ err, recordId }, 'DNS cleanup failed (non-critical)');\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;YAMqD;aACH;AAYlD,MAAM,MAAM,aAAa,aAAa;AAEtC,MAAM,iBAAiB;CACrB,YAAY;CACZ,SAAS;CACV;AAED,MAAM,wBAAwB;AAC9B,MAAM,qBAAqB;AAE3B,MAAM,iBAAiB,IAAI,MAAM,EAC/B,SAAS,EAAE,SAAS,uBAAuB,EAC5C,CAAC;;AAGF,MAAM,qBAAqB;CAAC;CAAW;CAAW;CAAU;AAE5D,IAAI,kBAAmC;AAEvC,SAAS,qBAA+B;AACtC,KAAI,CAAC,iBAAiB;AACpB,oBAAkB,IAAI,UAAU;AAChC,kBAAgB,WAAW,mBAAmB;;AAEhD,QAAO;;AAGT,eAAe,kBAAkB,MAAiC;AAEhE,SAAO,MADe,oBAAoB,CAAC,WAAW,KAAK,EAC5C,KAAK,UAAU,MAAM,KAAK,GAAG,CAAC;;AAgC/C,SAAS,MAAM,IAA2B;AACxC,QAAO,IAAI,SAAS,MAAM,WAAW,GAAG,GAAG,CAAC;;AAG9C,eAAe,cAAc,MAAc,eAAuB,YAAY,MAAwB;CACpG,MAAM,WAAW,KAAK,KAAK,GAAG;CAC9B,IAAI,WAAqB,EAAE;AAE3B,OAAM,MAAM,KAAO;AACnB,QAAO,KAAK,KAAK,GAAG,UAAU;AAC5B,MAAI;AACF,cAAW,MAAM,kBAAkB,KAAK;AACxC,OAAI,SAAS,MAAM,UAAU,UAAU,cAAc,EAAE;AACrD,QAAI,KAAK;KAAE;KAAM,WAAW;KAAoB,EAAE,4BAA4B;AAC9E;;UAEI;AAGR,QAAM,MAAM,IAAM;;AAEpB,OAAM,IAAI,MACR,2BAA2B,KAAK,aAAa,cAAc,eAAe,SAAS,KAAK,KAAK,IAAI,OAAO,GACzG;;AAGH,SAAS,aAAqB;CAC5B,MAAM,MAAM,KAAK,iBAAiB,EAAE,UAAU,OAAO;AACrD,WAAU,KAAK,EAAE,WAAW,MAAM,CAAC;AACnC,QAAO;;AAGT,SAAS,oBAA4B;CACnC,MAAM,UAAU,KAAK,YAAY,EAAE,kBAAkB;AACrD,KAAI,WAAW,QAAQ,CACrB,QAAO,aAAa,SAAS,OAAO;CAEtC,MAAM,MAAM,uBAAuB;AACnC,eAAc,SAAS,KAAK,EAAE,MAAM,KAAO,CAAC;AAC5C,QAAO;;AAGT,SAAS,eAAe,SAA0B;AAChD,QAAO,KAAK,YAAY,EAAE,UAAU,4BAA4B,6BAA6B;;AAG/F,SAAS,eAAe,SAA0B;CAChD,MAAM,OAAO,eAAe,QAAQ;AACpC,KAAI,WAAW,KAAK,CAClB,QAAO,aAAa,MAAM,OAAO,CAAC,MAAM;CAG1C,MAAM,aAAa,KAAK,YAAY,EAAE,kBAAkB;AACxD,KAAI,CAAC,WAAW,WAAW,WAAW,EAAE;EACtC,MAAM,YAAY,aAAa,YAAY,OAAO,CAAC,MAAM;AACzD,MAAI,UAAU,WAAW,OAAO,EAAE;AAChC,iBAAc,MAAM,WAAW,OAAO;AACtC,UAAO;;;AAGX,QAAO;;AAGT,SAAS,eAAe,SAAkB,KAAmB;AAC3D,eAAc,eAAe,QAAQ,EAAE,KAAK,OAAO;;AAGrD,SAAS,oBAAoB,YAAoB,SAA2B;CAC1E,MAAM,OAAO,UAAU,yCAAyC;AAChE,KAAI;AACF,SAAO,IAAI,IAAI,WAAW,CAAC,SAAS;SAC9B;AACN,SAAO;;;AAIX,SAAS,kBAAkB,WAAmB,kBAAkC;AAC9E,QAAO,GAAG,UAAU,GAAG;;AAGzB,eAAe,UAAU,KAAa,MAAuC;CAC3E,IAAI;AACJ,MAAK,IAAI,UAAU,GAAG,WAAW,oBAAoB,UACnD,KAAI;AACF,SAAO,MAAMA,MAAY,KAAK;GAC5B,GAAG;GACH,YAAY;GACZ,QAAQ,YAAY,QAAQ,wBAAwB,IAAM;GAC3D,CAAC;UACK,KAAK;AACZ,YAAU;AACV,MAAI,UAAU,oBAAoB;AAChC,OAAI,KAAK;IAAE;IAAK;IAAS;IAAK,EAAE,8BAA8B;AAC9D,SAAM,MAAM,MAAQ,QAAQ;;;AAIlC,OAAM,mBAAmB,QAAQ,UAAU,IAAI,MAAM,OAAO,QAAQ,CAAC;;AAGvE,eAAe,cACb,KACA,MACgE;CAChE,MAAM,MAAM,MAAM,UAAU,KAAK,KAAK;CACtC,MAAM,cAAc,IAAI,QAAQ,IAAI,eAAe,IAAI,KAAA;CACvD,MAAM,WAAW,IAAI,QAAQ,IAAI,WAAW;AAE5C,KAAI,CAAC,IAAI,IAAI;EACX,MAAM,OAAO,MAAM,IAAI,MAAM,CAAC,YAAY,GAAG;AAC7C,QAAM,IAAI,MAAM,aAAa,IAAI,OAAO,GAAG,IAAI,IAAI,KAAK,MAAM,GAAG,IAAI,GAAG;;AAG1E,KAAI,IAAI,WAAW,IACjB,QAAO;EAAE,MAAM,EAAE;EAAO,OAAO;EAAa;EAAU;AAIxD,QAAO;EAAE,MAAA,MADW,IAAI,MAAM;EACf,OAAO;EAAa;EAAU;;AAG/C,eAAe,eACb,KACA,SACA,OACA,SACgE;CAChE,MAAM,SAAS,QAAQ,IAAI,WAAW,OAAO;CAC7C,MAAM,MAAM,YAAY;EACtB,eAAe,QAAQ;EACvB;EACA;EACA;EACA,KAAK,SAAS,QAAQ,MAAM,KAAA;EAC5B,KAAK,SAAS,KAAA,IAAY,QAAQ;EACnC,CAAC;AAEF,QAAO,cAAiB,KAAK;EAC3B,QAAQ;EACR,SAAS,EAAE,gBAAgB,yBAAyB;EACpD,MAAM,KAAK,UAAU,IAAI;EAC1B,CAAC;;AAGJ,eAAe,oBAAoB,SAAiB,SAAsB,WAA2C;CACnH,MAAM,QAAQ,MAAM,SAAS,UAAU;CACvC,MAAM,MAAM,YAAY;EACtB,eAAe,QAAQ;EACvB,KAAK;EACL;EACA,SAAS;EACT,KAAK,QAAQ;EACd,CAAC;CACF,MAAM,MAAM,MAAM,UAAU,SAAS;EACnC,QAAQ;EACR,SAAS;GAAE,gBAAgB;GAAyB,QAAQ;GAAqC;EACjG,MAAM,KAAK,UAAU,IAAI;EAC1B,CAAC;AACF,KAAI,CAAC,IAAI,IAAI;EACX,MAAM,OAAO,MAAM,IAAI,MAAM,CAAC,YAAY,GAAG;AAC7C,QAAM,IAAI,MAAM,8BAA8B,IAAI,OAAO,GAAG,KAAK,MAAM,GAAG,IAAI,GAAG;;AAEnF,QAAO,IAAI,MAAM;;AAGnB,eAAe,aAAa,SAA0C;CAEpE,MAAM,EAAE,SAAS,MAAM,cADX,UAAU,eAAe,UAAU,eAAe,WACN;AACxD,QAAO;;AAGT,eAAe,SAAS,WAA2C;CAEjE,MAAM,SAAQ,MADI,UAAU,UAAU,UAAU,EAAE,QAAQ,QAAQ,CAAC,EACjD,QAAQ,IAAI,eAAe;AAC7C,KAAI,CAAC,MAAO,OAAM,IAAI,MAAM,sCAAsC;AAClE,QAAO;;AAGT,eAAe,cACb,WACA,QACA,SACsB;CACtB,MAAM,MAAM,2BAA2B,OAAO;CAC9C,IAAI,aAAa,eAAe,QAAQ;AACxC,KAAI,cAAc,CAAC,oBAAoB,YAAY,QAAQ,CACzD,cAAa;CAIf,MAAM,QAAQ,MAAM,SAAS,UAAU;CACvC,MAAM,SAAS,MAAM,eACnB,UAAU,YACV;EAAE,KAAK;EAAO;EAAK;EAAQ,EAC3B,OACA,EAAE,sBAAsB,MAAM,CAC/B;AAED,KAAI,OAAO,UAAU;AACnB,eAAa,OAAO;AACpB,iBAAe,SAAS,WAAW;;AAErC,KAAI,CAAC,WAAY,OAAM,IAAI,MAAM,oDAAoD;AAErF,QAAO;EAAE,KAAK;EAAY;EAAK;EAAQ;;AAGzC,eAAe,mBACb,cACA,SACA,WACe;AACf,MAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,QAAM,MAAM,MAAM,IAAI,MAAQ,IAAM;EAGpC,MAAM,EAAE,SAAS,MAAM,eAAoC,cAAc,SAAS,MAF9D,SAAS,UAAU,EAEkD,KAAK;AAC9F,MAAI,KAAK,WAAW,QAAS;AAC7B,MAAI,KAAK,WAAW,UAAW,OAAM,IAAI,MAAM,gCAAgC;;AAEjF,OAAM,IAAI,MAAM,kCAAkC;;AAGpD,eAAe,eACb,UACA,SACA,WACiB;AACjB,MAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK;EAE3B,MAAM,EAAE,SAAS,MAAM,eACrB,UACA,SACA,MAJkB,SAAS,UAAU,EAKrC,KACD;AACD,MAAI,KAAK,WAAW,WAAW,KAAK,YAAa,QAAO,KAAK;AAC7D,MAAI,KAAK,WAAW,UAAW,OAAM,IAAI,MAAM,qBAAqB;AACpE,QAAM,MAAM,IAAM;;AAEpB,OAAM,IAAI,MAAM,gCAAgC;;AAGlD,eAAsB,mBAAmB,QAA6C;CACpF,MAAM,UAAU,OAAO,WAAW;CAClC,MAAM,SAAS,kBAAkB,OAAO,WAAW,OAAO,iBAAiB;AAE3E,KAAI,KAAK;EAAE;EAAQ;EAAS,EAAE,oCAAoC;CAElE,MAAM,YAAY,MAAM,aAAa,QAAQ;CAE7C,MAAM,UAAU,MAAM,cAAc,WADd,mBACsC,EAAE,QAAQ;CAEtE,IAAI,QAAQ,MAAM,SAAS,UAAU;CACrC,MAAM,cAAc,MAAM,eACxB,UAAU,UACV,SACA,OACA,EAAE,aAAa,CAAC;EAAE,MAAM;EAAO,OAAO;EAAQ,CAAC,EAAE,CAClD;CACD,MAAM,WAAW,YAAY;AAC7B,KAAI,CAAC,SAAU,OAAM,IAAI,MAAM,kCAAkC;CAEjE,MAAM,WAAW,YAAY,KAAK,iBAAiB;AACnD,KAAI,CAAC,SAAU,OAAM,IAAI,MAAM,mCAAmC;AAElE,SAAQ,MAAM,SAAS,UAAU;CAIjC,MAAM,aAAY,MAHE,eAEjB,UAAU,SAAS,OAAO,KAAK,EACV,KAAK,YAAY,MAAM,MAAM,EAAE,SAAS,SAAS;AACzE,KAAI,CAAC,UAAW,OAAM,IAAI,MAAM,oCAAoC;CAEpE,MAAM,aAAa,cAAc,QAAQ,IAAI;CAC7C,MAAM,UAAU,GAAG,UAAU,MAAM,GAAG;CACtC,MAAM,WAAW,UAAU,WAAW,SAAS,CAAC,OAAO,QAAQ,CAAC,QAAQ,CAAC;AAEzE,KAAI,KAAK,EAAE,MAAM,mBAAmB,UAAU,EAAE,sCAAsC;CACtF,MAAM,EAAE,UAAU,SAAS,MAAM,OAAO,OAAO,gBAAgB;EAC7D,UAAU,OAAO;EACjB,aAAa,OAAO;EACpB,WAAW,OAAO;EAClB;EACD,CAAC;AAEF,KAAI;AACF,QAAM,cAAc,MAAM,SAAS;AAEnC,UAAQ,MAAM,SAAS,UAAU;AACjC,QAAM,eAAe,UAAU,KAAK,SAAS,OAAO,EAAE,CAAC;AAEvD,QAAM,mBAAmB,UAAU,KAAK,SAAS,UAAU;EAE3D,MAAM,EAAE,QAAQ,WAAW,kBAAkB,OAAO;EACpD,MAAM,cAAc,YAAY,KAAK;AACrC,MAAI,CAAC,YAAa,OAAM,IAAI,MAAM,kCAAkC;AAEpE,UAAQ,MAAM,SAAS,UAAU;AACjC,QAAM,eAAe,aAAa,SAAS,OAAO,EAAE,KAAK,UAAU,OAAO,EAAE,CAAC;EAG7E,MAAM,UAAU,MAAM,oBAAoB,MADpB,eAAe,UAAU,SAAS,UAAU,EACf,SAAS,UAAU;EAEtE,MAAM,YAAY,QAAQ,MAAM,+DAA+D,GAAG;AAClG,MAAI,CAAC,UAAW,OAAM,IAAI,MAAM,oCAAoC;EAEpE,MAAM,YAAY,qBAAqB,UAAU;AACjD,MAAI,KAAK;GAAE;GAAQ,WAAW,UAAU,aAAa;GAAE,EAAE,qBAAqB;AAE9E,SAAO;GAAE,SAAS,QAAQ,MAAM;GAAE;GAAQ;GAAQ;GAAW,0BAAU,IAAI,MAAM;GAAE;WAC3E;AACR,QAAM,OAAO,OACV,oBAAoB;GACnB,UAAU,OAAO;GACjB,aAAa,OAAO;GACpB;GACD,CAAC,CACD,OAAO,QAAQ;AACd,OAAI,KAAK;IAAE;IAAK;IAAU,EAAE,oCAAoC;IAChE"}
1
+ {"version":3,"file":"acme-client.js","names":["undiciFetch"],"sources":["../../../src/tunnel/acme-client.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { Resolver } from 'node:dns/promises';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { Agent, fetch as undiciFetch, type RequestInit } from 'undici';\n\nimport { resolveStateDir } from '../config/paths.js';\nimport { createLogger } from '../utils/logger.js';\nimport type { TunnelBrokerClient } from './broker-client.js';\nimport type { TunnelAcmeProgressStep } from './tunnel-types.js';\nimport {\n base64url,\n ensureEcAccountKeyPem,\n exportJwkFromPrivateKeyPem,\n getCertExpiryFromPem,\n jwkThumbprint,\n signAcmeJws,\n} from './acme-crypto.js';\nimport { generateDomainCsr } from './acme-csr.js';\n\nconst log = createLogger('TunnelACME');\n\nconst ACME_DIRECTORY = {\n production: 'https://acme-v02.api.letsencrypt.org/directory',\n staging: 'https://acme-staging-v02.api.letsencrypt.org/directory',\n} as const;\n\nconst ACME_FETCH_TIMEOUT_MS = 30_000;\nconst ACME_FETCH_RETRIES = 4;\n\nconst acmeDispatcher = new Agent({\n connect: { timeout: ACME_FETCH_TIMEOUT_MS },\n});\n\n/** Public resolvers — LE validators use global DNS, not the host's stale cache. */\nconst ACME_DNS_RESOLVERS = ['8.8.8.8', '1.1.1.1', '9.9.9.9'];\n\nlet acmeDnsResolver: Resolver | null = null;\n\nfunction getAcmeDnsResolver(): Resolver {\n if (!acmeDnsResolver) {\n acmeDnsResolver = new Resolver();\n acmeDnsResolver.setServers(ACME_DNS_RESOLVERS);\n }\n return acmeDnsResolver;\n}\n\nasync function resolveAcmeDnsTxt(fqdn: string): Promise<string[]> {\n const records = await getAcmeDnsResolver().resolveTxt(fqdn);\n return records.map((parts) => parts.join(''));\n}\n\nexport type AcmeConfig = {\n broker: TunnelBrokerClient;\n tunnelId: string;\n tunnelToken: string;\n subdomain: string;\n frpSubdomainHost: string;\n staging?: boolean;\n onProgress?: (step: TunnelAcmeProgressStep) => void;\n};\n\nexport type AcmeCertResult = {\n certPem: string;\n keyPem: string;\n domain: string;\n expiresAt: Date;\n issuedAt: Date;\n};\n\ntype AcmeDirectory = {\n newNonce: string;\n newAccount: string;\n newOrder: string;\n};\n\ntype AcmeAccount = {\n url: string;\n jwk: ReturnType<typeof exportJwkFromPrivateKeyPem>;\n keyPem: string;\n};\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\nfunction normalizeFqdn(fqdn: string): string {\n return fqdn.trim().replace(/\\.$/, '').toLowerCase();\n}\n\n/** LE always validates `_acme-challenge.{domain}` (RFC 8555 §8.4). */\nexport function resolveAcmeChallengeFqdn(domain: string): string {\n return `_acme-challenge.${domain}`;\n}\n\nexport function formatAcmeDnsChallengeInvalidError(\n fqdn: string,\n data: { error?: { detail?: string; type?: string }; validationRecord?: string[] },\n): string {\n const parts = [`ACME DNS-01 challenge invalid for ${fqdn}`];\n if (data.error?.detail) parts.push(data.error.detail);\n else if (data.error?.type) parts.push(data.error.type);\n if (data.validationRecord?.length) {\n parts.push(`validation: ${data.validationRecord.join('; ')}`);\n }\n return parts.join(' — ');\n}\n\nasync function waitForDnsTxt(\n fqdn: string,\n expectedValue: string,\n opts?: { initialDelayMs?: number; timeoutMs?: number },\n): Promise<void> {\n const initialDelayMs = opts?.initialDelayMs ?? 45_000;\n const timeoutMs = opts?.timeoutMs ?? 180_000;\n const deadline = Date.now() + timeoutMs;\n let lastSeen: string[] = [];\n // Dynadot authoritative DNS often needs ~60s after set_dns2 before TXT is queryable.\n if (initialDelayMs > 0) await sleep(initialDelayMs);\n while (Date.now() < deadline) {\n try {\n lastSeen = await resolveAcmeDnsTxt(fqdn);\n if (lastSeen.some((value) => value === expectedValue)) {\n log.info({ fqdn, resolvers: ACME_DNS_RESOLVERS, txtCount: lastSeen.length }, 'DNS-01 TXT record visible');\n return;\n }\n } catch {\n /* NXDOMAIN / timeout — keep polling until deadline */\n }\n await sleep(5_000);\n }\n throw new Error(\n `DNS TXT not visible for ${fqdn} (expected ${expectedValue}; last seen: ${lastSeen.join(', ') || 'none'})`,\n );\n}\n\nfunction getAcmeDir(): string {\n const dir = join(resolveStateDir(), 'tunnel', 'acme');\n mkdirSync(dir, { recursive: true });\n return dir;\n}\n\nfunction loadAccountKeyPem(): string {\n const keyPath = join(getAcmeDir(), 'account-key.pem');\n if (existsSync(keyPath)) {\n return readFileSync(keyPath, 'utf8');\n }\n const pem = ensureEcAccountKeyPem();\n writeFileSync(keyPath, pem, { mode: 0o600 });\n return pem;\n}\n\nfunction accountUrlPath(staging: boolean): string {\n return join(getAcmeDir(), staging ? 'account-url-staging.txt' : 'account-url-production.txt');\n}\n\nfunction loadAccountUrl(staging: boolean): string {\n const path = accountUrlPath(staging);\n if (existsSync(path)) {\n return readFileSync(path, 'utf8').trim();\n }\n // Legacy single file — treat as production only.\n const legacyPath = join(getAcmeDir(), 'account-url.txt');\n if (!staging && existsSync(legacyPath)) {\n const legacyUrl = readFileSync(legacyPath, 'utf8').trim();\n if (legacyUrl.startsWith('http')) {\n writeFileSync(path, legacyUrl, 'utf8');\n return legacyUrl;\n }\n }\n return '';\n}\n\nfunction saveAccountUrl(staging: boolean, url: string): void {\n writeFileSync(accountUrlPath(staging), url, 'utf8');\n}\n\nfunction accountUrlMatchesCa(accountUrl: string, staging: boolean): boolean {\n const host = staging ? 'acme-staging-v02.api.letsencrypt.org' : 'acme-v02.api.letsencrypt.org';\n try {\n return new URL(accountUrl).host === host;\n } catch {\n return false;\n }\n}\n\nfunction resolveCertDomain(subdomain: string, frpSubdomainHost: string): string {\n return `${subdomain}.${frpSubdomainHost}`;\n}\n\nasync function acmeFetch(url: string, init?: RequestInit): Promise<Response> {\n let lastErr: unknown;\n for (let attempt = 1; attempt <= ACME_FETCH_RETRIES; attempt++) {\n try {\n return await undiciFetch(url, {\n ...init,\n dispatcher: acmeDispatcher,\n signal: AbortSignal.timeout(ACME_FETCH_TIMEOUT_MS + 5_000),\n });\n } catch (err) {\n lastErr = err;\n if (attempt < ACME_FETCH_RETRIES) {\n log.warn({ url, attempt, err }, 'ACME fetch failed, retrying');\n await sleep(2_000 * attempt);\n }\n }\n }\n throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));\n}\n\nasync function acmeFetchJson<T>(\n url: string,\n init?: RequestInit,\n): Promise<{ data: T; nonce?: string; location?: string | null }> {\n const res = await acmeFetch(url, init);\n const replayNonce = res.headers.get('replay-nonce') ?? undefined;\n const location = res.headers.get('location');\n\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n throw new Error(`ACME HTTP ${res.status} ${url}: ${body.slice(0, 300)}`);\n }\n\n if (res.status === 204) {\n return { data: {} as T, nonce: replayNonce, location };\n }\n\n const data = (await res.json()) as T;\n return { data, nonce: replayNonce, location };\n}\n\nasync function acmeSignedPost<T>(\n url: string,\n account: AcmeAccount,\n nonce: string,\n payload: unknown,\n): Promise<{ data: T; nonce?: string; location?: string | null }> {\n const useKid = account.url.startsWith('http');\n const jws = signAcmeJws({\n privateKeyPem: account.keyPem,\n url,\n nonce,\n payload,\n kid: useKid ? account.url : undefined,\n jwk: useKid ? undefined : account.jwk,\n });\n\n return acmeFetchJson<T>(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/jose+json' },\n body: JSON.stringify(jws),\n });\n}\n\nasync function downloadCertificate(certUrl: string, account: AcmeAccount, directory: AcmeDirectory): Promise<string> {\n const nonce = await getNonce(directory);\n const jws = signAcmeJws({\n privateKeyPem: account.keyPem,\n url: certUrl,\n nonce,\n payload: null,\n kid: account.url,\n });\n const res = await acmeFetch(certUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/jose+json', Accept: 'application/pem-certificate-chain' },\n body: JSON.stringify(jws),\n });\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n throw new Error(`ACME cert download failed: ${res.status} ${body.slice(0, 200)}`);\n }\n return res.text();\n}\n\nasync function getDirectory(staging: boolean): Promise<AcmeDirectory> {\n const url = staging ? ACME_DIRECTORY.staging : ACME_DIRECTORY.production;\n const { data } = await acmeFetchJson<AcmeDirectory>(url);\n return data;\n}\n\nasync function getNonce(directory: AcmeDirectory): Promise<string> {\n const res = await acmeFetch(directory.newNonce, { method: 'HEAD' });\n const nonce = res.headers.get('replay-nonce');\n if (!nonce) throw new Error('ACME CA did not return replay-nonce');\n return nonce;\n}\n\nasync function ensureAccount(\n directory: AcmeDirectory,\n keyPem: string,\n staging: boolean,\n): Promise<AcmeAccount> {\n const jwk = exportJwkFromPrivateKeyPem(keyPem);\n let accountUrl = loadAccountUrl(staging);\n if (accountUrl && !accountUrlMatchesCa(accountUrl, staging)) {\n accountUrl = '';\n }\n\n // new-acct MUST use embedded jwk (RFC 8555 §7.3.1). LE returns an existing account when the JWK matches.\n const nonce = await getNonce(directory);\n const result = await acmeSignedPost<{ status?: string }>(\n directory.newAccount,\n { url: 'new', jwk, keyPem },\n nonce,\n { termsOfServiceAgreed: true },\n );\n\n if (result.location) {\n accountUrl = result.location;\n saveAccountUrl(staging, accountUrl);\n }\n if (!accountUrl) throw new Error('ACME account registration failed (no account URL)');\n\n return { url: accountUrl, jwk, keyPem };\n}\n\nasync function pollChallengeReady(\n challengeUrl: string,\n account: AcmeAccount,\n directory: AcmeDirectory,\n challengeFqdn: string,\n): Promise<void> {\n for (let i = 0; i < 30; i++) {\n await sleep(i === 0 ? 5_000 : 2_000);\n const nonce = await getNonce(directory);\n // POST-as-GET (payload null) — must not POST `{}` again; that re-submits the challenge response.\n const { data } = await acmeSignedPost<{\n status?: string;\n error?: { type?: string; detail?: string };\n validationRecord?: string[];\n }>(challengeUrl, account, nonce, null);\n if (data.status === 'valid') return;\n if (data.status === 'invalid') {\n const message = formatAcmeDnsChallengeInvalidError(challengeFqdn, data);\n log.error({ challengeFqdn, error: data.error, validationRecord: data.validationRecord }, message);\n throw new Error(message);\n }\n }\n throw new Error(`ACME DNS-01 challenge timed out for ${challengeFqdn}`);\n}\n\nasync function pollOrderValid(\n orderUrl: string,\n account: AcmeAccount,\n directory: AcmeDirectory,\n): Promise<string> {\n for (let i = 0; i < 30; i++) {\n const nonce = await getNonce(directory);\n const { data } = await acmeSignedPost<{ status?: string; certificate?: string }>(\n orderUrl,\n account,\n nonce,\n null,\n );\n if (data.status === 'valid' && data.certificate) return data.certificate;\n if (data.status === 'invalid') throw new Error('ACME order invalid');\n await sleep(2_000);\n }\n throw new Error('ACME order finalize timed out');\n}\n\nexport async function requestCertificate(config: AcmeConfig): Promise<AcmeCertResult> {\n const staging = config.staging ?? false;\n const domain = resolveCertDomain(config.subdomain, config.frpSubdomainHost);\n\n log.info({ domain, staging }, 'Starting ACME certificate request');\n config.onProgress?.('checking');\n\n const directory = await getDirectory(staging);\n const accountKeyPem = loadAccountKeyPem();\n const account = await ensureAccount(directory, accountKeyPem, staging);\n\n let nonce = await getNonce(directory);\n const orderResult = await acmeSignedPost<{ authorizations?: string[]; finalize?: string }>(\n directory.newOrder,\n account,\n nonce,\n { identifiers: [{ type: 'dns', value: domain }] },\n );\n const orderUrl = orderResult.location;\n if (!orderUrl) throw new Error('ACME newOrder missing order URL');\n\n const authzUrl = orderResult.data.authorizations?.[0];\n if (!authzUrl) throw new Error('ACME order missing authorization');\n\n nonce = await getNonce(directory);\n const authz = await acmeSignedPost<{\n challenges?: Array<{ type: string; url: string; token: string }>;\n }>(authzUrl, account, nonce, null);\n const challenge = authz.data.challenges?.find((c) => c.type === 'dns-01');\n if (!challenge) throw new Error('No DNS-01 challenge offered by CA');\n\n const thumbprint = jwkThumbprint(account.jwk);\n const keyAuth = `${challenge.token}.${thumbprint}`;\n const txtValue = base64url(createHash('sha256').update(keyAuth).digest());\n\n const challengeFqdn = resolveAcmeChallengeFqdn(domain);\n log.info({ fqdn: challengeFqdn, txtPreview: `${txtValue.slice(0, 8)}…` }, 'Setting DNS-01 challenge via Broker');\n config.onProgress?.('dns_challenge');\n const { recordId, fqdn } = await config.broker.setDnsChallenge({\n tunnelId: config.tunnelId,\n tunnelToken: config.tunnelToken,\n subdomain: config.subdomain,\n txtValue,\n });\n\n if (normalizeFqdn(fqdn) !== normalizeFqdn(challengeFqdn)) {\n log.warn(\n { brokerFqdn: fqdn, challengeFqdn, phase: 'acme_dns_fqdn_mismatch' },\n 'Broker returned unexpected ACME challenge FQDN — polling canonical name for Let\\'s Encrypt',\n );\n }\n\n try {\n config.onProgress?.('dns_propagation');\n await waitForDnsTxt(challengeFqdn, txtValue);\n // Public resolvers can lead LE validators; re-check before submitting the challenge.\n await sleep(15_000);\n await waitForDnsTxt(challengeFqdn, txtValue, { initialDelayMs: 0, timeoutMs: 60_000 });\n\n nonce = await getNonce(directory);\n await acmeSignedPost(challenge.url, account, nonce, {});\n\n config.onProgress?.('ca_validation');\n await pollChallengeReady(challenge.url, account, directory, challengeFqdn);\n\n const { csrDer, keyPem } = generateDomainCsr(domain);\n const finalizeUrl = orderResult.data.finalize;\n if (!finalizeUrl) throw new Error('ACME order missing finalize URL');\n\n config.onProgress?.('issuing');\n nonce = await getNonce(directory);\n await acmeSignedPost(finalizeUrl, account, nonce, { csr: base64url(csrDer) });\n\n const certUrl = await pollOrderValid(orderUrl, account, directory);\n const certPem = await downloadCertificate(certUrl, account, directory);\n\n const firstCert = certPem.match(/-----BEGIN CERTIFICATE-----[\\s\\S]+?-----END CERTIFICATE-----/)?.[0];\n if (!firstCert) throw new Error('ACME certificate PEM parse failed');\n\n const expiresAt = getCertExpiryFromPem(firstCert);\n log.info({ domain, expiresAt: expiresAt.toISOString() }, 'Certificate issued');\n\n return { certPem: certPem.trim(), keyPem, domain, expiresAt, issuedAt: new Date() };\n } finally {\n await config.broker\n .cleanupDnsChallenge({\n tunnelId: config.tunnelId,\n tunnelToken: config.tunnelToken,\n recordId,\n })\n .catch((err) => {\n log.warn({ err, recordId }, 'DNS cleanup failed (non-critical)');\n });\n }\n}\n"],"mappings":"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"}
@@ -1,9 +1,20 @@
1
+ import type { Config } from '../config/schema.js';
2
+ export declare const TUNNEL_MASKED_SECRET_SENTINELS: readonly ["***", "••••••••••••"];
3
+ export type TunnelRegistrationSecretSource = 'env' | 'config' | 'dev_default' | 'missing';
4
+ export type TunnelRegistrationSecretMeta = {
5
+ configured: boolean;
6
+ source: TunnelRegistrationSecretSource;
7
+ };
1
8
  /** True when the broker is the public frp.xopc.ai service (not local dev). */
2
9
  export declare function isProductionTunnelBroker(brokerUrl: string): boolean;
10
+ export declare function isMaskedTunnelSecretPatchValue(value: string): boolean;
11
+ /**
12
+ * Describe where the tunnel registration secret will be resolved from (no secret value returned).
13
+ */
14
+ export declare function getTunnelRegistrationSecretMeta(config: Config | undefined, env?: NodeJS.ProcessEnv, brokerUrl?: string): TunnelRegistrationSecretMeta;
3
15
  /**
4
16
  * Registration secret for Tunnel Broker register API.
5
- * Env `XOPC_TUNNEL_REGISTRATION_SECRET` always wins.
6
- * Dev default only for non-production brokers; production requires env.
17
+ * Priority: env `XOPC_TUNNEL_REGISTRATION_SECRET` `tunnel.registrationSecret` in config → dev default (non-production brokers only).
7
18
  */
8
- export declare function resolveTunnelRegistrationSecret(env?: NodeJS.ProcessEnv, brokerUrl?: string): string;
19
+ export declare function resolveTunnelRegistrationSecret(env?: NodeJS.ProcessEnv, brokerUrl?: string, configSecret?: string): string;
9
20
  export declare function resolveTunnelBrokerUrl(configBrokerUrl: string | undefined, env?: NodeJS.ProcessEnv): string;
@@ -1,5 +1,6 @@
1
1
  //#region src/tunnel/env.ts
2
2
  const DEV_REGISTRATION_SECRET = "dev-registration-secret";
3
+ const TUNNEL_MASKED_SECRET_SENTINELS = ["***", "••••••••••••"];
3
4
  function brokerHostname(brokerUrl) {
4
5
  try {
5
6
  const normalized = brokerUrl.includes("://") ? brokerUrl : `https://${brokerUrl}`;
@@ -16,21 +17,49 @@ function isProductionTunnelBroker(brokerUrl) {
16
17
  if (host.endsWith(".local")) return false;
17
18
  return host === "frp.xopc.ai";
18
19
  }
20
+ function isMaskedTunnelSecretPatchValue(value) {
21
+ return TUNNEL_MASKED_SECRET_SENTINELS.includes(value);
22
+ }
23
+ function effectiveBrokerUrl(brokerUrl, env) {
24
+ return brokerUrl ?? env.XOPC_TUNNEL_BROKER_URL ?? "https://frp.xopc.ai/api";
25
+ }
26
+ /**
27
+ * Describe where the tunnel registration secret will be resolved from (no secret value returned).
28
+ */
29
+ function getTunnelRegistrationSecretMeta(config, env = process.env, brokerUrl) {
30
+ if (env.XOPC_TUNNEL_REGISTRATION_SECRET?.trim()) return {
31
+ configured: true,
32
+ source: "env"
33
+ };
34
+ if (config?.tunnel?.registrationSecret?.trim()) return {
35
+ configured: true,
36
+ source: "config"
37
+ };
38
+ if (isProductionTunnelBroker(effectiveBrokerUrl(brokerUrl ?? config?.tunnel?.brokerUrl, env))) return {
39
+ configured: false,
40
+ source: "missing"
41
+ };
42
+ return {
43
+ configured: true,
44
+ source: "dev_default"
45
+ };
46
+ }
19
47
  /**
20
48
  * Registration secret for Tunnel Broker register API.
21
- * Env `XOPC_TUNNEL_REGISTRATION_SECRET` always wins.
22
- * Dev default only for non-production brokers; production requires env.
49
+ * Priority: env `XOPC_TUNNEL_REGISTRATION_SECRET` `tunnel.registrationSecret` in config → dev default (non-production brokers only).
23
50
  */
24
- function resolveTunnelRegistrationSecret(env = process.env, brokerUrl) {
51
+ function resolveTunnelRegistrationSecret(env = process.env, brokerUrl, configSecret) {
25
52
  const fromEnv = env.XOPC_TUNNEL_REGISTRATION_SECRET?.trim();
26
53
  if (fromEnv) return fromEnv;
27
- if (isProductionTunnelBroker(brokerUrl ?? env.XOPC_TUNNEL_BROKER_URL ?? "https://frp.xopc.ai/api")) throw new Error("XOPC_TUNNEL_REGISTRATION_SECRET is required for the production tunnel broker (frp.xopc.ai). Copy the value from the server .env or set it in your shell profile.");
54
+ const fromConfig = configSecret?.trim();
55
+ if (fromConfig) return fromConfig;
56
+ if (isProductionTunnelBroker(effectiveBrokerUrl(brokerUrl, env))) throw new Error("Tunnel registration secret is required for the production broker (frp.xopc.ai). Set XOPC_TUNNEL_REGISTRATION_SECRET or tunnel.registrationSecret in xopc.json (Remote access settings).");
28
57
  return DEV_REGISTRATION_SECRET;
29
58
  }
30
59
  function resolveTunnelBrokerUrl(configBrokerUrl, env = process.env) {
31
60
  return env.XOPC_TUNNEL_BROKER_URL ?? configBrokerUrl ?? "https://frp.xopc.ai/api";
32
61
  }
33
62
  //#endregion
34
- export { isProductionTunnelBroker, resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret };
63
+ export { TUNNEL_MASKED_SECRET_SENTINELS, getTunnelRegistrationSecretMeta, isMaskedTunnelSecretPatchValue, isProductionTunnelBroker, resolveTunnelBrokerUrl, resolveTunnelRegistrationSecret };
35
64
 
36
65
  //# sourceMappingURL=env.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"env.js","names":[],"sources":["../../../src/tunnel/env.ts"],"sourcesContent":["const DEV_REGISTRATION_SECRET = 'dev-registration-secret';\n\nfunction brokerHostname(brokerUrl: string): string | null {\n try {\n const normalized = brokerUrl.includes('://') ? brokerUrl : `https://${brokerUrl}`;\n return new URL(normalized.replace(/\\/+$/, '')).hostname;\n } catch {\n return null;\n }\n}\n\n/** True when the broker is the public frp.xopc.ai service (not local dev). */\nexport function isProductionTunnelBroker(brokerUrl: string): boolean {\n const host = brokerHostname(brokerUrl);\n if (!host) return true;\n if (host === 'localhost' || host === '127.0.0.1' || host === '::1') return false;\n if (host.endsWith('.local')) return false;\n return host === 'frp.xopc.ai';\n}\n\n/**\n * Registration secret for Tunnel Broker register API.\n * Env `XOPC_TUNNEL_REGISTRATION_SECRET` always wins.\n * Dev default only for non-production brokers; production requires env.\n */\nexport function resolveTunnelRegistrationSecret(\n env: NodeJS.ProcessEnv = process.env,\n brokerUrl?: string,\n): string {\n const fromEnv = env.XOPC_TUNNEL_REGISTRATION_SECRET?.trim();\n if (fromEnv) return fromEnv;\n\n const effectiveUrl =\n brokerUrl ?? env.XOPC_TUNNEL_BROKER_URL ?? 'https://frp.xopc.ai/api';\n\n if (isProductionTunnelBroker(effectiveUrl)) {\n throw new Error(\n 'XOPC_TUNNEL_REGISTRATION_SECRET is required for the production tunnel broker (frp.xopc.ai). ' +\n 'Copy the value from the server .env or set it in your shell profile.',\n );\n }\n\n return DEV_REGISTRATION_SECRET;\n}\n\nexport function resolveTunnelBrokerUrl(\n configBrokerUrl: string | undefined,\n env: NodeJS.ProcessEnv = process.env,\n): string {\n return env.XOPC_TUNNEL_BROKER_URL ?? configBrokerUrl ?? 'https://frp.xopc.ai/api';\n}\n"],"mappings":";AAAA,MAAM,0BAA0B;AAEhC,SAAS,eAAe,WAAkC;AACxD,KAAI;EACF,MAAM,aAAa,UAAU,SAAS,MAAM,GAAG,YAAY,WAAW;AACtE,SAAO,IAAI,IAAI,WAAW,QAAQ,QAAQ,GAAG,CAAC,CAAC;SACzC;AACN,SAAO;;;;AAKX,SAAgB,yBAAyB,WAA4B;CACnE,MAAM,OAAO,eAAe,UAAU;AACtC,KAAI,CAAC,KAAM,QAAO;AAClB,KAAI,SAAS,eAAe,SAAS,eAAe,SAAS,MAAO,QAAO;AAC3E,KAAI,KAAK,SAAS,SAAS,CAAE,QAAO;AACpC,QAAO,SAAS;;;;;;;AAQlB,SAAgB,gCACd,MAAyB,QAAQ,KACjC,WACQ;CACR,MAAM,UAAU,IAAI,iCAAiC,MAAM;AAC3D,KAAI,QAAS,QAAO;AAKpB,KAAI,yBAFF,aAAa,IAAI,0BAA0B,0BAEH,CACxC,OAAM,IAAI,MACR,mKAED;AAGH,QAAO;;AAGT,SAAgB,uBACd,iBACA,MAAyB,QAAQ,KACzB;AACR,QAAO,IAAI,0BAA0B,mBAAmB"}
1
+ {"version":3,"file":"env.js","names":[],"sources":["../../../src/tunnel/env.ts"],"sourcesContent":["import type { Config } from '../config/schema.js';\n\nconst DEV_REGISTRATION_SECRET = 'dev-registration-secret';\n\nexport const TUNNEL_MASKED_SECRET_SENTINELS = ['***', '••••••••••••'] as const;\n\nexport type TunnelRegistrationSecretSource = 'env' | 'config' | 'dev_default' | 'missing';\n\nexport type TunnelRegistrationSecretMeta = {\n configured: boolean;\n source: TunnelRegistrationSecretSource;\n};\n\nfunction brokerHostname(brokerUrl: string): string | null {\n try {\n const normalized = brokerUrl.includes('://') ? brokerUrl : `https://${brokerUrl}`;\n return new URL(normalized.replace(/\\/+$/, '')).hostname;\n } catch {\n return null;\n }\n}\n\n/** True when the broker is the public frp.xopc.ai service (not local dev). */\nexport function isProductionTunnelBroker(brokerUrl: string): boolean {\n const host = brokerHostname(brokerUrl);\n if (!host) return true;\n if (host === 'localhost' || host === '127.0.0.1' || host === '::1') return false;\n if (host.endsWith('.local')) return false;\n return host === 'frp.xopc.ai';\n}\n\nexport function isMaskedTunnelSecretPatchValue(value: string): boolean {\n return (TUNNEL_MASKED_SECRET_SENTINELS as readonly string[]).includes(value);\n}\n\nfunction effectiveBrokerUrl(\n brokerUrl: string | undefined,\n env: NodeJS.ProcessEnv,\n): string {\n return brokerUrl ?? env.XOPC_TUNNEL_BROKER_URL ?? 'https://frp.xopc.ai/api';\n}\n\n/**\n * Describe where the tunnel registration secret will be resolved from (no secret value returned).\n */\nexport function getTunnelRegistrationSecretMeta(\n config: Config | undefined,\n env: NodeJS.ProcessEnv = process.env,\n brokerUrl?: string,\n): TunnelRegistrationSecretMeta {\n if (env.XOPC_TUNNEL_REGISTRATION_SECRET?.trim()) {\n return { configured: true, source: 'env' };\n }\n\n if (config?.tunnel?.registrationSecret?.trim()) {\n return { configured: true, source: 'config' };\n }\n\n const effectiveUrl = effectiveBrokerUrl(brokerUrl ?? config?.tunnel?.brokerUrl, env);\n if (isProductionTunnelBroker(effectiveUrl)) {\n return { configured: false, source: 'missing' };\n }\n\n return { configured: true, source: 'dev_default' };\n}\n\n/**\n * Registration secret for Tunnel Broker register API.\n * Priority: env `XOPC_TUNNEL_REGISTRATION_SECRET` `tunnel.registrationSecret` in config dev default (non-production brokers only).\n */\nexport function resolveTunnelRegistrationSecret(\n env: NodeJS.ProcessEnv = process.env,\n brokerUrl?: string,\n configSecret?: string,\n): string {\n const fromEnv = env.XOPC_TUNNEL_REGISTRATION_SECRET?.trim();\n if (fromEnv) return fromEnv;\n\n const fromConfig = configSecret?.trim();\n if (fromConfig) return fromConfig;\n\n const effectiveUrl = effectiveBrokerUrl(brokerUrl, env);\n\n if (isProductionTunnelBroker(effectiveUrl)) {\n throw new Error(\n 'Tunnel registration secret is required for the production broker (frp.xopc.ai). ' +\n 'Set XOPC_TUNNEL_REGISTRATION_SECRET or tunnel.registrationSecret in xopc.json (Remote access settings).',\n );\n }\n\n return DEV_REGISTRATION_SECRET;\n}\n\nexport function resolveTunnelBrokerUrl(\n configBrokerUrl: string | undefined,\n env: NodeJS.ProcessEnv = process.env,\n): string {\n return env.XOPC_TUNNEL_BROKER_URL ?? configBrokerUrl ?? 'https://frp.xopc.ai/api';\n}\n"],"mappings":";AAEA,MAAM,0BAA0B;AAEhC,MAAa,iCAAiC,CAAC,OAAO,eAAe;AASrE,SAAS,eAAe,WAAkC;AACxD,KAAI;EACF,MAAM,aAAa,UAAU,SAAS,MAAM,GAAG,YAAY,WAAW;AACtE,SAAO,IAAI,IAAI,WAAW,QAAQ,QAAQ,GAAG,CAAC,CAAC;SACzC;AACN,SAAO;;;;AAKX,SAAgB,yBAAyB,WAA4B;CACnE,MAAM,OAAO,eAAe,UAAU;AACtC,KAAI,CAAC,KAAM,QAAO;AAClB,KAAI,SAAS,eAAe,SAAS,eAAe,SAAS,MAAO,QAAO;AAC3E,KAAI,KAAK,SAAS,SAAS,CAAE,QAAO;AACpC,QAAO,SAAS;;AAGlB,SAAgB,+BAA+B,OAAwB;AACrE,QAAQ,+BAAqD,SAAS,MAAM;;AAG9E,SAAS,mBACP,WACA,KACQ;AACR,QAAO,aAAa,IAAI,0BAA0B;;;;;AAMpD,SAAgB,gCACd,QACA,MAAyB,QAAQ,KACjC,WAC8B;AAC9B,KAAI,IAAI,iCAAiC,MAAM,CAC7C,QAAO;EAAE,YAAY;EAAM,QAAQ;EAAO;AAG5C,KAAI,QAAQ,QAAQ,oBAAoB,MAAM,CAC5C,QAAO;EAAE,YAAY;EAAM,QAAQ;EAAU;AAI/C,KAAI,yBADiB,mBAAmB,aAAa,QAAQ,QAAQ,WAAW,IACvC,CAAC,CACxC,QAAO;EAAE,YAAY;EAAO,QAAQ;EAAW;AAGjD,QAAO;EAAE,YAAY;EAAM,QAAQ;EAAe;;;;;;AAOpD,SAAgB,gCACd,MAAyB,QAAQ,KACjC,WACA,cACQ;CACR,MAAM,UAAU,IAAI,iCAAiC,MAAM;AAC3D,KAAI,QAAS,QAAO;CAEpB,MAAM,aAAa,cAAc,MAAM;AACvC,KAAI,WAAY,QAAO;AAIvB,KAAI,yBAFiB,mBAAmB,WAAW,IAEV,CAAC,CACxC,OAAM,IAAI,MACR,0LAED;AAGH,QAAO;;AAGT,SAAgB,uBACd,iBACA,MAAyB,QAAQ,KACzB;AACR,QAAO,IAAI,0BAA0B,mBAAmB"}
@@ -1,6 +1,11 @@
1
+ import type { FrpcDownloadProgress } from './tunnel-types.js';
1
2
  export declare const FRPC_VERSION = "0.62.1";
3
+ export type { FrpcDownloadProgress };
4
+ export type EnsureFrpcBinaryOptions = {
5
+ onProgress?: (progress: FrpcDownloadProgress) => void;
6
+ };
2
7
  /** Set after a successful tunnel start so subprocesses can resolve the same binary. */
3
8
  export declare function publishFrpcPathForProcess(binPath: string): void;
4
9
  /** Clear runtime frpc path when the tunnel stops (no bundled Electron binary). */
5
10
  export declare function clearFrpcPathForProcess(): void;
6
- export declare function ensureFrpcBinary(): Promise<string>;
11
+ export declare function ensureFrpcBinary(opts?: EnsureFrpcBinaryOptions): Promise<string>;
@@ -1,11 +1,12 @@
1
1
  import { createLogger } from "../utils/logger/index.js";
2
2
  import { init_logger } from "../utils/logger.js";
3
3
  import { init_paths, resolveBinDir } from "../config/paths.js";
4
+ import { extractFrpcFromTarGzArchive } from "./frpc-extract.js";
4
5
  import { join } from "node:path";
5
6
  import { tmpdir } from "node:os";
6
- import { chmodSync, createWriteStream, existsSync, mkdirSync } from "node:fs";
7
+ import { chmodSync, createWriteStream, existsSync, mkdirSync, rmSync } from "node:fs";
7
8
  import { randomBytes } from "node:crypto";
8
- import { spawn } from "node:child_process";
9
+ import { Readable } from "node:stream";
9
10
  import { pipeline } from "node:stream/promises";
10
11
  //#region src/tunnel/frpc-binary.ts
11
12
  init_paths();
@@ -22,36 +23,45 @@ const ARCH_MAP = {
22
23
  arm64: "arm64",
23
24
  ia32: "386"
24
25
  };
26
+ function frpcPlatformArch() {
27
+ const platform = PLATFORM_MAP[process.platform] ?? process.platform;
28
+ const arch = ARCH_MAP[process.arch] ?? process.arch;
29
+ return {
30
+ platform,
31
+ arch,
32
+ folder: `frp_${FRPC_VERSION}_${platform}_${arch}`
33
+ };
34
+ }
25
35
  function frpcDownloadUrls() {
26
- const base = `frp_${FRPC_VERSION}_${PLATFORM_MAP[process.platform] ?? process.platform}_${ARCH_MAP[process.arch] ?? process.arch}`;
36
+ const { folder } = frpcPlatformArch();
27
37
  return [
28
- `https://github.com/fatedier/frp/releases/download/v${FRPC_VERSION}/${base}.tar.gz`,
29
- `https://ghfast.top/https://github.com/fatedier/frp/releases/download/v${FRPC_VERSION}/${base}.tar.gz`,
30
- `https://frp.xopc.ai/bin/${base}.tar.gz`
38
+ `https://github.com/fatedier/frp/releases/download/v${FRPC_VERSION}/${folder}.tar.gz`,
39
+ `https://ghfast.top/https://github.com/fatedier/frp/releases/download/v${FRPC_VERSION}/${folder}.tar.gz`,
40
+ `https://frp.xopc.ai/bin/${folder}.tar.gz`
31
41
  ];
32
42
  }
33
- async function extractFrpcFromTarGz(archivePath, destDir, binName) {
34
- const innerPath = `${`frp_${FRPC_VERSION}_${PLATFORM_MAP[process.platform] ?? process.platform}_${ARCH_MAP[process.arch] ?? process.arch}`}/${binName}`;
35
- await new Promise((resolve, reject) => {
36
- const child = spawn("tar", [
37
- "xzf",
38
- archivePath,
39
- "-C",
40
- destDir,
41
- innerPath,
42
- "--strip-components=1"
43
- ], { stdio: "ignore" });
44
- child.on("error", reject);
45
- child.on("exit", (code) => {
46
- if (code === 0) resolve();
47
- else reject(/* @__PURE__ */ new Error(`tar exited with code ${code ?? "unknown"}`));
48
- });
49
- });
50
- }
51
- async function downloadToFile(url, destPath) {
43
+ async function downloadToFile(url, destPath, onProgress) {
52
44
  const res = await fetch(url);
53
45
  if (!res.ok || !res.body) throw new Error(`Download failed: ${res.status} ${url}`);
54
- await pipeline(res.body, createWriteStream(destPath));
46
+ const contentLength = res.headers.get("content-length");
47
+ const totalBytes = contentLength && Number.isFinite(Number(contentLength)) ? Number(contentLength) : null;
48
+ let bytesReceived = 0;
49
+ const report = () => {
50
+ onProgress?.({
51
+ phase: "downloading",
52
+ bytesReceived,
53
+ totalBytes,
54
+ percent: totalBytes && totalBytes > 0 ? Math.min(100, Math.round(bytesReceived / totalBytes * 100)) : null
55
+ });
56
+ };
57
+ report();
58
+ const nodeStream = Readable.fromWeb(res.body);
59
+ nodeStream.on("data", (chunk) => {
60
+ bytesReceived += typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.length;
61
+ report();
62
+ });
63
+ await pipeline(nodeStream, createWriteStream(destPath));
64
+ report();
55
65
  }
56
66
  /** Set after a successful tunnel start so subprocesses can resolve the same binary. */
57
67
  function publishFrpcPathForProcess(binPath) {
@@ -61,7 +71,8 @@ function publishFrpcPathForProcess(binPath) {
61
71
  function clearFrpcPathForProcess() {
62
72
  delete process.env.XOPC_FRPC_PATH;
63
73
  }
64
- async function ensureFrpcBinary() {
74
+ async function ensureFrpcBinary(opts) {
75
+ const onProgress = opts?.onProgress;
65
76
  const binName = `frpc${process.platform === "win32" ? ".exe" : ""}`;
66
77
  const fromEnv = process.env.XOPC_FRPC_PATH?.trim();
67
78
  if (fromEnv && existsSync(fromEnv)) return fromEnv;
@@ -74,20 +85,35 @@ async function ensureFrpcBinary() {
74
85
  const archivePath = join(tmpBase, "frpc.tar.gz");
75
86
  const urls = frpcDownloadUrls();
76
87
  let lastErr;
77
- for (const url of urls) try {
78
- log.info({ url }, "Downloading frpc");
79
- await downloadToFile(url, archivePath);
80
- await extractFrpcFromTarGz(archivePath, cacheDir, binName);
81
- if (process.platform !== "win32") chmodSync(cachePath, 493);
82
- return cachePath;
83
- } catch (err) {
84
- lastErr = err;
85
- log.warn({
86
- url,
87
- err
88
- }, "frpc download attempt failed");
88
+ try {
89
+ for (const url of urls) try {
90
+ log.info({ url }, "Downloading frpc");
91
+ await downloadToFile(url, archivePath, onProgress);
92
+ onProgress?.({
93
+ phase: "extracting",
94
+ bytesReceived: 0,
95
+ totalBytes: null,
96
+ percent: null
97
+ });
98
+ const { folder } = frpcPlatformArch();
99
+ await extractFrpcFromTarGzArchive(archivePath, cachePath, folder);
100
+ if (process.platform !== "win32") chmodSync(cachePath, 493);
101
+ return cachePath;
102
+ } catch (err) {
103
+ lastErr = err;
104
+ log.warn({
105
+ url,
106
+ err
107
+ }, "frpc download attempt failed");
108
+ }
109
+ } finally {
110
+ rmSync(tmpBase, {
111
+ recursive: true,
112
+ force: true
113
+ });
89
114
  }
90
- throw new Error(`Failed to download frpc v${FRPC_VERSION} for ${process.platform}/${process.arch}: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
115
+ const { platform, arch } = frpcPlatformArch();
116
+ throw new Error(`Failed to download frpc v${FRPC_VERSION} for ${platform}/${arch}: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
91
117
  }
92
118
  //#endregion
93
119
  export { FRPC_VERSION, clearFrpcPathForProcess, ensureFrpcBinary, publishFrpcPathForProcess };
@@ -1 +1 @@
1
- {"version":3,"file":"frpc-binary.js","names":[],"sources":["../../../src/tunnel/frpc-binary.ts"],"sourcesContent":["import { chmodSync, createWriteStream, existsSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { pipeline } from 'node:stream/promises';\nimport { spawn } from 'node:child_process';\nimport { tmpdir } from 'node:os';\nimport { randomBytes } from 'node:crypto';\n\nimport { resolveBinDir } from '../config/paths.js';\nimport { createLogger } from '../utils/logger.js';\n\nconst log = createLogger('TunnelFrpc');\n\nexport const FRPC_VERSION = '0.62.1';\n\nconst PLATFORM_MAP: Record<string, string> = {\n darwin: 'darwin',\n linux: 'linux',\n win32: 'windows',\n};\n\nconst ARCH_MAP: Record<string, string> = {\n x64: 'amd64',\n arm64: 'arm64',\n ia32: '386',\n};\n\nfunction frpcDownloadUrls(): string[] {\n const platform = PLATFORM_MAP[process.platform] ?? process.platform;\n const arch = ARCH_MAP[process.arch] ?? process.arch;\n const base = `frp_${FRPC_VERSION}_${platform}_${arch}`;\n return [\n `https://github.com/fatedier/frp/releases/download/v${FRPC_VERSION}/${base}.tar.gz`,\n `https://ghfast.top/https://github.com/fatedier/frp/releases/download/v${FRPC_VERSION}/${base}.tar.gz`,\n `https://frp.xopc.ai/bin/${base}.tar.gz`,\n ];\n}\n\nasync function extractFrpcFromTarGz(archivePath: string, destDir: string, binName: string): Promise<void> {\n const platform = PLATFORM_MAP[process.platform] ?? process.platform;\n const arch = ARCH_MAP[process.arch] ?? process.arch;\n const folder = `frp_${FRPC_VERSION}_${platform}_${arch}`;\n const innerPath = `${folder}/${binName}`;\n await new Promise<void>((resolve, reject) => {\n const child = spawn('tar', ['xzf', archivePath, '-C', destDir, innerPath, '--strip-components=1'], {\n stdio: 'ignore',\n });\n child.on('error', reject);\n child.on('exit', (code) => {\n if (code === 0) resolve();\n else reject(new Error(`tar exited with code ${code ?? 'unknown'}`));\n });\n });\n}\n\nasync function downloadToFile(url: string, destPath: string): Promise<void> {\n const res = await fetch(url);\n if (!res.ok || !res.body) {\n throw new Error(`Download failed: ${res.status} ${url}`);\n }\n await pipeline(res.body as unknown as NodeJS.ReadableStream, createWriteStream(destPath));\n}\n\n/** Set after a successful tunnel start so subprocesses can resolve the same binary. */\nexport function publishFrpcPathForProcess(binPath: string): void {\n process.env.XOPC_FRPC_PATH = binPath;\n}\n\n/** Clear runtime frpc path when the tunnel stops (no bundled Electron binary). */\nexport function clearFrpcPathForProcess(): void {\n delete process.env.XOPC_FRPC_PATH;\n}\n\nexport async function ensureFrpcBinary(): Promise<string> {\n const ext = process.platform === 'win32' ? '.exe' : '';\n const binName = `frpc${ext}`;\n\n const fromEnv = process.env.XOPC_FRPC_PATH?.trim();\n if (fromEnv && existsSync(fromEnv)) {\n return fromEnv;\n }\n\n const cacheDir = resolveBinDir();\n mkdirSync(cacheDir, { recursive: true });\n const cachePath = join(cacheDir, binName);\n if (existsSync(cachePath)) return cachePath;\n\n const tmpBase = join(tmpdir(), `xopc-frpc-${randomBytes(6).toString('hex')}`);\n mkdirSync(tmpBase, { recursive: true });\n const archivePath = join(tmpBase, 'frpc.tar.gz');\n\n const urls = frpcDownloadUrls();\n let lastErr: unknown;\n for (const url of urls) {\n try {\n log.info({ url }, 'Downloading frpc');\n await downloadToFile(url, archivePath);\n await extractFrpcFromTarGz(archivePath, cacheDir, binName);\n if (process.platform !== 'win32') chmodSync(cachePath, 0o755);\n return cachePath;\n } catch (err) {\n lastErr = err;\n log.warn({ url, err }, 'frpc download attempt failed');\n }\n }\n\n throw new Error(\n `Failed to download frpc v${FRPC_VERSION} for ${process.platform}/${process.arch}: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`,\n );\n}\n"],"mappings":";;;;;;;;;;YAOmD;aACD;AAElD,MAAM,MAAM,aAAa,aAAa;AAEtC,MAAa,eAAe;AAE5B,MAAM,eAAuC;CAC3C,QAAQ;CACR,OAAO;CACP,OAAO;CACR;AAED,MAAM,WAAmC;CACvC,KAAK;CACL,OAAO;CACP,MAAM;CACP;AAED,SAAS,mBAA6B;CAGpC,MAAM,OAAO,OAAO,aAAa,GAFhB,aAAa,QAAQ,aAAa,QAAQ,SAEd,GADhC,SAAS,QAAQ,SAAS,QAAQ;AAE/C,QAAO;EACL,sDAAsD,aAAa,GAAG,KAAK;EAC3E,yEAAyE,aAAa,GAAG,KAAK;EAC9F,2BAA2B,KAAK;EACjC;;AAGH,eAAe,qBAAqB,aAAqB,SAAiB,SAAgC;CAIxG,MAAM,YAAY,GAAG,OADC,aAAa,GAFlB,aAAa,QAAQ,aAAa,QAAQ,SAEZ,GADlC,SAAS,QAAQ,SAAS,QAAQ,OAEnB,GAAG;AAC/B,OAAM,IAAI,SAAe,SAAS,WAAW;EAC3C,MAAM,QAAQ,MAAM,OAAO;GAAC;GAAO;GAAa;GAAM;GAAS;GAAW;GAAuB,EAAE,EACjG,OAAO,UACR,CAAC;AACF,QAAM,GAAG,SAAS,OAAO;AACzB,QAAM,GAAG,SAAS,SAAS;AACzB,OAAI,SAAS,EAAG,UAAS;OACpB,wBAAO,IAAI,MAAM,wBAAwB,QAAQ,YAAY,CAAC;IACnE;GACF;;AAGJ,eAAe,eAAe,KAAa,UAAiC;CAC1E,MAAM,MAAM,MAAM,MAAM,IAAI;AAC5B,KAAI,CAAC,IAAI,MAAM,CAAC,IAAI,KAClB,OAAM,IAAI,MAAM,oBAAoB,IAAI,OAAO,GAAG,MAAM;AAE1D,OAAM,SAAS,IAAI,MAA0C,kBAAkB,SAAS,CAAC;;;AAI3F,SAAgB,0BAA0B,SAAuB;AAC/D,SAAQ,IAAI,iBAAiB;;;AAI/B,SAAgB,0BAAgC;AAC9C,QAAO,QAAQ,IAAI;;AAGrB,eAAsB,mBAAoC;CAExD,MAAM,UAAU,OADJ,QAAQ,aAAa,UAAU,SAAS;CAGpD,MAAM,UAAU,QAAQ,IAAI,gBAAgB,MAAM;AAClD,KAAI,WAAW,WAAW,QAAQ,CAChC,QAAO;CAGT,MAAM,WAAW,eAAe;AAChC,WAAU,UAAU,EAAE,WAAW,MAAM,CAAC;CACxC,MAAM,YAAY,KAAK,UAAU,QAAQ;AACzC,KAAI,WAAW,UAAU,CAAE,QAAO;CAElC,MAAM,UAAU,KAAK,QAAQ,EAAE,aAAa,YAAY,EAAE,CAAC,SAAS,MAAM,GAAG;AAC7E,WAAU,SAAS,EAAE,WAAW,MAAM,CAAC;CACvC,MAAM,cAAc,KAAK,SAAS,cAAc;CAEhD,MAAM,OAAO,kBAAkB;CAC/B,IAAI;AACJ,MAAK,MAAM,OAAO,KAChB,KAAI;AACF,MAAI,KAAK,EAAE,KAAK,EAAE,mBAAmB;AACrC,QAAM,eAAe,KAAK,YAAY;AACtC,QAAM,qBAAqB,aAAa,UAAU,QAAQ;AAC1D,MAAI,QAAQ,aAAa,QAAS,WAAU,WAAW,IAAM;AAC7D,SAAO;UACA,KAAK;AACZ,YAAU;AACV,MAAI,KAAK;GAAE;GAAK;GAAK,EAAE,+BAA+B;;AAI1D,OAAM,IAAI,MACR,4BAA4B,aAAa,OAAO,QAAQ,SAAS,GAAG,QAAQ,KAAK,IAAI,mBAAmB,QAAQ,QAAQ,UAAU,OAAO,QAAQ,GAClJ"}
1
+ {"version":3,"file":"frpc-binary.js","names":[],"sources":["../../../src/tunnel/frpc-binary.ts"],"sourcesContent":["import {\n chmodSync,\n createWriteStream,\n existsSync,\n mkdirSync,\n rmSync,\n} from 'node:fs';\nimport { join } from 'node:path';\nimport { pipeline } from 'node:stream/promises';\nimport { Readable } from 'node:stream';\nimport { tmpdir } from 'node:os';\nimport { randomBytes } from 'node:crypto';\n\nimport { resolveBinDir } from '../config/paths.js';\nimport { createLogger } from '../utils/logger.js';\nimport { extractFrpcFromTarGzArchive } from './frpc-extract.js';\nimport type { FrpcDownloadProgress } from './tunnel-types.js';\n\nconst log = createLogger('TunnelFrpc');\n\nexport const FRPC_VERSION = '0.62.1';\n\nexport type { FrpcDownloadProgress };\n\nexport type EnsureFrpcBinaryOptions = {\n onProgress?: (progress: FrpcDownloadProgress) => void;\n};\n\nconst PLATFORM_MAP: Record<string, string> = {\n darwin: 'darwin',\n linux: 'linux',\n win32: 'windows',\n};\n\nconst ARCH_MAP: Record<string, string> = {\n x64: 'amd64',\n arm64: 'arm64',\n ia32: '386',\n};\n\nfunction frpcPlatformArch(): { platform: string; arch: string; folder: string } {\n const platform = PLATFORM_MAP[process.platform] ?? process.platform;\n const arch = ARCH_MAP[process.arch] ?? process.arch;\n return { platform, arch, folder: `frp_${FRPC_VERSION}_${platform}_${arch}` };\n}\n\nfunction frpcDownloadUrls(): string[] {\n const { folder } = frpcPlatformArch();\n return [\n `https://github.com/fatedier/frp/releases/download/v${FRPC_VERSION}/${folder}.tar.gz`,\n `https://ghfast.top/https://github.com/fatedier/frp/releases/download/v${FRPC_VERSION}/${folder}.tar.gz`,\n `https://frp.xopc.ai/bin/${folder}.tar.gz`,\n ];\n}\n\nasync function downloadToFile(\n url: string,\n destPath: string,\n onProgress?: (progress: FrpcDownloadProgress) => void,\n): Promise<void> {\n const res = await fetch(url);\n if (!res.ok || !res.body) {\n throw new Error(`Download failed: ${res.status} ${url}`);\n }\n\n const contentLength = res.headers.get('content-length');\n const totalBytes =\n contentLength && Number.isFinite(Number(contentLength)) ? Number(contentLength) : null;\n let bytesReceived = 0;\n\n const report = () => {\n onProgress?.({\n phase: 'downloading',\n bytesReceived,\n totalBytes,\n percent:\n totalBytes && totalBytes > 0\n ? Math.min(100, Math.round((bytesReceived / totalBytes) * 100))\n : null,\n });\n };\n\n report();\n\n const nodeStream = Readable.fromWeb(res.body as Parameters<typeof Readable.fromWeb>[0]);\n nodeStream.on('data', (chunk: Buffer | string) => {\n bytesReceived += typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length;\n report();\n });\n\n await pipeline(nodeStream, createWriteStream(destPath));\n report();\n}\n\n/** Set after a successful tunnel start so subprocesses can resolve the same binary. */\nexport function publishFrpcPathForProcess(binPath: string): void {\n process.env.XOPC_FRPC_PATH = binPath;\n}\n\n/** Clear runtime frpc path when the tunnel stops (no bundled Electron binary). */\nexport function clearFrpcPathForProcess(): void {\n delete process.env.XOPC_FRPC_PATH;\n}\n\nexport async function ensureFrpcBinary(opts?: EnsureFrpcBinaryOptions): Promise<string> {\n const onProgress = opts?.onProgress;\n const ext = process.platform === 'win32' ? '.exe' : '';\n const binName = `frpc${ext}`;\n\n const fromEnv = process.env.XOPC_FRPC_PATH?.trim();\n if (fromEnv && existsSync(fromEnv)) {\n return fromEnv;\n }\n\n const cacheDir = resolveBinDir();\n mkdirSync(cacheDir, { recursive: true });\n const cachePath = join(cacheDir, binName);\n if (existsSync(cachePath)) return cachePath;\n\n const tmpBase = join(tmpdir(), `xopc-frpc-${randomBytes(6).toString('hex')}`);\n mkdirSync(tmpBase, { recursive: true });\n const archivePath = join(tmpBase, 'frpc.tar.gz');\n\n const urls = frpcDownloadUrls();\n let lastErr: unknown;\n try {\n for (const url of urls) {\n try {\n log.info({ url }, 'Downloading frpc');\n await downloadToFile(url, archivePath, onProgress);\n onProgress?.({ phase: 'extracting', bytesReceived: 0, totalBytes: null, percent: null });\n const { folder } = frpcPlatformArch();\n await extractFrpcFromTarGzArchive(archivePath, cachePath, folder);\n if (process.platform !== 'win32') chmodSync(cachePath, 0o755);\n return cachePath;\n } catch (err) {\n lastErr = err;\n log.warn({ url, err }, 'frpc download attempt failed');\n }\n }\n } finally {\n rmSync(tmpBase, { recursive: true, force: true });\n }\n\n const { platform, arch } = frpcPlatformArch();\n throw new Error(\n `Failed to download frpc v${FRPC_VERSION} for ${platform}/${arch}: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`,\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export declare function buildFrpcArchiveMemberPath(folder: string, platform: NodeJS.Platform): string;
2
+ /** Resolve on-disk path after system tar extracts a POSIX member path. */
3
+ export declare function resolveExtractedMemberPath(extractDir: string, memberPath: string): string;
4
+ /** Pure Node tar.gz member extract — no system `tar` (Windows / minimal Linux). */
5
+ export declare function extractTarGzMemberNode(archivePath: string, memberPath: string, destPath: string): void;
6
+ /**
7
+ * Extract frpc from a release tarball.
8
+ * Tries system `tar` first; falls back to built-in Node parser (macOS BSD tar quirks, Windows without tar, etc.).
9
+ */
10
+ export declare function extractFrpcFromTarGzArchive(archivePath: string, destBin: string, folder: string, platform?: NodeJS.Platform): Promise<void>;