chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
# @pdsjs/storage-sqlite
## 2.5.2
### Patch Changes
- Updated dependencies
- @pdsjs/core@2.5.2
## 2.5.1
### Patch Changes
- Updated dependencies [542c4dc]
- @pdsjs/core@2.5.1
## 2.5.0
### Patch Changes
- 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.
- 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.4.3
### Patch Changes
- Updated dependencies [d307f57]
- Updated dependencies [2d0d653]
- @pdsjs/core@2.4.3
## 2.4.2
### Patch Changes
- Updated dependencies [c235e96]
- @pdsjs/core@2.4.2
## 2.4.1
### Patch Changes
- Updated dependencies [fa1558c]
- @pdsjs/core@2.4.1
## 2.4.0
### 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.3.0
### Patch Changes
- 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.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.
- 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.2.0
### Minor Changes
- d679bf1: Offer to combine Apps rows that are one app split across two names.
An app appears twice when the domain it signs in from is not the authority its
records are named by: a rebrand (tangled.sh records, tangled.org sign-in) or a
client hosted on a subdomain (is.currents.\* records, api.currents.is sign-in).
The Apps screen now detects that signature — a session-only row and a
record-only row sharing a name label, with the session's granular scopes, if it
announces any, required to cover the record row's authority — and shows an
inline offer to combine them. Scope coverage alone never pairs rows, so a
third-party client writing another app's lexicons is not mistaken for that app.
Confirming folds the rows into one entry, kept under the name the app is known
by: the apex when the client is its subdomain, the live client host after a
rebrand. The combined row's detail page says where the other half lives
("Stores records as sh.tangled.\*", "Signs in from api.currents.is") and a
Separate action undoes the fold; declining an offer is remembered too. The
choice is stored server-side in a new optional actor-storage document
(`getAccountUiState`/`setAccountUiState`, implemented for SQLite and the
Cloudflare Durable Object) via `POST /account/api/apps/alias`, and the alias
map rides on `GET /account/api/apps`. On storage without the document the
Apps screen simply never offers.
- f800d5a: A Files page in the account UI: every blob the account holds, with the
records that use it.
The page opens with a storage bar — bytes by media kind, one fixed hue per
kind, doubling as a filter — and chips for kind, app, and "not used", all
combinable with a Newest/Largest sort. Filters run server-side with keyset
pagination and live in the URL. Filtered to images, the list becomes a
thumbnail grid with details in a dialog. On phones the chip rows scroll
sideways with sort pinned in view.
Dates mean when the content was made, not when the bytes arrived: each
blob→record link now stores the record's own timestamp (any top-level
ISO-dated field, earliest wins, window-checked so a birthday or a scheduled
future date never counts), falling back to the record's TID time and then to
arrival here. A file only dated by arrival reads "Stored". On a migrated
repo this keeps the original post order, where arrival time would collapse
everything onto the migration date.
Behind it, `ActorStoragePort` grows optional `listBlobDetails` and
`getBlobFacets` methods, implemented for better-sqlite3 and Durable Object
SQLite from one shared builder (`@pdsjs/core/blob-sql`), and
`linkBlobToRecord` takes the record time. The `record_blobs` table self-heals:
`relinkBlobRecords()` rebuilds links and times from record content on CAR
import, on the daily blob cleanup, and on demand via the page's "Recheck
usage" action (POST /account/api/blobs/relink) — so an imported repo's files
stop reading as unused. The overview's blob count uses the exact aggregate
instead of the capped page-through when the storage supports it.
- 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 as `BACKUPS`. Each snapshot holds the full
signed repo CAR (the same bytes `com.atproto.sync.getRepo` serves) and a
manifest, written last so an interrupted snapshot never counts; blobs are
stored content-addressed at `blobs/<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
`metadata` table as one JSON document, read through new optional
`getBackupState`/`setBackupState` methods on `ActorStoragePort`.
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.enc` in 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 `.car` download that works even with no
target configured.
### Patch Changes
- faceaf7: Expire stale password/app-password logins instead of listing them forever.
Each legacy `createSession` writes a session record, but the records carried no
expiry, so a client that re-logs in rather than refreshing (many app-password
feed generators and bots do) piled up a permanent record every time — inflating
"N logins have access right now" with logins whose refresh tokens had long since
died. Refresh tokens now last 90 days, matching the reference PDS (they were
24h). Sessions store the refresh token's own expiry in a new SQLite `expires_at`
column, and the read path prunes any login that has outlived it as it lists
them. A row from before
this field is aged by the earlier 24h lifetime, so existing stale logins retire
themselves the next time the page loads (safely: an expired token can no longer
refresh, so removing its record signs no one out).
A fresh login also retires the same client's prior session — keyed by what
opened it (the account password, or an app password's name) and the device it
ran on — so a client that signs in every run instead of refreshing keeps one
live session per credential per device rather than one per run, while two
genuinely different devices stay separate. Without this, the 90-day lifetime
would let re-login clients accumulate far more than the old 24h did.
"Where you're signed in" and the account overview count now reflect only live
logins, and the list is capped to the five most recent with a "Show all" toggle.
- 9abf6d6: Keep a spent refresh token redeemable for two hours, so a client that refreshes
twice is not signed out.
Rotation used to retire the old token the instant it issued the replacement.
Any client with several requests in flight refreshes more than once — and a
refresh whose response is lost leaves the client holding a token the server has
already retired — so a single race ended the session for good, with no way back
short of signing in again. A rotation now names its successor and keeps the
spent record for a grace window, its expiry shortened to match; redeeming that
token again is answered with the same replacement, so both callers converge on
one live token. The grace only ever brings an expiry forward, so a token near
the end of its 90 days gains nothing from being rotated. Past the window it is
refused with `ExpiredToken`. This matches the reference PDS, which keeps the
same two-hour window and the same sticky successor.
Spent records are held for the grace window but are not logins in their own
right: they are hidden from "Where you're signed in" and the Apps page, they do
not count toward the overview total, and they are pruned once the window
closes. Revoking a session follows the chain backwards and removes them too, so
a replayed token cannot put a revoked session back.
Two refreshes that reach the server together used to be able to name different
successors, forking one login into two records — both usable, but "sign out"
on either left the other running on the same device. Naming a successor is now
conditional on no other rotation having named one, through a new optional
`spendSession` on the storage port; the rotation that loses adopts the
successor already recorded. Every store does this in a single statement.
OAuth refresh tokens are left alone: the reference authorization server has no
grace window there, treating a replayed token as a theft signal rather than a
race, so pds.js keeps rotating them the way it did.
Two related fixes:
- Revoking an app password now ends the logins it opened. Deleting the
credential stopped it opening new sessions but left existing ones refreshing
for their full 90 days, contradicting the page's own wording. Matches the
reference PDS, which deletes the refresh tokens tied to that app password.
- Only a fresh login collapses a client's earlier session. Rotation ran the
same dedupe, so a client whose User-Agent changed between signing in and
refreshing — an app updating itself — could sign out a different device that
reported the new string.
- 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.1.0
### Patch Changes
- Updated dependencies [8ac38c1]
- Updated dependencies [f5335a0]
- @pdsjs/core@2.1.0
## 2.0.0
### Minor Changes
- 62c6521: Add app passwords: `createAppPassword`, `listAppPasswords`, and
`revokeAppPassword`, with `createSession` accepting them alongside the account
password.
Sessions opened with an app password carry a restricted scope and cannot manage
app passwords, so a leaked one cannot mint its own replacements. The restriction
survives `refreshSession` — a refresh reissues at the same access level rather
than widening to full access.
Only the hash is stored, derived with PBKDF2 rather than the reference
implementation's scrypt, which exists in neither WebCrypto nor Workers.
`SharedStoragePort` gains four methods; both the SQLite and Cloudflare adapters
implement them against a shared test suite.
### 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
### Patch Changes
- Updated dependencies [3b7895c]
- @pdsjs/core@1.0.0