~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
489 lines · 21.7 KB Raw History
# @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