{
  "$type": "site.standard.document",
  "path": "/changelog",
  "publishedAt": "2026-05-25T15:56:58Z",
  "site": "at://did:plc:mkqt76xvfgxuemlwlx6ruc3w/site.standard.publication/3khuwc44c2222",
  "textContent": "# changelog\n\n## 0.3.3\n\n- **feat**: `jwt.verifyJose(alg, message, signature, public_key)` — lenient ES256/ES256K verification for JOSE/JWT contexts (OAuth client assertions, DPoP), accepting high-S signatures per RFC 7515.\n- **fix**: `Jwt.verify` now verifies leniently (accepts high-S). a JWT is a JOSE token, where the atproto low-S requirement does not apply and WebCrypto-based signers routinely emit high-S signatures; the previous strict verification rejected valid service-auth and OAuth tokens. matches the reference SDKs (`@atproto/crypto` `allowMalleableSig`, atmos `HashAndVerifyLenient`). strict low-S enforcement is unchanged for content-addressed signatures — `verifyCommitCar`, `verifyCommitDiff`, and `verifyDidKeySignature` still reject high-S via `verifyP256` / `verifySecp256k1`.\n- **docs**: README \"used by\" section listing downstream consumers.\n\n## 0.3.2\n\n- **feat**: bump websocket.zig to v0.1.1 — streaming `Handshake.parse` onto `std.http.HeadParser` plus RFC 7230 strictness, fixing the recurring class of TCP-fragmentation parser bugs. two new error variants (`WhitespaceBeforeColon`, `AmbiguousBodyLength`) surface as 400 responses.\n\n## 0.3.1\n\n- **feat**: `XrpcClient.queryChecked` and `XrpcClient.procedureChecked` return a checked union of successful `Response` or structured `XrpcError`, preserving AT Protocol `error` / `message` envelopes for non-2xx responses.\n- **feat**: `XrpcClient.RetryPolicy` adds status-driven retries for transient transport errors plus HTTP 429/5xx responses, with `retry-after` and rate-limit header support.\n- **feat**: `HttpTransport.fetch` captures `ratelimit-limit`, `ratelimit-remaining`, `ratelimit-reset`, and `retry-after` headers on responses.\n- **fix**: DID and handle resolution now reject unsafe network targets by default, including private, loopback, link-local, multicast, documentation, and unspecified IP ranges.\n- **fix**: identity resolution validates DNS and redirect targets before issuing HTTP requests, reducing SSRF exposure when resolving untrusted AT Protocol identifiers.\n\n## 0.3.0\n\n- **breaking**: zig 0.16 — all networking APIs take `io: std.Io` as first parameter\n- **breaking**: streaming clients use `subscribe(handler)` pattern instead of `connect()` + `next()` loop\n- **breaking**: websocket.zig bumped — Io-native server accept loop, client write lock, TLS stream support\n- **fix**: `DidResolver.resolve` and `HttpTransport.fetch` propagate the underlying `std.http.Client.fetch` error instead of collapsing every transport-layer failure (DNS, connect, TLS) to `error.DidResolutionFailed` / `error.RequestFailed` — callers can distinguish failure modes via `@errorName(err)`. soft-breaking: the inferred error set widens\n- **feat**: `Io.Timestamp` replaces libc `gettimeofday` in JWT/OAuth\n- **feat**: `io.sleep()` replaces libc `nanosleep` in reconnect backoff (cancellation-aware)\n- **feat**: `car.streamBlocks` + `BlockIterator` — pull-style, zero-allocation CAR iteration for large repos (sync.getRepo). shares parser helpers with `read`/`readWithOptions`; yields slices into the input buffer and exposes `offset()` so callers can build their own CID → byte-offset index. strictly additive, no existing consumers affected. pass `max_size = data.len` for sync.getRepo responses — the 2 MB default targets firehose frames.\n- **docs**: [devlog 008](devlog/008-the-io-migration.md) — the 0.16 migration\n\n## 0.2.18\n\n- **feat**: export `HttpTransport` from root module — consumers can now use `zat.HttpTransport` for direct HTTP access without going through `XrpcClient`\n\n## 0.2.17\n\n- **feat**: `Keypair.jwk()`, `Keypair.jwkThumbprint()`, `Keypair.uncompressedPublicKey()` — JWK export and RFC 7638 thumbprints for both P-256 and secp256k1\n- **feat**: `oauth` module — stateless PKCE, DPoP proofs, client assertions, form encoding, and related helpers for AT Protocol OAuth flows (based on OAuth 2.1)\n- **feat**: `jwt.base64UrlEncode`, `jwt.base64UrlDecode` now public\n- **test**: interop tests for did:key derivation and data model fixtures\n\n## 0.2.16\n\n- **fix**: bump websocket.zig to `395d0f4` — reads full HTTP body on TCP split writes (fixes empty body when headers and body arrive in separate TCP segments)\n\n## 0.2.15\n\n- **feat**: `parseDidKey`, `verifyDidKeySignature` — parse `did:key` strings back to key type + raw bytes, verify signatures by `did:key` with automatic curve dispatch\n- **feat**: `Keypair` struct — unified abstraction over secp256k1/P-256 for sign, publicKey, did:key formatting\n- **feat**: optional `onConnect` callback on `JetstreamClient` — exposes which host the client connected to\n\n## 0.2.14\n\n- **fix**: memory leak in `HttpTransport.fetch()` — `toArrayList()` transferred buffer ownership without freeing; use `written()` instead to keep ownership with the deferred `deinit()`\n\n## 0.2.13\n\n- **docs**: devlog 007 — up and to the right (corrections to 006, sync 1.1 verification, lightrail collection index)\n\n## 0.2.12\n\n- **feat**: configurable `keep_alive` on `HttpTransport` and `DidResolver.initWithOptions` — allows disabling HTTP connection reuse for memory leak investigation\n\n## 0.2.11\n\n- **fix**: enable TCP keepalive on websocket connections — detect dead peers in ~20s instead of blocking forever\n\n## 0.2.10\n\n- **deps**: bump websocket.zig to fork commit `9e6d732` — TCP split guard for HTTP body reads behind reverse proxies\n\n## 0.2.9\n\n- **fix**: SPA fallback routing for standard.site deep links — `_redirects`, `<base href=\"/\">`, devlog short-name aliases\n- **fix**: add glibc to nixery deps for wisp-cli patchelf in CI\n- **docs**: devlog 006 — building a relay in zig (zlay architecture, deployment war stories, backfill)\n- **fix**: publish-docs.zig missing devlog entries 004-006\n\n## 0.2.8\n\n- **feat**: sync 1.1 — `ChildRef` union, `loadFromBlocks`, `putReturn`/`deleteReturn`, `verifyCommitDiff`\n- **feat**: `loadCommitFromCAR` returns unsigned commit bytes\n\n## 0.2.7\n\n- **feat**: `Value.getUint()` — extract unsigned integers as `?u64` from CBOR maps. `getInt()` truncates values > `i64` max; upstream AT Protocol firehose seq numbers now exceed this limit.\n\n## 0.2.6\n\n- **feat**: specialized MST decoder — `decodeMstNode()` parses known MST CBOR schema directly, zero-copy byte slicing, avoids generic `Value` union construction\n- **feat**: in-walk MST structure verification — `walkAndVerifyMst` checks key heights during traversal instead of full tree rebuild. MST step: 218ms → 39ms (5.5x), compute total: 300ms → 123ms (2.4x)\n- **docs**: devlog 005 — updated benchmark numbers and chart\n\n## 0.2.5\n\n- **feat**: O(1) block lookup in CAR parser — `StringHashMap` index built during `read()`/`readWithOptions()`, `findBlock()` uses index instead of linear scan\n- **fix**: `verifyRepo` bypasses default 2 MB / 10k block limits so large repos (e.g. pfrazee.com at 70 MB / 243k blocks) actually work\n- **docs**: devlog 005 — clarify Rust ecosystem (rsky, jacquard, hand-rolled RustCrypto)\n\n## 0.2.4\n\n- **feat**: configurable CAR size limits — `max_size` and `max_blocks` options in `readWithOptions` for large repo verification\n- **feat**: export `jwt` module (not just `Jwt` type) for direct access to `verifySecp256k1`/`verifyP256`\n- **docs**: devlog 005 — three-way trust chain verification (zig vs Go vs Rust)\n- **docs**: README rewrite — added CBOR, CAR, MST, firehose, jetstream, signing, repo verification\n\n## 0.2.3\n\n- **docs**: devlog 004 — the sig-verify saga (k256 5×52-bit field, Fermat scalar inversion, three-way bench with rsky)\n- changelog backfill for 0.2.1 and 0.2.2\n\n## 0.2.2\n\n- **feat**: CAR parser enforces size limits — 2MB max on blocks field, max block count. matches indigo's limits for production parity.\n\n## 0.2.1\n\n- **feat**: CID hash verification in CAR parser — `car.read()` SHA-256 hashes each block and compares against the CID digest. proves block content wasn't corrupted or tampered with. `readWithOptions(.{ .verify_block_hashes = false })` to skip for trusted local data.\n- **fix**: remove pfrazee.com from default test suite (network-dependent)\n\n## 0.2.0\n\n- **feat**: end-to-end repo verification — `verifyRepo(allocator, identifier)` exercises the full AT Protocol trust chain: handle → DID → DID document → signing key → fetch repo CAR → verify commit signature → walk MST → rebuild tree → CID match\n- **refactor**: organize `src/internal/` into domain subdirectories following the [TypeScript SDK](https://github.com/bluesky-social/atproto/tree/main/packages): `syntax/`, `crypto/`, `identity/`, `repo/`, `xrpc/`, `streaming/`, `testing/`\n\n## 0.1.9\n\n- **feat**: merkle search tree (MST) — `mst.Mst` with `put`, `get`, `delete`, `rootCid`\n- **feat**: ECDSA signing — `signSecp256k1`, `signP256` with low-S normalization (RFC 6979)\n- **feat**: `did:key` construction — `multicodec.formatDidKey`, `multicodec.encodePublicKey`\n- **feat**: multibase encoding — base58btc encode, base32lower encode/decode\n- interop tests: MST common prefix (13 vectors), commit proofs (6 fixtures)\n\n## 0.1.8\n\n- **fix**: NSID parser rejects TLD starting with digit (e.g. `1.0.0.127.record`)\n- **fix**: AT-URI parser validates authority (DID/handle), collection (NSID), and rkey components; rejects `#`, `?`, spaces\n- **fix**: reject high-S ECDSA signatures — atproto requires low-S normalization (BIP-62 style)\n- `verifySecp256k1` and `verifyP256` are now `pub`\n- atproto interop test suite: syntax validation (6 types), crypto signature verification (6 vectors), MST key heights (9 vectors)\n\n## 0.1.7\n\n- slim `Cid` struct from 56 to 16 bytes — store only raw bytes, parse version/codec/digest lazily on demand\n- `Value` union shrinks from 64 to 24 bytes, `MapEntry` from 80 to 40 bytes\n- zero-cost CID decode — tag 42 handler stores a byte slice reference instead of parsing varint fields\n- inline map key reading in CBOR decoder — skips full `decodeAt` + union construction per key\n- comptime size assertions for `Value` and `MapEntry`\n- **breaking**: `Cid` fields (`version`, `codec`, `hash_fn`, `digest`) are now accessor methods returning optionals — e.g. `cid.version` → `cid.version().?`\n- `parseCid` simplified to a trivial raw-bytes wrapper\n\n## 0.1.6\n\n- round-robin host rotation for jetstream and firehose clients\n- `Options.host` → `Options.hosts` with sensible defaults (bsky + community relays)\n- backoff resets on host switch, jetstream rewinds cursor by 10s\n- default jetstream hosts: 4 official bsky, waow.tech, fire.hose.cam, 6 firehose.stream regions\n- default firehose hosts: bsky.network + 3 firehose.network regions\n\n## 0.1.5\n\n- align firehose event types with AT Protocol sync spec\n\n## 0.1.4\n\n- firehose support: DAG-CBOR codec, CAR codec, CID creation, firehose client\n- encode and decode `com.atproto.sync.subscribeRepos` binary frames\n\n## 0.1.3\n\n- jetstream WebSocket client with typed events, reconnection, and cursor tracking\n- `extractAt` ignores unknown JSON fields by default\n- HTTP I/O isolated behind `HttpTransport` for 0.16 prep\n- websocket dependency pinned to specific commit\n\n## 0.1.2\n\n- `extractAt` logs diagnostic info on parse failures (enable with `.zat` debug scope)\n\n## 0.1.1\n\n- xrpc client sets `Content-Type: application/json` for POST requests\n- docs published as `site.standard.document` records on tag releases\n\n## 0.1.0\n\nsync types for firehose consumption:\n\n- `CommitAction` - `.create`, `.update`, `.delete`\n- `EventKind` - `.commit`, `.sync`, `.identity`, `.account`, `.info`\n- `AccountStatus` - `.takendown`, `.suspended`, `.deleted`, `.deactivated`, `.desynchronized`, `.throttled`\n\nthese integrate with `std.json` for automatic parsing.\n\n## 0.0.2\n\n- xrpc client with gzip workaround for zig 0.15.x deflate bug\n- jwt parsing and verification\n\n## 0.0.1\n\n- string primitives (Tid, Did, Handle, Nsid, Rkey, AtUri)\n- did/handle resolution\n- json helpers\n",
  "title": "changelog"
}