@alteran/astro 0.1.0

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.
Files changed (90) hide show
  1. package/README.md +558 -0
  2. package/index.d.ts +12 -0
  3. package/index.js +129 -0
  4. package/package.json +75 -0
  5. package/src/_worker.ts +44 -0
  6. package/src/app.ts +10 -0
  7. package/src/db/client.ts +7 -0
  8. package/src/db/dal.ts +97 -0
  9. package/src/db/repo.ts +135 -0
  10. package/src/db/schema.ts +89 -0
  11. package/src/db/seed.ts +14 -0
  12. package/src/env.d.ts +4 -0
  13. package/src/handlers/debug.ts +34 -0
  14. package/src/handlers/health.ts +6 -0
  15. package/src/handlers/ready.ts +14 -0
  16. package/src/handlers/root.ts +5 -0
  17. package/src/handlers/wellknown.ts +7 -0
  18. package/src/handlers/xrpc.repo.core.ts +57 -0
  19. package/src/handlers/xrpc.server.createSession.ts +25 -0
  20. package/src/handlers/xrpc.server.refreshSession.ts +43 -0
  21. package/src/lib/auth.ts +20 -0
  22. package/src/lib/blockstore-gc.ts +197 -0
  23. package/src/lib/cache.ts +236 -0
  24. package/src/lib/car-reader.ts +157 -0
  25. package/src/lib/commit-log-pruning.ts +76 -0
  26. package/src/lib/commit.ts +162 -0
  27. package/src/lib/config.ts +208 -0
  28. package/src/lib/errors.ts +142 -0
  29. package/src/lib/firehose/frames.ts +229 -0
  30. package/src/lib/firehose/parse.ts +82 -0
  31. package/src/lib/firehose/validation.ts +9 -0
  32. package/src/lib/handle.ts +90 -0
  33. package/src/lib/jwt.ts +150 -0
  34. package/src/lib/logger.ts +73 -0
  35. package/src/lib/metrics.ts +194 -0
  36. package/src/lib/mst/blockstore.ts +105 -0
  37. package/src/lib/mst/index.ts +3 -0
  38. package/src/lib/mst/mst.ts +643 -0
  39. package/src/lib/mst/util.ts +86 -0
  40. package/src/lib/ratelimit.ts +34 -0
  41. package/src/lib/sequencer.ts +10 -0
  42. package/src/lib/streaming-car.ts +137 -0
  43. package/src/lib/token-cleanup.ts +38 -0
  44. package/src/lib/tracing.ts +136 -0
  45. package/src/lib/util.ts +55 -0
  46. package/src/middleware.ts +102 -0
  47. package/src/pages/.well-known/atproto-did.ts +7 -0
  48. package/src/pages/.well-known/did.json.ts +76 -0
  49. package/src/pages/debug/blob/[...key].ts +27 -0
  50. package/src/pages/debug/db/bootstrap.ts +23 -0
  51. package/src/pages/debug/db/commits.ts +20 -0
  52. package/src/pages/debug/gc/blobs.ts +16 -0
  53. package/src/pages/debug/record.ts +33 -0
  54. package/src/pages/health.ts +68 -0
  55. package/src/pages/index.astro +57 -0
  56. package/src/pages/index.ts +2 -0
  57. package/src/pages/ready.ts +16 -0
  58. package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +38 -0
  59. package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +45 -0
  60. package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +73 -0
  61. package/src/pages/xrpc/com.atproto.repo.createRecord.ts +36 -0
  62. package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +36 -0
  63. package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +51 -0
  64. package/src/pages/xrpc/com.atproto.repo.getRecord.ts +25 -0
  65. package/src/pages/xrpc/com.atproto.repo.listRecords.ts +57 -0
  66. package/src/pages/xrpc/com.atproto.repo.putRecord.ts +36 -0
  67. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +53 -0
  68. package/src/pages/xrpc/com.atproto.server.createSession.ts +92 -0
  69. package/src/pages/xrpc/com.atproto.server.deleteSession.ts +25 -0
  70. package/src/pages/xrpc/com.atproto.server.describeServer.ts +17 -0
  71. package/src/pages/xrpc/com.atproto.server.getSession.ts +46 -0
  72. package/src/pages/xrpc/com.atproto.server.refreshSession.ts +67 -0
  73. package/src/pages/xrpc/com.atproto.sync.getBlocks.json.ts +16 -0
  74. package/src/pages/xrpc/com.atproto.sync.getBlocks.ts +56 -0
  75. package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +20 -0
  76. package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +43 -0
  77. package/src/pages/xrpc/com.atproto.sync.getHead.ts +11 -0
  78. package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +42 -0
  79. package/src/pages/xrpc/com.atproto.sync.getRecord.ts +63 -0
  80. package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +20 -0
  81. package/src/pages/xrpc/com.atproto.sync.getRepo.range.ts +34 -0
  82. package/src/pages/xrpc/com.atproto.sync.getRepo.ts +17 -0
  83. package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +53 -0
  84. package/src/pages/xrpc/com.atproto.sync.listRepos.ts +31 -0
  85. package/src/services/car.ts +249 -0
  86. package/src/services/r2-blob-store.ts +87 -0
  87. package/src/services/repo-manager.ts +339 -0
  88. package/src/shims/astro-internal-handler.d.ts +4 -0
  89. package/src/worker/sequencer.ts +563 -0
  90. package/types/env.d.ts +48 -0
