~ 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. 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