commits
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
exposes which host the client connected to, enabling callers to track
the current relay. follows the same comptime @hasDecl pattern as onError.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
completes the crypto module's did:key lifecycle — formatDidKey already
existed but the inverse (parsing a did:key string back to key type +
raw bytes) was missing. adds a unified Keypair struct for sign/verify
workflows and a convenience verifyDidKeySignature that dispatches by
curve type.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
corrections to 006, ReleaseSafe stack incident, sync 1.1
verification plumbing, lightrail collection index design.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: enable TCP keepalive on websocket connections — detect dead peers
in ~20s instead of blocking forever when remote disappears without
FIN/RST.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update websocket.zig fork with HTTP fallback support for serving
both WebSocket and HTTP on a single port.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
devlog 006: building a relay in zig — zlay architecture, deployment
war stories (musl/glibc, TCP splits, RocksDB iterator lifetimes,
pg.zig type strictness), collection index backfill, and operational
numbers.
also: SPA deep link fixes for standard.site, missing devlog entries
in publish-docs.zig, CI glibc fix.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ATProto paths use /devlog/001 but files are 001-self-publishing-docs.md.
copy short-name versions (001.md) during build so SPA deep links resolve.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
relative ./style.css etc. resolve against the current pathname, so
/devlog/002 requests /devlog/style.css (404). <base href="/"> anchors
all relative URLs to the site root.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
add _redirects with SPA rewrite so Wisp serves index.html for all
paths. app.js converts pathname to hash route on load, making URLs
like /roadmap and /devlog/001 work when linked from search indexes.
also add glibc to nixery deps so patchelf can actually find the
dynamic linker for wisp-cli.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
wisp-cli is dynamically linked against glibc, but nixery containers
don't have /lib64/ld-linux-x86-64.so.2. Use patchelf to rewrite the
interpreter and rpath to the nix store glibc paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch to zzstoatzz/websocket.zig fix/handshake-tcp-split which
prevents panics when TCP splits data between \r and \n during
WebSocket handshake parsing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
getInt returns ?i64 which truncates values > i64 max. upstream AT Protocol
firehose seq numbers now exceed i64 max, so callers need direct u64 access.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ChildRef union (none/node/stub) for partial trees loaded from CAR blocks.
putReturn/deleteReturn return displaced CIDs, copy() for deep clone,
loadFromBlocks() deserializes commit CARs into partial MSTs.
Operation/normalizeOps/invertOp implement the inductive firehose:
undoing ops against the post-commit MST root must recover prevData CID.
verifyCommitDiff() is the top-level pipeline: parse CAR, verify signature,
load partial MST, copy, normalize, invert, compare root to prevData.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
deprecated in zig 0.15 — migrate all 11 callsites across cbor tests,
firehose client, and jetstream client to the new API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
decodeMstNode() parses known MST CBOR schema directly — zero-copy byte
slicing, no Value unions. walkAndVerifyMst checks key heights during
traversal instead of full rebuild. MST step: 218ms → 39ms (5.5x),
compute total: 300ms → 123ms (2.4x) on pfrazee.com (192k records).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CAR parser now builds a StringHashMap index during read(), findBlock()
uses O(1) hash lookup instead of linear scan. verifyRepo bypasses
default 2MB/10k block limits for fetched repos.
Clarify Rust ecosystem in devlog: rsky (BlackSky), jacquard
(@nonbinary.computer), and hand-rolled RustCrypto bench.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
relative path doesn't resolve on zat.dev (serves from ATProto records,
not git). use tangled.org raw URL so the SVG renders on both zat.dev
and tangled.org.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
README was missing everything since 0.1.3 — added CBOR, CAR, MST,
firehose, jetstream, signing, repo verification sections. embedded
verify-compute.svg in devlog 005. updated changelog and roadmap.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CAR parser now accepts max_size and max_blocks options for large repo
verification. export jwt module for verify tool. devlog 005 covers the
three-way trust chain comparison (zig vs go vs rust).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
k256 5×52-bit field, Fermat scalar inversion, three-way benchmark
with blacksky's rsky stack. changelog backfill for 0.2.1 and 0.2.2.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
reject CAR data > 2MB and block count > 10,000, matching indigo's
safety limits. removes the last correctness asymmetry in the benchmarks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
traced indigo's full decode path to verify no correctness work is
skipped. the ~20x gap is implementation cost (reflection, heap alloc,
byte copying), not correctness differences.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Go has an experimental arena package (GOEXPERIMENT=arenas), but it's on
hold indefinitely and not recommended for production.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
car.read() now SHA-256 hashes each block and compares against the CID
digest. this is the correct behavior for untrusted data from the network.
car.readWithOptions() accepts a verify_block_hashes flag to skip
verification for trusted local data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
address work asymmetry (zig decoded ~2.3k op-linked blocks while
rust/go decoded all ~23k), note block decode cardinality as dominant
factor over async overhead, explain python > rust result (libipld
sync vs iroh-car async), call out indigo as slowest despite being
bluesky's production relay.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
the domain subdirectories (syntax/, crypto/, identity/, repo/, xrpc/)
mirror bluesky-social/atproto packages, not indigo's layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
192k records is too slow for CI. kept as a commented-out stress test
for local use.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
devlog 003: trust chain verification — covers interop tests, MST,
code reorg, and the new repo verifier pipeline.
also registers devlog 002 and 003 in publish-docs script.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
verifyRepo(allocator, identifier) exercises the full AT Protocol trust chain:
handle → DID → DID doc → signing key → fetch repo → verify commit sig → walk MST → rebuild → CID match
integration tests against zzstoatzz.io (self-hosted PDS) and pfrazee.com (bsky.network PDS)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
group modules by AT Protocol domain boundaries (following bluesky-social/indigo):
syntax/ — string primitives (did, handle, nsid, tid, rkey, at_uri)
crypto/ — jwt, multibase, multicodec
identity/ — did_document, did_resolver, handle_resolver
repo/ — cbor, car, mst
xrpc/ — transport, xrpc client, json helpers
streaming/ — firehose, jetstream, sync
testing/ — interop tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
merkle search tree with put/get/delete/rootCid, verified against
interop commit-proof fixtures (6) and common-prefix vectors (13).
ECDSA signing (signSecp256k1/signP256) with RFC 6979 deterministic
nonces and low-S normalization. did:key formatting via multicodec
encode + base58btc. base32lower encode/decode for CID strings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire up official bluesky-social/atproto-interop-tests as a lazy build
dependency. Tests are embedded at comptime and only fetched when running
zig build test.
Tier 1 — syntax validation for Tid, Did, Handle, Nsid, Rkey, AtUri
against valid/invalid fixture files.
Tier 2 — crypto signature verification against 6 test vectors covering
ES256/ES256K with valid low-S, invalid high-S, and invalid DER-encoded
signatures.
Tier 3 — MST key height computation (SHA-256 leading zero bits / 2)
against 9 test vectors.
Bumps version to 0.1.8.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AT Protocol requires low-S normalization (BIP-62 style). Signatures
where S > curve_order/2 are now rejected for both secp256k1 and P-256.
Without this, malleable signatures would verify successfully. The
verify functions are now pub for direct use outside JWT context.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NSID parser now enforces that the first domain segment (TLD) must start
with a letter, per the spec regex. Previously accepted strings like
"1.0.0.127.record".
AT-URI parser now validates all components: authority must be a valid DID
or handle, collection must be a valid NSID, rkey must be a valid record
key. Also rejects forbidden characters (space, #, ?).
Found by running official atproto-interop-tests fixtures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cid now stores only raw bytes (16B) instead of parsed fields (56B).
Value union shrinks from 64B to 24B, MapEntry from 80B to 40B.
CID decode is zero-cost (byte slice reference), map keys are read
inline without full decodeAt dispatch.
Breaking: Cid field access is now via methods — cid.version().?
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rotate through multiple hosts on reconnect for client-side load balancing
and resilience. defaults include official bsky instances plus community
relays (waow.tech, fire.hose.cam, firehose.stream, firehose.network).
- Options.host → Options.hosts (slice with sensible defaults)
- subscribe() advances host_index on each reconnect
- backoff resets when switching to a new host
- jetstream rewinds cursor by 10s on host switch (instances may lag)
- single-host usage still works: .hosts = &.{"my-host"}
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- add missing CommitEvent fields: since, commit CID, blobs
- add cid to RepoOp for downstream CID verification
- make rev, time required on CommitEvent (spec: required)
- make time required on IdentityEvent and AccountEvent (spec: required)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
full encode/decode for the com.atproto.sync.subscribeRepos firehose:
- DAG-CBOR codec with deterministic key sorting and shortest integer encoding
- CID creation (SHA-256 hashing → CIDv1) and parsing
- CAR v1 reader/writer with root extraction and block lookup
- firehose frame encoder/decoder with WebSocket client
- 47 tests including real-record CID verification against production data
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
use json_helpers.getString/getInt/getBool consistently instead of
manual switch chains. collapse duplicated processMessage into a
call to parseEvent. fix subtle use-after-free on record field by
making arena lifetime explicit in the API.
702 → 499 lines, parsing core 130 → 30 lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pointing at master.tar.gz is a moving target — causes hash mismatches
in clean builds (e.g. Docker) when upstream master advances.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
completes the crypto module's did:key lifecycle — formatDidKey already
existed but the inverse (parsing a did:key string back to key type +
raw bytes) was missing. adds a unified Keypair struct for sign/verify
workflows and a convenience verifyDidKeySignature that dispatches by
curve type.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: enable TCP keepalive on websocket connections — detect dead peers
in ~20s instead of blocking forever when remote disappears without
FIN/RST.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update websocket.zig fork with HTTP fallback support for serving
both WebSocket and HTTP on a single port.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
devlog 006: building a relay in zig — zlay architecture, deployment
war stories (musl/glibc, TCP splits, RocksDB iterator lifetimes,
pg.zig type strictness), collection index backfill, and operational
numbers.
also: SPA deep link fixes for standard.site, missing devlog entries
in publish-docs.zig, CI glibc fix.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
add _redirects with SPA rewrite so Wisp serves index.html for all
paths. app.js converts pathname to hash route on load, making URLs
like /roadmap and /devlog/001 work when linked from search indexes.
also add glibc to nixery deps so patchelf can actually find the
dynamic linker for wisp-cli.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ChildRef union (none/node/stub) for partial trees loaded from CAR blocks.
putReturn/deleteReturn return displaced CIDs, copy() for deep clone,
loadFromBlocks() deserializes commit CARs into partial MSTs.
Operation/normalizeOps/invertOp implement the inductive firehose:
undoing ops against the post-commit MST root must recover prevData CID.
verifyCommitDiff() is the top-level pipeline: parse CAR, verify signature,
load partial MST, copy, normalize, invert, compare root to prevData.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
decodeMstNode() parses known MST CBOR schema directly — zero-copy byte
slicing, no Value unions. walkAndVerifyMst checks key heights during
traversal instead of full rebuild. MST step: 218ms → 39ms (5.5x),
compute total: 300ms → 123ms (2.4x) on pfrazee.com (192k records).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CAR parser now builds a StringHashMap index during read(), findBlock()
uses O(1) hash lookup instead of linear scan. verifyRepo bypasses
default 2MB/10k block limits for fetched repos.
Clarify Rust ecosystem in devlog: rsky (BlackSky), jacquard
(@nonbinary.computer), and hand-rolled RustCrypto bench.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
address work asymmetry (zig decoded ~2.3k op-linked blocks while
rust/go decoded all ~23k), note block decode cardinality as dominant
factor over async overhead, explain python > rust result (libipld
sync vs iroh-car async), call out indigo as slowest despite being
bluesky's production relay.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
verifyRepo(allocator, identifier) exercises the full AT Protocol trust chain:
handle → DID → DID doc → signing key → fetch repo → verify commit sig → walk MST → rebuild → CID match
integration tests against zzstoatzz.io (self-hosted PDS) and pfrazee.com (bsky.network PDS)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
group modules by AT Protocol domain boundaries (following bluesky-social/indigo):
syntax/ — string primitives (did, handle, nsid, tid, rkey, at_uri)
crypto/ — jwt, multibase, multicodec
identity/ — did_document, did_resolver, handle_resolver
repo/ — cbor, car, mst
xrpc/ — transport, xrpc client, json helpers
streaming/ — firehose, jetstream, sync
testing/ — interop tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
merkle search tree with put/get/delete/rootCid, verified against
interop commit-proof fixtures (6) and common-prefix vectors (13).
ECDSA signing (signSecp256k1/signP256) with RFC 6979 deterministic
nonces and low-S normalization. did:key formatting via multicodec
encode + base58btc. base32lower encode/decode for CID strings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire up official bluesky-social/atproto-interop-tests as a lazy build
dependency. Tests are embedded at comptime and only fetched when running
zig build test.
Tier 1 — syntax validation for Tid, Did, Handle, Nsid, Rkey, AtUri
against valid/invalid fixture files.
Tier 2 — crypto signature verification against 6 test vectors covering
ES256/ES256K with valid low-S, invalid high-S, and invalid DER-encoded
signatures.
Tier 3 — MST key height computation (SHA-256 leading zero bits / 2)
against 9 test vectors.
Bumps version to 0.1.8.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AT Protocol requires low-S normalization (BIP-62 style). Signatures
where S > curve_order/2 are now rejected for both secp256k1 and P-256.
Without this, malleable signatures would verify successfully. The
verify functions are now pub for direct use outside JWT context.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NSID parser now enforces that the first domain segment (TLD) must start
with a letter, per the spec regex. Previously accepted strings like
"1.0.0.127.record".
AT-URI parser now validates all components: authority must be a valid DID
or handle, collection must be a valid NSID, rkey must be a valid record
key. Also rejects forbidden characters (space, #, ?).
Found by running official atproto-interop-tests fixtures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cid now stores only raw bytes (16B) instead of parsed fields (56B).
Value union shrinks from 64B to 24B, MapEntry from 80B to 40B.
CID decode is zero-cost (byte slice reference), map keys are read
inline without full decodeAt dispatch.
Breaking: Cid field access is now via methods — cid.version().?
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rotate through multiple hosts on reconnect for client-side load balancing
and resilience. defaults include official bsky instances plus community
relays (waow.tech, fire.hose.cam, firehose.stream, firehose.network).
- Options.host → Options.hosts (slice with sensible defaults)
- subscribe() advances host_index on each reconnect
- backoff resets when switching to a new host
- jetstream rewinds cursor by 10s on host switch (instances may lag)
- single-host usage still works: .hosts = &.{"my-host"}
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
full encode/decode for the com.atproto.sync.subscribeRepos firehose:
- DAG-CBOR codec with deterministic key sorting and shortest integer encoding
- CID creation (SHA-256 hashing → CIDv1) and parsing
- CAR v1 reader/writer with root extraction and block lookup
- firehose frame encoder/decoder with WebSocket client
- 47 tests including real-record CID verification against production data
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
use json_helpers.getString/getInt/getBool consistently instead of
manual switch chains. collapse duplicated processMessage into a
call to parseEvent. fix subtle use-after-free on record field by
making arena lifetime explicit in the API.
702 → 499 lines, parsing core 130 → 30 lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>