-
DcRouter v33.0.1
StableRelease / build-and-release (push) Successful in 34m10sreleased this
2026-10-17 17:16:09 +00:00 | 2 commits to main since this release2026-10-17 - 33.0.1
Fixes
- Return which reconciler owns a DNS record (ops API).
getDnsRecordsandgetDnsRecordleft outmanagedBy,managedOwnerIdandmanagedRecordKey, whichIDnsRecordalready declared, so every record read as operator-owned, including records a gateway route owns and removes when the route is deleted. They now carry the three fields when a reconciler owns the record and omit them for operator and provider records, so an operator can tell the records a route deletion will remove from their own. The MCPdcrouter_list_dns_recordssummaries carrymanagedByandmanagedOwnerId(smartmcp redacts any field named like a key, somanagedRecordKeyis left out there); gateway client DNS record reads keep theIGatewayDnsRecordshape, which has no ownership fields.
Maintenance
- Release tooling:
@git.zone/tsbundle3.7.0 → 3.7.1.
Downloads
- Return which reconciler owns a DNS record (ops API).
-
DcRouter v33.0.0
StableRelease / build-and-release (push) Successful in 19m49sreleased this
2026-10-16 23:29:55 +00:00 | 5 commits to main since this release2026-10-16 - 33.0.0
Breaking Changes
@push.rocks/smartmta^9.5.0 → ^13.0.1. dcrouter re-exportsUnifiedEmailServer,IUnifiedEmailServerOptions,IEmailRouteandIEmailDomainConfigfrom it and@serve.zone/dcrouter-apiclienttypes the email settings'emailConfigagainst it, so consumers see SmartMTA 10–13's changes: inbound mail is parsed with@push.rocks/smartmail3 (postal-mime) instead ofmailparser;Email.headersholds the raw field values as written (unfolded, a repeated field joined with,) instead of mailparser's interpreted forms;Email.fromis the first mailbox of the From fields in message order with groups flattened, and group members count as header To/Cc;baseLoggeris a@push.rocks/smartlog5 instance;Email.toSmartmail()/fromSmartmail()use smartmail 3; the Linux mail engine is a static musl executable nameddist_rust/mailer-bin_linux_amd64_musl/mailer-bin_linux_arm64_muslwith its third-party notices next to it (<binary>.third-party-notices.{json,txt}) and innative-notices/, instead ofmailer-bin_linux_amd64/_arm64,notices/andthird-party-notices.md; therawMessageSmartMTA hands toonAcceptEnvelope/onMessageDatakeeps the CRLF that ends the message's last line. dcrouter itself references none of those paths.- Email route
match.headerssemantics follow the raw headers: mailparser's derived keyspriorityandlistno longer exist, so a route that names them never matches again; name the raw field (x-priority,importance,list-id,list-unsubscribe, …) instead. dcrouter's own generated routes use no header matches. engines.nodeis>=22.12.0(was>=22), the floor SmartMTA 11.1 and its dependencies need.- WorkApp SMTP identities are composed as scoped
auth.accounts(sender scope = the identity's address) instead of plainauth.users, and their outbound relay routes matchauthenticatedUser.emailConfig.auth.usersandauthUserCountingetEmailServerSettingstherefore no longer include WorkApp credentials.
Security
- The two sender-authorization issues below need a valid SMTP credential of the dcrouter instance (a WorkApp credential, an operator
auth.userslogin or an SMTP account); an unauthenticated sender cannot use them. Upgrade to 33.0.0. - Enforce WorkApp sender authorization in SmartMTA's scope checker (email). A WorkApp SMTP credential could put another tenant's address into the header From in forms dcrouter's own From parsing misread (a quoted display name or comment that looks like an address, a group, a second From field far into the header section) and have the message relayed through the other tenant's route and signed with that tenant's DKIM key. dcrouter no longer parses From itself: each WorkApp credential is an SMTP account scoped to its own address, and SmartMTA refuses with 553 any MAIL FROM and any header From mailbox outside that scope.
- Bind WorkApp outbound relay routes to their own credential (email). The routes matched any authenticated session whose sender was the identity's address, so an operator
auth.userslogin or an unscoped SMTP account could send as a WorkApp address through that identity's route and DKIM key. The routes now also matchauthenticatedUser; other credentials relay only through routes the operator configured for them. - Refuse SMTP envelope addresses that carry CR, LF or NUL before connecting (email). SmartMTA 13's Rust SMTP client refuses such an envelope sender or recipient, which could otherwise end the MAIL FROM or RCPT TO command and inject further SMTP commands, as a permanent delivery failure (
errorType: 'envelope', not retried). dcrouter does not call SmartMTA's Rust bridge directly: its outbound mail goes through SmartMTA's server and durable queue, whose raw path (CoreMail handoffs) already refused CR and LF in TypeScript, so no dcrouter path is known to have passed such an address; the check closes the gap below that layer. - Read an oversized message to its end before refusing it (email). dcrouter's SMTP listener sent 552 as soon as a message crossed its size limit (
emailConfig.maxMessageSize, 10 MiB by default) and then read the rest of the message as SMTP commands, so lines in the body (RSET,MAIL FROM,RCPT TO,DATAand a further message) were executed in that session. Any SMTP client can trigger this without a credential, on every listener port. A client gains nothing it could not send as commands itself; the harm is to a client that relays a body someone else wrote, such as an authenticated submission client or an upstream MTA forwarding mail: commands hidden in an oversized body ran with that client's authentication and connection source, so a third party could inject messages under the client's identity, and the client received replies out of step. SmartMTA 13.0.1 reads the message to its end-of-data marker, refuses it with 552 and continues the session; commands pipelined after the end-of-data marker are answered in order instead of being dropped.
Fixes
- Remove the probe for SmartMTA's
smtpAuthAccountsAndScopescapability and its fallback, which dropped stored SMTP accounts and madecreateAccountrefuse; SmartMTA 12 and later always have the capability. - The production dependency tree no longer contains
mailparser,nodemaileror theirhtml-to-text/deepmerge-tssubtree. - Store received mail with the CRLF that ends its last line (email). SmartMTA's DATA reader removed it with the end-of-data marker, so the accepted-email spool stored each SMTP message without its final CRLF and the CoreMail inbound handoff passed that copy on. With SmartMTA 13.0.1 and the byte-exact spool below, the stored copy is the received message exactly (dot-stuffing removed), behind the
X-Dcrouter-Cached-Email-Idheader dcrouter prepends, and the CoreMail inbound handoff carries those bytes, final CRLF included. dcrouter never compensated for the missing CRLF, so nothing else changes; messages stored before the upgrade are not rewritten.test/test.smtp-received-bytes.node.tscovers it end to end. - Store and hand on received mail byte for byte (email). The accepted-email spool decoded every received message as UTF-8 and encoded it again, so each byte that is not valid UTF-8 (an 8-bit body in ISO-8859-1 or another legacy charset, raw binary content) was replaced with U+FFFD in the stored copy, in the CoreMail inbound handoff (the bytes it hashes and uploads) and in every relay of the stored copy (SmartMTA handoff, durable envelope dispatch and their retries). Such mail reached CoreMail mailboxes and relay recipients altered; 7-bit and valid UTF-8 mail was not affected. The spool now keeps the message as bytes from SmartMTA to storage and to every relay. Messages stored before the upgrade stay as stored: their replaced bytes cannot be recovered. WorkApp typed endpoints still receive the message as the UTF-8 string their
@serve.zone/interfacescontract defines. - Write the
X-Dcrouter-Cached-Email-Idheader into the header section only (email). Before prepending its own, the spool removed every line starting withX-Dcrouter-Cached-Email-Id:anywhere in the message, so such a line in the body (a quoted or forwarded header, for example) was deleted from the stored and relayed copy. Only fields of that name in the header section, with their folded lines, are now replaced. The header line ends like the message's first line, so a message with bare-LF line endings no longer gets one CRLF line.
Maintenance
- Release tooling: pnpm 12.8.2 → 12.11.2,
@git.zone/tstest7.2.0 → 8.0.0,@git.zone/tsbundle3.2.0 → 3.7.0,@git.zone/tswatch7.0.0 → 7.1.1,@git.zone/tsdocker3.11.0 → 3.12.0 and@types/node26.6.3 → 26.6.5;@git.zone/tsbuild6.0.2 and@git.zone/tsrun3.0.3 are current. tstest 8 drops the Deno runtime and refuses a test file whose name namesdeno, so the new sender-scope test istest/test.smtp-sender-scope.node.tsand runs on Node.js, the runtime dcrouter ships; no file carries a// tstest:deno:*directive. dcrouter's tsbundle configuration (one esbuild browser bundle) uses none of the options tsbundle 3.3–3.7 added (3.7.0 adds thetsbundle noticescommand; 3.6.2 fixes esbuild browser bundles that failed withENOENTon a module a package'sbrowserfield disables), and the tswatch and tsdocker updates change nothing dcrouter configures.@git.zone/tsdenostays on 1.15.0: it only drivesbuild:binaryand leaves the project with the move to a Node.js single executable application. The Dockerfile's base image digests are unchanged.
Downloads
-
DcRouter v32.7.0
StableRelease / build-and-release (push) Successful in 32m59sreleased this
2026-10-05 20:43:22 +00:00 | 19 commits to main since this release2026-10-05 - 32.7.0
Fixes
- Push every renewal of the SMTP certificate to the running listener (email). A renewal pushed to the listener used to be copied into the runtime email configuration as inline PEM, which the next resolution served ahead of the certificate store, so only the first renewal per process reached the listener and later renewals re-pushed the old certificate. Resolved material now stays out of the configuration and every listener start or renewal reads the store again. A renewal also applies when its certificate is stored under a wildcard or sibling route domain and covers the mail hostname, not only when it is stored under the mail hostname itself.
- Report a served SMTP certificate that is expired, not yet valid, does not cover the mail hostname or an enabled mail-edge hostname, or does not match its key as an error in the log and in
getEmailServerSettings, instead of serving it silently.
Features
- Add the email server setting
tlsSource(managedCertificate|configuredMaterial) to choose where the SMTP certificate comes from (email).updateEmailServerSettingsswitches it in-process; switching tomanagedCertificateis refused while no currently valid stored certificate covers the mail hostname and every enabled mail-edge hostname, and removes the configuredcertPath/keyPath/inline PEM when accepted. Settings stored without the field keep their behaviour: configured material while it is present, otherwise the stored certificate. - Report the served SMTP certificate in
getEmailServerSettingsastlsCertificate(source, origin, DNS names,notBefore/notAfter, required hostnames, problems) (email).
Maintenance
- Release tooling: pnpm 12.5.1 → 12.8.1,
@git.zone/tsbuild5.0.0 → 5.1.2,@git.zone/tsbundle3.0.0 → 3.1.3,@git.zone/tsdeno1.12.0 → 1.15.0,@git.zone/tsdocker3.9.0 → 3.11.0,@git.zone/tstest6.3.2 → 7.1.0,@git.zone/tswatch4.0.2 → 6.0.1 and@types/node26.6.2 → 26.6.3;@git.zone/tsrun3.0.2 is current. No test uses the tstest helpers whose results changed in 6.3.3–7.0.0 (TapNodeTools.runCommand()no longer folds stderr intostdout,createHttpsCert()writes a PKCS#8 key,createBrowser()), and thetestscript's options stay valid under tstest 7. tswatch 5 and 6 keep the@git.zone/tswatchwatcher configuration unchanged; tswatch 6 bundles through tsbundle 3, whichdcrouter-devdoes not use (its command runspnpm run build, already on tsbundle 3 with third-party notices). tsdeno stays on 1.x: 2.0.0 changestsdeno compileto embed exactly the versionspnpm-lock.yamlpins and fail on any difference, a lockfile-model migration ofbuild:binarythat gets its own change. pnpm 12.8.2 and@types/node26.6.4 wait for the seven-day rule.
Downloads
-
DcRouter v32.6.3
StableRelease / build-and-release (push) Successful in 21m45sreleased this
2026-09-29 05:07:41 +00:00 | 22 commits to main since this release2026-09-29 - 32.6.3
Maintenance
- Release tooling:
@git.zone/tsdeno^1.11.0→^1.12.0,@git.zone/tsdocker^3.8.1→^3.9.0,@git.zone/tswatch^4.0.1→^4.0.2and@git.zone/tsrun^3.0.1→^3.0.2; none needs a migration. tsdeno 1.12.0 reads the module graph withdeno infobefore each compile and fails withUNRESOLVED_DEPENDENCYinstead of writing a binary without an npm package Deno could not resolve, the failure behind the short 32.6.1 binaries; tsdocker 3.9.0 adds--repositorytopush, which dcrouter does not use; tswatch 4.0.2 keeps watching after a failed bundle build. tsrun 3.0.2 no longer loads two modules it never used, which starts everytsrunprocess about a second sooner; tsbuild 5.0.0, tsbundle 3.0.0 and tstest 6.3.2 are current; pnpm 12.5.1 and@types/node26.6.2 stay until pnpm 12.6.0 (2026-09-29 17:08 UTC) and@types/node26.6.3 (2026-10-02) are seven days old. @design.estate/dees-catalog^16.6.0→^16.12.0(locked 16.6.0 → 16.12.0), which brings@design.estate/dees-element3.3.0 → 3.4.0; the tree keeps one copy of each. The Ops UI binds noframelessand uses neither the stepper's removedscrollernorsetScrollStatus(), so no source changes. What looks different comes from the catalog: the login screen (dees-simple-login) drops its card surface and page backdrop, the bootstrap and email-domain steppers show the new progress ribbon with a close control in the step header, anddees-input-listrows take the reworked layout.- The SmartDB upgrade/rollback test gives its four restart phases one shared 50 s budget, inside the suite's 60 s per-file limit, instead of 20 s each, so one phase slowed by host load no longer fails a file that still has time; a timeout names the phase that ran out. It starts the fixture with
tsrundirectly rather than throughpnpm exec, which did not forward SIGTERM, so a timed-out fixture no longer outlives the test and re-creates its work directory after cleanup.
Downloads
- Release tooling:
-
DcRouter v32.6.2
StableRelease / build-and-release (push) Successful in 22m2sreleased this
2026-09-28 02:50:05 +00:00 | 27 commits to main since this release2026-09-28 - 32.6.2
Fixes
-
Move the in-process RemoteIngress hub to
@serve.zone/remoteingress^32.0.3(from^32.0.0, locked at 32.0.0), whose QUIC control stream is read cancel-safely on both sides. Up to 32.0.2 a control message was read as a 5-byte header and then its payload with tworead_exactcalls inside atokio::select!, and written in two writes that could arrive in separate packets; when another branch won after the header was consumed, the next read decoded the payload as a header. A splitCTRL_UDP_SESSION_CLOSEframe made the edge refusecontrol message too large: <N> bytes (max 65536)and tear its tunnel down, dropping every port listener of that edge while the hub loggedcontrol stream error: connection lost- the teardowns seen on the SSH tunnel tocode.foss.global:29419. From 32.0.3 the hub keeps a partially received control message buffered across cancelled reads and writes each message as one buffer, so it no longer loses a frame an edge sent, and the frames it sends no longer leave the header and payload in separate writes. The hub API dcrouter uses (RemoteIngressHub, its events,start,stop,getStatus,updateAllowedEdges, the egress proxy calls andencodeConnectionToken) is unchanged across 32.0.1-32.0.3, and the wire format is unchanged, so no dcrouter code changes. Edges read the hub's control stream with their own reader: an edge is fully protected only once it runs remoteingress 32.0.3 as well (see the readme's RemoteIngress version note). -
The release binaries resolve a first-party release published the same day. Deno 2.9 compiles with a 24-hour minimum dependency age by default, which left
@serve.zone/remoteingress@^32.0.3unresolved indeno compile(the smoke check caught it).deno.jsonnow commits the owner's policy that pnpm already applies: seven days for third-party packages, owner scopes (@api.global,@apiclient.xyz,@design.estate,@git.zone,@idp.global,@lossless.org,@push.rocks,@serve.zone,@tempfix,@tsclass) exempt, so every build host compiles under the same age policy whatever its own pnpm configuration (tsdeno 1.11.0 leaves a committed policy untouched). Deno resolves thepackage.jsonranges against the registry at compile time, notpnpm-lock.yaml. The binaries return to their complete size (816 MiB x64, 798 MiB arm64 from 564 npm packages, the size of the June builds): the 32.6.1 binaries (about 475 MiB) were most likely compiled without owner packages published within 24 hours of the build, which the--versionsmoke check cannot see.
Maintenance
@design.estate/dees-catalog^16.4.0→^16.6.0(locked 16.5.0 → 16.6.0).- Release tooling:
@git.zone/tsdeno1.11.0 and@git.zone/tsbundle3.0.0 (the dashboard bundle shipsdist_serve/bundle.js.third-party-notices.{txt,json}); pnpm 12.6.0 and@types/node26.6.3 wait for the seven-day rule.
Downloads
-
-
DcRouter v32.6.1
StableRelease / build-and-release (push) Successful in 19m53sreleased this
2026-09-27 21:05:57 +00:00 | 32 commits to main since this release2026-09-27 - 32.6.1
Fixes
- The Ops UI's application log (Logs > Application Logs) hands the log each entry once. It counted the entries it had passed on, so once the browser held its cap of 2000 entries no new entry reached the log, returning to the tab showed an empty log until the next entry arrived, and a view that was attached again passed every entry a second time. It now remembers the newest entry the log on screen holds and passes the entries after it; a fetched list that replaces the shown one clears the log first, and a log created anew, on returning to the tab, gets the whole list.
test/test.logs-view.chromium.tscovers streaming, the cap, a replacing fetch and the return to the tab. ts_web/appstate/runtime.tsimports the platform-events state statically. It loaded it through a dynamicimport(), so a bundle evaluated that module lazily and an element that readconfigEventStatePartwhile it was constructed, as the logs view does, could find it undefined; the Chromium test bundle did.
Maintenance
-
Move the Ops UI to
@design.estate/dees-catalog^10.0.0→^16.4.0(16.5.0 in the lockfile),@design.estate/dees-element^3.1.1→^3.3.0and@serve.zone/catalog^4.0.0→^33.0.0, which renders on dees-catalog 16; the three move together, because two copies of the catalog register the samedees-*elements and the page stops withNotSupportedError.pnpm dedupeleaves one copy each of dees-catalog, dees-element, dees-domtools (3.0.1), lit and@serve.zone/catalogin the bundle; it also drops duplicate resolutions of@types/node26.6.1,@push.rocks/smartrust2.0.0,@push.rocks/smartwatch6.4.3,@design.estate/dees-commsandpicomatch. The one source change the catalog needs is in the application log: dees-catalog 14 removeddees-chart-log'sterminalReady, and the log keeps entries it is given before its terminal is up, so the view no longer waits for it (see Fixes). The other catalog breaks between 11 and 16 do not touch the Ops UI: it usesdees-simple-appdashrather thandees-appui, gives everydees-chart-areaits ownyAxisFormatter, reads noseriesStats, binds novintegratedand uses thesz-*elements, whose API did not change from@serve.zone/catalog4 to 33. What looks different comes from the catalog: button glyph size and inset (15.2.0), stats-grid titles that break between words (16.3.0), the log on xterm 6 with the theme's terminal palette (14.0.0), and Enter in the last field of a dialog's form now runs the dialog's last action (16.3.0). -
Bring release tooling current:
@git.zone/tstest^6.3.0→^6.3.2(a Node.js test file that runs past--timeoutnow fails even when it handles SIGTERM and exits cleanly),@git.zone/tsrun^3.0.0→^3.0.1and@git.zone/tsdeno^1.8.0→^1.10.0; tsbuild 5.0.0, tsbundle 2.15.0, tsdocker 3.8.1 and tswatch 4.0.1 were already current, and pnpm 12.5.1 and@types/node26.6.2 stay until pnpm 12.6.0 and@types/node26.6.3 are seven days old. None of the three needs a migration. With tsdeno 1.10.0 the release binaries leave out the native engines built for the other architecture and for macOS (tsdeno 1.9.0's default per-target pruning): on Deno 2.9.7dcrouter-linux-x64is 498,262,576 bytes anddcrouter-linux-arm64489,086,376 bytes, down from 984,393,616 and 975,118,288 bytes in the 32.6.0 release. The compile targets in.smartconfig.jsonnow pin Deno withdenoVersion2.9.7(tsdeno refuses any other version withDENO_VERSION_MISMATCH), fail a binary below a size floor (minSize355 MiB for x64 and 350 MiB for arm64, about three quarters of the measured size, becausedeno compilecan exit 0 after writing an incomplete binary) and run the x64 binary once with--versionin an empty environment, expecting exit code 0 and a version line (the arm64 binary cannot run on the x64 runner and is skipped). The release workflow installs the pinned Deno 2.9.7 from its checksum-verified release archive and puts it first onPATHfor the binary build, because the base image's own Deno shadoweddenoland/setup-deno: 32.6.0 was compiled with the image's Deno 2.9.6 although setup-deno had installed 2.9.7.
Downloads
- The Ops UI's application log (Logs > Application Logs) hands the log each entry once. It counted the entries it had passed on, so once the browser held its cap of 2000 entries no new entry reached the log, returning to the tab showed an empty log until the next entry arrived, and a view that was attached again passed every entry a second time. It now remembers the newest entry the log on screen holds and passes the entries after it; a fetched list that replaces the shown one clears the log first, and a log created anew, on returning to the tab, gets the whole list.
-
DcRouter v32.6.0
StableRelease / build-and-release (push) Successful in 26m16sreleased this
2026-09-26 17:03:29 +00:00 | 36 commits to main since this release2026-09-26 - 32.6.0
Features
- Report stored routes that cannot be served. Route warnings (
IRouteWarning) gain the typeroute-ingress-disabled: a stored route whose only ingress paths are off (SmartVPN or the RemoteIngress hub not running) is left out of the applied routes and listed with this warning, instead of stopping every other route.
Fixes
- Hold one local mirror row per provider DNS record. Releases before 19.1.0 ran provider-zone syncs, gateway route DNS reconciles and mail DNS reconciles concurrently, so two writers could each mirror the same new provider record;
syncDomain()then refuses the zone withLocal mirror has duplicate provider record id …, and every automated DNS write for that zone fails aszone-unavailableuntil the row is removed.DnsRecordDocnow storesproviderRecordKey(<domainId>:<providerRecordId>), derived in one place (ts/dns/provider-record-key.ts) bycreateSavableObject()and by the ACME and testing mirror writers, and cleared with the provider id when a zone migrates to dcrouter. The sparse unique indexprovider_record_key_uniquemakes a racing second mirror insert fail instead of persisting. The new migration stepreconcile-provider-record-mirror-duplicates(32.0.1to32.6.0) keeps the row that carriesmanagedByownership and deletes its unowned synced copies, keeps the oldest row where none is owned, trims stored ids, backfills the key and then creates the index. It deletes local mirror rows only and never calls the provider. When rows with different owners share one provider record, it refuses by name and changes nothing. The error lists the domain, the provider id, the row ids and the owners. - Start dcrouter when neither SmartVPN nor a RemoteIngress hub runs. Since 17.9.0 every route without an explicit ingress policy, including the generated
dns-over-https-dns-queryroute and email routes, was authorized for SmartVPN, and aremoteIngressroute for the edge tunnel, whether or not that runtime was on. The listener therefore gotinboundProxyProtocoloptionalorrequiredwhile SmartProxy had no trusted proxy, and SmartProxy refused the route set:Route 'dns-over-https-dns-query': inboundProxyProtocol mode optional/required needs trustedProxyIPs or global trustedProxyIPs, a critical startup failure. Ingress origins are now authorized only for the paths whose runtime is on:smartVpnwhenvpnConfig.enabled,trustedProxywhen the RemoteIngress hub is enabled. A listener that only direct traffic reaches getsreject. A route created through the API whose only declared paths are off, for example a VPN-only route on a hub without SmartVPN, is now left out of the applied route set on its own. Every other route is still applied, at startup and on every route application. The route is logged atwarnwhen it is first refused and listed among the route warnings ofgetMergedRoutesand the Ops UI routes view asroute-ingress-disabled. A route from dcrouter's own configuration whose only paths are off still fails startup withRoute '<name>' has no enabled ingress path, because it restates the operator's options.
Downloads
- Report stored routes that cannot be served. Route warnings (
-
DcRouter v32.5.0
StableRelease / build-and-release (push) Failing after 13m3sreleased this
2026-09-26 09:13:58 +00:00 | 39 commits to main since this release2026-09-26 - 32.5.0
Features
- Let the ACME settings name another CA's ACME directory.
IAcmeConfig,AcmeConfigDocandupdateAcmeConfigcarry an optionaldirectoryUrl, which dcrouter passes to SmartAcme in place of Let's Encrypt, for example SmartAcme's ownserver.AcmeServeron the loopback of a disposable rehearsal host; absent, dcrouter orders from Let's Encrypt as before, andnullreturns to it.updateAcmeConfigaccepts only SmartAcme's own rule, anhttps:URL or anhttp:URL onlocalhost,127.0.0.1or[::1], without credentials or fragment, and refuses anything else with the rule it breaks, without repeating the URL. A change is treated like an account change: it is refused while certificate jobs or ACME DNS cleanup are unfinished, and issuance waits until SmartAcme is rebuilt on the new directory. A directory stored past that check, by a direct database write, is refused by SmartAcme at startup and now classified as a permanentacme-account-configurationfailure instead of consuming the startup retry budget. The settings dialog under Domains > Certificates gains an ACME directory field and keeps the dialog open with dcrouter's reason when it refuses the settings; the settings tile and the configuration view show the directory, and dcrouter's logs name only its origin. Rows written by earlier builds carry no directory and keep Let's Encrypt, so no migration is needed.test/test.acme-directory.node.tscovers the rule against SmartAcme's own refusal, persistence, reload and clearing, the refusal during unfinished DNS cleanup, a dcrouter that rebuilds SmartAcme from the directory stored throughupdateAcmeConfigonto two loopback CAs in turn, with an account key per directory, and a route certificate and a gateway-client exact certificate issued by the CA of the stored directory.
Maintenance
- Release tooling currency:
@git.zone/tstest^6.2.0→^6.3.0(Chromium test files tear down their browser, servers and timers on every outcome and no longer share one browser in a parallel group; no migration),@types/node26.6.1 → 26.6.2 andpackageManagerpnpm@12.4.2→pnpm@12.5.1. The other@git.zone/*dev tools are already at their current releases. The lockfile keeps@types/node26.6.1 only as the resolution of four@types/*packages'*range.
Downloads
- Let the ACME settings name another CA's ACME directory.
-
DcRouter v32.4.0
StableRelease / build-and-release (push) Successful in 21m9sreleased this
2026-09-25 14:52:19 +00:00 | 42 commits to main since this release2026-09-25 - 32.4.0
Features
- Serve gateway-client hostname records and exact certificates, the
@serve.zone/interfaces32.21.0 contractssyncGatewayClientDnsRecord,getGatewayClientCertificateandreleaseGatewayClientCertificate, and advertise them asdns.clientRecordsandcertificates.exactIssuance. A gateway client can now hold an exact hostname without a gateway route: oneAorAAAArecord at an address inside its newallowedDnsRecordAddressespolicy field, and one certificate for exactly that name, which Cloudly needs for its cluster relay names. Only gateway-client credentials can call them, with thesyncDnsRecords,requestCertificatesandreadCertificatescapabilities, for hostnames inside the client'shostnamePatterns. A hostname has one holder: a testing reservation, an enabled gateway-client route, another client or app, or an address record dcrouter does not own is a conflict, and dcrouter never overwrites or adopts such a record. The route DNS reconciler in turn answersrecord-conflictfor a hostname an ownership holds, also through a certificate alone. Records are managed asgateway-client-hostnameunder the route reconciler's DNS exclusion. Certificates need a zone with verified authority and are issued by SmartAcme's exact-identifier DNS-01 path in a namespace per client and app; a request answerspendingwhile the issuance runs. A request for a hostname nobody holds yet first synchronizes a provider zone and is refused ashostname-conflictwhen the name carries an address record dcrouter does not own, or answersissuance-unavailable(retryable) when the synchronization fails; polls for a held hostname synchronize nothing. Releasing a certificate retires its stored material, and a sweep at start and every minute removes the records, certificates and claims of deleted clients, of hostnames outside the client's patterns and of addresses outside its allowance. The allowance is set throughprovisionGatewayClientCredential,createGatewayClientandupdateGatewayClient, stored canonically, and left out of the policy digest while it is empty, so existing credentials keep matching. The exact-certificate issuance now takes its SmartAcme namespace from the caller, and testing grants keep theirs.@serve.zone/interfacesmoves from 32.0.0 to 32.21.0 in the lockfile (^32.0.0→^32.21.0), which brings@push.rocks/smartlog4.0.0 as its dependency.test/test.gateway-client-hostname.node.tscovers the record lifecycle, the conflicts in both directions, certificate issuance, release and the sweep against a real database and DNS manager, and the handlers' credential, pattern and capability checks. - Show and edit a gateway client's
allowedDnsRecordAddressesin the Ops UI under Access > Gateway Clients: a DNS addresses column, a field in the create dialog and the row action Edit DNS Addresses. Both dialogs judge the comma separated list with@serve.zone/interfaces'validateGatewayAllowedDnsRecordAddresses, which the web bundle now imports, keep the dialog open with the reason when it or dcrouter refuses the value, and close only on success. The create dialog previously closed before the request and ignored a refusal, andupdateGatewayClientActionignored an unsuccessful answer; it now records dcrouter's message as the view's error. Changing the allowance through an admin update raises the client's policy generation like any policy change, so its credentials must be provisioned again.test/test.gateway-clients.chromium.tsrenders the view and drives the edit dialog through a refused value, a refused update and a saved one. - Refuse a testing grant, on request and on approval, for a hostname a gateway-client hostname ownership holds, with
ownership_conflict. The ownership is looked up by hostname under the DNS exclusion its writers take, so one that holds only a certificate, with no DNS record, is refused too.test/test.testing-access.node.tscovers the certificate-only case.
Maintenance
- Make
test/test.email-domain-creation-progress.node.ts"domain observation switches from list discovery to id polling" independent of the wall clock; it failed on every run, also on v32.3.2. The test gaveobserveNewEmailDomaina 100 ms deadline with 1 ms polls and asserted two list and two id reads. tsx, which runs the test, scans its disk cache synchronously once per process (131,641 entries in the shared cache on the build host) and blocked the event loop for about 110 ms during the first 1 ms wait, so the deadline expired after one list read.observeNewEmailDomainnow takes an optionalnowclock for its deadline (defaultDate.now), and the test passes a frozen one, so the reads alone decide the result and tstest's timeout still bounds a hang. The module's behaviour with the default clock is unchanged.
Downloads
- Serve gateway-client hostname records and exact certificates, the
-
DcRouter v32.3.2
StableRelease / build-and-release (push) Successful in 16m51sreleased this
2026-09-24 14:22:04 +00:00 | 44 commits to main since this release2026-09-24 - 32.3.2
Fixes
- Serve the OpsServer through
@api.global/typedserver13.0.2 (from 11.2.1), with@api.global/typedsocket8.5.0 (from 8.0.3) and@push.rocks/smartserve7.2.0 (from 6.2.2), which typedserver 13 and typedsocket 8.5.0 run on. typedserver 11.2.1'sUtilityWebsiteServer.stop()threw aTypeErrorbeforestart(), so the OpsServer tracked whether it had calledstart()and stopped the web server only then; 13.x resolves thatstop(), and the tracking is gone. The OpsServer now injects the live reload client and watchesdist_serveonly under tswatch (TSWATCH=true), as typedserver 13'sUtilityWebsiteServerdefaults; 11.2.1 did both in production as well. TypedServer answers the service worker'sserviceworker_versionInfoitself with the app hash of the served files, and the OpsServer now declares dcrouter's version from its commit info as the app version, which that answer and/appversioncarry (11.2.1 answeredx.x.x, later1.0.0). Without the reload client, production no longer serves typedserver's/typedserver/devtoolsand/typedserver/reloadcheck, and responses of added routes,/mcpincluded, carryCache-Control: no-cacheand the app-hash stamp. Otherwise routes,/typedrequest,/mcp, log streams and their authorization, and static serving with the SPA fallback are unchanged.
Maintenance
- The lockfile carries one typedserver line for runtime and dev tooling (tswatch 4 already brought typedserver 13 and typedsocket 8.5.0), drops typedserver 11.2.1, typedsocket 8.0.3 and 8.1.0, smartserve 6.2.2 and
@push.rocks/smartlog-destination-devtools1.0.12, which only typedserver 11.2.1 used, and resolves@idp.global/sdk's@push.rocks/smartserve^6.2.2to 6.3.1, which meets typedsocket 8.5.0's>=6.2.4peer range.@api.global/typedrequeststays at 8.0.4, which typedserver 13 and typedsocket 8.5.0 accept. - Bundle the web UI with
@git.zone/tsbundle2.15.0 (from 2.14.0), which tstest 6.2.0 and tswatch 4.0.1 resolve to as well, so the lockfile keeps one tsbundle. 2.15.0 changes howincludeFilesentries are placed and matched; dcrouter's one entry,./html/**/*.html, matches onlyhtml/index.html, and a cleandist_servebuilt with 2.15.0 is byte-identical to one built with 2.14.0 (bundle.js, its source map, the worker chunk with its map and artifact manifest, andindex.html).
Downloads
- Serve the OpsServer through