• DcRouter v34.0.0
    Release / build-and-release (push) Successful in 42m41s

    jkunz released this 2026-10-24 08:47:12 +00:00 | 23 commits to main since this release

    2026-10-24 - 34.0.0

    Breaking Changes

    • Route DNS no longer takes operator records (DNS). In reconcile mode the gateway route DNS reconciler adopted an identical unowned A/AAAA record and replaced a differing one (deleting it and creating its own, managed-overwrote-manual); a record taken that way was later deleted with the route, which is how code.foss.global lost its A record on 2026-10-21 although an operator route still served it. Route DNS now acts only on records it owns: identical unowned records satisfy the claim as they are (manual-satisfied, evidence managed: false), and a differing unowned record, any unowned CNAME, or unowned records next to owned ones are a non-retryable record-conflict with a dns-conflict platform event, with no DNS change. manual-satisfied and the new managed-released need the @serve.zone/interfaces release that adds them to TGatewayRouteDnsStatus; managed-overwrote-manual, managed-orphan-deleted and compensation-failed are no longer produced by route DNS. The release-annexed-gateway-route-dns-records migration step returns the records route DNS adopted (createdBy not gateway-route-dns) or replaced (named by a "Gateway route DNS automation claimed a manual hostname" event) to operator ownership and logs each one.
    • Every DnsManager record mutation takes a required IDnsMutationContext (actor, trigger, reason, ownerId?): createRecord, updateRecord, deleteRecord, deleteRecords, and the new releaseRecordManagement and handoffRecordManagement. An automation actor may change only records its own managedBy namespace manages (refusal: 'not-owned'); an operator actor is not bound. updateRecord refuses a type change on a provider zone. deleteRecordsByNameAndType, unused and blind to ownership, is removed, and so is adoptRecordManagement, whose only caller was the route DNS adoption path.
    • Downgrade limit (DNS, data). The release-annexed-gateway-route-dns-records migration step (data version 34.0.0) returns adopted and replaced records to operator ownership; running an older dcrouter does not undo it, so going back means restoring the checkpoint taken before the upgrade.

    Fixes

    • Plan a domain migration to a DNS provider before its first write (DNS). migrateDomain created each record at the provider one by one, only logged a failed create and still reported success, unregistered the embedded DNS server's handlers record by record inside that loop and switched the domain metadata regardless; with deleteExistingProviderRecords it first deleted every record in the target zone, ignoring delete failures. It now reads the complete target zone and refuses, with nothing written, when a record there conflicts with a migrated record (another value of the same name and type, an A beside a foreign AAAA or the reverse, or a CNAME beside any other record) or when the zone holds an explicit SOA row or apex NS row (an authority record of the dcrouter-hosted zone, to be deleted in dcrouter first; the provider's own apex NS and SOA are left alone, and NS delegations below the apex migrate), adopts identical records, and creates the rest, passing the local record's proxied flag to the provider; a failed create or local switch deletes exactly the records the run created and reports failure. When the local switch throws, dcrouter reads the domain back: a switch that landed counts as success, one that did not is rolled back, and if the read fails nothing is deleted and the failure names the created provider records. The records, the domain metadata and the embedded DNS server switch only after every write succeeded. A migration never changes or deletes a target record it did not create, so deleteExistingProviderRecords: true is now refused (the field is deprecated) and the Ops UI migration dialog no longer offers it (it defaulted to on); the dialog shows dcrouter's refusal instead of reporting success.
    • Authorize the DNS mode a gateway-client route sync runs (gateway clients). The capability check used the requested dnsMode, but deleting a route stored with reconcile, or moving it to skip/observe or another hostname, removes its record in reconcile mode, so a token without syncDnsRecords could delete a reconciled route's DNS record by sending dnsMode: 'skip'. The stored mode is now authorized for that cleanup; the Onebox delete call, which sends no dnsMode, is unchanged.
    • Keep answering a name while its DNS records are rebuilt (DNS). Rebuilding a dcrouter-hosted RRset unregistered its handlers before reading the records, so a query during the read found no handler, and a failed read left the name unanswered until the next change or restart. The records are read first and the handlers swapped in one synchronous step; a failed read keeps the old handlers.
    • Keep an address another route still serves (DNS). When a gateway route's claim ends — route deleted, disabled, moved to another hostname, or found without a route by the periodic prune — its records were deleted while only a reconcile route of the same gateway client counted as still using the hostname. While any enabled route on the dcrouter (operator route or gateway-client route in any DNS mode, the claim's own route after a move to skip or observe included) matches the exact hostname through any match.domains entry, the records now stay in place and their ownership is released to the operator (managed-released, journaled, with a platform event). The periodic prune leaves a record of an ended claim alone while another claim is live at the hostname, so that claim takes the record over in place.
    • Do nothing while the route inventory is not loaded (DNS). RouteConfigManager is assigned before it initializes, so a failed initialize left a live manager with no routes and the route DNS prune deleted every route-owned record. Route DNS, the gateway-client hostname manager (record syncs, certificate admissions, orphan sweep), the testing hostname check and gateway-client certificate ownership now read getReadyMergedRoutes(), which is undefined until initialize() finished, and fail closed (zone-unavailable / issuance-unavailable, retryable). getGatewayClientRoutes, getGatewayClientDomains and getGatewayClientDnsRecords answer a retryable error (errorData.code: 'route-inventory-unavailable') until then instead of an empty list. A prune pass that would withdraw more than two and more than 20 % of the route-owned records withdraws none and records one Gateway route DNS cleanup held: mass withdrawal event per pass; it counts only the records the prune would withdraw, so the record of an ended claim at a hostname a live claim holds (left for that claim to take over) neither triggers the hold nor appears in its count.
    • Never leave a hostname without an address while its record changes (DNS). Route DNS and the gateway-client hostname manager deleted the old record before creating its successor, and route DNS restored deleted records by deleting and recreating again. An owned record of the target type is now edited in place, a change between A and AAAA creates the new record before the old one is deleted, an owned CNAME that would have to become an address is a non-retryable record-conflict with a dns-conflict platform event (it can neither stay beside the address nor change type without a gap, and no automation writes one, so it is old data for the operator to remove), and an owned provider record without its provider id is refused as retryable instead of being deleted and recreated; the compensation path is removed. A record of a claim that has ended is handed over in place to the claim that now holds the hostname instead of being pruned and recreated.
    • Release ownership of a provider record that changed identity outside dcrouter (DNS). When provider zone synchronization finds the record behind a managed id renamed or retyped, the record now becomes an operator record in the same single write that mirrors the change (journaled, with a platform event), unless a pending journal intent shows dcrouter's own interrupted mutation made that change.
    • Clear a stale proxy flag when a provider stops reporting it (DNS). Provider zone synchronization mirrored a provider record without proxied through save(), which only sets the fields a document holds, so the stored proxied: true stayed and the record read as changed and was rewritten on every pass. The mirror rewrite now removes the stored flag in the same write.

    Features

    • Protected hostnames (DNS, ops API, admin). getDnsProtectedHostnames, addDnsProtectedHostname and removeDnsProtectedHostname manage exact hostnames at which no automation (route DNS, gateway-client hostname records, mail DNS, migration to a DNS provider) may create, update, delete, adopt or hand over any record; releasing ownership stays allowed and operator changes are not bound. Nothing is protected by default: after upgrading, protect the hostnames whose records must only change by hand, such as code.foss.global. getDnsRecords marks such records protectedHostname: true, and the Ops UI shows them.
    • DNS mutation journal (DNS, ops API). Every record mutation dcrouter makes writes its intent before it runs (and does not run if that write fails) and its outcome afterwards, with actor, trigger, reason, owner and the record before and after; refusals, releases, handoffs, protected-list changes and the mirror rewrites and deletions provider zone synchronization observes are journaled too; an observation is written before the mirror write and completed observed after it, or failed when the record changed in between. A migration to a provider journals each provider create and rollback delete the same way under provider-migration, and its record links and domain switch inside the commit transaction; a protected name among its records refuses it before the first write. migrateDomain takes the requester's IDnsMutationContext. getDnsMutationJournal (dns-records:read) reads it by hostname, zone and time; entries are kept 180 days. ACME DNS-01 challenge records and testing DNS keep their own durable owners and are not part of it.

    Maintenance

    • Release tooling: the deprecated single tools @git.zone/tsbuild 6.0.2, @git.zone/tsbundle 3.7.1, @git.zone/tsrun 3.0.3, @git.zone/tstest 8.0.0 and @git.zone/tswatch 7.1.1 are replaced by @git.zone/tsrepo 1.1.1, following its "Migrating from the single tools" section: the build, bundle, test and watch scripts run tsrepo build, tsrepo bundle, tsrepo test and tsrepo watch with the same arguments, the tests import tap and expect from @git.zone/tsrepo/tapbundle, the @git.zone/tsbundle and @git.zone/tswatch sections of .smartconfig.json moved unchanged to "@git.zone/tsrepo".bundle and .watch (the dcrouter-dev watcher starts the dev server with tsrepo run), the SmartDB rollback test starts its fixture with tsrepo run, and cli.ts.js registers tsx (now a dev dependency, 4.23.15, the version tsrepo uses) and imports cli.child.ts instead of calling tsrun.runPath. pnpm 12.11.2 → 12.14.0 (12.15.0 waits for the seven-day rule); @git.zone/tsdocker 3.12.0 and @types/node 26.6.5 are current. @git.zone/tsdeno stays on 1.15.0 for build:binary, whose move to a Node.js single executable application is its own change (lane D5 of the Deno-removal programme). This tooling change moves no runtime dependency version; @push.rocks/smartconsole 2.1.0 (through @push.rocks/smartlog 4) now resolves its optional peer @push.rocks/smartmarkdown to 3.3.0, which tsrepo brings into the install. The Dockerfile's base image digests are unchanged.
    Downloads