• DcRouter v32.3.1
    Release / build-and-release (push) Successful in 15m46s
    Stable

    jkunz released this 2026-09-24 12:21:48 +00:00 | 25 commits to main since this release

    2026-09-24 - 32.3.1

    Fixes

    • Pass the ACME renewal threshold to SmartAcme, and consume @push.rocks/smartacme 9.8.1 (from 9.7.0), which honours it. SmartAcme received no threshold and kept a stored certificate until 10 days before expiry, so SmartProxy's daily renewal sweep, which asks from the configured 30 days on, got the old certificate back every day until the 10-day mark. The configured threshold (default 30 days) now governs both: a certificate is renewed once at most that many days, or a third of its lifetime if that is less, remain, and testing certificates carry the same margin in renewAfter. A failed renewal keeps the current certificate stored. updateAcmeConfig refuses a threshold that is not a positive number of days, because SmartAcme refuses it at construction. One already stored, by an earlier build or a direct database write, leaves ACME unavailable: dcrouter builds neither SmartAcme nor SmartProxy's ACME options from it, logs an AcmePermanentFailureError (invalid-renewal-threshold) that points to Domains > Certificates > Settings and updateAcmeConfig, and certificate issuance reports acme_unavailable, while SmartProxy and the OpsServer keep running for the correction. test/test.acme-renewal-threshold.node.ts runs dcrouter's provision path against SmartAcme's loopback CA: a stored 90-day certificate with 20 days left is renewed under the default threshold and kept under a configured 10 days. It also starts dcrouter on a stored threshold of 0: SmartProxy runs without SmartAcme, the OpsServer lists the error in its logs and accepts a corrected threshold. smartacme 9.8.1 stores through @lossless.org/client 1.5.1, which the tree already carried, so the deprecated @push.rocks/smartdata 7.4.0 leaves the lockfile.
    • Defer certificate provisioning while SmartAcme is not ready, and consume @push.rocks/smartproxy 27.22.0 (from 27.21.0), whose certProvisionFunction may answer 'defer'. dcrouter answered SmartProxy's provision callback with acme_unavailable while SmartAcme was starting, retrying or out of its startup budget. SmartProxy recorded that as a failure and skipped the domain for its 30-minute cooldown, including in the sweep dcrouter runs once SmartAcme becomes ready, so after every restart the routes that needed a certificate got none until a sweep ran after the cooldown. The callback now defers whenever issuance is not ready, which also covers ACME being disabled and an account change waiting for the rebuild. SmartProxy records nothing for a deferral, and the sweep that follows SmartAcme becoming ready, after rearm() too, issues the certificate. The log line for an exhausted SmartAcme startup budget, a code comment and the readme said the callback fell back to HTTP-01; certificate provisioning is DNS-01 only, and they now say what the code does. test/test.acme-provision-cooldown.node.ts runs a real SmartProxy with dcrouter's callback and SmartAcme lifecycle against SmartAcme's loopback CA: the startup sweep is deferred without a cooldown and the domain is issued once SmartAcme is ready, and the same holds after a permanent startup failure and rearm(). It also rebuilds SmartProxy on the same DcRouter after ACME was disabled and shows that neither SmartAcme nor the provision callback remains.
    • Let an operator reprovision take effect at once after a failed attempt. A failed provisioning attempt puts the domain on SmartProxy's 30-minute provisioning cooldown, and reprovisionCertificateDomain only re-applied the routes, whose sweep skipped the cooling-down domain without a log line, so the reprovision did nothing until the cooldown ended. The handler now goes through the new DcRouter.reprovisionProxyCertificate(), which lifts the domain's cooldown with smartproxy 27.22.0's invalidateCertProvisionCooldown() before it re-applies the routes, and the sweep asks for the certificate again. A domain that was not cooling down is reprovisioned as before. The message returned while the proxy still serves an expired certificate said SmartProxy exposed no per-domain invalidation and that toggling the route off and on was the only remedy; it now says that the background provisioning sweep had not replaced the certificate when the check ran, or failed. test/test.acme-provision-cooldown.node.ts fails an attempt on a zone whose delegation is not verified yet, verifies it and reprovisions: the certificate is issued without waiting for the cooldown.
    • Correct the documented reload semantics of the ACME settings. SmartProxy and SmartAcme receive them, renewThresholdDays included, when dcrouter builds them, so a change takes effect at the next restart or SmartProxy rebuild. The config manager, the updateAcmeConfig contract and the settings dialog said the threshold applied to the next renewal check at once.
    • Store a renewed certificate's id with its material. SmartAcme now renews every certificate by storing the replacement over the due one, as forced renewals already did, and dcrouter's certificate store kept the old id on an overwrite, so the record, and a certificate exported from it, named a certificate it no longer held.
    • Keep an abandoned testing certificate job from ordering a replacement. smartacme 9.8.1 replaces an abandoned exact-identifier attempt with a new key and order on the next request once an hour has passed, where 9.7.0 answered it as pending for good. The grant reservation already kept new jobs out, but a job claimed again after its abandon, because recording the result was interrupted, requested the certificate and relied on that old answer. A job now publishes an attempt it finds abandoned as abandoned_unverified instead of requesting it. The readme and the job model state that abandonment is final at dcrouter's job layer, not in SmartAcme. test/test.testing-certificates.node.ts interrupts the recording of an abandon, lets SmartAcme's retry time pass and shows that the job claimed again stays abandoned without a request.
    • Let a stopped DcRouter, or one whose start failed, start again. start() after stop() on the same instance failed with Critical service 'OpsServer' failed to start: a TypedHandler for getAdminBootstrapStatus alredy exists!, because each OpsServer linked its routers into the DcRouter's main typedrouter and never detached them, so the new OpsServer's handlers collided with the stopped one's. OpsServer now links into that router in start() and detaches both links in stop(), also when its cleanup fails. A failed start(), such as one whose port is taken, runs the same cleanup before it rethrows its error, because the service manager never stops a service whose start failed; a failing cleanup is raised together with the start error. Either way the next OpsServer composes into a router that no longer reaches the previous one's handlers.
    • Log service events again after a DcRouter is stopped and started, and consume @push.rocks/taskbuffer 9.0.4 (from 9.0.3), which carries the root fix. taskbuffer 9.0.3's ServiceManager.stop() unsubscribed the forwarders that feed serviceSubject and start() never recreated them, and dcrouter subscribed to serviceSubject once in its constructor and ended that subscription in stop(), so the ops log received no service event after the first restart. DcRouter now subscribes in start() and unsubscribes in stop(), and also when start() fails. test/test.opsserver-restart.node.ts shows that the second run's Service 'OpsServer': started event reaches the ops log; the test fails with taskbuffer 9.0.3 and passes with 9.0.4.

    Maintenance

    • Build, test and watch tooling on its current line: @git.zone/tsbuild 5.0.0 (from 4.5.1), @git.zone/tsrun 3.0.0 (from 2.0.6), @git.zone/tstest 6.2.0 (from 6.1.5) and @git.zone/tswatch 4.0.1 (from 3.3.5); tsbundle 2.14.0, tsdocker 3.8.1 and tsdeno 1.8.0 were already current, and pnpm 12.4.2 and @types/node 26.6.1 stay until their successors are seven days old. No source folder carries its own tsconfig.json or tspublish.json compiler options, so tsbuild 5's folder configuration needs no migration. Under tsrun 3 the cli.ts.js development entry gives the script Node's standard process.argv, which runCli() already reads with process.argv.slice(2); tsrun 2 passed the user arguments alone, so that slice dropped them, and node cli.ts.js --version now prints the version. The dcrouter-dev watcher in .smartconfig.json keeps its shape under tswatch 4. tswatch 4 brings @api.global/typedserver 13 and @api.global/typedsocket 8.5.0 into the dev tooling only; dcrouter's runtime dependencies are unchanged in the lockfile.
    Downloads