@peerbit/server 6.0.41 → 7.0.0
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/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +161 -352
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +1 -1
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +2 -5
- package/dist/src/client.js.map +1 -1
- package/dist/src/docker.d.ts +70 -1
- package/dist/src/docker.d.ts.map +1 -1
- package/dist/src/docker.js +559 -14
- package/dist/src/docker.js.map +1 -1
- package/dist/src/domain-lease.d.ts +126 -0
- package/dist/src/domain-lease.d.ts.map +1 -0
- package/dist/src/domain-lease.js +800 -0
- package/dist/src/domain-lease.js.map +1 -0
- package/dist/src/domain.d.ts +50 -2
- package/dist/src/domain.d.ts.map +1 -1
- package/dist/src/domain.js +484 -67
- package/dist/src/domain.js.map +1 -1
- package/dist/src/hetzner.d.ts +4 -21
- package/dist/src/hetzner.d.ts.map +1 -1
- package/dist/src/hetzner.js +4 -116
- package/dist/src/hetzner.js.map +1 -1
- package/dist/src/nginx-template.conf +19 -1
- package/dist/src/remotes.browser.d.ts +4 -1
- package/dist/src/remotes.browser.d.ts.map +1 -1
- package/dist/src/remotes.browser.js +1 -2
- package/dist/src/remotes.browser.js.map +1 -1
- package/dist/src/remotes.d.ts +2 -1
- package/dist/src/remotes.d.ts.map +1 -1
- package/dist/src/remotes.js +1 -0
- package/dist/src/remotes.js.map +1 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +3 -5
- package/dist/src/server.js.map +1 -1
- package/dist/test/api.spec.js +2 -1
- package/dist/test/api.spec.js.map +1 -1
- package/dist/test/cli.spec.js +48 -1
- package/dist/test/cli.spec.js.map +1 -1
- package/dist/test/docker.spec.d.ts +2 -0
- package/dist/test/docker.spec.d.ts.map +1 -0
- package/dist/test/docker.spec.js +822 -0
- package/dist/test/docker.spec.js.map +1 -0
- package/dist/test/domain-lease.spec.d.ts +2 -0
- package/dist/test/domain-lease.spec.d.ts.map +1 -0
- package/dist/test/domain-lease.spec.js +530 -0
- package/dist/test/domain-lease.spec.js.map +1 -0
- package/dist/test/domain.integration.spec.js +358 -75
- package/dist/test/domain.integration.spec.js.map +1 -1
- package/dist/test/remotes.spec.d.ts +2 -0
- package/dist/test/remotes.spec.d.ts.map +1 -0
- package/dist/test/remotes.spec.js +15 -0
- package/dist/test/remotes.spec.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/ui/assets/index-CjZwCewh.js +10 -0
- package/dist/ui/assets/index-e3g42MbC.js +9 -0
- package/dist/ui/index.html +1 -1
- package/package.json +14 -19
- package/src/cli.ts +204 -408
- package/src/client.ts +2 -6
- package/src/docker.ts +841 -20
- package/src/domain-lease.ts +1118 -0
- package/src/domain.ts +650 -90
- package/src/hetzner.ts +4 -184
- package/src/nginx-template.conf +19 -1
- package/src/remotes.browser.ts +7 -1
- package/src/remotes.ts +6 -1
- package/src/server.ts +3 -5
- package/dist/src/aws.browser.d.ts +0 -2
- package/dist/src/aws.browser.d.ts.map +0 -1
- package/dist/src/aws.browser.js +0 -3
- package/dist/src/aws.browser.js.map +0 -1
- package/dist/src/aws.d.ts +0 -30
- package/dist/src/aws.d.ts.map +0 -1
- package/dist/src/aws.js +0 -240
- package/dist/src/aws.js.map +0 -1
- package/dist/test/launch.spec.d.ts +0 -2
- package/dist/test/launch.spec.d.ts.map +0 -1
- package/dist/test/launch.spec.js +0 -23
- package/dist/test/launch.spec.js.map +0 -1
- package/dist/ui/assets/index-BBTKOM0z.js +0 -6
- package/dist/ui/assets/index-DVtj_GOf.js +0 -13
- package/src/aws.browser.ts +0 -1
- package/src/aws.ts +0 -327
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import http from "http";
|
|
4
|
+
import { isIP } from "net";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { isPeerbitCertbotContainer, normalizeDomain } from "./domain.js";
|
|
7
|
+
export const DNS_LEASE_STATE_VERSION = 1;
|
|
8
|
+
export const DNS_LEASE_ACCESS_TOKEN_ENV = "PEERBIT_DNS_LEASE_ACCESS_TOKEN";
|
|
9
|
+
export const DNS_LEASE_SERVICE_URL_ENV = "PEERBIT_DNS_LEASE_SERVICE_URL";
|
|
10
|
+
export const DNS_LEASE_STATE_FILE_ENV = "PEERBIT_DNS_LEASE_STATE_FILE";
|
|
11
|
+
const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
12
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
13
|
+
const TOKEN_PATTERN = /^[A-Za-z0-9._~-]{32,512}$/;
|
|
14
|
+
const GENERATED_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
15
|
+
const LEASE_ID_PATTERN = /^[A-Za-z0-9_-]{8,128}$/;
|
|
16
|
+
const MANAGED_DOMAIN_PATTERN = /^p-[a-f0-9]{20}\.nodes\.peerchecker\.com$/;
|
|
17
|
+
const DNS_LEASE_CHALLENGE_PROXY_PORT = 8093;
|
|
18
|
+
const DNS_LEASE_CHALLENGE_PROXY_MARKER = `proxy_pass http://127.0.0.1:${DNS_LEASE_CHALLENGE_PROXY_PORT};`;
|
|
19
|
+
class BearerAuthorization {
|
|
20
|
+
token;
|
|
21
|
+
constructor(token) {
|
|
22
|
+
this.token = token;
|
|
23
|
+
if (!TOKEN_PATTERN.test(token)) {
|
|
24
|
+
throw new Error("DNS lease authorization token is invalid");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
apply(headers) {
|
|
28
|
+
headers.Authorization = `Bearer ${this.token}`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const normalizeServiceUrl = (input) => {
|
|
32
|
+
let url;
|
|
33
|
+
try {
|
|
34
|
+
url = new URL(input);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error("DNS lease service URL is invalid");
|
|
38
|
+
}
|
|
39
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
40
|
+
throw new Error("DNS lease service URL must not contain credentials or query data");
|
|
41
|
+
}
|
|
42
|
+
const localHttp = url.protocol === "http:" &&
|
|
43
|
+
["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
44
|
+
if (url.protocol !== "https:" && !localHttp) {
|
|
45
|
+
throw new Error("DNS lease service URL must use HTTPS");
|
|
46
|
+
}
|
|
47
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
48
|
+
return `${url.origin}${url.pathname}`;
|
|
49
|
+
};
|
|
50
|
+
const normalizeAddress = (address, recordType) => {
|
|
51
|
+
const normalized = address.trim();
|
|
52
|
+
const version = isIP(normalized);
|
|
53
|
+
if (version === 0)
|
|
54
|
+
throw new Error("DNS lease address must be an IP address");
|
|
55
|
+
const inferred = version === 4 ? "A" : "AAAA";
|
|
56
|
+
if (recordType && recordType !== inferred) {
|
|
57
|
+
throw new Error(`DNS lease ${recordType} record does not match its address`);
|
|
58
|
+
}
|
|
59
|
+
const canonicalAddress = version === 6
|
|
60
|
+
? new URL(`http://[${normalized}]/`).hostname.replace(/^\[|\]$/g, "")
|
|
61
|
+
: normalized;
|
|
62
|
+
return { address: canonicalAddress, recordType: inferred };
|
|
63
|
+
};
|
|
64
|
+
const requireString = (value, label, pattern) => {
|
|
65
|
+
if (typeof value !== "string" || (pattern && !pattern.test(value))) {
|
|
66
|
+
throw new Error(`DNS lease service returned an invalid ${label}`);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
};
|
|
70
|
+
const requireTimestamp = (value, label) => {
|
|
71
|
+
const timestamp = requireString(value, label);
|
|
72
|
+
if (!Number.isFinite(Date.parse(timestamp))) {
|
|
73
|
+
throw new Error(`DNS lease service returned an invalid ${label}`);
|
|
74
|
+
}
|
|
75
|
+
return timestamp;
|
|
76
|
+
};
|
|
77
|
+
const requireStoredTimestamp = (value, label) => {
|
|
78
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
79
|
+
throw new Error(`DNS lease state has an invalid ${label}`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
};
|
|
83
|
+
const parseLeaseBase = (value) => {
|
|
84
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
85
|
+
throw new Error("DNS lease service returned an invalid response");
|
|
86
|
+
}
|
|
87
|
+
const response = value;
|
|
88
|
+
const rawRecordType = requireString(response.recordType, "record type");
|
|
89
|
+
if (rawRecordType !== "A" && rawRecordType !== "AAAA") {
|
|
90
|
+
throw new Error("DNS lease service returned an invalid record type");
|
|
91
|
+
}
|
|
92
|
+
const recordType = rawRecordType;
|
|
93
|
+
const { address } = normalizeAddress(requireString(response.address, "address"), recordType);
|
|
94
|
+
let domain;
|
|
95
|
+
try {
|
|
96
|
+
domain = normalizeDomain(requireString(response.domain, "domain"));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new Error("DNS lease service returned an invalid domain");
|
|
100
|
+
}
|
|
101
|
+
if (!MANAGED_DOMAIN_PATTERN.test(domain)) {
|
|
102
|
+
throw new Error("DNS lease service returned a domain outside the managed zone");
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
response,
|
|
106
|
+
id: requireString(response.id, "lease id", LEASE_ID_PATTERN),
|
|
107
|
+
domain,
|
|
108
|
+
recordType,
|
|
109
|
+
address,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
const parseClaimResponse = (value) => {
|
|
113
|
+
const parsed = parseLeaseBase(value);
|
|
114
|
+
if (parsed.response.status !== "pending") {
|
|
115
|
+
throw new Error("DNS lease service returned an invalid claim status");
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
id: parsed.id,
|
|
119
|
+
domain: parsed.domain,
|
|
120
|
+
recordType: parsed.recordType,
|
|
121
|
+
address: parsed.address,
|
|
122
|
+
status: "pending",
|
|
123
|
+
challengeToken: requireString(parsed.response.challengeToken, "challenge token", GENERATED_TOKEN_PATTERN),
|
|
124
|
+
challengeUrl: requireString(parsed.response.challengeUrl, "challenge URL"),
|
|
125
|
+
challengeExpiresAt: requireTimestamp(parsed.response.challengeExpiresAt, "challenge expiry"),
|
|
126
|
+
pendingExpiresAt: requireTimestamp(parsed.response.pendingExpiresAt, "pending expiry"),
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
const parseRenewChallengeResponse = (value) => {
|
|
130
|
+
const active = parseActiveResponse(value);
|
|
131
|
+
const response = value;
|
|
132
|
+
return {
|
|
133
|
+
...active,
|
|
134
|
+
challengeToken: requireString(response.challengeToken, "challenge token", GENERATED_TOKEN_PATTERN),
|
|
135
|
+
challengeUrl: requireString(response.challengeUrl, "challenge URL"),
|
|
136
|
+
challengeExpiresAt: requireTimestamp(response.challengeExpiresAt, "challenge expiry"),
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
const parseActiveResponse = (value) => {
|
|
140
|
+
const parsed = parseLeaseBase(value);
|
|
141
|
+
if (parsed.response.status !== "active") {
|
|
142
|
+
throw new Error("DNS lease service returned an invalid active status");
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
id: parsed.id,
|
|
146
|
+
domain: parsed.domain,
|
|
147
|
+
recordType: parsed.recordType,
|
|
148
|
+
address: parsed.address,
|
|
149
|
+
status: "active",
|
|
150
|
+
pendingExpiresAt: typeof parsed.response.pendingExpiresAt === "string"
|
|
151
|
+
? requireTimestamp(parsed.response.pendingExpiresAt, "pending expiry")
|
|
152
|
+
: undefined,
|
|
153
|
+
expiresAt: requireTimestamp(parsed.response.expiresAt, "lease expiry"),
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
const parseReleaseResponse = (value) => {
|
|
157
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
158
|
+
throw new Error("DNS lease service returned an invalid response");
|
|
159
|
+
}
|
|
160
|
+
const response = value;
|
|
161
|
+
if (response.status !== "released" && response.status !== "expired") {
|
|
162
|
+
throw new Error("DNS lease service returned an invalid release status");
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
id: requireString(response.id, "lease id", LEASE_ID_PATTERN),
|
|
166
|
+
status: response.status,
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
export class DnsLeaseClient {
|
|
170
|
+
serviceUrl;
|
|
171
|
+
request;
|
|
172
|
+
timeoutMs;
|
|
173
|
+
constructor(properties) {
|
|
174
|
+
this.serviceUrl = normalizeServiceUrl(properties.serviceUrl);
|
|
175
|
+
this.request = properties.request || ((url, init) => fetch(url, init));
|
|
176
|
+
this.timeoutMs = properties.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
177
|
+
if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) {
|
|
178
|
+
throw new Error("DNS lease request timeout must be positive");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async post(action, body, authorization) {
|
|
182
|
+
const url = `${this.serviceUrl}/${action}`;
|
|
183
|
+
const headers = {
|
|
184
|
+
Accept: "application/json",
|
|
185
|
+
"Content-Type": "application/json",
|
|
186
|
+
};
|
|
187
|
+
authorization.apply(headers);
|
|
188
|
+
const controller = new AbortController();
|
|
189
|
+
let timeout;
|
|
190
|
+
try {
|
|
191
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
192
|
+
timeout = setTimeout(() => {
|
|
193
|
+
controller.abort();
|
|
194
|
+
reject(new Error("timeout"));
|
|
195
|
+
}, this.timeoutMs);
|
|
196
|
+
});
|
|
197
|
+
const response = await Promise.race([
|
|
198
|
+
this.request(url, {
|
|
199
|
+
body: JSON.stringify(body),
|
|
200
|
+
headers,
|
|
201
|
+
method: "POST",
|
|
202
|
+
redirect: "error",
|
|
203
|
+
signal: controller.signal,
|
|
204
|
+
}),
|
|
205
|
+
timeoutPromise,
|
|
206
|
+
]);
|
|
207
|
+
const declaredLength = Number(response.headers?.get("content-length"));
|
|
208
|
+
if (Number.isFinite(declaredLength) &&
|
|
209
|
+
declaredLength > MAX_RESPONSE_BYTES) {
|
|
210
|
+
throw new Error(`DNS lease ${action} response is too large`);
|
|
211
|
+
}
|
|
212
|
+
let text;
|
|
213
|
+
if (response.body?.getReader) {
|
|
214
|
+
const reader = response.body.getReader();
|
|
215
|
+
const chunks = [];
|
|
216
|
+
let size = 0;
|
|
217
|
+
try {
|
|
218
|
+
while (true) {
|
|
219
|
+
const result = await Promise.race([reader.read(), timeoutPromise]);
|
|
220
|
+
if (result.done)
|
|
221
|
+
break;
|
|
222
|
+
size += result.value.byteLength;
|
|
223
|
+
if (size > MAX_RESPONSE_BYTES) {
|
|
224
|
+
throw new Error(`DNS lease ${action} response is too large`);
|
|
225
|
+
}
|
|
226
|
+
chunks.push(result.value);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
reader.releaseLock();
|
|
231
|
+
}
|
|
232
|
+
text = Buffer.concat(chunks).toString("utf8");
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
text = await Promise.race([response.text(), timeoutPromise]);
|
|
236
|
+
}
|
|
237
|
+
if (response.status < 200 || response.status >= 300) {
|
|
238
|
+
throw new Error(`DNS lease ${action} failed with HTTP ${response.status}`);
|
|
239
|
+
}
|
|
240
|
+
if (Buffer.byteLength(text) > MAX_RESPONSE_BYTES) {
|
|
241
|
+
throw new Error(`DNS lease ${action} response is too large`);
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
return JSON.parse(text);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
throw new Error(`DNS lease ${action} returned invalid JSON`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (error?.message?.startsWith("DNS lease "))
|
|
252
|
+
throw error;
|
|
253
|
+
if (controller.signal.aborted || error?.message === "timeout") {
|
|
254
|
+
throw new Error(`DNS lease ${action} timed out`);
|
|
255
|
+
}
|
|
256
|
+
throw new Error(`DNS lease ${action} request failed`);
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
if (timeout)
|
|
260
|
+
clearTimeout(timeout);
|
|
261
|
+
controller.abort();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async claim(request, accessToken) {
|
|
265
|
+
return parseClaimResponse(await this.post("claim", request, new BearerAuthorization(accessToken)));
|
|
266
|
+
}
|
|
267
|
+
async verify(id, leaseToken) {
|
|
268
|
+
return parseActiveResponse(await this.post("verify", { id }, new BearerAuthorization(leaseToken)));
|
|
269
|
+
}
|
|
270
|
+
async renew(id, leaseToken) {
|
|
271
|
+
return parseActiveResponse(await this.post("renew", { id }, new BearerAuthorization(leaseToken)));
|
|
272
|
+
}
|
|
273
|
+
async renewChallenge(id, leaseToken) {
|
|
274
|
+
return parseRenewChallengeResponse(await this.post("renew-challenge", { id }, new BearerAuthorization(leaseToken)));
|
|
275
|
+
}
|
|
276
|
+
async release(id, leaseToken) {
|
|
277
|
+
return parseReleaseResponse(await this.post("release", { id }, new BearerAuthorization(leaseToken)));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
export const getDnsLeaseStatePath = (cwd = process.cwd()) => path.join(cwd, ".peerbit-domain", "lease.json");
|
|
281
|
+
const validateStoredState = (value) => {
|
|
282
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
283
|
+
throw new Error("DNS lease state is invalid");
|
|
284
|
+
}
|
|
285
|
+
const state = value;
|
|
286
|
+
if (state.version !== DNS_LEASE_STATE_VERSION) {
|
|
287
|
+
throw new Error("DNS lease state version is unsupported");
|
|
288
|
+
}
|
|
289
|
+
if (state.status !== "claiming" &&
|
|
290
|
+
state.status !== "pending" &&
|
|
291
|
+
state.status !== "active" &&
|
|
292
|
+
state.status !== "released") {
|
|
293
|
+
throw new Error("DNS lease state has an invalid status");
|
|
294
|
+
}
|
|
295
|
+
const normalized = normalizeAddress(requireString(state.address, "stored address"), state.recordType);
|
|
296
|
+
const stored = {
|
|
297
|
+
version: DNS_LEASE_STATE_VERSION,
|
|
298
|
+
serviceUrl: normalizeServiceUrl(requireString(state.serviceUrl, "stored service URL")),
|
|
299
|
+
idempotencyKey: requireString(state.idempotencyKey, "stored idempotency key", LEASE_ID_PATTERN),
|
|
300
|
+
recordType: normalized.recordType,
|
|
301
|
+
address: normalized.address,
|
|
302
|
+
status: state.status,
|
|
303
|
+
createdAt: requireStoredTimestamp(state.createdAt, "creation time"),
|
|
304
|
+
updatedAt: requireStoredTimestamp(state.updatedAt, "update time"),
|
|
305
|
+
};
|
|
306
|
+
for (const key of [
|
|
307
|
+
"leaseToken",
|
|
308
|
+
"challengeToken",
|
|
309
|
+
"id",
|
|
310
|
+
"domain",
|
|
311
|
+
"challengeUrl",
|
|
312
|
+
"challengeExpiresAt",
|
|
313
|
+
"pendingExpiresAt",
|
|
314
|
+
"expiresAt",
|
|
315
|
+
"configuredAt",
|
|
316
|
+
]) {
|
|
317
|
+
if (typeof state[key] === "string")
|
|
318
|
+
stored[key] = state[key];
|
|
319
|
+
}
|
|
320
|
+
if (stored.domain)
|
|
321
|
+
stored.domain = normalizeDomain(stored.domain);
|
|
322
|
+
if (stored.id)
|
|
323
|
+
requireString(stored.id, "stored lease id", LEASE_ID_PATTERN);
|
|
324
|
+
for (const key of [
|
|
325
|
+
"challengeExpiresAt",
|
|
326
|
+
"pendingExpiresAt",
|
|
327
|
+
"expiresAt",
|
|
328
|
+
"configuredAt",
|
|
329
|
+
]) {
|
|
330
|
+
if (stored[key])
|
|
331
|
+
requireStoredTimestamp(stored[key], key);
|
|
332
|
+
}
|
|
333
|
+
if (stored.status !== "released") {
|
|
334
|
+
requireString(stored.leaseToken, "stored lease token", GENERATED_TOKEN_PATTERN);
|
|
335
|
+
}
|
|
336
|
+
if (stored.status === "pending") {
|
|
337
|
+
requireString(stored.challengeToken, "stored challenge token", GENERATED_TOKEN_PATTERN);
|
|
338
|
+
}
|
|
339
|
+
if ((stored.status === "pending" || stored.status === "active") &&
|
|
340
|
+
!stored.id) {
|
|
341
|
+
throw new Error("DNS lease state is missing its lease id");
|
|
342
|
+
}
|
|
343
|
+
if (stored.status === "pending" &&
|
|
344
|
+
(!stored.challengeUrl || !stored.challengeExpiresAt)) {
|
|
345
|
+
throw new Error("DNS lease state is missing challenge data");
|
|
346
|
+
}
|
|
347
|
+
if (stored.status === "active" && (!stored.domain || !stored.expiresAt)) {
|
|
348
|
+
throw new Error("DNS lease state is missing active lease data");
|
|
349
|
+
}
|
|
350
|
+
return stored;
|
|
351
|
+
};
|
|
352
|
+
export const readDnsLeaseState = (statePath = getDnsLeaseStatePath()) => {
|
|
353
|
+
if (!fs.existsSync(statePath))
|
|
354
|
+
return undefined;
|
|
355
|
+
const stat = fs.lstatSync(statePath);
|
|
356
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
357
|
+
throw new Error("DNS lease state must be a regular file");
|
|
358
|
+
}
|
|
359
|
+
if (process.platform !== "win32" && (stat.mode & 0o077) !== 0) {
|
|
360
|
+
throw new Error("DNS lease state permissions are too broad; expected mode 0600");
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
return validateStoredState(JSON.parse(fs.readFileSync(statePath, "utf8")));
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
if (error?.message?.startsWith("DNS lease state"))
|
|
367
|
+
throw error;
|
|
368
|
+
throw new Error("DNS lease state is invalid");
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
export const writeDnsLeaseState = (state, statePath = getDnsLeaseStatePath()) => {
|
|
372
|
+
const validated = validateStoredState(state);
|
|
373
|
+
const parent = path.dirname(statePath);
|
|
374
|
+
fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
375
|
+
const temporary = path.join(parent, `.${path.basename(statePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
376
|
+
let descriptor;
|
|
377
|
+
try {
|
|
378
|
+
descriptor = fs.openSync(temporary, "wx", 0o600);
|
|
379
|
+
fs.writeFileSync(descriptor, `${JSON.stringify(validated, undefined, 2)}\n`);
|
|
380
|
+
fs.fsyncSync(descriptor);
|
|
381
|
+
fs.closeSync(descriptor);
|
|
382
|
+
descriptor = undefined;
|
|
383
|
+
fs.renameSync(temporary, statePath);
|
|
384
|
+
fs.chmodSync(statePath, 0o600);
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
if (descriptor !== undefined)
|
|
388
|
+
fs.closeSync(descriptor);
|
|
389
|
+
fs.rmSync(temporary, { force: true });
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
const withStateLock = async (statePath, fn) => {
|
|
393
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true, mode: 0o700 });
|
|
394
|
+
const lockTarget = `${statePath}.lock-target`;
|
|
395
|
+
try {
|
|
396
|
+
fs.closeSync(fs.openSync(lockTarget, "wx", 0o600));
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
if (error?.code !== "EEXIST")
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
const lockStat = fs.lstatSync(lockTarget);
|
|
403
|
+
if (!lockStat.isFile() || lockStat.isSymbolicLink()) {
|
|
404
|
+
throw new Error("DNS lease lock target must be a regular file");
|
|
405
|
+
}
|
|
406
|
+
if (process.platform !== "win32" && (lockStat.mode & 0o077) !== 0) {
|
|
407
|
+
throw new Error("DNS lease lock target permissions are too broad");
|
|
408
|
+
}
|
|
409
|
+
const { createRequire } = await import("module");
|
|
410
|
+
const properLockfile = createRequire(import.meta.url)("proper-lockfile");
|
|
411
|
+
let release;
|
|
412
|
+
try {
|
|
413
|
+
release = await properLockfile.lock(lockTarget, {
|
|
414
|
+
realpath: false,
|
|
415
|
+
stale: 30_000,
|
|
416
|
+
update: 5_000,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
if (error?.code === "ELOCKED") {
|
|
421
|
+
throw new Error("Another DNS lease operation is already running");
|
|
422
|
+
}
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
return await fn();
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
await release();
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
const assertLeaseMatches = (state, lease) => {
|
|
433
|
+
if ((state.id && state.id !== lease.id) ||
|
|
434
|
+
state.address !== lease.address ||
|
|
435
|
+
state.recordType !== lease.recordType ||
|
|
436
|
+
(state.domain && state.domain !== lease.domain)) {
|
|
437
|
+
throw new Error("DNS lease service returned conflicting lease data");
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
const toActiveResponse = (state) => ({
|
|
441
|
+
id: state.id,
|
|
442
|
+
domain: state.domain,
|
|
443
|
+
recordType: state.recordType,
|
|
444
|
+
address: state.address,
|
|
445
|
+
status: "active",
|
|
446
|
+
pendingExpiresAt: state.pendingExpiresAt,
|
|
447
|
+
expiresAt: state.expiresAt,
|
|
448
|
+
});
|
|
449
|
+
const markReleased = (state) => {
|
|
450
|
+
const released = {
|
|
451
|
+
...state,
|
|
452
|
+
status: "released",
|
|
453
|
+
updatedAt: new Date().toISOString(),
|
|
454
|
+
};
|
|
455
|
+
delete released.leaseToken;
|
|
456
|
+
delete released.challengeToken;
|
|
457
|
+
delete released.challengeUrl;
|
|
458
|
+
delete released.challengeExpiresAt;
|
|
459
|
+
return released;
|
|
460
|
+
};
|
|
461
|
+
const validateChallengeUrl = (state) => {
|
|
462
|
+
let challengeUrl;
|
|
463
|
+
try {
|
|
464
|
+
challengeUrl = new URL(state.challengeUrl);
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
throw new Error("DNS lease service returned an invalid challenge URL");
|
|
468
|
+
}
|
|
469
|
+
const hostname = challengeUrl.hostname.replace(/^\[|\]$/g, "");
|
|
470
|
+
if (challengeUrl.protocol !== "http:" ||
|
|
471
|
+
(challengeUrl.port && challengeUrl.port !== "80") ||
|
|
472
|
+
challengeUrl.username ||
|
|
473
|
+
challengeUrl.password ||
|
|
474
|
+
challengeUrl.search ||
|
|
475
|
+
challengeUrl.hash ||
|
|
476
|
+
hostname.toLowerCase() !== state.address.toLowerCase() ||
|
|
477
|
+
challengeUrl.pathname !== `/.well-known/peerbit-dns/${state.id}`) {
|
|
478
|
+
throw new Error("DNS lease service returned an unsafe challenge URL");
|
|
479
|
+
}
|
|
480
|
+
return challengeUrl;
|
|
481
|
+
};
|
|
482
|
+
const managedChallengeProxyIsAvailable = async (execute) => {
|
|
483
|
+
try {
|
|
484
|
+
const { inspectDockerContainer } = await import("./docker.js");
|
|
485
|
+
const inspection = await inspectDockerContainer("nginx-certbot", execute);
|
|
486
|
+
if (!inspection ||
|
|
487
|
+
inspection.State?.Running !== true ||
|
|
488
|
+
!isPeerbitCertbotContainer(inspection)) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
const source = inspection.Mounts?.find((mount) => mount.Destination === "/etc/nginx/user_conf.d")?.Source;
|
|
492
|
+
if (!source)
|
|
493
|
+
return false;
|
|
494
|
+
const sourceStat = fs.lstatSync(source);
|
|
495
|
+
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink())
|
|
496
|
+
return false;
|
|
497
|
+
const configPath = path.join(source, "default.conf");
|
|
498
|
+
const configStat = fs.lstatSync(configPath);
|
|
499
|
+
if (!configStat.isFile() ||
|
|
500
|
+
configStat.isSymbolicLink() ||
|
|
501
|
+
configStat.size > 1024 * 1024) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
return fs
|
|
505
|
+
.readFileSync(configPath, "utf8")
|
|
506
|
+
.includes(DNS_LEASE_CHALLENGE_PROXY_MARKER);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
export const serveDnsLeaseChallenge = async (state, whileServing, listen = {}) => {
|
|
513
|
+
if (listen.port === undefined && listen.preferManagedProxy) {
|
|
514
|
+
return serveDnsLeaseChallenge(state, whileServing, {
|
|
515
|
+
...listen,
|
|
516
|
+
host: "127.0.0.1",
|
|
517
|
+
port: DNS_LEASE_CHALLENGE_PROXY_PORT,
|
|
518
|
+
preferManagedProxy: false,
|
|
519
|
+
quiesceManagedContainer: false,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
if (listen.port === undefined &&
|
|
523
|
+
listen.quiesceManagedContainer !== false &&
|
|
524
|
+
(await managedChallengeProxyIsAvailable(listen.dockerExecute))) {
|
|
525
|
+
return serveDnsLeaseChallenge(state, whileServing, {
|
|
526
|
+
...listen,
|
|
527
|
+
host: "127.0.0.1",
|
|
528
|
+
port: DNS_LEASE_CHALLENGE_PROXY_PORT,
|
|
529
|
+
preferManagedProxy: false,
|
|
530
|
+
quiesceManagedContainer: false,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
const challengeUrl = validateChallengeUrl(state);
|
|
534
|
+
const expectedPath = challengeUrl.pathname;
|
|
535
|
+
const challengeToken = state.challengeToken;
|
|
536
|
+
const server = http.createServer((request, response) => {
|
|
537
|
+
let requestPath;
|
|
538
|
+
try {
|
|
539
|
+
requestPath = new URL(request.url || "", "http://localhost").pathname;
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
// Handled as not found below.
|
|
543
|
+
}
|
|
544
|
+
if (request.method !== "GET" || requestPath !== expectedPath) {
|
|
545
|
+
response.writeHead(404, { "Cache-Control": "no-store" });
|
|
546
|
+
response.end("Not found");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
response.writeHead(200, {
|
|
550
|
+
"Cache-Control": "no-store",
|
|
551
|
+
"Content-Length": Buffer.byteLength(challengeToken),
|
|
552
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
553
|
+
"X-Content-Type-Options": "nosniff",
|
|
554
|
+
});
|
|
555
|
+
response.end(challengeToken);
|
|
556
|
+
});
|
|
557
|
+
server.headersTimeout = 5_000;
|
|
558
|
+
server.requestTimeout = 5_000;
|
|
559
|
+
const listenPort = listen.port ?? 80;
|
|
560
|
+
if (!Number.isInteger(listenPort) || listenPort < 1 || listenPort > 65_535) {
|
|
561
|
+
throw new Error("DNS lease challenge server port is invalid");
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
await new Promise((resolve, reject) => {
|
|
565
|
+
server.once("error", reject);
|
|
566
|
+
server.listen({
|
|
567
|
+
host: listen.host || (state.recordType === "A" ? "0.0.0.0" : "::"),
|
|
568
|
+
port: listenPort,
|
|
569
|
+
}, resolve);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
if (error?.code === "EACCES") {
|
|
574
|
+
throw new Error(`DNS lease verification must bind port ${listenPort}; rerun with permission to use that port`);
|
|
575
|
+
}
|
|
576
|
+
if (error?.code === "EADDRINUSE") {
|
|
577
|
+
if (listenPort === 80 && listen.quiesceManagedContainer !== false) {
|
|
578
|
+
const { withQuiescedDockerContainer } = await import("./docker.js");
|
|
579
|
+
return withQuiescedDockerContainer("nginx-certbot", isPeerbitCertbotContainer, () => serveDnsLeaseChallenge(state, whileServing, {
|
|
580
|
+
...listen,
|
|
581
|
+
preferManagedProxy: false,
|
|
582
|
+
quiesceManagedContainer: false,
|
|
583
|
+
}), { execute: listen.dockerExecute });
|
|
584
|
+
}
|
|
585
|
+
throw new Error(`DNS lease verification needs port ${listenPort}, but another service is using it; if it is the managed nginx-certbot container, stop that container and rerun this command`);
|
|
586
|
+
}
|
|
587
|
+
throw new Error("DNS lease challenge server could not start");
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
return await whileServing();
|
|
591
|
+
}
|
|
592
|
+
finally {
|
|
593
|
+
server.closeAllConnections?.();
|
|
594
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
export const provisionDnsLease = async (properties) => {
|
|
598
|
+
const statePath = properties.statePath || getDnsLeaseStatePath();
|
|
599
|
+
return withStateLock(statePath, async () => {
|
|
600
|
+
let state = readDnsLeaseState(statePath);
|
|
601
|
+
if (!state || state.status === "released") {
|
|
602
|
+
if (!properties.serviceUrl) {
|
|
603
|
+
throw new Error("DNS lease service URL is required for a new claim");
|
|
604
|
+
}
|
|
605
|
+
if (!properties.address) {
|
|
606
|
+
throw new Error("DNS lease public IP address is required for a new claim");
|
|
607
|
+
}
|
|
608
|
+
if (!properties.accessToken) {
|
|
609
|
+
throw new Error("DNS lease access token is required for a new claim");
|
|
610
|
+
}
|
|
611
|
+
new BearerAuthorization(properties.accessToken);
|
|
612
|
+
const normalized = normalizeAddress(properties.address, properties.recordType);
|
|
613
|
+
const now = new Date().toISOString();
|
|
614
|
+
state = {
|
|
615
|
+
version: DNS_LEASE_STATE_VERSION,
|
|
616
|
+
serviceUrl: normalizeServiceUrl(properties.serviceUrl),
|
|
617
|
+
idempotencyKey: randomUUID(),
|
|
618
|
+
recordType: normalized.recordType,
|
|
619
|
+
address: normalized.address,
|
|
620
|
+
leaseToken: randomBytes(32).toString("base64url"),
|
|
621
|
+
status: "claiming",
|
|
622
|
+
createdAt: now,
|
|
623
|
+
updatedAt: now,
|
|
624
|
+
};
|
|
625
|
+
writeDnsLeaseState(state, statePath);
|
|
626
|
+
}
|
|
627
|
+
if (properties.serviceUrl &&
|
|
628
|
+
normalizeServiceUrl(properties.serviceUrl) !== state.serviceUrl) {
|
|
629
|
+
throw new Error("DNS lease service URL does not match stored lease state");
|
|
630
|
+
}
|
|
631
|
+
if (properties.address && properties.address.trim() !== state.address) {
|
|
632
|
+
throw new Error("DNS lease address does not match stored lease state");
|
|
633
|
+
}
|
|
634
|
+
const client = new DnsLeaseClient({
|
|
635
|
+
serviceUrl: state.serviceUrl,
|
|
636
|
+
request: properties.request,
|
|
637
|
+
timeoutMs: properties.timeoutMs,
|
|
638
|
+
});
|
|
639
|
+
if (state.status === "claiming") {
|
|
640
|
+
if (!properties.accessToken) {
|
|
641
|
+
throw new Error("DNS lease access token is required to finish the claim");
|
|
642
|
+
}
|
|
643
|
+
const claim = await client.claim({
|
|
644
|
+
idempotencyKey: state.idempotencyKey,
|
|
645
|
+
recordType: state.recordType,
|
|
646
|
+
address: state.address,
|
|
647
|
+
leaseToken: state.leaseToken,
|
|
648
|
+
}, properties.accessToken);
|
|
649
|
+
assertLeaseMatches(state, claim);
|
|
650
|
+
state = {
|
|
651
|
+
...state,
|
|
652
|
+
...claim,
|
|
653
|
+
updatedAt: new Date().toISOString(),
|
|
654
|
+
};
|
|
655
|
+
writeDnsLeaseState(state, statePath);
|
|
656
|
+
}
|
|
657
|
+
if (state.status === "pending") {
|
|
658
|
+
const active = await (properties.serveChallenge || serveDnsLeaseChallenge)(state, () => client.verify(state.id, state.leaseToken));
|
|
659
|
+
assertLeaseMatches(state, active);
|
|
660
|
+
state = {
|
|
661
|
+
...state,
|
|
662
|
+
...active,
|
|
663
|
+
updatedAt: new Date().toISOString(),
|
|
664
|
+
};
|
|
665
|
+
delete state.challengeToken;
|
|
666
|
+
delete state.challengeUrl;
|
|
667
|
+
delete state.challengeExpiresAt;
|
|
668
|
+
writeDnsLeaseState(state, statePath);
|
|
669
|
+
}
|
|
670
|
+
if (state.status !== "active") {
|
|
671
|
+
throw new Error("DNS lease did not become active");
|
|
672
|
+
}
|
|
673
|
+
if (properties.configure && !state.configuredAt) {
|
|
674
|
+
try {
|
|
675
|
+
await properties.configure(state.domain);
|
|
676
|
+
state = {
|
|
677
|
+
...state,
|
|
678
|
+
configuredAt: new Date().toISOString(),
|
|
679
|
+
updatedAt: new Date().toISOString(),
|
|
680
|
+
};
|
|
681
|
+
writeDnsLeaseState(state, statePath);
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
throw error;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return toActiveResponse(state);
|
|
688
|
+
});
|
|
689
|
+
};
|
|
690
|
+
export const renewDnsLease = async (properties) => {
|
|
691
|
+
const statePath = properties.statePath || getDnsLeaseStatePath();
|
|
692
|
+
return withStateLock(statePath, async () => {
|
|
693
|
+
let state = readDnsLeaseState(statePath);
|
|
694
|
+
if (state?.status !== "active" || !state.id || !state.leaseToken) {
|
|
695
|
+
throw new Error("No active DNS lease is available to renew");
|
|
696
|
+
}
|
|
697
|
+
if (properties.serviceUrl &&
|
|
698
|
+
normalizeServiceUrl(properties.serviceUrl) !== state.serviceUrl) {
|
|
699
|
+
throw new Error("DNS lease service URL does not match stored lease state");
|
|
700
|
+
}
|
|
701
|
+
const client = new DnsLeaseClient({
|
|
702
|
+
serviceUrl: state.serviceUrl,
|
|
703
|
+
request: properties.request,
|
|
704
|
+
timeoutMs: properties.timeoutMs,
|
|
705
|
+
});
|
|
706
|
+
const challenge = await client.renewChallenge(state.id, state.leaseToken);
|
|
707
|
+
assertLeaseMatches(state, challenge);
|
|
708
|
+
const challengeState = {
|
|
709
|
+
...state,
|
|
710
|
+
...challenge,
|
|
711
|
+
};
|
|
712
|
+
const active = await (properties.serveChallenge || serveDnsLeaseChallenge)(challengeState, () => client.renew(state.id, state.leaseToken), { preferManagedProxy: Boolean(state.configuredAt) });
|
|
713
|
+
assertLeaseMatches(state, active);
|
|
714
|
+
state = { ...state, ...active, updatedAt: new Date().toISOString() };
|
|
715
|
+
writeDnsLeaseState(state, statePath);
|
|
716
|
+
return active;
|
|
717
|
+
});
|
|
718
|
+
};
|
|
719
|
+
export const releaseDnsLease = async (properties) => {
|
|
720
|
+
const statePath = properties.statePath || getDnsLeaseStatePath();
|
|
721
|
+
await withStateLock(statePath, async () => {
|
|
722
|
+
const state = readDnsLeaseState(statePath);
|
|
723
|
+
if (!state || state.status === "released")
|
|
724
|
+
return;
|
|
725
|
+
if (!state.id || !state.leaseToken) {
|
|
726
|
+
throw new Error("DNS lease claim has no server id; retry the claim before releasing it");
|
|
727
|
+
}
|
|
728
|
+
if (properties.serviceUrl &&
|
|
729
|
+
normalizeServiceUrl(properties.serviceUrl) !== state.serviceUrl) {
|
|
730
|
+
throw new Error("DNS lease service URL does not match stored lease state");
|
|
731
|
+
}
|
|
732
|
+
const client = new DnsLeaseClient({
|
|
733
|
+
serviceUrl: state.serviceUrl,
|
|
734
|
+
request: properties.request,
|
|
735
|
+
timeoutMs: properties.timeoutMs,
|
|
736
|
+
});
|
|
737
|
+
const released = await client.release(state.id, state.leaseToken);
|
|
738
|
+
if (released.id !== state.id) {
|
|
739
|
+
throw new Error("DNS lease service returned conflicting lease data");
|
|
740
|
+
}
|
|
741
|
+
writeDnsLeaseState(markReleased(state), statePath);
|
|
742
|
+
});
|
|
743
|
+
};
|
|
744
|
+
export const startDnsLeaseRenewal = async (properties = {}) => {
|
|
745
|
+
const statePath = properties.statePath || getDnsLeaseStatePath();
|
|
746
|
+
const initialState = readDnsLeaseState(statePath);
|
|
747
|
+
if (initialState?.status !== "active" || !initialState.configuredAt) {
|
|
748
|
+
return () => undefined;
|
|
749
|
+
}
|
|
750
|
+
let stopped = false;
|
|
751
|
+
let timer;
|
|
752
|
+
const schedule = (delayMs) => {
|
|
753
|
+
if (stopped)
|
|
754
|
+
return;
|
|
755
|
+
timer = setTimeout(run, delayMs);
|
|
756
|
+
timer.unref?.();
|
|
757
|
+
};
|
|
758
|
+
const run = async () => {
|
|
759
|
+
try {
|
|
760
|
+
const active = await renewDnsLease({
|
|
761
|
+
statePath,
|
|
762
|
+
request: properties.request,
|
|
763
|
+
timeoutMs: properties.timeoutMs,
|
|
764
|
+
serveChallenge: properties.serveChallenge,
|
|
765
|
+
});
|
|
766
|
+
const remaining = Date.parse(active.expiresAt) - Date.now();
|
|
767
|
+
const baseDelay = Math.min(12 * 60 * 60 * 1000, Math.max(5 * 60 * 1000, remaining / 2));
|
|
768
|
+
schedule(baseDelay * (0.9 + Math.random() * 0.2));
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
let retryMs = 5 * 60 * 1000;
|
|
772
|
+
try {
|
|
773
|
+
const currentState = readDnsLeaseState(statePath);
|
|
774
|
+
if (currentState?.status !== "active") {
|
|
775
|
+
properties.onError?.("Managed DNS lease renewal stopped because the lease is no longer active");
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
const expiresAt = currentState.expiresAt;
|
|
779
|
+
const remaining = expiresAt ? Date.parse(expiresAt) - Date.now() : 0;
|
|
780
|
+
if (remaining <= 0) {
|
|
781
|
+
properties.onError?.("Managed DNS lease renewal stopped because the stored lease has expired");
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
retryMs = Math.max(5_000, Math.min(retryMs, remaining / 4));
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
// Keep the bounded fallback when local state cannot be re-read.
|
|
788
|
+
}
|
|
789
|
+
properties.onError?.("Managed DNS lease renewal failed; a bounded retry has been scheduled");
|
|
790
|
+
schedule(retryMs * (0.9 + Math.random() * 0.2));
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
void run();
|
|
794
|
+
return () => {
|
|
795
|
+
stopped = true;
|
|
796
|
+
if (timer)
|
|
797
|
+
clearTimeout(timer);
|
|
798
|
+
};
|
|
799
|
+
};
|
|
800
|
+
//# sourceMappingURL=domain-lease.js.map
|