-
DcRouter v32.3.1
StableRelease / build-and-release (push) Successful in 15m46sreleased this
2026-09-24 12:21:48 +00:00 | 25 commits to main since this release2026-09-24 - 32.3.1
Fixes
- Pass the ACME renewal threshold to SmartAcme, and consume
@push.rocks/smartacme9.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 inrenewAfter. A failed renewal keeps the current certificate stored.updateAcmeConfigrefuses 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 anAcmePermanentFailureError(invalid-renewal-threshold) that points to Domains > Certificates > Settings andupdateAcmeConfig, and certificate issuance reportsacme_unavailable, while SmartProxy and the OpsServer keep running for the correction.test/test.acme-renewal-threshold.node.tsruns 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/client1.5.1, which the tree already carried, so the deprecated@push.rocks/smartdata7.4.0 leaves the lockfile. - Defer certificate provisioning while SmartAcme is not ready, and consume
@push.rocks/smartproxy27.22.0 (from 27.21.0), whosecertProvisionFunctionmay answer'defer'. dcrouter answered SmartProxy's provision callback withacme_unavailablewhile 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, afterrearm()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.tsruns 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 andrearm(). It also rebuilds SmartProxy on the sameDcRouterafter 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
reprovisionCertificateDomainonly 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 newDcRouter.reprovisionProxyCertificate(), which lifts the domain's cooldown with smartproxy 27.22.0'sinvalidateCertProvisionCooldown()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.tsfails 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,
renewThresholdDaysincluded, when dcrouter builds them, so a change takes effect at the next restart or SmartProxy rebuild. The config manager, theupdateAcmeConfigcontract 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_unverifiedinstead 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.tsinterrupts 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()afterstop()on the same instance failed withCritical 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 instart()and detaches both links instop(), also when its cleanup fails. A failedstart(), 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
DcRouteris stopped and started, and consume@push.rocks/taskbuffer9.0.4 (from 9.0.3), which carries the root fix. taskbuffer 9.0.3'sServiceManager.stop()unsubscribed the forwarders that feedserviceSubjectandstart()never recreated them, and dcrouter subscribed toserviceSubjectonce in its constructor and ended that subscription instop(), so the ops log received no service event after the first restart.DcRouternow subscribes instart()and unsubscribes instop(), and also whenstart()fails.test/test.opsserver-restart.node.tsshows that the second run'sService 'OpsServer': startedevent 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/tsbuild5.0.0 (from 4.5.1),@git.zone/tsrun3.0.0 (from 2.0.6),@git.zone/tstest6.2.0 (from 6.1.5) and@git.zone/tswatch4.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/node26.6.1 stay until their successors are seven days old. No source folder carries its owntsconfig.jsonortspublish.jsoncompiler options, so tsbuild 5's folder configuration needs no migration. Under tsrun 3 thecli.ts.jsdevelopment entry gives the script Node's standardprocess.argv, whichrunCli()already reads withprocess.argv.slice(2); tsrun 2 passed the user arguments alone, so that slice dropped them, andnode cli.ts.js --versionnow prints the version. Thedcrouter-devwatcher in.smartconfig.jsonkeeps its shape under tswatch 4. tswatch 4 brings@api.global/typedserver13 and@api.global/typedsocket8.5.0 into the dev tooling only; dcrouter's runtime dependencies are unchanged in the lockfile.
Downloads
- Pass the ACME renewal threshold to SmartAcme, and consume