@getcirrus/pds 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,442 @@
1
+ <div align="center">
2
+ <h1>☁️ Cirrus</h1>
3
+ <p><em>The lightest PDS in the Atmosphere</em></p>
4
+ </div>
5
+
6
+ Cirrus is a single-user [AT Protocol](https://atproto.com) Personal Data Server (PDS) that runs on Cloudflare Workers. Named for the highest, lightest clouds in a blue sky – fitting for a Bluesky server running on Cloudflare.
7
+
8
+ Host your own Bluesky identity with minimal infrastructure.
9
+
10
+ > **⚠️ Experimental Software**
11
+ >
12
+ > This is an early-stage project under active development. **Do not migrate your main Bluesky account to this PDS yet.** Use a test account or create a new identity for experimentation. Data loss, breaking changes, and missing features are expected.
13
+
14
+ ## What is a PDS?
15
+
16
+ A Personal Data Server is where your Bluesky data lives – your posts, follows, profile, and media. This package lets you run your own PDS on Cloudflare Workers, giving you control over your data and identity.
17
+
18
+ Key benefits:
19
+
20
+ - **Independence from platform changes** – If Bluesky's ownership or policies change, the account remains under full control
21
+ - **Network resilience** – More independent PDS providers make the AT Protocol network stronger
22
+ - **Data sovereignty** – The repository lives on infrastructure under direct control
23
+ - **Portability** – Move between hosting providers without losing followers or identity
24
+ - **Edge performance** – Runs globally on Cloudflare's edge network
25
+
26
+ ## Quick Start
27
+
28
+ ```bash
29
+ pnpm create pds
30
+ # or
31
+ npm create pds
32
+ ```
33
+
34
+ This scaffolds a new project, installs dependencies, and runs the setup wizard. Start the dev server:
35
+
36
+ ```bash
37
+ cd pds-worker
38
+ npm run dev
39
+ ```
40
+
41
+ ## Manual Installation
42
+
43
+ ### 1. Install the package
44
+
45
+ ```bash
46
+ npm install @getcirrus/pds
47
+ ```
48
+
49
+ ### 2. Create a worker entry point
50
+
51
+ ```typescript
52
+ // src/index.ts
53
+ export { default, AccountDurableObject } from "@getcirrus/pds";
54
+ ```
55
+
56
+ ### 3. Configure wrangler.jsonc
57
+
58
+ ```jsonc
59
+ {
60
+ "name": "my-pds",
61
+ "main": "src/index.ts",
62
+ "compatibility_date": "2024-12-01",
63
+ "compatibility_flags": ["nodejs_compat"],
64
+ "durable_objects": {
65
+ "bindings": [{ "name": "ACCOUNT", "class_name": "AccountDurableObject" }],
66
+ },
67
+ "migrations": [
68
+ { "tag": "v1", "new_sqlite_classes": ["AccountDurableObject"] },
69
+ ],
70
+ "r2_buckets": [{ "binding": "BLOBS", "bucket_name": "pds-blobs" }],
71
+ }
72
+ ```
73
+
74
+ ### 4. Run the setup wizard
75
+
76
+ ```bash
77
+ pnpm pds init
78
+ ```
79
+
80
+ This prompts for your hostname, handle, and password, then generates signing keys and writes configuration.
81
+
82
+ ## CLI Reference
83
+
84
+ The package includes a CLI for setup, migration, and secret management.
85
+
86
+ ### `pds init`
87
+
88
+ Interactive setup wizard for configuring the PDS.
89
+
90
+ ```bash
91
+ pds init # Configure for local development
92
+ pds init --production # Deploy secrets to Cloudflare
93
+ ```
94
+
95
+ **What it does:**
96
+
97
+ - Prompts for PDS hostname, handle, and account password
98
+ - Generates cryptographic signing keys (secp256k1)
99
+ - Creates authentication token and JWT secret
100
+ - Writes public configuration to `wrangler.jsonc`
101
+ - Saves secrets to `.dev.vars` (local) or Cloudflare (production)
102
+
103
+ For migrations, it detects existing accounts and configures the PDS in deactivated mode, ready for data import.
104
+
105
+ ### `pds migrate`
106
+
107
+ Transfers account data from an existing PDS to a new one.
108
+
109
+ ```bash
110
+ pds migrate # Migrate to production PDS
111
+ pds migrate --dev # Migrate to local development server
112
+ pds migrate --clean # Reset and start fresh migration
113
+ ```
114
+
115
+ **What it does:**
116
+
117
+ 1. Resolves the DID to find the current PDS
118
+ 2. Authenticates with the source PDS
119
+ 3. Downloads the repository (posts, follows, likes, etc.)
120
+ 4. Imports the repository to the new PDS
121
+ 5. Transfers all blobs (images, videos)
122
+ 6. Copies user preferences
123
+
124
+ The migration is resumable. If interrupted, run `pds migrate` again to continue.
125
+
126
+ **Flags:**
127
+
128
+ - `--dev` – Target the local development server instead of production
129
+ - `--clean` – Delete any existing imported data and start fresh (only works on deactivated accounts)
130
+
131
+ ### `pds activate`
132
+
133
+ Enables writes on the account after migration.
134
+
135
+ ```bash
136
+ pds activate # Activate production account
137
+ pds activate --dev # Activate local development account
138
+ ```
139
+
140
+ Run this after migrating data and updating the DID document to point to the new PDS. The account will start accepting new posts, follows, and other writes.
141
+
142
+ ### `pds deactivate`
143
+
144
+ Disables writes on the account.
145
+
146
+ ```bash
147
+ pds deactivate # Deactivate production account
148
+ pds deactivate --dev # Deactivate local development account
149
+ ```
150
+
151
+ Use this before re-importing data (for example, to recover from issues). Deactivating prevents new writes during the reset and re-migration.
152
+
153
+ After deactivating:
154
+
155
+ ```bash
156
+ pds migrate --clean # Reset and re-import
157
+ pds activate # Go live again
158
+ ```
159
+
160
+ ### `pds secret`
161
+
162
+ Manage individual secrets.
163
+
164
+ ```bash
165
+ pds secret key # Generate new signing keypair
166
+ pds secret jwt # Generate new JWT secret
167
+ pds secret password # Set account password
168
+ ```
169
+
170
+ All secret commands support:
171
+
172
+ - `--local` – Write to `.dev.vars` instead of Cloudflare
173
+
174
+ #### `pds secret key`
175
+
176
+ Generates a new secp256k1 signing keypair. Updates both the private key secret and the public key in your configuration.
177
+
178
+ #### `pds secret jwt`
179
+
180
+ Generates a new JWT signing secret for session tokens.
181
+
182
+ #### `pds secret password`
183
+
184
+ Prompts for a new password and stores the bcrypt hash.
185
+
186
+ ## Architecture
187
+
188
+ The PDS runs as a Cloudflare Worker with a Durable Object for state:
189
+
190
+ ```
191
+ ┌─────────────────────────────────────────────────────────────┐
192
+ │ Cloudflare Worker │
193
+ │ ┌─────────────────────────────────────────────────────┐ │
194
+ │ │ Hono Router │ │
195
+ │ │ • Authentication middleware │ │
196
+ │ │ • CORS handling │ │
197
+ │ │ • DID document serving │ │
198
+ │ │ • XRPC endpoint routing │ │
199
+ │ │ • OAuth 2.1 provider │ │
200
+ │ │ • Proxy to AppView for read endpoints │ │
201
+ │ └─────────────────────────────────────────────────────┘ │
202
+ │ │ │
203
+ │ ▼ │
204
+ │ ┌─────────────────────────────────────────────────────┐ │
205
+ │ │ AccountDurableObject │ │
206
+ │ │ • SQLite repository storage │ │
207
+ │ │ • Merkle tree for commits │ │
208
+ │ │ • Record indexing │ │
209
+ │ │ • WebSocket firehose │ │
210
+ │ │ • OAuth token storage │ │
211
+ │ └─────────────────────────────────────────────────────┘ │
212
+ │ │ │
213
+ │ ▼ │
214
+ │ ┌─────────────────────────────────────────────────────┐ │
215
+ │ │ R2 Bucket │ │
216
+ │ │ • Blob storage (images, videos) │ │
217
+ │ └─────────────────────────────────────────────────────┘ │
218
+ └─────────────────────────────────────────────────────────────┘
219
+ ```
220
+
221
+ ### Components
222
+
223
+ - **Worker** – Stateless edge handler for routing, authentication, and DID document serving
224
+ - **AccountDurableObject** – Single-instance SQLite storage for your AT Protocol repository. Handles all write coordination and maintains the commit history.
225
+ - **R2** – Object storage for blobs (images, videos). Blobs are content-addressed by CID.
226
+
227
+ ### XRPC Proxy
228
+
229
+ For endpoints this PDS doesn't implement directly (like feed generation or notifications), requests are proxied to the Bluesky AppView. The PDS signs these requests with service authentication, so you get full Bluesky functionality without implementing every endpoint.
230
+
231
+ ## Identity: DIDs and Handles
232
+
233
+ AT Protocol uses two types of identifiers:
234
+
235
+ - **DID** (Decentralized Identifier): A permanent, cryptographic identity (for example, `did:web:pds.example.com` or `did:plc:abc123`). This never changes and is tied to a signing key.
236
+ - **Handle**: A human-readable username (for example, `alice.example.com`). This can be any domain under the owner's control.
237
+
238
+ The DID document (served at `/.well-known/did.json`) contains the public key and tells the network where the PDS is. The `alsoKnownAs` field links the DID to the handle.
239
+
240
+ ### Supported DID Methods
241
+
242
+ - **did:web** – Domain-based DIDs. The DID document is served by the PDS at `/.well-known/did.json`
243
+ - **did:plc** – PLC directory DIDs. Used when migrating from an existing Bluesky account
244
+
245
+ ### Handle Verification
246
+
247
+ Bluesky verifies control of the handle domain. Two methods are available:
248
+
249
+ #### Option A: Handle matches PDS hostname
250
+
251
+ When the handle matches the PDS hostname (for example, both are `pds.example.com`), the PDS automatically serves `/.well-known/atproto-did` with the DID. No additional DNS setup required.
252
+
253
+ #### Option B: Handle on a different domain
254
+
255
+ For a handle on a different domain (for example, handle `alice.example.com` while PDS is at `pds.example.com`):
256
+
257
+ 1. Add a DNS TXT record to the handle domain:
258
+
259
+ ```
260
+ _atproto.alice.example.com TXT "did=did:web:pds.example.com"
261
+ ```
262
+
263
+ 2. Verify the record:
264
+
265
+ ```bash
266
+ dig TXT _atproto.alice.example.com
267
+ ```
268
+
269
+ ## Configuration
270
+
271
+ The PDS uses environment variables for configuration. Public values go in `wrangler.jsonc`, secrets are stored via Wrangler or in `.dev.vars` for local development.
272
+
273
+ ### Public Variables (wrangler.jsonc)
274
+
275
+ | Variable | Description |
276
+ | -------------------- | ------------------------------------------ |
277
+ | `PDS_HOSTNAME` | Public hostname (e.g., pds.example.com) |
278
+ | `DID` | Account DID (did:web:... or did:plc:...) |
279
+ | `HANDLE` | Account handle |
280
+ | `SIGNING_KEY_PUBLIC` | Public key for DID document (multibase) |
281
+ | `INITIAL_ACTIVE` | Whether account starts active (true/false) |
282
+
283
+ ### Secrets
284
+
285
+ | Variable | Description |
286
+ | --------------- | ------------------------------------- |
287
+ | `AUTH_TOKEN` | Bearer token for API write operations |
288
+ | `SIGNING_KEY` | Private signing key (secp256k1 JWK) |
289
+ | `JWT_SECRET` | Secret for signing session JWTs |
290
+ | `PASSWORD_HASH` | Bcrypt hash of password for app login |
291
+
292
+ ## API Endpoints
293
+
294
+ ### Identity
295
+
296
+ | Endpoint | Description |
297
+ | ------------------------------ | ----------------------------------------------------- |
298
+ | `GET /.well-known/did.json` | DID document for did:web resolution |
299
+ | `GET /.well-known/atproto-did` | Handle verification (only if handle matches hostname) |
300
+ | `GET /health` | Health check with version info |
301
+
302
+ ### Federation (Sync)
303
+
304
+ | Endpoint | Description |
305
+ | ------------------------------------------- | ------------------------------------------- |
306
+ | `GET /xrpc/com.atproto.sync.getRepo` | Export repository as CAR file |
307
+ | `GET /xrpc/com.atproto.sync.getRepoStatus` | Repository status (commit, rev) |
308
+ | `GET /xrpc/com.atproto.sync.getBlocks` | Get specific blocks from repository |
309
+ | `GET /xrpc/com.atproto.sync.getBlob` | Download a blob by CID |
310
+ | `GET /xrpc/com.atproto.sync.listRepos` | List repositories (single-user: just yours) |
311
+ | `GET /xrpc/com.atproto.sync.listBlobs` | List all blobs in repository |
312
+ | `GET /xrpc/com.atproto.sync.subscribeRepos` | WebSocket firehose for real-time updates |
313
+
314
+ ### Repository Operations
315
+
316
+ | Endpoint | Auth | Description |
317
+ | --------------------------------------------- | ---- | ------------------------------------------ |
318
+ | `GET /xrpc/com.atproto.repo.describeRepo` | No | Repository metadata |
319
+ | `GET /xrpc/com.atproto.repo.getRecord` | No | Get a single record |
320
+ | `GET /xrpc/com.atproto.repo.listRecords` | No | List records in a collection |
321
+ | `POST /xrpc/com.atproto.repo.createRecord` | Yes | Create a new record |
322
+ | `POST /xrpc/com.atproto.repo.putRecord` | Yes | Create or update a record |
323
+ | `POST /xrpc/com.atproto.repo.deleteRecord` | Yes | Delete a record |
324
+ | `POST /xrpc/com.atproto.repo.applyWrites` | Yes | Batch create/update/delete operations |
325
+ | `POST /xrpc/com.atproto.repo.uploadBlob` | Yes | Upload an image or video |
326
+ | `POST /xrpc/com.atproto.repo.importRepo` | Yes | Import repository from CAR file |
327
+ | `GET /xrpc/com.atproto.repo.listMissingBlobs` | Yes | List blobs referenced but not yet uploaded |
328
+
329
+ ### Server & Session
330
+
331
+ | Endpoint | Auth | Description |
332
+ | ------------------------------------------------- | ---- | ----------------------------------- |
333
+ | `GET /xrpc/com.atproto.server.describeServer` | No | Server capabilities and info |
334
+ | `POST /xrpc/com.atproto.server.createSession` | No | Login with password, get JWT |
335
+ | `POST /xrpc/com.atproto.server.refreshSession` | Yes | Refresh JWT tokens |
336
+ | `GET /xrpc/com.atproto.server.getSession` | Yes | Get current session info |
337
+ | `POST /xrpc/com.atproto.server.deleteSession` | Yes | Logout |
338
+ | `GET /xrpc/com.atproto.server.getServiceAuth` | Yes | Get JWT for external services |
339
+ | `GET /xrpc/com.atproto.server.getAccountStatus` | Yes | Account status (active/deactivated) |
340
+ | `POST /xrpc/com.atproto.server.activateAccount` | Yes | Enable writes |
341
+ | `POST /xrpc/com.atproto.server.deactivateAccount` | Yes | Disable writes |
342
+
343
+ ### Handle Resolution
344
+
345
+ | Endpoint | Description |
346
+ | ---------------------------------------------- | ---------------------------------------- |
347
+ | `GET /xrpc/com.atproto.identity.resolveHandle` | Resolve handle to DID (local or proxied) |
348
+
349
+ ### Actor Preferences
350
+
351
+ | Endpoint | Auth | Description |
352
+ | ------------------------------------------ | ---- | -------------------- |
353
+ | `GET /xrpc/app.bsky.actor.getPreferences` | Yes | Get user preferences |
354
+ | `POST /xrpc/app.bsky.actor.putPreferences` | Yes | Set user preferences |
355
+
356
+ ### OAuth 2.1
357
+
358
+ The PDS includes a complete OAuth 2.1 provider for "Login with Bluesky":
359
+
360
+ | Endpoint | Description |
361
+ | --------------------------------------------- | ------------------------------ |
362
+ | `GET /.well-known/oauth-authorization-server` | OAuth server metadata |
363
+ | `POST /oauth/par` | Pushed Authorization Request |
364
+ | `GET /oauth/authorize` | Authorization endpoint |
365
+ | `POST /oauth/authorize` | Process authorization decision |
366
+ | `POST /oauth/token` | Token exchange |
367
+ | `POST /oauth/revoke` | Token revocation |
368
+
369
+ See the [@getcirrus/oauth-provider](../oauth-provider/) package for implementation details.
370
+
371
+ ## Deploying to Production
372
+
373
+ 1. **Enable R2** in your [Cloudflare dashboard](https://dash.cloudflare.com/?to=/:account/r2/overview). The bucket will be created automatically on first deploy.
374
+
375
+ 2. **Run the production setup** to deploy secrets:
376
+
377
+ ```bash
378
+ npx pds init --production
379
+ ```
380
+
381
+ 3. **Deploy your worker:**
382
+
383
+ ```bash
384
+ wrangler deploy
385
+ ```
386
+
387
+ 4. **Configure DNS** to point your domain to the worker. In Cloudflare DNS, add a CNAME record pointing to your workers.dev subdomain, or use a custom domain in your Worker settings.
388
+
389
+ ## Migration Guide
390
+
391
+ Moving an existing Bluesky account to your own PDS:
392
+
393
+ ### 1. Configure for migration
394
+
395
+ ```bash
396
+ npx pds init
397
+ # Answer "Yes" when asked about migrating an existing account
398
+ ```
399
+
400
+ ### 2. Deploy and migrate data
401
+
402
+ ```bash
403
+ wrangler deploy
404
+ npx pds migrate
405
+ ```
406
+
407
+ ### 3. Update your identity
408
+
409
+ Follow the [AT Protocol account migration guide](https://atproto.com/guides/account-migration) to update your DID document. This typically requires email verification from your current PDS.
410
+
411
+ ### 4. Go live
412
+
413
+ ```bash
414
+ npx pds activate
415
+ ```
416
+
417
+ ## Validation
418
+
419
+ Records are validated against AT Protocol lexicon schemas before being stored. The PDS uses optimistic validation:
420
+
421
+ - If a schema exists for the collection, the record must pass validation
422
+ - If no schema is loaded, the record is accepted (fail-open)
423
+
424
+ This allows the PDS to accept records for new or custom collection types while still enforcing validation for known types like `app.bsky.feed.post`.
425
+
426
+ ## Limitations
427
+
428
+ - **Single-user only** – One account per deployment
429
+ - **No account creation** – The owner is configured at deploy time
430
+ - **No email** – Password reset and email verification are not supported
431
+ - **No moderation** – No reporting or content moderation features
432
+
433
+ ## Resources
434
+
435
+ - [AT Protocol Documentation](https://atproto.com)
436
+ - [Bluesky](https://bsky.app)
437
+ - [Cloudflare Workers](https://developers.cloudflare.com/workers/)
438
+ - [Account Migration Guide](https://atproto.com/guides/account-migration)
439
+
440
+ ## License
441
+
442
+ MIT