chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
@pdsjs/node
2.5.2
Patch Changes
- Updated dependencies
- @pdsjs/core@2.5.2
- @pdsjs/drive@0.1.7
- @pdsjs/npm@0.1.3
- @pdsjs/oci@0.1.3
- @pdsjs/sites@2.1.9
- @pdsjs/spaces@2.0.10
- @pdsjs/storage-sqlite@2.5.2
- @pdsjs/git@0.1.5
- @pdsjs/lexicon-resolver@2.5.2
- @pdsjs/blobs-fs@2.5.2
2.5.1
Patch Changes
- Updated dependencies [542c4dc]
- @pdsjs/core@2.5.1
- @pdsjs/drive@0.1.6
- @pdsjs/npm@0.1.2
- @pdsjs/oci@0.1.2
- @pdsjs/sites@2.1.8
- @pdsjs/spaces@2.0.9
- @pdsjs/storage-sqlite@2.5.1
- @pdsjs/git@0.1.4
- @pdsjs/lexicon-resolver@2.5.1
- @pdsjs/blobs-fs@2.5.1
2.5.0
Minor Changes
- 04b15d0:
experimental.git.externalhands the git reads to a service of the operator's choosing: the PDS keeps the data and the identity, and stops serving the git routes.experimental.git.guardOnlyis the finer knife: drop the routes but keep the branch-protection write guard, which is the one piece that cannot move, because enforcement only means something at the write.externalimpliesguardOnly.
Patch Changes
-
d66fcff: The default blob upload limit is 50 MiB. An upload streams to storage, so the limit bounds cost rather than memory, and 50 MiB stays under the 100 MB request body a Cloudflare Worker accepts on every plan.
PDS_BLOB_UPLOAD_LIMITstill overrides it.git-remote-atproto uploads a bundle in 8 MiB chunks. A server that refuses a chunk as too large gets the push again at half the size, down to 64 KiB, so a reference PDS at 5 MB still takes it. Existing bundles keep their parts until a repack rewrites them.
-
f79dd82: Feature packages mount on the server as extensions.
PersonalDataServertakes anextensionsoption: factories core calls with anExtensionHost, which carries the storage ports, the space ports when spaces are on, and late-bindinggetDid,xrpc,createCommit,getSigner,reapOrphanedBlobs,accountApiand the server's composedwriteGuard. An extension contributes routes, a handler for paths under prefixes it declares, a handler for hostnames other than the server's, a write guard, a commit tap, account feature flags and rail counts, and declares the record collections it owns: the storage on its host refuses a record call naming any other, a commit refuses an op naming any other, and a self-addressedxrpccall is anonymous whatever credentials the request carried. Core merges the pieces: a route core already serves is refused, a prefix core serves under or that overlaps another extension's is refused, route handlers are called unbound, write guards compose over the union of their collections and apply to space writes as well as public ones, and every commit tap runs in its own try/catch. The types areExtensionFactoryandPdsExtensionin@pdsjs/core/ports.Every feature package is on the seam, each from its
/extensionexport:createGitExtension({ http, browse }),createDriveExtension({ upload }),createSitesExtension({ domain, apexName, queries }),createSpacesExtension(),createOciExtension()andcreateNpmExtension(). Each serves its own/account/apiroutes through the host, so those routes, their helpers and their tests move out of core's account handler into the packages. The git and drive packages build the space admin methods they use over the host's storage from@pdsjs/spaces, now an optional peer of each.@pdsjs/denomounts the git and sites extensions whenever those packages are installed, the same as@pdsjs/node, and the sites extension mounts with or without a domain so the installs listing is always served.For a deployment nothing changes:
createServeron node and deno takes the samespacesandexperimentaloptions, the Worker reads the samePDS_ENABLE_SPACESandPDS_EXPERIMENTAL_*vars, and every endpoint and account API path answers as before. What changes is the constructor ofPersonalDataServeritself, for a platform that embeds core directly: the feature-wiring optionsspaceRoutes,spaceBrowser,spaceAdmin,siteHandler,siteBrowser,siteInstaller,queryRoutes,gitBrowser,driveBrowser,driveWriter,gitHttpEnabled,ociEnabledandnpmEnabledare gone, replaced byextensions;spaceStoragestays, and core builds the space browser over it. Those options were the seam between core and its own adapters rather than a supported embedding surface, which is why this ships as a minor. -
f777a2c: The protected-branch check reads the commits out of the chain instead of downloading it. The guard answers one question, whether the old tip is an ancestor of the new one, and every object in the repository was materialized to answer it. On pds.js's own repository that costs 199 MB, which no Cloudflare isolate has.
The lazy object store moves to
store.js, where the write guard reads it as the repository browser does: the bundle indexes, then ranged reads for the commits alone. Both platform adapters give the guard agetBlobRangewherever the blob store reads ranges. The same check costs 43 MB and 33 ms, against 199 MB and 428 ms. A chain with no index, or a store with no ranges, reads whole as before. -
cc4577b:
POST /account/api/git/mergeputs a pull request's work on a repository's canonical branch, in either merge or squash mode.Merging is the one act in this model that needs authority over the repository, so it runs on that account's own server under the session cookie the account page already answers to. An atproto
repo:scope names a collection and its actions and cannot name one record, so asking a browser page forrepo:dev.pdsjs.git.repowould hand a token in IndexedDB create, update and delete over every repository the account holds. The cookie is the narrower grant, and the repository browser reaches the endpoint because it is served from the account's own site.createGitMergeris the seam. The pack format is the git package's and the authority over the record is the PDS's, so the platform builds the merger from storage it owns and injects it asgitMerger, the way it already builds the write guard. Core carries no dependency on the git package.A merge advances the canonical branch to a commit whose first parent is its current tip, so it is a fast-forward by construction and cannot force-push or delete a protected branch. The write of the repository record consults the server's composed write guard, the git rules among them, so the same rule a bearer-token write is held to holds a merge, which is the backstop should the merge ever produce anything but a fast-forward. The new bundle carries a pack index, so the chain stays readable through ranged blob reads rather than downloading whole.
The pull request page gains Merge and Squash for the account that owns the repository. A branch main has moved under is told to rebase rather than offered a button that cannot work.
-
aeb61f9: Repositories in a permissioned space browse with ranged reads. The space reader served whole blobs only, so the account page downloaded a private repository's whole bundle chain to list a directory, while a public one read single objects out of the chain.
SpaceReadertakes an optionalgetBlobRange, and both platform adapters supply it wherever the blob store reads ranges. A space repository whose bundles all carry an object index now reads lazily, as a public one does; a reader without ranges still reads the chain whole. -
411b47b: The git extension serves its reads and writes as XRPC methods under
dev.pdsjs.git.*, each described by a lexicon:listRepos,getRepo,getTree,listFiles,getFile,getRaw,listCommits,getCommit,getLastCommit,listChecks,getAuthors,createRepo,updateSettingsandrequestCheck. A read of the bundle chain keeps the last four repositories opened and downloads a chain without object indexes whole only up toeagerLimit, answeringRepositoryTooLargepast it. A read of the public repo is anonymous and answers under the record cid as its ETag; a read of a space repository and every write take the owner's account session, or for a write a bearer token with therepo:scopes of the collections it writes.community.lexicon.service.describelists the methods a server serves. The/account/api/gitroutes and/git/<account>/<repo>/last-commitare gone; the methods replace them.An extension host gains
readAccountSessionandreadJsonBounded, so a route that answers in its own shape can take the account session alongside a bearer token.The account application's repositories pages call these methods, with the raw file view served through
getRaw. -
e2f072c: An npm registry served from the account's own repo, as the new
@pdsjs/npmoptional peer.experimental.npm = trueon node, orPDS_EXPERIMENTAL_NPM = "true"on Cloudflare, serves the registry protocol under/npm/: stock npm publishes with the account's credentials and anyone installs anonymously. Dist-tags, version manifests and tarballs land in adev.pdsjs.npm.packagerecord plus blob storage, so packages ride along in CAR backups and migrate with the account.npm login,npm publish,npm install,npm viewandnpm dist-tagall work against it. -
fe20e9f: An OCI container registry served from the account's own repo, as the new
@pdsjs/ocioptional peer.experimental.oci = trueon node, orPDS_EXPERIMENTAL_OCI = "true"on Cloudflare, serves the distribution API under/v2/: stock docker pushes with the account's credentials and anyone pulls anonymously. Tags, manifests and layers land in adev.pdsjs.oci.reporecord plus blob storage, so images ride along in CAR backups and migrate with the account. A layer's OCI digest and its blob CID hold the same SHA-256 bytes, so layers store as single blobs with no mapping table, streamed through the blob writer without ever being held whole. -
9149663: A migrated account announces its identity until a relay hears it. An AppView takes an account's signing key from its own identity store, filled by the
#identityevent on the firehose, and theforceRefreshits token verifier asks for is not propagated. So the announcementactivateAccountmakes at the end of a migration reaches nobody when no relay has subscribed yet, and nothing retries it: every service token the account mints is then rejected against the key it used to have, and feeds, notifications and profiles fail withjwt signature does not match jwt issuer.The hourly maintenance tick now calls
maybeAnnounceIdentity. It asks the configured relay whether that relay reads this server, and announces again while the answer is no, so the event lands once a subscription exists. It stops as soon as the relay reports it is reading.activateAccountalso asks for the crawl before it announces, rather than after. -
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. -
Updated dependencies [a67a568]
-
Updated dependencies [d66fcff]
-
Updated dependencies [05a11b4]
-
Updated dependencies [f79dd82]
-
Updated dependencies [c68858b]
-
Updated dependencies [04b15d0]
-
Updated dependencies [288063a]
-
Updated dependencies [c3c4caa]
-
Updated dependencies [bcb5758]
-
Updated dependencies [e0b38cd]
-
Updated dependencies [4fb8f23]
-
Updated dependencies [1cf113d]
-
Updated dependencies [04b15d0]
-
Updated dependencies [c4af198]
-
Updated dependencies [f777a2c]
-
Updated dependencies [a981627]
-
Updated dependencies [3febe1c]
-
Updated dependencies [ac5c8eb]
-
Updated dependencies [00bd461]
-
Updated dependencies [83083a5]
-
Updated dependencies [2c68bdc]
-
Updated dependencies [fe7ffe0]
-
Updated dependencies [211c7c8]
-
Updated dependencies [6cf84d5]
-
Updated dependencies [35f794f]
-
Updated dependencies [522071f]
-
Updated dependencies [32db640]
-
Updated dependencies [04b15d0]
-
Updated dependencies [cc4577b]
-
Updated dependencies [cc4577b]
-
Updated dependencies [f5aae7c]
-
Updated dependencies [cc4577b]
-
Updated dependencies [fe3c9ac]
-
Updated dependencies [b99acf8]
-
Updated dependencies [c1ec3d6]
-
Updated dependencies [04b15d0]
-
Updated dependencies [7c06024]
-
Updated dependencies [6935a55]
-
Updated dependencies [15c69af]
-
Updated dependencies [a56be28]
-
Updated dependencies [673ec05]
-
Updated dependencies [74819af]
-
Updated dependencies [4dfdbfa]
-
Updated dependencies [04b15d0]
-
Updated dependencies [aeb61f9]
-
Updated dependencies [04b15d0]
-
Updated dependencies [04b15d0]
-
Updated dependencies [f27dcf7]
-
Updated dependencies [c9e3550]
-
Updated dependencies [c01859b]
-
Updated dependencies [41df38b]
-
Updated dependencies [411b47b]
-
Updated dependencies [ca47f96]
-
Updated dependencies [861663f]
-
Updated dependencies [617df4a]
-
Updated dependencies [154e7d7]
-
Updated dependencies [bda4794]
-
Updated dependencies [10c91f6]
-
Updated dependencies [0e1bf66]
-
Updated dependencies [77eb9db]
-
Updated dependencies [76f5187]
-
Updated dependencies [b51250e]
-
Updated dependencies [799286b]
-
Updated dependencies [4adc49e]
-
Updated dependencies [e83838c]
-
Updated dependencies [9149663]
-
Updated dependencies [16ea1fe]
-
Updated dependencies [fd401a4]
-
Updated dependencies [abae790]
-
Updated dependencies [93816a1]
-
Updated dependencies [3d475f8]
-
Updated dependencies [04b15d0]
-
Updated dependencies [5bb14a2]
-
Updated dependencies [078c2df]
-
Updated dependencies [ec85961]
-
Updated dependencies [d5f111c]
- @pdsjs/sites@2.1.7
- @pdsjs/core@2.5.0
- @pdsjs/git@0.1.3
- @pdsjs/spaces@2.0.8
- @pdsjs/lexicon-resolver@2.5.0
- @pdsjs/npm@0.1.1
- @pdsjs/oci@0.1.1
- @pdsjs/storage-sqlite@2.5.0
- @pdsjs/drive@0.1.5
- @pdsjs/blobs-fs@2.5.0
2.4.3
Patch Changes
- Updated dependencies [d307f57]
- Updated dependencies [2d0d653]
- @pdsjs/core@2.4.3
- @pdsjs/drive@0.1.4
- @pdsjs/sites@2.1.6
- @pdsjs/spaces@2.0.7
- @pdsjs/storage-sqlite@2.4.3
- @pdsjs/lexicon-resolver@2.4.3
- @pdsjs/blobs-fs@2.4.3
2.4.2
Patch Changes
- Updated dependencies [c235e96]
- @pdsjs/core@2.4.2
- @pdsjs/drive@0.1.3
- @pdsjs/sites@2.1.5
- @pdsjs/spaces@2.0.6
- @pdsjs/storage-sqlite@2.4.2
- @pdsjs/lexicon-resolver@2.4.2
- @pdsjs/blobs-fs@2.4.2
2.4.1
Patch Changes
- Updated dependencies [fa1558c]
- @pdsjs/core@2.4.1
- @pdsjs/drive@0.1.2
- @pdsjs/sites@2.1.4
- @pdsjs/spaces@2.0.5
- @pdsjs/storage-sqlite@2.4.1
- @pdsjs/lexicon-resolver@2.4.1
- @pdsjs/blobs-fs@2.4.1
2.4.0
Minor Changes
-
54bec84: A drive: files and folders kept in the account's own repo, under Labs on the account page. Files upload through
/account/api/drive/upload, split across blobs when they exceed the server's upload limit, and download whole or by byte range. Folders are records of their own, so an empty one stays and a rename leaves every file's key alone. A drive can live in the public repo or in a permissioned space, where its blobs are private.Off unless composed in:
experimental.driveon the Node adapter (on when@pdsjs/driveis installed),PDS_EXPERIMENTAL_DRIVEon Cloudflare.
Patch Changes
- 6b79149: Video-ready blob hosting. Blob uploads and downloads stream:
uploadBlobhashes and stages bytes chunk by chunk instead of buffering the body, andgetBlobanswers byte ranges from a streamed read, so a 300 MB video fits through a Durable Object and seeks in a browser. The blob port gains optionalcreateWriterandgetStream; the fs, R2, S3 and Deno stores implement both, and each platform injects its native SHA-256. Drive stores a file as one blob of its own type when the store streams, takes large files across several requests through upload sessions, and offers a Link dialog with the blob ref any record can embed. The Worker serves public blobs through the edge cache, and a site's/.blobs/route serves ranges and now applies the same space-privacy filter asgetBlob. A newvideos.mjsexample app is a gallery over the drive's videos. The node server accepts ablobUploadLimitoption (PDS_BLOB_UPLOAD_LIMITin the example), which Bluesky video posting needs. - 9a6f2e8: The account rail heading over Sites and Repositories reads "Labs".
- 267d341: The repository page draws skeleton placeholders while it loads. The file listing, a file's contents, and the commit history each get the shape of the content they are about to show, instead of a blank panel.
- Updated dependencies [ee6d696]
- Updated dependencies [cb64c59]
- Updated dependencies [54bec84]
- Updated dependencies [922fb50]
- Updated dependencies [68ad18f]
- Updated dependencies [f716d9d]
- Updated dependencies [68ad18f]
- Updated dependencies [68ad18f]
- Updated dependencies [ae02630]
- Updated dependencies [68ad18f]
- Updated dependencies [4c334b3]
- Updated dependencies [86d8eb4]
- Updated dependencies [6b79149]
- Updated dependencies [8616855]
- Updated dependencies [af8b1fd]
- Updated dependencies [715c475]
- @pdsjs/core@2.4.0
- @pdsjs/git@0.1.2
- @pdsjs/blobs-fs@2.4.0
- @pdsjs/drive@0.1.1
- @pdsjs/sites@2.1.3
- @pdsjs/spaces@2.0.4
- @pdsjs/storage-sqlite@2.4.0
- @pdsjs/lexicon-resolver@2.4.0
2.3.0
Patch Changes
-
232a280: The sign-in page names the server's one account instead of asking for an identifier. The handle (or the DID, when no handle is set) renders as text above a single password field and posts through a hidden field. The editable identifier input remains only on a server with no identity yet. Both the account app and the server-rendered fallback follow the same shape.
-
c541c98: The account password can be changed from the security page. The configured password (PDS_PASSWORD) seeds a fresh server; a change stores a hash in actor storage and wins from then on. Setting a different configured password afterwards is the operator's reset: it wins over the stored hash, which is the recovery path when the password is lost. The change endpoint takes the current password even inside a signed-in session. Open sessions and app passwords stay valid through a change.
-
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. -
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. -
035450d: The account page reports the release the server runs and whether a newer one is published.
/account/api/versionasks the npm registry server-side, cached in memory for six hours, and the settings page and home screen surface the answer. A deployment that names an update page (updateUrlin core,PDS_UPDATE_URLon Cloudflare) gets a direct "Update this server" link; other deployments see the release and its notes. -
Updated dependencies [232a280]
-
Updated dependencies [c541c98]
-
Updated dependencies [f8e3dff]
-
Updated dependencies [9e04a41]
-
Updated dependencies [4a19757]
-
Updated dependencies [bdad8c4]
-
Updated dependencies [51d6402]
-
Updated dependencies [d8cae21]
-
Updated dependencies [d8cae21]
-
Updated dependencies [df17b17]
-
Updated dependencies [61f1ce8]
-
Updated dependencies [c7f3181]
-
Updated dependencies [24c9e1e]
-
Updated dependencies [035450d]
-
Updated dependencies [3920c52]
-
Updated dependencies [585139b]
-
Updated dependencies [89b33a7]
-
Updated dependencies [5125583]
- @pdsjs/core@2.3.0
- @pdsjs/storage-sqlite@2.3.0
- @pdsjs/git@0.1.1
- @pdsjs/spaces@2.0.3
- @pdsjs/lexicon-resolver@2.3.0
- @pdsjs/sites@2.1.2
- @pdsjs/blobs-fs@2.3.0
2.2.0
Minor Changes
-
b368c19: New package: host git repositories natively in an atproto account. Ships a
dev.pdsjs.git.repolexicon and agit-remote-atprotoremote helper that stores repository history as chunked git bundles in PDS blob storage, with compare-and-swap ref updates through putRecord. Clone, fetch, push, force push, tags, deletions, and automatic bundle-chain repacking all work against any stock PDS with no server changes.The package also ships a read-only git smart HTTP handler so stock git can clone these repositories without the helper. It advertises refs from the record and serves fetches by merging the bundle chain's packfiles without inflating objects, so it runs fine on Cloudflare Workers. Opt in with
experimental: { git: { http: true } }in the Node adapter orPDS_EXPERIMENTAL_GIT_HTTP = "true"in the Worker's vars; clone URLs look likehttps://<pds-host>/git/<handle-or-did>/<repo>.Server adapters gain an
experimentaloption group, exempt from semver: anything in it may change shape in any release. It holds one entry per feature experiment,git: { http, browse }andsites: { domain, apexName, queries }(the Deno adapter takes the samesitesgroup), replacing the flatsiteDomain,siteApexName, andenableQueriesprops. Experimental env vars carry the same signal as a prefix:PDS_EXPERIMENTAL_GIT_HTTP,PDS_EXPERIMENTAL_GIT_BROWSE,PDS_EXPERIMENTAL_SITE_DOMAIN,PDS_EXPERIMENTAL_SITE_APEX_NAME, andPDS_EXPERIMENTAL_QUERIES. The git package itself versions independently at 0.x until it stabilizes.The account page grows a Repositories section under the Beta heading, next to Sites: each hosted repository with its branches, tags, size, and last push, linking to a repository page with copyable clone commands, a ref picker, and a browsable file tree with text previews. Browsing is served by a pure-JS pack object reader (no git binary server-side) exposed through
/account/api/git/repos,/git/tree, and/git/fileendpoints, with clone URLs including the plain-git HTTP one when the platform serves it. -
72965f8: Installable apps: a repo-hosted site, published once, installed anywhere.
A developer publishes a
dev.pdsjs.app.manifestrecord naming their app and pinning its site record by CID, with an icon, the query definitions the app reads, declared settings ({key, label, type, default, options}, data and never code), a suggested subdomain, and anexternalflag for apps that talk beyond the user's own PDS. The pinned record key is an artifact address with no naming meaning.Installing copies the app into the user's own repo: every blob is fetched from the developer's PDS and hashed locally against the CID the record names, the site record copies verbatim so its CID stays the one the manifest pinned, query defs and a
dev.pdsjs.app.installrecord land with it in one commit. The install record snapshots the name, icon, settings schema, and chosen values, so the account page needs no cross-server read and the app survives its developer's repo disappearing. A drifted pin refuses to install; a manifest requesting scopes refuses until grants exist.Two ways in:
pdsjs-site install <at-uri>over stock XRPC, or the account page's install sheet, which previews the manifest before anything happens. Installed apps list with their icon and name, carry a generated settings form, move to a new subdomain without touching their content, and show an update action when the developer publishes a new manifest version; updates keep the user's settings and subdomain. Apps discover their host from their own origin at load, so one published file serves every installer's data. -
098af68: Materialized queries: a
dev.pdsjs.query.defrecord names a live view over one collection of the repo, with equality filters and one sort field, andGET /xrpc/dev.pdsjs.query.runreturns the whole view as JSON in a single request. Reading a thousand-record collection throughlistRecordscosts a serial cursor walk; through a query it is one round trip. The engine keeps a decoded copy of each queried collection in memory, replayed from storage on first use and maintained from every commit, so results reflect a write immediately. Off unlessPDS_EXPERIMENTAL_QUERIES(theexperimental.sites.queriesoption on Node and Deno) is set.Behind it,
@pdsjs/coregains the app platform's live-data seams, both inert until a platform package fills them: an optionalonCommitcallback called after each successful commit (an error there is logged and never fails the write), and aqueryRoutestable composed likespaceRoutes.Site hostnames also now answer
/.blobs/<cid>with that blob from the repo, immutable and edge-cacheable, sandboxed by CSP, media mimes only. A client-rendered site can query its data live and show repo media without a manifest entry per file. -
224cb48: Scheduled backups, with a Backups page in the account UI.
A backup is a snapshot in a new
BackupTargetPort— a directory (createFsBackupTarget), an S3-compatible bucket (createS3BackupTarget), or on Cloudflare an R2 bucket bound asBACKUPS. Each snapshot holds the full signed repo CAR (the same bytescom.atproto.sync.getReposerves) and a manifest, written last so an interrupted snapshot never counts; blobs are stored content-addressed atblobs/<cid>and shared across snapshots, so a run only uploads what the target doesn't already hold. Retention keeps the newest N snapshots and reaps blobs no kept manifest references.Scheduling rides the maintenance tick each runtime already had: the hourly interval on Node and Deno, and the Durable Object alarm on Cloudflare, which now fires hourly to match. Settings and run history live in the existing
metadatatable as one JSON document, read through new optionalgetBackupState/setBackupStatemethods onActorStoragePort.The signing key stays out of snapshots unless opted in: entering a passphrase on the Backups page seals a copy under PBKDF2 → AES-256-GCM (
key.encin each snapshot), and the page warns when the key has rotated since it was sealed. The page also offers one-click manual runs, run history, schedule and retention settings, and a direct.cardownload that works even with no target configured. -
5a39d42: Sign with secp256k1 account keys, the curve the reference PDS gives every account.
Signing was P-256 only. That is spec-legal, but services that verify service-auth JWTs on secp256k1 alone reject it — a Go feed generator answers a bare
400, soapp.bsky.feed.sendInteractionssilently never lands and a feed keeps serving posts the reader has already seen. WebCrypto cannot sign secp256k1, so the implementation is injected as aSecp256k1SignerPort, mirroring the verifier port that already existed on the verification side: core stays free of the dependency and the platform packages supply@noble/curves.A stored key now describes its own curve, so an existing bare 32-byte key still reads as P-256 and every P-256 deployment is unaffected. Where a signer is injected, new accounts are minted on secp256k1, and the JWT
alg(ES256K), JWKS entry,did:keymulticodec, published verification method, and DPoP self-verification all follow the key's curve rather than assuming one.Rotating an existing account's key is worth planning: services that have cached the DID document keep verifying against the old key until they re-resolve it, so expect proxied calls to fail until an
#identityevent reaches them. -
e628859: Static sites served from the repo, in a new
@pdsjs/sitespackage.A site's files are ordinary blobs and a site is one
dev.pdsjs.site.deployrecord holding the path→blob manifest, so a deploy is a stock atproto write: it rides the firehose, survives migration, and lands in backups with everything else.pdsjs-site deploy ./dist --site homeuploads only the files whose CID changed and writes the record.Serving is by hostname. The record key is a DNS label, so
blogserves atblog.<site domain>and the site named byPDS_EXPERIMENTAL_SITE_APEX_NAME(defaulthome) serves at the apex. Responses carry an ETag derived from the file's CID, so conditional requests answer 304, and a missing path falls back toindex.htmlfor single-page apps.Caching is per file:
pdsjs-site deploy --cache-control "[glob:]value"sets the Cache-Control each matched path serves with, and content addressing makesimmutablesafe for fingerprinted assets. On Cloudflare, site responses are also held in the edge cache for theirmax-age, so repeat requests are answered at the visitor's edge location instead of crossing to the Durable Object's region. The PDS's own hostname never enters that cache.The feature is off until a site domain is configured:
PDS_EXPERIMENTAL_SITE_DOMAINon the Worker,experimental: { sites: { domain } }on the Node and DenocreateServer. Without it no hostname is inspected and dispatch is untouched.@pdsjs/coregains two optional ports,SiteHandlerPortandSiteBrowserPort, and never imports the sites package itself; a platform package builds both and passes them in. On Node and Deno@pdsjs/sitesis an optional peer dependency, so a deployment that does not want sites does not install it.Site content is only ever served on the site domain, never on the PDS's own hostname. That keeps repo-controlled HTML on a separate origin from the account session cookie.
Every servable site hostname also answers
/.blobs/<cid>with that blob from the repo, immutable and sandboxed, so a client-rendered site can show repo media without a manifest entry per file.wwwwith no site namedwwwdeployed redirects to the apex.The account interface grows a Sites page listing what is deployed, with a delete that unlinks the record's blobs and reaps the orphans. It appears only when the server composes the feature in, and it warns when the PDS's own hostname shadows the apex site.
Patch Changes
- Updated dependencies [261d4f4]
- Updated dependencies [dfce920]
- Updated dependencies [a4cae1f]
- 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 [91e52aa]
- Updated dependencies [1852faf]
- Updated dependencies [70925d3]
- Updated dependencies [9ceb6ad]
- Updated dependencies [e628859]
- @pdsjs/core@2.2.0
- @pdsjs/sites@2.1.1
- @pdsjs/storage-sqlite@2.2.0
- @pdsjs/lexicon-resolver@2.2.0
- @pdsjs/blobs-fs@2.2.0
- @pdsjs/spaces@2.0.2
2.1.0
Patch Changes
- Updated dependencies [8ac38c1]
- Updated dependencies [f5335a0]
- @pdsjs/core@2.1.0
- @pdsjs/spaces@2.0.1
- @pdsjs/storage-sqlite@2.1.0
- @pdsjs/lexicon-resolver@2.1.0
- @pdsjs/blobs-fs@2.1.0
2.0.0
Patch Changes
- Updated dependencies [62c6521]
- Updated dependencies [13e3049]
- @pdsjs/core@2.0.0
- @pdsjs/storage-sqlite@2.0.0
- @pdsjs/spaces@2.0.0
- @pdsjs/blobs-fs@2.0.0
- @pdsjs/lexicon-resolver@2.0.0
1.0.2
Patch Changes
- Updated dependencies
- @pdsjs/core@1.0.2
- @pdsjs/spaces@1.0.2
- @pdsjs/storage-sqlite@1.0.2
- @pdsjs/blobs-fs@1.0.2
- @pdsjs/lexicon-resolver@1.0.2
1.0.1
Patch Changes
- Updated dependencies [de2ceed]
- @pdsjs/core@1.0.1
- @pdsjs/spaces@1.0.1
- @pdsjs/storage-sqlite@1.0.1
- @pdsjs/blobs-fs@1.0.1
- @pdsjs/lexicon-resolver@1.0.1
1.0.0
Patch Changes
- Updated dependencies [3b7895c]
- @pdsjs/core@1.0.0
- @pdsjs/spaces@1.0.0
- @pdsjs/storage-sqlite@1.0.0
- @pdsjs/blobs-fs@1.0.0
- @pdsjs/lexicon-resolver@1.0.0