A zero-dependency AT Protocol Personal Data Server written in JavaScript

feat: add TypeScript checking and reorganize pds.js

- Add JSDoc type annotations for TypeScript checking
- Reorganize pds.js into 13 logical sections with box-style headers:
Types & Constants, Utilities, CBOR Encoding, Content Identifiers,
Cryptography, Authentication, Merkle Search Tree, CAR Files,
Blob Handling, Relay Notification, Routing, Personal Data Server,
Workers Entry Point
- Add ASCII art file header with feature list
- Add tsconfig.json for type checking
- Add typecheck npm script

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

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

+496
docs/plans/2026-01-06-pds-file-reorganization.md
··· 1 + # PDS File Reorganization Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Reorganize pds.js into logical domain sections with box-style headers for improved readability. 6 + 7 + **Architecture:** Reorder existing code into 12 logical domains without changing functionality. Add Unicode box-style section headers. Group related utilities that are currently scattered. 8 + 9 + **Tech Stack:** JavaScript, JSDoc 10 + 11 + --- 12 + 13 + ## Box Header Format 14 + 15 + All section headers use this format (80 chars wide): 16 + ```javascript 17 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 18 + // โ•‘ SECTION NAME โ•‘ 19 + // โ•‘ Brief description of what this section contains โ•‘ 20 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 21 + ``` 22 + 23 + --- 24 + 25 + ### Task 1: Types & Constants Section 26 + 27 + **Files:** 28 + - Modify: `src/pds.js` (lines 17-84, plus scattered constants) 29 + 30 + **Step 1: Create the new section header and gather all types/constants** 31 + 32 + Move these items to the top (after the file header comment): 33 + - `CBOR_FALSE`, `CBOR_TRUE`, `CBOR_NULL`, `CBOR_TAG_CID` (from lines 19-24) 34 + - `CODEC_DAG_CBOR`, `CODEC_RAW` (from lines 480-481) 35 + - `TID_CHARS`, `clockId`, `lastTimestamp` (from lines 563-566) 36 + - `P256_N`, `P256_N_DIV_2` (from lines 638-641) 37 + - All typedefs: `Env`, `BlockRow`, `RecordRow`, `CommitRow`, `SeqEventRow`, `BlobRow`, `JwtPayload` 38 + 39 + Add header: 40 + ```javascript 41 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 42 + // โ•‘ TYPES & CONSTANTS โ•‘ 43 + // โ•‘ Environment bindings, SQL row types, protocol constants โ•‘ 44 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 45 + ``` 46 + 47 + **Step 2: Run typecheck to verify no breakage** 48 + 49 + Run: `npm run typecheck` 50 + Expected: 0 errors 51 + 52 + **Step 3: Commit** 53 + 54 + ```bash 55 + git add src/pds.js 56 + git commit -m "refactor: consolidate types and constants section" 57 + ``` 58 + 59 + --- 60 + 61 + ### Task 2: Utilities Section 62 + 63 + **Files:** 64 + - Modify: `src/pds.js` 65 + 66 + **Step 1: Create utilities section after types/constants** 67 + 68 + Move these functions together: 69 + - `errorResponse()` (from line 92) 70 + - `bytesToHex()` (from line 990) 71 + - `hexToBytes()` (from line 1001) 72 + - `bytesToBigInt()` (from line 647) 73 + - `bigIntToBytes()` (from line 660) 74 + - `base32Encode()` (from line 538) 75 + - `base32Decode()` (from line 1237) 76 + - `base64UrlEncode()` (from line 745) 77 + - `base64UrlDecode()` (from line 759) 78 + - `varint()` (from line 1211) 79 + 80 + Add header: 81 + ```javascript 82 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 83 + // โ•‘ UTILITIES โ•‘ 84 + // โ•‘ Error responses, byte conversion, base encoding โ•‘ 85 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 86 + ``` 87 + 88 + **Step 2: Run typecheck** 89 + 90 + Run: `npm run typecheck` 91 + Expected: 0 errors 92 + 93 + **Step 3: Commit** 94 + 95 + ```bash 96 + git add src/pds.js 97 + git commit -m "refactor: consolidate utilities section" 98 + ``` 99 + 100 + --- 101 + 102 + ### Task 3: CBOR Encoding Section 103 + 104 + **Files:** 105 + - Modify: `src/pds.js` 106 + 107 + **Step 1: Create CBOR section** 108 + 109 + Keep together (already grouped, just add new header): 110 + - `encodeHead()` 111 + - `cborEncode()` 112 + - `cborEncodeDagCbor()` 113 + - `cborDecode()` 114 + 115 + Replace `// === CBOR ENCODING ===` with: 116 + ```javascript 117 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 118 + // โ•‘ CBOR ENCODING โ•‘ 119 + // โ•‘ RFC 8949 CBOR and DAG-CBOR for content-addressed data โ•‘ 120 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 121 + ``` 122 + 123 + **Step 2: Run typecheck** 124 + 125 + Run: `npm run typecheck` 126 + Expected: 0 errors 127 + 128 + **Step 3: Commit** 129 + 130 + ```bash 131 + git add src/pds.js 132 + git commit -m "refactor: add CBOR encoding section header" 133 + ``` 134 + 135 + --- 136 + 137 + ### Task 4: Content Identifiers Section 138 + 139 + **Files:** 140 + - Modify: `src/pds.js` 141 + 142 + **Step 1: Create CID/TID section** 143 + 144 + Group together: 145 + - `class CID` (from line 238) 146 + - `createCidWithCodec()` (from line 489) 147 + - `createCid()` (from line 510) 148 + - `createBlobCid()` (from line 519) 149 + - `cidToString()` (from line 528) 150 + - `cidToBytes()` (from line 1226) 151 + - `createTid()` (from line 572) 152 + 153 + Add header: 154 + ```javascript 155 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 156 + // โ•‘ CONTENT IDENTIFIERS โ•‘ 157 + // โ•‘ CIDs (content hashes) and TIDs (timestamp IDs) โ•‘ 158 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 159 + ``` 160 + 161 + **Step 2: Run typecheck** 162 + 163 + Run: `npm run typecheck` 164 + Expected: 0 errors 165 + 166 + **Step 3: Commit** 167 + 168 + ```bash 169 + git add src/pds.js 170 + git commit -m "refactor: consolidate content identifiers section" 171 + ``` 172 + 173 + --- 174 + 175 + ### Task 5: Cryptography Section 176 + 177 + **Files:** 178 + - Modify: `src/pds.js` 179 + 180 + **Step 1: Create cryptography section** 181 + 182 + Group together: 183 + - `sha256()` (from line 1016) 184 + - `importPrivateKey()` (from line 606) 185 + - `generateKeyPair()` (from line 705) 186 + - `compressPublicKey()` (from line 728) 187 + - `sign()` (from line 675) 188 + 189 + Add header: 190 + ```javascript 191 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 192 + // โ•‘ CRYPTOGRAPHY โ•‘ 193 + // โ•‘ P-256 signing with low-S normalization, key management โ•‘ 194 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 195 + ``` 196 + 197 + **Step 2: Run typecheck** 198 + 199 + Run: `npm run typecheck` 200 + Expected: 0 errors 201 + 202 + **Step 3: Commit** 203 + 204 + ```bash 205 + git add src/pds.js 206 + git commit -m "refactor: create cryptography section" 207 + ``` 208 + 209 + --- 210 + 211 + ### Task 6: Authentication Section 212 + 213 + **Files:** 214 + - Modify: `src/pds.js` 215 + 216 + **Step 1: Create authentication section** 217 + 218 + Group together: 219 + - `hmacSign()` (from line 777) 220 + - `createAccessJwt()` (from line 800) 221 + - `createRefreshJwt()` (from line 829) 222 + - `verifyJwt()` (from line 876) 223 + - `verifyAccessJwt()` (from line 919) 224 + - `verifyRefreshJwt()` (from line 931) 225 + - `createServiceJwt()` (from line 952) 226 + 227 + Add header: 228 + ```javascript 229 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 230 + // โ•‘ AUTHENTICATION โ•‘ 231 + // โ•‘ JWT creation/verification for sessions and service auth โ•‘ 232 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 233 + ``` 234 + 235 + **Step 2: Run typecheck** 236 + 237 + Run: `npm run typecheck` 238 + Expected: 0 errors 239 + 240 + **Step 3: Commit** 241 + 242 + ```bash 243 + git add src/pds.js 244 + git commit -m "refactor: create authentication section" 245 + ``` 246 + 247 + --- 248 + 249 + ### Task 7: Merkle Search Tree Section 250 + 251 + **Files:** 252 + - Modify: `src/pds.js` 253 + 254 + **Step 1: Update MST section header** 255 + 256 + Keep together (already grouped): 257 + - `keyDepthCache` 258 + - `getKeyDepth()` 259 + - `commonPrefixLen()` 260 + - `class MST` 261 + 262 + Replace `// === MERKLE SEARCH TREE ===` with: 263 + ```javascript 264 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 265 + // โ•‘ MERKLE SEARCH TREE โ•‘ 266 + // โ•‘ MST for ATProto repository structure โ•‘ 267 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 268 + ``` 269 + 270 + **Step 2: Run typecheck** 271 + 272 + Run: `npm run typecheck` 273 + Expected: 0 errors 274 + 275 + **Step 3: Commit** 276 + 277 + ```bash 278 + git add src/pds.js 279 + git commit -m "refactor: update MST section header" 280 + ``` 281 + 282 + --- 283 + 284 + ### Task 8: CAR Files Section 285 + 286 + **Files:** 287 + - Modify: `src/pds.js` 288 + 289 + **Step 1: Update CAR section** 290 + 291 + Keep only: 292 + - `buildCarFile()` 293 + 294 + (Note: `varint()`, `cidToBytes()`, `base32Decode()` moved to earlier sections) 295 + 296 + Replace `// === CAR FILE BUILDER ===` with: 297 + ```javascript 298 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 299 + // โ•‘ CAR FILES โ•‘ 300 + // โ•‘ Content Addressable aRchive format for repo sync โ•‘ 301 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 302 + ``` 303 + 304 + **Step 2: Run typecheck** 305 + 306 + Run: `npm run typecheck` 307 + Expected: 0 errors 308 + 309 + **Step 3: Commit** 310 + 311 + ```bash 312 + git add src/pds.js 313 + git commit -m "refactor: update CAR files section" 314 + ``` 315 + 316 + --- 317 + 318 + ### Task 9: Blob Handling Section 319 + 320 + **Files:** 321 + - Modify: `src/pds.js` 322 + 323 + **Step 1: Create blob handling section** 324 + 325 + Group together: 326 + - `sniffMimeType()` (from line 105) 327 + - `findBlobRefs()` (from line 181) 328 + - `CRAWL_NOTIFY_THRESHOLD`, `lastCrawlNotify` (from lines 207-208) 329 + - `notifyCrawlers()` (from line 214) 330 + 331 + Add header: 332 + ```javascript 333 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 334 + // โ•‘ BLOB HANDLING โ•‘ 335 + // โ•‘ MIME detection, blob reference scanning, crawler notification โ•‘ 336 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 337 + ``` 338 + 339 + **Step 2: Run typecheck** 340 + 341 + Run: `npm run typecheck` 342 + Expected: 0 errors 343 + 344 + **Step 3: Commit** 345 + 346 + ```bash 347 + git add src/pds.js 348 + git commit -m "refactor: create blob handling section" 349 + ``` 350 + 351 + --- 352 + 353 + ### Task 10: Routing Section 354 + 355 + **Files:** 356 + - Modify: `src/pds.js` 357 + 358 + **Step 1: Add routing section header** 359 + 360 + Before `RouteHandler` callback typedef, add: 361 + ```javascript 362 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 363 + // โ•‘ ROUTING โ•‘ 364 + // โ•‘ XRPC endpoint definitions โ•‘ 365 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 366 + ``` 367 + 368 + This section contains: 369 + - `RouteHandler` (callback typedef) 370 + - `Route` (typedef) 371 + - `pdsRoutes` 372 + 373 + **Step 2: Run typecheck** 374 + 375 + Run: `npm run typecheck` 376 + Expected: 0 errors 377 + 378 + **Step 3: Commit** 379 + 380 + ```bash 381 + git add src/pds.js 382 + git commit -m "refactor: add routing section header" 383 + ``` 384 + 385 + --- 386 + 387 + ### Task 11: Personal Data Server Section 388 + 389 + **Files:** 390 + - Modify: `src/pds.js` 391 + 392 + **Step 1: Add PDS class section header** 393 + 394 + Before `class PersonalDataServer`, add: 395 + ```javascript 396 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 397 + // โ•‘ PERSONAL DATA SERVER โ•‘ 398 + // โ•‘ Durable Object class implementing ATProto PDS โ•‘ 399 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 400 + ``` 401 + 402 + **Step 2: Run typecheck** 403 + 404 + Run: `npm run typecheck` 405 + Expected: 0 errors 406 + 407 + **Step 3: Commit** 408 + 409 + ```bash 410 + git add src/pds.js 411 + git commit -m "refactor: add PDS class section header" 412 + ``` 413 + 414 + --- 415 + 416 + ### Task 12: Workers Entry Point Section 417 + 418 + **Files:** 419 + - Modify: `src/pds.js` 420 + 421 + **Step 1: Add workers entry point section header** 422 + 423 + Before `corsHeaders`, add: 424 + ```javascript 425 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 426 + // โ•‘ WORKERS ENTRY POINT โ•‘ 427 + // โ•‘ Request handling, CORS, auth middleware โ•‘ 428 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 429 + ``` 430 + 431 + This section contains: 432 + - `corsHeaders` 433 + - `addCorsHeaders()` 434 + - `getSubdomain()` 435 + - `requireAuth()` 436 + - `handleAuthenticatedBlobUpload()` 437 + - `handleAuthenticatedRepoWrite()` 438 + - `handleRequest()` 439 + - `default export` 440 + 441 + **Step 2: Run typecheck** 442 + 443 + Run: `npm run typecheck` 444 + Expected: 0 errors 445 + 446 + **Step 3: Commit** 447 + 448 + ```bash 449 + git add src/pds.js 450 + git commit -m "refactor: add workers entry point section header" 451 + ``` 452 + 453 + --- 454 + 455 + ### Task 13: Final Verification 456 + 457 + **Step 1: Run full typecheck** 458 + 459 + Run: `npm run typecheck` 460 + Expected: 0 errors 461 + 462 + **Step 2: Run tests** 463 + 464 + Run: `npm test` 465 + Expected: All tests pass 466 + 467 + **Step 3: Run e2e tests if available** 468 + 469 + Run: `npm run test:e2e` 470 + Expected: All tests pass 471 + 472 + **Step 4: Final commit if any cleanup needed** 473 + 474 + ```bash 475 + git add src/pds.js 476 + git commit -m "refactor: complete pds.js reorganization with box headers" 477 + ``` 478 + 479 + --- 480 + 481 + ## Section Order Summary 482 + 483 + Final file structure (top to bottom): 484 + 1. File header comment 485 + 2. TYPES & CONSTANTS 486 + 3. UTILITIES 487 + 4. CBOR ENCODING 488 + 5. CONTENT IDENTIFIERS 489 + 6. CRYPTOGRAPHY 490 + 7. AUTHENTICATION 491 + 8. MERKLE SEARCH TREE 492 + 9. CAR FILES 493 + 10. BLOB HANDLING 494 + 11. ROUTING 495 + 12. PERSONAL DATA SERVER 496 + 13. WORKERS ENTRY POINT
+23
package-lock.json
··· 9 9 "version": "0.1.0", 10 10 "devDependencies": { 11 11 "@biomejs/biome": "^2.3.11", 12 + "@cloudflare/workers-types": "^4.20260103.0", 13 + "typescript": "^5.9.3", 12 14 "wrangler": "^4.54.0" 13 15 } 14 16 }, ··· 288 290 "engines": { 289 291 "node": ">=16" 290 292 } 293 + }, 294 + "node_modules/@cloudflare/workers-types": { 295 + "version": "4.20260103.0", 296 + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260103.0.tgz", 297 + "integrity": "sha512-jANmoGpJcXARnwlkvrQOeWyjYD1quTfHcs+++Z544XRHOSfLc4XSlts7snIhbiIGgA5bo66zDhraF+9lKUr2hw==", 298 + "dev": true, 299 + "license": "MIT OR Apache-2.0" 291 300 }, 292 301 "node_modules/@cspotcode/source-map-support": { 293 302 "version": "0.8.1", ··· 1563 1572 "dev": true, 1564 1573 "license": "0BSD", 1565 1574 "optional": true 1575 + }, 1576 + "node_modules/typescript": { 1577 + "version": "5.9.3", 1578 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 1579 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 1580 + "dev": true, 1581 + "license": "Apache-2.0", 1582 + "bin": { 1583 + "tsc": "bin/tsc", 1584 + "tsserver": "bin/tsserver" 1585 + }, 1586 + "engines": { 1587 + "node": ">=14.17" 1588 + } 1566 1589 }, 1567 1590 "node_modules/undici": { 1568 1591 "version": "7.14.0",
+4 -1
package.json
··· 12 12 "setup": "node scripts/setup.js", 13 13 "format": "biome format --write . && shfmt -w -i 2 test/*.sh", 14 14 "lint": "biome lint .", 15 - "check": "biome check ." 15 + "check": "biome check .", 16 + "typecheck": "tsc --noEmit" 16 17 }, 17 18 "devDependencies": { 18 19 "@biomejs/biome": "^2.3.11", 20 + "@cloudflare/workers-types": "^4.20260103.0", 21 + "typescript": "^5.9.3", 19 22 "wrangler": "^4.54.0" 20 23 } 21 24 }
+691 -381
src/pds.js
··· 1 - /** 2 - * Minimal AT Protocol Personal Data Server (PDS) 3 - * 4 - * A single-file implementation of an ATProto PDS for Cloudflare Workers 5 - * with Durable Objects. Implements the core protocol primitives: 6 - * 7 - * - CBOR/DAG-CBOR encoding for content-addressed data 8 - * - CID generation (CIDv1 with dag-cbor + sha-256) 9 - * - Merkle Search Tree (MST) for repository structure 10 - * - P-256 signing with low-S normalization 11 - * - CAR file building for repo sync 12 - * - XRPC endpoints for repo operations and sync 13 - * 14 - * @see https://atproto.com 15 - */ 1 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 2 + // โ•‘ โ•‘ 3 + // โ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— Personal Data Server โ•‘ 4 + // โ•‘ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ• for AT Protocol โ•‘ 5 + // โ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ•‘ 6 + // โ•‘ โ–ˆโ–ˆโ•”โ•โ•โ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ•‘ 7 + // โ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘ โ•‘ 8 + // โ•‘ โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•โ• โ•‘ 9 + // โ•‘ โ•‘ 10 + // โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ 11 + // โ•‘ โ•‘ 12 + // โ•‘ A single-file ATProto PDS for Cloudflare Workers + Durable Objects โ•‘ 13 + // โ•‘ โ•‘ 14 + // โ•‘ Features: โ•‘ 15 + // โ•‘ โ€ข CBOR/DAG-CBOR encoding for content-addressed data โ•‘ 16 + // โ•‘ โ€ข CID generation (CIDv1 with dag-cbor + sha-256) โ•‘ 17 + // โ•‘ โ€ข Merkle Search Tree (MST) for repository structure โ•‘ 18 + // โ•‘ โ€ข P-256 signing with low-S normalization โ•‘ 19 + // โ•‘ โ€ข JWT authentication (access, refresh, service tokens) โ•‘ 20 + // โ•‘ โ€ข CAR file building for repo sync โ•‘ 21 + // โ•‘ โ€ข R2 blob storage with MIME detection โ•‘ 22 + // โ•‘ โ€ข SQLite persistence via Durable Objects โ•‘ 23 + // โ•‘ โ•‘ 24 + // โ•‘ @see https://atproto.com โ•‘ 25 + // โ•‘ โ•‘ 26 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 27 + 28 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 29 + // โ•‘ TYPES & CONSTANTS โ•‘ 30 + // โ•‘ Environment bindings, SQL row types, protocol constants โ•‘ 31 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 16 32 17 - // === CONSTANTS === 18 33 // CBOR primitive markers (RFC 8949) 19 34 const CBOR_FALSE = 0xf4; 20 35 const CBOR_TRUE = 0xf5; ··· 23 38 // DAG-CBOR CID link tag 24 39 const CBOR_TAG_CID = 42; 25 40 26 - // === ERROR HELPER === 41 + // CID codec constants 42 + const CODEC_DAG_CBOR = 0x71; 43 + const CODEC_RAW = 0x55; 44 + 45 + // TID generation constants 46 + const TID_CHARS = '234567abcdefghijklmnopqrstuvwxyz'; 47 + let lastTimestamp = 0; 48 + const clockId = Math.floor(Math.random() * 1024); 49 + 50 + // P-256 curve order N (for low-S signature normalization) 51 + const P256_N = BigInt( 52 + '0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551', 53 + ); 54 + const P256_N_DIV_2 = P256_N / 2n; 55 + 56 + // Crawler notification throttle 57 + const CRAWL_NOTIFY_THRESHOLD = 20 * 60 * 1000; // 20 minutes (matches official PDS) 58 + let lastCrawlNotify = 0; 59 + 60 + /** 61 + * Cloudflare Workers environment bindings 62 + * @typedef {Object} Env 63 + * @property {string} JWT_SECRET - Secret for signing/verifying session JWTs 64 + * @property {string} [RELAY_HOST] - Relay host to notify of repo updates (e.g., bsky.network) 65 + * @property {string} [APPVIEW_URL] - AppView URL for proxying app.bsky.* requests 66 + * @property {string} [APPVIEW_DID] - AppView DID for service auth 67 + * @property {string} [PDS_PASSWORD] - Password for createSession authentication 68 + * @property {DurableObjectNamespace} PDS - Durable Object namespace for PDS instances 69 + * @property {R2Bucket} [BLOB_BUCKET] - R2 bucket for blob storage (legacy name) 70 + * @property {R2Bucket} [BLOBS] - R2 bucket for blob storage 71 + */ 72 + 73 + /** 74 + * Row from the `blocks` table - stores raw CBOR-encoded data blocks 75 + * @typedef {Object} BlockRow 76 + * @property {string} cid - Content ID (CIDv1 base32lower) 77 + * @property {ArrayBuffer} data - Raw block data (CBOR-encoded) 78 + */ 79 + 80 + /** 81 + * Row from the `records` table - indexes AT Protocol records 82 + * @typedef {Object} RecordRow 83 + * @property {string} uri - AT URI (at://did/collection/rkey) 84 + * @property {string} cid - Content ID of the record block 85 + * @property {string} collection - Collection NSID (e.g., app.bsky.feed.post) 86 + * @property {string} rkey - Record key within collection 87 + * @property {ArrayBuffer} value - CBOR-encoded record value 88 + */ 89 + 90 + /** 91 + * Row from the `commits` table - tracks repo commit history 92 + * @typedef {Object} CommitRow 93 + * @property {string} cid - Content ID of the signed commit block 94 + * @property {string} rev - Revision TID for ordering 95 + * @property {string|null} prev - Previous commit CID (null for first commit) 96 + */ 97 + 98 + /** 99 + * Row from the `seq_events` table - stores firehose events for subscribeRepos 100 + * @typedef {Object} SeqEventRow 101 + * @property {number} seq - Sequence number for cursor-based pagination 102 + * @property {string} did - DID of the repo that changed 103 + * @property {string} commit_cid - CID of the commit 104 + * @property {ArrayBuffer|Uint8Array} evt - CBOR-encoded event with ops, blocks, rev, time 105 + */ 106 + 107 + /** 108 + * Row from the `blob` table - tracks uploaded blob metadata 109 + * @typedef {Object} BlobRow 110 + * @property {string} cid - Content ID of the blob (raw codec) 111 + * @property {string} mimeType - MIME type (sniffed or from Content-Type header) 112 + * @property {number} size - Size in bytes 113 + * @property {string} createdAt - ISO timestamp of upload 114 + */ 115 + 116 + /** 117 + * Decoded JWT payload for session tokens 118 + * @typedef {Object} JwtPayload 119 + * @property {string} [scope] - Token scope (e.g., "com.atproto.access") 120 + * @property {string} sub - Subject DID (the authenticated user) 121 + * @property {string} [aud] - Audience (for refresh tokens, should match sub) 122 + * @property {number} [iat] - Issued-at timestamp (Unix seconds) 123 + * @property {number} [exp] - Expiration timestamp (Unix seconds) 124 + * @property {string} [jti] - Unique token identifier 125 + */ 126 + 127 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 128 + // โ•‘ UTILITIES โ•‘ 129 + // โ•‘ Error responses, byte conversion, base encoding โ•‘ 130 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 131 + 132 + /** 133 + * @param {string} error - Error code 134 + * @param {string} message - Error message 135 + * @param {number} status - HTTP status code 136 + * @returns {Response} 137 + */ 27 138 function errorResponse(error, message, status) { 28 139 return Response.json({ error, message }, { status }); 29 140 } 30 141 31 - // === MIME TYPE SNIFFING === 32 - // Detect file type from magic bytes (first 12 bytes) 33 - // Reference: https://en.wikipedia.org/wiki/List_of_file_signatures 142 + /** 143 + * Convert bytes to hexadecimal string 144 + * @param {Uint8Array} bytes - Bytes to convert 145 + * @returns {string} Hex string 146 + */ 147 + export function bytesToHex(bytes) { 148 + return Array.from(bytes) 149 + .map((b) => b.toString(16).padStart(2, '0')) 150 + .join(''); 151 + } 34 152 35 153 /** 36 - * Sniff MIME type from file magic bytes 37 - * @param {Uint8Array|ArrayBuffer} bytes - File bytes (only first 12 needed) 38 - * @returns {string|null} Detected MIME type or null if unknown 154 + * Convert hexadecimal string to bytes 155 + * @param {string} hex - Hex string 156 + * @returns {Uint8Array} Decoded bytes 39 157 */ 40 - export function sniffMimeType(bytes) { 41 - const arr = new Uint8Array(bytes.slice(0, 12)); 42 - 43 - // JPEG: FF D8 FF 44 - if (arr[0] === 0xff && arr[1] === 0xd8 && arr[2] === 0xff) { 45 - return 'image/jpeg'; 158 + export function hexToBytes(hex) { 159 + const bytes = new Uint8Array(hex.length / 2); 160 + for (let i = 0; i < hex.length; i += 2) { 161 + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); 46 162 } 163 + return bytes; 164 + } 47 165 48 - // PNG: 89 50 4E 47 0D 0A 1A 0A 49 - if ( 50 - arr[0] === 0x89 && 51 - arr[1] === 0x50 && 52 - arr[2] === 0x4e && 53 - arr[3] === 0x47 && 54 - arr[4] === 0x0d && 55 - arr[5] === 0x0a && 56 - arr[6] === 0x1a && 57 - arr[7] === 0x0a 58 - ) { 59 - return 'image/png'; 166 + /** 167 + * @param {Uint8Array} bytes 168 + * @returns {bigint} 169 + */ 170 + function bytesToBigInt(bytes) { 171 + let result = 0n; 172 + for (const byte of bytes) { 173 + result = (result << 8n) | BigInt(byte); 60 174 } 175 + return result; 176 + } 61 177 62 - // GIF: 47 49 46 38 (GIF8) 63 - if ( 64 - arr[0] === 0x47 && 65 - arr[1] === 0x49 && 66 - arr[2] === 0x46 && 67 - arr[3] === 0x38 68 - ) { 69 - return 'image/gif'; 178 + /** 179 + * @param {bigint} n 180 + * @param {number} length 181 + * @returns {Uint8Array} 182 + */ 183 + function bigIntToBytes(n, length) { 184 + const bytes = new Uint8Array(length); 185 + for (let i = length - 1; i >= 0; i--) { 186 + bytes[i] = Number(n & 0xffn); 187 + n >>= 8n; 70 188 } 189 + return bytes; 190 + } 71 191 72 - // WebP: RIFF....WEBP 73 - if ( 74 - arr[0] === 0x52 && 75 - arr[1] === 0x49 && 76 - arr[2] === 0x46 && 77 - arr[3] === 0x46 && 78 - arr[8] === 0x57 && 79 - arr[9] === 0x45 && 80 - arr[10] === 0x42 && 81 - arr[11] === 0x50 82 - ) { 83 - return 'image/webp'; 84 - } 192 + /** 193 + * Encode bytes as base32lower string 194 + * @param {Uint8Array} bytes - Bytes to encode 195 + * @returns {string} Base32lower-encoded string 196 + */ 197 + export function base32Encode(bytes) { 198 + const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'; 199 + let result = ''; 200 + let bits = 0; 201 + let value = 0; 85 202 86 - // ISOBMFF container: ....ftyp at byte 4 (MP4, AVIF, HEIC, etc.) 87 - if ( 88 - arr[4] === 0x66 && 89 - arr[5] === 0x74 && 90 - arr[6] === 0x79 && 91 - arr[7] === 0x70 92 - ) { 93 - // Check brand code at bytes 8-11 94 - const brand = String.fromCharCode(arr[8], arr[9], arr[10], arr[11]); 95 - if (brand === 'avif') { 96 - return 'image/avif'; 203 + for (const byte of bytes) { 204 + value = (value << 8) | byte; 205 + bits += 8; 206 + while (bits >= 5) { 207 + bits -= 5; 208 + result += alphabet[(value >> bits) & 31]; 97 209 } 98 - if (brand === 'heic' || brand === 'heix' || brand === 'mif1') { 99 - return 'image/heic'; 100 - } 101 - return 'video/mp4'; 102 210 } 103 211 104 - return null; 105 - } 212 + if (bits > 0) { 213 + result += alphabet[(value << (5 - bits)) & 31]; 214 + } 106 215 107 - // === BLOB REF DETECTION === 108 - // Recursively find blob references in records 216 + return result; 217 + } 109 218 110 219 /** 111 - * Find all blob CID references in a record 112 - * @param {*} obj - Record value to scan 113 - * @param {string[]} refs - Accumulator array (internal) 114 - * @returns {string[]} Array of blob CID strings 220 + * Decode base32lower string to bytes 221 + * @param {string} str - Base32lower-encoded string 222 + * @returns {Uint8Array} Decoded bytes 115 223 */ 116 - export function findBlobRefs(obj, refs = []) { 117 - if (!obj || typeof obj !== 'object') { 118 - return refs; 119 - } 120 - 121 - // Check if this object is a blob ref 122 - if (obj.$type === 'blob' && obj.ref?.$link) { 123 - refs.push(obj.ref.$link); 124 - } 224 + export function base32Decode(str) { 225 + const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'; 226 + let bits = 0; 227 + let value = 0; 228 + const output = []; 125 229 126 - // Recurse into arrays and objects 127 - if (Array.isArray(obj)) { 128 - for (const item of obj) { 129 - findBlobRefs(item, refs); 130 - } 131 - } else { 132 - for (const value of Object.values(obj)) { 133 - findBlobRefs(value, refs); 230 + for (const char of str) { 231 + const idx = alphabet.indexOf(char); 232 + if (idx === -1) continue; 233 + value = (value << 5) | idx; 234 + bits += 5; 235 + if (bits >= 8) { 236 + bits -= 8; 237 + output.push((value >> bits) & 0xff); 134 238 } 135 239 } 136 240 137 - return refs; 241 + return new Uint8Array(output); 138 242 } 139 243 140 - // === CRAWLER NOTIFICATION === 141 - // Notify relays to come crawl us after writes (like official PDS) 142 - let lastCrawlNotify = 0; 143 - const CRAWL_NOTIFY_THRESHOLD = 20 * 60 * 1000; // 20 minutes (matches official PDS) 144 - 145 - async function notifyCrawlers(env, hostname) { 146 - const now = Date.now(); 147 - if (now - lastCrawlNotify < CRAWL_NOTIFY_THRESHOLD) { 148 - return; // Throttle notifications 244 + /** 245 + * Encode bytes as base64url string (no padding) 246 + * @param {Uint8Array} bytes - Bytes to encode 247 + * @returns {string} Base64url-encoded string 248 + */ 249 + export function base64UrlEncode(bytes) { 250 + let binary = ''; 251 + for (const byte of bytes) { 252 + binary += String.fromCharCode(byte); 149 253 } 150 - 151 - const relayHost = env.RELAY_HOST; 152 - if (!relayHost) return; 254 + const base64 = btoa(binary); 255 + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 256 + } 153 257 154 - lastCrawlNotify = now; 258 + /** 259 + * Decode base64url string to bytes 260 + * @param {string} str - Base64url-encoded string 261 + * @returns {Uint8Array} Decoded bytes 262 + */ 263 + export function base64UrlDecode(str) { 264 + const base64 = str.replace(/-/g, '+').replace(/_/g, '/'); 265 + const pad = base64.length % 4; 266 + const padded = pad ? base64 + '='.repeat(4 - pad) : base64; 267 + const binary = atob(padded); 268 + const bytes = new Uint8Array(binary.length); 269 + for (let i = 0; i < binary.length; i++) { 270 + bytes[i] = binary.charCodeAt(i); 271 + } 272 + return bytes; 273 + } 155 274 156 - // Fire and forget - don't block writes on relay notification 157 - fetch(`${relayHost}/xrpc/com.atproto.sync.requestCrawl`, { 158 - method: 'POST', 159 - headers: { 'Content-Type': 'application/json' }, 160 - body: JSON.stringify({ hostname }), 161 - }).catch(() => { 162 - // Silently ignore relay notification failures 163 - }); 275 + /** 276 + * Encode integer as unsigned varint 277 + * @param {number} n - Non-negative integer 278 + * @returns {Uint8Array} Varint-encoded bytes 279 + */ 280 + export function varint(n) { 281 + const bytes = []; 282 + while (n >= 0x80) { 283 + bytes.push((n & 0x7f) | 0x80); 284 + n >>>= 7; 285 + } 286 + bytes.push(n); 287 + return new Uint8Array(bytes); 164 288 } 165 289 166 290 // === CID WRAPPER === 167 291 // Explicit CID type for DAG-CBOR encoding (avoids fragile heuristic detection) 168 292 169 293 class CID { 294 + /** @param {Uint8Array} bytes */ 170 295 constructor(bytes) { 171 296 if (!(bytes instanceof Uint8Array)) { 172 297 throw new Error('CID must be constructed with Uint8Array'); ··· 175 300 } 176 301 } 177 302 178 - // === CBOR ENCODING === 179 - // Minimal deterministic CBOR (RFC 8949) - sorted keys, minimal integers 303 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 304 + // โ•‘ CBOR ENCODING โ•‘ 305 + // โ•‘ RFC 8949 CBOR and DAG-CBOR for content-addressed data โ•‘ 306 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 180 307 181 308 /** 182 309 * Encode CBOR type header (major type + length) ··· 210 337 * @returns {Uint8Array} CBOR-encoded bytes 211 338 */ 212 339 export function cborEncode(value) { 340 + /** @type {number[]} */ 213 341 const parts = []; 214 342 343 + /** @param {*} val */ 215 344 function encode(val) { 216 345 if (val === null) { 217 346 parts.push(CBOR_NULL); ··· 242 371 } 243 372 } 244 373 374 + /** @param {number} n */ 245 375 function encodeInteger(n) { 246 376 if (n >= 0) { 247 377 encodeHead(parts, 0, n); // major type 0 = unsigned int ··· 254 384 return new Uint8Array(parts); 255 385 } 256 386 257 - // DAG-CBOR encoder that handles CIDs with tag 42 387 + /** 388 + * DAG-CBOR encoder that handles CIDs with tag 42 389 + * @param {*} value 390 + * @returns {Uint8Array} 391 + */ 258 392 function cborEncodeDagCbor(value) { 393 + /** @type {number[]} */ 259 394 const parts = []; 260 395 396 + /** @param {*} val */ 261 397 function encode(val) { 262 398 if (val === null) { 263 399 parts.push(CBOR_NULL); ··· 319 455 export function cborDecode(bytes) { 320 456 let offset = 0; 321 457 458 + /** @returns {*} */ 322 459 function read() { 323 460 const initial = bytes[offset++]; 324 461 const major = initial >> 5; ··· 364 501 } 365 502 case 5: { 366 503 // map 504 + /** @type {Record<string, *>} */ 367 505 const obj = {}; 368 506 for (let i = 0; i < length; i++) { 369 - const key = read(); 507 + const key = /** @type {string} */ (read()); 370 508 obj[key] = read(); 371 509 } 372 510 return obj; ··· 394 532 return read(); 395 533 } 396 534 397 - // === CID GENERATION === 398 - // CID codec constants 399 - const CODEC_DAG_CBOR = 0x71; 400 - const CODEC_RAW = 0x55; 535 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 536 + // โ•‘ CONTENT IDENTIFIERS โ•‘ 537 + // โ•‘ CIDs (content hashes) and TIDs (timestamp IDs) โ•‘ 538 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 401 539 402 540 /** 403 541 * Create a CIDv1 with SHA-256 hash ··· 406 544 * @returns {Promise<Uint8Array>} CID bytes (36 bytes: version + codec + multihash) 407 545 */ 408 546 async function createCidWithCodec(bytes, codec) { 409 - const hash = await crypto.subtle.digest('SHA-256', bytes); 547 + const hash = await crypto.subtle.digest('SHA-256', /** @type {BufferSource} */(bytes)); 410 548 const hashBytes = new Uint8Array(hash); 411 549 412 550 // CIDv1: version(1) + codec + multihash(sha256) ··· 450 588 } 451 589 452 590 /** 453 - * Encode bytes as base32lower string 454 - * @param {Uint8Array} bytes - Bytes to encode 455 - * @returns {string} Base32lower-encoded string 591 + * Convert base32lower CID string to raw bytes 592 + * @param {string} cidStr - CID string with 'b' prefix 593 + * @returns {Uint8Array} CID bytes 456 594 */ 457 - export function base32Encode(bytes) { 458 - const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'; 459 - let result = ''; 460 - let bits = 0; 461 - let value = 0; 462 - 463 - for (const byte of bytes) { 464 - value = (value << 8) | byte; 465 - bits += 8; 466 - while (bits >= 5) { 467 - bits -= 5; 468 - result += alphabet[(value >> bits) & 31]; 469 - } 470 - } 471 - 472 - if (bits > 0) { 473 - result += alphabet[(value << (5 - bits)) & 31]; 474 - } 475 - 476 - return result; 595 + export function cidToBytes(cidStr) { 596 + // Decode base32lower CID string to bytes 597 + if (!cidStr.startsWith('b')) throw new Error('expected base32lower CID'); 598 + return base32Decode(cidStr.slice(1)); 477 599 } 478 600 479 - // === TID GENERATION === 480 - // Timestamp-based IDs: base32-sort encoded microseconds + clock ID 481 - 482 - const TID_CHARS = '234567abcdefghijklmnopqrstuvwxyz'; 483 - let lastTimestamp = 0; 484 - const clockId = Math.floor(Math.random() * 1024); 485 - 486 601 /** 487 602 * Generate a timestamp-based ID (TID) for record keys 488 603 * Monotonic within a process, sortable by time ··· 514 629 return tid; 515 630 } 516 631 517 - // === P-256 SIGNING === 518 - // Web Crypto ECDSA with P-256 curve 632 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 633 + // โ•‘ CRYPTOGRAPHY โ•‘ 634 + // โ•‘ P-256 signing with low-S normalization, key management โ•‘ 635 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 636 + 637 + /** 638 + * @param {BufferSource} data 639 + * @returns {Promise<Uint8Array>} 640 + */ 641 + async function sha256(data) { 642 + const hash = await crypto.subtle.digest('SHA-256', data); 643 + return new Uint8Array(hash); 644 + } 519 645 520 646 /** 521 647 * Import a raw P-256 private key for signing ··· 546 672 547 673 return crypto.subtle.importKey( 548 674 'pkcs8', 549 - pkcs8, 675 + /** @type {BufferSource} */(pkcs8), 550 676 { name: 'ECDSA', namedCurve: 'P-256' }, 551 677 false, 552 678 ['sign'], 553 679 ); 554 680 } 555 681 556 - // P-256 curve order N 557 - const P256_N = BigInt( 558 - '0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551', 559 - ); 560 - const P256_N_DIV_2 = P256_N / 2n; 561 - 562 - function bytesToBigInt(bytes) { 563 - let result = 0n; 564 - for (const byte of bytes) { 565 - result = (result << 8n) | BigInt(byte); 566 - } 567 - return result; 568 - } 569 - 570 - function bigIntToBytes(n, length) { 571 - const bytes = new Uint8Array(length); 572 - for (let i = length - 1; i >= 0; i--) { 573 - bytes[i] = Number(n & 0xffn); 574 - n >>= 8n; 575 - } 576 - return bytes; 577 - } 578 - 579 682 /** 580 683 * Sign data with ECDSA P-256, returning low-S normalized signature 581 684 * @param {CryptoKey} privateKey - Web Crypto key from importPrivateKey ··· 586 689 const signature = await crypto.subtle.sign( 587 690 { name: 'ECDSA', hash: 'SHA-256' }, 588 691 privateKey, 589 - data, 692 + /** @type {BufferSource} */(data), 590 693 ); 591 694 const sig = new Uint8Array(signature); 592 695 ··· 621 724 622 725 // Export private key as raw bytes 623 726 const privateJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey); 624 - const privateBytes = base64UrlDecode(privateJwk.d); 727 + const privateBytes = base64UrlDecode(/** @type {string} */(privateJwk.d)); 625 728 626 729 // Export public key as compressed point 627 730 const publicRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey); ··· 631 734 return { privateKey: privateBytes, publicKey: compressed }; 632 735 } 633 736 737 + /** 738 + * @param {Uint8Array} uncompressed 739 + * @returns {Uint8Array} 740 + */ 634 741 function compressPublicKey(uncompressed) { 635 742 // uncompressed is 65 bytes: 0x04 + x(32) + y(32) 636 743 // compressed is 33 bytes: prefix(02 or 03) + x(32) ··· 643 750 return compressed; 644 751 } 645 752 646 - /** 647 - * Encode bytes as base64url string (no padding) 648 - * @param {Uint8Array} bytes - Bytes to encode 649 - * @returns {string} Base64url-encoded string 650 - */ 651 - export function base64UrlEncode(bytes) { 652 - let binary = ''; 653 - for (const byte of bytes) { 654 - binary += String.fromCharCode(byte); 655 - } 656 - const base64 = btoa(binary); 657 - return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 658 - } 659 - 660 - /** 661 - * Decode base64url string to bytes 662 - * @param {string} str - Base64url-encoded string 663 - * @returns {Uint8Array} Decoded bytes 664 - */ 665 - export function base64UrlDecode(str) { 666 - const base64 = str.replace(/-/g, '+').replace(/_/g, '/'); 667 - const pad = base64.length % 4; 668 - const padded = pad ? base64 + '='.repeat(4 - pad) : base64; 669 - const binary = atob(padded); 670 - const bytes = new Uint8Array(binary.length); 671 - for (let i = 0; i < binary.length; i++) { 672 - bytes[i] = binary.charCodeAt(i); 673 - } 674 - return bytes; 675 - } 753 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 754 + // โ•‘ AUTHENTICATION โ•‘ 755 + // โ•‘ JWT creation/verification for sessions and service auth โ•‘ 756 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 676 757 677 758 /** 678 759 * Create HMAC-SHA256 signature for JWT ··· 683 764 async function hmacSign(data, secret) { 684 765 const key = await crypto.subtle.importKey( 685 766 'raw', 686 - new TextEncoder().encode(secret), 767 + /** @type {BufferSource} */(new TextEncoder().encode(secret)), 687 768 { name: 'HMAC', hash: 'SHA-256' }, 688 769 false, 689 770 ['sign'], ··· 691 772 const sig = await crypto.subtle.sign( 692 773 'HMAC', 693 774 key, 694 - new TextEncoder().encode(data), 775 + /** @type {BufferSource} */(new TextEncoder().encode(data)), 695 776 ); 696 777 return base64UrlEncode(new Uint8Array(sig)); 697 778 } ··· 765 846 * @param {string} jwt - JWT string to verify 766 847 * @param {string} secret - JWT signing secret 767 848 * @param {string} expectedType - Expected token type (e.g., 'at+jwt', 'refresh+jwt') 768 - * @returns {Promise<{header: Object, payload: Object}>} Decoded header and payload 849 + * @returns {Promise<{header: {typ: string, alg: string}, payload: JwtPayload}>} Decoded header and payload 769 850 * @throws {Error} If token is invalid, expired, or wrong type 770 851 */ 771 852 async function verifyJwt(jwt, secret, expectedType) { ··· 808 889 * Verify and decode an access JWT 809 890 * @param {string} jwt - JWT string to verify 810 891 * @param {string} secret - JWT signing secret 811 - * @returns {Promise<Object>} Decoded payload 892 + * @returns {Promise<JwtPayload>} Decoded payload 812 893 * @throws {Error} If token is invalid, expired, or wrong type 813 894 */ 814 895 export async function verifyAccessJwt(jwt, secret) { ··· 820 901 * Verify and decode a refresh JWT 821 902 * @param {string} jwt - JWT string to verify 822 903 * @param {string} secret - JWT signing secret 823 - * @returns {Promise<Object>} Decoded payload 904 + * @returns {Promise<JwtPayload>} Decoded payload 824 905 * @throws {Error} If token is invalid, expired, or wrong type 825 906 */ 826 907 export async function verifyRefreshJwt(jwt, secret) { ··· 853 934 crypto.getRandomValues(jtiBytes); 854 935 const jti = bytesToHex(jtiBytes); 855 936 937 + /** @type {{ iss: string, aud: string, exp: number, iat: number, jti: string, lxm?: string }} */ 856 938 const payload = { 857 939 iss, 858 940 aud, ··· 876 958 return `${headerB64}.${payloadB64}.${sigB64}`; 877 959 } 878 960 879 - /** 880 - * Convert bytes to hexadecimal string 881 - * @param {Uint8Array} bytes - Bytes to convert 882 - * @returns {string} Hex string 883 - */ 884 - export function bytesToHex(bytes) { 885 - return Array.from(bytes) 886 - .map((b) => b.toString(16).padStart(2, '0')) 887 - .join(''); 888 - } 889 - 890 - /** 891 - * Convert hexadecimal string to bytes 892 - * @param {string} hex - Hex string 893 - * @returns {Uint8Array} Decoded bytes 894 - */ 895 - export function hexToBytes(hex) { 896 - const bytes = new Uint8Array(hex.length / 2); 897 - for (let i = 0; i < hex.length; i += 2) { 898 - bytes[i / 2] = parseInt(hex.substr(i, 2), 16); 899 - } 900 - return bytes; 901 - } 902 - 903 - // === MERKLE SEARCH TREE === 904 - // ATProto-compliant MST implementation 905 - 906 - async function sha256(data) { 907 - const hash = await crypto.subtle.digest('SHA-256', data); 908 - return new Uint8Array(hash); 909 - } 961 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 962 + // โ•‘ MERKLE SEARCH TREE โ•‘ 963 + // โ•‘ MST for ATProto repository structure โ•‘ 964 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 910 965 911 966 // Cache for key depths (SHA-256 is expensive) 912 967 const keyDepthCache = new Map(); ··· 945 1000 return depth; 946 1001 } 947 1002 948 - // Compute common prefix length between two byte arrays 1003 + /** 1004 + * Compute common prefix length between two byte arrays 1005 + * @param {Uint8Array} a 1006 + * @param {Uint8Array} b 1007 + * @returns {number} 1008 + */ 949 1009 function commonPrefixLen(a, b) { 950 1010 const minLen = Math.min(a.length, b.length); 951 1011 for (let i = 0; i < minLen; i++) { ··· 955 1015 } 956 1016 957 1017 class MST { 1018 + /** @param {SqlStorage} sql */ 958 1019 constructor(sql) { 959 1020 this.sql = sql; 960 1021 } ··· 982 1043 entries.push({ 983 1044 key, 984 1045 keyBytes: new TextEncoder().encode(key), 985 - cid: r.cid, 1046 + cid: /** @type {string} */ (r.cid), 986 1047 depth, 987 1048 }); 988 1049 } ··· 991 1052 return this.buildTree(entries, maxDepth); 992 1053 } 993 1054 1055 + /** 1056 + * @param {Array<{key: string, keyBytes: Uint8Array, cid: string, depth: number}>} entries 1057 + * @param {number} layer 1058 + * @returns {Promise<string|null>} 1059 + */ 994 1060 async buildTree(entries, layer) { 995 1061 if (entries.length === 0) return null; 996 1062 997 1063 // Separate entries for this layer vs lower layers (subtrees) 998 1064 // Keys with depth == layer stay at this node 999 1065 // Keys with depth < layer go into subtrees (going down toward layer 0) 1066 + /** @type {Array<{type: 'subtree', cid: string|null} | {type: 'entry', entry: {key: string, keyBytes: Uint8Array, cid: string, depth: number}}>} */ 1000 1067 const thisLayer = []; 1068 + /** @type {Array<{key: string, keyBytes: Uint8Array, cid: string, depth: number}>} */ 1001 1069 let leftSubtree = []; 1002 1070 1003 1071 for (const entry of entries) { ··· 1023 1091 } 1024 1092 1025 1093 // Build node with proper ATProto format 1094 + /** @type {{ e: Array<{p: number, k: Uint8Array, v: CID, t: CID|null}>, l?: CID|null }} */ 1026 1095 const node = { e: [] }; 1096 + /** @type {string|null} */ 1027 1097 let leftCid = null; 1028 1098 let prevKeyBytes = new Uint8Array(0); 1029 1099 ··· 1035 1105 leftCid = item.cid; 1036 1106 } else { 1037 1107 // Attach to previous entry's 't' field 1038 - node.e[node.e.length - 1].t = new CID(cidToBytes(item.cid)); 1108 + if (item.cid !== null) { 1109 + node.e[node.e.length - 1].t = new CID(cidToBytes(item.cid)); 1110 + } 1039 1111 } 1040 1112 } else { 1041 1113 // Entry - compute prefix compression ··· 1052 1124 }; 1053 1125 1054 1126 node.e.push(e); 1055 - prevKeyBytes = keyBytes; 1127 + prevKeyBytes = /** @type {Uint8Array<ArrayBuffer>} */ (keyBytes); 1056 1128 } 1057 1129 } 1058 1130 ··· 1074 1146 } 1075 1147 } 1076 1148 1077 - // === CAR FILE BUILDER === 1078 - 1079 - /** 1080 - * Encode integer as unsigned varint 1081 - * @param {number} n - Non-negative integer 1082 - * @returns {Uint8Array} Varint-encoded bytes 1083 - */ 1084 - export function varint(n) { 1085 - const bytes = []; 1086 - while (n >= 0x80) { 1087 - bytes.push((n & 0x7f) | 0x80); 1088 - n >>>= 7; 1089 - } 1090 - bytes.push(n); 1091 - return new Uint8Array(bytes); 1092 - } 1093 - 1094 - /** 1095 - * Convert base32lower CID string to raw bytes 1096 - * @param {string} cidStr - CID string with 'b' prefix 1097 - * @returns {Uint8Array} CID bytes 1098 - */ 1099 - export function cidToBytes(cidStr) { 1100 - // Decode base32lower CID string to bytes 1101 - if (!cidStr.startsWith('b')) throw new Error('expected base32lower CID'); 1102 - return base32Decode(cidStr.slice(1)); 1103 - } 1104 - 1105 - /** 1106 - * Decode base32lower string to bytes 1107 - * @param {string} str - Base32lower-encoded string 1108 - * @returns {Uint8Array} Decoded bytes 1109 - */ 1110 - export function base32Decode(str) { 1111 - const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'; 1112 - let bits = 0; 1113 - let value = 0; 1114 - const output = []; 1115 - 1116 - for (const char of str) { 1117 - const idx = alphabet.indexOf(char); 1118 - if (idx === -1) continue; 1119 - value = (value << 5) | idx; 1120 - bits += 5; 1121 - if (bits >= 8) { 1122 - bits -= 8; 1123 - output.push((value >> bits) & 0xff); 1124 - } 1125 - } 1126 - 1127 - return new Uint8Array(output); 1128 - } 1149 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 1150 + // โ•‘ CAR FILES โ•‘ 1151 + // โ•‘ Content Addressable aRchive format for repo sync โ•‘ 1152 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 1129 1153 1130 1154 /** 1131 1155 * Build a CAR (Content Addressable aRchive) file ··· 1166 1190 return car; 1167 1191 } 1168 1192 1193 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 1194 + // โ•‘ BLOB HANDLING โ•‘ 1195 + // โ•‘ MIME detection, blob reference scanning โ•‘ 1196 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 1197 + 1198 + /** 1199 + * Sniff MIME type from file magic bytes 1200 + * @param {Uint8Array|ArrayBuffer} bytes - File bytes (only first 12 needed) 1201 + * @returns {string|null} Detected MIME type or null if unknown 1202 + */ 1203 + export function sniffMimeType(bytes) { 1204 + const arr = new Uint8Array(bytes.slice(0, 12)); 1205 + 1206 + // JPEG: FF D8 FF 1207 + if (arr[0] === 0xff && arr[1] === 0xd8 && arr[2] === 0xff) { 1208 + return 'image/jpeg'; 1209 + } 1210 + 1211 + // PNG: 89 50 4E 47 0D 0A 1A 0A 1212 + if ( 1213 + arr[0] === 0x89 && 1214 + arr[1] === 0x50 && 1215 + arr[2] === 0x4e && 1216 + arr[3] === 0x47 && 1217 + arr[4] === 0x0d && 1218 + arr[5] === 0x0a && 1219 + arr[6] === 0x1a && 1220 + arr[7] === 0x0a 1221 + ) { 1222 + return 'image/png'; 1223 + } 1224 + 1225 + // GIF: 47 49 46 38 (GIF8) 1226 + if ( 1227 + arr[0] === 0x47 && 1228 + arr[1] === 0x49 && 1229 + arr[2] === 0x46 && 1230 + arr[3] === 0x38 1231 + ) { 1232 + return 'image/gif'; 1233 + } 1234 + 1235 + // WebP: RIFF....WEBP 1236 + if ( 1237 + arr[0] === 0x52 && 1238 + arr[1] === 0x49 && 1239 + arr[2] === 0x46 && 1240 + arr[3] === 0x46 && 1241 + arr[8] === 0x57 && 1242 + arr[9] === 0x45 && 1243 + arr[10] === 0x42 && 1244 + arr[11] === 0x50 1245 + ) { 1246 + return 'image/webp'; 1247 + } 1248 + 1249 + // ISOBMFF container: ....ftyp at byte 4 (MP4, AVIF, HEIC, etc.) 1250 + if ( 1251 + arr[4] === 0x66 && 1252 + arr[5] === 0x74 && 1253 + arr[6] === 0x79 && 1254 + arr[7] === 0x70 1255 + ) { 1256 + // Check brand code at bytes 8-11 1257 + const brand = String.fromCharCode(arr[8], arr[9], arr[10], arr[11]); 1258 + if (brand === 'avif') { 1259 + return 'image/avif'; 1260 + } 1261 + if (brand === 'heic' || brand === 'heix' || brand === 'mif1') { 1262 + return 'image/heic'; 1263 + } 1264 + return 'video/mp4'; 1265 + } 1266 + 1267 + return null; 1268 + } 1269 + 1270 + /** 1271 + * Find all blob CID references in a record 1272 + * @param {*} obj - Record value to scan 1273 + * @param {string[]} refs - Accumulator array (internal) 1274 + * @returns {string[]} Array of blob CID strings 1275 + */ 1276 + export function findBlobRefs(obj, refs = []) { 1277 + if (!obj || typeof obj !== 'object') { 1278 + return refs; 1279 + } 1280 + 1281 + // Check if this object is a blob ref 1282 + if (obj.$type === 'blob' && obj.ref?.$link) { 1283 + refs.push(obj.ref.$link); 1284 + } 1285 + 1286 + // Recurse into arrays and objects 1287 + if (Array.isArray(obj)) { 1288 + for (const item of obj) { 1289 + findBlobRefs(item, refs); 1290 + } 1291 + } else { 1292 + for (const value of Object.values(obj)) { 1293 + findBlobRefs(value, refs); 1294 + } 1295 + } 1296 + 1297 + return refs; 1298 + } 1299 + 1300 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 1301 + // โ•‘ RELAY NOTIFICATION โ•‘ 1302 + // โ•‘ Notify relays to crawl after repo updates โ•‘ 1303 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 1304 + 1305 + /** 1306 + * Notify relays to come crawl us after writes (like official PDS) 1307 + * @param {{ RELAY_HOST?: string }} env 1308 + * @param {string} hostname 1309 + */ 1310 + async function notifyCrawlers(env, hostname) { 1311 + const now = Date.now(); 1312 + if (now - lastCrawlNotify < CRAWL_NOTIFY_THRESHOLD) { 1313 + return; // Throttle notifications 1314 + } 1315 + 1316 + const relayHost = env.RELAY_HOST; 1317 + if (!relayHost) return; 1318 + 1319 + lastCrawlNotify = now; 1320 + 1321 + // Fire and forget - don't block writes on relay notification 1322 + fetch(`${relayHost}/xrpc/com.atproto.sync.requestCrawl`, { 1323 + method: 'POST', 1324 + headers: { 'Content-Type': 'application/json' }, 1325 + body: JSON.stringify({ hostname }), 1326 + }).catch(() => { 1327 + // Silently ignore relay notification failures 1328 + }); 1329 + } 1330 + 1331 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 1332 + // โ•‘ ROUTING โ•‘ 1333 + // โ•‘ XRPC endpoint definitions โ•‘ 1334 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 1335 + 1169 1336 /** 1170 1337 * Route handler function type 1171 1338 * @callback RouteHandler 1172 1339 * @param {PersonalDataServer} pds - PDS instance 1173 1340 * @param {Request} request - HTTP request 1174 1341 * @param {URL} url - Parsed URL 1175 - * @returns {Promise<Response>} HTTP response 1342 + * @returns {Response | Promise<Response>} HTTP response 1176 1343 */ 1177 1344 1178 1345 /** 1346 + * Route definition for the PDS router 1179 1347 * @typedef {Object} Route 1180 1348 * @property {string} [method] - Required HTTP method (default: any) 1181 1349 * @property {RouteHandler} handler - Handler function ··· 1291 1459 }, 1292 1460 }; 1293 1461 1462 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 1463 + // โ•‘ PERSONAL DATA SERVER โ•‘ 1464 + // โ•‘ Durable Object class implementing ATProto PDS โ•‘ 1465 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 1466 + 1294 1467 export class PersonalDataServer { 1468 + /** @type {string | undefined} */ 1469 + _did; 1470 + 1471 + /** 1472 + * @param {DurableObjectState} state 1473 + * @param {Env} env 1474 + */ 1295 1475 constructor(state, env) { 1296 1476 this.state = state; 1297 1477 this.sql = state.storage.sql; ··· 1345 1525 `); 1346 1526 } 1347 1527 1528 + /** 1529 + * @param {string} did 1530 + * @param {string} privateKeyHex 1531 + * @param {string|null} [handle] 1532 + */ 1348 1533 async initIdentity(did, privateKeyHex, handle = null) { 1349 1534 await this.state.storage.put('did', did); 1350 1535 await this.state.storage.put('privateKey', privateKeyHex); ··· 1373 1558 async getSigningKey() { 1374 1559 const hex = await this.state.storage.get('privateKey'); 1375 1560 if (!hex) return null; 1376 - return importPrivateKey(hexToBytes(hex)); 1561 + return importPrivateKey(hexToBytes(/** @type {string} */(hex))); 1377 1562 } 1378 1563 1379 - // Collect MST node blocks for a given root CID 1564 + /** 1565 + * Collect MST node blocks for a given root CID 1566 + * @param {string} rootCidStr 1567 + * @returns {Array<{cid: string, data: Uint8Array}>} 1568 + */ 1380 1569 collectMstBlocks(rootCidStr) { 1570 + /** @type {Array<{cid: string, data: Uint8Array}>} */ 1381 1571 const blocks = []; 1382 1572 const visited = new Set(); 1383 1573 1574 + /** @param {string} cidStr */ 1384 1575 const collect = (cidStr) => { 1385 1576 if (visited.has(cidStr)) return; 1386 1577 visited.add(cidStr); 1387 1578 1388 - const rows = this.sql 1579 + const rows = /** @type {BlockRow[]} */ (this.sql 1389 1580 .exec(`SELECT data FROM blocks WHERE cid = ?`, cidStr) 1390 - .toArray(); 1581 + .toArray()); 1391 1582 if (rows.length === 0) return; 1392 1583 1393 1584 const data = new Uint8Array(rows[0].data); ··· 1411 1602 return blocks; 1412 1603 } 1413 1604 1605 + /** 1606 + * @param {string} collection 1607 + * @param {Record<string, *>} record 1608 + * @param {string|null} [rkey] 1609 + * @returns {Promise<{uri: string, cid: string, commit: string}>} 1610 + */ 1414 1611 async createRecord(collection, record, rkey = null) { 1415 1612 const did = await this.getDid(); 1416 1613 if (!did) throw new Error('PDS not initialized'); ··· 1478 1675 const commit = { 1479 1676 did, 1480 1677 version: 3, 1481 - data: new CID(cidToBytes(dataRoot)), // CID wrapped for explicit encoding 1678 + data: new CID(cidToBytes(/** @type {string} */(dataRoot))), // CID wrapped for explicit encoding 1482 1679 rev, 1483 - prev: prevCommit?.cid ? new CID(cidToBytes(prevCommit.cid)) : null, 1680 + prev: prevCommit?.cid ? new CID(cidToBytes(/** @type {string} */(prevCommit.cid))) : null, 1484 1681 }; 1485 1682 1486 1683 // Sign commit (using dag-cbor encoder for CIDs) 1487 1684 const commitBytes = cborEncodeDagCbor(commit); 1488 1685 const signingKey = await this.getSigningKey(); 1686 + if (!signingKey) throw new Error('No signing key'); 1489 1687 const sig = await sign(signingKey, commitBytes); 1490 1688 1491 1689 const signedCommit = { ...commit, sig }; ··· 1520 1718 // Add commit block 1521 1719 newBlocks.push({ cid: commitCidStr, data: signedBytes }); 1522 1720 // Add MST node blocks (get all blocks referenced by commit.data) 1523 - const mstBlocks = this.collectMstBlocks(dataRoot); 1721 + const mstBlocks = this.collectMstBlocks(/** @type {string} */(dataRoot)); 1524 1722 newBlocks.push(...mstBlocks); 1525 1723 1526 1724 // Sequence event with blocks - store complete event data including rev and time ··· 1542 1740 ); 1543 1741 1544 1742 // Broadcast to subscribers (both local and via default DO for relay) 1545 - const evtRows = this.sql 1743 + const evtRows = /** @type {SeqEventRow[]} */ (this.sql 1546 1744 .exec(`SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1`) 1547 - .toArray(); 1745 + .toArray()); 1548 1746 if (evtRows.length > 0) { 1549 1747 this.broadcastEvent(evtRows[0]); 1550 1748 // Also forward to default DO for relay subscribers ··· 1562 1760 body: JSON.stringify({ ...row, evt: evtArray }), 1563 1761 }), 1564 1762 ) 1565 - .catch(() => {}); // Ignore forward errors 1763 + .catch(() => { }); // Ignore forward errors 1566 1764 } 1567 1765 } 1568 1766 1569 1767 return { uri, cid: recordCidStr, commit: commitCidStr }; 1570 1768 } 1571 1769 1770 + /** 1771 + * @param {string} collection 1772 + * @param {string} rkey 1773 + */ 1572 1774 async deleteRecord(collection, rkey) { 1573 1775 const did = await this.getDid(); 1574 1776 if (!did) throw new Error('PDS not initialized'); ··· 1602 1804 1603 1805 if (stillReferenced.length === 0) { 1604 1806 // Blob is orphaned, delete from R2 and database 1605 - await this.env.BLOBS.delete(`${did}/${blobCid}`); 1807 + await this.env?.BLOBS?.delete(`${did}/${blobCid}`); 1606 1808 this.sql.exec('DELETE FROM blob WHERE cid = ?', blobCid); 1607 1809 } 1608 1810 } ··· 1622 1824 const commit = { 1623 1825 did, 1624 1826 version: 3, 1625 - data: dataRoot ? new CID(cidToBytes(dataRoot)) : null, 1827 + data: dataRoot ? new CID(cidToBytes(/** @type {string} */(dataRoot))) : null, 1626 1828 rev, 1627 - prev: prevCommit?.cid ? new CID(cidToBytes(prevCommit.cid)) : null, 1829 + prev: prevCommit?.cid ? new CID(cidToBytes(/** @type {string} */(prevCommit.cid))) : null, 1628 1830 }; 1629 1831 1630 1832 // Sign commit 1631 1833 const commitBytes = cborEncodeDagCbor(commit); 1632 1834 const signingKey = await this.getSigningKey(); 1835 + if (!signingKey) throw new Error('No signing key'); 1633 1836 const sig = await sign(signingKey, commitBytes); 1634 1837 1635 1838 const signedCommit = { ...commit, sig }; ··· 1660 1863 const newBlocks = []; 1661 1864 newBlocks.push({ cid: commitCidStr, data: signedBytes }); 1662 1865 if (dataRoot) { 1663 - const mstBlocks = this.collectMstBlocks(dataRoot); 1866 + const mstBlocks = this.collectMstBlocks(/** @type {string} */(dataRoot)); 1664 1867 newBlocks.push(...mstBlocks); 1665 1868 } 1666 1869 ··· 1680 1883 ); 1681 1884 1682 1885 // Broadcast to subscribers 1683 - const evtRows = this.sql 1886 + const evtRows = /** @type {SeqEventRow[]} */ (this.sql 1684 1887 .exec(`SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1`) 1685 - .toArray(); 1888 + .toArray()); 1686 1889 if (evtRows.length > 0) { 1687 1890 this.broadcastEvent(evtRows[0]); 1688 1891 // Forward to default DO for relay subscribers ··· 1698 1901 body: JSON.stringify({ ...row, evt: evtArray }), 1699 1902 }), 1700 1903 ) 1701 - .catch(() => {}); // Ignore forward errors 1904 + .catch(() => { }); // Ignore forward errors 1702 1905 } 1703 1906 } 1704 1907 1705 1908 return { ok: true }; 1706 1909 } 1707 1910 1911 + /** 1912 + * @param {SeqEventRow} evt 1913 + * @returns {Uint8Array} 1914 + */ 1708 1915 formatEvent(evt) { 1709 1916 // AT Protocol frame format: header + body 1710 1917 // Use DAG-CBOR encoding for body (CIDs need tag 42 + 0x00 prefix) ··· 1712 1919 1713 1920 // Decode stored event to get ops, blocks, rev, and time 1714 1921 const evtData = cborDecode(new Uint8Array(evt.evt)); 1715 - const ops = evtData.ops.map((op) => ({ 1922 + /** @type {Array<{action: string, path: string, cid: CID|null}>} */ 1923 + const ops = evtData.ops.map((/** @type {{action: string, path: string, cid?: string}} */ op) => ({ 1716 1924 ...op, 1717 1925 cid: op.cid ? new CID(cidToBytes(op.cid)) : null, // Wrap in CID class for tag 42 encoding 1718 1926 })); ··· 1740 1948 return frame; 1741 1949 } 1742 1950 1951 + /** 1952 + * @param {WebSocket} ws 1953 + * @param {string | ArrayBuffer} message 1954 + */ 1743 1955 async webSocketMessage(ws, message) { 1744 1956 // Handle ping 1745 1957 if (message === 'ping') ws.send('pong'); 1746 1958 } 1747 1959 1960 + /** 1961 + * @param {WebSocket} _ws 1962 + * @param {number} _code 1963 + * @param {string} _reason 1964 + */ 1748 1965 async webSocketClose(_ws, _code, _reason) { 1749 1966 // Durable Object will hibernate when no connections remain 1750 1967 } 1751 1968 1969 + /** 1970 + * @param {SeqEventRow} evt 1971 + */ 1752 1972 broadcastEvent(evt) { 1753 1973 const frame = this.formatEvent(evt); 1754 1974 for (const ws of this.state.getWebSockets()) { ··· 1763 1983 async handleAtprotoDid() { 1764 1984 let did = await this.getDid(); 1765 1985 if (!did) { 1986 + /** @type {string[]} */ 1766 1987 const registeredDids = 1767 1988 (await this.state.storage.get('registeredDids')) || []; 1768 1989 did = registeredDids[0]; ··· 1770 1991 if (!did) { 1771 1992 return new Response('User not found', { status: 404 }); 1772 1993 } 1773 - return new Response(did, { headers: { 'Content-Type': 'text/plain' } }); 1994 + return new Response(/** @type {string} */(did), { headers: { 'Content-Type': 'text/plain' } }); 1774 1995 } 1775 1996 1997 + /** @param {Request} request */ 1776 1998 async handleInit(request) { 1777 1999 const body = await request.json(); 1778 2000 if (!body.did || !body.privateKey) { ··· 1801 2023 return Response.json({ ok: true, message: 'repo data cleared' }); 1802 2024 } 1803 2025 2026 + /** @param {Request} request */ 1804 2027 async handleForwardEvent(request) { 1805 2028 const evt = await request.json(); 1806 2029 const numSockets = [...this.state.getWebSockets()].length; ··· 1813 2036 return Response.json({ ok: true, sockets: numSockets }); 1814 2037 } 1815 2038 2039 + /** @param {Request} request */ 1816 2040 async handleRegisterDid(request) { 1817 2041 const body = await request.json(); 2042 + /** @type {string[]} */ 1818 2043 const registeredDids = 1819 2044 (await this.state.storage.get('registeredDids')) || []; 1820 2045 if (!registeredDids.includes(body.did)) { ··· 1830 2055 return Response.json({ dids: registeredDids }); 1831 2056 } 1832 2057 2058 + /** @param {Request} request */ 1833 2059 async handleRegisterHandle(request) { 1834 2060 const body = await request.json(); 1835 2061 const { handle, did } = body; 1836 2062 if (!handle || !did) { 1837 2063 return errorResponse('InvalidRequest', 'missing handle or did', 400); 1838 2064 } 2065 + /** @type {Record<string, string>} */ 1839 2066 const handleMap = (await this.state.storage.get('handleMap')) || {}; 1840 2067 handleMap[handle] = did; 1841 2068 await this.state.storage.put('handleMap', handleMap); 1842 2069 return Response.json({ ok: true }); 1843 2070 } 1844 2071 2072 + /** @param {URL} url */ 1845 2073 async handleResolveHandle(url) { 1846 2074 const handle = url.searchParams.get('handle'); 1847 2075 if (!handle) { 1848 2076 return errorResponse('InvalidRequest', 'missing handle', 400); 1849 2077 } 2078 + /** @type {Record<string, string>} */ 1850 2079 const handleMap = (await this.state.storage.get('handleMap')) || {}; 1851 2080 const did = handleMap[handle]; 1852 2081 if (!did) { ··· 1861 2090 return Response.json({ head: head || null, rev: rev || null }); 1862 2091 } 1863 2092 2093 + /** @param {Request} request */ 1864 2094 handleDescribeServer(request) { 1865 2095 const hostname = request.headers.get('x-hostname') || 'localhost'; 1866 2096 return Response.json({ ··· 1873 2103 }); 1874 2104 } 1875 2105 2106 + /** @param {Request} request */ 1876 2107 async handleCreateSession(request) { 1877 2108 const body = await request.json(); 1878 2109 const { identifier, password } = body; ··· 1899 2130 let did = identifier; 1900 2131 if (!identifier.startsWith('did:')) { 1901 2132 // Try to resolve handle 2133 + /** @type {Record<string, string>} */ 1902 2134 const handleMap = (await this.state.storage.get('handleMap')) || {}; 1903 2135 did = handleMap[identifier]; 1904 2136 if (!did) { ··· 1931 2163 }); 1932 2164 } 1933 2165 2166 + /** @param {Request} request */ 1934 2167 async handleGetSession(request) { 1935 2168 const authHeader = request.headers.get('Authorization'); 1936 2169 if (!authHeader || !authHeader.startsWith('Bearer ')) { ··· 1962 2195 active: true, 1963 2196 }); 1964 2197 } catch (err) { 1965 - return errorResponse('InvalidToken', err.message, 401); 2198 + const message = err instanceof Error ? err.message : String(err); 2199 + return errorResponse('InvalidToken', message, 401); 1966 2200 } 1967 2201 } 1968 2202 2203 + /** @param {Request} request */ 1969 2204 async handleRefreshSession(request) { 1970 2205 const authHeader = request.headers.get('Authorization'); 1971 2206 if (!authHeader || !authHeader.startsWith('Bearer ')) { ··· 2003 2238 active: true, 2004 2239 }); 2005 2240 } catch (err) { 2006 - if (err.message === 'Token expired') { 2241 + const message = err instanceof Error ? err.message : String(err); 2242 + if (message === 'Token expired') { 2007 2243 return errorResponse('ExpiredToken', 'Refresh token has expired', 400); 2008 2244 } 2009 - return errorResponse('InvalidToken', err.message, 400); 2245 + return errorResponse('InvalidToken', message, 400); 2010 2246 } 2011 2247 } 2012 2248 2249 + /** @param {Request} _request */ 2013 2250 async handleGetPreferences(_request) { 2014 2251 // Preferences are stored per-user in their DO 2015 2252 const preferences = (await this.state.storage.get('preferences')) || []; 2016 2253 return Response.json({ preferences }); 2017 2254 } 2018 2255 2256 + /** @param {Request} request */ 2019 2257 async handlePutPreferences(request) { 2020 2258 const body = await request.json(); 2021 2259 const { preferences } = body; ··· 2030 2268 return Response.json({}); 2031 2269 } 2032 2270 2271 + /** 2272 + * @param {string} did 2273 + * @returns {Promise<string|null>} 2274 + */ 2033 2275 async getHandleForDid(did) { 2034 2276 // Check if this DID has a handle registered 2277 + /** @type {Record<string, string>} */ 2035 2278 const handleMap = (await this.state.storage.get('handleMap')) || {}; 2036 2279 for (const [handle, mappedDid] of Object.entries(handleMap)) { 2037 2280 if (mappedDid === did) return handle; ··· 2039 2282 // Check instance's own handle 2040 2283 const instanceDid = await this.getDid(); 2041 2284 if (instanceDid === did) { 2042 - return await this.state.storage.get('handle'); 2285 + return /** @type {string|null} */ (await this.state.storage.get('handle')); 2043 2286 } 2044 2287 return null; 2045 2288 } 2046 2289 2290 + /** 2291 + * @param {string} did 2292 + * @param {string|null} lxm 2293 + */ 2047 2294 async createServiceAuthForAppView(did, lxm) { 2048 2295 const signingKey = await this.getSigningKey(); 2296 + if (!signingKey) throw new Error('No signing key available'); 2049 2297 return createServiceJwt({ 2050 2298 iss: did, 2051 2299 aud: 'did:web:api.bsky.app', ··· 2054 2302 }); 2055 2303 } 2056 2304 2305 + /** 2306 + * @param {Request} request 2307 + * @param {string} userDid 2308 + */ 2057 2309 async handleAppViewProxy(request, userDid) { 2058 2310 const url = new URL(request.url); 2059 2311 // Extract lexicon method from path: /xrpc/app.bsky.actor.getPreferences -> app.bsky.actor.getPreferences ··· 2075 2327 'Content-Type', 2076 2328 request.headers.get('Content-Type') || 'application/json', 2077 2329 ); 2078 - if (request.headers.get('Accept')) { 2079 - headers.set('Accept', request.headers.get('Accept')); 2330 + const acceptHeader = request.headers.get('Accept'); 2331 + if (acceptHeader) { 2332 + headers.set('Accept', acceptHeader); 2080 2333 } 2081 - if (request.headers.get('Accept-Language')) { 2082 - headers.set('Accept-Language', request.headers.get('Accept-Language')); 2334 + const acceptLangHeader = request.headers.get('Accept-Language'); 2335 + if (acceptLangHeader) { 2336 + headers.set('Accept-Language', acceptLangHeader); 2083 2337 } 2084 2338 2085 2339 const proxyReq = new Request(appViewUrl.toString(), { ··· 2102 2356 headers: responseHeaders, 2103 2357 }); 2104 2358 } catch (err) { 2359 + const message = err instanceof Error ? err.message : String(err); 2105 2360 return errorResponse( 2106 2361 'UpstreamFailure', 2107 - `Failed to reach AppView: ${err.message}`, 2362 + `Failed to reach AppView: ${message}`, 2108 2363 502, 2109 2364 ); 2110 2365 } 2111 2366 } 2112 2367 2113 2368 async handleListRepos() { 2369 + /** @type {string[]} */ 2114 2370 const registeredDids = 2115 2371 (await this.state.storage.get('registeredDids')) || []; 2116 2372 const did = await this.getDid(); 2117 2373 const repos = did 2118 2374 ? [{ did, head: null, rev: null }] 2119 - : registeredDids.map((d) => ({ did: d, head: null, rev: null })); 2375 + : registeredDids.map((/** @type {string} */ d) => ({ did: d, head: null, rev: null })); 2120 2376 return Response.json({ repos }); 2121 2377 } 2122 2378 2379 + /** @param {Request} request */ 2123 2380 async handleCreateRecord(request) { 2124 2381 const body = await request.json(); 2125 2382 if (!body.collection || !body.record) { ··· 2144 2401 validationStatus: 'valid', 2145 2402 }); 2146 2403 } catch (err) { 2147 - return errorResponse('InternalError', err.message, 500); 2404 + const message = err instanceof Error ? err.message : String(err); 2405 + return errorResponse('InternalError', message, 500); 2148 2406 } 2149 2407 } 2150 2408 2409 + /** @param {Request} request */ 2151 2410 async handleDeleteRecord(request) { 2152 2411 const body = await request.json(); 2153 2412 if (!body.collection || !body.rkey) { ··· 2160 2419 } 2161 2420 return Response.json({}); 2162 2421 } catch (err) { 2163 - return errorResponse('InternalError', err.message, 500); 2422 + const message = err instanceof Error ? err.message : String(err); 2423 + return errorResponse('InternalError', message, 500); 2164 2424 } 2165 2425 } 2166 2426 2427 + /** @param {Request} request */ 2167 2428 async handlePutRecord(request) { 2168 2429 const body = await request.json(); 2169 2430 if (!body.collection || !body.rkey || !body.record) { ··· 2189 2450 validationStatus: 'valid', 2190 2451 }); 2191 2452 } catch (err) { 2192 - return errorResponse('InternalError', err.message, 500); 2453 + const message = err instanceof Error ? err.message : String(err); 2454 + return errorResponse('InternalError', message, 500); 2193 2455 } 2194 2456 } 2195 2457 2458 + /** @param {Request} request */ 2196 2459 async handleApplyWrites(request) { 2197 2460 const body = await request.json(); 2198 2461 if (!body.writes || !Array.isArray(body.writes)) { ··· 2244 2507 const rev = await this.state.storage.get('rev'); 2245 2508 return Response.json({ commit: { cid: head, rev }, results }); 2246 2509 } catch (err) { 2247 - return errorResponse('InternalError', err.message, 500); 2510 + const message = err instanceof Error ? err.message : String(err); 2511 + return errorResponse('InternalError', message, 500); 2248 2512 } 2249 2513 } 2250 2514 2515 + /** @param {URL} url */ 2251 2516 async handleGetRecord(url) { 2252 2517 const collection = url.searchParams.get('collection'); 2253 2518 const rkey = url.searchParams.get('rkey'); ··· 2256 2521 } 2257 2522 const did = await this.getDid(); 2258 2523 const uri = `at://${did}/${collection}/${rkey}`; 2259 - const rows = this.sql 2524 + const rows = /** @type {RecordRow[]} */ (this.sql 2260 2525 .exec(`SELECT cid, value FROM records WHERE uri = ?`, uri) 2261 - .toArray(); 2526 + .toArray()); 2262 2527 if (rows.length === 0) { 2263 2528 return errorResponse('RecordNotFound', 'record not found', 404); 2264 2529 } ··· 2288 2553 }); 2289 2554 } 2290 2555 2556 + /** @param {URL} url */ 2291 2557 async handleListRecords(url) { 2292 2558 const collection = url.searchParams.get('collection'); 2293 2559 if (!collection) { ··· 2304 2570 const query = `SELECT uri, cid, value FROM records WHERE collection = ? ORDER BY rkey ${reverse ? 'DESC' : 'ASC'} LIMIT ?`; 2305 2571 const params = [collection, limit + 1]; 2306 2572 2307 - const rows = this.sql.exec(query, ...params).toArray(); 2573 + const rows = /** @type {RecordRow[]} */ (this.sql.exec(query, ...params).toArray()); 2308 2574 const hasMore = rows.length > limit; 2309 2575 const records = rows.slice(0, limit).map((r) => ({ 2310 2576 uri: r.uri, ··· 2319 2585 } 2320 2586 2321 2587 handleGetLatestCommit() { 2322 - const commits = this.sql 2588 + const commits = /** @type {CommitRow[]} */ (this.sql 2323 2589 .exec(`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`) 2324 - .toArray(); 2590 + .toArray()); 2325 2591 if (commits.length === 0) { 2326 2592 return errorResponse('RepoNotFound', 'repo not found', 404); 2327 2593 } ··· 2330 2596 2331 2597 async handleGetRepoStatus() { 2332 2598 const did = await this.getDid(); 2333 - const commits = this.sql 2599 + const commits = /** @type {CommitRow[]} */ (this.sql 2334 2600 .exec(`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`) 2335 - .toArray(); 2601 + .toArray()); 2336 2602 if (commits.length === 0 || !did) { 2337 2603 return errorResponse('RepoNotFound', 'repo not found', 404); 2338 2604 } ··· 2345 2611 } 2346 2612 2347 2613 handleGetRepo() { 2348 - const commits = this.sql 2614 + const commits = /** @type {CommitRow[]} */ (this.sql 2349 2615 .exec(`SELECT cid FROM commits ORDER BY seq DESC LIMIT 1`) 2350 - .toArray(); 2616 + .toArray()); 2351 2617 if (commits.length === 0) { 2352 2618 return errorResponse('RepoNotFound', 'repo not found', 404); 2353 2619 } ··· 2357 2623 const neededCids = new Set(); 2358 2624 2359 2625 // Helper to get block data 2626 + /** @param {string} cid */ 2360 2627 const getBlock = (cid) => { 2361 - const rows = this.sql 2628 + const rows = /** @type {BlockRow[]} */ (this.sql 2362 2629 .exec(`SELECT data FROM blocks WHERE cid = ?`, cid) 2363 - .toArray(); 2630 + .toArray()); 2364 2631 return rows.length > 0 ? new Uint8Array(rows[0].data) : null; 2365 2632 }; 2366 2633 2367 2634 // Collect all reachable blocks starting from commit 2635 + /** @param {string} cid */ 2368 2636 const collectBlocks = (cid) => { 2369 2637 if (neededCids.has(cid)) return; 2370 2638 neededCids.add(cid); ··· 2412 2680 } 2413 2681 2414 2682 const car = buildCarFile(commitCid, blocksForCar); 2415 - return new Response(car, { 2683 + return new Response(/** @type {BodyInit} */(car), { 2416 2684 headers: { 'content-type': 'application/vnd.ipld.car' }, 2417 2685 }); 2418 2686 } 2419 2687 2688 + /** @param {URL} url */ 2420 2689 async handleSyncGetRecord(url) { 2421 2690 const collection = url.searchParams.get('collection'); 2422 2691 const rkey = url.searchParams.get('rkey'); ··· 2425 2694 } 2426 2695 const did = await this.getDid(); 2427 2696 const uri = `at://${did}/${collection}/${rkey}`; 2428 - const rows = this.sql 2697 + const rows = /** @type {RecordRow[]} */ (this.sql 2429 2698 .exec(`SELECT cid FROM records WHERE uri = ?`, uri) 2430 - .toArray(); 2699 + .toArray()); 2431 2700 if (rows.length === 0) { 2432 2701 return errorResponse('RecordNotFound', 'record not found', 404); 2433 2702 } 2434 2703 const recordCid = rows[0].cid; 2435 2704 2436 2705 // Get latest commit 2437 - const commits = this.sql 2706 + const commits = /** @type {CommitRow[]} */ (this.sql 2438 2707 .exec(`SELECT cid FROM commits ORDER BY seq DESC LIMIT 1`) 2439 - .toArray(); 2708 + .toArray()); 2440 2709 if (commits.length === 0) { 2441 2710 return errorResponse('RepoNotFound', 'no commits', 404); 2442 2711 } ··· 2444 2713 2445 2714 // Build proof chain: commit -> MST path -> record 2446 2715 // Include commit block, all MST nodes on path to record, and record block 2716 + /** @type {Array<{cid: string, data: Uint8Array}>} */ 2447 2717 const blocks = []; 2448 2718 const included = new Set(); 2449 2719 2720 + /** @param {string} cidStr */ 2450 2721 const addBlock = (cidStr) => { 2451 2722 if (included.has(cidStr)) return; 2452 2723 included.add(cidStr); 2453 - const blockRows = this.sql 2724 + const blockRows = /** @type {BlockRow[]} */ (this.sql 2454 2725 .exec(`SELECT data FROM blocks WHERE cid = ?`, cidStr) 2455 - .toArray(); 2726 + .toArray()); 2456 2727 if (blockRows.length > 0) { 2457 2728 blocks.push({ cid: cidStr, data: new Uint8Array(blockRows[0].data) }); 2458 2729 } ··· 2462 2733 addBlock(commitCid); 2463 2734 2464 2735 // Get commit to find data root 2465 - const commitRows = this.sql 2736 + const commitRows = /** @type {BlockRow[]} */ (this.sql 2466 2737 .exec(`SELECT data FROM blocks WHERE cid = ?`, commitCid) 2467 - .toArray(); 2738 + .toArray()); 2468 2739 if (commitRows.length > 0) { 2469 2740 const commit = cborDecode(new Uint8Array(commitRows[0].data)); 2470 2741 if (commit.data) { ··· 2481 2752 addBlock(recordCid); 2482 2753 2483 2754 const car = buildCarFile(commitCid, blocks); 2484 - return new Response(car, { 2755 + return new Response(/** @type {BodyInit} */(car), { 2485 2756 headers: { 'content-type': 'application/vnd.ipld.car' }, 2486 2757 }); 2487 2758 } 2488 2759 2760 + /** @param {Request} request */ 2489 2761 async handleUploadBlob(request) { 2490 2762 // Require auth 2491 2763 const authHeader = request.headers.get('Authorization'); ··· 2510 2782 try { 2511 2783 await verifyAccessJwt(token, jwtSecret); 2512 2784 } catch (err) { 2513 - return errorResponse('InvalidToken', err.message, 401); 2785 + const message = err instanceof Error ? err.message : String(err); 2786 + return errorResponse('InvalidToken', message, 401); 2514 2787 } 2515 2788 2516 2789 const did = await this.getDid(); ··· 2547 2820 2548 2821 // Upload to R2 (idempotent - same CID always has same content) 2549 2822 const r2Key = `${did}/${cidStr}`; 2550 - await this.env.BLOBS.put(r2Key, bodyBytes, { 2823 + await this.env?.BLOBS?.put(r2Key, bodyBytes, { 2551 2824 httpMetadata: { contentType: mimeType }, 2552 2825 }); 2553 2826 ··· 2572 2845 }); 2573 2846 } 2574 2847 2848 + /** @param {URL} url */ 2575 2849 async handleGetBlob(url) { 2576 2850 const did = url.searchParams.get('did'); 2577 2851 const cid = url.searchParams.get('cid'); ··· 2604 2878 2605 2879 // Fetch from R2 2606 2880 const r2Key = `${did}/${cid}`; 2607 - const object = await this.env.BLOBS.get(r2Key); 2881 + const object = await this.env?.BLOBS?.get(r2Key); 2608 2882 2609 2883 if (!object) { 2610 2884 return errorResponse('BlobNotFound', 'blob not found in storage', 404); ··· 2613 2887 // Return blob with security headers 2614 2888 return new Response(object.body, { 2615 2889 headers: { 2616 - 'Content-Type': mimeType, 2890 + 'Content-Type': /** @type {string} */ (mimeType), 2617 2891 'Content-Length': String(size), 2618 2892 'X-Content-Type-Options': 'nosniff', 2619 2893 'Content-Security-Policy': "default-src 'none'; sandbox", ··· 2622 2896 }); 2623 2897 } 2624 2898 2899 + /** @param {URL} url */ 2625 2900 async handleListBlobs(url) { 2626 2901 const did = url.searchParams.get('did'); 2627 2902 const cursor = url.searchParams.get('cursor'); ··· 2666 2941 }); 2667 2942 } 2668 2943 2944 + /** 2945 + * @param {Request} request 2946 + * @param {URL} url 2947 + */ 2669 2948 handleSubscribeRepos(request, url) { 2670 2949 const upgradeHeader = request.headers.get('Upgrade'); 2671 2950 if (upgradeHeader !== 'websocket') { ··· 2675 2954 this.state.acceptWebSocket(server); 2676 2955 const cursor = url.searchParams.get('cursor'); 2677 2956 if (cursor) { 2678 - const events = this.sql 2957 + const events = /** @type {SeqEventRow[]} */ (this.sql 2679 2958 .exec( 2680 2959 `SELECT * FROM seq_events WHERE seq > ? ORDER BY seq`, 2681 2960 parseInt(cursor, 10), 2682 2961 ) 2683 - .toArray(); 2962 + .toArray()); 2684 2963 for (const evt of events) { 2685 2964 server.send(this.formatEvent(evt)); 2686 2965 } ··· 2688 2967 return new Response(null, { status: 101, webSocket: client }); 2689 2968 } 2690 2969 2970 + /** @param {Request} request */ 2691 2971 async fetch(request) { 2692 2972 const url = new URL(request.url); 2693 2973 const route = pdsRoutes[url.pathname]; ··· 2735 3015 .toArray(); 2736 3016 2737 3017 for (const { cid } of orphans) { 2738 - await this.env.BLOBS.delete(`${did}/${cid}`); 3018 + await this.env?.BLOBS?.delete(`${did}/${cid}`); 2739 3019 this.sql.exec('DELETE FROM blob WHERE cid = ?', cid); 2740 3020 } 2741 3021 2742 3022 } 2743 3023 } 2744 3024 3025 + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 3026 + // โ•‘ WORKERS ENTRY POINT โ•‘ 3027 + // โ•‘ Request handling, CORS, auth middleware โ•‘ 3028 + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 3029 + 2745 3030 const corsHeaders = { 2746 3031 'Access-Control-Allow-Origin': '*', 2747 3032 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', ··· 2749 3034 'Content-Type, Authorization, atproto-accept-labelers, atproto-proxy, x-bsky-topics', 2750 3035 }; 2751 3036 3037 + /** 3038 + * @param {Response} response 3039 + * @returns {Response} 3040 + */ 2752 3041 function addCorsHeaders(response) { 2753 3042 const newHeaders = new Headers(response.headers); 2754 3043 for (const [key, value] of Object.entries(corsHeaders)) { ··· 2762 3051 } 2763 3052 2764 3053 export default { 3054 + /** 3055 + * @param {Request} request 3056 + * @param {Env} env 3057 + */ 2765 3058 async fetch(request, env) { 2766 3059 // Handle CORS preflight 2767 3060 if (request.method === 'OPTIONS') { ··· 2777 3070 }, 2778 3071 }; 2779 3072 2780 - // Extract subdomain from hostname (e.g., "alice" from "alice.foo.workers.dev") 3073 + /** 3074 + * Extract subdomain from hostname (e.g., "alice" from "alice.foo.workers.dev") 3075 + * @param {string} hostname 3076 + * @returns {string|null} 3077 + */ 2781 3078 function getSubdomain(hostname) { 2782 3079 const parts = hostname.split('.'); 2783 3080 // workers.dev domains: [subdomain?].[worker-name].[account].workers.dev ··· 2793 3090 /** 2794 3091 * Verify auth and return DID from token 2795 3092 * @param {Request} request - HTTP request with Authorization header 2796 - * @param {Object} env - Environment with JWT_SECRET 3093 + * @param {Env} env - Environment with JWT_SECRET 2797 3094 * @returns {Promise<{did: string} | {error: Response}>} DID or error response 2798 3095 */ 2799 3096 async function requireAuth(request, env) { ··· 2828 3125 const payload = await verifyAccessJwt(token, jwtSecret); 2829 3126 return { did: payload.sub }; 2830 3127 } catch (err) { 3128 + const message = err instanceof Error ? err.message : String(err); 2831 3129 return { 2832 3130 error: Response.json( 2833 3131 { 2834 3132 error: 'InvalidToken', 2835 - message: err.message, 3133 + message: message, 2836 3134 }, 2837 3135 { status: 401 }, 2838 3136 ), ··· 2840 3138 } 2841 3139 } 2842 3140 3141 + /** 3142 + * @param {Request} request 3143 + * @param {Env} env 3144 + */ 2843 3145 async function handleAuthenticatedBlobUpload(request, env) { 2844 3146 const auth = await requireAuth(request, env); 2845 - if (auth.error) return auth.error; 3147 + if ('error' in auth) return auth.error; 2846 3148 2847 3149 // Route to the user's DO based on their DID from the token 2848 3150 const id = env.PDS.idFromName(auth.did); ··· 2850 3152 return pds.fetch(request); 2851 3153 } 2852 3154 3155 + /** 3156 + * @param {Request} request 3157 + * @param {Env} env 3158 + */ 2853 3159 async function handleAuthenticatedRepoWrite(request, env) { 2854 3160 const auth = await requireAuth(request, env); 2855 - if (auth.error) return auth.error; 3161 + if ('error' in auth) return auth.error; 2856 3162 2857 3163 const body = await request.json(); 2858 3164 const repo = body.repo; ··· 2883 3189 return response; 2884 3190 } 2885 3191 3192 + /** 3193 + * @param {Request} request 3194 + * @param {Env} env 3195 + */ 2886 3196 async function handleRequest(request, env) { 2887 3197 const url = new URL(request.url); 2888 3198 const subdomain = getSubdomain(url.hostname); ··· 2945 3255 if (url.pathname.startsWith('/xrpc/app.bsky.')) { 2946 3256 // Authenticate the user first 2947 3257 const auth = await requireAuth(request, env); 2948 - if (auth.error) return auth.error; 3258 + if ('error' in auth) return auth.error; 2949 3259 2950 3260 // Route to the user's DO instance to create service auth and proxy 2951 3261 const id = env.PDS.idFromName(auth.did);
+16
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ES2022", 5 + "moduleResolution": "bundler", 6 + "checkJs": true, 7 + "allowJs": true, 8 + "noEmit": true, 9 + "strict": true, 10 + "skipLibCheck": true, 11 + "useUnknownInCatchVariables": false, 12 + "types": ["@cloudflare/workers-types"] 13 + }, 14 + "include": ["src/**/*.js"], 15 + "exclude": ["node_modules"] 16 + }