fix(spaces)!: simplespace independent read/write access and authority interop #113

Closed
opened 2026-09-24 12:28:55 +00:00 by Grandiras · 0 comments
Owner

Part of the v0.7.0 update cycle. Upstream changed com.atproto.simplespace to separate read access from write access (bluesky-social/atproto#5496, merged 2026-09-10; proposal PR #105). The spaces-alpha PDS image, the hosted alpha PDS and the reference app (bulletin) all run the new API. Against them the SDK is now wire-incompatible on both the client and the server side:

  • createSpace → 400.
  • getSpace throws JsonException, because the required Policy field is missing.
  • updateSpace(policy:) returns 200 and silently changes nothing.
  • addMember no longer exists.

Comparing against the current reference also surfaced interop bugs that were already present in v0.6.0 (pre-existing below). com.atproto.space.*, LtHash, the commit/MAC, the CAR format, the tokens, DPoP and the space: scope grammar are all unchanged upstream.

Client (ATProtoNet)

  • createSpace / updateSpace / getSpace: replace policy with readPolicy and writePolicy. On create both are required on the wire; the client defaults each to MemberListPolicy and always sends both. On update each is optional and replaces the old value wholesale.
  • Remove addMember. Add putMember(space, did, read, write), an upsert that replaces both flags, plus the SpaceUri overload.
  • listMembers entries gain read / write.
  • checkUserAccess gains a required access (read | write); clientId is omitted for write checks.

Server (ATProtoNet.Server)

  • SimpleSpaceRecord, ISimpleSpaceStore, the in-memory and EF stores, and the EF entities: split Policy into ReadPolicy and WritePolicy. Replace AddMemberAsync / IsMemberAsync with PutMemberAsync(space, did, read, write) / GetMemberAsync returning the flags. Document the migration: copy Policy into both columns and give existing members read=write=true, matching upstream's 003-space-access migration.
  • Endpoints: putMember (owner only) replaces addMember; create/update/get carry both policies. SimpleSpaceAccessPolicy evaluates readPolicy + appAccess for credentials, and writePolicy with no app perimeter for writes. The authority is always admitted.
  • The authority must gate notifyWrite on write access and answer 403 when the writer is refused. Pre-existing: no user perimeter is applied today, while upstream already had one at our baseline.
  • The authority forwards accepted notifyWrites to endpoints registered with registerNotify (best-effort, off the request path). Pre-existing: SpaceWriteNotifier.NotifyWriteAsync has no caller.
  • Pre-existing: the managing-app client sends ?did=, but the lexicon parameter is user (SimpleSpaceAccessPolicy.cs:221), so every ManagingAppPolicy space refuses everyone. Send user and access, and omit clientId for writes.
  • Pre-existing: write notifications to an authority are resolved through #atproto_space_host only, with no fallback to #atproto_pds (SpaceAuthority.cs:87-98). Notifications to authorities on ordinary PDSes are logged and dropped, so their writer set never fills. Resolve with the fallback and send aud = the bare authority DID (what the reference authority checks).
  • Pre-existing: outbound service auth to fragment-bearing services (checkUserAccess → did:web:…#bulletin, registered notify recipients) must use the full service identifier as aud, not the bare DID.
  • Pre-existing: inbound notifyWrite on an SDK authority configured with ServiceDid rejects notifications from reference PDSes, which send aud = the authority DID. Accept {authority, authority#atproto_space_host, ServiceDid}.
  • notifyWrite.rev must be a valid TID; reject it as InvalidRequest before auth.

Docs, sample, tests

  • docs/testing-spaces.md: the alpha artifacts exist (ghcr.io/bluesky-social/atproto:pds-spaces-alpha, @atproto/pds@alpha, the invite-only spaces-alpha.host.bsky.network). Pin the image digest or source commit. The "no image" claim is also in CHANGELOG 0.6.0 and should be corrected in [Unreleased], not rewritten in history.
  • docs/spaces.md: read/write policies, putMember, and writer-set semantics (writers admitted by writePolicy, and an authority may exclude writers).
  • Update SpacesSample and the integration tests (SpaceNetworkFixture, SimpleSpacePolicyTests). Add a test that putMember(read: true, write: false) gets a credential but stays out of the writer set.
  • Unit tests for every item above, including managing-app wire parameters and delivery to an ordinary-PDS authority.

Before merging

Re-check the open upstream issues #5487 (type → spaceType), #5491 (a repo param on registerNotify) and #5462 (an unauthenticated policy variant). Adopt any that have landed.

CHANGELOG: Breaking changes, with migration notes (policy: → readPolicy:/writePolicy:, AddMemberAsync → PutMemberAsync, ISimpleSpaceStore/SimpleSpaceRecord shape changes, EF column split). The pre-existing items go under Fixed.

Part of the v0.7.0 update cycle. Upstream changed `com.atproto.simplespace` to separate read access from write access ([bluesky-social/atproto#5496](https://github.com/bluesky-social/atproto/pull/5496), merged 2026-09-10; proposal PR #105). The spaces-alpha PDS image, the hosted alpha PDS and the reference app (bulletin) all run the new API. Against them the SDK is now wire-incompatible on both the client and the server side: - `createSpace` → 400. - `getSpace` throws `JsonException`, because the `required` `Policy` field is missing. - `updateSpace(policy:)` returns 200 and silently changes nothing. - `addMember` no longer exists. Comparing against the current reference also surfaced interop bugs that were already present in v0.6.0 (*pre-existing* below). `com.atproto.space.*`, LtHash, the commit/MAC, the CAR format, the tokens, DPoP and the `space:` scope grammar are all **unchanged** upstream. ## Client (`ATProtoNet`) - [ ] `createSpace` / `updateSpace` / `getSpace`: replace `policy` with `readPolicy` and `writePolicy`. On create both are required on the wire; the client defaults each to `MemberListPolicy` and always sends both. On update each is optional and replaces the old value wholesale. - [ ] Remove `addMember`. Add `putMember(space, did, read, write)`, an upsert that replaces both flags, plus the `SpaceUri` overload. - [ ] `listMembers` entries gain `read` / `write`. - [ ] `checkUserAccess` gains a required `access` (`read` | `write`); `clientId` is omitted for write checks. ## Server (`ATProtoNet.Server`) - [ ] `SimpleSpaceRecord`, `ISimpleSpaceStore`, the in-memory and EF stores, and the EF entities: split `Policy` into `ReadPolicy` and `WritePolicy`. Replace `AddMemberAsync` / `IsMemberAsync` with `PutMemberAsync(space, did, read, write)` / `GetMemberAsync` returning the flags. Document the migration: copy `Policy` into both columns and give existing members `read=write=true`, matching upstream's `003-space-access` migration. - [ ] Endpoints: `putMember` (owner only) replaces `addMember`; create/update/get carry both policies. `SimpleSpaceAccessPolicy` evaluates `readPolicy` + `appAccess` for credentials, and `writePolicy` with **no** app perimeter for writes. The authority is always admitted. - [ ] **The authority must gate `notifyWrite` on write access** and answer 403 when the writer is refused. *Pre-existing:* no user perimeter is applied today, while upstream already had one at our baseline. - [ ] **The authority forwards accepted `notifyWrite`s to endpoints registered with `registerNotify`** (best-effort, off the request path). *Pre-existing:* `SpaceWriteNotifier.NotifyWriteAsync` has no caller. - [ ] *Pre-existing:* the managing-app client sends `?did=`, but the lexicon parameter is `user` (`SimpleSpaceAccessPolicy.cs:221`), so every `ManagingAppPolicy` space refuses everyone. Send `user` and `access`, and omit `clientId` for writes. - [ ] *Pre-existing:* write notifications to an authority are resolved through `#atproto_space_host` only, with **no fallback to `#atproto_pds`** (`SpaceAuthority.cs:87-98`). Notifications to authorities on ordinary PDSes are logged and dropped, so their writer set never fills. Resolve with the fallback and send `aud` = the bare authority DID (what the reference authority checks). - [ ] *Pre-existing:* outbound service auth to fragment-bearing services (`checkUserAccess` → `did:web:…#bulletin`, registered notify recipients) must use the full service identifier as `aud`, not the bare DID. - [ ] *Pre-existing:* inbound `notifyWrite` on an SDK authority configured with `ServiceDid` rejects notifications from reference PDSes, which send `aud` = the authority DID. Accept `{authority, authority#atproto_space_host, ServiceDid}`. - [ ] `notifyWrite.rev` must be a valid TID; reject it as `InvalidRequest` before auth. ## Docs, sample, tests - [ ] `docs/testing-spaces.md`: the alpha artifacts exist (`ghcr.io/bluesky-social/atproto:pds-spaces-alpha`, `@atproto/pds@alpha`, the invite-only `spaces-alpha.host.bsky.network`). Pin the image digest or source commit. The "no image" claim is also in CHANGELOG 0.6.0 and should be corrected in `[Unreleased]`, not rewritten in history. - [ ] `docs/spaces.md`: read/write policies, `putMember`, and writer-set semantics (writers admitted by `writePolicy`, and an authority may exclude writers). - [ ] Update `SpacesSample` and the integration tests (`SpaceNetworkFixture`, `SimpleSpacePolicyTests`). Add a test that `putMember(read: true, write: false)` gets a credential but stays out of the writer set. - [ ] Unit tests for every item above, including managing-app wire parameters and delivery to an ordinary-PDS authority. ## Before merging Re-check the open upstream issues [#5487](https://github.com/bluesky-social/atproto/issues/5487) (`type` → `spaceType`), [#5491](https://github.com/bluesky-social/atproto/issues/5491) (a `repo` param on `registerNotify`) and [#5462](https://github.com/bluesky-social/atproto/issues/5462) (an unauthenticated policy variant). Adopt any that have landed. CHANGELOG: `Breaking changes`, with migration notes (`policy:` → `readPolicy:`/`writePolicy:`, `AddMemberAsync` → `PutMemberAsync`, `ISimpleSpaceStore`/`SimpleSpaceRecord` shape changes, EF column split). The *pre-existing* items go under `Fixed`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/ATProto.NET#113
No description provided.