fix(spaces)!: simplespace independent read/write access and authority interop #148
No reviewers
Labels
No labels
breaking-change
bug
documentation
duplicate
enhancement
good first issue
help wanted
performance
question
wontfix
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/ATProto.NET!148
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-113-simplespace-access"
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?
Closes #113.
Brings
com.atproto.simplespacein line with upstream's independent read/write access (bluesky-social/atproto#5496, change1ce408be). The spaces-alpha PDS and bulletin already run it. Against them, v0.6 got a 400 fromcreateSpace, threw ongetSpace, hadupdateSpace(policy:)silently ignored, and called anaddMemberthat no longer exists. This PR also fixes the authority-side interop bugs that surfaced while checking the SDK against the current reference. Wire shapes were checked against the lexicons,simplespace/manager.ts,space/util.ts, and the003-space-accessmigration atpermissioned-data@787a730f, and against bulletin0acf237b.Client (
ATProtoNet)createSpace/updateSpace/getSpacecarryreadPolicy+writePolicyin place ofpolicy.CreateSpaceAsyncdefaults both toMemberListPolicyand always sends both, since both are required on the wire. On update each one is optional and replaces the old value wholesale.addMemberis removed.PutMemberAsync(space, did, read, write)is an upsert that replaces both flags, with aSpaceUrioverload.PutSimpleSpaceMemberRequestreplacesAddSimpleSpaceMemberRequest.listMembersentries (SimpleSpaceMember) gain requiredRead/Write.CheckUserAccessAsync(space, user, access, clientId?)takes the new requiredaccessparameter.SimpleSpaceAccess.Read/.Writeare the known values.Server (
ATProtoNet.Server)SimpleSpaceRecord,ISimpleSpaceStore, the in-memory and EF stores, and the EF entities carryReadPolicy/WritePolicy. Members carryRead/Write.AddMemberAsync/IsMemberAsyncbecomePutMemberAsync/GetMemberAsync.PutSimpleSpaceMemberEndpoint(owner only) replacesaddMember. Create, update, and get carry both policies.createSpacerefuses anullpolicy instead of storing it.ISpaceAccessPolicyalso decides writes.SpaceAccessRequest.AccessisSpaceAccessKind.Read(the default) orWrite.SimpleSpaceAccessPolicyevaluatesreadPolicy+appAccessfor credentials andwritePolicyalone for writes. The owner is always admitted.notifyWritegates on write access and answers 403 to a refused writer, which is neither recorded nor forwarded. Pre-existing: there was no user perimeter at all.notifyWrites are forwarded toregisterNotifysubscribers in the background (SpaceWriteNotifier.ForwardWriteAsync, never awaited on the request path, never faults), skipping the authority's own registration. Pre-existing:NotifyWriteAsynchad no caller on the authority side.NotifySpaceDeletedAsyncalso skips the authority's own registration now.user=(wasdid=, pre-existing) andaccess=read|write, and omitsclientIdon write checks.audis the full service identifier (did:web:…#forum), which is what bulletin verifies.{authority}#atproto_space_hostresolves with the#atproto_pdsfallback (SpaceAuthority.GetServiceEndpoint) and is addressed to the bare authority DID. Pre-existing: these notifications were dropped.audfor every other subscriber is the service identifier it registered, fragment and all. Pre-existing: it was the bare DID.notifyWriteaudaccepts{authority, authority#atproto_space_host, ServiceDid}. Pre-existing: a host configured with its ownServiceDidrefused every reference PDS.notifyWrite.revmust be a TID, andhashmust be present. Both are checked asInvalidRequestbefore auth.Docs, sample, tests
docs/testing-spaces.mddocuments the alpha artifacts: the GHCR image, pinned by digestsha256:4b19b376…(built from158c439b, which includes #5496),@atproto/pds@alpha/@atproto/dev-env@alpha, and the hosted PDS. The source recipe is pinned to787a730f, and there is a new npm recipe that I ran. It needs pnpm overrides because several@atproto/lex*packages have broken plain0.0.0releases.docs/spaces.mdcovers read/write policies,putMember, and writer-set semantics: writers admitted bywritePolicy, an authority may exclude writers, andpublicwrite policy tracks non-members. It also covers the authority-side notification flow, a bespokeISpaceAccessPolicyhandlingWrite, the EF upgrade migration, and a link tosamples/SpacesSample.docs/api-reference.mdis updated to match.SpacesSample,SpaceNetworkFixture, andSimpleSpacePolicyTestsare updated. Five new integration tests are added, includingputMember(read: true, write: false): the member gets a credential but stays out of the writer set.Breaking changes (migration)
CreateSpaceAsync/UpdateSpaceAsync:policy:becomesreadPolicy:+writePolicy:. Pass the same value to both to keep the old behaviour.GetSimpleSpaceResponse.PolicybecomesReadPolicy/WritePolicy.AddMemberAsync(space, did)becomesPutMemberAsync(space, did, read: true, write: true).CheckUserAccessAsync:accessis inserted beforeclientId.SimpleSpaceRecordtakes both policies.ISimpleSpaceStorehasPutMemberAsync/GetMemberAsyncin place ofAddMemberAsync/IsMemberAsync.SpaceNsids.AddSimpleSpaceMemberandAddSimpleSpaceMemberEndpointbecomePutSimpleSpaceMemberandPutSimpleSpaceMemberEndpoint.AtProtoSimpleSpaces.PolicybecomesReadPolicy+WritePolicy, andAtProtoSimpleSpaceMembersgainsRead+Write. Consumers own their migrations. CopyPolicyinto both new columns before dropping it, and default the member flags totruefor existing rows. A snippet is in docs/spaces.md. The model deliberately has no DB default on the flags, because EF would then skip sendingfalse; a regression test covers this.ISpaceAccessPolicyimplementations must handleSpaceAccessKind.Writewithout an app perimeter, or they refuse every writer.ISimpleSpaceManagingAppClient.CheckUserAccessAsynctakes aSpaceAccessKind.ISpaceServiceAuthVerifier.VerifyAsynctakesIReadOnlyCollection<string>accepted audiences.SpaceServiceAuthVerifierkeeps a single-audience overload.NotifyWriteEndpoint's constructor takesISpaceAccessPolicy, plus an optional notifier and logger. DI users are unaffected.Verification
aud, delivery to an ordinary-PDS authority, fragmentaud, forwarding over aTestServerhost, write-policy refusal (403), the three accepted audiences, TIDrevrejected before auth, member flag round trips in both stores, and the EFfalse-flag regression.TestNetworkNoAppViewfrom@atproto/dev-env@0.0.0-spaces-alpha-20260915165437(the same build as the GHCR image). All 31Space*integration tests pass (26 existing, 5 new) withATPROTO_REQUIRE_INTEGRATION=1, so none were skipped.type→spaceType), #5491 (arepoparam onregisterNotify), and #5462 (an unauthenticated policy variant) are all still open.permissioned-datais still at787a730f, and nothing has landed in its lexicons, so nothing was adopted.Deferred / not in scope
kidallow-list, DPoP strictness, CAR hardening, space-type name/collections, open unions,sync.getBlobnote) belong to the hardening PR.[JsonPolymorphic]style, as agreed. An unknown$typefrom a future server (e.g. #5462) still throws.notifyWriteas its ownServiceDid, but the reference authority requiresiss == repo. So notifications from SDK repo hosts to reference authorities are still refused. Likewise, bulletin requiresiss= the space authority oncheckUserAccess, which an SDK authority only satisfies whenServiceDidis the authority's DID. Fixing either needs per-account signing, which is outside this issue.🤖 Generated with Claude Code
@claude /review
🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
✅ Verdict: approve — no blocking issues found.
This is a large, carefully executed breaking-change PR bringing
simplespacein line with upstream's read/write policy split. I traced the trickiest parts of the diff rather than skimming:PutMemberAsyncrace handling (EfCoreSimpleSpaceStore.cs): the retry-on-insert-conflict logic correctly scopes thecatch (DbUpdateException) when (entity is null)to only the insert path, and a losing insert falls through to a second attempt that finds the winner's row and overwrites both flags. Sound.SpaceWriteNotifier.ForwardWriteAsync, called viaTask.Runoff the request path): I checked whether this risks using ASP.NET-scoped, request-disposed dependencies after the response completes.SpaceWriteNotifier,ISpaceAuthorityStore, andISimpleSpaceStoreare all registeredTryAddSingletoninSpaceServerExtensions.cs, and the EF store usesIDbContextFactoryper-call, so this is safe.#atproto_space_hostregistration vs. other subscribers:SpaceAuthority.GetServiceEndpoint's newHostServiceIdbranch correctly reusesGetHostEndpoint(with its#atproto_pdsfallback), andSpaceWriteNotifier.Audience/IsAuthoritycorrectly special-case only the authority's own subscription to address by bare DID while every other subscriber gets its full registered service identifier — consistent with the PR's stated fix for previously-dropped/misaddressed notifications.notifyWritevalidation ordering and accepted audiences: malformedrev/hashare rejected before the service-auth check, andAcceptedAudiencescorrectly includes the bare authority DID,{authority}#atproto_space_host, and (when distinct) the configuredServiceDid.putMemberreplacingaddMember), CHANGELOG entries, and the extensive doc/sample/test updates are all consistent with the described breaking changes.I did not find any correctness issues worth blocking on.
⏱ 0m 58s · 9 turns · tokens: 85.0k in / 5.9k out (+754.2k cache-read)
📜 Full session transcript