chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
Spaces follow the October 29 alpha
.changeset/git-spaces-alpha-20261029.mdnew+8−0
| @@ -0,0 +1,8 @@ | ||
--- | ||
'@pdsjs/git': patch | ||
--- | ||
| ||
The README creates a private repository's space with | ||
`com.atproto.space.createSpace` and a memberspace policy, and adds | ||
collaborators with `com.atproto.memberspace.putMember`, as the spaces alpha of | ||
2026-10-29 names them. | ||
.changeset/spaces-alpha-20261029.mdnew+39−0
| @@ -0,0 +1,39 @@ | ||
--- | ||
'@pdsjs/spaces': patch | ||
'@pdsjs/core': minor | ||
'@pdsjs/storage-sqlite': patch | ||
'@pdsjs/cloudflare': patch | ||
--- | ||
| ||
Spaces follow the reference's `0.0.0-spaces-alpha-20261029171913`. | ||
`com.atproto.simplespace.*` is gone, with every older spelling it accepted. | ||
| ||
- **Policies.** A space holds one policy: | ||
`com.atproto.personalspace.policy`, `com.atproto.memberspace.policy` | ||
(`publicRead`, `publicWrite`, `allowedApps`, `publishPolicy`, | ||
`publishMembers`) or `com.atproto.managedspace.policy` (`managingApp`, | ||
`publishPolicy`). `com.atproto.space.createSpace`, `updateSpace`, | ||
`deleteSpace` and `getSpace` manage it; `updateSpace` replaces it whole and | ||
a change of type drops every member. The authority is no longer a member of | ||
its own space and is always admitted. | ||
- **Members.** `com.atproto.memberspace.putMember` (`read` and `write` | ||
required), `removeMember` and `listMembers` answer `PolicyMismatch` outside a | ||
memberspace. Removing a member, or taking its `write`, drops it from the | ||
writer set, as `com.atproto.space.removeWriter` does for any writer. | ||
- **Credentials.** `getSpaceCredential` checks the user before the app. A user | ||
the policy refuses gets `SpaceNotFound`; an app the space does not allow gets | ||
`AppNotAuthorized`. A managedspace asks its app through | ||
`com.atproto.managedspace.checkUserAccess`. | ||
- **Published records.** `publishPolicy` writes `com.atproto.space.policy/self` | ||
and `publishMembers` writes one `com.atproto.memberspace.member` per member | ||
into the authority's repo in the space. Direct writes to either collection are | ||
refused. | ||
- **clearRecords.** `com.atproto.space.clearRecords` deletes every record the | ||
caller holds in a space. | ||
- **Storage.** `SpaceRow` carries `policy` in place of the six read, write and | ||
app fields, and `SpaceStoragePort` gains `removeAllMembers` and | ||
`removeSpaceWriter`. SQLite and Durable Object storage convert existing spaces | ||
on start: one with a managing app becomes a managedspace and loses its | ||
members, and any other becomes a memberspace that keeps them. | ||
- Account migration of spaces (`importRepo`, `exportSpaceMeta`, | ||
`importSpaceMeta`) is not implemented. | ||
docs/permissioned-data.md+74−37
| @@ -10,15 +10,11 @@ the credential that lets a reader pull every member repo. The PDS stores and | ||
serves those repos. It does not assemble them. That job belongs to the | ||
application. | ||
| ||
Reads and writes are governed separately. A space names a `readPolicy`, which | ||
decides who may hold a credential, and a `writePolicy`, which decides whose | ||
write notices the authority records and forwards. Under a member-list policy | ||
each member carries the two rights on their own, so a space can admit a reader | ||
who may not write into it. | ||
| ||
`@pdsjs/spaces` provides LtHash commits, MACed reads, space credentials, | ||
`space:` OAuth scopes, and the `com.atproto.simplespace` management endpoints. | ||
Node, Deno, and Cloudflare Workers all support it. | ||
A space's policy decides who may hold a credential and whose writes the | ||
authority records. `@pdsjs/spaces` provides LtHash commits, MACed reads, space | ||
credentials, `space:` OAuth scopes, and the `com.atproto.space`, | ||
`com.atproto.memberspace` and `com.atproto.managedspace` methods. Node, Deno, | ||
and Cloudflare Workers all support it. | ||
| ||
## Telling a client spaces are here | ||
| ||
| @@ -28,31 +24,70 @@ Node, Deno, and Cloudflare Workers all support it. | ||
every XRPC method it routes, so the space methods appear in that list exactly | ||
when the feature is on. | ||
| ||
## Two spellings of the same request | ||
## Policies | ||
| ||
A space holds exactly one policy, set by `createSpace` and replaced whole by | ||
`updateSpace`. The authority always reads and writes its own space, whatever | ||
the policy says. | ||
| ||
| Policy | Who reads | Whose writes count | | ||
| --- | --- | --- | | ||
| `com.atproto.personalspace.policy` | the authority alone | the authority alone | | ||
| `com.atproto.memberspace.policy` | anyone under `publicRead`, else members with `read` | anyone under `publicWrite`, else members with `write` | | ||
| `com.atproto.managedspace.policy` | whoever `managingApp` admits | whoever `managingApp` admits | | ||
| ||
A memberspace may also name `allowedApps`, the client_ids that may receive a | ||
credential. Absent, any client may. A managedspace asks its managing app on | ||
each credential request, through `com.atproto.managedspace.checkUserAccess` | ||
with service auth for that method. A managing app that does not answer, or | ||
answers no, admits nobody. | ||
| ||
`com.atproto.memberspace.putMember`, `removeMember` and `listMembers` work only | ||
on a memberspace. On any other policy they answer `PolicyMismatch`. A member | ||
added without `write`, removed, or changed to lose `write` leaves the writer | ||
set at once, and `com.atproto.space.removeWriter` does the same for any writer. | ||
A change of policy type drops every member. | ||
| ||
`com.atproto.space.getSpace` answers the authority with the space's URI and its | ||
policy. Nobody else may read the policy this way. | ||
| ||
### Published policy and members | ||
| ||
A space may publish its policy to its readers. With `publishPolicy` set, the | ||
authority writes the policy as `com.atproto.space.policy/self` into its own | ||
repo in the space. A memberspace with `publishMembers` set also writes one | ||
`com.atproto.memberspace.member` record per member, keyed by the member's DID. | ||
The server keeps these records in step with the policy and the member list. | ||
Nobody writes those two collections directly: `createRecord`, `putRecord`, | ||
`deleteRecord` and `applyWrites` refuse them with `InvalidRequest`. | ||
| ||
### Clearing a repo | ||
| ||
`com.atproto.space.clearRecords` deletes every record the caller holds in a | ||
space, in commits of at most 200 writes. It needs a delete scope for each | ||
collection it touches. In the authority's own repo it leaves the published | ||
policy and member records in place. | ||
| ||
### Spaces made before policies | ||
| ||
The proposal renamed things after implementations had shipped, so a client may | ||
know either spelling and this server answers to both. A client that guesses the | ||
wrong one otherwise reads the whole feature as absent. | ||
A database from the October 22 alpha holds each space's read and write rules | ||
in separate columns. On first start the server converts each space it is the | ||
authority of. A space with a managing app on either side becomes a | ||
managedspace, and its members are dropped, since its app decides access. Any | ||
other becomes a memberspace: public read or write carries over, an app allow | ||
list becomes `allowedApps`, and members keep their rights. Neither kind | ||
publishes anything until its authority asks. The old columns stay in the table | ||
and nothing reads them. | ||
| ||
| | Also accepted | | ||
| --- | --- | | ||
| `com.atproto.simplespace.getSpace` | `com.atproto.space.getSpace` | | ||
| `com.atproto.space.getLatestCommit` | `com.atproto.space.getRepoState` | | ||
| `com.atproto.simplespace.putMember` | `com.atproto.simplespace.addMember` | | ||
| `readPolicy` and `writePolicy` | one `policy`, which states both sides | | ||
| `policy` and `appAccess` unions | a nested `config` with a string `policy`, or the older `mintPolicy`, and a sibling `managingApp` | | ||
| no `did` on `createSpace` | a `did` naming the caller | | ||
| `applyWrites` writes tagged `$type` | writes named by an `action` string | | ||
| `spaceType` on `createSpace` and `listSpaces` | `type` | | ||
### Not implemented | ||
| ||
Every config field is optional. Absent, a space gets `member-list` on both | ||
policies and `#open`. `getSpace` answers with the two policy unions, the single | ||
`policy` union, and the `#spaceConfig` object together; the single one states | ||
the read side. `addMember` states no rights, so the member it adds reads and | ||
writes. | ||
The reference also moves spaces between servers with an account: | ||
`importRepo` restores space records, and `exportSpaceMeta` and | ||
`importSpaceMeta` carry a space's policy and members. pds.js implements none of | ||
the three. | ||
| ||
These pairs stay until the draft settles. `scripts/space-reference/` tracks the | ||
atproto PR; `ngerakines.me/atproto-crates` is the other implementation in use. | ||
`scripts/space-reference/` tracks the atproto PR; `ngerakines.me/atproto-crates` | ||
is the other implementation in use. | ||
| ||
## Getting a credential | ||
| ||
| @@ -74,10 +109,12 @@ holds the private half of. The authority puts that did:key in the credential's | ||
The delegation token comes from the user's own PDS through | ||
`getDelegationToken`, addressed to `<authority DID>#atproto_pds` and signed | ||
with the user's `#atproto` key. It says which user the application acts for, | ||
and a space whose read policy admits only some users needs one. A space with a | ||
public read policy issues a credential without one. Without a token, a space | ||
that is not public answers `SpaceNotFound`, the same as one that does not | ||
exist, so the request cannot learn that it does. A deleted space answers | ||
and a space whose policy admits only some users needs one. A memberspace with | ||
`publicRead` issues a credential without one. The authority checks the user | ||
before the application. A user the policy does not admit, or a missing token on | ||
a space that is not public, answers `SpaceNotFound`, the same as a space that | ||
does not exist, so the request cannot learn that it does. An admitted user | ||
through an application the space does not allow answers `AppNotAuthorized`. A deleted space answers | ||
`SpaceDeleted` either way. A token that does not verify, or one already spent, | ||
is `InvalidDelegationToken`. | ||
| ||
| @@ -177,7 +214,7 @@ says who: a JWT the client signs with its own authentication key. The authority | ||
resolves the `iss` to its `client-metadata.json`, fetches the key set that | ||
document publishes, and checks the signature against the key its `kid` names. | ||
| ||
An `appAccess` allow list rests on that check. Without it the client_id would be | ||
A memberspace's `allowedApps` list rests on that check. Without it the client_id would be | ||
a claim the caller writes for itself, and the list would state a rule the | ||
authority could not enforce. | ||
| ||
| @@ -209,7 +246,7 @@ space revision, every page with entries ends on one, and only an empty page has | ||
none. A syncer that sees a gap in `prevSpaceRev` catches up from its last | ||
`spaceRev` through `listRepos`. | ||
| ||
The authority applies the space's write policy to each notice it receives. A | ||
The authority applies the space's policy to each notice it receives. A | ||
writer the space does not admit stays out of the writer set, so `listRepos` | ||
never names them and no syncer pulls their repo. | ||
| ||
| @@ -274,7 +311,7 @@ Without the flag the space endpoints do not exist, and token issuance refuses | ||
| ||
The proposal and its reference implementation (atproto, published as | ||
`@atproto/space@0.0.0-spaces-alpha-*`; pds.js tracks | ||
`0.0.0-spaces-alpha-20261022141228`) are both drafts. Wire formats may change, | ||
`0.0.0-spaces-alpha-20261029171913`) are both drafts. Wire formats may change, | ||
and a change may need a repo rebuild. | ||
| ||
`scripts/space-reference/` tests both directions against the reference. pds.js | ||
examples/runclub/index.js+6−7
| @@ -164,21 +164,20 @@ async function main() { | ||
step('Maya creates the club and adds members'); | ||
const { uri: space } = await xrpcPost( | ||
maya.base, | ||
'com.atproto.simplespace.createSpace', | ||
'com.atproto.space.createSpace', | ||
{ | ||
type: CLUB_TYPE, | ||
spaceType: CLUB_TYPE, | ||
skey: CLUB_SKEY, | ||
readPolicy: { $type: 'com.atproto.simplespace.defs#memberListPolicy' }, | ||
writePolicy: { $type: 'com.atproto.simplespace.defs#memberListPolicy' }, | ||
appAccess: { $type: 'com.atproto.simplespace.defs#open' }, | ||
policy: { $type: 'com.atproto.memberspace.policy' }, | ||
}, | ||
maya.jwt, | ||
); | ||
console.log(` ${space}`); | ||
// Maya joined when she created the club, so only Sam is added here. | ||
// Maya is the club's authority, which every policy admits, so only Sam is | ||
// added here. | ||
await xrpcPost( | ||
maya.base, | ||
'com.atproto.simplespace.putMember', | ||
'com.atproto.memberspace.putMember', | ||
{ space, did: sam.did, read: true, write: true }, | ||
maya.jwt, | ||
); | ||
examples/runclub/README.md+4−3
| @@ -12,9 +12,10 @@ npm run dev:runclub # boots everything, feed at http://localhost:2800 | ||
| ||
What the demo exercises, over plain HTTP: | ||
| ||
1. `com.atproto.simplespace.createSpace` / `addMember` — Maya's PDS is the | ||
space authority (she adds herself too; the authority is not implicitly a | ||
member). | ||
1. `com.atproto.space.createSpace` under the `com.atproto.memberspace.policy` | ||
policy, then `com.atproto.memberspace.putMember` for Sam. Maya's PDS is the | ||
space authority, and every policy admits the authority, so she is not on | ||
the member list. | ||
2. `com.atproto.space.createRecord` — each member's runs land in their own | ||
repo on their own PDS. Sam's PDS notifies the authority, which is how his | ||
repo enters the writer set. | ||
packages/cloudflare/src/space.js+17−15
| @@ -11,7 +11,7 @@ import { | ||
spaceRecordsQuery, | ||
} from '@pdsjs/core/space-schema'; | ||
| ||
/** @typedef {{uri: string, space_did: string, space_type: string, is_owner: number, read_policy: string, read_managing_app: string|null, write_policy: string, write_managing_app: string|null, app_access_type: string, app_allowed: string, created_at: string, deleted_at: string|null}} SpaceDbRow */ | ||
/** @typedef {{uri: string, space_did: string, space_type: string, is_owner: number, policy_json: string|null, created_at: string, deleted_at: string|null}} SpaceDbRow */ | ||
/** @typedef {{did: string, can_read: number, can_write: number}} SpaceMemberDbRow */ | ||
/** @typedef {{collection: string, rkey: string, cid: string, value: ArrayBuffer|Uint8Array, repo_rev: string, indexed_at: string}} SpaceRecordDbRow */ | ||
/** @typedef {Omit<SpaceRecordDbRow, 'value'> & {value?: ArrayBuffer|Uint8Array}} SpaceRecordListDbRow */ | ||
| @@ -85,12 +85,7 @@ function toSpaceRow(row) { | ||
spaceDid: row.space_did, | ||
spaceType: row.space_type, | ||
isOwner: row.is_owner === 1, | ||
readPolicy: row.read_policy, | ||
readManagingApp: row.read_managing_app, | ||
writePolicy: row.write_policy, | ||
writeManagingApp: row.write_managing_app, | ||
appAccessType: row.app_access_type, | ||
appAllowed: JSON.parse(row.app_allowed), | ||
policy: row.policy_json ? JSON.parse(row.policy_json) : null, | ||
createdAt: row.created_at, | ||
deletedAt: row.deleted_at, | ||
}; | ||
| @@ -186,18 +181,13 @@ export function createSpaceStorage(sql, storage) { | ||
async putSpace(space) { | ||
sql.exec( | ||
`INSERT OR REPLACE INTO space | ||
(uri, space_did, space_type, is_owner, read_policy, read_managing_app, write_policy, write_managing_app, app_access_type, app_allowed, created_at, deleted_at) | ||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, | ||
(uri, space_did, space_type, is_owner, policy_json, created_at, deleted_at) | ||
VALUES (?, ?, ?, ?, ?, ?, ?)`, | ||
space.uri, | ||
space.spaceDid, | ||
space.spaceType, | ||
space.isOwner ? 1 : 0, | ||
space.readPolicy, | ||
space.readManagingApp, | ||
space.writePolicy, | ||
space.writeManagingApp, | ||
space.appAccessType, | ||
JSON.stringify(space.appAllowed), | ||
space.policy ? JSON.stringify(space.policy) : null, | ||
space.createdAt, | ||
space.deletedAt, | ||
); | ||
| @@ -286,6 +276,10 @@ export function createSpaceStorage(sql, storage) { | ||
: null; | ||
}, | ||
| ||
async removeAllMembers(space) { | ||
sql.exec('DELETE FROM space_member WHERE space = ?', space); | ||
}, | ||
| ||
async getSpaceRecord(space, collection, rkey) { | ||
const row = one( | ||
'SELECT collection, rkey, cid, value, repo_rev, indexed_at FROM space_record WHERE space = ? AND collection = ? AND rkey = ?', | ||
| @@ -439,6 +433,14 @@ export function createSpaceStorage(sql, storage) { | ||
return row?.head ?? null; | ||
}, | ||
| ||
async removeSpaceWriter(space, did) { | ||
sql.exec( | ||
'DELETE FROM space_writer WHERE space = ? AND did = ?', | ||
space, | ||
did, | ||
); | ||
}, | ||
| ||
async putSpaceWriter(space, did, repoRev, data, spaceRev) { | ||
sql.exec( | ||
'INSERT OR REPLACE INTO space_writer (space, did, rev, data, space_rev) VALUES (?, ?, ?, ?, ?)', | ||
packages/core/src/handlers/account.js+3−2
| @@ -2721,8 +2721,9 @@ export function createAccountHandlers(ctx) { | ||
spaceType: space.spaceType, | ||
authority: space.spaceDid, | ||
isOwner: space.isOwner, | ||
readPolicy: space.readPolicy, | ||
writePolicy: space.writePolicy, | ||
// The policy's name, as `memberspace` for | ||
// `com.atproto.memberspace.policy`. | ||
policy: space.policy?.$type.split('.').at(-2) ?? null, | ||
createdAt: space.createdAt.slice(0, 10), | ||
deleted: Boolean(space.deletedAt), | ||
members: await browser.countMembers(space.uri), | ||
packages/core/src/account-ui.js+2−2
| @@ -686,7 +686,7 @@ ${ | ||
| ||
/** | ||
* Render the spaces page (permissioned data, proposal 0016). | ||
* @param {{ hostname: string, spaces: Array<{uri: string, spaceType: string, authority: string, isOwner: boolean, readPolicy: string, writePolicy: string, createdAt: string, deleted: boolean, members: number, collections: Array<{collection: string, count: number}>}>, enabled: boolean }} params | ||
* @param {{ hostname: string, spaces: Array<{uri: string, spaceType: string, authority: string, isOwner: boolean, policy: string|null, createdAt: string, deleted: boolean, members: number, collections: Array<{collection: string, count: number}>}>, enabled: boolean }} params | ||
* @returns {string} HTML page content | ||
*/ | ||
export function renderSpacesPage({ hostname, spaces, enabled }) { | ||
| @@ -701,7 +701,7 @@ export function renderSpacesPage({ hostname, spaces, enabled }) { | ||
return `<div class="row"><div> | ||
<div class="title">${escapeHtml(space.spaceType)}${space.isOwner ? '<span class="tag">yours</span>' : ''}${space.deleted ? '<span class="tag">deleted</span>' : ''}</div> | ||
<div class="meta">${escapeHtml(space.uri)}<br> | ||
Authority ${escapeHtml(space.authority)} · reads ${escapeHtml(space.readPolicy)} · writes ${escapeHtml(space.writePolicy)} · created ${escapeHtml(space.createdAt)}<br> | ||
Authority ${escapeHtml(space.authority)}${space.policy ? ` · ${escapeHtml(space.policy)}` : ''} · created ${escapeHtml(space.createdAt)}<br> | ||
${space.members} member${space.members === 1 ? '' : 's'}${records ? ` · ${records}` : ' · no records'}</div> | ||
</div></div>`; | ||
}) | ||
packages/core/src/ports.js+14−7
| @@ -607,19 +607,24 @@ | ||
* @property {string} spaceDid - the space authority's DID, as carried in `uri` | ||
* @property {string} spaceType - the space type NSID, as carried in `uri` | ||
* @property {boolean} isOwner - whether this actor is the space authority | ||
* @property {string} readPolicy - 'public' | 'member-list' | 'managing-app' | ||
* @property {string|null} readManagingApp - service identifier for a managing-app read policy | ||
* @property {string} writePolicy - 'public' | 'member-list' | 'managing-app' | ||
* @property {string|null} writeManagingApp - service identifier for a managing-app write policy | ||
* @property {string} appAccessType - 'open' | 'allowList' | ||
* @property {string[]} appAllowed - client ids when appAccessType is 'allowList' | ||
* @property {SpacePolicy|null} policy - the policy governing the space, with | ||
* its defaults filled in. Null when this actor is not the authority. | ||
* @property {string} createdAt - ISO timestamp | ||
* @property {string|null} deletedAt - ISO timestamp, set by soft delete | ||
*/ | ||
| ||
/** | ||
* A space policy, as `com.atproto.space.createSpace` takes it: an open union | ||
* whose `$type` names the policy (`com.atproto.personalspace.policy`, | ||
* `com.atproto.memberspace.policy` or `com.atproto.managedspace.policy`) and | ||
* whose other fields configure it. | ||
* | ||
* @typedef {{$type: string} & Record<string, unknown>} SpacePolicy | ||
*/ | ||
| ||
/** | ||
* What a member of a space may do. Both flags are host-internal state the | ||
* authority consults when a member-list policy governs the space. | ||
* authority consults when the memberspace policy governs the space. | ||
* | ||
* @typedef {Object} SpaceMemberAccess | ||
* @property {boolean} read - whether the member may hold a space credential | ||
| @@ -728,6 +733,7 @@ | ||
* @property {(space: string, did: string) => Promise<void>} removeMember | ||
* @property {(space: string, cursor: string|null, limit: number) => Promise<{members: SpaceMemberRow[], cursor: string|null}>} listMembers | ||
* @property {(space: string, did: string) => Promise<SpaceMemberAccess|null>} getMember | ||
* @property {(space: string) => Promise<void>} removeAllMembers | ||
* @property {(space: string, collection: string, rkey: string) => Promise<SpaceRecordRow|null>} getSpaceRecord | ||
* @property {(space: string, collection: string, rkey: string) => Promise<string|null>} getSpaceRecordCid | ||
* @property {(space: string, opts: {collection?: string|null, cursor?: string|null, limit: number, reverse?: boolean, excludeValues?: boolean}) => Promise<{records: SpaceRecordListRow[], cursor: string|null}>} listSpaceRecords | ||
| @@ -738,6 +744,7 @@ | ||
* @property {(space: string, did: string) => Promise<SpaceWriterRow|null>} getSpaceWriter | ||
* @property {(space: string) => Promise<string|null>} getSpaceRevHead - the latest spaceRev allocated in the space | ||
* @property {(space: string, did: string, repoRev: string, data: Uint8Array, spaceRev: string) => Promise<void>} putSpaceWriter | ||
* @property {(space: string, did: string) => Promise<void>} removeSpaceWriter | ||
* @property {(space: string, cursor: string|null, limit: number) => Promise<{writers: SpaceWriterRow[], cursor: string|null}>} listSpaceWriters - ascending spaceRev after `cursor`, exclusive; `cursor` is the last entry's spaceRev, null on an empty page | ||
* @property {(space: string, jtis: string[], expiresAt: string) => Promise<void>} putCredentialRevocations - idempotent; also drops expired revocations | ||
* @property {(space: string, jti: string) => Promise<boolean>} isCredentialRevoked | ||
packages/core/src/space-schema.js+32−1
| @@ -23,7 +23,8 @@ export const SPACE_SCHEMA_SQL = ` | ||
app_access_type TEXT NOT NULL DEFAULT 'open', | ||
app_allowed TEXT NOT NULL DEFAULT '[]', | ||
created_at TEXT NOT NULL, | ||
deleted_at TEXT | ||
deleted_at TEXT, | ||
policy_json TEXT | ||
); | ||
-- space_did and space_type are the URI's authority and type components, | ||
-- supplied by the domain rather than re-derived here: parsing space URIs is | ||
| @@ -152,6 +153,36 @@ export const SPACE_MIGRATIONS = [ | ||
// A commit's digest is its `data`. | ||
'ALTER TABLE space_writer RENAME COLUMN hash TO data', | ||
'ALTER TABLE space_notify_outbox RENAME COLUMN hash TO data', | ||
// A space is governed by one policy object. The six simplespace columns | ||
// above hold a space's configuration from before policies, and only these | ||
// statements read them. They convert it as the reference's migration 006 | ||
// does. A managing app on either side makes a managedspace, which keeps no | ||
// member list and restricts no apps. Any other configuration makes a | ||
// memberspace. | ||
'ALTER TABLE space ADD COLUMN policy_json TEXT', | ||
// Before the conversion below, so it touches only the spaces it converts. | ||
`DELETE FROM space_member WHERE space IN ( | ||
SELECT uri FROM space | ||
WHERE policy_json IS NULL AND is_owner = 1 | ||
AND COALESCE(read_managing_app, write_managing_app) IS NOT NULL)`, | ||
`UPDATE space SET policy_json = CASE | ||
WHEN COALESCE(read_managing_app, write_managing_app) IS NOT NULL THEN | ||
json_object( | ||
'$type', 'com.atproto.managedspace.policy', | ||
'managingApp', COALESCE(read_managing_app, write_managing_app), | ||
'publishPolicy', json('false')) | ||
ELSE json_patch( | ||
json_object( | ||
'$type', 'com.atproto.memberspace.policy', | ||
'publicRead', json(CASE WHEN read_policy = 'public' THEN 'true' ELSE 'false' END), | ||
'publicWrite', json(CASE WHEN write_policy = 'public' THEN 'true' ELSE 'false' END), | ||
'publishPolicy', json('false'), | ||
'publishMembers', json('false')), | ||
CASE WHEN app_access_type = 'allowList' | ||
THEN json_object('allowedApps', json(app_allowed)) | ||
ELSE '{}' END) | ||
END | ||
WHERE policy_json IS NULL AND is_owner = 1`, | ||
]; | ||
| ||
// A record's position in its repo. The reference orders by the record's AT-URI, | ||
packages/core/test/include-scope.test.js+1−1
packages/git/test/oauth-remote.test.js+10−3
packages/git/test/space-remote.test.js+8−4
packages/git/README.md+1−1
packages/git-ui/src/lib/spaceSupport.js+1−1
packages/spaces/src/handlers/auth.js+44−49
packages/spaces/src/handlers/manage.js+208−349
557 lines changed: not loaded until asked for.
packages/spaces/src/handlers/write.js+86−54
packages/spaces/src/account.js+1−2
packages/spaces/src/admin.js+50−12
packages/spaces/src/attestation.js+2−2
packages/spaces/src/memory-storage.js+20−3
packages/spaces/src/notify.js+57−0
packages/spaces/src/policy.jsnew+272−0
packages/spaces/src/space-row.js+7−6
packages/spaces/test/account-api.test.js+8−4
packages/spaces/test/auth.test.js+98−21
packages/spaces/test/clear-records.test.jsnew+207−0
packages/spaces/test/manage.test.js+305−264
569 lines changed: not loaded until asked for.