chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
# @pdsjs/spaces
## 2.0.10
### Patch Changes
- Updated dependencies
- @pdsjs/core@2.5.2
## 2.0.9
### Patch Changes
- Updated dependencies [542c4dc]
- @pdsjs/core@2.5.1
## 2.0.8
### Patch Changes
- e0b38cd: `dev.pdsjs.git.deleteRepo` takes a repository: the record, the config record
beside it, and the bundle blobs neither of them holds any more, freed at once
rather than at the hourly orphan sweep. The write guard reads the config for
its protected branches before either record goes, so a repository holding one
refuses with the branch named, which is the refusal the guard already had
words for.
A private repository keeps its space, which is keyed by the repository name
and is reused by a repository created under that name later. Its member list
goes with the repository, so the next repository of that name admits nobody
its owner did not add.
`SpaceAdminPort` gains `deleteRecord`, over the `delete` action the space
writer already applies.
- 93816a1: A space governs reads and writes separately, tracking
[atproto PR #5187](https://github.com/bluesky-social/atproto/pull/5187).
`createSpace` and `updateSpace` take a `readPolicy` and a `writePolicy`,
`getSpace` answers with both, and `com.atproto.simplespace.putMember` sets a
member's `read` and `write` rights, which `listMembers` reports. The read
policy decides who may hold a space credential. The write policy decides whose
`notifyWrite` the authority records, so a writer the space does not admit stays
out of the writer set and no syncer pulls their repo. The authority passes
`access` to a managing app's `checkUserAccess`, and omits `clientId` on a write
check: that notice comes from a PDS, not an app.
The earlier spellings still answer. A single `policy` sets both sides,
`addMember` adds a member who reads and writes, and `getSpace` sends the two
unions, the single one, and the `#spaceConfig` object together. A space created
before the split keeps its policy on both sides, and an existing member keeps
both rights.
A delegation token and a client attestation are each spent once. Both must
carry a `jti`, and an authority that has honoured one refuses it again, so a
captured credential exchange cannot be replayed.
A client attestation is verified rather than read. 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, and until now stated a rule the
authority could not enforce.
`SpaceStoragePort` replaces `addMember` and `isMember` with `putMember` and
`getMember`, `listMembers` answers with `members` rather than `dids`, and
`SpaceRow` carries `readPolicy`, `readManagingApp`, `writePolicy` and
`writeManagingApp`. Both SQLite adapters add the columns to a database they
already hold. `@pdsjs/core/oauth` exports `fetchClientMetadata`,
`resolveClientJwk` and `verifyEs256WithJwk`, which the OAuth token endpoint and
the space authority both use.
- 3d475f8: Permissioned data tracks the October 1 spaces alpha of the reference
implementation (`0.0.0-spaces-alpha-20261001173819`, atproto
`permissioned-data-alpha` at `79d6307e1`).
A space credential is bound to its holder's key with an RFC 9421 HTTP message
signature instead of DPoP. The application signs the `getSpaceCredential`
request's `Authorization` header with a fresh P-256 key, named as a did:key in
`keyid`, and the authority binds the credential to it as `cnf.kid`. A request
presenting the credential uses the `Atproto-Space` scheme and an
`Atproto-Space-Audience` header naming the account it is addressed to, and
signs both. A host refuses a signature made for another account
(`BadSpaceAudience`) or by another key (`BadSpaceSignature`). DPoP credentials,
the `dpopJkt` field and the `DPoP` presentation scheme are no longer accepted.
`@pdsjs/spaces/http-signature` signs and verifies both shapes, and the git
remote helper and the spaces inspector sign with it.
A credential lives ten minutes by default and never more than an hour, and must
carry a finite `iat`, an `exp` after it and a `jti`. A repo host serves
`com.atproto.space.notifyCredentialRevoked`: a space's authority names up to 100
credential ids, and the host refuses those credentials with
`CredentialRevoked` for an hour and ten seconds.
The authority sequences the writer set. Each accepted `notifyWrite` gets a
`spaceRev` that increases across the space, and the notice forwarded to a
syncer carries `repoRev`, `spaceRev` and `prevSpaceRev`. A repeated or older
repo rev is not forwarded again, and a repo rev more than five minutes ahead is
refused with `FutureRev`. `listRepos` entries carry `repoRev` and `spaceRev`,
are ordered by `spaceRev`, and page on a space-revision cursor that every
non-empty page ends on. A writer recorded before this release is sequenced at
its repo rev. The authority accepts a notice addressed to
`<authority>#atproto_space_host` as well as to its bare DID, and a body naming
`rev` instead of `repoRev`.
A repo host stores each write notice it owes an authority before sending it,
and retries a transient failure with backoff for up to a day, keeping only the
newest notice per space. Extensions may declare a `tick`, which core runs
through `runExtensionTicks`: Node and Deno every ten seconds, Cloudflare at its
alarm, which an extension brings forward with the host's new `scheduleWakeup`.
`createSpace` and `listSpaces` take `spaceType`, and still read `type`.
`SpaceStoragePort` changes: `putSpaceWriter` takes a `spaceRev`, writer rows
carry `repoRev` and `spaceRev`, and the port gains `getSpaceWriter`,
`getSpaceRevHead`, the revocation methods and the notice outbox methods. The
schema adds `space_writer.space_rev`, `space_credential_revocation` and
`space_notify_outbox`, migrated in place.
- 04b15d0: `com.atproto.space.getSpace` answers `SpaceNotFound` for a space that does
not exist before it asks who is reading, so a caller with no credential can
tell whether a space is there. A space's address is its owner's DID and a
key, so its existence was never a secret; what it holds still needs a
session or a space credential.
- 5bb14a2: Require a space credential on `com.atproto.space.listRepos`.
The method served the writer set to anyone who could name a space: the DIDs
that have written to it, and each one's current rev, which is a TID and so
decodes to the time of that write. For a permissioned space that is its
membership and its activity, disclosed without authentication. A space URI is
guessable, since the authority DID is public, the type is a published NSID and
`self` is a common record key.
It was the only method in the package with no authorization at all. Every other
route either declares `auth` or verifies a credential itself. The atproto
reference holds `listRepos` to `spaceCredentialAuth`, alongside `registerNotify`
and `unregisterNotify`, which this package already gates the same way.
The credential is checked before the space is looked up, so a caller holding
none cannot learn which spaces a server is the authority for.
A client reading the writer set must now present a credential for that space,
which is the same credential it already needs to read any repo in it.
- Updated dependencies [d66fcff]
- Updated dependencies [05a11b4]
- Updated dependencies [f79dd82]
- Updated dependencies [e0b38cd]
- Updated dependencies [cc4577b]
- Updated dependencies [7c06024]
- Updated dependencies [6935a55]
- Updated dependencies [15c69af]
- Updated dependencies [411b47b]
- Updated dependencies [861663f]
- Updated dependencies [154e7d7]
- Updated dependencies [10c91f6]
- Updated dependencies [77eb9db]
- Updated dependencies [76f5187]
- Updated dependencies [b51250e]
- Updated dependencies [799286b]
- Updated dependencies [4adc49e]
- Updated dependencies [9149663]
- Updated dependencies [fd401a4]
- Updated dependencies [93816a1]
- Updated dependencies [3d475f8]
- Updated dependencies [078c2df]
- Updated dependencies [d5f111c]
- @pdsjs/core@2.5.0
## 2.0.7
### Patch Changes
- Updated dependencies [d307f57]
- Updated dependencies [2d0d653]
- @pdsjs/core@2.4.3
## 2.0.6
### Patch Changes
- Updated dependencies [c235e96]
- @pdsjs/core@2.4.2
## 2.0.5
### Patch Changes
- Updated dependencies [fa1558c]
- @pdsjs/core@2.4.1
## 2.0.4
### Patch Changes
- Updated dependencies [ee6d696]
- Updated dependencies [54bec84]
- Updated dependencies [f716d9d]
- Updated dependencies [4c334b3]
- Updated dependencies [6b79149]
- Updated dependencies [8616855]
- Updated dependencies [af8b1fd]
- @pdsjs/core@2.4.0
## 2.0.3
### Patch Changes
- f8e3dff: Private repositories in the account page, and a blob privacy fix. The
repositories page lists space-held repos with a Private badge, browses their
files, and manages the space's members; a New repository dialog creates the
space behind a private repo and shows the push commands. The public
`com.atproto.sync.getBlob` and `sync.listBlobs` endpoints no longer serve or
list a blob that only space records reference; `com.atproto.space.getBlob`
remains the authenticated path to those.
- 4a19757: Bind a space credential to the key a DPoP proof demonstrates.
`getSpaceCredential` read the key's thumbprint from a `dpopJkt` field in the
request body. A thumbprint named in a body is a claim: any holder of the
delegation token can make it about a key somebody else controls. A request may
now carry an RFC 9449 proof instead, and the credential binds to the key that
proof demonstrates. The proof carries no `ath`, because a delegation token is
an authorization grant rather than an access token.
The body field still works, so a client written against either shape is served.
A request carrying both must name the key it proves.
- bdad8c4: Branch protection, enforced server-side. Repository settings on the account
page write a `dev.pdsjs.git.config` record beside the repo record; a write
guard built by the git package and injected by the platforms refuses a repo
record write that force-pushes, deletes, or drops a protected branch,
checking ancestry against the bundle chain. Settings also edit the
description and default branch. The guard rides a new RecordWriteGuardPort
consulted by the public and space record write paths.
- d8cae21: Hold resolved DID documents, so a space request stops reading the directory.
Every space credential names its issuer, and verifying one reads that issuer's
key. plc.directory sends no `Cache-Control`, so nothing below the resolver held
the answer and every space request paid a round trip to it.
`createCachedDidResolver` holds an answer for an hour, an unresolvable DID for a
minute, and serves what it holds for up to a day when the directory stops
answering. The platform packages build one at startup and share it between the
PDS and the space routes.
A held document can name a key its issuer has rotated away from. Verification
now reads past the held answer and tries once more when a signature fails,
which is how `getSigningKey` gained its `forceRefresh` argument. The service
token that authorizes `com.atproto.server.createAccount` always reads past it:
adopting a DID is worth a fresh read, and it happens once per account.
- d8cae21: Take the JWT `alg` from the signing key's curve, on both sides of the wire.
An account with a secp256k1 key signed delegation tokens, space credentials and
`notifyWrite` service auth under a hardcoded `alg: ES256` header. A verifier
resolves the issuer's key, reads K256, and refuses the token before it checks
the signature. The platform packages now declare the curve on the signer they
hand `@pdsjs/spaces`, and secp256k1 keys sign ES256K.
Verification refuses a header naming an algorithm the issuer's key cannot
produce, which the reference implementation has always done. This covers space
tokens, inbound space service auth, and the service token that authorizes
`com.atproto.server.createAccount`. The signature itself verifies under the
resolved key either way, so a mismatched header used to pass here and fail
everywhere else.
- df17b17: Mint the authority a credential for its own space.
`getSpaceCredential` sent the authority through the same policy check as anybody
else, so a `member-list` space refused its own authority a credential until a
member row named it. The authority is the only party who can add that row, and
the space it could not open was its own. It now passes the user perimeter for a
space it hosts, which is what the reference implementation does and for the same
reason: no policy may lock out the only account that can change the policy.
- c7f3181: Private git repositories over spaces. A remote of the form
`atproto://<authority>/space/<space-type>/<skey>/<repo>` stores the repo record
and its bundles in a permissioned space instead of the public repo. The owner
pushes and clones with their account session; a space member clones through
the credential chain (delegation token, space credential, DPoP-proofed reads).
`com.atproto.space.putRecord` accepts an optional `swapRecord` compare-and-swap
so concurrent pushes fail cleanly instead of losing bundles.
- Updated dependencies [232a280]
- Updated dependencies [c541c98]
- Updated dependencies [f8e3dff]
- Updated dependencies [bdad8c4]
- Updated dependencies [51d6402]
- Updated dependencies [d8cae21]
- Updated dependencies [d8cae21]
- Updated dependencies [24c9e1e]
- Updated dependencies [035450d]
- Updated dependencies [3920c52]
- Updated dependencies [585139b]
- Updated dependencies [5125583]
- @pdsjs/core@2.3.0
## 2.0.2
### Patch Changes
- 70925d3: Bind space credentials to a key, and forward write notifications to the syncers
registered for a space. Follows atproto PR #5187 as of 2026-08-11.
A space credential is now presented under the `DPoP` scheme with a proof signed
by the key it names, and `Bearer` is refused. `getSpaceCredential` requires a
`dpopJkt` and records it as the credential's `cnf.jkt`; a credential without one
is rejected wherever a credential is read. One credential reads a whole space and
its holder presents it to every repo host in that space, so as a bearer token it
was a shared secret that any one of those hosts could replay against the others.
A proof is accepted once, so a captured request cannot be replayed either.
A credential's `kid` is `#atproto`, the key a PDS-hosted authority signs with.
`#atproto_space` named a verification method no such authority publishes, so a
foreign host could not resolve the key to verify against. A credential arriving
with that `kid` is still verified against a dedicated space key.
`notifyWrite` reaches the services that asked for it. The authority forwards each
notice to every unexpired registration `registerNotify` recorded, which the
lexicon requires and which nothing did before: registrations were only ever read
to announce a deleted space. A write into the authority's own space forwards them
too, from the write path, since no notification arrives over HTTP for that case
and a Durable Object cannot post to itself to be told.
Both halves are what a syncer needs to see a space advance. Without them an
application that registered for notifications was never called, and read a space
that only ever changed when something else made it walk.
- 9ceb6ad: Describe what this server serves, bring permissioned data back in line with the
reference implementation, and answer unregistered XRPC methods the way the
protocol specifies.
`community.lexicon.service.describe` is served: one unauthenticated query
answering with this service's roles and every XRPC method it routes. Optional
endpoints cannot be discovered any other way, since atproto publishes no method
list, and a client that has to call a space method and read the failure cannot
tell an absent feature from a broken one. The answer is derived from the route
table rather than kept beside it, so it cannot drift and it names the space
methods exactly when spaces are enabled.
`createSpace` joins the owner to the space it creates. A member-list space, the
default, refuses a credential to an account that is not a member, so an owner
who never called `addMember` on themselves could not read the space they had
just made.
Proposal 0016 renamed things after implementations had shipped, so several
requests have two correct spellings and this server answers to both:
- `com.atproto.space.getSpace` alongside `com.atproto.simplespace.getSpace`.
- `com.atproto.space.getRepoState` alongside `com.atproto.space.getLatestCommit`.
- On `createSpace` and `updateSpace`, a nested `config` object holding a string
`policy` (or the older `mintPolicy`) and a sibling `managingApp`, alongside
the top-level `policy` and `appAccess` unions. Both fields are optional again:
absent, the host defaults of `member-list` and `#open` apply, which is what
the earlier lexicon documents. `createSpace` accepts an optional `did`, which
must name the caller.
- `getSpace` answers with the union fields and the `#spaceConfig` object at
once, so a client reads whichever it knows.
- On `applyWrites`, a write named by an `action` string alongside one tagged
with a `$type` union member. A create may leave `rkey` empty for the server
to mint one.
An unrouted `/xrpc/<nsid>` returned a `text/plain` 404, which a client cannot
tell apart from a transport failure. It now returns 501 with a
`MethodNotImplemented` error body. atproto publishes no list of supported
methods, so calling an NSID and reading its error is the only way a client can
discover an optional endpoint such as a space method.
Space changes, following atproto PR #5187 as of 2026-08-07:
- `getSpace` moves to `com.atproto.simplespace.getSpace` and answers with
`policy` and `appAccess` at the top level. It also authorizes the caller
first, taking the hosted account's session or a space credential.
- `createSpace`, `updateSpace` and `getSpace` carry both policies as
`com.atproto.simplespace.defs` unions. A variant the host does not implement
is refused with `UnsupportedPolicy` or `UnsupportedAppAccess`.
- `createSpace` drops its `did` field. The space is anchored on the
authenticated user.
- `registerNotify` takes a service identifier rather than a URL and resolves
the delivery endpoint from that service's DID document. It answers with an
`expiresAt` a day out, and a lapsed registration stops receiving
notifications.
- `unregisterNotify` is implemented.
- `getBlob` and `listBlobs` are implemented, over the account's ordinary blob
store. `getBlob` serves only a blob the named space references, where the
reference implementation serves any blob the account holds.
`createSpaceRoutes` now requires `blobs`, `linkBlob` and `unlinkBlobs` in its
context, and refuses to build without them: absent the link callbacks a space
record's blobs look unreferenced, and orphan cleanup deletes them.
A deployment that already created the space tables must drop
`space_credential_recipient`, whose `last_issued_at` column is now `expires_at`.
Registrations are re-established by the syncers that hold them.
- Updated dependencies [261d4f4]
- Updated dependencies [dfce920]
- Updated dependencies [dfcf1cc]
- Updated dependencies [99c414f]
- Updated dependencies [261d4f4]
- Updated dependencies [218f25c]
- Updated dependencies [1b8cd1e]
- Updated dependencies [f2df586]
- Updated dependencies [e761cdb]
- Updated dependencies [d679bf1]
- Updated dependencies [fd33b13]
- Updated dependencies [d88b21d]
- Updated dependencies [8ce9621]
- Updated dependencies [faceaf7]
- Updated dependencies [bc5314a]
- Updated dependencies [f800d5a]
- Updated dependencies [eeb7976]
- Updated dependencies [b368c19]
- Updated dependencies [44bd2b5]
- Updated dependencies [6e47bc6]
- Updated dependencies [5a39d42]
- Updated dependencies [72965f8]
- Updated dependencies [09e110d]
- Updated dependencies [098af68]
- Updated dependencies [7898894]
- Updated dependencies [378e2bb]
- Updated dependencies [4363389]
- Updated dependencies [011e28d]
- Updated dependencies [4169189]
- Updated dependencies [ee386d7]
- Updated dependencies [8cbae75]
- Updated dependencies [4363389]
- Updated dependencies [4a9943d]
- Updated dependencies [9abf6d6]
- Updated dependencies [9524139]
- Updated dependencies [62dc25a]
- Updated dependencies [8d62485]
- Updated dependencies [f1c6639]
- Updated dependencies [224cb48]
- Updated dependencies [5a39d42]
- Updated dependencies [fd54c7d]
- Updated dependencies [67387fe]
- Updated dependencies [1852faf]
- Updated dependencies [9ceb6ad]
- Updated dependencies [e628859]
- @pdsjs/core@2.2.0
## 2.0.1
### Patch Changes
- Updated dependencies [8ac38c1]
- Updated dependencies [f5335a0]
- @pdsjs/core@2.1.0
## 2.0.0
### Patch Changes
- Updated dependencies [62c6521]
- Updated dependencies [13e3049]
- @pdsjs/core@2.0.0
## 1.0.2
### Patch Changes
- Updated dependencies
- @pdsjs/core@1.0.2
## 1.0.1
### Patch Changes
- Updated dependencies [de2ceed]
- @pdsjs/core@1.0.1
## 1.0.0
### Minor Changes
- 3b7895c: Add permissioned data (proposal 0016) and publish under the `@pdsjs` scope.
`@pdsjs/spaces` is new: permissioned repos, space credentials, LtHash set
hashing, deniable commit signatures, and the space HTTP routes. It is opt-in —
a PDS only serves space endpoints when it is passed `spaceRoutes`. Verified
against the reference implementation (`bluesky-social/atproto` PR #5187) in both
directions.
`@pdsjs/core` gains `SpaceStoragePort`, the shared space schema, `space:` OAuth
scope parsing, and consent-screen rendering for space grants.
All packages move from the unpublished `@pds` scope to `@pdsjs`.
### Patch Changes
- Updated dependencies [3b7895c]
- @pdsjs/core@1.0.0