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