import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../dist_ts_interfaces/index.js'; /** * Mirrors `SmartacmeCertMatcher.getCertificateDomainNameByDomainName` from * @push.rocks/smartacme. Inlined here because the original is `private` on * SmartAcme. The cert identity ('task.vc' for both 'outline.task.vc' and * '*.task.vc') is what AcmeCertDoc is keyed by, so two route domains with * the same identity share the same underlying ACME cert. * * Returns undefined for domains with 4+ levels (matching smartacme's * "deeper domains not supported" behavior) and for malformed inputs. * * Exported for unit testing. */ export declare function deriveCertDomainName(domain: string): string | undefined; export declare class CertificateHandler { private opsServerRef; typedrouter: plugins.typedrequest.TypedRouter; constructor(opsServerRef: OpsServer); private requireAuth; private requireCertificateAuth; private registerHandlers; /** * Build domain-centric certificate overview. * Instead of one row per route, we produce one row per unique domain. */ private buildCertificateOverview; private buildSummary; /** * Domain-based reprovisioning — clears backoff first, refreshes the smartacme * cert (when forceRenew is set), then re-applies routes so the running Rust * proxy actually picks up the new cert. * * Why applyRoutes (not smartProxy.provisionCertificate)? * smartProxy.provisionCertificate(routeName) routes through the Rust ACME * path, which is forcibly disabled whenever certProvisionFunction is set * (smart-proxy.ts:168-171). The only path that re-invokes * certProvisionFunction → bridge.loadCertificate is updateRoutes(), which * we trigger via routeConfigManager.applyRoutes(). */ private reprovisionCertificateDomain; /** * After triggering the apply pipeline, check whether the certificate the proxy * is actually serving is still expired. If it is, the reprovision did not * propagate and reporting success would be a lie. * * The known cause is SmartProxy's own per-domain provisioning cooldown * (`certProvisionFailureCooldownMs`, 30 min default). It is private in-memory * state with no invalidation path: a provisioning sweep skips a cooling-down * domain, and because the sweep returns before its summary log when every * domain is skipped, the skip is not logged either. So a valid certificate can * sit in storage while the proxy keeps serving the expired one, silently. * * Returns undefined when propagation looks fine or cannot be determined — * `getCertificateStatus` goes through the Rust bridge and may be unavailable. */ private describeCertPropagationGap; /** * After a force-renew, walk every route in the smartproxy that resolves to * the same cert identity as `forcedDomain` and write the freshly-issued cert * PEM into ProxyCertDoc for each. This guarantees that the next applyRoutes * → provisionCertificatesViaCallback iteration will hot-swap every sibling's * rust loaded_certs entry with the new (correct) PEM, rather than relying on * the in-memory cert returned by smartacme's per-domain cache. * * Why this is necessary: * Rust's `loaded_certs` is a HashMap. Each * bridge.loadCertificate(domain, ...) only swaps that one entry. The * fire-and-forget cert provisioning path triggered by updateRoutes does * eventually iterate every auto-cert route, but it returns the cached * (broken pre-fix) cert from smartacme's per-domain mutex. With this * helper, ProxyCertDoc is updated synchronously to the correct PEM before * applyRoutes runs, so even the transient window stays consistent. */ private propagateCertToSiblings; /** * Delete certificate data for a domain from storage */ private deleteCertificate; /** * Export certificate data for a domain as ICert-shaped JSON */ private exportCertificate; /** * Import a certificate from ICert-shaped JSON */ private importCertificate; }