@rine-network/core 0.6.0 → 0.6.1
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/index.js
CHANGED
|
@@ -2101,7 +2101,10 @@ async function issueCert(args) {
|
|
|
2101
2101
|
const setResult = await deps.dnsPost("set", order.dnsChallengeToken);
|
|
2102
2102
|
txtSet = true;
|
|
2103
2103
|
const fqdn = setResult.fqdn ?? `_acme-challenge.${delegationId}.hook.rine.network`;
|
|
2104
|
-
await deps.waitForPropagation({
|
|
2104
|
+
await deps.waitForPropagation({
|
|
2105
|
+
fqdn,
|
|
2106
|
+
expectedValue: order.dnsChallengeToken
|
|
2107
|
+
});
|
|
2105
2108
|
await deps.acme.validate({ handle });
|
|
2106
2109
|
const { certPem } = await deps.acme.finalizeAndDownload({
|
|
2107
2110
|
handle,
|
|
@@ -2198,6 +2201,7 @@ function describe(err) {
|
|
|
2198
2201
|
}
|
|
2199
2202
|
//#endregion
|
|
2200
2203
|
//#region src/funnel/propagation.ts
|
|
2204
|
+
const INITIAL_PROPAGATION_DELAY_MS = 45e3;
|
|
2201
2205
|
const PROPAGATION_TIMEOUT_MS = 12e4;
|
|
2202
2206
|
const PROPAGATION_INTERVAL_MS = 5e3;
|
|
2203
2207
|
function sleep(ms) {
|
|
@@ -2210,12 +2214,13 @@ function sleep(ms) {
|
|
|
2210
2214
|
* asking for NS until a parent answers (the apex — e.g. `rine.network`). A plain
|
|
2211
2215
|
* `resolveSoa(fqdn)` returns ENODATA at a sub-apex name, which is why the prior
|
|
2212
2216
|
* SOA->NS approach silently fell through to the host resolver every time.
|
|
2217
|
+
* Exported (with an injectable `nsResolve`) so the climb is unit-tested directly.
|
|
2213
2218
|
*/
|
|
2214
|
-
async function nsHostsForZoneOf(fqdn) {
|
|
2219
|
+
async function nsHostsForZoneOf(fqdn, nsResolve = resolveNs) {
|
|
2215
2220
|
let name = fqdn.replace(/\.$/, "");
|
|
2216
2221
|
while (name.includes(".")) {
|
|
2217
2222
|
try {
|
|
2218
|
-
const ns = await
|
|
2223
|
+
const ns = await nsResolve(name);
|
|
2219
2224
|
if (ns.length > 0) return ns;
|
|
2220
2225
|
} catch {}
|
|
2221
2226
|
name = name.slice(name.indexOf(".") + 1);
|
|
@@ -2224,34 +2229,54 @@ async function nsHostsForZoneOf(fqdn) {
|
|
|
2224
2229
|
}
|
|
2225
2230
|
/**
|
|
2226
2231
|
* Build a resolver pinned to the AUTHORITATIVE nameservers of `fqdn`'s zone
|
|
2227
|
-
* (NS -> A, then `Resolver.setServers`).
|
|
2228
|
-
*
|
|
2232
|
+
* (NS -> A, then `Resolver.setServers`). One flaky NS-host A lookup must NOT cost
|
|
2233
|
+
* us the others, so `Promise.allSettled` keeps every address that did resolve.
|
|
2234
|
+
* Falls back to the system resolver only if NS discovery fails or NO address
|
|
2235
|
+
* resolves (best-effort — the bounded poll still backstops). Exported (with
|
|
2236
|
+
* injectable resolvers) so the NS-walk path is unit-tested directly.
|
|
2229
2237
|
*/
|
|
2230
|
-
async function authoritativeResolver(fqdn) {
|
|
2238
|
+
async function authoritativeResolver(fqdn, nsResolve = resolveNs, a4Resolve = resolve4) {
|
|
2231
2239
|
const resolver = new Resolver();
|
|
2232
2240
|
try {
|
|
2233
|
-
const nsHosts = await nsHostsForZoneOf(fqdn);
|
|
2234
|
-
const addresses = (await Promise.
|
|
2241
|
+
const nsHosts = await nsHostsForZoneOf(fqdn, nsResolve);
|
|
2242
|
+
const addresses = (await Promise.allSettled(nsHosts.map((ns) => a4Resolve(ns)))).flatMap((r) => r.status === "fulfilled" ? r.value : []).filter(Boolean);
|
|
2235
2243
|
if (addresses.length > 0) resolver.setServers(addresses);
|
|
2236
2244
|
} catch {}
|
|
2237
2245
|
return resolver;
|
|
2238
2246
|
}
|
|
2247
|
+
/** A TXT answer satisfies the gate iff it carries THIS order's key-authorization. */
|
|
2248
|
+
function recordsMatch(records, expectedValue) {
|
|
2249
|
+
if (records.length === 0) return false;
|
|
2250
|
+
if (!expectedValue) return true;
|
|
2251
|
+
return records.some((chunks) => chunks.join("").includes(expectedValue));
|
|
2252
|
+
}
|
|
2239
2253
|
/**
|
|
2240
2254
|
* Poll the authoritative DNS for the challenge TXT at the server-returned
|
|
2241
|
-
* `_acme-challenge.<cert-domain>.hook.rine.network` until
|
|
2242
|
-
*
|
|
2243
|
-
*
|
|
2244
|
-
*
|
|
2255
|
+
* `_acme-challenge.<cert-domain>.hook.rine.network` until the record carrying
|
|
2256
|
+
* `expectedValue` (this order's key-authorization) resolves (bounded) before
|
|
2257
|
+
* telling LE to validate, so a slow zone never burns a validation attempt. The
|
|
2258
|
+
* FQDN is the one the backend wrote the TXT under (the dns-challenge `set`
|
|
2259
|
+
* response's `fqdn`) — never derived from a relay-side guess.
|
|
2260
|
+
*
|
|
2261
|
+
* Order: build the authoritative resolver (NS-walk) -> hold the grace delay ->
|
|
2262
|
+
* poll TXT. The NS-walk overlaps the grace window and the grace delay keeps the
|
|
2263
|
+
* first TXT query from preceding propagation (avoids poisoning the negative
|
|
2264
|
+
* cache — see the module header).
|
|
2245
2265
|
*/
|
|
2246
|
-
async function waitForTxtPropagation(args) {
|
|
2247
|
-
const { fqdn } = args;
|
|
2248
|
-
const
|
|
2249
|
-
const
|
|
2266
|
+
async function waitForTxtPropagation(args, opts = {}) {
|
|
2267
|
+
const { fqdn, expectedValue } = args;
|
|
2268
|
+
const initialDelayMs = opts.initialDelayMs ?? INITIAL_PROPAGATION_DELAY_MS;
|
|
2269
|
+
const timeoutMs = opts.timeoutMs ?? PROPAGATION_TIMEOUT_MS;
|
|
2270
|
+
const intervalMs = opts.intervalMs ?? PROPAGATION_INTERVAL_MS;
|
|
2271
|
+
const doSleep = opts.sleep ?? sleep;
|
|
2272
|
+
const resolver = await (opts.resolverFactory ?? ((name) => authoritativeResolver(name, opts.nsResolve, opts.a4Resolve)))(fqdn);
|
|
2273
|
+
await doSleep(initialDelayMs);
|
|
2274
|
+
const deadline = Date.now() + timeoutMs;
|
|
2250
2275
|
while (Date.now() < deadline) {
|
|
2251
2276
|
try {
|
|
2252
|
-
if ((await resolver.resolveTxt(fqdn)
|
|
2277
|
+
if (recordsMatch(await resolver.resolveTxt(fqdn), expectedValue)) return;
|
|
2253
2278
|
} catch {}
|
|
2254
|
-
await
|
|
2279
|
+
await doSleep(intervalMs);
|
|
2255
2280
|
}
|
|
2256
2281
|
throw new Error(`DNS propagation timeout for ${fqdn}`);
|
|
2257
2282
|
}
|
|
@@ -36,9 +36,14 @@ export interface IssueCertDeps {
|
|
|
36
36
|
* poll (the relay never derives it).
|
|
37
37
|
*/
|
|
38
38
|
dnsPost(action: "set" | "clear", value?: string): Promise<DnsChallengeResult>;
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Bounded poll of the authoritative DNS at `fqdn` until the TXT carrying
|
|
41
|
+
* `expectedValue` (this order's key-authorization) propagates — matching the
|
|
42
|
+
* value, not mere presence, so a stale/converging record never satisfies it.
|
|
43
|
+
*/
|
|
40
44
|
waitForPropagation(args: {
|
|
41
45
|
fqdn: string;
|
|
46
|
+
expectedValue?: string;
|
|
42
47
|
}): Promise<void>;
|
|
43
48
|
now(): number;
|
|
44
49
|
}
|
|
@@ -1,11 +1,59 @@
|
|
|
1
|
+
import { Resolver } from "node:dns/promises";
|
|
1
2
|
export declare function sleep(ms: number): Promise<void>;
|
|
3
|
+
/** Minimal TXT-resolving surface (node:dns `Resolver` satisfies this). */
|
|
4
|
+
interface TxtResolver {
|
|
5
|
+
resolveTxt(hostname: string): Promise<string[][]>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Test seams + tunables for `waitForTxtPropagation`. Production passes none — the
|
|
9
|
+
* defaults wire the real authoritative resolver and real timers. Tests inject a
|
|
10
|
+
* fake resolver + sleep so they assert ordering/timeout without touching the
|
|
11
|
+
* network or waiting the real 45s grace. `nsResolve`/`a4Resolve` let tests drive
|
|
12
|
+
* the REAL NS-walk (`authoritativeResolver`) without `resolverFactory`.
|
|
13
|
+
*/
|
|
14
|
+
export interface PropagationOptions {
|
|
15
|
+
initialDelayMs?: number;
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
intervalMs?: number;
|
|
18
|
+
sleep?: (ms: number) => Promise<void>;
|
|
19
|
+
resolverFactory?: (fqdn: string) => Promise<TxtResolver>;
|
|
20
|
+
nsResolve?: (name: string) => Promise<string[]>;
|
|
21
|
+
a4Resolve?: (host: string) => Promise<string[]>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The authoritative NS hostnames for the zone that contains `fqdn`. The
|
|
25
|
+
* challenge name (`_acme-challenge.<label>.hook.rine.network`) is BELOW the zone
|
|
26
|
+
* apex, where neither SOA nor NS records live, so we walk UP one label at a time
|
|
27
|
+
* asking for NS until a parent answers (the apex — e.g. `rine.network`). A plain
|
|
28
|
+
* `resolveSoa(fqdn)` returns ENODATA at a sub-apex name, which is why the prior
|
|
29
|
+
* SOA->NS approach silently fell through to the host resolver every time.
|
|
30
|
+
* Exported (with an injectable `nsResolve`) so the climb is unit-tested directly.
|
|
31
|
+
*/
|
|
32
|
+
export declare function nsHostsForZoneOf(fqdn: string, nsResolve?: (name: string) => Promise<string[]>): Promise<string[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Build a resolver pinned to the AUTHORITATIVE nameservers of `fqdn`'s zone
|
|
35
|
+
* (NS -> A, then `Resolver.setServers`). One flaky NS-host A lookup must NOT cost
|
|
36
|
+
* us the others, so `Promise.allSettled` keeps every address that did resolve.
|
|
37
|
+
* Falls back to the system resolver only if NS discovery fails or NO address
|
|
38
|
+
* resolves (best-effort — the bounded poll still backstops). Exported (with
|
|
39
|
+
* injectable resolvers) so the NS-walk path is unit-tested directly.
|
|
40
|
+
*/
|
|
41
|
+
export declare function authoritativeResolver(fqdn: string, nsResolve?: (name: string) => Promise<string[]>, a4Resolve?: (host: string) => Promise<string[]>): Promise<Resolver>;
|
|
2
42
|
/**
|
|
3
43
|
* Poll the authoritative DNS for the challenge TXT at the server-returned
|
|
4
|
-
* `_acme-challenge.<cert-domain>.hook.rine.network` until
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
44
|
+
* `_acme-challenge.<cert-domain>.hook.rine.network` until the record carrying
|
|
45
|
+
* `expectedValue` (this order's key-authorization) resolves (bounded) before
|
|
46
|
+
* telling LE to validate, so a slow zone never burns a validation attempt. The
|
|
47
|
+
* FQDN is the one the backend wrote the TXT under (the dns-challenge `set`
|
|
48
|
+
* response's `fqdn`) — never derived from a relay-side guess.
|
|
49
|
+
*
|
|
50
|
+
* Order: build the authoritative resolver (NS-walk) -> hold the grace delay ->
|
|
51
|
+
* poll TXT. The NS-walk overlaps the grace window and the grace delay keeps the
|
|
52
|
+
* first TXT query from preceding propagation (avoids poisoning the negative
|
|
53
|
+
* cache — see the module header).
|
|
8
54
|
*/
|
|
9
55
|
export declare function waitForTxtPropagation(args: {
|
|
10
56
|
fqdn: string;
|
|
11
|
-
|
|
57
|
+
expectedValue?: string;
|
|
58
|
+
}, opts?: PropagationOptions): Promise<void>;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|