Skip to content

Permissioned data - #5187

Draft
dholms wants to merge 93 commits into
mainfrom
permissioned-data
Draft

Permissioned data#5187
dholms wants to merge 93 commits into
mainfrom
permissioned-data

Conversation

@dholms

@dholms dholms commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

⚠️ WIP ⚠️

dholms and others added 30 commits April 2, 2026 11:48
Covers record CRUD, space management, credential flow, and sync
protocol for the permissioned data ("spaces") system.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Multi-op atomic commits in oplog (rev + idx)
- isMember flag on space table for membership lifecycle
- Separate space_member_state table for owner-only commitment
- JWT typ headers for domain discrimination (space_member_grant, space_credential)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
28 tasks across 7 phases: core refactoring, credentials,
DB migration, actor store updates, lexicons, endpoints, tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename SpaceStorage interface to SpaceRepoStorage and MemoryStorage class to MemoryRepoStorage to better reflect their purpose and make room for SpaceMembersStorage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add comprehensive tests for SpaceMembers class including member addition/removal, duplicate detection, order-independent setHash, and storage reload. Also add domain separation test verifying that commits created with 'records' scope cannot be verified with 'members' scope and vice versa.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements JWT creation and verification for two credential types:
- MemberGrant: short-lived (5 min) token for members to request space credentials
- SpaceCredential: longer-lived (2 hour default) token for space access

Both use manual JWT encoding/decoding following atproto patterns, with proper signature verification, expiry checking, and type validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds new database schema types for space repo state, member state, oplogs,
and credential recipients. Updates migration 003 to restructure the space
and space_member tables by moving setHash/rev to separate state tables and
adding isMember flag to space table.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…or new table structure

Updated SpaceReader to read from new space_repo and space_member_state tables:
- getSpace() now returns isMember instead of setHash/rev
- getSetHash() and getRev() read from space_repo table
- Added getRepoState() and getMemberState() methods
- Added getRepoOplog() and getMemberOplog() for oplog queries
- Added getCredentialRecipients() for credential flow
- listMembers() now includes memberRev field

Updated SpaceTransactor with new commit and state management:
- createSpace() now creates space_repo and space_member_state rows
- Renamed applyCommit() to applyRepoCommit() with oplog tracking
- Added applyMemberCommit() for member operations
- Added updateMembership() to manage isMember flag
- Added recordCredentialRecipient() for credential tracking
- Updated deleteSpace() to clean up all related tables
- Updated addMember() to include memberRev field

Created new SQL storage adapters:
- SqlRepoStorage implements SpaceRepoStorage
- SqlMembersStorage implements SpaceMembersStorage
- Replaced ScopedSpaceRepoStorage with SqlRepoStorage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Updates PDS endpoints to use the restructured actor store:
- Rename ScopedSpaceRepoStorage to SqlRepoStorage in CRUD endpoints
- Replace applyCommit() with applyRepoCommit()
- Replace getRev() with getRepoState() for swap commit checks
- Update addMember/removeMember to use SpaceMembers class with proper
  set hash tracking and member commits
- Update notifyMembership to use updateMembership() instead of deleteSpace()
  when removing membership, preserving data while updating membership flag

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…points

Implements Tasks 21, 22, and 23 for the permissioned data protocol.

Task 21: Created 5 sync endpoints:
- getRepoState: Get sync state for a user's repo in a space
- getRepoOplog: Get operation log for incremental repo sync
- getMemberState: Get sync state for space member list
- getMemberOplog: Get operation log for member list sync
- getMembers: Get full member list for a space

All sync endpoints use spaceCredentialAuth and validate that the credential's space matches the requested space. Member endpoints read from the space owner's actor store.

Task 22: Created notifyWrite endpoint:
- Uses serviceAuth for PDS-to-PDS communication
- When space owner receives notification, fans out to credential recipients
- Fire-and-forget delivery (best effort, not guaranteed)

Task 23: Updated space/index.ts to register all new endpoints including getMemberGrant and getSpaceCredential that were created earlier.

All endpoints properly handle branded string types (DidString, NsidString, etc.) from the generated lexicon types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add comprehensive integration tests for the permissioned data protocol:
- Oplog and state tracking: verify record operations produce oplog entries, setHash updates, and batch writes use same rev with different idx
- Credential flow: test member grant/credential exchange, non-member rejection, and credential usage for sync endpoints
- Sync flow: test full and incremental record sync via oplog, and member list sync

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@matthieusieben matthieusieben left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, it feels quite unintuitive that a space has to be created with com.atproto.simplespace.createSpace or app.example.complexspace.newSpace, and then retrieved with com.atproto.space.getSpace.

It feels even more unintuitive, and unfortunate, that a user can make their community go from "member based" to "public" to "private", but can never in the future go from simplespace to complexspace.

I am strongly against this dual way of configuring space access (the "polity type", and then the "policy type"'s config). Even naming them is awkward: is "simplespace" a "config", a "policy", a "policy type" ?. I think we should have only one way of configuring access:

  • Either have multiple policy types that have a single behavior each ("simplespace", "publicspace", "privatespace", "managedspace", and more custom 3rd party policies in the future)
  • Or a single policy type that has a configurable behavior (ie merge all the "simplespace" stuff under "com.atproto.space")

If we do the former (which I believe is the right call), spaces would all be created with com.atproto.space.createSpace. That endpoint would have a required policy property, that can be any string token (e.g. com.atproto.space.defs#publicspace).

Then, based on that policy value, more endpoints become available to configure the space:

  • simplespace enables the member list management endpoints (addMember, etc.)
  • managedspace has endpoints to configure the managing app (like getConfig, setManagingApp, etc.)
  • publicspace and privatespace might not really need any config

We could codify this under a new lexicon type but I don't think we need to go that way. A simple convention around "policy X enables endpoints Y" (which can be setup as each TOKEN's description) is enough IMO.

Comment on lines +428 to +430
for (const collection of def.collections) {
yield collection
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (const collection of def.collections) {
yield collection
}
yield* def.collections

"lexicon": 1,
"id": "com.atproto.space.defs",
"defs": {
"signedCommit": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make this it's own lexicon document:

$type: "com.atproto.space.signedCommit"

instead of:

$type: "com.atproto.space.defs#signedCommit"

@@ -0,0 +1,80 @@
import { IdResolver, getHandle } from '@atproto/identity'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I've been trying to avoid @atproto/identity because it does not support "safeFetch" and is suceptible to SSRF attacks.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See @atproto-labs/handle-resolver-node and @atproto-labs/identity-resolver for alternatives that support custom (safe) fetch implementation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants