@xopcai/xopc 0.0.77 → 0.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{agents-DN3vr8pb.js → agents-Bh_9-1KB.js} +2 -2
- package/dist/gateway/static/root/assets/{agents-DN3vr8pb.js.map → agents-Bh_9-1KB.js.map} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-BUn41aPi.js → apps-page-CB5anZpc.js} +2 -2
- package/dist/gateway/static/root/assets/{apps-page-BUn41aPi.js.map → apps-page-CB5anZpc.js.map} +1 -1
- package/dist/gateway/static/root/assets/{channels-settings-CYMmWDtP.js → channels-settings-Bt1sprhC.js} +2 -2
- package/dist/gateway/static/root/assets/{channels-settings-CYMmWDtP.js.map → channels-settings-Bt1sprhC.js.map} +1 -1
- package/dist/gateway/static/root/assets/{channels-status-swr-sJj4ueTp.js → channels-status-swr-Crgak3fg.js} +2 -2
- package/dist/gateway/static/root/assets/{channels-status-swr-sJj4ueTp.js.map → channels-status-swr-Crgak3fg.js.map} +1 -1
- package/dist/gateway/static/root/assets/{cron-api-CLxnaHdq.js → cron-api-CzJGvQQ7.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-api-CLxnaHdq.js.map → cron-api-CzJGvQQ7.js.map} +1 -1
- package/dist/gateway/static/root/assets/{cron-page-BAQ8xSnJ.js → cron-page-BoNRJNVV.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-page-BAQ8xSnJ.js.map → cron-page-BoNRJNVV.js.map} +1 -1
- package/dist/gateway/static/root/assets/{dist-BfJYxiK5.js → dist-a-eaOUvs.js} +2 -2
- package/dist/gateway/static/root/assets/{dist-BfJYxiK5.js.map → dist-a-eaOUvs.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-bgvVs-Sy.js → extension-debug-page-D-O1XjAa.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-debug-page-bgvVs-Sy.js.map → extension-debug-page-D-O1XjAa.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-SG4TVv-u.js → extension-page-B2VpqBTH.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-page-SG4TVv-u.js.map → extension-page-B2VpqBTH.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-CJZRTsjF.js → extension-settings-page-CmBcQfeO.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-settings-page-CJZRTsjF.js.map → extension-settings-page-CmBcQfeO.js.map} +1 -1
- package/dist/gateway/static/root/assets/{fetch-K_0JRCXU.js → fetch-EGO9T3MN.js} +3 -3
- package/dist/gateway/static/root/assets/{fetch-K_0JRCXU.js.map → fetch-EGO9T3MN.js.map} +1 -1
- package/dist/gateway/static/root/assets/{field-primitives-Z76hyBYS.js → field-primitives-Bh7G1y4D.js} +2 -2
- package/dist/gateway/static/root/assets/{field-primitives-Z76hyBYS.js.map → field-primitives-Bh7G1y4D.js.map} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-config-api-BqfDabSI.js → heartbeat-config-api-DxpIEZNs.js} +2 -2
- package/dist/gateway/static/root/assets/{heartbeat-config-api-BqfDabSI.js.map → heartbeat-config-api-DxpIEZNs.js.map} +1 -1
- package/dist/gateway/static/root/assets/{index-ChiUhJAs.js → index-Dxy9ZCtC.js} +5 -5
- package/dist/gateway/static/root/assets/{index-ChiUhJAs.js.map → index-Dxy9ZCtC.js.map} +1 -1
- package/dist/gateway/static/root/assets/{logs-page-DrIMhDE2.js → logs-page-Dw58E2GE.js} +2 -2
- package/dist/gateway/static/root/assets/{logs-page-DrIMhDE2.js.map → logs-page-Dw58E2GE.js.map} +1 -1
- package/dist/gateway/static/root/assets/{sessions-page-B-RGO3N0.js → sessions-page-CPkhCy57.js} +2 -2
- package/dist/gateway/static/root/assets/{sessions-page-B-RGO3N0.js.map → sessions-page-CPkhCy57.js.map} +1 -1
- package/dist/gateway/static/root/assets/{settings-form-section-Csvl1iL6.js → settings-form-section-DLZDVMEf.js} +2 -2
- package/dist/gateway/static/root/assets/{settings-form-section-Csvl1iL6.js.map → settings-form-section-DLZDVMEf.js.map} +1 -1
- package/dist/gateway/static/root/assets/settings-page-CVPCa0PE.js +4 -0
- package/dist/gateway/static/root/assets/settings-page-CVPCa0PE.js.map +1 -0
- package/dist/gateway/static/root/assets/{skills-page-dHwx2vh0.js → skills-page-DueZ9Qfg.js} +2 -2
- package/dist/gateway/static/root/assets/{skills-page-dHwx2vh0.js.map → skills-page-DueZ9Qfg.js.map} +1 -1
- package/dist/gateway/static/root/assets/{theme-store-Bl5A2Fd_.js → theme-store-CWPq9gW1.js} +2 -2
- package/dist/gateway/static/root/assets/{theme-store-Bl5A2Fd_.js.map → theme-store-CWPq9gW1.js.map} +1 -1
- package/dist/gateway/static/root/assets/{utils-COYrNFF7.js → utils-Cnix55r9.js} +2 -2
- package/dist/gateway/static/root/assets/{utils-COYrNFF7.js.map → utils-Cnix55r9.js.map} +1 -1
- package/dist/gateway/static/root/assets/{voice-api-key-field-5WZZaxH3.js → voice-api-key-field-BR3Ut06g.js} +2 -2
- package/dist/gateway/static/root/assets/{voice-api-key-field-5WZZaxH3.js.map → voice-api-key-field-BR3Ut06g.js.map} +1 -1
- package/dist/gateway/static/root/index.html +3 -3
- package/dist/package.js +1 -1
- package/dist/src/browser/providers/browser-ext-install.js +23 -4
- package/dist/src/browser/providers/browser-ext-install.js.map +1 -1
- package/dist/src/cli/commands/tunnel.js +4 -5
- package/dist/src/cli/commands/tunnel.js.map +1 -1
- package/dist/src/config/index.js +2 -2
- package/dist/src/config/rules.js +0 -5
- package/dist/src/config/rules.js.map +1 -1
- package/dist/src/config/schema.d.ts +0 -27
- package/dist/src/config/schema.js +4 -18
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/gateway/auth-rate-limit.d.ts +2 -0
- package/dist/src/gateway/auth-rate-limit.js +9 -3
- package/dist/src/gateway/auth-rate-limit.js.map +1 -1
- package/dist/src/gateway/hono/app.js +19 -13
- package/dist/src/gateway/hono/app.js.map +1 -1
- package/dist/src/gateway/hono/lib/config-payload.d.ts +3 -1
- package/dist/src/gateway/hono/lib/config-payload.js +1 -2
- package/dist/src/gateway/hono/lib/config-payload.js.map +1 -1
- package/dist/src/gateway/hono/routes/tunnel.js +32 -30
- package/dist/src/gateway/hono/routes/tunnel.js.map +1 -1
- package/dist/src/gateway/host.d.ts +24 -0
- package/dist/src/gateway/host.js +33 -1
- package/dist/src/gateway/host.js.map +1 -1
- package/dist/src/gateway/index.d.ts +1 -1
- package/dist/src/gateway/index.js +2 -2
- package/dist/src/gateway/runtime-config.js +1 -8
- package/dist/src/gateway/runtime-config.js.map +1 -1
- package/dist/src/gateway/security/audit.js +4 -4
- package/dist/src/gateway/security/audit.js.map +1 -1
- package/dist/src/gateway/server.js +2 -3
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service/types.d.ts +2 -0
- package/dist/src/gateway/service.d.ts +2 -0
- package/dist/src/gateway/service.js +7 -2
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/tunnel/frp-subdomain-host.d.ts +2 -0
- package/dist/src/tunnel/frp-subdomain-host.js +15 -0
- package/dist/src/tunnel/frp-subdomain-host.js.map +1 -0
- package/dist/src/tunnel/gateway-lifecycle.d.ts +0 -4
- package/dist/src/tunnel/gateway-lifecycle.js +9 -11
- package/dist/src/tunnel/gateway-lifecycle.js.map +1 -1
- package/dist/src/tunnel/index.d.ts +2 -4
- package/dist/src/tunnel/index.js +2 -4
- package/dist/src/tunnel/pair-url.js +7 -1
- package/dist/src/tunnel/pair-url.js.map +1 -1
- package/dist/src/tunnel/pairing.d.ts +13 -0
- package/dist/src/tunnel/pairing.js +48 -1
- package/dist/src/tunnel/pairing.js.map +1 -1
- package/dist/src/tunnel/tunnel-config.js +2 -16
- package/dist/src/tunnel/tunnel-config.js.map +1 -1
- package/dist/src/tunnel/tunnel-service.d.ts +1 -10
- package/dist/src/tunnel/tunnel-service.js +7 -60
- package/dist/src/tunnel/tunnel-service.js.map +1 -1
- package/dist/src/tunnel/tunnel-types.d.ts +3 -18
- package/dist/src/tunnel/well-known.d.ts +5 -0
- package/dist/src/tunnel/well-known.js +2 -1
- package/dist/src/tunnel/well-known.js.map +1 -1
- package/package.json +2 -2
- package/dist/gateway/static/root/assets/settings-page-nxAc0ta1.js +0 -4
- package/dist/gateway/static/root/assets/settings-page-nxAc0ta1.js.map +0 -1
- package/dist/src/tunnel/acme-cert-store.d.ts +0 -34
- package/dist/src/tunnel/acme-cert-store.js +0 -184
- package/dist/src/tunnel/acme-cert-store.js.map +0 -1
- package/dist/src/tunnel/acme-client.d.ts +0 -50
- package/dist/src/tunnel/acme-client.js +0 -473
- package/dist/src/tunnel/acme-client.js.map +0 -1
- package/dist/src/tunnel/acme-crypto.d.ts +0 -25
- package/dist/src/tunnel/acme-crypto.js +0 -58
- package/dist/src/tunnel/acme-crypto.js.map +0 -1
- package/dist/src/tunnel/acme-csr.d.ts +0 -5
- package/dist/src/tunnel/acme-csr.js +0 -48
- package/dist/src/tunnel/acme-csr.js.map +0 -1
- package/dist/src/tunnel/tls-server.d.ts +0 -14
- package/dist/src/tunnel/tls-server.js +0 -126
- package/dist/src/tunnel/tls-server.js.map +0 -1
- package/dist/src/tunnel/tunnel-e2e-config.d.ts +0 -11
- package/dist/src/tunnel/tunnel-e2e-config.js +0 -29
- package/dist/src/tunnel/tunnel-e2e-config.js.map +0 -1
|
@@ -1,473 +0,0 @@
|
|
|
1
|
-
import { resolveStateDir } from "../config/paths-state.js";
|
|
2
|
-
import { createLogger } from "../utils/logger/index.js";
|
|
3
|
-
import { init_logger } from "../utils/logger.js";
|
|
4
|
-
import { init_paths } from "../config/paths.js";
|
|
5
|
-
import { base64url, ensureEcAccountKeyPem, exportJwkFromPrivateKeyPem, getCertExpiryFromPem, jwkThumbprint, signAcmeJws } from "./acme-crypto.js";
|
|
6
|
-
import { generateDomainCsr } from "./acme-csr.js";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { createHash } from "node:crypto";
|
|
10
|
-
import { Agent, fetch } from "undici";
|
|
11
|
-
import { Resolver } from "node:dns/promises";
|
|
12
|
-
//#region src/tunnel/acme-client.ts
|
|
13
|
-
init_paths();
|
|
14
|
-
init_logger();
|
|
15
|
-
const log = createLogger("TunnelACME");
|
|
16
|
-
const ACME_DIRECTORY = {
|
|
17
|
-
production: "https://acme-v02.api.letsencrypt.org/directory",
|
|
18
|
-
staging: "https://acme-staging-v02.api.letsencrypt.org/directory"
|
|
19
|
-
};
|
|
20
|
-
const ACME_FETCH_TIMEOUT_MS = 3e4;
|
|
21
|
-
const ACME_FETCH_RETRIES = 4;
|
|
22
|
-
const acmeDispatcher = new Agent({ connect: { timeout: ACME_FETCH_TIMEOUT_MS } });
|
|
23
|
-
/** Public resolvers — LE validators use global DNS, not the host's stale cache. */
|
|
24
|
-
const ACME_DNS_RESOLVERS = [
|
|
25
|
-
"8.8.8.8",
|
|
26
|
-
"1.1.1.1",
|
|
27
|
-
"9.9.9.9"
|
|
28
|
-
];
|
|
29
|
-
/** Consecutive consensus rounds required before trusting DNS propagation. */
|
|
30
|
-
const ACME_DNS_CONSENSUS_ROUNDS = 2;
|
|
31
|
-
/** Minimum time after publishing TXT before accepting consensus (Dynadot ~60s). */
|
|
32
|
-
const ACME_DNS_MIN_PROPAGATION_MS = 3e4;
|
|
33
|
-
/** Hold consensus this long before submitting (LE secondary validation). */
|
|
34
|
-
const ACME_DNS_STABLE_MS = 3e4;
|
|
35
|
-
/** Poll interval while waiting for DNS. */
|
|
36
|
-
const ACME_DNS_POLL_INTERVAL_MS = 5e3;
|
|
37
|
-
/** Max time to wait for DNS consensus (first + stable phases). */
|
|
38
|
-
const ACME_DNS_TIMEOUT_MS = 24e4;
|
|
39
|
-
/** Retry whole DNS-01 attempt when LE reports secondary validation failures. */
|
|
40
|
-
const ACME_DNS_ATTEMPTS = 2;
|
|
41
|
-
/** Pause between ACME DNS-01 retries. */
|
|
42
|
-
const ACME_DNS_RETRY_DELAY_MS = 45e3;
|
|
43
|
-
async function resolveAcmeDnsTxtFromResolver(fqdn, resolver) {
|
|
44
|
-
const dns = new Resolver();
|
|
45
|
-
dns.setServers([resolver]);
|
|
46
|
-
return (await dns.resolveTxt(fqdn)).map((parts) => parts.join(""));
|
|
47
|
-
}
|
|
48
|
-
/** Query each public resolver independently (Node tries setServers in order — not sufficient for LE). */
|
|
49
|
-
async function probeAcmeDnsTxtAllResolvers(fqdn) {
|
|
50
|
-
return Promise.all(ACME_DNS_RESOLVERS.map(async (resolver) => {
|
|
51
|
-
try {
|
|
52
|
-
return {
|
|
53
|
-
resolver,
|
|
54
|
-
values: await resolveAcmeDnsTxtFromResolver(fqdn, resolver)
|
|
55
|
-
};
|
|
56
|
-
} catch (err) {
|
|
57
|
-
return {
|
|
58
|
-
resolver,
|
|
59
|
-
values: [],
|
|
60
|
-
error: err instanceof Error ? err.message : String(err)
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
}));
|
|
64
|
-
}
|
|
65
|
-
function allAcmeDnsResolversHaveTxt(probes, expectedValue) {
|
|
66
|
-
return probes.length > 0 && probes.every((probe) => probe.values.some((value) => value === expectedValue));
|
|
67
|
-
}
|
|
68
|
-
function summarizeAcmeDnsProbes(probes) {
|
|
69
|
-
return probes.map((probe) => {
|
|
70
|
-
const seen = probe.values.join(", ") || probe.error || "none";
|
|
71
|
-
return `${probe.resolver}=${seen}`;
|
|
72
|
-
}).join("; ");
|
|
73
|
-
}
|
|
74
|
-
function sleep(ms) {
|
|
75
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
76
|
-
}
|
|
77
|
-
function normalizeFqdn(fqdn) {
|
|
78
|
-
return fqdn.trim().replace(/\.$/, "").toLowerCase();
|
|
79
|
-
}
|
|
80
|
-
/** LE always validates `_acme-challenge.{domain}` (RFC 8555 §8.4). */
|
|
81
|
-
function resolveAcmeChallengeFqdn(domain) {
|
|
82
|
-
return `_acme-challenge.${domain}`;
|
|
83
|
-
}
|
|
84
|
-
function formatValidationRecordEntry(entry) {
|
|
85
|
-
if (typeof entry === "string") return entry;
|
|
86
|
-
if (entry && typeof entry === "object") {
|
|
87
|
-
const obj = entry;
|
|
88
|
-
if (typeof obj.detail === "string") return obj.detail;
|
|
89
|
-
const hostname = typeof obj.hostname === "string" ? obj.hostname : "";
|
|
90
|
-
const error = typeof obj.error === "string" ? obj.error : "";
|
|
91
|
-
if (hostname && error) return `${hostname}: ${error}`;
|
|
92
|
-
if (error) return error;
|
|
93
|
-
try {
|
|
94
|
-
return JSON.stringify(entry);
|
|
95
|
-
} catch {
|
|
96
|
-
return String(entry);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return String(entry);
|
|
100
|
-
}
|
|
101
|
-
function formatAcmeDnsChallengeInvalidError(fqdn, data) {
|
|
102
|
-
const parts = [`ACME DNS-01 challenge invalid for ${fqdn}`];
|
|
103
|
-
if (data.error?.detail) parts.push(data.error.detail);
|
|
104
|
-
else if (data.error?.type) parts.push(data.error.type);
|
|
105
|
-
if (data.validationRecord?.length) parts.push(`validation: ${data.validationRecord.map(formatValidationRecordEntry).join("; ")}`);
|
|
106
|
-
return parts.join(" — ");
|
|
107
|
-
}
|
|
108
|
-
function isRetryableAcmeDnsError(err) {
|
|
109
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
110
|
-
return /secondary validation/i.test(message) || /DNS TXT not visible on all resolvers/i.test(message) || /ACME DNS-01 challenge timed out/i.test(message);
|
|
111
|
-
}
|
|
112
|
-
/** Poll public resolvers until TXT is stable — no blind initial sleep. */
|
|
113
|
-
async function waitForDnsTxtConsensus(fqdn, expectedValue, opts) {
|
|
114
|
-
const minPropagationMs = opts?.minPropagationMs ?? ACME_DNS_MIN_PROPAGATION_MS;
|
|
115
|
-
const stableMs = opts?.stableMs ?? ACME_DNS_STABLE_MS;
|
|
116
|
-
const timeoutMs = opts?.timeoutMs ?? ACME_DNS_TIMEOUT_MS;
|
|
117
|
-
const consensusRounds = opts?.consensusRounds ?? ACME_DNS_CONSENSUS_ROUNDS;
|
|
118
|
-
const pollIntervalMs = opts?.pollIntervalMs ?? ACME_DNS_POLL_INTERVAL_MS;
|
|
119
|
-
const probe = opts?.probe ?? probeAcmeDnsTxtAllResolvers;
|
|
120
|
-
const delay = opts?.sleep ?? sleep;
|
|
121
|
-
const startedAt = Date.now();
|
|
122
|
-
const deadline = startedAt + timeoutMs;
|
|
123
|
-
let lastProbes = [];
|
|
124
|
-
let consensusStreak = 0;
|
|
125
|
-
let stableSince = null;
|
|
126
|
-
while (Date.now() < deadline) {
|
|
127
|
-
lastProbes = await probe(fqdn);
|
|
128
|
-
const elapsedMs = Date.now() - startedAt;
|
|
129
|
-
if (elapsedMs >= minPropagationMs && allAcmeDnsResolversHaveTxt(lastProbes, expectedValue)) {
|
|
130
|
-
consensusStreak += 1;
|
|
131
|
-
if (consensusStreak >= consensusRounds) {
|
|
132
|
-
if (stableMs <= 0) {
|
|
133
|
-
log.info({
|
|
134
|
-
fqdn,
|
|
135
|
-
resolvers: ACME_DNS_RESOLVERS,
|
|
136
|
-
elapsedMs,
|
|
137
|
-
probes: summarizeAcmeDnsProbes(lastProbes)
|
|
138
|
-
}, "DNS-01 TXT visible on all public resolvers");
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (stableSince === null) stableSince = Date.now();
|
|
142
|
-
if (Date.now() - stableSince >= stableMs) {
|
|
143
|
-
const finalProbes = await probe(fqdn);
|
|
144
|
-
if (allAcmeDnsResolversHaveTxt(finalProbes, expectedValue)) {
|
|
145
|
-
log.info({
|
|
146
|
-
fqdn,
|
|
147
|
-
resolvers: ACME_DNS_RESOLVERS,
|
|
148
|
-
elapsedMs: Date.now() - startedAt,
|
|
149
|
-
stableMs,
|
|
150
|
-
probes: summarizeAcmeDnsProbes(finalProbes)
|
|
151
|
-
}, "DNS-01 TXT stable on all public resolvers");
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
stableSince = null;
|
|
155
|
-
consensusStreak = 0;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
consensusStreak = 0;
|
|
160
|
-
stableSince = null;
|
|
161
|
-
}
|
|
162
|
-
await delay(pollIntervalMs);
|
|
163
|
-
}
|
|
164
|
-
throw new Error(`DNS TXT not visible on all resolvers for ${fqdn} (expected ${expectedValue}; last probe: ${summarizeAcmeDnsProbes(lastProbes)})`);
|
|
165
|
-
}
|
|
166
|
-
function getAcmeDir() {
|
|
167
|
-
const dir = join(resolveStateDir(), "tunnel", "acme");
|
|
168
|
-
mkdirSync(dir, { recursive: true });
|
|
169
|
-
return dir;
|
|
170
|
-
}
|
|
171
|
-
function loadAccountKeyPem() {
|
|
172
|
-
const keyPath = join(getAcmeDir(), "account-key.pem");
|
|
173
|
-
if (existsSync(keyPath)) return readFileSync(keyPath, "utf8");
|
|
174
|
-
const pem = ensureEcAccountKeyPem();
|
|
175
|
-
writeFileSync(keyPath, pem, { mode: 384 });
|
|
176
|
-
return pem;
|
|
177
|
-
}
|
|
178
|
-
function accountUrlPath(staging) {
|
|
179
|
-
return join(getAcmeDir(), staging ? "account-url-staging.txt" : "account-url-production.txt");
|
|
180
|
-
}
|
|
181
|
-
function loadAccountUrl(staging) {
|
|
182
|
-
const path = accountUrlPath(staging);
|
|
183
|
-
if (existsSync(path)) return readFileSync(path, "utf8").trim();
|
|
184
|
-
const legacyPath = join(getAcmeDir(), "account-url.txt");
|
|
185
|
-
if (!staging && existsSync(legacyPath)) {
|
|
186
|
-
const legacyUrl = readFileSync(legacyPath, "utf8").trim();
|
|
187
|
-
if (legacyUrl.startsWith("http")) {
|
|
188
|
-
writeFileSync(path, legacyUrl, "utf8");
|
|
189
|
-
return legacyUrl;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return "";
|
|
193
|
-
}
|
|
194
|
-
function saveAccountUrl(staging, url) {
|
|
195
|
-
writeFileSync(accountUrlPath(staging), url, "utf8");
|
|
196
|
-
}
|
|
197
|
-
function accountUrlMatchesCa(accountUrl, staging) {
|
|
198
|
-
const host = staging ? "acme-staging-v02.api.letsencrypt.org" : "acme-v02.api.letsencrypt.org";
|
|
199
|
-
try {
|
|
200
|
-
return new URL(accountUrl).host === host;
|
|
201
|
-
} catch {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
function resolveCertDomain(subdomain, frpSubdomainHost) {
|
|
206
|
-
return `${subdomain}.${frpSubdomainHost}`;
|
|
207
|
-
}
|
|
208
|
-
async function acmeFetch(url, init) {
|
|
209
|
-
let lastErr;
|
|
210
|
-
for (let attempt = 1; attempt <= ACME_FETCH_RETRIES; attempt++) try {
|
|
211
|
-
return await fetch(url, {
|
|
212
|
-
...init,
|
|
213
|
-
dispatcher: acmeDispatcher,
|
|
214
|
-
signal: AbortSignal.timeout(ACME_FETCH_TIMEOUT_MS + 5e3)
|
|
215
|
-
});
|
|
216
|
-
} catch (err) {
|
|
217
|
-
lastErr = err;
|
|
218
|
-
if (attempt < ACME_FETCH_RETRIES) {
|
|
219
|
-
log.warn({
|
|
220
|
-
url,
|
|
221
|
-
attempt,
|
|
222
|
-
err
|
|
223
|
-
}, "ACME fetch failed, retrying");
|
|
224
|
-
await sleep(2e3 * attempt);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
228
|
-
}
|
|
229
|
-
async function acmeFetchJson(url, init) {
|
|
230
|
-
const res = await acmeFetch(url, init);
|
|
231
|
-
const replayNonce = res.headers.get("replay-nonce") ?? void 0;
|
|
232
|
-
const location = res.headers.get("location");
|
|
233
|
-
if (!res.ok) {
|
|
234
|
-
const body = await res.text().catch(() => "");
|
|
235
|
-
throw new Error(`ACME HTTP ${res.status} ${url}: ${body.slice(0, 300)}`);
|
|
236
|
-
}
|
|
237
|
-
if (res.status === 204) return {
|
|
238
|
-
data: {},
|
|
239
|
-
nonce: replayNonce,
|
|
240
|
-
location
|
|
241
|
-
};
|
|
242
|
-
return {
|
|
243
|
-
data: await res.json(),
|
|
244
|
-
nonce: replayNonce,
|
|
245
|
-
location
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
async function acmeSignedPost(url, account, nonce, payload) {
|
|
249
|
-
const useKid = account.url.startsWith("http");
|
|
250
|
-
const jws = signAcmeJws({
|
|
251
|
-
privateKeyPem: account.keyPem,
|
|
252
|
-
url,
|
|
253
|
-
nonce,
|
|
254
|
-
payload,
|
|
255
|
-
kid: useKid ? account.url : void 0,
|
|
256
|
-
jwk: useKid ? void 0 : account.jwk
|
|
257
|
-
});
|
|
258
|
-
return acmeFetchJson(url, {
|
|
259
|
-
method: "POST",
|
|
260
|
-
headers: { "Content-Type": "application/jose+json" },
|
|
261
|
-
body: JSON.stringify(jws)
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
async function downloadCertificate(certUrl, account, directory) {
|
|
265
|
-
const nonce = await getNonce(directory);
|
|
266
|
-
const jws = signAcmeJws({
|
|
267
|
-
privateKeyPem: account.keyPem,
|
|
268
|
-
url: certUrl,
|
|
269
|
-
nonce,
|
|
270
|
-
payload: null,
|
|
271
|
-
kid: account.url
|
|
272
|
-
});
|
|
273
|
-
const res = await acmeFetch(certUrl, {
|
|
274
|
-
method: "POST",
|
|
275
|
-
headers: {
|
|
276
|
-
"Content-Type": "application/jose+json",
|
|
277
|
-
Accept: "application/pem-certificate-chain"
|
|
278
|
-
},
|
|
279
|
-
body: JSON.stringify(jws)
|
|
280
|
-
});
|
|
281
|
-
if (!res.ok) {
|
|
282
|
-
const body = await res.text().catch(() => "");
|
|
283
|
-
throw new Error(`ACME cert download failed: ${res.status} ${body.slice(0, 200)}`);
|
|
284
|
-
}
|
|
285
|
-
return res.text();
|
|
286
|
-
}
|
|
287
|
-
async function getDirectory(staging) {
|
|
288
|
-
const { data } = await acmeFetchJson(staging ? ACME_DIRECTORY.staging : ACME_DIRECTORY.production);
|
|
289
|
-
return data;
|
|
290
|
-
}
|
|
291
|
-
async function getNonce(directory) {
|
|
292
|
-
const nonce = (await acmeFetch(directory.newNonce, { method: "HEAD" })).headers.get("replay-nonce");
|
|
293
|
-
if (!nonce) throw new Error("ACME CA did not return replay-nonce");
|
|
294
|
-
return nonce;
|
|
295
|
-
}
|
|
296
|
-
async function ensureAccount(directory, keyPem, staging) {
|
|
297
|
-
const jwk = exportJwkFromPrivateKeyPem(keyPem);
|
|
298
|
-
let accountUrl = loadAccountUrl(staging);
|
|
299
|
-
if (accountUrl && !accountUrlMatchesCa(accountUrl, staging)) accountUrl = "";
|
|
300
|
-
const nonce = await getNonce(directory);
|
|
301
|
-
const result = await acmeSignedPost(directory.newAccount, {
|
|
302
|
-
url: "new",
|
|
303
|
-
jwk,
|
|
304
|
-
keyPem
|
|
305
|
-
}, nonce, { termsOfServiceAgreed: true });
|
|
306
|
-
if (result.location) {
|
|
307
|
-
accountUrl = result.location;
|
|
308
|
-
saveAccountUrl(staging, accountUrl);
|
|
309
|
-
}
|
|
310
|
-
if (!accountUrl) throw new Error("ACME account registration failed (no account URL)");
|
|
311
|
-
return {
|
|
312
|
-
url: accountUrl,
|
|
313
|
-
jwk,
|
|
314
|
-
keyPem
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
async function pollChallengeReady(challengeUrl, account, directory, challengeFqdn) {
|
|
318
|
-
for (let i = 0; i < 30; i++) {
|
|
319
|
-
await sleep(i === 0 ? 5e3 : 2e3);
|
|
320
|
-
const { data } = await acmeSignedPost(challengeUrl, account, await getNonce(directory), null);
|
|
321
|
-
if (data.status === "valid") return;
|
|
322
|
-
if (data.status === "invalid") {
|
|
323
|
-
const message = formatAcmeDnsChallengeInvalidError(challengeFqdn, data);
|
|
324
|
-
log.error({
|
|
325
|
-
challengeFqdn,
|
|
326
|
-
error: data.error,
|
|
327
|
-
validationRecord: data.validationRecord
|
|
328
|
-
}, message);
|
|
329
|
-
throw new Error(message);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
throw new Error(`ACME DNS-01 challenge timed out for ${challengeFqdn}`);
|
|
333
|
-
}
|
|
334
|
-
async function pollOrderValid(orderUrl, account, directory) {
|
|
335
|
-
for (let i = 0; i < 30; i++) {
|
|
336
|
-
const { data } = await acmeSignedPost(orderUrl, account, await getNonce(directory), null);
|
|
337
|
-
if (data.status === "valid" && data.certificate) return data.certificate;
|
|
338
|
-
if (data.status === "invalid") throw new Error("ACME order invalid");
|
|
339
|
-
await sleep(2e3);
|
|
340
|
-
}
|
|
341
|
-
throw new Error("ACME order finalize timed out");
|
|
342
|
-
}
|
|
343
|
-
async function cleanupDnsChallengeRecord(broker, tunnelId, tunnelToken, recordId) {
|
|
344
|
-
await broker.cleanupDnsChallenge({
|
|
345
|
-
tunnelId,
|
|
346
|
-
tunnelToken,
|
|
347
|
-
recordId
|
|
348
|
-
}).catch((err) => {
|
|
349
|
-
log.warn({
|
|
350
|
-
err,
|
|
351
|
-
recordId
|
|
352
|
-
}, "DNS cleanup failed (non-critical)");
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
async function runDns01Attempt(params) {
|
|
356
|
-
const { config, directory, account, domain, challengeFqdn } = params;
|
|
357
|
-
let nonce = await getNonce(directory);
|
|
358
|
-
const orderResult = await acmeSignedPost(directory.newOrder, account, nonce, { identifiers: [{
|
|
359
|
-
type: "dns",
|
|
360
|
-
value: domain
|
|
361
|
-
}] });
|
|
362
|
-
const orderUrl = orderResult.location;
|
|
363
|
-
if (!orderUrl) throw new Error("ACME newOrder missing order URL");
|
|
364
|
-
const authzUrl = orderResult.data.authorizations?.[0];
|
|
365
|
-
if (!authzUrl) throw new Error("ACME order missing authorization");
|
|
366
|
-
nonce = await getNonce(directory);
|
|
367
|
-
const challenge = (await acmeSignedPost(authzUrl, account, nonce, null)).data.challenges?.find((c) => c.type === "dns-01");
|
|
368
|
-
if (!challenge) throw new Error("No DNS-01 challenge offered by CA");
|
|
369
|
-
const thumbprint = jwkThumbprint(account.jwk);
|
|
370
|
-
const keyAuth = `${challenge.token}.${thumbprint}`;
|
|
371
|
-
const txtValue = base64url(createHash("sha256").update(keyAuth).digest());
|
|
372
|
-
log.info({
|
|
373
|
-
fqdn: challengeFqdn,
|
|
374
|
-
txtPreview: `${txtValue.slice(0, 8)}…`
|
|
375
|
-
}, "Setting DNS-01 challenge via Broker");
|
|
376
|
-
config.onProgress?.("dns_challenge");
|
|
377
|
-
const { recordId, fqdn } = await config.broker.setDnsChallenge({
|
|
378
|
-
tunnelId: config.tunnelId,
|
|
379
|
-
tunnelToken: config.tunnelToken,
|
|
380
|
-
subdomain: config.subdomain,
|
|
381
|
-
txtValue
|
|
382
|
-
});
|
|
383
|
-
if (normalizeFqdn(fqdn) !== normalizeFqdn(challengeFqdn)) log.warn({
|
|
384
|
-
brokerFqdn: fqdn,
|
|
385
|
-
challengeFqdn,
|
|
386
|
-
phase: "acme_dns_fqdn_mismatch"
|
|
387
|
-
}, "Broker returned unexpected ACME challenge FQDN — polling canonical name for Let's Encrypt");
|
|
388
|
-
try {
|
|
389
|
-
config.onProgress?.("dns_propagation");
|
|
390
|
-
await waitForDnsTxtConsensus(challengeFqdn, txtValue);
|
|
391
|
-
nonce = await getNonce(directory);
|
|
392
|
-
await acmeSignedPost(challenge.url, account, nonce, {});
|
|
393
|
-
config.onProgress?.("ca_validation");
|
|
394
|
-
await pollChallengeReady(challenge.url, account, directory, challengeFqdn);
|
|
395
|
-
const { csrDer, keyPem } = generateDomainCsr(domain);
|
|
396
|
-
const finalizeUrl = orderResult.data.finalize;
|
|
397
|
-
if (!finalizeUrl) throw new Error("ACME order missing finalize URL");
|
|
398
|
-
config.onProgress?.("issuing");
|
|
399
|
-
nonce = await getNonce(directory);
|
|
400
|
-
await acmeSignedPost(finalizeUrl, account, nonce, { csr: base64url(csrDer) });
|
|
401
|
-
const certPem = await downloadCertificate(await pollOrderValid(orderUrl, account, directory), account, directory);
|
|
402
|
-
const firstCert = certPem.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/)?.[0];
|
|
403
|
-
if (!firstCert) throw new Error("ACME certificate PEM parse failed");
|
|
404
|
-
const expiresAt = getCertExpiryFromPem(firstCert);
|
|
405
|
-
log.info({
|
|
406
|
-
domain,
|
|
407
|
-
expiresAt: expiresAt.toISOString()
|
|
408
|
-
}, "Certificate issued");
|
|
409
|
-
await cleanupDnsChallengeRecord(config.broker, config.tunnelId, config.tunnelToken, recordId);
|
|
410
|
-
return {
|
|
411
|
-
recordId,
|
|
412
|
-
result: {
|
|
413
|
-
certPem: certPem.trim(),
|
|
414
|
-
keyPem,
|
|
415
|
-
domain,
|
|
416
|
-
expiresAt,
|
|
417
|
-
issuedAt: /* @__PURE__ */ new Date()
|
|
418
|
-
}
|
|
419
|
-
};
|
|
420
|
-
} catch (err) {
|
|
421
|
-
await cleanupDnsChallengeRecord(config.broker, config.tunnelId, config.tunnelToken, recordId);
|
|
422
|
-
throw err;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
async function requestCertificate(config) {
|
|
426
|
-
const staging = config.staging ?? false;
|
|
427
|
-
const domain = resolveCertDomain(config.subdomain, config.frpSubdomainHost);
|
|
428
|
-
const challengeFqdn = resolveAcmeChallengeFqdn(domain);
|
|
429
|
-
log.info({
|
|
430
|
-
domain,
|
|
431
|
-
staging
|
|
432
|
-
}, "Starting ACME certificate request");
|
|
433
|
-
config.onProgress?.("checking");
|
|
434
|
-
const directory = await getDirectory(staging);
|
|
435
|
-
const account = await ensureAccount(directory, loadAccountKeyPem(), staging);
|
|
436
|
-
let lastErr;
|
|
437
|
-
for (let attempt = 1; attempt <= ACME_DNS_ATTEMPTS; attempt++) try {
|
|
438
|
-
if (attempt > 1) {
|
|
439
|
-
log.warn({
|
|
440
|
-
domain,
|
|
441
|
-
attempt,
|
|
442
|
-
phase: "acme_dns_retry"
|
|
443
|
-
}, "Retrying ACME DNS-01 after propagation delay");
|
|
444
|
-
await sleep(ACME_DNS_RETRY_DELAY_MS);
|
|
445
|
-
}
|
|
446
|
-
const { result } = await runDns01Attempt({
|
|
447
|
-
config,
|
|
448
|
-
directory,
|
|
449
|
-
account,
|
|
450
|
-
domain,
|
|
451
|
-
challengeFqdn
|
|
452
|
-
});
|
|
453
|
-
return result;
|
|
454
|
-
} catch (err) {
|
|
455
|
-
lastErr = err;
|
|
456
|
-
if (attempt < ACME_DNS_ATTEMPTS && isRetryableAcmeDnsError(err)) {
|
|
457
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
458
|
-
log.warn({
|
|
459
|
-
domain,
|
|
460
|
-
attempt,
|
|
461
|
-
errorMessage,
|
|
462
|
-
phase: "acme_dns_retryable"
|
|
463
|
-
}, `ACME DNS-01 failed, will retry: ${errorMessage}`);
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
throw err;
|
|
467
|
-
}
|
|
468
|
-
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
469
|
-
}
|
|
470
|
-
//#endregion
|
|
471
|
-
export { ACME_DNS_RESOLVERS, allAcmeDnsResolversHaveTxt, formatAcmeDnsChallengeInvalidError, isRetryableAcmeDnsError, probeAcmeDnsTxtAllResolvers, requestCertificate, resolveAcmeChallengeFqdn, waitForDnsTxtConsensus };
|
|
472
|
-
|
|
473
|
-
//# sourceMappingURL=acme-client.js.map
|
|
@@ -1 +0,0 @@
|
|
|
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. */\nexport const ACME_DNS_RESOLVERS = ['8.8.8.8', '1.1.1.1', '9.9.9.9'] as const;\n\n/** Consecutive consensus rounds required before trusting DNS propagation. */\nconst ACME_DNS_CONSENSUS_ROUNDS = 2;\n\n/** Minimum time after publishing TXT before accepting consensus (Dynadot ~60s). */\nconst ACME_DNS_MIN_PROPAGATION_MS = 30_000;\n\n/** Hold consensus this long before submitting (LE secondary validation). */\nconst ACME_DNS_STABLE_MS = 30_000;\n\n/** Poll interval while waiting for DNS. */\nconst ACME_DNS_POLL_INTERVAL_MS = 5_000;\n\n/** Max time to wait for DNS consensus (first + stable phases). */\nconst ACME_DNS_TIMEOUT_MS = 240_000;\n\n/** Retry whole DNS-01 attempt when LE reports secondary validation failures. */\nconst ACME_DNS_ATTEMPTS = 2;\n\n/** Pause between ACME DNS-01 retries. */\nconst ACME_DNS_RETRY_DELAY_MS = 45_000;\n\nexport type AcmeDnsTxtProbe = {\n resolver: string;\n values: string[];\n error?: string;\n};\n\nasync function resolveAcmeDnsTxtFromResolver(fqdn: string, resolver: string): Promise<string[]> {\n const dns = new Resolver();\n dns.setServers([resolver]);\n const records = await dns.resolveTxt(fqdn);\n return records.map((parts) => parts.join(''));\n}\n\n/** Query each public resolver independently (Node tries setServers in order — not sufficient for LE). */\nexport async function probeAcmeDnsTxtAllResolvers(fqdn: string): Promise<AcmeDnsTxtProbe[]> {\n return Promise.all(\n ACME_DNS_RESOLVERS.map(async (resolver) => {\n try {\n const values = await resolveAcmeDnsTxtFromResolver(fqdn, resolver);\n return { resolver, values };\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err);\n return { resolver, values: [] as string[], error: errorMessage };\n }\n }),\n );\n}\n\nexport function allAcmeDnsResolversHaveTxt(probes: AcmeDnsTxtProbe[], expectedValue: string): boolean {\n return (\n probes.length > 0 &&\n probes.every((probe) => probe.values.some((value) => value === expectedValue))\n );\n}\n\nfunction summarizeAcmeDnsProbes(probes: AcmeDnsTxtProbe[]): string {\n return probes\n .map((probe) => {\n const seen = probe.values.join(', ') || probe.error || 'none';\n return `${probe.resolver}=${seen}`;\n })\n .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\nfunction formatValidationRecordEntry(entry: unknown): string {\n if (typeof entry === 'string') return entry;\n if (entry && typeof entry === 'object') {\n const obj = entry as Record<string, unknown>;\n if (typeof obj.detail === 'string') return obj.detail;\n const hostname = typeof obj.hostname === 'string' ? obj.hostname : '';\n const error = typeof obj.error === 'string' ? obj.error : '';\n if (hostname && error) return `${hostname}: ${error}`;\n if (error) return error;\n try {\n return JSON.stringify(entry);\n } catch {\n return String(entry);\n }\n }\n return String(entry);\n}\n\nexport function formatAcmeDnsChallengeInvalidError(\n fqdn: string,\n data: { error?: { detail?: string; type?: string }; validationRecord?: unknown[] },\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(\n `validation: ${data.validationRecord.map(formatValidationRecordEntry).join('; ')}`,\n );\n }\n return parts.join(' — ');\n}\n\nexport function isRetryableAcmeDnsError(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n return (\n /secondary validation/i.test(message) ||\n /DNS TXT not visible on all resolvers/i.test(message) ||\n /ACME DNS-01 challenge timed out/i.test(message)\n );\n}\n\nexport type WaitForDnsTxtOptions = {\n minPropagationMs?: number;\n stableMs?: number;\n timeoutMs?: number;\n consensusRounds?: number;\n pollIntervalMs?: number;\n probe?: (fqdn: string) => Promise<AcmeDnsTxtProbe[]>;\n sleep?: (ms: number) => Promise<void>;\n};\n\n/** Poll public resolvers until TXT is stable — no blind initial sleep. */\nexport async function waitForDnsTxtConsensus(\n fqdn: string,\n expectedValue: string,\n opts?: WaitForDnsTxtOptions,\n): Promise<void> {\n const minPropagationMs = opts?.minPropagationMs ?? ACME_DNS_MIN_PROPAGATION_MS;\n const stableMs = opts?.stableMs ?? ACME_DNS_STABLE_MS;\n const timeoutMs = opts?.timeoutMs ?? ACME_DNS_TIMEOUT_MS;\n const consensusRounds = opts?.consensusRounds ?? ACME_DNS_CONSENSUS_ROUNDS;\n const pollIntervalMs = opts?.pollIntervalMs ?? ACME_DNS_POLL_INTERVAL_MS;\n const probe = opts?.probe ?? probeAcmeDnsTxtAllResolvers;\n const delay = opts?.sleep ?? sleep;\n\n const startedAt = Date.now();\n const deadline = startedAt + timeoutMs;\n let lastProbes: AcmeDnsTxtProbe[] = [];\n let consensusStreak = 0;\n let stableSince: number | null = null;\n\n while (Date.now() < deadline) {\n lastProbes = await probe(fqdn);\n const elapsedMs = Date.now() - startedAt;\n const hasConsensus =\n elapsedMs >= minPropagationMs &&\n allAcmeDnsResolversHaveTxt(lastProbes, expectedValue);\n\n if (hasConsensus) {\n consensusStreak += 1;\n if (consensusStreak >= consensusRounds) {\n if (stableMs <= 0) {\n log.info(\n {\n fqdn,\n resolvers: ACME_DNS_RESOLVERS,\n elapsedMs,\n probes: summarizeAcmeDnsProbes(lastProbes),\n },\n 'DNS-01 TXT visible on all public resolvers',\n );\n return;\n }\n if (stableSince === null) stableSince = Date.now();\n if (Date.now() - stableSince >= stableMs) {\n const finalProbes = await probe(fqdn);\n if (allAcmeDnsResolversHaveTxt(finalProbes, expectedValue)) {\n log.info(\n {\n fqdn,\n resolvers: ACME_DNS_RESOLVERS,\n elapsedMs: Date.now() - startedAt,\n stableMs,\n probes: summarizeAcmeDnsProbes(finalProbes),\n },\n 'DNS-01 TXT stable on all public resolvers',\n );\n return;\n }\n stableSince = null;\n consensusStreak = 0;\n }\n }\n } else {\n consensusStreak = 0;\n stableSince = null;\n }\n await delay(pollIntervalMs);\n }\n\n throw new Error(\n `DNS TXT not visible on all resolvers for ${fqdn} (expected ${expectedValue}; last probe: ${summarizeAcmeDnsProbes(lastProbes)})`,\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?: unknown[];\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\nasync function cleanupDnsChallengeRecord(\n broker: TunnelBrokerClient,\n tunnelId: string,\n tunnelToken: string,\n recordId: string,\n): Promise<void> {\n await broker\n .cleanupDnsChallenge({ tunnelId, tunnelToken, recordId })\n .catch((err) => {\n log.warn({ err, recordId }, 'DNS cleanup failed (non-critical)');\n });\n}\n\nasync function runDns01Attempt(params: {\n config: AcmeConfig;\n directory: AcmeDirectory;\n account: AcmeAccount;\n domain: string;\n challengeFqdn: string;\n}): Promise<{ result: AcmeCertResult; recordId: string }> {\n const { config, directory, account, domain, challengeFqdn } = params;\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: 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 waitForDnsTxtConsensus(challengeFqdn, txtValue);\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 await cleanupDnsChallengeRecord(config.broker, config.tunnelId, config.tunnelToken, recordId);\n\n return {\n recordId,\n result: {\n certPem: certPem.trim(),\n keyPem,\n domain,\n expiresAt,\n issuedAt: new Date(),\n },\n };\n } catch (err) {\n await cleanupDnsChallengeRecord(config.broker, config.tunnelId, config.tunnelToken, recordId);\n throw err;\n }\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 const challengeFqdn = resolveAcmeChallengeFqdn(domain);\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 lastErr: unknown;\n for (let attempt = 1; attempt <= ACME_DNS_ATTEMPTS; attempt++) {\n try {\n if (attempt > 1) {\n log.warn({ domain, attempt, phase: 'acme_dns_retry' }, 'Retrying ACME DNS-01 after propagation delay');\n await sleep(ACME_DNS_RETRY_DELAY_MS);\n }\n const { result } = await runDns01Attempt({\n config,\n directory,\n account,\n domain,\n challengeFqdn,\n });\n return result;\n } catch (err) {\n lastErr = err;\n if (attempt < ACME_DNS_ATTEMPTS && isRetryableAcmeDnsError(err)) {\n const errorMessage = err instanceof Error ? err.message : String(err);\n log.warn({ domain, attempt, errorMessage, phase: 'acme_dns_retryable' }, `ACME DNS-01 failed, will retry: ${errorMessage}`);\n continue;\n }\n throw err;\n }\n }\n\n throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));\n}\n"],"mappings":"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"}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export type EcPublicJwk = {
|
|
2
|
-
kty: 'EC';
|
|
3
|
-
crv: string;
|
|
4
|
-
x: string;
|
|
5
|
-
y: string;
|
|
6
|
-
};
|
|
7
|
-
export declare function base64url(input: Buffer | string): string;
|
|
8
|
-
export declare function ensureEcAccountKeyPem(existingPem?: string): string;
|
|
9
|
-
export declare function exportJwkFromPrivateKeyPem(privateKeyPem: string): EcPublicJwk;
|
|
10
|
-
/** RFC 7638 JWK thumbprint for ACME account key. */
|
|
11
|
-
export declare function jwkThumbprint(jwk: EcPublicJwk): string;
|
|
12
|
-
export type AcmeJwsParts = {
|
|
13
|
-
protected: string;
|
|
14
|
-
payload: string;
|
|
15
|
-
signature: string;
|
|
16
|
-
};
|
|
17
|
-
export declare function signAcmeJws(params: {
|
|
18
|
-
privateKeyPem: string;
|
|
19
|
-
url: string;
|
|
20
|
-
nonce: string;
|
|
21
|
-
payload: unknown;
|
|
22
|
-
jwk?: EcPublicJwk;
|
|
23
|
-
kid?: string;
|
|
24
|
-
}): AcmeJwsParts;
|
|
25
|
-
export declare function getCertExpiryFromPem(certPem: string): Date;
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { X509Certificate, createHash, createPrivateKey, createPublicKey, createSign, generateKeyPairSync } from "node:crypto";
|
|
2
|
-
//#region src/tunnel/acme-crypto.ts
|
|
3
|
-
function base64url(input) {
|
|
4
|
-
return (typeof input === "string" ? Buffer.from(input, "utf8") : input).toString("base64url");
|
|
5
|
-
}
|
|
6
|
-
function ensureEcAccountKeyPem(existingPem) {
|
|
7
|
-
if (existingPem?.trim()) return existingPem;
|
|
8
|
-
const { privateKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
9
|
-
return privateKey.export({
|
|
10
|
-
type: "pkcs8",
|
|
11
|
-
format: "pem"
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
function exportJwkFromPrivateKeyPem(privateKeyPem) {
|
|
15
|
-
const jwk = createPublicKey(createPrivateKey(privateKeyPem)).export({ format: "jwk" });
|
|
16
|
-
if (jwk.kty !== "EC") throw new Error("Expected EC account key");
|
|
17
|
-
return jwk;
|
|
18
|
-
}
|
|
19
|
-
/** RFC 7638 JWK thumbprint for ACME account key. */
|
|
20
|
-
function jwkThumbprint(jwk) {
|
|
21
|
-
const ordered = {
|
|
22
|
-
crv: jwk.crv,
|
|
23
|
-
kty: jwk.kty,
|
|
24
|
-
x: jwk.x,
|
|
25
|
-
y: jwk.y
|
|
26
|
-
};
|
|
27
|
-
return base64url(createHash("sha256").update(JSON.stringify(ordered)).digest());
|
|
28
|
-
}
|
|
29
|
-
function signAcmeJws(params) {
|
|
30
|
-
const payloadStr = params.payload === null ? "" : JSON.stringify(params.payload);
|
|
31
|
-
const headerObj = {
|
|
32
|
-
alg: "ES256",
|
|
33
|
-
url: params.url,
|
|
34
|
-
nonce: params.nonce
|
|
35
|
-
};
|
|
36
|
-
if (params.kid) headerObj.kid = params.kid;
|
|
37
|
-
else if (params.jwk) headerObj.jwk = params.jwk;
|
|
38
|
-
const protectedB64 = base64url(JSON.stringify(headerObj));
|
|
39
|
-
const payloadB64 = payloadStr === "" ? "" : base64url(payloadStr);
|
|
40
|
-
const signingInput = payloadB64 === "" ? `${protectedB64}.` : `${protectedB64}.${payloadB64}`;
|
|
41
|
-
const key = createPrivateKey(params.privateKeyPem);
|
|
42
|
-
return {
|
|
43
|
-
protected: protectedB64,
|
|
44
|
-
payload: payloadB64,
|
|
45
|
-
signature: base64url(createSign("SHA256").update(signingInput).sign({
|
|
46
|
-
key,
|
|
47
|
-
dsaEncoding: "ieee-p1363"
|
|
48
|
-
}))
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
function getCertExpiryFromPem(certPem) {
|
|
52
|
-
const cert = new X509Certificate(certPem);
|
|
53
|
-
return new Date(cert.validTo);
|
|
54
|
-
}
|
|
55
|
-
//#endregion
|
|
56
|
-
export { base64url, ensureEcAccountKeyPem, exportJwkFromPrivateKeyPem, getCertExpiryFromPem, jwkThumbprint, signAcmeJws };
|
|
57
|
-
|
|
58
|
-
//# sourceMappingURL=acme-crypto.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"acme-crypto.js","names":[],"sources":["../../../src/tunnel/acme-crypto.ts"],"sourcesContent":["import {\n createHash,\n createPrivateKey,\n createPublicKey,\n createSign,\n generateKeyPairSync,\n X509Certificate,\n} from 'node:crypto';\n\nexport type EcPublicJwk = {\n kty: 'EC';\n crv: string;\n x: string;\n y: string;\n};\n\nexport function base64url(input: Buffer | string): string {\n const buf = typeof input === 'string' ? Buffer.from(input, 'utf8') : input;\n return buf.toString('base64url');\n}\n\nexport function ensureEcAccountKeyPem(existingPem?: string): string {\n if (existingPem?.trim()) return existingPem;\n const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' });\n return privateKey.export({ type: 'pkcs8', format: 'pem' }) as string;\n}\n\nexport function exportJwkFromPrivateKeyPem(privateKeyPem: string): EcPublicJwk {\n const jwk = createPublicKey(createPrivateKey(privateKeyPem)).export({ format: 'jwk' }) as EcPublicJwk;\n if (jwk.kty !== 'EC') throw new Error('Expected EC account key');\n return jwk;\n}\n\n/** RFC 7638 JWK thumbprint for ACME account key. */\nexport function jwkThumbprint(jwk: EcPublicJwk): string {\n const ordered = {\n crv: jwk.crv,\n kty: jwk.kty,\n x: jwk.x,\n y: jwk.y,\n };\n return base64url(createHash('sha256').update(JSON.stringify(ordered)).digest());\n}\n\nexport type AcmeJwsParts = {\n protected: string;\n payload: string;\n signature: string;\n};\n\nexport function signAcmeJws(params: {\n privateKeyPem: string;\n url: string;\n nonce: string;\n payload: unknown;\n jwk?: EcPublicJwk;\n kid?: string;\n}): AcmeJwsParts {\n const payloadStr = params.payload === null ? '' : JSON.stringify(params.payload);\n const headerObj: Record<string, unknown> = {\n alg: 'ES256',\n url: params.url,\n nonce: params.nonce,\n };\n if (params.kid) headerObj.kid = params.kid;\n else if (params.jwk) headerObj.jwk = params.jwk;\n\n const protectedB64 = base64url(JSON.stringify(headerObj));\n const payloadB64 = payloadStr === '' ? '' : base64url(payloadStr);\n const signingInput = payloadB64 === '' ? `${protectedB64}.` : `${protectedB64}.${payloadB64}`;\n\n const key = createPrivateKey(params.privateKeyPem);\n const signature = createSign('SHA256')\n .update(signingInput)\n .sign({ key, dsaEncoding: 'ieee-p1363' });\n\n return {\n protected: protectedB64,\n payload: payloadB64,\n signature: base64url(signature),\n };\n}\n\nexport function getCertExpiryFromPem(certPem: string): Date {\n const cert = new X509Certificate(certPem);\n return new Date(cert.validTo);\n}\n"],"mappings":";;AAgBA,SAAgB,UAAU,OAAgC;AAExD,SADY,OAAO,UAAU,WAAW,OAAO,KAAK,OAAO,OAAO,GAAG,OAC1D,SAAS,YAAY;;AAGlC,SAAgB,sBAAsB,aAA8B;AAClE,KAAI,aAAa,MAAM,CAAE,QAAO;CAChC,MAAM,EAAE,eAAe,oBAAoB,MAAM,EAAE,YAAY,SAAS,CAAC;AACzE,QAAO,WAAW,OAAO;EAAE,MAAM;EAAS,QAAQ;EAAO,CAAC;;AAG5D,SAAgB,2BAA2B,eAAoC;CAC7E,MAAM,MAAM,gBAAgB,iBAAiB,cAAc,CAAC,CAAC,OAAO,EAAE,QAAQ,OAAO,CAAC;AACtF,KAAI,IAAI,QAAQ,KAAM,OAAM,IAAI,MAAM,0BAA0B;AAChE,QAAO;;;AAIT,SAAgB,cAAc,KAA0B;CACtD,MAAM,UAAU;EACd,KAAK,IAAI;EACT,KAAK,IAAI;EACT,GAAG,IAAI;EACP,GAAG,IAAI;EACR;AACD,QAAO,UAAU,WAAW,SAAS,CAAC,OAAO,KAAK,UAAU,QAAQ,CAAC,CAAC,QAAQ,CAAC;;AASjF,SAAgB,YAAY,QAOX;CACf,MAAM,aAAa,OAAO,YAAY,OAAO,KAAK,KAAK,UAAU,OAAO,QAAQ;CAChF,MAAM,YAAqC;EACzC,KAAK;EACL,KAAK,OAAO;EACZ,OAAO,OAAO;EACf;AACD,KAAI,OAAO,IAAK,WAAU,MAAM,OAAO;UAC9B,OAAO,IAAK,WAAU,MAAM,OAAO;CAE5C,MAAM,eAAe,UAAU,KAAK,UAAU,UAAU,CAAC;CACzD,MAAM,aAAa,eAAe,KAAK,KAAK,UAAU,WAAW;CACjE,MAAM,eAAe,eAAe,KAAK,GAAG,aAAa,KAAK,GAAG,aAAa,GAAG;CAEjF,MAAM,MAAM,iBAAiB,OAAO,cAAc;AAKlD,QAAO;EACL,WAAW;EACX,SAAS;EACT,WAAW,UAPK,WAAW,SAAS,CACnC,OAAO,aAAa,CACpB,KAAK;GAAE;GAAK,aAAa;GAAc,CAKV,CAAC;EAChC;;AAGH,SAAgB,qBAAqB,SAAuB;CAC1D,MAAM,OAAO,IAAI,gBAAgB,QAAQ;AACzC,QAAO,IAAI,KAAK,KAAK,QAAQ"}
|