A minimal AT Protocol Personal Data Server written in JavaScript.

feat: add Deno runtime support

Add Deno as a supported platform alongside Node.js and Cloudflare Workers.

New packages:
- @pds/deno: HTTP server adapter using Deno.serve() and Deno.upgradeWebSocket()
- @pds/blobs-deno: Filesystem blob storage using Deno.readFile/writeFile

Changes:
- Abstract SQLite driver interface for better-sqlite3 and node:sqlite compatibility
- Align Node.js and Deno APIs: both use async createServer() with listen()/close()
- Remove createServerFromEnv convenience wrapper for explicit configuration
- Add Deno e2e tests (npm run test:e2e:deno)

Usage:
import { createServer } from '@pds/deno';
const { listen, close } = await createServer({ dbPath, blobsDir, jwtSecret });
await listen();

Requires Deno 2.2+ for node:sqlite support.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+1315 -30
+28
README.md
··· 20 20 21 21 **Platform options:** 22 22 - **Node.js** - Simple setup, filesystem storage, ideal for self-hosting 23 + - **Deno** - Modern runtime with built-in TypeScript, uses node:sqlite 23 24 - **Cloudflare Workers** - Edge deployment with Durable Objects and R2 24 25 25 26 ## Quick Start (Node.js) ··· 114 115 npm run setup -- --pds https://your-worker.workers.dev 115 116 ``` 116 117 118 + ## Deploy: Deno 119 + 120 + Requires Deno 2.2+ for `node:sqlite` support. 121 + 122 + ```bash 123 + cd examples/deno 124 + deno run --allow-net --allow-read --allow-write --allow-env main.ts 125 + ``` 126 + 127 + See [examples/deno/README.md](examples/deno/README.md) for configuration options. 128 + 117 129 ## Testing 118 130 119 131 ```bash 120 132 npm test # Unit tests 121 133 npm run test:e2e:node # E2E against Node.js 134 + npm run test:e2e:deno # E2E against Deno 122 135 npm run test:e2e:cloudflare # E2E against Cloudflare 123 136 npm run test:coverage # Coverage report 124 137 ``` ··· 163 176 |---------|-------------| 164 177 | @pds/core | Platform-agnostic business logic and XRPC handlers | 165 178 | @pds/node | Node.js HTTP server with WebSocket support | 179 + | @pds/deno | Deno HTTP server with WebSocket support | 166 180 | @pds/cloudflare | Cloudflare Workers entry point with Durable Objects | 167 181 | @pds/storage-sqlite | SQLite storage adapter (better-sqlite3 or node:sqlite) | 168 182 | @pds/blobs-fs | Filesystem blob storage for Node.js | 183 + | @pds/blobs-deno | Filesystem blob storage for Deno | 169 184 | @pds/blobs-s3 | S3-compatible blob storage | 170 185 171 186 **Node.js usage:** ··· 189 204 export { PDSDurableObject } from '@pds/cloudflare' 190 205 ``` 191 206 207 + **Deno usage:** 208 + ```typescript 209 + import { createServer } from '@pds/deno' 210 + 211 + const server = createServer({ 212 + dbPath: './pds.db', 213 + blobsDir: './blobs', 214 + jwtSecret: Deno.env.get('JWT_SECRET'), 215 + }) 216 + ``` 217 + 192 218 ## Contributing 193 219 194 220 **Before submitting a PR:** ··· 211 237 packages/ 212 238 core/ # Platform-agnostic core 213 239 node/ # Node.js adapter 240 + deno/ # Deno adapter 214 241 cloudflare/ # Cloudflare adapter 215 242 storage-*/ # Storage implementations 216 243 blobs-*/ # Blob storage implementations 217 244 examples/ 218 245 node/ # Node.js example 246 + deno/ # Deno example 219 247 cloudflare/ # Cloudflare example 220 248 ```
+703
docs/plans/2026-01-12-deno-support.md
··· 1 + # Deno Support Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Add Deno runtime support to pds.js by abstracting the SQLite driver and creating a Deno HTTP/WebSocket adapter. 6 + 7 + **Architecture:** Create a minimal SQLite driver interface that both `better-sqlite3` (Node) and `node:sqlite` (Deno 2.2+) implement natively. Add a new `@pds/deno` package for the HTTP server adapter using `Deno.serve()`. Reuse `@pds/storage-sqlite` unchanged except for type annotations. 8 + 9 + **Tech Stack:** Deno 2.2+, node:sqlite, Deno.serve(), TypeScript/JSDoc 10 + 11 + --- 12 + 13 + ## Task 1: Add SQLite Driver Interface Types 14 + 15 + **Files:** 16 + - Create: `packages/storage-sqlite/driver.js` 17 + 18 + **Step 1: Create the driver interface file** 19 + 20 + ```javascript 21 + // @pds/storage-sqlite/driver - SQLite driver interface types 22 + // Both better-sqlite3 and node:sqlite implement this interface natively 23 + 24 + /** 25 + * @typedef {Object} SQLiteStatement 26 + * @property {(...args: unknown[]) => unknown} get - Single row query 27 + * @property {(...args: unknown[]) => unknown[]} all - Multi-row query 28 + * @property {(...args: unknown[]) => {changes: number}} run - Execute INSERT/UPDATE/DELETE 29 + */ 30 + 31 + /** 32 + * @typedef {Object} SQLiteDatabase 33 + * @property {(sql: string) => void} exec - Execute raw SQL (DDL, multiple statements) 34 + * @property {(sql: string) => SQLiteStatement} prepare - Create prepared statement 35 + */ 36 + 37 + export {}; 38 + ``` 39 + 40 + **Step 2: Verify file created** 41 + 42 + Run: `cat packages/storage-sqlite/driver.js` 43 + Expected: File contents shown 44 + 45 + **Step 3: Commit** 46 + 47 + ```bash 48 + git add packages/storage-sqlite/driver.js 49 + git commit -m "feat(storage-sqlite): add SQLite driver interface types" 50 + ``` 51 + 52 + --- 53 + 54 + ## Task 2: Update Storage Functions to Use Driver Interface 55 + 56 + **Files:** 57 + - Modify: `packages/storage-sqlite/index.js` (lines 24-28 and 314-318) 58 + 59 + **Step 1: Update createActorStorage type annotation** 60 + 61 + Change line 28 from: 62 + ```javascript 63 + * @param {import('better-sqlite3').Database} db 64 + ``` 65 + to: 66 + ```javascript 67 + * @param {import('./driver.js').SQLiteDatabase} db 68 + ``` 69 + 70 + **Step 2: Update createSharedStorage type annotation** 71 + 72 + Change line 318 from: 73 + ```javascript 74 + * @param {import('better-sqlite3').Database} db 75 + ``` 76 + to: 77 + ```javascript 78 + * @param {import('./driver.js').SQLiteDatabase} db 79 + ``` 80 + 81 + **Step 3: Run existing tests to verify no breakage** 82 + 83 + Run: `pnpm test` 84 + Expected: All tests pass 85 + 86 + **Step 4: Commit** 87 + 88 + ```bash 89 + git add packages/storage-sqlite/index.js 90 + git commit -m "feat(storage-sqlite): use driver interface instead of better-sqlite3 types" 91 + ``` 92 + 93 + --- 94 + 95 + ## Task 3: Export Driver Types from Package 96 + 97 + **Files:** 98 + - Modify: `packages/storage-sqlite/package.json` 99 + 100 + **Step 1: Add driver.js to exports** 101 + 102 + Add to the exports field: 103 + ```json 104 + { 105 + "exports": { 106 + ".": "./index.js", 107 + "./driver": "./driver.js" 108 + } 109 + } 110 + ``` 111 + 112 + **Step 2: Verify package.json is valid** 113 + 114 + Run: `node -e "console.log(JSON.parse(require('fs').readFileSync('packages/storage-sqlite/package.json')))"` 115 + Expected: No JSON parse errors 116 + 117 + **Step 3: Commit** 118 + 119 + ```bash 120 + git add packages/storage-sqlite/package.json 121 + git commit -m "feat(storage-sqlite): export driver interface types" 122 + ``` 123 + 124 + --- 125 + 126 + ## Task 4: Create @pds/deno Package Structure 127 + 128 + **Files:** 129 + - Create: `packages/deno/package.json` 130 + - Create: `packages/deno/deno.json` 131 + 132 + **Step 1: Create package.json** 133 + 134 + ```json 135 + { 136 + "name": "@pds/deno", 137 + "version": "0.7.0", 138 + "type": "module", 139 + "exports": { 140 + ".": "./index.js" 141 + }, 142 + "dependencies": { 143 + "@pds/core": "workspace:*" 144 + } 145 + } 146 + ``` 147 + 148 + **Step 2: Create deno.json** 149 + 150 + ```json 151 + { 152 + "name": "@pds/deno", 153 + "version": "0.7.0", 154 + "exports": "./index.js", 155 + "imports": { 156 + "@pds/core": "../core/index.js", 157 + "@pds/core/": "../core/" 158 + } 159 + } 160 + ``` 161 + 162 + **Step 3: Commit** 163 + 164 + ```bash 165 + git add packages/deno/package.json packages/deno/deno.json 166 + git commit -m "feat(deno): create package structure" 167 + ``` 168 + 169 + --- 170 + 171 + ## Task 5: Implement Deno WebSocket Port 172 + 173 + **Files:** 174 + - Create: `packages/deno/index.js` 175 + 176 + **Step 1: Create the WebSocket port implementation** 177 + 178 + ```javascript 179 + // @pds/deno - Deno HTTP server adapter 180 + 181 + /** 182 + * Create WebSocket port for Deno 183 + * @param {Map<Request, {socket: WebSocket, response: Response}>} upgradeMap 184 + * @returns {import('@pds/core/ports.js').WebSocketPort} 185 + */ 186 + export function createWebSocketPort(upgradeMap) { 187 + /** @type {Set<WebSocket>} */ 188 + const clients = new Set(); 189 + 190 + return { 191 + isUpgrade(request) { 192 + return request.headers.get('upgrade')?.toLowerCase() === 'websocket'; 193 + }, 194 + 195 + upgrade(request, onConnect) { 196 + const upgrade = Deno.upgradeWebSocket(request); 197 + const { socket, response } = upgrade; 198 + 199 + clients.add(socket); 200 + socket.onclose = () => clients.delete(socket); 201 + 202 + socket.onopen = () => { 203 + onConnect({ 204 + send(data) { 205 + if (socket.readyState === WebSocket.OPEN) { 206 + socket.send(data); 207 + } 208 + }, 209 + close() { 210 + socket.close(); 211 + }, 212 + }); 213 + }; 214 + 215 + return response; 216 + }, 217 + 218 + broadcast(data) { 219 + for (const client of clients) { 220 + if (client.readyState === WebSocket.OPEN) { 221 + try { 222 + client.send(data); 223 + } catch { 224 + // Client may have disconnected 225 + } 226 + } 227 + } 228 + }, 229 + }; 230 + } 231 + ``` 232 + 233 + **Step 2: Commit** 234 + 235 + ```bash 236 + git add packages/deno/index.js 237 + git commit -m "feat(deno): implement WebSocket port" 238 + ``` 239 + 240 + --- 241 + 242 + ## Task 6: Implement Deno HTTP Server 243 + 244 + **Files:** 245 + - Modify: `packages/deno/index.js` 246 + 247 + **Step 1: Add createServer function** 248 + 249 + Append to `packages/deno/index.js`: 250 + 251 + ```javascript 252 + import { PersonalDataServer } from '@pds/core'; 253 + 254 + /** 255 + * @typedef {Object} DenoServerOptions 256 + * @property {import('@pds/core/ports.js').ActorStoragePort} actorStorage 257 + * @property {import('@pds/core/ports.js').SharedStoragePort} sharedStorage 258 + * @property {import('@pds/core/ports.js').BlobPort} blobs 259 + * @property {string} jwtSecret 260 + * @property {number} [port=3000] 261 + * @property {string} [hostname] 262 + * @property {string} [appviewUrl] 263 + * @property {string} [appviewDid] 264 + * @property {string} [relayUrl] 265 + * @property {string} [password] 266 + */ 267 + 268 + /** 269 + * @typedef {Object} DenoServer 270 + * @property {PersonalDataServer} pds 271 + * @property {Deno.HttpServer} server 272 + * @property {() => void} close 273 + */ 274 + 275 + /** 276 + * Create Deno PDS server 277 + * @param {DenoServerOptions} options 278 + * @returns {DenoServer} 279 + */ 280 + export function createServer({ 281 + actorStorage, 282 + sharedStorage, 283 + blobs, 284 + jwtSecret, 285 + port = 3000, 286 + hostname, 287 + appviewUrl, 288 + appviewDid, 289 + relayUrl, 290 + password, 291 + }) { 292 + /** @type {Map<Request, {socket: WebSocket, response: Response}>} */ 293 + const upgradeMap = new Map(); 294 + const webSocket = createWebSocketPort(upgradeMap); 295 + 296 + const pds = new PersonalDataServer({ 297 + actorStorage, 298 + sharedStorage, 299 + blobs, 300 + webSocket, 301 + jwtSecret, 302 + hostname, 303 + appviewUrl, 304 + appviewDid, 305 + relayUrl, 306 + password, 307 + }); 308 + 309 + const server = Deno.serve({ port }, async (request) => { 310 + // Handle WebSocket upgrade 311 + if (webSocket.isUpgrade(request)) { 312 + return pds.fetch(request); 313 + } 314 + 315 + // Handle regular HTTP 316 + return pds.fetch(request); 317 + }); 318 + 319 + // Cleanup interval (every hour) 320 + const cleanupInterval = setInterval( 321 + async () => { 322 + await pds.runBlobCleanup(); 323 + await sharedStorage.cleanupExpiredDpopJtis(); 324 + }, 325 + 60 * 60 * 1000, 326 + ); 327 + 328 + return { 329 + pds, 330 + server, 331 + port, 332 + close() { 333 + clearInterval(cleanupInterval); 334 + server.shutdown(); 335 + }, 336 + }; 337 + } 338 + 339 + /** 340 + * Create server from environment variables 341 + * @param {Object} deps - Storage dependencies 342 + * @param {import('@pds/core/ports.js').ActorStoragePort} deps.actorStorage 343 + * @param {import('@pds/core/ports.js').SharedStoragePort} deps.sharedStorage 344 + * @param {import('@pds/core/ports.js').BlobPort} deps.blobs 345 + * @returns {DenoServer} 346 + */ 347 + export function createServerFromEnv({ actorStorage, sharedStorage, blobs }) { 348 + return createServer({ 349 + actorStorage, 350 + sharedStorage, 351 + blobs, 352 + jwtSecret: Deno.env.get('JWT_SECRET') || 'development-secret', 353 + port: parseInt(Deno.env.get('PORT') || '3000', 10), 354 + hostname: Deno.env.get('HOSTNAME'), 355 + appviewUrl: Deno.env.get('APPVIEW_URL'), 356 + appviewDid: Deno.env.get('APPVIEW_DID'), 357 + relayUrl: Deno.env.get('RELAY_URL'), 358 + password: Deno.env.get('PDS_PASSWORD'), 359 + }); 360 + } 361 + ``` 362 + 363 + **Step 2: Run type check** 364 + 365 + Run: `pnpm exec tsc --noEmit` 366 + Expected: No type errors 367 + 368 + **Step 3: Commit** 369 + 370 + ```bash 371 + git add packages/deno/index.js 372 + git commit -m "feat(deno): implement HTTP server with createServer" 373 + ``` 374 + 375 + --- 376 + 377 + ## Task 7: Create Deno Example 378 + 379 + **Files:** 380 + - Create: `examples/deno/main.ts` 381 + - Create: `examples/deno/deno.json` 382 + 383 + **Step 1: Create deno.json** 384 + 385 + ```json 386 + { 387 + "imports": { 388 + "@pds/core": "../../packages/core/index.js", 389 + "@pds/core/": "../../packages/core/", 390 + "@pds/deno": "../../packages/deno/index.js", 391 + "@pds/storage-sqlite": "../../packages/storage-sqlite/index.js", 392 + "@pds/blobs-s3": "../../packages/blobs-s3/index.js" 393 + } 394 + } 395 + ``` 396 + 397 + **Step 2: Create main.ts** 398 + 399 + ```typescript 400 + import { DatabaseSync } from 'node:sqlite'; 401 + import { createActorStorage, createSharedStorage } from '@pds/storage-sqlite'; 402 + import { createServer } from '@pds/deno'; 403 + 404 + // Initialize SQLite database 405 + const dbPath = Deno.env.get('PDS_DB_PATH') || './pds.db'; 406 + const db = new DatabaseSync(dbPath); 407 + 408 + // Create storage adapters 409 + const actorStorage = createActorStorage(db); 410 + const sharedStorage = createSharedStorage(db); 411 + 412 + // For blobs, use S3 or implement Deno filesystem adapter 413 + // This example uses a minimal in-memory blob store for demo purposes 414 + const blobs = { 415 + async get(_did: string, _cid: string) { 416 + return null; // TODO: implement with Deno.readFile or S3 417 + }, 418 + async put(_did: string, _cid: string, _data: Uint8Array, _mimeType: string) { 419 + // TODO: implement 420 + }, 421 + async delete(_did: string, _cid: string) { 422 + // TODO: implement 423 + }, 424 + }; 425 + 426 + // Create and start server 427 + const server = createServer({ 428 + actorStorage, 429 + sharedStorage, 430 + blobs, 431 + jwtSecret: Deno.env.get('JWT_SECRET') || 'development-secret', 432 + port: parseInt(Deno.env.get('PORT') || '3000', 10), 433 + hostname: Deno.env.get('HOSTNAME'), 434 + appviewUrl: Deno.env.get('APPVIEW_URL'), 435 + appviewDid: Deno.env.get('APPVIEW_DID'), 436 + relayUrl: Deno.env.get('RELAY_URL'), 437 + password: Deno.env.get('PDS_PASSWORD'), 438 + }); 439 + 440 + console.log(`PDS running on http://localhost:${server.port}`); 441 + 442 + // Handle shutdown 443 + Deno.addSignalListener('SIGINT', () => { 444 + console.log('Shutting down...'); 445 + server.close(); 446 + db.close(); 447 + Deno.exit(0); 448 + }); 449 + ``` 450 + 451 + **Step 3: Commit** 452 + 453 + ```bash 454 + git add examples/deno/main.ts examples/deno/deno.json 455 + git commit -m "feat(examples): add Deno example" 456 + ``` 457 + 458 + --- 459 + 460 + ## Task 8: Add Deno Filesystem Blobs (Optional) 461 + 462 + **Files:** 463 + - Create: `packages/blobs-deno/index.js` 464 + - Create: `packages/blobs-deno/package.json` 465 + - Create: `packages/blobs-deno/deno.json` 466 + 467 + **Step 1: Create package.json** 468 + 469 + ```json 470 + { 471 + "name": "@pds/blobs-deno", 472 + "version": "0.7.0", 473 + "type": "module", 474 + "exports": { 475 + ".": "./index.js" 476 + } 477 + } 478 + ``` 479 + 480 + **Step 2: Create deno.json** 481 + 482 + ```json 483 + { 484 + "name": "@pds/blobs-deno", 485 + "version": "0.7.0", 486 + "exports": "./index.js" 487 + } 488 + ``` 489 + 490 + **Step 3: Create index.js** 491 + 492 + ```javascript 493 + // @pds/blobs-deno - Deno filesystem blob storage 494 + 495 + import { join, dirname } from 'node:path'; 496 + 497 + /** 498 + * Create filesystem blob storage for Deno 499 + * @param {string} baseDir - Base directory for blob storage 500 + * @returns {import('@pds/core/ports.js').BlobPort} 501 + */ 502 + export function createDenoBlobs(baseDir) { 503 + /** 504 + * Get blob file path 505 + * @param {string} did 506 + * @param {string} cid 507 + */ 508 + function blobPath(did, cid) { 509 + // Use first 2 chars of CID for sharding 510 + const shard = cid.slice(0, 2); 511 + return join(baseDir, did, shard, cid); 512 + } 513 + 514 + return { 515 + async get(did, cid) { 516 + try { 517 + const path = blobPath(did, cid); 518 + const data = await Deno.readFile(path); 519 + const metaPath = `${path}.meta`; 520 + const meta = JSON.parse(await Deno.readTextFile(metaPath)); 521 + return { data, mimeType: meta.mimeType }; 522 + } catch (e) { 523 + if (e instanceof Deno.errors.NotFound) { 524 + return null; 525 + } 526 + throw e; 527 + } 528 + }, 529 + 530 + async put(did, cid, data, mimeType) { 531 + const path = blobPath(did, cid); 532 + await Deno.mkdir(dirname(path), { recursive: true }); 533 + await Deno.writeFile(path, data); 534 + await Deno.writeTextFile(`${path}.meta`, JSON.stringify({ mimeType })); 535 + }, 536 + 537 + async delete(did, cid) { 538 + try { 539 + const path = blobPath(did, cid); 540 + await Deno.remove(path); 541 + await Deno.remove(`${path}.meta`); 542 + } catch (e) { 543 + if (!(e instanceof Deno.errors.NotFound)) { 544 + throw e; 545 + } 546 + } 547 + }, 548 + }; 549 + } 550 + ``` 551 + 552 + **Step 4: Commit** 553 + 554 + ```bash 555 + git add packages/blobs-deno/ 556 + git commit -m "feat(blobs-deno): add Deno filesystem blob storage" 557 + ``` 558 + 559 + --- 560 + 561 + ## Task 9: Update Deno Example to Use Blobs 562 + 563 + **Files:** 564 + - Modify: `examples/deno/main.ts` 565 + - Modify: `examples/deno/deno.json` 566 + 567 + **Step 1: Update deno.json imports** 568 + 569 + Add to imports: 570 + ```json 571 + "@pds/blobs-deno": "../../packages/blobs-deno/index.js" 572 + ``` 573 + 574 + **Step 2: Update main.ts to use blobs-deno** 575 + 576 + Replace the placeholder blobs with: 577 + 578 + ```typescript 579 + import { createDenoBlobs } from '@pds/blobs-deno'; 580 + 581 + const blobsDir = Deno.env.get('PDS_BLOBS_DIR') || './blobs'; 582 + const blobs = createDenoBlobs(blobsDir); 583 + ``` 584 + 585 + **Step 3: Commit** 586 + 587 + ```bash 588 + git add examples/deno/main.ts examples/deno/deno.json 589 + git commit -m "feat(examples): use blobs-deno in Deno example" 590 + ``` 591 + 592 + --- 593 + 594 + ## Task 10: Add README for Deno Example 595 + 596 + **Files:** 597 + - Create: `examples/deno/README.md` 598 + 599 + **Step 1: Create README** 600 + 601 + ```markdown 602 + # Deno PDS Example 603 + 604 + Minimal PDS server running on Deno. 605 + 606 + ## Requirements 607 + 608 + - Deno 2.2+ (for `node:sqlite` support) 609 + 610 + ## Quick Start 611 + 612 + ```bash 613 + cd examples/deno 614 + deno run --allow-net --allow-read --allow-write --allow-env main.ts 615 + ``` 616 + 617 + ## Environment Variables 618 + 619 + | Variable | Description | Default | 620 + |----------|-------------|---------| 621 + | `PDS_DB_PATH` | SQLite database path | `./pds.db` | 622 + | `PDS_BLOBS_DIR` | Blob storage directory | `./blobs` | 623 + | `JWT_SECRET` | JWT signing secret | `development-secret` | 624 + | `PORT` | Server port | `3000` | 625 + | `HOSTNAME` | PDS hostname | - | 626 + | `APPVIEW_URL` | AppView URL for proxying | - | 627 + | `APPVIEW_DID` | AppView DID for service auth | - | 628 + | `RELAY_URL` | Relay URL for firehose | - | 629 + | `PDS_PASSWORD` | Password for createSession | - | 630 + 631 + ## Permissions 632 + 633 + The server requires these Deno permissions: 634 + 635 + - `--allow-net` - HTTP server and outbound requests 636 + - `--allow-read` - Read database and blob files 637 + - `--allow-write` - Write database and blob files 638 + - `--allow-env` - Read environment variables 639 + ``` 640 + 641 + **Step 2: Commit** 642 + 643 + ```bash 644 + git add examples/deno/README.md 645 + git commit -m "docs(examples): add Deno example README" 646 + ``` 647 + 648 + --- 649 + 650 + ## Task 11: Test Deno Example Manually 651 + 652 + **Step 1: Run the Deno example** 653 + 654 + ```bash 655 + cd examples/deno 656 + deno run --allow-net --allow-read --allow-write --allow-env main.ts 657 + ``` 658 + 659 + Expected: Server starts, prints "PDS running on http://localhost:3000" 660 + 661 + **Step 2: Test health endpoint** 662 + 663 + ```bash 664 + curl http://localhost:3000/xrpc/_health 665 + ``` 666 + 667 + Expected: `{"version":"..."}` 668 + 669 + **Step 3: Test describe server** 670 + 671 + ```bash 672 + curl http://localhost:3000/xrpc/com.atproto.server.describeServer 673 + ``` 674 + 675 + Expected: JSON response with server description 676 + 677 + --- 678 + 679 + ## Task 12: Update Root README 680 + 681 + **Files:** 682 + - Modify: `README.md` 683 + 684 + **Step 1: Add Deno to supported platforms** 685 + 686 + Add to the platforms section: 687 + ```markdown 688 + ### Deno 689 + 690 + ```bash 691 + cd examples/deno 692 + deno run --allow-net --allow-read --allow-write --allow-env main.ts 693 + ``` 694 + 695 + Requires Deno 2.2+ for `node:sqlite` support. 696 + ``` 697 + 698 + **Step 2: Commit** 699 + 700 + ```bash 701 + git add README.md 702 + git commit -m "docs: add Deno to supported platforms" 703 + ```
+37
examples/deno/README.md
··· 1 + # Deno PDS Example 2 + 3 + Minimal PDS server running on Deno. 4 + 5 + ## Requirements 6 + 7 + - Deno 2.2+ (for `node:sqlite` support) 8 + 9 + ## Quick Start 10 + 11 + ```bash 12 + cd examples/deno 13 + deno run --allow-net --allow-read --allow-write --allow-env main.ts 14 + ``` 15 + 16 + ## Environment Variables 17 + 18 + | Variable | Description | Default | 19 + |----------|-------------|---------| 20 + | `PDS_DB_PATH` | SQLite database path | `./pds.db` | 21 + | `PDS_BLOBS_DIR` | Blob storage directory | `./blobs` | 22 + | `JWT_SECRET` | JWT signing secret | `development-secret` | 23 + | `PORT` | Server port | `3000` | 24 + | `HOSTNAME` | PDS hostname | - | 25 + | `APPVIEW_URL` | AppView URL for proxying | - | 26 + | `APPVIEW_DID` | AppView DID for service auth | - | 27 + | `RELAY_URL` | Relay URL for firehose | - | 28 + | `PDS_PASSWORD` | Password for createSession | - | 29 + 30 + ## Permissions 31 + 32 + The server requires these Deno permissions: 33 + 34 + - `--allow-net` - HTTP server and outbound requests 35 + - `--allow-read` - Read database and blob files 36 + - `--allow-write` - Write database and blob files 37 + - `--allow-env` - Read environment variables
+9
examples/deno/deno.json
··· 1 + { 2 + "imports": { 3 + "@pds/deno": "../../packages/deno/index.js", 4 + "@pds/core": "../../packages/core/index.js", 5 + "@pds/core/": "../../packages/core/", 6 + "@pds/storage-sqlite": "../../packages/storage-sqlite/index.js", 7 + "@pds/blobs-deno": "../../packages/blobs-deno/index.js" 8 + } 9 + }
+25
examples/deno/main.ts
··· 1 + import { createServer } from '@pds/deno'; 2 + 3 + const { listen, close } = await createServer({ 4 + dbPath: Deno.env.get('PDS_DB_PATH') || './pds.db', 5 + blobsDir: Deno.env.get('PDS_BLOBS_DIR') || './blobs', 6 + jwtSecret: Deno.env.get('JWT_SECRET') || 'development-secret', 7 + port: parseInt(Deno.env.get('PORT') || '3000', 10), 8 + hostname: Deno.env.get('HOSTNAME'), 9 + appviewUrl: Deno.env.get('APPVIEW_URL'), 10 + appviewDid: Deno.env.get('APPVIEW_DID'), 11 + relayUrl: Deno.env.get('RELAY_URL'), 12 + password: Deno.env.get('PDS_PASSWORD'), 13 + }); 14 + 15 + await listen(); 16 + 17 + // Handle shutdown (SIGINT for Ctrl+C, SIGTERM for Docker/K8s) 18 + async function shutdown() { 19 + console.log('Shutting down...'); 20 + await close(); 21 + Deno.exit(0); 22 + } 23 + 24 + Deno.addSignalListener('SIGINT', shutdown); 25 + Deno.addSignalListener('SIGTERM', shutdown);
+8
examples/deno/package.json
··· 1 + { 2 + "name": "@pds/example-deno", 3 + "private": true, 4 + "type": "module", 5 + "devDependencies": { 6 + "@types/deno": "^2.5.0" 7 + } 8 + }
+23
examples/deno/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ES2022", 5 + "moduleResolution": "bundler", 6 + "strict": true, 7 + "noEmit": true, 8 + "skipLibCheck": true, 9 + "allowJs": true, 10 + "checkJs": false, 11 + "types": ["@types/deno", "node"], 12 + "baseUrl": ".", 13 + "paths": { 14 + "@pds/core": ["../../packages/core/index.js"], 15 + "@pds/core/*": ["../../packages/core/*"], 16 + "@pds/deno": ["../../packages/deno/index.js"], 17 + "@pds/storage-sqlite": ["../../packages/storage-sqlite/index.js"], 18 + "@pds/blobs-deno": ["../../packages/blobs-deno/index.js"] 19 + } 20 + }, 21 + "include": ["./**/*.ts"], 22 + "exclude": [] 23 + }
+13 -2
examples/node/index.js
··· 1 1 // Example Node.js PDS server 2 - import { createServerFromEnv } from '@pds/node'; 2 + import { createServer } from '@pds/node'; 3 + 4 + const { listen } = await createServer({ 5 + dbPath: process.env.PDS_DB_PATH || './pds.db', 6 + blobsDir: process.env.PDS_BLOBS_DIR || './blobs', 7 + jwtSecret: process.env.JWT_SECRET || 'development-secret', 8 + port: parseInt(process.env.PORT || '3000', 10), 9 + hostname: process.env.HOSTNAME, 10 + appviewUrl: process.env.APPVIEW_URL, 11 + appviewDid: process.env.APPVIEW_DID, 12 + relayUrl: process.env.RELAY_URL, 13 + password: process.env.PDS_PASSWORD, 14 + }); 3 15 4 - const { listen } = await createServerFromEnv(); 5 16 await listen();
+1
package.json
··· 15 15 "test:watch": "vitest", 16 16 "test:coverage": "vitest run --coverage", 17 17 "test:e2e:node": "PLATFORM=node vitest run test/e2e.test.js", 18 + "test:e2e:deno": "PLATFORM=deno vitest run test/e2e.test.js", 18 19 "test:e2e:cloudflare": "PLATFORM=cloudflare vitest run test/e2e.test.js", 19 20 "setup": "node scripts/setup.js", 20 21 "format": "biome format --write .",
+5
packages/blobs-deno/deno.json
··· 1 + { 2 + "name": "@pds/blobs-deno", 3 + "version": "0.7.0", 4 + "exports": "./index.js" 5 + }
+58
packages/blobs-deno/index.js
··· 1 + // @pds/blobs-deno - Deno filesystem blob storage 2 + /// <reference types="@types/deno" /> 3 + 4 + import { join, dirname } from 'node:path'; 5 + 6 + /** 7 + * Create filesystem blob storage for Deno 8 + * @param {string} baseDir - Base directory for blob storage 9 + * @returns {import('@pds/core/ports.js').BlobPort} 10 + */ 11 + export function createDenoBlobs(baseDir) { 12 + /** 13 + * Get blob file path 14 + * @param {string} did 15 + * @param {string} cid 16 + */ 17 + function blobPath(did, cid) { 18 + // Use chars 1-3 of CID for sharding (matches @pds/blobs-fs) 19 + const shard = cid.slice(1, 3); 20 + return join(baseDir, did, shard, cid); 21 + } 22 + 23 + return { 24 + async get(did, cid) { 25 + try { 26 + const path = blobPath(did, cid); 27 + const data = await Deno.readFile(path); 28 + const metaPath = `${path}.meta`; 29 + const meta = JSON.parse(await Deno.readTextFile(metaPath)); 30 + return { data, mimeType: meta.mimeType }; 31 + } catch (e) { 32 + if (e instanceof Deno.errors.NotFound) { 33 + return null; 34 + } 35 + throw e; 36 + } 37 + }, 38 + 39 + async put(did, cid, data, mimeType) { 40 + const path = blobPath(did, cid); 41 + await Deno.mkdir(dirname(path), { recursive: true }); 42 + await Deno.writeFile(path, data); 43 + await Deno.writeTextFile(`${path}.meta`, JSON.stringify({ mimeType })); 44 + }, 45 + 46 + async delete(did, cid) { 47 + try { 48 + const path = blobPath(did, cid); 49 + await Deno.remove(path); 50 + await Deno.remove(`${path}.meta`); 51 + } catch (e) { 52 + if (!(e instanceof Deno.errors.NotFound)) { 53 + throw e; 54 + } 55 + } 56 + }, 57 + }; 58 + }
+11
packages/blobs-deno/package.json
··· 1 + { 2 + "name": "@pds/blobs-deno", 3 + "version": "0.7.0", 4 + "type": "module", 5 + "exports": { 6 + ".": "./index.js" 7 + }, 8 + "devDependencies": { 9 + "@types/deno": "^2.5.0" 10 + } 11 + }
+13
packages/blobs-deno/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json", 3 + "compilerOptions": { 4 + "types": ["@types/deno", "node"], 5 + "baseUrl": ".", 6 + "paths": { 7 + "@pds/core": ["../core/index.js"], 8 + "@pds/core/*": ["../core/*"] 9 + } 10 + }, 11 + "include": ["./**/*.js"], 12 + "exclude": [] 13 + }
+11
packages/deno/deno.json
··· 1 + { 2 + "name": "@pds/deno", 3 + "version": "0.7.0", 4 + "exports": "./index.js", 5 + "imports": { 6 + "@pds/core": "../core/index.js", 7 + "@pds/core/": "../core/", 8 + "@pds/storage-sqlite": "../storage-sqlite/index.js", 9 + "@pds/blobs-deno": "../blobs-deno/index.js" 10 + } 11 + }
+165
packages/deno/index.js
··· 1 + // @pds/deno - Deno HTTP server adapter 2 + /// <reference types="@types/deno" /> 3 + 4 + import { DatabaseSync } from 'node:sqlite'; 5 + import { PersonalDataServer } from '@pds/core'; 6 + import { createActorStorage, createSharedStorage } from '@pds/storage-sqlite'; 7 + import { createDenoBlobs } from '@pds/blobs-deno'; 8 + 9 + /** 10 + * Create WebSocket port for Deno 11 + * @returns {import('@pds/core/ports.js').WebSocketPort} 12 + */ 13 + function createWebSocketPort() { 14 + /** @type {Set<WebSocket>} */ 15 + const clients = new Set(); 16 + 17 + return { 18 + isUpgrade(request) { 19 + return request.headers.get('upgrade')?.toLowerCase() === 'websocket'; 20 + }, 21 + 22 + upgrade(request, onConnect) { 23 + const upgrade = Deno.upgradeWebSocket(request); 24 + const { socket, response } = upgrade; 25 + 26 + clients.add(socket); 27 + socket.onclose = () => clients.delete(socket); 28 + socket.onerror = () => clients.delete(socket); 29 + 30 + socket.onopen = () => { 31 + onConnect({ 32 + send(data) { 33 + if (socket.readyState === WebSocket.OPEN) { 34 + socket.send(data); 35 + } 36 + }, 37 + close() { 38 + socket.close(); 39 + }, 40 + }); 41 + }; 42 + 43 + return response; 44 + }, 45 + 46 + broadcast(data) { 47 + for (const client of clients) { 48 + if (client.readyState === WebSocket.OPEN) { 49 + try { 50 + client.send(data); 51 + } catch { 52 + // Client may have disconnected 53 + } 54 + } 55 + } 56 + }, 57 + }; 58 + } 59 + 60 + /** 61 + * @typedef {Object} PdsServer 62 + * @property {PersonalDataServer} pds - The PDS instance 63 + * @property {Deno.HttpServer | null} server - HTTP server (null until listen() called) 64 + * @property {DatabaseSync} db - SQLite database instance 65 + * @property {() => Promise<Deno.HttpServer>} listen - Start server 66 + * @property {() => Promise<void>} close - Close server 67 + */ 68 + 69 + /** 70 + * Create Deno PDS server 71 + * @param {Object} options 72 + * @param {string} options.dbPath - Path to SQLite database 73 + * @param {string} options.blobsDir - Directory for blob storage 74 + * @param {string} options.jwtSecret - JWT signing secret 75 + * @param {number} [options.port=3000] - Server port 76 + * @param {string} [options.hostname] - PDS hostname 77 + * @param {string} [options.appviewUrl] - AppView URL for proxying 78 + * @param {string} [options.appviewDid] - AppView DID for service auth 79 + * @param {string} [options.relayUrl] - Relay URL for firehose notifications 80 + * @param {string} [options.password] - Password for createSession 81 + * @returns {Promise<PdsServer>} 82 + */ 83 + export async function createServer({ 84 + dbPath, 85 + blobsDir, 86 + jwtSecret, 87 + port = 3000, 88 + hostname, 89 + appviewUrl, 90 + appviewDid, 91 + relayUrl, 92 + password, 93 + }) { 94 + const db = new DatabaseSync(dbPath); 95 + const actorStorage = createActorStorage(db); 96 + const sharedStorage = createSharedStorage(db); 97 + const blobs = createDenoBlobs(blobsDir); 98 + const webSocket = createWebSocketPort(); 99 + 100 + const pds = new PersonalDataServer({ 101 + actorStorage, 102 + sharedStorage, 103 + blobs, 104 + webSocket, 105 + jwtSecret, 106 + hostname, 107 + appviewUrl, 108 + appviewDid, 109 + relayUrl, 110 + password, 111 + }); 112 + 113 + /** @type {Deno.HttpServer | null} */ 114 + let server = null; 115 + /** @type {ReturnType<typeof setInterval> | null} */ 116 + let cleanupInterval = null; 117 + 118 + return { 119 + pds, 120 + get server() { 121 + return server; 122 + }, 123 + db, 124 + 125 + /** 126 + * Start listening on configured port 127 + * @returns {Promise<Deno.HttpServer>} 128 + */ 129 + listen() { 130 + return new Promise((resolve) => { 131 + server = Deno.serve({ port, onListen: () => { 132 + console.log(`PDS listening on http://localhost:${port}`); 133 + resolve(server); 134 + }}, (request) => pds.fetch(request)); 135 + 136 + // Start cleanup interval (every hour) 137 + cleanupInterval = setInterval( 138 + async () => { 139 + try { 140 + await pds.runBlobCleanup(); 141 + await sharedStorage.cleanupExpiredDpopJtis(); 142 + } catch (e) { 143 + console.error('Cleanup error:', e); 144 + } 145 + }, 146 + 60 * 60 * 1000, 147 + ); 148 + }); 149 + }, 150 + 151 + /** 152 + * Close server and cleanup 153 + * @returns {Promise<void>} 154 + */ 155 + async close() { 156 + if (cleanupInterval) { 157 + clearInterval(cleanupInterval); 158 + } 159 + if (server) { 160 + await server.shutdown(); 161 + } 162 + db.close(); 163 + }, 164 + }; 165 + }
+14
packages/deno/package.json
··· 1 + { 2 + "name": "@pds/deno", 3 + "version": "0.7.0", 4 + "type": "module", 5 + "exports": { 6 + ".": "./index.js" 7 + }, 8 + "dependencies": { 9 + "@pds/core": "workspace:*" 10 + }, 11 + "devDependencies": { 12 + "@types/deno": "^2.5.0" 13 + } 14 + }
+13
packages/deno/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json", 3 + "compilerOptions": { 4 + "types": ["@types/deno"], 5 + "baseUrl": ".", 6 + "paths": { 7 + "@pds/core": ["../core/index.js"], 8 + "@pds/core/*": ["../core/*"] 9 + } 10 + }, 11 + "include": ["./**/*.js"], 12 + "exclude": [] 13 + }
-20
packages/node/index.js
··· 273 273 274 274 res.end(); 275 275 } 276 - 277 - /** 278 - * Create PDS server with configuration from environment variables 279 - * @param {Object} [overrides] - Override specific options 280 - * @returns {Promise<PdsServer>} 281 - */ 282 - export async function createServerFromEnv(overrides = {}) { 283 - return createServer({ 284 - dbPath: process.env.PDS_DB_PATH || './pds.db', 285 - blobsDir: process.env.PDS_BLOBS_DIR || './blobs', 286 - jwtSecret: process.env.JWT_SECRET || 'development-secret', 287 - port: parseInt(process.env.PORT || '3000', 10), 288 - hostname: process.env.HOSTNAME, 289 - appviewUrl: process.env.APPVIEW_URL, 290 - appviewDid: process.env.APPVIEW_DID, 291 - relayUrl: process.env.RELAY_URL, 292 - password: process.env.PDS_PASSWORD, 293 - ...overrides, 294 - }); 295 - }
+17
packages/storage-sqlite/driver.js
··· 1 + // @pds/storage-sqlite/driver - SQLite driver interface types 2 + // Both better-sqlite3 and node:sqlite implement this interface natively 3 + 4 + /** 5 + * @typedef {Object} SQLiteStatement 6 + * @property {(...args: any[]) => any} get - Single row query 7 + * @property {(...args: any[]) => any[]} all - Multi-row query 8 + * @property {(...args: any[]) => {changes: number | bigint}} run - Execute INSERT/UPDATE/DELETE 9 + */ 10 + 11 + /** 12 + * @typedef {Object} SQLiteDatabase 13 + * @property {(sql: string) => void} exec - Execute raw SQL (DDL, multiple statements) 14 + * @property {(sql: string) => SQLiteStatement} prepare - Create prepared statement 15 + */ 16 + 17 + export {};
+2 -2
packages/storage-sqlite/index.js
··· 23 23 24 24 /** 25 25 * Create storage adapter for better-sqlite3 (Node.js) 26 - * @param {import('better-sqlite3').Database} db 26 + * @param {import('./driver.js').SQLiteDatabase} db 27 27 * @returns {import('@pds/core/ports').ActorStoragePort} 28 28 */ 29 29 export function createActorStorage(db) { ··· 313 313 314 314 /** 315 315 * Create shared storage adapter for better-sqlite3 (Node.js) 316 - * @param {import('better-sqlite3').Database} db 316 + * @param {import('./driver.js').SQLiteDatabase} db 317 317 * @returns {import('@pds/core/ports').SharedStoragePort} 318 318 */ 319 319 export function createSharedStorage(db) {
+2 -1
packages/storage-sqlite/package.json
··· 5 5 "main": "index.js", 6 6 "types": "index.d.ts", 7 7 "exports": { 8 - ".": "./index.js" 8 + ".": "./index.js", 9 + "./driver": "./driver.js" 9 10 }, 10 11 "peerDependencies": { 11 12 "better-sqlite3": ">=9.0.0"
+27
pnpm-lock.yaml
··· 59 59 specifier: ^4.54.0 60 60 version: 4.58.0(@cloudflare/workers-types@4.20260111.0) 61 61 62 + examples/deno: 63 + devDependencies: 64 + '@types/deno': 65 + specifier: ^2.5.0 66 + version: 2.5.0 67 + 62 68 examples/node: 63 69 dependencies: 64 70 '@pds/node': ··· 68 74 specifier: ^12.6.0 69 75 version: 12.6.0 70 76 77 + packages/blobs-deno: 78 + devDependencies: 79 + '@types/deno': 80 + specifier: ^2.5.0 81 + version: 2.5.0 82 + 71 83 packages/blobs-fs: {} 72 84 73 85 packages/blobs-s3: {} ··· 79 91 version: link:../core 80 92 81 93 packages/core: {} 94 + 95 + packages/deno: 96 + dependencies: 97 + '@pds/core': 98 + specifier: workspace:* 99 + version: link:../core 100 + devDependencies: 101 + '@types/deno': 102 + specifier: ^2.5.0 103 + version: 2.5.0 82 104 83 105 packages/node: 84 106 dependencies: ··· 815 837 816 838 '@types/deep-eql@4.0.2': 817 839 resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} 840 + 841 + '@types/deno@2.5.0': 842 + resolution: {integrity: sha512-g8JS38vmc0S87jKsFzre+0ZyMOUDHPVokEJymSCRlL57h6f/FdKPWBXgdFh3Z8Ees9sz11qt9VWELU9Y9ZkiVw==} 818 843 819 844 '@types/estree@1.0.8': 820 845 resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} ··· 1800 1825 assertion-error: 2.0.1 1801 1826 1802 1827 '@types/deep-eql@4.0.2': {} 1828 + 1829 + '@types/deno@2.5.0': {} 1803 1830 1804 1831 '@types/estree@1.0.8': {} 1805 1832
+18 -4
test/e2e.test.js
··· 1 1 /** 2 - * E2E tests for PDS - runs against local wrangler dev or Node.js server 2 + * E2E tests for PDS - runs against local wrangler dev, Node.js, or Deno server 3 3 * Uses Vitest and fetch 4 4 */ 5 5 ··· 13 13 stopNodeServer, 14 14 USE_LOCAL_INFRA, 15 15 } from './helpers/node-server.js'; 16 + import { 17 + startDenoServer, 18 + stopDenoServer, 19 + } from './helpers/deno-server.js'; 16 20 import { getOAuthTokenWithScope, setBaseUrl } from './helpers/oauth.js'; 17 21 import { ensureDockerServices, RELAY_URL } from './helpers/docker-services.js'; 18 22 import { createTestIdentity } from './helpers/identity.js'; 19 23 20 24 const PLATFORM = process.env.PLATFORM || 'cloudflare'; 21 25 const BASE = 22 - PLATFORM === 'node' ? 'http://localhost:3000' : 'http://localhost:8787'; 26 + PLATFORM === 'node' || PLATFORM === 'deno' 27 + ? 'http://localhost:3000' 28 + : 'http://localhost:8787'; 23 29 24 30 // Configure oauth helper to use the same BASE 25 31 setBaseUrl(BASE); ··· 134 140 let wrangler = null; 135 141 /** @type {{close: () => Promise<void>}|null} */ 136 142 let nodeServer = null; 143 + /** @type {{close: () => Promise<void>}|null} */ 144 + let denoServer = null; 137 145 /** @type {string} */ 138 146 let token = ''; 139 147 /** @type {string} */ ··· 150 158 if (PLATFORM === 'node') { 151 159 // Start Node.js server 152 160 nodeServer = await startNodeServer(); 161 + } else if (PLATFORM === 'deno') { 162 + // Start Deno server 163 + denoServer = await startDenoServer(); 153 164 } else { 154 165 // Clear wrangler state for clean slate (like docker reset for node) 155 166 const { rmSync } = await import('node:fs'); ··· 220 231 afterAll(async () => { 221 232 if (PLATFORM === 'node') { 222 233 if (nodeServer) await stopNodeServer(nodeServer); 234 + } else if (PLATFORM === 'deno') { 235 + if (denoServer) await stopDenoServer(denoServer); 223 236 } else { 224 237 if (wrangler) wrangler.kill(); 225 238 } ··· 2124 2137 describe('Relay sync (requires docker)', () => { 2125 2138 // Skip entire suite if docker infrastructure is disabled or using cloudflare 2126 2139 // Cloudflare wrangler isn't accessible from Docker through Caddy 2127 - const skipRelaySyncTests = !USE_LOCAL_INFRA || PLATFORM !== 'node'; 2140 + const skipRelaySyncTests = 2141 + !USE_LOCAL_INFRA || (PLATFORM !== 'node' && PLATFORM !== 'deno'); 2128 2142 2129 2143 beforeAll(() => { 2130 2144 if (skipRelaySyncTests) { 2131 2145 console.log( 2132 - 'Skipping relay sync tests (requires USE_LOCAL_INFRA=true and PLATFORM=node)', 2146 + 'Skipping relay sync tests (requires USE_LOCAL_INFRA=true and PLATFORM=node or deno)', 2133 2147 ); 2134 2148 } 2135 2149 });
+111
test/helpers/deno-server.js
··· 1 + // test/helpers/deno-server.js 2 + import { spawn } from 'node:child_process'; 3 + import { mkdirSync, rmSync } from 'node:fs'; 4 + 5 + const TEST_DATA_DIR = './test-data'; 6 + const TEST_PORT = 3000; 7 + const USE_LOCAL_INFRA = process.env.USE_LOCAL_INFRA !== 'false'; 8 + 9 + /** @type {import('node:child_process').ChildProcess | null} */ 10 + let denoProcess = null; 11 + 12 + /** 13 + * Start Deno PDS server for e2e tests 14 + * Spawns Deno process running examples/deno/main.ts 15 + * @returns {Promise<{close: () => Promise<void>}>} Server instance with close() method 16 + */ 17 + export async function startDenoServer() { 18 + // Fresh data each run 19 + rmSync(TEST_DATA_DIR, { recursive: true, force: true }); 20 + mkdirSync(TEST_DATA_DIR, { recursive: true }); 21 + 22 + const hostname = USE_LOCAL_INFRA 23 + ? 'host.docker.internal:3443' 24 + : `localhost:${TEST_PORT}`; 25 + 26 + const env = { 27 + ...process.env, 28 + PDS_DB_PATH: `${TEST_DATA_DIR}/pds.db`, 29 + PDS_BLOBS_DIR: `${TEST_DATA_DIR}/blobs`, 30 + JWT_SECRET: 'test-secret-for-e2e', 31 + HOSTNAME: hostname, 32 + PORT: String(TEST_PORT), 33 + PDS_PASSWORD: 'test-password', 34 + RELAY_URL: USE_LOCAL_INFRA ? 'http://localhost:2470' : '', 35 + APPVIEW_URL: 'https://api.bsky.app', 36 + APPVIEW_DID: 'did:web:api.bsky.app', 37 + }; 38 + 39 + denoProcess = spawn( 40 + 'deno', 41 + [ 42 + 'run', 43 + '--allow-net', 44 + '--allow-read', 45 + '--allow-write', 46 + '--allow-env', 47 + '--unstable-ffi', 48 + 'examples/deno/main.ts', 49 + ], 50 + { 51 + env, 52 + stdio: ['ignore', 'pipe', 'pipe'], 53 + cwd: process.cwd(), 54 + }, 55 + ); 56 + 57 + // Log output for debugging 58 + denoProcess.stdout?.on('data', (data) => { 59 + const msg = data.toString().trim(); 60 + if (msg) console.log(`[deno] ${msg}`); 61 + }); 62 + 63 + denoProcess.stderr?.on('data', (data) => { 64 + const msg = data.toString().trim(); 65 + if (msg) console.error(`[deno] ${msg}`); 66 + }); 67 + 68 + // Wait for server to be ready 69 + await waitForReady(); 70 + 71 + return { 72 + async close() { 73 + if (denoProcess) { 74 + denoProcess.kill('SIGTERM'); 75 + // Wait for process to exit 76 + await new Promise((resolve) => { 77 + denoProcess?.on('exit', resolve); 78 + // Timeout after 5s 79 + setTimeout(resolve, 5000); 80 + }); 81 + denoProcess = null; 82 + } 83 + }, 84 + }; 85 + } 86 + 87 + /** 88 + * Wait for Deno server to be ready 89 + */ 90 + async function waitForReady(maxAttempts = 30) { 91 + for (let i = 0; i < maxAttempts; i++) { 92 + try { 93 + const res = await fetch(`http://localhost:${TEST_PORT}/`); 94 + if (res.ok) return; 95 + } catch { 96 + // Server not ready yet 97 + } 98 + await new Promise((r) => setTimeout(r, 500)); 99 + } 100 + throw new Error('Deno server failed to start'); 101 + } 102 + 103 + /** 104 + * Stop Deno PDS server 105 + * @param {{close: () => Promise<void>}} server - Server instance from startDenoServer 106 + */ 107 + export async function stopDenoServer(server) { 108 + await server.close(); 109 + } 110 + 111 + export { USE_LOCAL_INFRA, TEST_PORT };
+1 -1
tsconfig.json
··· 24 24 } 25 25 }, 26 26 "include": ["src/**/*.js", "packages/**/*.js", "test/**/*.js"], 27 - "exclude": ["node_modules"] 27 + "exclude": ["node_modules", "packages/deno", "packages/blobs-deno", "examples/deno"] 28 28 }