• DcRouter v33.0.1
    Release / build-and-release (push) Successful in 34m10s
    Stable

    jkunz released this 2026-10-17 17:16:09 +00:00 | 2 commits to main since this release

    2026-10-17 - 33.0.1

    Fixes

    • Return which reconciler owns a DNS record (ops API). getDnsRecords and getDnsRecord left out managedBy, managedOwnerId and managedRecordKey, which IDnsRecord already 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 MCP dcrouter_list_dns_records summaries carry managedBy and managedOwnerId (smartmcp redacts any field named like a key, so managedRecordKey is left out there); gateway client DNS record reads keep the IGatewayDnsRecord shape, which has no ownership fields.

    Maintenance

    • Release tooling: @git.zone/tsbundle 3.7.0 → 3.7.1.
    Downloads
  • DcRouter v33.0.0
    Release / build-and-release (push) Successful in 19m49s
    Stable

    jkunz released this 2026-10-16 23:29:55 +00:00 | 5 commits to main since this release

    2026-10-16 - 33.0.0

    Breaking Changes

    • @push.rocks/smartmta ^9.5.0 → ^13.0.1. dcrouter re-exports UnifiedEmailServer, IUnifiedEmailServerOptions, IEmailRoute and IEmailDomainConfig from it and @serve.zone/dcrouter-apiclient types the email settings' emailConfig against it, so consumers see SmartMTA 10–13's changes: inbound mail is parsed with @push.rocks/smartmail 3 (postal-mime) instead of mailparser; Email.headers holds the raw field values as written (unfolded, a repeated field joined with , ) instead of mailparser's interpreted forms; Email.from is the first mailbox of the From fields in message order with groups flattened, and group members count as header To/Cc; baseLogger is a @push.rocks/smartlog 5 instance; Email.toSmartmail()/fromSmartmail() use smartmail 3; the Linux mail engine is a static musl executable named dist_rust/mailer-bin_linux_amd64_musl / mailer-bin_linux_arm64_musl with its third-party notices next to it (<binary>.third-party-notices.{json,txt}) and in native-notices/, instead of mailer-bin_linux_amd64 / _arm64, notices/ and third-party-notices.md; the rawMessage SmartMTA hands to onAcceptEnvelope/onMessageData keeps the CRLF that ends the message's last line. dcrouter itself references none of those paths.
    • Email route match.headers semantics follow the raw headers: mailparser's derived keys priority and list no 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.node is >=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 plain auth.users, and their outbound relay routes match authenticatedUser. emailConfig.auth.users and authUserCount in getEmailServerSettings therefore 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.users login 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.users login or an unscoped SMTP account could send as a WorkApp address through that identity's route and DKIM key. The routes now also match authenticatedUser; 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, DATA and 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 smtpAuthAccountsAndScopes capability and its fallback, which dropped stored SMTP accounts and made createAccount refuse; SmartMTA 12 and later always have the capability.
    • The production dependency tree no longer contains mailparser, nodemailer or their html-to-text/deepmerge-ts subtree.
    • 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-Id header 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.ts covers 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/interfaces contract defines.
    • Write the X-Dcrouter-Cached-Email-Id header into the header section only (email). Before prepending its own, the spool removed every line starting with X-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/tstest 7.2.0 → 8.0.0, @git.zone/tsbundle 3.2.0 → 3.7.0, @git.zone/tswatch 7.0.0 → 7.1.1, @git.zone/tsdocker 3.11.0 → 3.12.0 and @types/node 26.6.3 → 26.6.5; @git.zone/tsbuild 6.0.2 and @git.zone/tsrun 3.0.3 are current. tstest 8 drops the Deno runtime and refuses a test file whose name names deno, so the new sender-scope test is test/test.smtp-sender-scope.node.ts and 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 the tsbundle notices command; 3.6.2 fixes esbuild browser bundles that failed with ENOENT on a module a package's browser field disables), and the tswatch and tsdocker updates change nothing dcrouter configures. @git.zone/tsdeno stays on 1.15.0: it only drives build:binary and 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
    Release / build-and-release (push) Successful in 32m59s
    Stable

    jkunz released this 2026-10-05 20:43:22 +00:00 | 19 commits to main since this release

    2026-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). updateEmailServerSettings switches it in-process; switching to managedCertificate is refused while no currently valid stored certificate covers the mail hostname and every enabled mail-edge hostname, and removes the configured certPath/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 getEmailServerSettings as tlsCertificate (source, origin, DNS names, notBefore/notAfter, required hostnames, problems) (email).

    Maintenance

    • Release tooling: pnpm 12.5.1 → 12.8.1, @git.zone/tsbuild 5.0.0 → 5.1.2, @git.zone/tsbundle 3.0.0 → 3.1.3, @git.zone/tsdeno 1.12.0 → 1.15.0, @git.zone/tsdocker 3.9.0 → 3.11.0, @git.zone/tstest 6.3.2 → 7.1.0, @git.zone/tswatch 4.0.2 → 6.0.1 and @types/node 26.6.2 → 26.6.3; @git.zone/tsrun 3.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 into stdout, createHttpsCert() writes a PKCS#8 key, createBrowser()), and the test script's options stay valid under tstest 7. tswatch 5 and 6 keep the @git.zone/tswatch watcher configuration unchanged; tswatch 6 bundles through tsbundle 3, which dcrouter-dev does not use (its command runs pnpm run build, already on tsbundle 3 with third-party notices). tsdeno stays on 1.x: 2.0.0 changes tsdeno compile to embed exactly the versions pnpm-lock.yaml pins and fail on any difference, a lockfile-model migration of build:binary that gets its own change. pnpm 12.8.2 and @types/node 26.6.4 wait for the seven-day rule.
    Downloads
  • DcRouter v32.6.3
    Release / build-and-release (push) Successful in 21m45s
    Stable

    jkunz released this 2026-09-29 05:07:41 +00:00 | 22 commits to main since this release

    2026-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.2 and @git.zone/tsrun ^3.0.1 → ^3.0.2; none needs a migration. tsdeno 1.12.0 reads the module graph with deno info before each compile and fails with UNRESOLVED_DEPENDENCY instead 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 --repository to push, 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 every tsrun process 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/node 26.6.2 stay until pnpm 12.6.0 (2026-09-29 17:08 UTC) and @types/node 26.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-element 3.3.0 → 3.4.0; the tree keeps one copy of each. The Ops UI binds no frameless and uses neither the stepper's removed scroller nor setScrollStatus(), 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, and dees-input-list rows 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 tsrun directly rather than through pnpm 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
  • DcRouter v32.6.2
    Release / build-and-release (push) Successful in 22m2s
    Stable

    jkunz released this 2026-09-28 02:50:05 +00:00 | 27 commits to main since this release

    2026-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 two read_exact calls inside a tokio::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 split CTRL_UDP_SESSION_CLOSE frame made the edge refuse control message too large: <N> bytes (max 65536) and tear its tunnel down, dropping every port listener of that edge while the hub logged control stream error: connection lost - the teardowns seen on the SSH tunnel to code.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 and encodeConnectionToken) 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.3 unresolved in deno compile (the smoke check caught it). deno.json now 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 the package.json ranges against the registry at compile time, not pnpm-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 --version smoke 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/tsdeno 1.11.0 and @git.zone/tsbundle 3.0.0 (the dashboard bundle ships dist_serve/bundle.js.third-party-notices.{txt,json}); pnpm 12.6.0 and @types/node 26.6.3 wait for the seven-day rule.
    Downloads
  • DcRouter v32.6.1
    Release / build-and-release (push) Successful in 19m53s
    Stable

    jkunz released this 2026-09-27 21:05:57 +00:00 | 32 commits to main since this release

    2026-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.ts covers streaming, the cap, a replacing fetch and the return to the tab.
    • ts_web/appstate/runtime.ts imports the platform-events state statically. It loaded it through a dynamic import(), so a bundle evaluated that module lazily and an element that read configEventStatePart while 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.0 and @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 same dees-* elements and the page stops with NotSupportedError. pnpm dedupe leaves one copy each of dees-catalog, dees-element, dees-domtools (3.0.1), lit and @serve.zone/catalog in the bundle; it also drops duplicate resolutions of @types/node 26.6.1, @push.rocks/smartrust 2.0.0, @push.rocks/smartwatch 6.4.3, @design.estate/dees-comms and picomatch. The one source change the catalog needs is in the application log: dees-catalog 14 removed dees-chart-log's terminalReady, 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 uses dees-simple-appdash rather than dees-appui, gives every dees-chart-area its own yAxisFormatter, reads no seriesStats, binds no vintegrated and uses the sz-* elements, whose API did not change from @serve.zone/catalog 4 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 --timeout now fails even when it handles SIGTERM and exits cleanly), @git.zone/tsrun ^3.0.0 → ^3.0.1 and @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/node 26.6.2 stay until pnpm 12.6.0 and @types/node 26.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.7 dcrouter-linux-x64 is 498,262,576 bytes and dcrouter-linux-arm64 489,086,376 bytes, down from 984,393,616 and 975,118,288 bytes in the 32.6.0 release. The compile targets in .smartconfig.json now pin Deno with denoVersion 2.9.7 (tsdeno refuses any other version with DENO_VERSION_MISMATCH), fail a binary below a size floor (minSize 355 MiB for x64 and 350 MiB for arm64, about three quarters of the measured size, because deno compile can exit 0 after writing an incomplete binary) and run the x64 binary once with --version in 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 on PATH for the binary build, because the base image's own Deno shadowed denoland/setup-deno: 32.6.0 was compiled with the image's Deno 2.9.6 although setup-deno had installed 2.9.7.

    Downloads
  • DcRouter v32.6.0
    Release / build-and-release (push) Successful in 26m16s
    Stable

    jkunz released this 2026-09-26 17:03:29 +00:00 | 36 commits to main since this release

    2026-09-26 - 32.6.0

    Features

    • Report stored routes that cannot be served. Route warnings (IRouteWarning) gain the type route-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 with Local mirror has duplicate provider record id …, and every automated DNS write for that zone fails as zone-unavailable until the row is removed. DnsRecordDoc now stores providerRecordKey (<domainId>:<providerRecordId>), derived in one place (ts/dns/provider-record-key.ts) by createSavableObject() and by the ACME and testing mirror writers, and cleared with the provider id when a zone migrates to dcrouter. The sparse unique index provider_record_key_unique makes a racing second mirror insert fail instead of persisting. The new migration step reconcile-provider-record-mirror-duplicates (32.0.1 to 32.6.0) keeps the row that carries managedBy ownership 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-query route and email routes, was authorized for SmartVPN, and a remoteIngress route for the edge tunnel, whether or not that runtime was on. The listener therefore got inboundProxyProtocol optional or required while 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: smartVpn when vpnConfig.enabled, trustedProxy when the RemoteIngress hub is enabled. A listener that only direct traffic reaches gets reject. 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 at warn when it is first refused and listed among the route warnings of getMergedRoutes and the Ops UI routes view as route-ingress-disabled. A route from dcrouter's own configuration whose only paths are off still fails startup with Route '<name>' has no enabled ingress path, because it restates the operator's options.
    Downloads
  • DcRouter v32.5.0
    Release / build-and-release (push) Failing after 13m3s
    Stable

    jkunz released this 2026-09-26 09:13:58 +00:00 | 39 commits to main since this release

    2026-09-26 - 32.5.0

    Features

    • Let the ACME settings name another CA's ACME directory. IAcmeConfig, AcmeConfigDoc and updateAcmeConfig carry an optional directoryUrl, which dcrouter passes to SmartAcme in place of Let's Encrypt, for example SmartAcme's own server.AcmeServer on the loopback of a disposable rehearsal host; absent, dcrouter orders from Let's Encrypt as before, and null returns to it. updateAcmeConfig accepts only SmartAcme's own rule, an https: URL or an http: URL on localhost, 127.0.0.1 or [::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 permanent acme-account-configuration failure 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.ts covers 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 through updateAcmeConfig onto 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/node 26.6.1 → 26.6.2 and packageManager pnpm@12.4.2 → pnpm@12.5.1. The other @git.zone/* dev tools are already at their current releases. The lockfile keeps @types/node 26.6.1 only as the resolution of four @types/* packages' * range.
    Downloads
  • DcRouter v32.4.0
    Release / build-and-release (push) Successful in 21m9s
    Stable

    jkunz released this 2026-09-25 14:52:19 +00:00 | 42 commits to main since this release

    2026-09-25 - 32.4.0

    Features

    • Serve gateway-client hostname records and exact certificates, the @serve.zone/interfaces 32.21.0 contracts syncGatewayClientDnsRecord, getGatewayClientCertificate and releaseGatewayClientCertificate, and advertise them as dns.clientRecords and certificates.exactIssuance. A gateway client can now hold an exact hostname without a gateway route: one A or AAAA record at an address inside its new allowedDnsRecordAddresses policy field, and one certificate for exactly that name, which Cloudly needs for its cluster relay names. Only gateway-client credentials can call them, with the syncDnsRecords, requestCertificates and readCertificates capabilities, for hostnames inside the client's hostnamePatterns. 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 answers record-conflict for a hostname an ownership holds, also through a certificate alone. Records are managed as gateway-client-hostname under 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 answers pending while the issuance runs. A request for a hostname nobody holds yet first synchronizes a provider zone and is refused as hostname-conflict when the name carries an address record dcrouter does not own, or answers issuance-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 through provisionGatewayClientCredential, createGatewayClient and updateGatewayClient, 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/interfaces moves from 32.0.0 to 32.21.0 in the lockfile (^32.0.0 → ^32.21.0), which brings @push.rocks/smartlog 4.0.0 as its dependency. test/test.gateway-client-hostname.node.ts covers 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 allowedDnsRecordAddresses in 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, and updateGatewayClientAction ignored 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.ts renders 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.ts covers 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 gave observeNewEmailDomain a 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. observeNewEmailDomain now takes an optional now clock for its deadline (default Date.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
  • DcRouter v32.3.2
    Release / build-and-release (push) Successful in 16m51s
    Stable

    jkunz released this 2026-09-24 14:22:04 +00:00 | 44 commits to main since this release

    2026-09-24 - 32.3.2

    Fixes

    • Serve the OpsServer through @api.global/typedserver 13.0.2 (from 11.2.1), with @api.global/typedsocket 8.5.0 (from 8.0.3) and @push.rocks/smartserve 7.2.0 (from 6.2.2), which typedserver 13 and typedsocket 8.5.0 run on. typedserver 11.2.1's UtilityWebsiteServer.stop() threw a TypeError before start(), so the OpsServer tracked whether it had called start() and stopped the web server only then; 13.x resolves that stop(), and the tracking is gone. The OpsServer now injects the live reload client and watches dist_serve only under tswatch (TSWATCH=true), as typedserver 13's UtilityWebsiteServer defaults; 11.2.1 did both in production as well. TypedServer answers the service worker's serviceworker_versionInfo itself 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 /appversion carry (11.2.1 answered x.x.x, later 1.0.0). Without the reload client, production no longer serves typedserver's /typedserver/devtools and /typedserver/reloadcheck, and responses of added routes, /mcp included, carry Cache-Control: no-cache and 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-devtools 1.0.12, which only typedserver 11.2.1 used, and resolves @idp.global/sdk's @push.rocks/smartserve ^6.2.2 to 6.3.1, which meets typedsocket 8.5.0's >=6.2.4 peer range. @api.global/typedrequest stays at 8.0.4, which typedserver 13 and typedsocket 8.5.0 accept.
    • Bundle the web UI with @git.zone/tsbundle 2.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 how includeFiles entries are placed and matched; dcrouter's one entry, ./html/**/*.html, matches only html/index.html, and a clean dist_serve built 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, and index.html).
    Downloads