# @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 `#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/` 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