chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
@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.deleteRepotakes 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.
SpaceAdminPortgainsdeleteRecord, over thedeleteaction the space writer already applies. -
93816a1: A space governs reads and writes separately, tracking atproto PR #5187.
createSpaceandupdateSpacetake areadPolicyand awritePolicy,getSpaceanswers with both, andcom.atproto.simplespace.putMembersets a member'sreadandwriterights, whichlistMembersreports. The read policy decides who may hold a space credential. The write policy decides whosenotifyWritethe authority records, so a writer the space does not admit stays out of the writer set and no syncer pulls their repo. The authority passesaccessto a managing app'scheckUserAccess, and omitsclientIdon a write check: that notice comes from a PDS, not an app.The earlier spellings still answer. A single
policysets both sides,addMemberadds a member who reads and writes, andgetSpacesends the two unions, the single one, and the#spaceConfigobject 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
issto itsclient-metadata.json, fetches the key set that document publishes, and checks the signature against the key itskidnames. AnappAccessallow list rests on that check, and until now stated a rule the authority could not enforce.SpaceStoragePortreplacesaddMemberandisMemberwithputMemberandgetMember,listMembersanswers withmembersrather thandids, andSpaceRowcarriesreadPolicy,readManagingApp,writePolicyandwriteManagingApp. Both SQLite adapters add the columns to a database they already hold.@pdsjs/core/oauthexportsfetchClientMetadata,resolveClientJwkandverifyEs256WithJwk, 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, atprotopermissioned-data-alphaat79d6307e1).A space credential is bound to its holder's key with an RFC 9421 HTTP message signature instead of DPoP. The application signs the
getSpaceCredentialrequest'sAuthorizationheader with a fresh P-256 key, named as a did:key inkeyid, and the authority binds the credential to it ascnf.kid. A request presenting the credential uses theAtproto-Spacescheme and anAtproto-Space-Audienceheader 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, thedpopJktfield and theDPoPpresentation scheme are no longer accepted.@pdsjs/spaces/http-signaturesigns 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, anexpafter it and ajti. A repo host servescom.atproto.space.notifyCredentialRevoked: a space's authority names up to 100 credential ids, and the host refuses those credentials withCredentialRevokedfor an hour and ten seconds.The authority sequences the writer set. Each accepted
notifyWritegets aspaceRevthat increases across the space, and the notice forwarded to a syncer carriesrepoRev,spaceRevandprevSpaceRev. A repeated or older repo rev is not forwarded again, and a repo rev more than five minutes ahead is refused withFutureRev.listReposentries carryrepoRevandspaceRev, are ordered byspaceRev, 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_hostas well as to its bare DID, and a body namingrevinstead ofrepoRev.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 throughrunExtensionTicks: Node and Deno every ten seconds, Cloudflare at its alarm, which an extension brings forward with the host's newscheduleWakeup.createSpaceandlistSpacestakespaceType, and still readtype.SpaceStoragePortchanges:putSpaceWritertakes aspaceRev, writer rows carryrepoRevandspaceRev, and the port gainsgetSpaceWriter,getSpaceRevHead, the revocation methods and the notice outbox methods. The schema addsspace_writer.space_rev,space_credential_revocationandspace_notify_outbox, migrated in place. -
04b15d0:
com.atproto.space.getSpaceanswersSpaceNotFoundfor 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
selfis a common record key.It was the only method in the package with no authorization at all. Every other route either declares
author verifies a credential itself. The atproto reference holdslistRepostospaceCredentialAuth, alongsideregisterNotifyandunregisterNotify, 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.getBlobandsync.listBlobsendpoints no longer serve or list a blob that only space records reference;com.atproto.space.getBlobremains the authenticated path to those. -
4a19757: Bind a space credential to the key a DPoP proof demonstrates.
getSpaceCredentialread the key's thumbprint from adpopJktfield 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 noath, 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.configrecord 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.createCachedDidResolverholds 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
getSigningKeygained itsforceRefreshargument. The service token that authorizescom.atproto.server.createAccountalways reads past it: adopting a DID is worth a fresh read, and it happens once per account. -
d8cae21: Take the JWT
algfrom the signing key's curve, on both sides of the wire.An account with a secp256k1 key signed delegation tokens, space credentials and
notifyWriteservice auth under a hardcodedalg: ES256header. 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.
getSpaceCredentialsent the authority through the same policy check as anybody else, so amember-listspace 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.putRecordaccepts an optionalswapRecordcompare-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
DPoPscheme with a proof signed by the key it names, andBeareris refused.getSpaceCredentialrequires adpopJktand records it as the credential'scnf.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
kidis#atproto, the key a PDS-hosted authority signs with.#atproto_spacenamed a verification method no such authority publishes, so a foreign host could not resolve the key to verify against. A credential arriving with thatkidis still verified against a dedicated space key.notifyWritereaches the services that asked for it. The authority forwards each notice to every unexpired registrationregisterNotifyrecorded, 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.describeis 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.createSpacejoins 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 calledaddMemberon 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.getSpacealongsidecom.atproto.simplespace.getSpace.com.atproto.space.getRepoStatealongsidecom.atproto.space.getLatestCommit.- On
createSpaceandupdateSpace, a nestedconfigobject holding a stringpolicy(or the oldermintPolicy) and a siblingmanagingApp, alongside the top-levelpolicyandappAccessunions. Both fields are optional again: absent, the host defaults ofmember-listand#openapply, which is what the earlier lexicon documents.createSpaceaccepts an optionaldid, which must name the caller. getSpaceanswers with the union fields and the#spaceConfigobject at once, so a client reads whichever it knows.- On
applyWrites, a write named by anactionstring alongside one tagged with a$typeunion member. A create may leaverkeyempty for the server to mint one.
An unrouted
/xrpc/<nsid>returned atext/plain404, which a client cannot tell apart from a transport failure. It now returns 501 with aMethodNotImplementederror 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:
getSpacemoves tocom.atproto.simplespace.getSpaceand answers withpolicyandappAccessat the top level. It also authorizes the caller first, taking the hosted account's session or a space credential.createSpace,updateSpaceandgetSpacecarry both policies ascom.atproto.simplespace.defsunions. A variant the host does not implement is refused withUnsupportedPolicyorUnsupportedAppAccess.createSpacedrops itsdidfield. The space is anchored on the authenticated user.registerNotifytakes a service identifier rather than a URL and resolves the delivery endpoint from that service's DID document. It answers with anexpiresAta day out, and a lapsed registration stops receiving notifications.unregisterNotifyis implemented.getBlobandlistBlobsare implemented, over the account's ordinary blob store.getBlobserves only a blob the named space references, where the reference implementation serves any blob the account holds.
createSpaceRoutesnow requiresblobs,linkBlobandunlinkBlobsin 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, whoselast_issued_atcolumn is nowexpires_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
@pdsjsscope.@pdsjs/spacesis 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 passedspaceRoutes. Verified against the reference implementation (bluesky-social/atprotoPR #5187) in both directions.@pdsjs/coregainsSpaceStoragePort, the shared space schema,space:OAuth scope parsing, and consent-screen rendering for space grants.All packages move from the unpublished
@pdsscope to@pdsjs.
Patch Changes
- Updated dependencies [3b7895c]
- @pdsjs/core@1.0.0