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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

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