~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2

Spaces are read and written from grain's core, tested on the alpha

Closed
Closed chadtmiller.com asked to merge into feature/rust from grain.social/grain:spaces/core

What changes

grain::space, the core that private galleries and groups' pools are built on, after knotless's space.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 by spaceType and pds.js's type); and Admin, 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, deleteRecord and read its own repo there. denied and not_served tell an authority's no from a PDS with no such method.

grain::auth::verify_delegation checks 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.mjs runs the dev-env GRAIN_DEV_ENV names, and says spaces: true for one on the alpha; harness/cov.mjs runs the test binaries named spaces against one such harness and the rest against the npm PDS as before; harness.yml builds the alpha from atproto commit 11c5d98 (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's server/spaces/client.ts sent (did named, the policy a bare string in a nested config).

The alpha is built from a commit rather than installed from npm because npm's 0.0.0-spaces-alpha-* builds name their dependencies workspace:, 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 from 11c5d98: a private gallery made and written with applyWrites, a reader put on it, its members listed, its blob read back by the reader with a credential, the owner finding it in listSpaces, 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

  • Reads use listRecords, not getRepo's archive, and check no MAC: zds answers getRepo with 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.
  • A collection read stops at 20 pages of 100.
  • The simplespace fallback is checked only against a stand-in, not pds.js or zds.
  • The first run builds the alpha (several minutes); later runs reuse /opt/atproto.

Review

Start at crates/grain/src/space.rs; the harness changes are harness/pds.mjs, harness/cov.mjs and .knotless/workflows/harness.yml.

🤖 Generated with Claude Code

Approved

This 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_jwt also checks exp.
  • 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, the simplespace.addMember fallback drops the write argument. 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 what addMember grants.
  • denied is broad: it treats any 403 and RepoNotFound as 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.