Spaces: server-side support in ATProtoNet.Server (space authority and repo host) #91
Labels
No labels
breaking-change
bug
documentation
duplicate
enhancement
good first issue
help wanted
performance
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/ATProto.NET#91
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
#89 landed the client side of the permissioned data protocol: an application can create spaces, read and write records, obtain space credentials, and sync. What it cannot do is serve a space.
A .NET service acting as a space authority or a repo host needs the other half, and none of it exists yet:
Xrpc/handler routing inATProtoNet.Server:getSpaceCredential,listRepos,registerNotify,unregisterNotify, and the repo-sidegetRecord/listRecords/getBlob/listBlobs/getLatestCommit/getRepo/listRepoOps.audagainstspaceHostAud(spaceDid)derived fromsub(a token minted for one authority must not work at another), verify against the issuer's#atprotokey, and enforce single use via a replay store keyed on(iss, jti, exp).SpaceTokens.Verifydoes the parsing and signature half already; the replay store and the ASP.NET plumbing do not exist.jwk, check that its thumbprint matches the credential'scnf.jkt, checkathagainst the presented credential, checkhtm/htuagainst the request as received, and reject a replayedjti. The SDK only generates proofs today (DPoPProofGenerator).iss(theclient_id) to itsclient-metadata.json, fetch the published JWKS (jwksorjwks_uri), and verify against the key named by the attestation'skid.com.atproto.simplespace.*handlers plus the member-list / policy storage they consult at credential-mint time, and thecheckUserAccesscall out to amanagingApp.notifyWriteout to registered syncers with service auth.The primitives are all in
ATProtoNet.Spacesalready (SpaceTokens,SpaceCommitVerifier,SpaceRepoCar.Serialize,SpaceAuthority); this is the ASP.NET Core layer over them.Worth splitting further — the auth verifiers are probably one issue and the handler surface another. See
docs/spaces.mdand proposal 0016.Filed by Claude while working on #89 (run).
🤖 On it — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
Pushed
claude/issue-91.What I built
The ASP.NET Core layer over the
ATProtoNet.Spacesprimitives, so a .NET service can act as a space authority, a repo host, or both. It registers as three separate opt-ins — a route that answers is a route that has to be secured, and a service that only needs to verify (a moderation service, a proxy) takes just the first:The verifiers are the half that has to be right. Three checks carry the protocol's guarantees:
audmust equalspaceHostAud(spaceDid)for the authority named in the token's ownsub— derived from the token, never taken from the request. An authority handed a token minted for a different authority cannot present it there.iss, so nobody but a space's authority can mint credentials for it (#atproto_spacepreferred,#atprotofallback).jwk— which proves nothing alone, since anyone can embed any key — so the thumbprint is matched against the credential'scnf.jkt, which is what makes it mean something.athpins the proof to the credential presented,htm/htuto this request as received,iatbounds its useful life,jtiis spent once.Single use is
ISpaceReplayStore, keyed on(iss, jti, exp)and consumed last — only once every other check has passed, so a forged token cannot burn the identifier of one a legitimate holder is about to present.Also: the full endpoint surface (authority + repo host), the seven
com.atproto.simplespacemethods with the baseline access policy (both perimeters, app first,checkUserAccessout to a managing app — and an unreachable managing app refuses, because failing open would turn every outage into an open space), and best-effortnotifyWritefan-out with first-write auto-registration of the authority.Two general additions to the XRPC routing came out of it and are reusable:
XrpcException, so a handler produces the named error clients branch on rather than a 500, andIXrpcBlobQuery<TParams>for methods whose outputencodingisn'tapplication/json(getRepo,getBlob).Verified
dotnet buildclean, 0 warnings (the project promotes CS1591 to an error). 1601 unit tests pass in both Debug and Release, 90 of them new — including aTestServerpass over the real HTTP surface from the credential exchange through a repo read authenticated with the credential it produced. The negative cases are the point: a token minted for another authority, a credential signed by an authority that doesn't gate the space, a stolen credential presented with the thief's own key, a proof minted for another host or another credential, a replayed token, an attestation signed by a key the client doesn't publish.Scope
The issue suggested splitting; I did all six bullets in one change instead, since the verifiers and the handlers are hard to test apart. What I deliberately left out is the write path (
createRecord/putRecord/applyWrites/getDelegationToken/listSpaces) — that's served by a user's PDS over its own OAuth session and needs the record store, oplog, and scope evaluation a PDS already has. This repo doesn't implement a PDS, so it isn't a gap here;docs/spaces.mdsays so explicitly.docs/spaces.mdgains a Serving a space section;api-reference.md,architecture.md,index.md,README.mdandCHANGELOG.mdupdated.⏱ 35m 5s · 107 turns · tokens: 340.9k in / 182.0k out (+22263.4k cache-read)
📜 Full session transcript
→ Opened PR: #101
→ Filed follow-up issue #102: Spaces: durable and multi-instance implementations of the space server stores