Spaces are read and written from grain's core, tested on the alpha
ClosedThis pull adds grain::space (space URIs, credential exchange, signed reads, and Admin writes with a simplespace fallback) and auth::verify_delegation. It also adds a harness that runs the spaces tests against the alpha built from atproto commit 11c5d98.
I read space.rs in full, verify_delegation and harness.yml. I did not read the diffs of harness/pds.mjs, harness/cov.mjs or tests/spaces.rs. The four checks (check, rust, test, harness) were still queued when I looked, so nothing has run yet.
I found nothing that must change.
- Delegation check: it checks the token type, that the audience is the space authority's PDS, and the signature.
verify_jwtalso checksexp. - Pagination: the loops stop on an empty page or a repeated cursor and are capped.
- Signing: the signing base matches the unit test.
Two notes to fix when convenient, neither blocking:
- Fallback loses the write flag: in
put_member, thesimplespace.addMemberfallback drops thewriteargument. A caller asking for read-only on pds.js or zds may get a different grant than on the alpha. Say so in the doc comment, or confirm whataddMembergrants. deniedis broad: it treats any 403 andRepoNotFoundas the authority's no. A caller that retries or shows an error on this should know a missing member repo looks the same as a refusal.
Once the checks finish, make sure the coverage gate in cov.json holds, since the new Admin and fallback paths are only reached by the alpha tests.
Landed on feature/rust at fc5f686 through the batch pull Seven reviewed pulls land on feature/rust together, checked once, checked there as a whole. This pull's head is in feature/rust's history.
What changes
grain::space, the core that private galleries and groups' pools are built on, after knotless'sspace.rs: a space's URI (at://<authority>/space/<type>/<skey>) and its parts; a credential to read one, made from a delegation token the reader's own PDS signs (getDelegationToken) and exchanged at the authority's PDS (getSpaceCredential) for a credential bound to a fresh P-256 key, each request signed for the repo it is addressed to (RFC 9421, as the alpha signs it); reading a repo's records (listRecords, every page), the space's writers (listRepos, credential only, as zds serves it) and a blob (getBlob) with it; the spaces an account's PDS lists (listSpaces, filtered byspaceTypeand pds.js'stype); andAdmin, what an account does with its own session: create a space with the memberspace policy, put a member to read or to read and write, list members,applyWrites,putRecord,deleteRecordand read its own repo there.deniedandnot_servedtell an authority's no from a PDS with no such method.grain::auth::verify_delegationchecks a delegation token as knotless's does (its type, that it names the space's authority's PDS, the reader's signature), for an app to prove a reader with in a later pull.The tests of spaces run on the spaces alpha.
harness/pds.mjsruns the dev-envGRAIN_DEV_ENVnames, and saysspaces: truefor one on the alpha;harness/cov.mjsruns the test binaries namedspacesagainst one such harness and the rest against the npm PDS as before;harness.ymlbuilds the alpha from atproto commit11c5d98(the one knotless's tests use) and caches it in/opt/atproto. AGENTS.md says how.Why
The first of the stack porting private galleries and groups' pools off the old server. Every read or write of a space goes through what is here, so it comes first and alone.
The old server read the same way, but through its framework; it is copied from knotless's Rust, with grain's fallback to
com.atproto.simplespace.*(pds.js, zds) kept: the reference names are asked first and the older ones when the PDS answers 404 or 501, in the shapes the old server'sserver/spaces/client.tssent (didnamed, the policy a bare string in a nestedconfig).The alpha is built from a commit rather than installed from npm because npm's
0.0.0-spaces-alpha-*builds name their dependenciesworkspace:, so they do not install (bun says so; npm exits 1 silently).How it was checked
crates/grain/tests/spaces.rs, against the alpha built locally from11c5d98: a private gallery made and written withapplyWrites, a reader put on it, its members listed, its blob read back by the reader with a credential, the owner finding it inlistSpaces, an outsider refused at the exchange (denied), and a real delegation token verified; a pool made by a stand-in group, a member writing and deleting their own records there, the group reading them with a credential and listing its writers, an outsider's write kept by their own PDS but never listed as the space's, and the outsider refused. Unit tests for URIs, signatures, refusals, blob CIDs, and the simplespace fallback against a stand-in PDS answering 501. fmt and clippy run here; the coverage gate and the answers check are left to the runner.Shortcomings
listRecords, notgetRepo's archive, and check no MAC: zds answersgetRepowith a CAR, not STAR, and these are live reads for a viewer rather than a sync, so there is no set hash kept to check against. grain's STAR-lite reader is not used here./opt/atproto.Review
Start at
crates/grain/src/space.rs; the harness changes areharness/pds.mjs,harness/cov.mjsand.knotless/workflows/harness.yml.🤖 Generated with Claude Code