package/README.md ADDED
@@ -0,0 +1,558 @@
1
+ # Alteran
2
+
3
+ ## Astro Integration
4
+
5
+ This repository now ships an Astro integration that turns any Cloudflare Worker-backed Astro app into a single-user ATProto Personal Data Server. Install the package (or link it locally), then add the integration to your `astro.config.mjs`:
6
+
7
+ ```bash
8
+ npm install @alteran/astro
9
+ # or
10
+ bun add @alteran/astro
11
+ ```
12
+
13
+ ```ts
14
+ import { defineConfig } from 'astro/config';
15
+ import cloudflare from '@astrojs/cloudflare';
16
+ import alteran from '@alteran/astro';
17
+
18
+ export default defineConfig({
19
+ adapter: cloudflare({ mode: 'advanced' }),
20
+ integrations: [alteran()],
21
+ });
22
+ ```
23
+
24
+ By default the integration injects all `/xrpc/*` ATProto routes, health/ready checks, and the Cloudflare Worker entrypoint that wires `locals.runtime`. Optional flags let you expose the `/debug/*` utilities or keep your own homepage:
25
+
26
+ ```ts
27
+ alteran({
28
+ debugRoutes: process.env.NODE_ENV !== 'production',
29
+ includeRootEndpoint: false,
30
+ injectServerEntry: true,
31
+ });
32
+ ```
33
+
34
+ The integration automatically:
35
+ - Adds a Vite alias of `@alteran/*` that points to the package runtime
36
+ - Registers the middleware that applies structured logging and CORS enforcement
37
+ - Injects all PDS HTTP endpoints into the host project
38
+ - Sets `build.serverEntry` to the packaged Cloudflare worker (unless you opt out)
39
+ - Publishes ambient env typings so `Env` and `App.Locals` are available from TypeScript
40
+
41
+ When deploying, continue to configure Wrangler/D1/R2 secrets exactly as before—the integration does not change the runtime requirements.
42
+
43
+ To install dependencies:
44
+
45
+ ```bash
46
+ bun install
47
+ ```
48
+
49
+ Dev server (Vite dev):
50
+
51
+ ```bash
52
+ bun run dev
53
+ ```
54
+
55
+ Cloudflare local dev (optional):
56
+
57
+ ```bash
58
+ bunx wrangler dev --local
59
+ ```
60
+
61
+ Build and deploy:
62
+
63
+ ```bash
64
+ bun run build
65
+ bun run deploy
66
+ ```
67
+
68
+ Health endpoints: `GET /health` and `GET /ready` return `200 ok`.
69
+
70
+ Auth (JWT)
71
+ - `POST /xrpc/com.atproto.server.createSession` returns `accessJwt` and `refreshJwt` (HS256).
72
+ - `POST /xrpc/com.atproto.server.refreshSession` expects `Authorization: Bearer <refreshJwt>` and issues a new pair.
73
+ - Use `Authorization: Bearer <accessJwt>` on write routes.
74
+ - Secrets to set (Wrangler secrets or local bindings):
75
+ - `USER_PASSWORD` (dev login password)
76
+ - `ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET` (HMAC keys)
77
+ - `PDS_DID`, `PDS_HANDLE`
78
+
79
+ Rate limiting & limits
80
+ - Per‑IP rate limit (best‑effort, D1‑backed): set `PDS_RATE_LIMIT_PER_MIN` (default writes=60/min, blobs=30/min). Responses include `x-ratelimit-*` headers.
81
+ - JSON body size cap via `PDS_MAX_JSON_BYTES` (default 65536/64 KiB).
82
+ - CORS: allow `*` by default in dev. In production, set `PDS_CORS_ORIGIN` to a CSV of allowed origins (e.g., `https://example.com,https://app.example.com`). Requests with an `Origin` not in this set are denied at the CORS layer (no wildcard fallback).
83
+
84
+ This project was created using `bun init` in bun v1.2.22 and configured for Cloudflare Workers with Vite and `@cloudflare/vite-plugin`.
85
+
86
+ ## Database Migrations
87
+
88
+ This project uses Drizzle Kit for database schema management and migrations.
89
+
90
+ ### Migration Workflow
91
+
92
+ 1. **Modify Schema**: Edit [`src/db/schema.ts`](src/db/schema.ts:1) to add/modify tables or indexes
93
+ 2. **Generate Migration**: Run `bun run db:generate` to create a new migration file in `drizzle/`
94
+ 3. **Review Migration**: Check the generated SQL in `drizzle/XXXX_*.sql`
95
+ 4. **Apply Locally**: Run `bun run db:apply:local` to apply to local D1 database
96
+ 5. **Apply to Production**: Run `wrangler d1 migrations apply pds --remote` after deployment
97
+
98
+ ### Migration Versioning
99
+
100
+ - Migrations are versioned sequentially (0000, 0001, 0002, etc.)
101
+ - Each migration is tracked in `drizzle/meta/_journal.json`
102
+ - Migrations are applied in order and cannot be skipped
103
+ - Applied migrations are recorded in D1's `_cf_KV` table
104
+
105
+ ### Rollback Procedures
106
+
107
+ **Important**: D1 does not support automatic rollbacks. To rollback:
108
+
109
+ 1. Create a new migration that reverses the changes
110
+ 2. Test thoroughly in local/staging environment
111
+ 3. Apply the rollback migration to production
112
+
113
+ Example rollback migration:
114
+ ```sql
115
+ -- Rollback: Remove index added in 0002
116
+ DROP INDEX IF EXISTS `record_cid_idx`;
117
+ ```
118
+
119
+ ### Data Retention & Pruning
120
+
121
+ **Commit Log**: Stores full commit history for firehose and sync
122
+ - Default retention: Last 10,000 commits
123
+ - Pruning: Use [`pruneOldCommits()`](src/lib/commit-log-pruning.ts:19) utility
124
+ - Older commits can be safely removed as current state is in MST
125
+
126
+ **Blockstore**: Stores MST nodes (Merkle Search Tree blocks)
127
+ - Retention: Blocks referenced by recent commits
128
+ - GC: Use [`pruneOrphanedBlocks()`](src/lib/blockstore-gc.ts:127) utility
129
+ - Orphaned blocks (not in recent commits) can be removed
130
+
131
+ **Token Revocation**: Stores revoked JWT tokens
132
+ - Automatic cleanup: Expired tokens removed lazily (1% of requests)
133
+ - Manual cleanup: Use token cleanup utility
134
+ ## Configuration Management
135
+
136
+ ### Environment Setup
137
+
138
+ This PDS supports multiple environments (dev, staging, production) with separate configurations.
139
+
140
+ **Deploy to specific environment:**
141
+ ```bash
142
+ # Development
143
+ wrangler deploy --env dev
144
+
145
+ # Staging
146
+ wrangler deploy --env staging
147
+
148
+ # Production
149
+ wrangler deploy --env production
150
+ ```
151
+
152
+ ### Required Secrets
153
+
154
+ Set these secrets for each environment using `wrangler secret put <NAME> --env <environment>`:
155
+
156
+ | Secret | Description | Example |
157
+ |--------|-------------|---------|
158
+ | `PDS_DID` | Your DID identifier | `did:plc:abc123` or `did:web:example.com` |
159
+ | `PDS_HANDLE` | Your handle | `user.bsky.social` |
160
+ | `USER_PASSWORD` | Login password | Strong password |
161
+ | `ACCESS_TOKEN_SECRET` | JWT access token secret | Random 32+ char string |
162
+ | `REFRESH_TOKEN_SECRET` | JWT refresh token secret | Random 32+ char string |
163
+ | `REPO_SIGNING_KEY` | Ed25519 signing key (base64) | From `generate-signing-key.ts` |
164
+
165
+ **Generate secrets:**
166
+ ```bash
167
+ # Generate signing key
168
+ bun run scripts/generate-signing-key.ts
169
+
170
+ # Set secrets (example for production)
171
+ wrangler secret put PDS_DID --env production
172
+ wrangler secret put PDS_HANDLE --env production
173
+ wrangler secret put USER_PASSWORD --env production
174
+ wrangler secret put ACCESS_TOKEN_SECRET --env production
175
+ wrangler secret put REFRESH_TOKEN_SECRET --env production
176
+ wrangler secret put REPO_SIGNING_KEY --env production
177
+ ```
178
+
179
+ ### Optional Configuration
180
+
181
+ These can be set as environment variables in [`wrangler.jsonc`](wrangler.jsonc:1) or as secrets:
182
+
183
+ | Variable | Default | Description |
184
+ |----------|---------|-------------|
185
+ | `PDS_ALLOWED_MIME` | `image/jpeg,image/png,...` | Comma-separated MIME types |
186
+ | `PDS_MAX_BLOB_SIZE` | `5242880` (5MB) | Max blob size in bytes |
187
+ | `PDS_MAX_JSON_BYTES` | `65536` (64KB) | Max JSON body size |
188
+ | `PDS_RATE_LIMIT_PER_MIN` | `60` | Write requests per minute |
189
+ | `PDS_CORS_ORIGIN` | `*` (dev), specific (prod) | Allowed CORS origins |
190
+ | `PDS_SEQ_WINDOW` | `512` | Firehose sequence window |
191
+ | `PDS_HOSTNAME` | - | Public hostname |
192
+ | `PDS_ACCESS_TTL_SEC` | `3600` (1 hour) | Access token TTL |
193
+ | `PDS_REFRESH_TTL_SEC` | `2592000` (30 days) | Refresh token TTL |
194
+
195
+ ### Configuration Validation
196
+
197
+ The PDS validates configuration on startup and will fail fast if required secrets are missing:
198
+
199
+ ```typescript
200
+ // Automatic validation in src/_worker.ts
201
+ validateConfigOrThrow(env);
202
+ ```
203
+
204
+ **Validation checks:**
205
+ - All required secrets are present
206
+ - CORS is not wildcard in production
207
+ - DID format is valid
208
+ - Handle format is valid
209
+ - Numeric values are positive
210
+
211
+ ### Environment-Specific Settings
212
+
213
+ See [`wrangler.jsonc`](wrangler.jsonc:40) for environment-specific configurations:
214
+
215
+ - **Development**: Relaxed CORS, larger blob limits, local D1/R2
216
+ - **Staging**: Production-like settings, separate D1/R2 instances
217
+ - **Production**: Strict CORS, production D1/R2, observability enabled
218
+
219
+
220
+ Debugging & storage
221
+ - D1 schema/migrations: generated with Drizzle Kit into `drizzle/`. Generate with `bunx drizzle-kit generate`.
222
+ - Apply schema locally: `bunx wrangler d1 migrations apply pds --local` (requires dev DB named `pds`).
223
+ - Bootstrap route (alt): `POST /debug/db/bootstrap` creates a minimal `record` table.
224
+ - Insert a test record: `POST /debug/record` with `{ "uri": "at://did:example/app.bsky.feed.post/123", "json": {"msg":"hi"} }`.
225
+ - Get a record: `GET /debug/record?uri=at://did:example/app.bsky.feed.post/123`.
226
+ - R2 test: `PUT /debug/blob/<key>` and `GET /debug/blob/<key>`.
227
+ - Run GC: `POST /debug/gc/blobs` removes R2 objects with no references
228
+
229
+ XRPC surface
230
+ - `GET /xrpc/com.atproto.server.describeServer`
231
+ - `POST /xrpc/com.atproto.server.createSession` (returns JWTs)
232
+ - `POST /xrpc/com.atproto.server.refreshSession`
233
+ - `GET /xrpc/com.atproto.repo.getRecord?uri=...` (reads from D1 `record` table) or `repo+collection+rkey`
234
+ - `POST /xrpc/com.atproto.repo.createRecord` (auth required)
235
+ - `POST /xrpc/com.atproto.repo.putRecord` (auth required)
236
+ - `POST /xrpc/com.atproto.repo.deleteRecord` (auth required)
237
+ - `POST /xrpc/com.atproto.repo.uploadBlob` (auth + MIME allowlist)
238
+ - Stores blob metadata in `blob` table (`cid`=sha256 b64url, `mime`, `size`)
239
+ - Blob references inside records tracked by R2 key; deleting a record drops usage and GC can reclaim orphaned objects
240
+
241
+ Sync (minimal JSON variants)
242
+ - `GET /xrpc/com.atproto.sync.getHead` → `{ root, rev }`
243
+ - `GET /xrpc/com.atproto.sync.getRepo.json?did=<did>` → `{ did, head, rev, records: [{uri,cid,value}] }`
244
+ - `GET /xrpc/com.atproto.sync.getCheckout.json?did=<did>` → same as above
245
+ - `GET /xrpc/com.atproto.sync.getBlocks.json?cids=<cid1,cid2>` → `{ blocks: [{cid,value}] }`
246
+
247
+ Sync (CAR v1)
248
+ - `GET /xrpc/com.atproto.sync.getRepo?did=<did>` → `application/vnd.ipld.car` snapshot
249
+ - `GET /xrpc/com.atproto.sync.getCheckout?did=<did>` → same as above
250
+ - `GET /xrpc/com.atproto.sync.getBlocks?cids=<cid1,cid2>` → `application/vnd.ipld.car` with requested blocks
251
+ - Blocks are DAG-CBOR encoded; CIDs are CIDv1 (dag-cbor + sha2-256)
252
+
253
+ Firehose (WebSocket)
254
+ - `GET /xrpc/com.atproto.sync.subscribeRepos` upgrades to WebSocket.
255
+ - On writes, the worker POSTs a small commit frame to the `Sequencer` Durable Object, which broadcasts to all subscribers.
256
+ - Frames (subject to change):
257
+ - `{"type":"hello","now":<ms>}` once on connect
258
+ - `{"type":"commit","did":"...","commitCid":"...","rev":<n>,"ts":<ms>}` on each write
259
+
260
+ Blob storage
261
+ - Keys are content-addressed: `blobs/by-cid/<sha256-b64url>`; upload response `$link` equals this key.
262
+ - Allowed MIME types via `PDS_ALLOWED_MIME` (CSV). Size limit via `PDS_MAX_BLOB_SIZE` (bytes).
263
+
264
+ Secrets & config (Wrangler)
265
+ - Required:
266
+ - `PDS_DID`, `PDS_HANDLE`, `USER_PASSWORD`
267
+ - `ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`
268
+ - Optional:
269
+ - `PDS_ALLOWED_MIME`, `PDS_MAX_BLOB_SIZE`, `PDS_MAX_JSON_BYTES`, `PDS_RATE_LIMIT_PER_MIN`, `PDS_CORS_ORIGIN`
270
+ - Durable Objects: ensure binding for `Sequencer` exists and migration tag added (see `wrangler.jsonc`).
271
+
272
+ Identity (DID)
273
+ - This single‑user PDS uses `did:web`.
274
+ - Host `/.well-known/atproto-did` on your production domain with the DID value.
275
+ - Set `PDS_DID` and `PDS_HANDLE` secrets to match your deployment.
276
+
277
+ ## P0 Implementation - Core Protocol Compliance ✅
278
+
279
+ This PDS now implements full AT Protocol core compliance with:
280
+
281
+ ### MST (Merkle Search Tree)
282
+ - ✅ Sorted, deterministic tree structure
283
+ - ✅ Automatic rebalancing (~4 fanout)
284
+ - ✅ Prefix compression for efficiency
285
+ - ✅ D1 blockstore integration
286
+
287
+ ### Signed Commits
288
+ - ✅ Ed25519 cryptographic signatures
289
+ - ✅ AT Protocol v3 commit structure
290
+ - ✅ TID-based revisions
291
+ - ✅ Commit chain tracking
292
+
293
+ ### Firehose
294
+ - ✅ WebSocket-based event stream
295
+ - ✅ CBOR-encoded frames (#info, #commit, #identity, #account)
296
+ - ✅ Cursor-based replay
297
+ - ✅ Backpressure handling
298
+ - ✅ Durable Object coordination
299
+
300
+ ### XRPC Endpoints
301
+ - ✅ Server: getSession, deleteSession
302
+ - ✅ Repo: listRecords, describeRepo, applyWrites
303
+ - ✅ Sync: listBlobs, getRecord, listRepos, getLatestCommit
304
+ - ✅ Identity: resolveHandle, updateHandle
305
+
306
+ ## Setup Instructions
307
+
308
+ ### 1. Generate Signing Key
309
+ ```bash
310
+ bun run scripts/generate-signing-key.ts
311
+ ```
312
+
313
+ ### 2. Configure Secrets
314
+
315
+ **Required Secrets:**
316
+ ```bash
317
+ wrangler secret put REPO_SIGNING_KEY # From step 1
318
+ wrangler secret put PDS_DID # Your DID
319
+ wrangler secret put PDS_HANDLE # Your handle
320
+ wrangler secret put USER_PASSWORD # Login password
321
+ wrangler secret put ACCESS_TOKEN_SECRET
322
+ wrangler secret put REFRESH_TOKEN_SECRET
323
+ ```
324
+
325
+ **For Local Development (.dev.vars):**
326
+ ```env
327
+ PDS_DID=did:plc:your-did-here
328
+ PDS_HANDLE=your-handle.bsky.social
329
+ REPO_SIGNING_KEY=<base64-key-from-step-1>
330
+ USER_PASSWORD=your-password
331
+ ACCESS_TOKEN_SECRET=your-access-secret
332
+ REFRESH_TOKEN_SECRET=your-refresh-secret
333
+ PDS_SEQ_WINDOW=512
334
+ ```
335
+
336
+ ### 3. Run Database Migration
337
+ ```bash
338
+ bun run db:generate
339
+ bun run db:apply:local
340
+ ```
341
+
342
+ ### 4. Run Tests
343
+ ```bash
344
+ bun test tests/mst.test.ts
345
+ bun test tests/commit.test.ts
346
+ ```
347
+
348
+ ### 5. Start Development
349
+ ```bash
350
+ bun run dev
351
+ ```
352
+
353
+ ## Testing the Implementation
354
+
355
+ ### Test Firehose
356
+ ```bash
357
+ npm install -g wscat
358
+ wscat -c "ws://localhost:4321/xrpc/com.atproto.sync.subscribeRepos"
359
+ ```
360
+
361
+ ### Test XRPC Endpoints
362
+ ```bash
363
+ # Get session
364
+ curl http://localhost:4321/xrpc/com.atproto.server.getSession
365
+
366
+ # Describe repo
367
+ curl "http://localhost:4321/xrpc/com.atproto.repo.describeRepo?repo=did:example:single-user"
368
+
369
+ # List records
370
+ curl "http://localhost:4321/xrpc/com.atproto.repo.listRecords?repo=did:example:single-user&collection=app.bsky.feed.post"
371
+ ```
372
+
373
+ ## Documentation
374
+
375
+ - [`P0_COMPLETE.md`](P0_COMPLETE.md) - Full P0 implementation details
376
+ - [`P0_IMPLEMENTATION_SUMMARY.md`](P0_IMPLEMENTATION_SUMMARY.md) - Technical summary
377
+ - [`PROGRESS.md`](PROGRESS.md) - Development progress notes
378
+
379
+ Repo signing key (REQUIRED)
380
+ - Generate an Ed25519 signing key: `bun run scripts/generate-signing-key.ts`
381
+ - Store as `REPO_SIGNING_KEY` secret (base64-encoded private key)
382
+
383
+ ## P1 Implementation - Production Readiness 🚀
384
+
385
+ This PDS now includes production-grade features for security, observability, and reliability:
386
+
387
+ ### Authentication Hardening
388
+ - ✅ **Single-use refresh tokens** with JTI tracking
389
+ - ✅ **Token rotation** on every refresh
390
+ - ✅ **Automatic token cleanup** (lazy cleanup on 1% of requests)
391
+ - ✅ **Account lockout** after 5 failed login attempts (15-minute lockout)
392
+ - ✅ **EdDSA (Ed25519) JWT signing** support (in addition to HS256)
393
+ - ✅ **Proper JWT claims**: `sub`, `aud`, `iat`, `exp`, `jti`, `scope`
394
+ - ✅ **Production CORS validation** (no wildcard in production)
395
+
396
+ ### Error Handling
397
+ - ✅ **XRPC error hierarchy** with AT Protocol error codes
398
+ - ✅ **Consistent error responses** with user-friendly messages
399
+ - ✅ **Error categorization** (client vs server errors)
400
+ - ✅ **Request ID tracking** in all error responses
401
+
402
+ ### Observability
403
+ - ✅ **Structured JSON logging** with levels (debug, info, warn, error)
404
+ - ✅ **Request ID tracking** in all logs and response headers
405
+ - ✅ **Enhanced health checks** for D1 and R2 dependencies
406
+ - ✅ **Performance metrics** in request logs (duration, status)
407
+
408
+ ### Additional Configuration
409
+
410
+ **JWT Configuration:**
411
+ ```bash
412
+ # Algorithm selection (HS256 or EdDSA)
413
+ PDS_HOSTNAME=your-pds.example.com
414
+ PDS_ACCESS_TTL_SEC=3600 # 1 hour
415
+ PDS_REFRESH_TTL_SEC=2592000 # 30 days
416
+ JWT_ALGORITHM=HS256 # or EdDSA
417
+
418
+ # For EdDSA (optional)
419
+ JWT_ED25519_PRIVATE_KEY=<base64-encoded-key>
420
+ JWT_ED25519_PUBLIC_KEY=<base64-encoded-key>
421
+ ```
422
+
423
+ **CORS Configuration:**
424
+ ```bash
425
+ # Comma-separated list of allowed origins (no wildcard in production)
426
+ PDS_CORS_ORIGIN=https://app.example.com,https://admin.example.com
427
+ ```
428
+
429
+ ### Logging & Monitoring
430
+
431
+ **View logs in development:**
432
+ ```bash
433
+ wrangler tail --format=pretty
434
+ ```
435
+
436
+ **View logs in production:**
437
+ ```bash
438
+ wrangler tail --env production --format=json
439
+ ```
440
+
441
+ **Configure Logpush (production):**
442
+ 1. Set up Logpush in Cloudflare dashboard
443
+ 2. Send logs to your preferred service (Datadog, Splunk, S3, etc.)
444
+ 3. Filter by `requestId` for request tracing
445
+
446
+ **Log format:**
447
+ ```json
448
+ {
449
+ "level": "info",
450
+ "type": "request",
451
+ "requestId": "uuid",
452
+ "method": "POST",
453
+ "path": "/xrpc/com.atproto.repo.createRecord",
454
+ "status": 200,
455
+ "duration": 45,
456
+ "timestamp": "2025-10-02T22:00:00.000Z"
457
+ }
458
+ ```
459
+
460
+ ### Health Check
461
+
462
+ **Endpoint:** `GET /health`
463
+
464
+ **Response:**
465
+ ```json
466
+ {
467
+ "status": "healthy",
468
+ "timestamp": "2025-10-02T22:00:00.000Z",
469
+ "checks": {
470
+ "database": { "status": "ok" },
471
+ "storage": { "status": "ok" }
472
+ }
473
+ }
474
+ ```
475
+
476
+ Returns `503` if any dependency is unhealthy.
477
+
478
+ ### Security Best Practices
479
+
480
+ 1. **Never use wildcard CORS in production** - Set explicit origins in `PDS_CORS_ORIGIN`
481
+ 2. **Use strong secrets** - Generate cryptographically secure values for all secrets
482
+ 3. **Enable EdDSA signing** - More secure than HS256 for production
483
+ 4. **Monitor failed login attempts** - Check logs for suspicious activity
484
+ 5. **Set appropriate token TTLs** - Balance security and user experience
485
+
486
+ ### Documentation
487
+
488
+ - [`P1_IMPLEMENTATION_SUMMARY.md`](P1_IMPLEMENTATION_SUMMARY.md) - Full P1 implementation details
489
+ - [`P1.md`](P1.md) - P1 task breakdown and requirements
490
+
491
+ ## P3 Implementation - Optimization & Interoperability 🚀
492
+
493
+ This PDS now includes optimization for Cloudflare Workers and interoperability features:
494
+
495
+ ### Cloudflare Workers Optimization
496
+ - ✅ **Streaming CAR encoding** for memory efficiency (< 128MB)
497
+ - ✅ **Edge caching** for DID documents and static assets
498
+ - ✅ **Performance tests** verifying CPU and memory constraints
499
+ - ✅ **Memory-efficient operations** for large repositories
500
+
501
+ ### Blob Storage Enhancement
502
+ - ✅ **Blob quota tracking** per DID (default: 10GB)
503
+ - ✅ **Quota enforcement** on upload
504
+ - ✅ **Reference counting** for garbage collection
505
+ - ✅ **Deduplication** by content-addressed storage
506
+
507
+ ### Identity Enhancement
508
+ - ✅ **DID document generation** at `/.well-known/did.json`
509
+ - ✅ **Handle validation** and normalization
510
+ - ✅ **Service endpoints** in DID document
511
+ - ✅ **Edge caching** for identity documents
512
+
513
+ ### Interoperability Testing
514
+ - ✅ **Federation test stubs** for PDS-to-PDS sync
515
+ - ✅ **Compliance test stubs** for AT Protocol
516
+ - ✅ **Protocol version** documentation
517
+ - ✅ **Lexicon validation** framework
518
+
519
+ ### Configuration
520
+
521
+ **Blob Quota:**
522
+ ```bash
523
+ PDS_BLOB_QUOTA_BYTES=10737418240 # Default: 10GB
524
+ ```
525
+
526
+ **Caching (automatic):**
527
+ - DID documents: 1 hour TTL, 24 hour stale-while-revalidate
528
+ - Records: 1 minute TTL, 5 minute stale-while-revalidate
529
+ - Repo snapshots: 5 minute TTL, 1 hour stale-while-revalidate
530
+
531
+ ### Testing
532
+
533
+ ```bash
534
+ # Performance tests
535
+ bun test tests/performance.test.ts
536
+
537
+ # Memory tests
538
+ bun test tests/memory.test.ts
539
+
540
+ # Blob tests
541
+ bun test tests/blob.test.ts
542
+
543
+ # Identity tests
544
+ bun test tests/identity.test.ts
545
+
546
+ # Federation tests
547
+ bun test tests/federation.test.ts
548
+
549
+ # Compliance tests
550
+ bun test tests/compliance.test.ts
551
+ ```
552
+
553
+ ### Documentation
554
+
555
+ - [`P3_IMPLEMENTATION_SUMMARY.md`](P3_IMPLEMENTATION_SUMMARY.md) - Full P3 implementation details
556
+ - [`P3.md`](P3.md) - P3 task breakdown and requirements
557
+
558
+ - Used for signing all repository commits
package/index.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ import type { Env, PdsLocals } from './types/env';
3
+
4
+ export interface PdsIntegrationOptions {
5
+ debugRoutes?: boolean;
6
+ includeRootEndpoint?: boolean;
7
+ injectServerEntry?: boolean;
8
+ }
9
+
10
+ export default function alteran(options?: PdsIntegrationOptions): AstroIntegration;
11
+
12
+ export type { Env, PdsLocals };
package/index.js ADDED
@@ -0,0 +1,129 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ const CORE_ROUTES = [
5
+ { pattern: '/.well-known/atproto-did', entrypoint: './src/pages/.well-known/atproto-did.ts' },
6
+ { pattern: '/.well-known/did.json', entrypoint: './src/pages/.well-known/did.json.ts' },
7
+ { pattern: '/health', entrypoint: './src/pages/health.ts' },
8
+ { pattern: '/ready', entrypoint: './src/pages/ready.ts' },
9
+ { pattern: '/xrpc/com.atproto.identity.resolveHandle', entrypoint: './src/pages/xrpc/com.atproto.identity.resolveHandle.ts' },
10
+ { pattern: '/xrpc/com.atproto.identity.updateHandle', entrypoint: './src/pages/xrpc/com.atproto.identity.updateHandle.ts' },
11
+ { pattern: '/xrpc/com.atproto.repo.applyWrites', entrypoint: './src/pages/xrpc/com.atproto.repo.applyWrites.ts' },
12
+ { pattern: '/xrpc/com.atproto.repo.createRecord', entrypoint: './src/pages/xrpc/com.atproto.repo.createRecord.ts' },
13
+ { pattern: '/xrpc/com.atproto.repo.deleteRecord', entrypoint: './src/pages/xrpc/com.atproto.repo.deleteRecord.ts' },
14
+ { pattern: '/xrpc/com.atproto.repo.describeRepo', entrypoint: './src/pages/xrpc/com.atproto.repo.describeRepo.ts' },
15
+ { pattern: '/xrpc/com.atproto.repo.getRecord', entrypoint: './src/pages/xrpc/com.atproto.repo.getRecord.ts' },
16
+ { pattern: '/xrpc/com.atproto.repo.listRecords', entrypoint: './src/pages/xrpc/com.atproto.repo.listRecords.ts' },
17
+ { pattern: '/xrpc/com.atproto.repo.putRecord', entrypoint: './src/pages/xrpc/com.atproto.repo.putRecord.ts' },
18
+ { pattern: '/xrpc/com.atproto.repo.uploadBlob', entrypoint: './src/pages/xrpc/com.atproto.repo.uploadBlob.ts' },
19
+ { pattern: '/xrpc/com.atproto.server.createSession', entrypoint: './src/pages/xrpc/com.atproto.server.createSession.ts' },
20
+ { pattern: '/xrpc/com.atproto.server.deleteSession', entrypoint: './src/pages/xrpc/com.atproto.server.deleteSession.ts' },
21
+ { pattern: '/xrpc/com.atproto.server.describeServer', entrypoint: './src/pages/xrpc/com.atproto.server.describeServer.ts' },
22
+ { pattern: '/xrpc/com.atproto.server.getSession', entrypoint: './src/pages/xrpc/com.atproto.server.getSession.ts' },
23
+ { pattern: '/xrpc/com.atproto.server.refreshSession', entrypoint: './src/pages/xrpc/com.atproto.server.refreshSession.ts' },
24
+ { pattern: '/xrpc/com.atproto.sync.getBlocks', entrypoint: './src/pages/xrpc/com.atproto.sync.getBlocks.ts' },
25
+ { pattern: '/xrpc/com.atproto.sync.getBlocks.json', entrypoint: './src/pages/xrpc/com.atproto.sync.getBlocks.json.ts' },
26
+ { pattern: '/xrpc/com.atproto.sync.getCheckout', entrypoint: './src/pages/xrpc/com.atproto.sync.getCheckout.ts' },
27
+ { pattern: '/xrpc/com.atproto.sync.getCheckout.json', entrypoint: './src/pages/xrpc/com.atproto.sync.getCheckout.json.ts' },
28
+ { pattern: '/xrpc/com.atproto.sync.getHead', entrypoint: './src/pages/xrpc/com.atproto.sync.getHead.ts' },
29
+ { pattern: '/xrpc/com.atproto.sync.getLatestCommit', entrypoint: './src/pages/xrpc/com.atproto.sync.getLatestCommit.ts' },
30
+ { pattern: '/xrpc/com.atproto.sync.getRecord', entrypoint: './src/pages/xrpc/com.atproto.sync.getRecord.ts' },
31
+ { pattern: '/xrpc/com.atproto.sync.getRepo', entrypoint: './src/pages/xrpc/com.atproto.sync.getRepo.ts' },
32
+ { pattern: '/xrpc/com.atproto.sync.getRepo.json', entrypoint: './src/pages/xrpc/com.atproto.sync.getRepo.json.ts' },
33
+ { pattern: '/xrpc/com.atproto.sync.getRepo.range', entrypoint: './src/pages/xrpc/com.atproto.sync.getRepo.range.ts' },
34
+ { pattern: '/xrpc/com.atproto.sync.listBlobs', entrypoint: './src/pages/xrpc/com.atproto.sync.listBlobs.ts' },
35
+ { pattern: '/xrpc/com.atproto.sync.listRepos', entrypoint: './src/pages/xrpc/com.atproto.sync.listRepos.ts' },
36
+ ];
37
+
38
+ const ROOT_ROUTE = {
39
+ pattern: '/',
40
+ entrypoint: './src/pages/index.ts',
41
+ };
42
+
43
+ const DEBUG_ROUTES = [
44
+ { pattern: '/debug/blob/[...key]', entrypoint: './src/pages/debug/blob/[...key].ts' },
45
+ { pattern: '/debug/db/bootstrap', entrypoint: './src/pages/debug/db/bootstrap.ts' },
46
+ { pattern: '/debug/db/commits', entrypoint: './src/pages/debug/db/commits.ts' },
47
+ { pattern: '/debug/gc/blobs', entrypoint: './src/pages/debug/gc/blobs.ts' },
48
+ { pattern: '/debug/record', entrypoint: './src/pages/debug/record.ts' },
49
+ ];
50
+
51
+ const pkgRoot = new URL('.', import.meta.url);
52
+
53
+ const resolvePackagePath = (relative) => fileURLToPath(new URL(relative, pkgRoot));
54
+
55
+ export default function alteran(options = {}) {
56
+ const {
57
+ debugRoutes = false,
58
+ includeRootEndpoint = false,
59
+ injectServerEntry = true,
60
+ } = options;
61
+
62
+ const aliasTarget = resolvePackagePath('./src');
63
+ const middlewareEntrypoint = resolvePackagePath('./src/middleware.ts');
64
+ const serverEntrypoint = resolvePackagePath('./src/_worker.ts');
65
+
66
+ const routes = CORE_ROUTES.slice();
67
+ if (includeRootEndpoint) {
68
+ routes.unshift(ROOT_ROUTE);
69
+ }
70
+ if (debugRoutes) {
71
+ routes.push(...DEBUG_ROUTES);
72
+ }
73
+
74
+ return {
75
+ name: 'alteran',
76
+ hooks: {
77
+ 'astro:config:setup'({ config, updateConfig, addMiddleware, injectRoute, logger }) {
78
+ if (config.output !== 'server') {
79
+ updateConfig({ output: 'server' });
80
+ }
81
+
82
+ updateConfig({
83
+ vite: {
84
+ resolve: {
85
+ alias: {
86
+ '@alteran': aliasTarget,
87
+ },
88
+ },
89
+ },
90
+ });
91
+
92
+ if (injectServerEntry) {
93
+ if (config.build?.serverEntry && config.build.serverEntry !== serverEntrypoint) {
94
+ logger.info(
95
+ '[alteran] Overriding existing build.serverEntry with the packaged worker entry. Pass { injectServerEntry: false } to opt out.'
96
+ );
97
+ }
98
+ updateConfig({
99
+ build: {
100
+ serverEntry: serverEntrypoint,
101
+ },
102
+ });
103
+ }
104
+
105
+ addMiddleware({
106
+ entrypoint: middlewareEntrypoint,
107
+ order: 'pre',
108
+ });
109
+
110
+ for (const route of routes) {
111
+ injectRoute({ pattern: route.pattern, entrypoint: resolvePackagePath(route.entrypoint) });
112
+ }
113
+ },
114
+
115
+ 'astro:config:done'({ config, injectTypes, logger }) {
116
+ const envTypesPath = resolvePackagePath('./types/env.d.ts');
117
+ const envTypes = readFileSync(envTypesPath, 'utf-8');
118
+ injectTypes({ filename: 'astro-cloudflare-pds.d.ts', content: envTypes });
119
+
120
+ const adapterName = config.adapter?.name ?? 'unknown adapter';
121
+ if (!adapterName.toLowerCase().includes('cloudflare')) {
122
+ logger.warn(
123
+ `[alteran] Expected a Cloudflare adapter. Found "${adapterName}". The PDS worker relies on Cloudflare runtime bindings.`
124
+ );
125
+ }
126
+ },
127
+ },
128
+ };
129
+ }