import { PlatformError, type IErrorContext } from '../errors/base.errors.js'; /** * ACME failure classification. * * dcrouter has two independent ACME retry budgets, and neither used to look at * *why* a failure happened: * * 1. `SmartAcmeLifecycle` — SmartAcme provider startup. 5 s→1 h exponential * backoff with jitter, hard cap of 20 attempts, then permanent give-up. * Only a SmartProxy rebuild or a process restart re-arms it. * 2. `CertProvisionScheduler` — per-domain certificate provisioning. * `min(failures², 24 h)` backoff, **no cap**, re-armed by time forever. * This is the budget that reached 31–45 failures on the broken domains. * * Retrying is correct for rate limits, DNS propagation and transport faults. It * is never correct for a configuration cause: a hostname with no managed domain * cannot acquire one by waiting, so every one of those attempts was a silent * no-op that also kept the real reason out of the operator's view. Permanent * causes must therefore skip the budget entirely and surface attributably. * * Unclassified causes stay transient on purpose. Guessing "permanent" would * strand recoverable domains, so the default preserves existing retry behaviour; * only causes we can positively recognise are treated as terminal. */ export type TAcmeFailureReason = /** Ownership of the hostname could not be proven (no DomainDoc / no provider zone / not delegation-verified). */ 'domain-ownership-unverified' /** The DNS-01 dispatcher found no managed zone able to hold the challenge record. */ | 'no-managed-dns-zone' /** No challenge handler / provider is wired for this domain at all. */ | 'no-challenge-handler' /** The ACME account itself is misconfigured (email, terms, key, directory URL). */ | 'acme-account-configuration' /** CAA forbids our issuer — only the domain holder can change this. */ | 'caa-forbids-issuance' /** ACME server rate limit — retrying is exactly right. */ | 'rate-limited' /** Challenge not yet visible, propagation delay, transport fault. */ | 'transient' /** Nothing recognised. Treated as transient so recoverable causes keep retrying. */ | 'unclassified'; export interface IAcmeFailureClassification { reason: TAcmeFailureReason; /** True when no retry can resolve the cause, so the retry budget must not be consumed. */ permanent: boolean; message: string; } /** * Classify an ACME/provisioning failure into a retryable or terminal cause. * Structured errors win over text matching: a `DomainOwnershipError` and any * NON_RECOVERABLE CONFIGURATION `PlatformError` are permanent by declaration. */ export declare const classifyAcmeFailure: (errorArg: unknown) => IAcmeFailureClassification; /** * Terminal ACME failure. HIGH severity so the automatic PlatformError log lands * at `error` rather than being lost in provisioning warn noise, and * NON_RECOVERABLE so `isRetryable()` and every downstream retry layer agree that * this must not be retried. */ export declare class AcmePermanentFailureError extends PlatformError { readonly classification: IAcmeFailureClassification; constructor(classification: IAcmeFailureClassification, operation: string, component: string, context?: IErrorContext); protected createWithContext(context: IErrorContext): PlatformError; }