chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
# Reference Parity Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Implement firehose completeness (#identity/#account/#sync, since/prevData), rpc: OAuth scopes, DPoP nonces, and account-migration endpoints per `docs/superpowers/specs/2026-07-30-reference-parity-design.md`.
**Architecture:** All endpoint logic lands in `packages/core/src/pds.js` (single route table); scope logic in `scope.js`; DPoP/nonce in `oauth.js`; PLC helpers in a new `plc.js` shared with `scripts/setup.js`. Storage ports gain `getLatestSeq` and `get/setAccountStatus`, implemented in sqlite + Cloudflare DO `metadata`/`seq_events` tables, the readonly shim, and test mocks.
**Tech Stack:** plain ESM JS + JSDoc types, vitest, TDD per task, one commit per task.
## Global Constraints
- Reference behavior (`~/code/atproto`) is authoritative; adaptations documented in the spec.
- Every task: failing test first, minimal implementation, full `pnpm test` green before commit.
- Wire shapes from `com.atproto.sync.subscribeRepos` lexicon (see spec §1b).
- Lxm/NSID comparisons case-insensitive.
---
### Task 1: `#commit` since + prevData
**Files:** Modify `packages/core/src/pds.js` (`createCommit`, `formatFirehoseEvent`); Test `test/e2e.test.js` (subscribeRepos assertions).
**Interfaces:** Produces stored event fields `since: string|null`, `prevData: {$link}|undefined`; frame fields `since`, `prevData` (CID class). Consumes `parseCommitBlock(bytes)` helper (new, in `repo.js`, DAG-CBOR decode of a commit block returning `{did, rev, data: string|null, prev}`), reused by Task 6 (checkAccountStatus) and Task 2 (#sync CAR).
- [x] e2e test: commit frames carry `since` (prev rev) and `prevData` (prev MST root) — verify second commit's `since` === first commit's `rev`.
- [x] Verify RED. Implement: `parseCommitBlock` in repo.js; in `createCommit` read prev commit block → store `since`/`prevData` in event payload; emit in `formatFirehoseEvent`. Verify GREEN. Commit.
### Task 2: typed events + getLatestSeq
**Files:** Modify `packages/core/src/ports.js`, `packages/storage-sqlite/src/index.js`, `packages/cloudflare/src/index.js`, `packages/readonly/src/index.js`, `packages/core/src/pds.js`, test mocks (`test/loader.test.js` mock unaffected — no events), `test/helpers` if needed; Test `test/e2e.test.js`.
**Interfaces:** Port gains `getLatestSeq(): Promise<number>` (0 when empty). New core method `emitEvent(type, body)` — allocates seq, stores CBOR `{type, ...body}` via `putEvent(seq, did, '', encoded)`, broadcasts frame. `formatFirehoseEvent` branches on `evtData.type ?? 'commit'` → `t: '#identity'|'#account'|'#sync'|'#commit'`. `/init` emits `#identity` `{did, handle, time}` + `#account` `{did, active: true, time}` (+ `#sync` when a commit exists); handle-changing re-init emits `#identity`.
- [x] e2e test: fresh init then subscribeRepos from cursor 0 yields `#identity` then `#account` frames before first `#commit`; createCommit seqs continue after them.
- [x] Verify RED. Implement port + adapters + emitEvent + init wiring. Verify GREEN. Commit.
### Task 3: rpc: scopes
**Files:** Modify `packages/core/src/scope.js`, `packages/core/src/pds.js` (AppView proxy + atproto-proxy path); Test `packages/core/test/scope.test.js`, `test/e2e.test.js` (scope-enforcement describe).
**Interfaces:** `parseRpcScope(scope)` → `{lxms: string[]|'*', aud: string|'*'}|null`; `ScopePermissions.allowsRpc(lxm, aud)` with aud compared in `did#serviceId` form, lxm case-insensitive, `rpc:*` and `aud=*` wildcards. Proxy paths: full access bypasses; granular tokens require `allowsRpc(lxm, `${serviceDid}#${serviceId}`)` → else 403 `InvalidScope`.
- [x] Unit tests: parse `rpc:app.bsky.feed.getTimeline?aud=did:web:api.bsky.app%23bsky_appview`, `rpc:*?aud=*`, repeated `lxm` params, case-insensitive match, reject missing aud.
- [x] e2e test: granular token without rpc scope → 403 on app.bsky proxy; with matching rpc scope → passes scope gate.
- [x] Verify RED → implement → GREEN → commit.
### Task 4: DPoP nonces
**Files:** Modify `packages/core/src/oauth.js` (nonce mint/verify helpers), `packages/core/src/pds.js` (PAR + token handlers), `test/helpers/dpop.js` (retry on `use_dpop_nonce`); Test `packages/core/test/oauth.test.js`, e2e via existing OAuth flow.
**Interfaces:** `mintDpopNonce(secret, now)` / `isValidDpopNonce(secret, nonce, now)` (HMAC-SHA256, 5-min windows, current+previous accepted). PAR/token: proof without valid `nonce` → 400 `{error:'use_dpop_nonce'}` + `DPoP-Nonce` header; success responses also set `DPoP-Nonce`.
- [x] Unit tests: nonce validates in current and previous window, rejects two windows back / garbage.
- [x] e2e: existing DPoP flow keeps passing once helper retries on `use_dpop_nonce` (asserts the 400-then-retry path implicitly); add explicit test that PAR without nonce returns `use_dpop_nonce` + header.
- [x] Verify RED → implement → GREEN → commit.
### Task 5: account status + activate/deactivate
**Files:** Modify `packages/core/src/ports.js`, sqlite + DO adapters (`metadata` key `accountStatus`), readonly shim (returns `'active'`), `packages/core/src/pds.js` (routes, write-gating, getRepoStatus/listRepos); Test `test/e2e.test.js`.
**Interfaces:** Port: `getAccountStatus(): Promise<string|null>` / `setAccountStatus(status)`. Routes `com.atproto.server.activateAccount`/`deactivateAccount` (POST, full access). Deactivated: record/blob writes → 409 `{error:'AccountDeactivated'}`; `getRepoStatus`/`listRepos` report `active:false, status:'deactivated'`; `#account` emitted on both transitions.
- [x] e2e test: deactivate → createRecord 409, getRepoStatus inactive, `#account` frame observed; activate → writes work again.
- [x] Verify RED → implement → GREEN → commit.
### Task 6: migration endpoints
**Files:** Modify `packages/core/src/pds.js` (routes: `server.getServiceAuth`, `server.checkAccountStatus`, `repo.importRepo`, `sync.listMissingBlobs`); Test `test/e2e.test.js`.
**Interfaces:** `getServiceAuth` GET params `aud` (DID or `did#serviceId`), `lxm` (required), `exp` (≤ now+3600) → `{token}` via existing `createServiceJwt` (bare-DID aud; rpc-scope gate from Task 3). `checkAccountStatus` → `{activated, validDid, repoCommit, repoRev, repoBlocks, indexedRecords, privateStateValues: 0, expectedBlobs, importedBlobs}` (block count via the `getRepo` walk; expectedBlobs = distinct `findBlobRefs` over all records; importedBlobs = those present in `listBlobs`). `importRepo` POST CAR body → `loadRepositoryFromCar`, DID must match auth, allowed while deactivated, emits `#sync`. `listMissingBlobs` → `{blobs:[{cid, recordUri}], cursor}` limit default 500.
- [x] e2e migration round-trip test: create records+blob → checkAccountStatus counts match → deactivate → getRepo CAR → importRepo (same PDS) 200 + `#sync` frame → listMissingBlobs empty after re-upload → activate.
- [x] getServiceAuth e2e: full-access token mints JWT with `aud`/`lxm`; granular token without rpc scope → 403.
- [x] Verify RED → implement → GREEN → commit.
### Task 7: PLC identity endpoints
**Files:** Create `packages/core/src/plc.js` (lift `publicKeyToDidKey`, `base58btcEncode`, op signing from `scripts/setup.js`; script imports from core); Modify `packages/core/src/pds.js` (routes: `identity.getRecommendedDidCredentials`, `identity.requestPlcOperationSignature`, `identity.signPlcOperation`, `identity.submitPlcOperation`, new `plcUrl` option); Test `packages/core/test/plc.test.js`, `test/e2e.test.js`.
**Interfaces:** `publicKeyToDidKey(compressedPubKey)` → `did:key:z...`; `buildPlcOperation({prev, rotationKeys, verificationMethods, alsoKnownAs, services})`; `signPlcOperationObject(op, cryptoKey)` → op with `sig`. Endpoints per spec §4: recommended credentials from signing key + hostname + handle; sign merges body over recommended, `prev` from `{plcUrl}/{did}/log/last`; submit POSTs to `{plcUrl}/{did}`; request-signature is a documented no-op 200.
- [x] Unit tests: did:key encoding matches a known vector from `scripts/setup.js` behavior; operation signing produces verifiable sig.
- [x] e2e (docker PLC): getRecommendedDidCredentials shape; signPlcOperation returns signed op with `prev`; submitPlcOperation round-trips against local PLC (update handle alsoKnownAs), then restore.
- [x] Verify RED → implement → GREEN → commit.
### Task 8: docs + changelog
- [x] Update `docs/endpoint-comparison.md` implemented-endpoint tables and `CHANGELOG.md` (0.9.0 entry). Full suite + build + lint green. Commit.