fake-pds
v0.7.0
Published
Lightweight in-memory AT Protocol PDS for testing
Downloads
67
Maintainers
Readme
fake-pds
A lightweight, in-memory AT Protocol PDS for testing. No database, no crypto verification, no Merkle Search Tree — just the XRPC endpoints that matter for integration tests.
Install
npm install fake-pdsQuick Start
import { createFakePds } from "fake-pds";
const pds = await createFakePds({
accounts: [
{ did: "did:plc:alice", handle: "alice.test" },
{ did: "did:plc:bob", handle: "bob.test" },
],
});
// Seed data
pds.putRecord("did:plc:alice", "app.opake.publicKey", "self", {
opakeVersion: 1,
publicKey: { $bytes: "..." },
algo: "x25519",
createdAt: "2026-01-01T00:00:00Z",
});
// Use in tests — it's a real HTTP server
const res = await fetch(
`${pds.url}/xrpc/com.atproto.repo.getRecord?repo=did:plc:alice&collection=app.opake.publicKey&rkey=self`,
);
const record = await res.json();
// Reset between tests
pds.reset();
// Cleanup
await pds.close();Endpoints
Repository (XRPC)
| Endpoint | Method | Auth | Notes |
|----------|--------|------|-------|
| com.atproto.repo.getRecord | GET | No | Returns { uri, cid, value } or 404 |
| com.atproto.repo.listRecords | GET | No | Cursor pagination, sorted by rkey |
| com.atproto.repo.createRecord | POST | Yes | Auto-generates TID rkey if omitted |
| com.atproto.repo.putRecord | POST | Yes | Upsert by rkey |
| com.atproto.repo.deleteRecord | POST | Yes | No-ops if missing |
| com.atproto.repo.uploadBlob | POST | Yes | Returns blob ref |
| com.atproto.sync.getBlob | GET | No | Returns stored blob bytes |
Identity
| Endpoint | Method | Auth | Notes |
|----------|--------|------|-------|
| com.atproto.identity.resolveHandle | GET | No | Returns { did } or 404 |
Auth (Legacy)
| Endpoint | Method | Notes |
|----------|--------|-------|
| com.atproto.server.createSession | POST | Returns fake JWT tokens |
| com.atproto.server.refreshSession | POST | Invalidates old tokens |
OAuth
| Endpoint | Method | Notes |
|----------|--------|-------|
| /.well-known/oauth-protected-resource | GET | Static metadata |
| /.well-known/oauth-authorization-server | GET | Server metadata |
| /oauth/par | POST | PAR, returns request_uri + DPoP nonce |
| /oauth/token | POST | Code exchange or refresh, returns DPoP tokens |
Auth Modes
// No auth checking — fastest for unit tests
const pds = await createFakePds({ auth: "none" });
// Bearer token auth (legacy JWT flow)
const pds = await createFakePds({ auth: "bearer" });
// OAuth with DPoP nonce tracking
const pds = await createFakePds({ auth: "oauth" });Legacy Auth
const pds = await createFakePds({
accounts: [{ did: "did:plc:alice", handle: "alice.test" }],
auth: "bearer",
});
// Login
const loginRes = await fetch(`${pds.url}/xrpc/com.atproto.server.createSession`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ identifier: "alice.test", password: "anything" }),
});
const { accessJwt } = await loginRes.json();
// Authenticated request
await fetch(`${pds.url}/xrpc/com.atproto.repo.putRecord`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessJwt}`,
},
body: JSON.stringify({ repo: "did:plc:alice", collection: "app.test", rkey: "x", record: {} }),
});OAuth (Direct Token Issuance)
Skip the browser redirect flow entirely:
const pds = await createFakePds({
accounts: [{ did: "did:plc:alice", handle: "alice.test" }],
auth: "oauth",
});
// Get tokens directly — no browser flow needed
const { accessToken, refreshToken, dpopNonce } = pds.issueOAuthTokens("did:plc:alice");
// Use with DPoP header
await fetch(`${pds.url}/xrpc/com.atproto.repo.putRecord`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `DPoP ${accessToken}`,
},
body: JSON.stringify({ repo: "did:plc:alice", collection: "app.test", rkey: "x", record: {} }),
});API
createFakePds(options?): Promise<FakePds>
Create and start a fake PDS.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| port | number | 0 (random) | Port to listen on |
| accounts | Account[] | [] | Pre-registered accounts |
| auth | "none" \| "bearer" \| "oauth" | "none" | Auth enforcement mode |
FakePds
| Method | Description |
|--------|-------------|
| url | Base URL of the running server |
| putRecord(did, collection, rkey, value) | Seed a record (bypasses auth) |
| listRecords(did, collection) | List records for test inspection |
| issueOAuthTokens(did) | Get OAuth tokens directly |
| reset() | Clear all records, blobs, and sessions |
| close() | Stop the server |
Account
interface Account {
did: string;
handle: string;
password?: string; // If set, createSession validates it
}What This Doesn't Do
- No Merkle Search Tree or commit history
- No record validation against lexicon schemas
- No federation, sync, or firehose
- No DID document hosting
- No PKCE or DPoP signature verification
- CIDs are fake (sha256-based, not real IPFS CIDs)
These are intentional tradeoffs. The goal is fast, reliable integration tests — not a spec-complete PDS.
License
AGPL-3.0-or-later
