@serve.zone/dcrouter 17.10.2 → 18.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/deno.json +1 -1
- package/dist_serve/bundle.js +360 -360
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/acme/acme-failure-classification.d.ts +64 -0
- package/dist_ts/acme/acme-failure-classification.js +114 -0
- package/dist_ts/acme/classes.smartacme-lifecycle.d.ts +42 -0
- package/dist_ts/acme/classes.smartacme-lifecycle.js +75 -4
- package/dist_ts/acme/index.d.ts +1 -0
- package/dist_ts/acme/index.js +2 -1
- package/dist_ts/classes.dcrouter.d.ts +33 -9
- package/dist_ts/classes.dcrouter.js +145 -11
- package/dist_ts/config/classes.route-config-manager.d.ts +56 -0
- package/dist_ts/config/classes.route-config-manager.js +161 -1
- package/dist_ts/db/documents/classes.dns-authority.doc.d.ts +30 -0
- package/dist_ts/db/documents/classes.dns-authority.doc.js +108 -0
- package/dist_ts/db/documents/index.d.ts +1 -0
- package/dist_ts/db/documents/index.js +2 -1
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +109 -1
- package/dist_ts/dns/classes.dns-server-runtime.js +212 -34
- package/dist_ts/dns/domain-ownership.d.ts +111 -0
- package/dist_ts/dns/domain-ownership.js +152 -0
- package/dist_ts/dns/index.d.ts +2 -0
- package/dist_ts/dns/index.js +3 -1
- package/dist_ts/dns/manager.dns-authority.d.ts +143 -0
- package/dist_ts/dns/manager.dns-authority.js +481 -0
- package/dist_ts/dns/manager.dns.d.ts +121 -12
- package/dist_ts/dns/manager.dns.js +298 -28
- package/dist_ts/errors/error.codes.d.ts +4 -0
- package/dist_ts/errors/error.codes.js +5 -1
- package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
- package/dist_ts/opsserver/classes.opsserver.js +3 -1
- package/dist_ts/opsserver/handlers/acme-config.handler.js +6 -1
- package/dist_ts/opsserver/handlers/certificate.handler.d.ts +16 -0
- package/dist_ts/opsserver/handlers/certificate.handler.js +96 -10
- package/dist_ts/opsserver/handlers/config.handler.js +4 -2
- package/dist_ts/opsserver/handlers/dns-authority.handler.d.ts +20 -0
- package/dist_ts/opsserver/handlers/dns-authority.handler.js +108 -0
- package/dist_ts/opsserver/handlers/dns-provider.handler.js +5 -1
- package/dist_ts/opsserver/handlers/domain.handler.js +9 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +2 -2
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts_interfaces/data/dns-authority.d.ts +98 -0
- package/dist_ts_interfaces/data/dns-authority.js +26 -0
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/route-management.d.ts +9 -2
- package/dist_ts_interfaces/data/route-management.js +3 -1
- package/dist_ts_interfaces/requests/certificate.d.ts +23 -0
- package/dist_ts_interfaces/requests/certificate.js +1 -1
- package/dist_ts_interfaces/requests/dns-authority.d.ts +80 -0
- package/dist_ts_interfaces/requests/dns-authority.js +3 -0
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_oci_container/index.js +9 -4
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/package.json +1 -1
- package/readme.hints.md +412 -0
- package/readme.md +48 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/acme/acme-failure-classification.ts +201 -0
- package/ts/acme/classes.smartacme-lifecycle.ts +104 -3
- package/ts/acme/index.ts +1 -0
- package/ts/classes.dcrouter.ts +193 -23
- package/ts/config/classes.route-config-manager.ts +197 -0
- package/ts/db/documents/classes.dns-authority.doc.ts +49 -0
- package/ts/db/documents/index.ts +1 -0
- package/ts/dns/classes.dns-server-runtime.ts +257 -38
- package/ts/dns/domain-ownership.ts +272 -0
- package/ts/dns/index.ts +2 -0
- package/ts/dns/manager.dns-authority.ts +558 -0
- package/ts/dns/manager.dns.ts +373 -27
- package/ts/errors/error.codes.ts +4 -0
- package/ts/opsserver/classes.opsserver.ts +2 -0
- package/ts/opsserver/handlers/acme-config.handler.ts +7 -0
- package/ts/opsserver/handlers/certificate.handler.ts +103 -8
- package/ts/opsserver/handlers/config.handler.ts +3 -1
- package/ts/opsserver/handlers/dns-authority.handler.ts +142 -0
- package/ts/opsserver/handlers/dns-provider.handler.ts +6 -0
- package/ts/opsserver/handlers/domain.handler.ts +12 -0
- package/ts/opsserver/handlers/gatewayclient.handler.ts +1 -1
- package/ts/opsserver/handlers/index.ts +1 -0
- package/ts/readme.md +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { PlatformError, type IErrorContext } from '../errors/base.errors.js';
|
|
2
|
+
import {
|
|
3
|
+
DCR_ACME_PERMANENT_FAILURE,
|
|
4
|
+
ErrorCategory,
|
|
5
|
+
ErrorRecoverability,
|
|
6
|
+
ErrorSeverity,
|
|
7
|
+
} from '../errors/error.codes.js';
|
|
8
|
+
import { DomainOwnershipError } from '../dns/domain-ownership.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ACME failure classification.
|
|
12
|
+
*
|
|
13
|
+
* dcrouter has two independent ACME retry budgets, and neither used to look at
|
|
14
|
+
* *why* a failure happened:
|
|
15
|
+
*
|
|
16
|
+
* 1. `SmartAcmeLifecycle` — SmartAcme provider startup. 5 s→1 h exponential
|
|
17
|
+
* backoff with jitter, hard cap of 20 attempts, then permanent give-up.
|
|
18
|
+
* Only a SmartProxy rebuild or a process restart re-arms it.
|
|
19
|
+
* 2. `CertProvisionScheduler` — per-domain certificate provisioning.
|
|
20
|
+
* `min(failures², 24 h)` backoff, **no cap**, re-armed by time forever.
|
|
21
|
+
* This is the budget that reached 31–45 failures on the broken domains.
|
|
22
|
+
*
|
|
23
|
+
* Retrying is correct for rate limits, DNS propagation and transport faults. It
|
|
24
|
+
* is never correct for a configuration cause: a hostname with no managed domain
|
|
25
|
+
* cannot acquire one by waiting, so every one of those attempts was a silent
|
|
26
|
+
* no-op that also kept the real reason out of the operator's view. Permanent
|
|
27
|
+
* causes must therefore skip the budget entirely and surface attributably.
|
|
28
|
+
*
|
|
29
|
+
* Unclassified causes stay transient on purpose. Guessing "permanent" would
|
|
30
|
+
* strand recoverable domains, so the default preserves existing retry behaviour;
|
|
31
|
+
* only causes we can positively recognise are treated as terminal.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export type TAcmeFailureReason =
|
|
35
|
+
/** Ownership of the hostname could not be proven (no DomainDoc / no provider zone / not delegation-verified). */
|
|
36
|
+
| 'domain-ownership-unverified'
|
|
37
|
+
/** The DNS-01 dispatcher found no managed zone able to hold the challenge record. */
|
|
38
|
+
| 'no-managed-dns-zone'
|
|
39
|
+
/** No challenge handler / provider is wired for this domain at all. */
|
|
40
|
+
| 'no-challenge-handler'
|
|
41
|
+
/** The ACME account itself is misconfigured (email, terms, key, directory URL). */
|
|
42
|
+
| 'acme-account-configuration'
|
|
43
|
+
/** CAA forbids our issuer — only the domain holder can change this. */
|
|
44
|
+
| 'caa-forbids-issuance'
|
|
45
|
+
/** ACME server rate limit — retrying is exactly right. */
|
|
46
|
+
| 'rate-limited'
|
|
47
|
+
/** Challenge not yet visible, propagation delay, transport fault. */
|
|
48
|
+
| 'transient'
|
|
49
|
+
/** Nothing recognised. Treated as transient so recoverable causes keep retrying. */
|
|
50
|
+
| 'unclassified';
|
|
51
|
+
|
|
52
|
+
export interface IAcmeFailureClassification {
|
|
53
|
+
reason: TAcmeFailureReason;
|
|
54
|
+
/** True when no retry can resolve the cause, so the retry budget must not be consumed. */
|
|
55
|
+
permanent: boolean;
|
|
56
|
+
message: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface IReasonPattern {
|
|
60
|
+
reason: TAcmeFailureReason;
|
|
61
|
+
permanent: boolean;
|
|
62
|
+
patterns: RegExp[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Ordered most-specific-first. Each pattern must only match text that uniquely
|
|
67
|
+
* identifies the cause — a false "permanent" verdict silently stops legitimate
|
|
68
|
+
* retries, which is a worse failure than an extra retry.
|
|
69
|
+
*/
|
|
70
|
+
const reasonPatterns: IReasonPattern[] = [
|
|
71
|
+
{
|
|
72
|
+
// Thrown by DnsManager.buildAcmeConvenientDnsProvider() when no DomainDoc covers the FQDN.
|
|
73
|
+
reason: 'no-managed-dns-zone',
|
|
74
|
+
permanent: true,
|
|
75
|
+
patterns: [
|
|
76
|
+
/no managed domain found for/i,
|
|
77
|
+
/add the domain in domains before issuing certificates/i,
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
reason: 'no-challenge-handler',
|
|
82
|
+
permanent: true,
|
|
83
|
+
patterns: [
|
|
84
|
+
/no (?:challenge )?handler (?:found |available )?for/i,
|
|
85
|
+
/no dns-01 (?:handler|provider)/i,
|
|
86
|
+
/domain is not supported by any challenge handler/i,
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
reason: 'caa-forbids-issuance',
|
|
91
|
+
permanent: true,
|
|
92
|
+
patterns: [
|
|
93
|
+
/caa record(?:s)? (?:for [^\s]+ )?(?:prevent|forbid|do not allow)/i,
|
|
94
|
+
/urn:ietf:params:acme:error:caa/i,
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
reason: 'acme-account-configuration',
|
|
99
|
+
permanent: true,
|
|
100
|
+
patterns: [
|
|
101
|
+
/urn:ietf:params:acme:error:invalidemail/i,
|
|
102
|
+
/urn:ietf:params:acme:error:accountdoesnotexist/i,
|
|
103
|
+
/urn:ietf:params:acme:error:unsupportedcontact/i,
|
|
104
|
+
/must agree to (?:the )?terms of service/i,
|
|
105
|
+
/accountemail is required/i,
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
reason: 'rate-limited',
|
|
110
|
+
permanent: false,
|
|
111
|
+
patterns: [
|
|
112
|
+
/urn:ietf:params:acme:error:ratelimited/i,
|
|
113
|
+
/too many (?:certificates|requests|failed authorizations)/i,
|
|
114
|
+
/rate ?limit/i,
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const extractMessage = (errorArg: unknown): string => {
|
|
120
|
+
if (errorArg instanceof Error) return errorArg.message;
|
|
121
|
+
if (typeof errorArg === 'string') return errorArg;
|
|
122
|
+
if (errorArg && typeof errorArg === 'object' && 'message' in errorArg) {
|
|
123
|
+
return String((errorArg as { message: unknown }).message);
|
|
124
|
+
}
|
|
125
|
+
return String(errorArg);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Classify an ACME/provisioning failure into a retryable or terminal cause.
|
|
130
|
+
* Structured errors win over text matching: a `DomainOwnershipError` and any
|
|
131
|
+
* NON_RECOVERABLE CONFIGURATION `PlatformError` are permanent by declaration.
|
|
132
|
+
*/
|
|
133
|
+
export const classifyAcmeFailure = (errorArg: unknown): IAcmeFailureClassification => {
|
|
134
|
+
const message = extractMessage(errorArg);
|
|
135
|
+
|
|
136
|
+
if (errorArg instanceof DomainOwnershipError) {
|
|
137
|
+
return { reason: 'domain-ownership-unverified', permanent: true, message };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
errorArg instanceof PlatformError
|
|
142
|
+
&& errorArg.category === ErrorCategory.CONFIGURATION
|
|
143
|
+
&& errorArg.recoverability === ErrorRecoverability.NON_RECOVERABLE
|
|
144
|
+
) {
|
|
145
|
+
return { reason: 'acme-account-configuration', permanent: true, message };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const candidate of reasonPatterns) {
|
|
149
|
+
if (candidate.patterns.some((pattern) => pattern.test(message))) {
|
|
150
|
+
return { reason: candidate.reason, permanent: candidate.permanent, message };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { reason: 'unclassified', permanent: false, message };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Terminal ACME failure. HIGH severity so the automatic PlatformError log lands
|
|
159
|
+
* at `error` rather than being lost in provisioning warn noise, and
|
|
160
|
+
* NON_RECOVERABLE so `isRetryable()` and every downstream retry layer agree that
|
|
161
|
+
* this must not be retried.
|
|
162
|
+
*/
|
|
163
|
+
export class AcmePermanentFailureError extends PlatformError {
|
|
164
|
+
public readonly classification: IAcmeFailureClassification;
|
|
165
|
+
|
|
166
|
+
constructor(
|
|
167
|
+
classification: IAcmeFailureClassification,
|
|
168
|
+
operation: string,
|
|
169
|
+
component: string,
|
|
170
|
+
context: IErrorContext = {},
|
|
171
|
+
) {
|
|
172
|
+
super(
|
|
173
|
+
`${operation} failed permanently (${classification.reason}): ${classification.message}`,
|
|
174
|
+
DCR_ACME_PERMANENT_FAILURE,
|
|
175
|
+
ErrorSeverity.HIGH,
|
|
176
|
+
ErrorCategory.CONFIGURATION,
|
|
177
|
+
ErrorRecoverability.NON_RECOVERABLE,
|
|
178
|
+
{
|
|
179
|
+
component,
|
|
180
|
+
operation,
|
|
181
|
+
userMessage:
|
|
182
|
+
`${operation} cannot succeed until its configuration is fixed (${classification.reason}): ${classification.message}`,
|
|
183
|
+
...context,
|
|
184
|
+
data: {
|
|
185
|
+
acmeFailureReason: classification.reason,
|
|
186
|
+
...context.data,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
this.classification = classification;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
protected createWithContext(context: IErrorContext): PlatformError {
|
|
194
|
+
return new AcmePermanentFailureError(
|
|
195
|
+
this.classification,
|
|
196
|
+
this.context.operation || 'operation',
|
|
197
|
+
this.context.component || 'acme',
|
|
198
|
+
context,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -1,18 +1,58 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import { logger } from '../logger.js';
|
|
3
3
|
import type { DcRouter } from '../classes.dcrouter.js';
|
|
4
|
+
import {
|
|
5
|
+
AcmePermanentFailureError,
|
|
6
|
+
classifyAcmeFailure,
|
|
7
|
+
type TAcmeFailureReason,
|
|
8
|
+
} from './acme-failure-classification.js';
|
|
9
|
+
|
|
10
|
+
/** Terminal state of the SmartAcme provider startup budget. */
|
|
11
|
+
export interface IAcmeStartFailure {
|
|
12
|
+
reason: TAcmeFailureReason;
|
|
13
|
+
/** True when no further retry can resolve the cause. */
|
|
14
|
+
permanent: boolean;
|
|
15
|
+
/** Attempts consumed when the budget ended. */
|
|
16
|
+
attempts: number;
|
|
17
|
+
message: string;
|
|
18
|
+
at: number;
|
|
19
|
+
/** What an operator has to do to re-arm startup. */
|
|
20
|
+
rearmedBy: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Attempts allowed for transient SmartAcme startup failures before giving up. */
|
|
24
|
+
export const smartAcmeStartMaxAttempts = 20;
|
|
4
25
|
|
|
5
26
|
/**
|
|
6
27
|
* Background start/retry/stop lifecycle for the DcRouter-owned SmartAcme
|
|
7
28
|
* instance. SmartAcme startup can hit ACME rate limits, so startup runs in
|
|
8
29
|
* the background with generation-guarded exponential retry, and certificate
|
|
9
30
|
* provisioning is re-triggered once DNS-01 becomes ready.
|
|
31
|
+
*
|
|
32
|
+
* Retry semantics, which are NOT shared with the per-domain
|
|
33
|
+
* `CertProvisionScheduler` budget:
|
|
34
|
+
* - increments once per failed `smartAcme.start()`;
|
|
35
|
+
* - 5 s → 1 h exponential backoff with ±20% jitter;
|
|
36
|
+
* - capped at `smartAcmeStartMaxAttempts` transient attempts;
|
|
37
|
+
* - reset by `startInBackground()`, `stop()`, and a successful start;
|
|
38
|
+
* - re-armed only by `startInBackground()`, i.e. a SmartProxy rebuild, an
|
|
39
|
+
* explicit `rearm()` after a configuration change, or a process restart.
|
|
40
|
+
* Nothing re-arms it on a timer, so the terminal state is recorded in
|
|
41
|
+
* `startFailure` and logged at `error` instead of scrolling past as a warning.
|
|
42
|
+
*
|
|
43
|
+
* A permanent cause never enters the backoff at all: retrying a misconfigured
|
|
44
|
+
* ACME account cannot fix it, and spending the budget on it hides the reason.
|
|
10
45
|
*/
|
|
11
46
|
export class SmartAcmeLifecycle {
|
|
12
47
|
/** True once the SmartAcme DNS-01 provider finished starting. */
|
|
13
48
|
public ready = false;
|
|
14
49
|
/** Tracks whether the taskbuffer SmartAcme service is started, so SmartProxy rebuilds can re-kick startup. */
|
|
15
50
|
public serviceStarted = false;
|
|
51
|
+
/**
|
|
52
|
+
* Set when the startup budget ended — either immediately for a permanent cause
|
|
53
|
+
* or after the transient attempt cap. Cleared on every (re-)arm and on success.
|
|
54
|
+
*/
|
|
55
|
+
public startFailure?: IAcmeStartFailure;
|
|
16
56
|
|
|
17
57
|
private startGeneration = 0;
|
|
18
58
|
private startPromise?: Promise<void>;
|
|
@@ -30,14 +70,35 @@ export class SmartAcmeLifecycle {
|
|
|
30
70
|
const generation = ++this.startGeneration;
|
|
31
71
|
this.ready = false;
|
|
32
72
|
this.retryAttempt = 0;
|
|
73
|
+
this.startFailure = undefined;
|
|
33
74
|
this.clearRetryTimer();
|
|
34
75
|
this.scheduleStart(generation, 0);
|
|
35
76
|
}
|
|
36
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Re-arm startup after a configuration change that can plausibly fix a
|
|
80
|
+
* previously terminal cause (e.g. the ACME account settings were corrected).
|
|
81
|
+
* Without this, an exhausted or permanently-failed budget could only be reset
|
|
82
|
+
* by a SmartProxy rebuild or a full restart — and a dcrouter restart is a
|
|
83
|
+
* measured 30–60 s of total public outage.
|
|
84
|
+
*/
|
|
85
|
+
public rearm(reasonArg: string): boolean {
|
|
86
|
+
if (!this.dcRouterRef.smartAcme || !this.serviceStarted) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (this.ready && !this.startFailure) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
logger.log('info', `Re-arming SmartAcme DNS-01 provider startup: ${reasonArg}`);
|
|
93
|
+
this.startInBackground();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
37
97
|
public async stop(): Promise<void> {
|
|
38
98
|
this.startGeneration++;
|
|
39
99
|
this.ready = false;
|
|
40
100
|
this.retryAttempt = 0;
|
|
101
|
+
this.startFailure = undefined;
|
|
41
102
|
this.clearRetryTimer();
|
|
42
103
|
|
|
43
104
|
const smartAcme = this.dcRouterRef.smartAcme;
|
|
@@ -91,6 +152,7 @@ export class SmartAcmeLifecycle {
|
|
|
91
152
|
|
|
92
153
|
this.ready = true;
|
|
93
154
|
this.retryAttempt = 0;
|
|
155
|
+
this.startFailure = undefined;
|
|
94
156
|
logger.log('info', 'SmartAcme DNS-01 provider is now ready');
|
|
95
157
|
this.retriggerCertificateProvisioning();
|
|
96
158
|
} catch (err) {
|
|
@@ -102,9 +164,48 @@ export class SmartAcmeLifecycle {
|
|
|
102
164
|
await smartAcme.stop().catch((stopErr) => {
|
|
103
165
|
logger.log('warn', `Failed to clean up SmartAcme after startup failure: ${(stopErr as Error).message}`);
|
|
104
166
|
});
|
|
167
|
+
|
|
168
|
+
// Classify before spending any of the budget: a permanent cause is not made
|
|
169
|
+
// truer by 20 more attempts, and every attempt spent on it is an hour in
|
|
170
|
+
// which nothing tells the operator what is actually wrong.
|
|
171
|
+
const classification = classifyAcmeFailure(err);
|
|
172
|
+
if (classification.permanent) {
|
|
173
|
+
this.startFailure = {
|
|
174
|
+
reason: classification.reason,
|
|
175
|
+
permanent: true,
|
|
176
|
+
attempts: this.retryAttempt,
|
|
177
|
+
message: classification.message,
|
|
178
|
+
at: Date.now(),
|
|
179
|
+
rearmedBy: 'fixing the ACME configuration (which calls rearm()), a SmartProxy rebuild, or a process restart',
|
|
180
|
+
};
|
|
181
|
+
// Constructing this logs at `error` with the code and category attached.
|
|
182
|
+
new AcmePermanentFailureError(
|
|
183
|
+
classification,
|
|
184
|
+
'SmartAcme DNS-01 provider startup',
|
|
185
|
+
'smartacme-lifecycle',
|
|
186
|
+
{ data: { attempts: this.retryAttempt, rearmedBy: this.startFailure.rearmedBy } },
|
|
187
|
+
);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
105
191
|
this.retryAttempt++;
|
|
106
|
-
if (this.retryAttempt >
|
|
107
|
-
|
|
192
|
+
if (this.retryAttempt > smartAcmeStartMaxAttempts) {
|
|
193
|
+
this.startFailure = {
|
|
194
|
+
reason: classification.reason,
|
|
195
|
+
permanent: false,
|
|
196
|
+
attempts: this.retryAttempt - 1,
|
|
197
|
+
message: classification.message,
|
|
198
|
+
at: Date.now(),
|
|
199
|
+
rearmedBy: 'a SmartProxy rebuild, an explicit rearm() after a configuration change, or a process restart',
|
|
200
|
+
};
|
|
201
|
+
logger.log(
|
|
202
|
+
'error',
|
|
203
|
+
`SmartAcme DNS-01 provider gave up after ${smartAcmeStartMaxAttempts} startup attempts `
|
|
204
|
+
+ `(${classification.reason}): ${classification.message}. `
|
|
205
|
+
+ 'Nothing re-arms this on a timer — DNS-01 stays unavailable and every affected route falls back to a '
|
|
206
|
+
+ `no-op http-01 path until ${this.startFailure.rearmedBy}.`,
|
|
207
|
+
{ acmeFailureReason: classification.reason, attempts: this.startFailure.attempts },
|
|
208
|
+
);
|
|
108
209
|
return;
|
|
109
210
|
}
|
|
110
211
|
|
|
@@ -113,7 +214,7 @@ export class SmartAcmeLifecycle {
|
|
|
113
214
|
const delayMs = Math.min(baseDelayMs * Math.pow(2, this.retryAttempt - 1), maxDelayMs);
|
|
114
215
|
const jitter = 0.8 + Math.random() * 0.4;
|
|
115
216
|
const actualDelayMs = Math.floor(delayMs * jitter);
|
|
116
|
-
logger.log('warn', `SmartAcme DNS-01 provider startup failed: ${(err as Error).message}; retrying in ${actualDelayMs}ms (attempt ${this.retryAttempt}
|
|
217
|
+
logger.log('warn', `SmartAcme DNS-01 provider startup failed: ${(err as Error).message}; retrying in ${actualDelayMs}ms (attempt ${this.retryAttempt}/${smartAcmeStartMaxAttempts})`);
|
|
117
218
|
this.scheduleStart(generation, actualDelayMs);
|
|
118
219
|
} finally {
|
|
119
220
|
if (this.startPromise === startPromise) {
|
package/ts/acme/index.ts
CHANGED