Core resolves identities, verifies atproto keys and calls XRPC
MergedThis pull brings knotless's protocol modules into the grain core: keys and JWTs, handle/DID/PDS resolution with its caches, the XRPC client, and the env, store and telemetry that sit under them.
I read identity.rs, xrpc.rs and the JWT verification in crypto.rs closely. I did not read env, store, telemetry or the tests.
I found nothing that must change.
- JWT verification requires
algto match the key, checks the signature, and rejects a missing or pastexp. - Handle resolution validates names before any lookup.
- Lookups at hosts the name points to are capped at 3 seconds.
- A handle is returned only if it resolves back to the DID.
- Refusals map cleanly to
XrpcError.
Two notes, neither blocking:
resolveandhandleread the cache file, change it, and write it back. Two concurrent lookups can overwrite each other's entry. This costs an extra lookup later, not a wrong answer.document_urlturns only%3Ainto:fordid:web. Adid:webwith path segments (did:web:host:user:alice) would build a bad URL and fail to resolve. Atproto uses host-onlydid:web, so this is fine for now.
The rust, check and test checks were still running or queued when I looked. Merge only once they pass, since the description says nothing was built or run locally.
The only change since my last round is in a test helper in xrpc.rs: it drops the unused Read import. My earlier review asked for nothing to change, and I found nothing new. The two earlier notes still apply and neither blocks the merge: concurrent cache writes can overwrite each other, and did:web with path segments isn't handled. The rust, check and test checks were still running or queued when I looked, so merge only once they pass. The description says nothing was built here.
Both notes are left as knotless has them, on purpose. The identity cache's read-modify-write can lose a concurrent entry, which costs a lookup and never a wrong answer; grain-serve's identities will live in its index rather than this file, so the file is the command line's, where lookups rarely race. did:web with path segments is not one atproto allows (host-only, as the spec says), so it failing to resolve is the right answer.
- Core resolves identities, verifies atproto keys and calls XRPC
- main

What changes
Keys and JWTs (
crypto): secp256k1 and P-256 public keys as multikeys,did:keys and DID document verification methods (the olderEcdsaSecp256*VerificationKey2019types too); low-S signature checks; P-256 signing keys with their JWK; compact JWTs signed with ES256, and verified against a key whosealgthey must name, withexpchecked.Identity (
identity): handle to DID (/.well-known/atproto-did, thencom.atproto.identity.resolveHandleon the Bluesky appview, asGRAIN_HANDLE_RESOLVERorders them), DID to document (PLC directory ordid:web), the#atproto_pdsendpoint and the#atprotosigning key, and a DID's handle only once it resolves back. Identities and handles are cached on disk underGRAIN_CACHE_DIR; DID documents in memory for five minutes. A PDS that answers that an account is not there marks the identity cached as on it stale, so an account that moved PDS is looked up again.XRPC (
xrpc): queries, procedures (JSON, none, or raw bytes),uploadBlob,getBlobandcom.atproto.space.getBlob(the repo namedrepo), with refusals asXrpcError(status, error, message,RateLimit-Reset). Auth is a bearer token or fixed headers; DPoP comes with OAuth in the next pull.Underneath:
env(theGRAIN_*settings these read, and the one shared HTTP client),keys(the clock and TIDs),store(JSON files written through a rename, private when asked), andtelemetry(each outgoing call logged at debug, or warn when it fails or takes a second, never its query or headers, with the trace handed on astraceparent).Why
grain-serve, the
grainCLI and later MCP all resolve identities and call PDSes; this is the first of the two pulls that bring knotless's protocol modules into the core, the second being OAuth, sessions and service auth on top of these.Taken from knotless's
crates/knotless/srcand cut to the protocol:crypto,identity,xrpc: as they are, lesskem_key(the#atproto_kemkey a space-syncing service agrees a secret with; grain reads member repos directly) and withBlobRefmoved intoxrpcfrom knotless'srecords.env: only the cache directory, PLC URL, handle resolvers,did:webscheme, extra CA certificates, a fixed PDS, identity TTL and timeout. Pack sizes, compaction, claims, appview, moderator, admin and bot DIDs, mirrors, the run SHA and update checks are knotless's. The config directory, identifier, password and browser come with the session pull, which reads them.keys: the clock and TIDs; every ref, claim, check, pack and release key is the forge's.telemetry: outgoing calls and traces only. Its/metricscounters, per-minute summaries, served-request lines, git and SQL lines, panic hook and filter reloading are left for when grain-serve has the requests to count; the slow threshold is a constant second rather thanKNOTLESS_SLOW_MS.store: as it is.How it was checked
Nothing was built or run here beyond
cargo fmtandcargo update -p grain; therustcheck builds and tests it. knotless's unit tests come along where their code did, with the variables renamed. Added: ES256K JWTs, as most PDSes sign service tokens; adid:webaccount resolved end to end against local servers (handle, document, PDS, signing key, verified handle, the disk cache, forgetting); document URLs;GRAIN_SERVICE; XRPC calls, blobs, empty answers and refusals against a local server; the env defaults and overrides.Shortcomings
No test talks to a real PDS or PLC directory: grain has no PDS harness yet, which is a later pull. Untested for that reason: PLC resolution against a directory,
resolveHandleagainst an appview, and every XRPC method against a PDS's real answers (uploadBlob,getBlob,space.getBlob), all of which the local servers stand in for only as far as the request's shape.Handles are resolved by HTTPS well-known and then the Bluesky appview, as knotless does; there is no DNS
_atprotoTXT lookup, so a handle with only a TXT record resolves through the appview.Review
Start at
identity.rs, thenxrpc.rs.crypto.rsis knotless's with comments adjusted and one test added.🤖 Generated with Claude Code