{
"$type": "site.standard.document",
"path": "/devlog/013",
"publishedAt": "2026-07-10T18:43:50Z",
"site": "at://did:plc:mkqt76xvfgxuemlwlx6ruc3w/site.standard.publication/devlog",
"textContent": "# doors\n\ndevlog 012 ended at `v0.3.5`. this covers `v0.3.6` and `v0.3.7`, and there is no grand thesis in it. the recurring shape of these notes — a downstream app reaches for something zat half-has, and the reach becomes the next change — is by now house lore. 010 called it \"the network is input\" and let downstream use drive the XRPC error API; 011 was zds surfacing the high-S bug; 012's \"missing middle\" was ZDS reaching through MST internals until `collectBlocks` and `walk` existed. so this is the same loop, not a discovery. what's worth writing down is the two specific doors that opened, because they are a different kind of gap than the ones before.\n\n## a capability with no door\n\nthe new consumer this cycle is [leaflet-search](https://tangled.org/zzstoatzz.io/leaflet-search) (now pub-search), which grew its own firehose ingester to replace indigo's `tap`. the ingester's last blocker was verification: it wanted to consume the firehose and verify each commit in-process before trusting it.\n\nzat already had the verifier for exactly this. `verifyCommitCar` says so in its own doc comment — \"used by the relay to verify firehose commit frames directly\" — and takes the raw CAR bytes from a frame's `blocks` field plus a resolved signing key. the function was right there.\n\nthe problem was that you could not get the bytes to it. every prior verification story in these notes is *low-level*: the relay (devlog 006), zlay, and zds all decode firehose frames themselves, so they were already holding the raw CAR when it came time to verify. a high-level `FirehoseClient` consumer was not. `decodeFrame` read the `blocks`, parsed the CAR to hydrate each op's record, and then **discarded the raw bytes**. `CommitEvent` exposed `seq`, `repo`, `rev`, `ops`, `blobs`, `too_big` — and nothing you could hand to `verifyCommitCar`. the documented path was real and unreachable at the same time.\n\nthis is not 012's gap. \"the missing middle\" was a consumer reaching *through* internals that then needed to change. this was the opposite: a public capability that had no public entrance. the fix is small and purely additive — [`f0d781e`](https://tangled.org/zat.dev/zat/commit/f0d781e) puts the bytes back on the event:\n\n```zig\npub const CommitEvent = struct {\n seq: i64,\n repo: []const u8,\n rev: []const u8,\n // ...\n blocks: []const u8 = &.{}, // raw CAR diff bytes\n prev_data: ?cbor.Cid = null, // MST root CID of the previous revision\n // ...\n};\n```\n\nnow the high-level consumer does what the relay always could:\n\n```zig\ntry zat.verifyCommitCar(allocator, event.commit.blocks, signing_key, .{});\n```\n\nthe same change carries `prev_data` and the per-op `prev` CIDs through, adds a `toMstOperations()` helper, and decodes `#sync` events — so a consumer that wants the stronger [`verifyCommitDiff`](https://tangled.org/zat.dev/zat/blob/main/src/internal/repo/repo_verifier.zig) (invert the ops, prove the previous root) has the inputs for that too. CAR parse failure during hydration is now non-fatal: the wire event still surfaces, and a consumer rejects it by verifying `blocks` explicitly rather than by never seeing it. leaflet-search's ingester verifies in-consumer now, which was the sole thing standing between it and replacing `tap`.\n\n## the boundary 011 drew, crossed on purpose\n\n011 was careful about a line. zds runs a full OAuth authorization server — PAR, DPoP, client assertions, permission sets — and that devlog kept it on zds's side: zat supplies protocol primitives, the application keeps \"cookies, redirects, sessions, and storage policy.\" that was the right call then. the ceremony was new, only one app did it, and it was tangled up with zds's own persistence.\n\na second OAuth consumer makes the same line look different. the primitives were never the hard part; the *ceremony* is — discovery, building the authorization URL, PAR, the code and refresh exchanges, DPoP nonce retry, DPoP-authenticated resource requests. every consumer reimplements the same dance around the same primitives, and the storage policy 011 fenced off is genuinely the only part that differs between apps.\n\nso [`1f7dbc5`](https://tangled.org/zat.dev/zat/commit/1f7dbc5) splits `zat.oauth` into two halves and lifts the framework-neutral one into the library. `oauth/primitives.zig` keeps PKCE, DPoP, client assertions, and form/JWKS encoding. `oauth/client.zig` is the ceremony:\n\n```zig\nconst authserver = try zat.oauth.discoverAuthorizationServer(allocator, &transport, pds_url);\nvar metadata = try zat.oauth.fetchAuthorizationServerMetadata(allocator, &transport, authserver);\nvar secrets = try zat.oauth.prepareAuthRequestSecrets(allocator, io);\nvar par = try zat.oauth.sendParRequest(allocator, io, &transport, .{ ... });\n// then authorizationUrl, exchangeCodeForToken, refreshAccessToken, dpopRequest\n```\n\nthe line 011 drew did not move — cookies, sessions, redirects, and persistence still stay with the app. what moved is everything *up to* that line. the OAuth client also stays on the shared `HttpTransport` instead of dropping to raw `std.http.Client`, because [`FetchResult`](https://tangled.org/zat.dev/zat/blob/main/src/internal/xrpc/transport.zig) now captures the response headers OAuth needs — `Content-Type`, `DPoP-Nonce`, `WWW-Authenticate`.\n\nthis is not a new insight, it is the boundary from 011 acted on once a second app proved the part below it was reusable.\n\n## the small correctness fix\n\nleaflet-search also left the other door ajar. its author filtering was silently no-opping for `dholms.at`, and the cause was DNS-over-HTTPS handle resolution. Cloudflare appends a `Comment` field to DoH JSON for handles in DNSSEC zones:\n\n```json\n{\"Status\":0, ...,\n \"Answer\":[{\"name\":\"_atproto.dholms.at\",\"type\":16,\"data\":\"\\\"did=did:plc:...\\\"\"}],\n \"Comment\":[\"EDE(10): RRSIGs Missing for DNSKEY at., id = 1253\"]}\n```\n\n`HandleResolver` parsed that body strictly, so the undeclared `Comment` aborted the parse with `error.InvalidDnsResponse`. `.well-known` HTTP handles were fine, so the failure looked intermittent — it only hit handles whose `_atproto` TXT record lives in a DNSSEC zone. [`27f810b`](https://tangled.org/zat.dev/zat/commit/27f810b) parses with `ignore_unknown_fields` and pins the behavior with a network-free regression test built from a real Cloudflare DNSSEC body.\n\n## also in these releases\n\nthe rest is hygiene, not story:\n\n- websocket.zig moved to `v0.1.7` (an IPv6 `Address` shim and socket-race fixes), and the re-export from `zat`'s root was added and then dropped so the dependency stays a dependency.\n- `repo_verifier.invertOp` had its error handling made exhaustive.\n- MST `collectBlocks` now caches node encodings across passes and keeps the `(cid, encoded)` pair consistent in `nodeCid`, so gathering commit blocks does not re-serialize clean nodes — a small follow-on to 012's hot-path work.\n\n## the releases\n\n`v0.3.6` is the firehose verification inputs plus the websocket and `invertOp` items. `v0.3.7` is the OAuth toolkit, the DoH fix, and the MST encoding cache. both are patch releases: every public change is additive, and nothing that verified or resolved before stops doing so.\n\nzat is `v0.3.7`.\n",
"title": "doors"
}