atproto user agency toolkit for individuals and groups
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit: minimal AT Protocol PDS forked from Cirrus

Port Cirrus (Cloudflare Workers PDS) to a standalone Node.js server:
- Hono HTTP framework via @hono/node-server
- better-sqlite3 replacing Cloudflare SqlStorage
- Filesystem blob storage replacing R2
- ws library replacing hibernatable WebSockets
- In-memory DID cache replacing Workers Cache API
- Bearer token + JWT session auth (OAuth/passkeys deferred)

Verified: health check, DID document, session management, record
CRUD, CAR export, and WebSocket firehose all functional.

dietrich ayala 600186bf

+7366
+14
.env.example
··· 1 + # Required 2 + DID=did:web:example.com 3 + HANDLE=alice.example.com 4 + PDS_HOSTNAME=example.com 5 + AUTH_TOKEN=your-bearer-token-here 6 + SIGNING_KEY=hex-encoded-secp256k1-private-key 7 + SIGNING_KEY_PUBLIC=multibase-encoded-public-key 8 + JWT_SECRET=your-jwt-secret-here 9 + PASSWORD_HASH=$2a$10$... # bcrypt hash of account password 10 + 11 + # Optional 12 + EMAIL=alice@example.com 13 + DATA_DIR=./data 14 + PORT=3000
+8
.gitignore
··· 1 + node_modules/ 2 + dist/ 3 + data/ 4 + .env 5 + *.db 6 + *.db-journal 7 + *.db-wal 8 + *.db-shm
+31
CLAUDE.md
··· 1 + # CLAUDE.md 2 + 3 + This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 + 5 + ## Project Overview 6 + 7 + P2PDS is an AT Protocol (atproto) Personal Data Server with P2P capabilities. It syncs and stores records for a set of accounts, provides them on P2P networks, and fetches/stores records from P2P networks for serviced accounts. 8 + 9 + ## Planned Tech Stack 10 + 11 + - **Runtime:** Node.js 12 + - **Base:** Generalized version of [Cirrus](https://github.com/ascorbic/cirrus) (Cloudflare-specific parts abstracted away) 13 + - **IPFS:** Helia with DHT and pubsub enabled 14 + - **Identity:** AT Protocol DIDs via PLC directory 15 + - **Addressing:** DASL addresses for IPFS-stored records 16 + 17 + ## Architecture (Planned) 18 + 19 + The system is configured with a list of DIDs and operates as follows: 20 + 1. On first run, queries PLC directory to resolve PDSes for all configured DIDs 21 + 2. Fetches records from each DID's PDS 22 + 3. Stores and provides records over IPFS using DASL addresses 23 + 4. Syncs records bidirectionally between PDS and IPFS 24 + 25 + Open design problem: DID-to-PeerID mapping (both can update/rotate). 26 + 27 + ## Development Phases 28 + 29 + 1. Single-user PDS working as local node service 30 + 2. Record replication with local storage 31 + 3. IPFS integration for replicated records
+2902
package-lock.json
··· 1 + { 2 + "name": "p2pds", 3 + "version": "0.1.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "p2pds", 9 + "version": "0.1.0", 10 + "license": "MIT", 11 + "dependencies": { 12 + "@atcute/atproto": "^3.1.10", 13 + "@atcute/bluesky": "^3.2.14", 14 + "@atcute/cbor": "^2.2.8", 15 + "@atcute/cid": "^2.3.0", 16 + "@atcute/client": "^4.2.0", 17 + "@atcute/identity": "^1.1.3", 18 + "@atcute/identity-resolver": "^1.2.2", 19 + "@atcute/lexicons": "^1.2.6", 20 + "@atcute/tid": "^1.1.1", 21 + "@atproto/crypto": "^0.4.5", 22 + "@atproto/lex-cbor": "^0.0.3", 23 + "@atproto/lex-data": "^0.0.3", 24 + "@atproto/lex-json": "^0.0.11", 25 + "@atproto/repo": "^0.8.12", 26 + "@hono/node-server": "^1.13.8", 27 + "bcryptjs": "^3.0.3", 28 + "better-sqlite3": "^11.8.1", 29 + "hono": "^4.11.3", 30 + "jose": "^6.1.3", 31 + "picocolors": "^1.1.1", 32 + "ws": "^8.18.3" 33 + }, 34 + "devDependencies": { 35 + "@types/bcryptjs": "^3.0.0", 36 + "@types/better-sqlite3": "^7.6.12", 37 + "@types/ws": "^8.18.1", 38 + "tsx": "^4.21.0", 39 + "typescript": "^5.9.3", 40 + "vitest": "^3.0.0" 41 + } 42 + }, 43 + "node_modules/@atcute/atproto": { 44 + "version": "3.1.10", 45 + "resolved": "https://registry.npmjs.org/@atcute/atproto/-/atproto-3.1.10.tgz", 46 + "integrity": "sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ==", 47 + "license": "0BSD", 48 + "dependencies": { 49 + "@atcute/lexicons": "^1.2.6" 50 + } 51 + }, 52 + "node_modules/@atcute/bluesky": { 53 + "version": "3.2.17", 54 + "resolved": "https://registry.npmjs.org/@atcute/bluesky/-/bluesky-3.2.17.tgz", 55 + "integrity": "sha512-Li+RsPkcRNC6AnNlqOGnlmAcjSwBdXIKFubJL1nwACDngKNXG4ooGL5cvzeekdDEfHmtFhS/tyZNaUx9QXYEUw==", 56 + "license": "0BSD", 57 + "dependencies": { 58 + "@atcute/atproto": "^3.1.10", 59 + "@atcute/lexicons": "^1.2.7" 60 + } 61 + }, 62 + "node_modules/@atcute/cbor": { 63 + "version": "2.3.1", 64 + "resolved": "https://registry.npmjs.org/@atcute/cbor/-/cbor-2.3.1.tgz", 65 + "integrity": "sha512-yX036iKstCck3zrK6B4FRxB7oBwAskal6m2RIRKXAN4cx27Pa92ECPuecNe/D3F6CcLzioP32/RAiOOhDAPPEw==", 66 + "license": "0BSD", 67 + "dependencies": { 68 + "@atcute/cid": "^2.4.0", 69 + "@atcute/multibase": "^1.1.7", 70 + "@atcute/uint8array": "^1.1.0" 71 + } 72 + }, 73 + "node_modules/@atcute/cid": { 74 + "version": "2.4.0", 75 + "resolved": "https://registry.npmjs.org/@atcute/cid/-/cid-2.4.0.tgz", 76 + "integrity": "sha512-6+5u9MpUrgSRQ94z7vaIX4BYk8fYr2KXUBS+rrr2NhlPy8xam8nbTlmd3hvBbtpSwShbhRAE4tA5Ab7eYUp2Yw==", 77 + "license": "0BSD", 78 + "dependencies": { 79 + "@atcute/multibase": "^1.1.6", 80 + "@atcute/uint8array": "^1.0.6" 81 + } 82 + }, 83 + "node_modules/@atcute/client": { 84 + "version": "4.2.1", 85 + "resolved": "https://registry.npmjs.org/@atcute/client/-/client-4.2.1.tgz", 86 + "integrity": "sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==", 87 + "license": "0BSD", 88 + "dependencies": { 89 + "@atcute/identity": "^1.1.3", 90 + "@atcute/lexicons": "^1.2.6" 91 + } 92 + }, 93 + "node_modules/@atcute/identity": { 94 + "version": "1.1.3", 95 + "resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-1.1.3.tgz", 96 + "integrity": "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==", 97 + "license": "0BSD", 98 + "dependencies": { 99 + "@atcute/lexicons": "^1.2.4", 100 + "@badrap/valita": "^0.4.6" 101 + } 102 + }, 103 + "node_modules/@atcute/identity-resolver": { 104 + "version": "1.2.2", 105 + "resolved": "https://registry.npmjs.org/@atcute/identity-resolver/-/identity-resolver-1.2.2.tgz", 106 + "integrity": "sha512-eUh/UH4bFvuXS0X7epYCeJC/kj4rbBXfSRumLEH4smMVwNOgTo7cL/0Srty+P/qVPoZEyXdfEbS0PHJyzoXmHw==", 107 + "license": "0BSD", 108 + "dependencies": { 109 + "@atcute/lexicons": "^1.2.6", 110 + "@atcute/util-fetch": "^1.0.5", 111 + "@badrap/valita": "^0.4.6" 112 + }, 113 + "peerDependencies": { 114 + "@atcute/identity": "^1.0.0" 115 + } 116 + }, 117 + "node_modules/@atcute/lexicons": { 118 + "version": "1.2.8", 119 + "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.2.8.tgz", 120 + "integrity": "sha512-3WPyU5droiovgVyWYuhsNibFAknna7uuVqvcnCd++oVp7ZPvXtXPFVx1MuAjJV7FspYwkuMh/GlTPsdfIOetBQ==", 121 + "license": "0BSD", 122 + "dependencies": { 123 + "@atcute/uint8array": "^1.1.0", 124 + "@atcute/util-text": "^1.1.0", 125 + "@standard-schema/spec": "^1.1.0", 126 + "esm-env": "^1.2.2" 127 + } 128 + }, 129 + "node_modules/@atcute/multibase": { 130 + "version": "1.1.7", 131 + "resolved": "https://registry.npmjs.org/@atcute/multibase/-/multibase-1.1.7.tgz", 132 + "integrity": "sha512-YmWds7U52b7Qri0xNfGeqSOvgyNfHR8Yy/NNDQx4d5TkCX2fHJIo0pXquEhCyMNAwKt53uH5yQDswy4TNP1Zhw==", 133 + "license": "0BSD", 134 + "dependencies": { 135 + "@atcute/uint8array": "^1.1.0" 136 + } 137 + }, 138 + "node_modules/@atcute/tid": { 139 + "version": "1.1.1", 140 + "resolved": "https://registry.npmjs.org/@atcute/tid/-/tid-1.1.1.tgz", 141 + "integrity": "sha512-djJ8UGhLkTU5V51yCnBEruMg35qETjWzWy5sJG/2gEOl2Gd7rQWHSaf+yrO6vMS5EFA38U2xOWE3EDUPzvc2ZQ==", 142 + "license": "0BSD", 143 + "dependencies": { 144 + "@atcute/time-ms": "^1.0.0" 145 + } 146 + }, 147 + "node_modules/@atcute/time-ms": { 148 + "version": "1.2.1", 149 + "resolved": "https://registry.npmjs.org/@atcute/time-ms/-/time-ms-1.2.1.tgz", 150 + "integrity": "sha512-7tzcYMLz1IQCyneupNP5AMkgfLywzYnsWLTW/n1Ku3vM/LgicGgBVbHKhqP10H8LUz3gEf8DUMrKbTkM/VLUgA==", 151 + "hasInstallScript": true, 152 + "license": "0BSD", 153 + "dependencies": { 154 + "@types/bun": "^1.3.8", 155 + "node-gyp-build": "^4.8.4" 156 + } 157 + }, 158 + "node_modules/@atcute/uint8array": { 159 + "version": "1.1.0", 160 + "resolved": "https://registry.npmjs.org/@atcute/uint8array/-/uint8array-1.1.0.tgz", 161 + "integrity": "sha512-JtHXIVW6LPU9FMWp7SgE4HbUs3uV2WdfkK/2RWdEGjr4EgMV50P3FdU6fPeGlTfDNBJVYMIsuD2wwaKRPV/Aqg==", 162 + "license": "0BSD" 163 + }, 164 + "node_modules/@atcute/util-fetch": { 165 + "version": "1.0.5", 166 + "resolved": "https://registry.npmjs.org/@atcute/util-fetch/-/util-fetch-1.0.5.tgz", 167 + "integrity": "sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig==", 168 + "license": "0BSD", 169 + "dependencies": { 170 + "@badrap/valita": "^0.4.6" 171 + } 172 + }, 173 + "node_modules/@atcute/util-text": { 174 + "version": "1.1.0", 175 + "resolved": "https://registry.npmjs.org/@atcute/util-text/-/util-text-1.1.0.tgz", 176 + "integrity": "sha512-34G9KD5Z9f7oEdFpZOmqrMnU86p8ne6LlxJowfZzKNszRcl1GH+FtEPh3N1woelJT2SkPXMK2anwT8DESTluwA==", 177 + "license": "0BSD", 178 + "dependencies": { 179 + "unicode-segmenter": "^0.14.5" 180 + } 181 + }, 182 + "node_modules/@atproto/common": { 183 + "version": "0.5.11", 184 + "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.11.tgz", 185 + "integrity": "sha512-WRlT4s+wv80WdQuzkQub9D5vTD82O8dH2p91u4b+x3O17q5IQbmA3Lj+1NICINNSy2voqloqAWdqXEkRfdlAPw==", 186 + "license": "MIT", 187 + "dependencies": { 188 + "@atproto/common-web": "^0.4.16", 189 + "@atproto/lex-cbor": "^0.0.11", 190 + "@atproto/lex-data": "^0.0.11", 191 + "iso-datestring-validator": "^2.2.2", 192 + "multiformats": "^9.9.0", 193 + "pino": "^8.21.0" 194 + }, 195 + "engines": { 196 + "node": ">=18.7.0" 197 + } 198 + }, 199 + "node_modules/@atproto/common-web": { 200 + "version": "0.4.16", 201 + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.16.tgz", 202 + "integrity": "sha512-Ufvaff5JgxUyUyTAG0/3o7ltpy3lnZ1DvLjyAnvAf+hHfiK7OMQg+8byr+orN+KP9MtIQaRTsCgYPX+PxMKUoA==", 203 + "license": "MIT", 204 + "dependencies": { 205 + "@atproto/lex-data": "^0.0.11", 206 + "@atproto/lex-json": "^0.0.11", 207 + "@atproto/syntax": "^0.4.3", 208 + "zod": "^3.23.8" 209 + } 210 + }, 211 + "node_modules/@atproto/common-web/node_modules/@atproto/lex-data": { 212 + "version": "0.0.11", 213 + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.11.tgz", 214 + "integrity": "sha512-4+KTtHdqwlhiTKA7D4SACea4jprsNpCQsNALW09wsZ6IHhCDGO5tr1cmV+QnLYe3G3mu1E1yXHXbPUHrUUDT/A==", 215 + "license": "MIT", 216 + "dependencies": { 217 + "multiformats": "^9.9.0", 218 + "tslib": "^2.8.1", 219 + "uint8arrays": "3.0.0", 220 + "unicode-segmenter": "^0.14.0" 221 + } 222 + }, 223 + "node_modules/@atproto/common-web/node_modules/@atproto/syntax": { 224 + "version": "0.4.3", 225 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", 226 + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", 227 + "license": "MIT", 228 + "dependencies": { 229 + "tslib": "^2.8.1" 230 + } 231 + }, 232 + "node_modules/@atproto/common/node_modules/@atproto/lex-cbor": { 233 + "version": "0.0.11", 234 + "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.11.tgz", 235 + "integrity": "sha512-A7ETtPsEsJ/VuPJOFw4bPNTKxHvFN1JbTQ2NjLuisd3ry7fVxgMpo/qGXsUQsAh/I/uziGbhpNqdS6GnI2p/Wg==", 236 + "license": "MIT", 237 + "dependencies": { 238 + "@atproto/lex-data": "^0.0.11", 239 + "tslib": "^2.8.1" 240 + } 241 + }, 242 + "node_modules/@atproto/common/node_modules/@atproto/lex-data": { 243 + "version": "0.0.11", 244 + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.11.tgz", 245 + "integrity": "sha512-4+KTtHdqwlhiTKA7D4SACea4jprsNpCQsNALW09wsZ6IHhCDGO5tr1cmV+QnLYe3G3mu1E1yXHXbPUHrUUDT/A==", 246 + "license": "MIT", 247 + "dependencies": { 248 + "multiformats": "^9.9.0", 249 + "tslib": "^2.8.1", 250 + "uint8arrays": "3.0.0", 251 + "unicode-segmenter": "^0.14.0" 252 + } 253 + }, 254 + "node_modules/@atproto/crypto": { 255 + "version": "0.4.5", 256 + "resolved": "https://registry.npmjs.org/@atproto/crypto/-/crypto-0.4.5.tgz", 257 + "integrity": "sha512-n40aKkMoCatP0u9Yvhrdk6fXyOHFDDbkdm4h4HCyWW+KlKl8iXfD5iV+ECq+w5BM+QH25aIpt3/j6EUNerhLxw==", 258 + "license": "MIT", 259 + "dependencies": { 260 + "@noble/curves": "^1.7.0", 261 + "@noble/hashes": "^1.6.1", 262 + "uint8arrays": "3.0.0" 263 + }, 264 + "engines": { 265 + "node": ">=18.7.0" 266 + } 267 + }, 268 + "node_modules/@atproto/lex-cbor": { 269 + "version": "0.0.3", 270 + "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.3.tgz", 271 + "integrity": "sha512-N8lCV3kK5ZcjSOWxKLWqzlnaSpK4isjXRZ0EqApl/5y9KB64s78hQ/U3KIE5qnPRlBbW5kSH3YACoU27u9nTOA==", 272 + "license": "MIT", 273 + "dependencies": { 274 + "@atproto/lex-data": "0.0.3", 275 + "multiformats": "^9.9.0", 276 + "tslib": "^2.8.1" 277 + } 278 + }, 279 + "node_modules/@atproto/lex-data": { 280 + "version": "0.0.3", 281 + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.3.tgz", 282 + "integrity": "sha512-ivo1IpY/EX+RIpxPgCf4cPhQo5bfu4nrpa1vJCt8hCm9SfoonJkDFGa0n4SMw4JnXZoUcGcrJ46L+D8bH6GI2g==", 283 + "license": "MIT", 284 + "dependencies": { 285 + "@atproto/syntax": "0.4.2", 286 + "multiformats": "^9.9.0", 287 + "tslib": "^2.8.1", 288 + "uint8arrays": "3.0.0", 289 + "unicode-segmenter": "^0.14.0" 290 + } 291 + }, 292 + "node_modules/@atproto/lex-json": { 293 + "version": "0.0.11", 294 + "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.11.tgz", 295 + "integrity": "sha512-2IExAoQ4KsR5fyPa1JjIvtR316PvdgRH/l3BVGLBd3cSxM3m5MftIv1B6qZ9HjNiK60SgkWp0mi9574bTNDhBQ==", 296 + "license": "MIT", 297 + "dependencies": { 298 + "@atproto/lex-data": "^0.0.11", 299 + "tslib": "^2.8.1" 300 + } 301 + }, 302 + "node_modules/@atproto/lex-json/node_modules/@atproto/lex-data": { 303 + "version": "0.0.11", 304 + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.11.tgz", 305 + "integrity": "sha512-4+KTtHdqwlhiTKA7D4SACea4jprsNpCQsNALW09wsZ6IHhCDGO5tr1cmV+QnLYe3G3mu1E1yXHXbPUHrUUDT/A==", 306 + "license": "MIT", 307 + "dependencies": { 308 + "multiformats": "^9.9.0", 309 + "tslib": "^2.8.1", 310 + "uint8arrays": "3.0.0", 311 + "unicode-segmenter": "^0.14.0" 312 + } 313 + }, 314 + "node_modules/@atproto/lexicon": { 315 + "version": "0.6.1", 316 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.1.tgz", 317 + "integrity": "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw==", 318 + "license": "MIT", 319 + "dependencies": { 320 + "@atproto/common-web": "^0.4.13", 321 + "@atproto/syntax": "^0.4.3", 322 + "iso-datestring-validator": "^2.2.2", 323 + "multiformats": "^9.9.0", 324 + "zod": "^3.23.8" 325 + } 326 + }, 327 + "node_modules/@atproto/lexicon/node_modules/@atproto/syntax": { 328 + "version": "0.4.3", 329 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", 330 + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", 331 + "license": "MIT", 332 + "dependencies": { 333 + "tslib": "^2.8.1" 334 + } 335 + }, 336 + "node_modules/@atproto/repo": { 337 + "version": "0.8.12", 338 + "resolved": "https://registry.npmjs.org/@atproto/repo/-/repo-0.8.12.tgz", 339 + "integrity": "sha512-QpVTVulgfz5PUiCTELlDBiRvnsnwrFWi+6CfY88VwXzrRHd9NE8GItK7sfxQ6U65vD/idH8ddCgFrlrsn1REPQ==", 340 + "license": "MIT", 341 + "dependencies": { 342 + "@atproto/common": "^0.5.3", 343 + "@atproto/common-web": "^0.4.7", 344 + "@atproto/crypto": "^0.4.5", 345 + "@atproto/lexicon": "^0.6.0", 346 + "@ipld/dag-cbor": "^7.0.0", 347 + "multiformats": "^9.9.0", 348 + "uint8arrays": "3.0.0", 349 + "varint": "^6.0.0", 350 + "zod": "^3.23.8" 351 + }, 352 + "engines": { 353 + "node": ">=18.7.0" 354 + } 355 + }, 356 + "node_modules/@atproto/syntax": { 357 + "version": "0.4.2", 358 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", 359 + "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", 360 + "license": "MIT" 361 + }, 362 + "node_modules/@badrap/valita": { 363 + "version": "0.4.6", 364 + "resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.4.6.tgz", 365 + "integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==", 366 + "license": "MIT", 367 + "engines": { 368 + "node": ">= 18" 369 + } 370 + }, 371 + "node_modules/@esbuild/aix-ppc64": { 372 + "version": "0.27.3", 373 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", 374 + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", 375 + "cpu": [ 376 + "ppc64" 377 + ], 378 + "dev": true, 379 + "license": "MIT", 380 + "optional": true, 381 + "os": [ 382 + "aix" 383 + ], 384 + "engines": { 385 + "node": ">=18" 386 + } 387 + }, 388 + "node_modules/@esbuild/android-arm": { 389 + "version": "0.27.3", 390 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", 391 + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", 392 + "cpu": [ 393 + "arm" 394 + ], 395 + "dev": true, 396 + "license": "MIT", 397 + "optional": true, 398 + "os": [ 399 + "android" 400 + ], 401 + "engines": { 402 + "node": ">=18" 403 + } 404 + }, 405 + "node_modules/@esbuild/android-arm64": { 406 + "version": "0.27.3", 407 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", 408 + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", 409 + "cpu": [ 410 + "arm64" 411 + ], 412 + "dev": true, 413 + "license": "MIT", 414 + "optional": true, 415 + "os": [ 416 + "android" 417 + ], 418 + "engines": { 419 + "node": ">=18" 420 + } 421 + }, 422 + "node_modules/@esbuild/android-x64": { 423 + "version": "0.27.3", 424 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", 425 + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", 426 + "cpu": [ 427 + "x64" 428 + ], 429 + "dev": true, 430 + "license": "MIT", 431 + "optional": true, 432 + "os": [ 433 + "android" 434 + ], 435 + "engines": { 436 + "node": ">=18" 437 + } 438 + }, 439 + "node_modules/@esbuild/darwin-arm64": { 440 + "version": "0.27.3", 441 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", 442 + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", 443 + "cpu": [ 444 + "arm64" 445 + ], 446 + "dev": true, 447 + "license": "MIT", 448 + "optional": true, 449 + "os": [ 450 + "darwin" 451 + ], 452 + "engines": { 453 + "node": ">=18" 454 + } 455 + }, 456 + "node_modules/@esbuild/darwin-x64": { 457 + "version": "0.27.3", 458 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", 459 + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", 460 + "cpu": [ 461 + "x64" 462 + ], 463 + "dev": true, 464 + "license": "MIT", 465 + "optional": true, 466 + "os": [ 467 + "darwin" 468 + ], 469 + "engines": { 470 + "node": ">=18" 471 + } 472 + }, 473 + "node_modules/@esbuild/freebsd-arm64": { 474 + "version": "0.27.3", 475 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", 476 + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", 477 + "cpu": [ 478 + "arm64" 479 + ], 480 + "dev": true, 481 + "license": "MIT", 482 + "optional": true, 483 + "os": [ 484 + "freebsd" 485 + ], 486 + "engines": { 487 + "node": ">=18" 488 + } 489 + }, 490 + "node_modules/@esbuild/freebsd-x64": { 491 + "version": "0.27.3", 492 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", 493 + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", 494 + "cpu": [ 495 + "x64" 496 + ], 497 + "dev": true, 498 + "license": "MIT", 499 + "optional": true, 500 + "os": [ 501 + "freebsd" 502 + ], 503 + "engines": { 504 + "node": ">=18" 505 + } 506 + }, 507 + "node_modules/@esbuild/linux-arm": { 508 + "version": "0.27.3", 509 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", 510 + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", 511 + "cpu": [ 512 + "arm" 513 + ], 514 + "dev": true, 515 + "license": "MIT", 516 + "optional": true, 517 + "os": [ 518 + "linux" 519 + ], 520 + "engines": { 521 + "node": ">=18" 522 + } 523 + }, 524 + "node_modules/@esbuild/linux-arm64": { 525 + "version": "0.27.3", 526 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", 527 + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", 528 + "cpu": [ 529 + "arm64" 530 + ], 531 + "dev": true, 532 + "license": "MIT", 533 + "optional": true, 534 + "os": [ 535 + "linux" 536 + ], 537 + "engines": { 538 + "node": ">=18" 539 + } 540 + }, 541 + "node_modules/@esbuild/linux-ia32": { 542 + "version": "0.27.3", 543 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", 544 + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", 545 + "cpu": [ 546 + "ia32" 547 + ], 548 + "dev": true, 549 + "license": "MIT", 550 + "optional": true, 551 + "os": [ 552 + "linux" 553 + ], 554 + "engines": { 555 + "node": ">=18" 556 + } 557 + }, 558 + "node_modules/@esbuild/linux-loong64": { 559 + "version": "0.27.3", 560 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", 561 + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", 562 + "cpu": [ 563 + "loong64" 564 + ], 565 + "dev": true, 566 + "license": "MIT", 567 + "optional": true, 568 + "os": [ 569 + "linux" 570 + ], 571 + "engines": { 572 + "node": ">=18" 573 + } 574 + }, 575 + "node_modules/@esbuild/linux-mips64el": { 576 + "version": "0.27.3", 577 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", 578 + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", 579 + "cpu": [ 580 + "mips64el" 581 + ], 582 + "dev": true, 583 + "license": "MIT", 584 + "optional": true, 585 + "os": [ 586 + "linux" 587 + ], 588 + "engines": { 589 + "node": ">=18" 590 + } 591 + }, 592 + "node_modules/@esbuild/linux-ppc64": { 593 + "version": "0.27.3", 594 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", 595 + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", 596 + "cpu": [ 597 + "ppc64" 598 + ], 599 + "dev": true, 600 + "license": "MIT", 601 + "optional": true, 602 + "os": [ 603 + "linux" 604 + ], 605 + "engines": { 606 + "node": ">=18" 607 + } 608 + }, 609 + "node_modules/@esbuild/linux-riscv64": { 610 + "version": "0.27.3", 611 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", 612 + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", 613 + "cpu": [ 614 + "riscv64" 615 + ], 616 + "dev": true, 617 + "license": "MIT", 618 + "optional": true, 619 + "os": [ 620 + "linux" 621 + ], 622 + "engines": { 623 + "node": ">=18" 624 + } 625 + }, 626 + "node_modules/@esbuild/linux-s390x": { 627 + "version": "0.27.3", 628 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", 629 + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", 630 + "cpu": [ 631 + "s390x" 632 + ], 633 + "dev": true, 634 + "license": "MIT", 635 + "optional": true, 636 + "os": [ 637 + "linux" 638 + ], 639 + "engines": { 640 + "node": ">=18" 641 + } 642 + }, 643 + "node_modules/@esbuild/linux-x64": { 644 + "version": "0.27.3", 645 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", 646 + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", 647 + "cpu": [ 648 + "x64" 649 + ], 650 + "dev": true, 651 + "license": "MIT", 652 + "optional": true, 653 + "os": [ 654 + "linux" 655 + ], 656 + "engines": { 657 + "node": ">=18" 658 + } 659 + }, 660 + "node_modules/@esbuild/netbsd-arm64": { 661 + "version": "0.27.3", 662 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", 663 + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", 664 + "cpu": [ 665 + "arm64" 666 + ], 667 + "dev": true, 668 + "license": "MIT", 669 + "optional": true, 670 + "os": [ 671 + "netbsd" 672 + ], 673 + "engines": { 674 + "node": ">=18" 675 + } 676 + }, 677 + "node_modules/@esbuild/netbsd-x64": { 678 + "version": "0.27.3", 679 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", 680 + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", 681 + "cpu": [ 682 + "x64" 683 + ], 684 + "dev": true, 685 + "license": "MIT", 686 + "optional": true, 687 + "os": [ 688 + "netbsd" 689 + ], 690 + "engines": { 691 + "node": ">=18" 692 + } 693 + }, 694 + "node_modules/@esbuild/openbsd-arm64": { 695 + "version": "0.27.3", 696 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", 697 + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", 698 + "cpu": [ 699 + "arm64" 700 + ], 701 + "dev": true, 702 + "license": "MIT", 703 + "optional": true, 704 + "os": [ 705 + "openbsd" 706 + ], 707 + "engines": { 708 + "node": ">=18" 709 + } 710 + }, 711 + "node_modules/@esbuild/openbsd-x64": { 712 + "version": "0.27.3", 713 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", 714 + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", 715 + "cpu": [ 716 + "x64" 717 + ], 718 + "dev": true, 719 + "license": "MIT", 720 + "optional": true, 721 + "os": [ 722 + "openbsd" 723 + ], 724 + "engines": { 725 + "node": ">=18" 726 + } 727 + }, 728 + "node_modules/@esbuild/openharmony-arm64": { 729 + "version": "0.27.3", 730 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", 731 + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", 732 + "cpu": [ 733 + "arm64" 734 + ], 735 + "dev": true, 736 + "license": "MIT", 737 + "optional": true, 738 + "os": [ 739 + "openharmony" 740 + ], 741 + "engines": { 742 + "node": ">=18" 743 + } 744 + }, 745 + "node_modules/@esbuild/sunos-x64": { 746 + "version": "0.27.3", 747 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", 748 + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", 749 + "cpu": [ 750 + "x64" 751 + ], 752 + "dev": true, 753 + "license": "MIT", 754 + "optional": true, 755 + "os": [ 756 + "sunos" 757 + ], 758 + "engines": { 759 + "node": ">=18" 760 + } 761 + }, 762 + "node_modules/@esbuild/win32-arm64": { 763 + "version": "0.27.3", 764 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", 765 + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", 766 + "cpu": [ 767 + "arm64" 768 + ], 769 + "dev": true, 770 + "license": "MIT", 771 + "optional": true, 772 + "os": [ 773 + "win32" 774 + ], 775 + "engines": { 776 + "node": ">=18" 777 + } 778 + }, 779 + "node_modules/@esbuild/win32-ia32": { 780 + "version": "0.27.3", 781 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", 782 + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", 783 + "cpu": [ 784 + "ia32" 785 + ], 786 + "dev": true, 787 + "license": "MIT", 788 + "optional": true, 789 + "os": [ 790 + "win32" 791 + ], 792 + "engines": { 793 + "node": ">=18" 794 + } 795 + }, 796 + "node_modules/@esbuild/win32-x64": { 797 + "version": "0.27.3", 798 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", 799 + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", 800 + "cpu": [ 801 + "x64" 802 + ], 803 + "dev": true, 804 + "license": "MIT", 805 + "optional": true, 806 + "os": [ 807 + "win32" 808 + ], 809 + "engines": { 810 + "node": ">=18" 811 + } 812 + }, 813 + "node_modules/@hono/node-server": { 814 + "version": "1.19.9", 815 + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", 816 + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", 817 + "license": "MIT", 818 + "engines": { 819 + "node": ">=18.14.1" 820 + }, 821 + "peerDependencies": { 822 + "hono": "^4" 823 + } 824 + }, 825 + "node_modules/@ipld/dag-cbor": { 826 + "version": "7.0.3", 827 + "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-7.0.3.tgz", 828 + "integrity": "sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA==", 829 + "license": "(Apache-2.0 AND MIT)", 830 + "dependencies": { 831 + "cborg": "^1.6.0", 832 + "multiformats": "^9.5.4" 833 + } 834 + }, 835 + "node_modules/@jridgewell/sourcemap-codec": { 836 + "version": "1.5.5", 837 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", 838 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", 839 + "dev": true, 840 + "license": "MIT" 841 + }, 842 + "node_modules/@noble/curves": { 843 + "version": "1.9.7", 844 + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", 845 + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", 846 + "license": "MIT", 847 + "dependencies": { 848 + "@noble/hashes": "1.8.0" 849 + }, 850 + "engines": { 851 + "node": "^14.21.3 || >=16" 852 + }, 853 + "funding": { 854 + "url": "https://paulmillr.com/funding/" 855 + } 856 + }, 857 + "node_modules/@noble/hashes": { 858 + "version": "1.8.0", 859 + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", 860 + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", 861 + "license": "MIT", 862 + "engines": { 863 + "node": "^14.21.3 || >=16" 864 + }, 865 + "funding": { 866 + "url": "https://paulmillr.com/funding/" 867 + } 868 + }, 869 + "node_modules/@rollup/rollup-android-arm-eabi": { 870 + "version": "4.57.1", 871 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", 872 + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", 873 + "cpu": [ 874 + "arm" 875 + ], 876 + "dev": true, 877 + "license": "MIT", 878 + "optional": true, 879 + "os": [ 880 + "android" 881 + ] 882 + }, 883 + "node_modules/@rollup/rollup-android-arm64": { 884 + "version": "4.57.1", 885 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", 886 + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", 887 + "cpu": [ 888 + "arm64" 889 + ], 890 + "dev": true, 891 + "license": "MIT", 892 + "optional": true, 893 + "os": [ 894 + "android" 895 + ] 896 + }, 897 + "node_modules/@rollup/rollup-darwin-arm64": { 898 + "version": "4.57.1", 899 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", 900 + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", 901 + "cpu": [ 902 + "arm64" 903 + ], 904 + "dev": true, 905 + "license": "MIT", 906 + "optional": true, 907 + "os": [ 908 + "darwin" 909 + ] 910 + }, 911 + "node_modules/@rollup/rollup-darwin-x64": { 912 + "version": "4.57.1", 913 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", 914 + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", 915 + "cpu": [ 916 + "x64" 917 + ], 918 + "dev": true, 919 + "license": "MIT", 920 + "optional": true, 921 + "os": [ 922 + "darwin" 923 + ] 924 + }, 925 + "node_modules/@rollup/rollup-freebsd-arm64": { 926 + "version": "4.57.1", 927 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", 928 + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", 929 + "cpu": [ 930 + "arm64" 931 + ], 932 + "dev": true, 933 + "license": "MIT", 934 + "optional": true, 935 + "os": [ 936 + "freebsd" 937 + ] 938 + }, 939 + "node_modules/@rollup/rollup-freebsd-x64": { 940 + "version": "4.57.1", 941 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", 942 + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", 943 + "cpu": [ 944 + "x64" 945 + ], 946 + "dev": true, 947 + "license": "MIT", 948 + "optional": true, 949 + "os": [ 950 + "freebsd" 951 + ] 952 + }, 953 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 954 + "version": "4.57.1", 955 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", 956 + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", 957 + "cpu": [ 958 + "arm" 959 + ], 960 + "dev": true, 961 + "license": "MIT", 962 + "optional": true, 963 + "os": [ 964 + "linux" 965 + ] 966 + }, 967 + "node_modules/@rollup/rollup-linux-arm-musleabihf": { 968 + "version": "4.57.1", 969 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", 970 + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", 971 + "cpu": [ 972 + "arm" 973 + ], 974 + "dev": true, 975 + "license": "MIT", 976 + "optional": true, 977 + "os": [ 978 + "linux" 979 + ] 980 + }, 981 + "node_modules/@rollup/rollup-linux-arm64-gnu": { 982 + "version": "4.57.1", 983 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", 984 + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", 985 + "cpu": [ 986 + "arm64" 987 + ], 988 + "dev": true, 989 + "license": "MIT", 990 + "optional": true, 991 + "os": [ 992 + "linux" 993 + ] 994 + }, 995 + "node_modules/@rollup/rollup-linux-arm64-musl": { 996 + "version": "4.57.1", 997 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", 998 + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", 999 + "cpu": [ 1000 + "arm64" 1001 + ], 1002 + "dev": true, 1003 + "license": "MIT", 1004 + "optional": true, 1005 + "os": [ 1006 + "linux" 1007 + ] 1008 + }, 1009 + "node_modules/@rollup/rollup-linux-loong64-gnu": { 1010 + "version": "4.57.1", 1011 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", 1012 + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", 1013 + "cpu": [ 1014 + "loong64" 1015 + ], 1016 + "dev": true, 1017 + "license": "MIT", 1018 + "optional": true, 1019 + "os": [ 1020 + "linux" 1021 + ] 1022 + }, 1023 + "node_modules/@rollup/rollup-linux-loong64-musl": { 1024 + "version": "4.57.1", 1025 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", 1026 + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", 1027 + "cpu": [ 1028 + "loong64" 1029 + ], 1030 + "dev": true, 1031 + "license": "MIT", 1032 + "optional": true, 1033 + "os": [ 1034 + "linux" 1035 + ] 1036 + }, 1037 + "node_modules/@rollup/rollup-linux-ppc64-gnu": { 1038 + "version": "4.57.1", 1039 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", 1040 + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", 1041 + "cpu": [ 1042 + "ppc64" 1043 + ], 1044 + "dev": true, 1045 + "license": "MIT", 1046 + "optional": true, 1047 + "os": [ 1048 + "linux" 1049 + ] 1050 + }, 1051 + "node_modules/@rollup/rollup-linux-ppc64-musl": { 1052 + "version": "4.57.1", 1053 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", 1054 + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", 1055 + "cpu": [ 1056 + "ppc64" 1057 + ], 1058 + "dev": true, 1059 + "license": "MIT", 1060 + "optional": true, 1061 + "os": [ 1062 + "linux" 1063 + ] 1064 + }, 1065 + "node_modules/@rollup/rollup-linux-riscv64-gnu": { 1066 + "version": "4.57.1", 1067 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", 1068 + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", 1069 + "cpu": [ 1070 + "riscv64" 1071 + ], 1072 + "dev": true, 1073 + "license": "MIT", 1074 + "optional": true, 1075 + "os": [ 1076 + "linux" 1077 + ] 1078 + }, 1079 + "node_modules/@rollup/rollup-linux-riscv64-musl": { 1080 + "version": "4.57.1", 1081 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", 1082 + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", 1083 + "cpu": [ 1084 + "riscv64" 1085 + ], 1086 + "dev": true, 1087 + "license": "MIT", 1088 + "optional": true, 1089 + "os": [ 1090 + "linux" 1091 + ] 1092 + }, 1093 + "node_modules/@rollup/rollup-linux-s390x-gnu": { 1094 + "version": "4.57.1", 1095 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", 1096 + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", 1097 + "cpu": [ 1098 + "s390x" 1099 + ], 1100 + "dev": true, 1101 + "license": "MIT", 1102 + "optional": true, 1103 + "os": [ 1104 + "linux" 1105 + ] 1106 + }, 1107 + "node_modules/@rollup/rollup-linux-x64-gnu": { 1108 + "version": "4.57.1", 1109 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", 1110 + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", 1111 + "cpu": [ 1112 + "x64" 1113 + ], 1114 + "dev": true, 1115 + "license": "MIT", 1116 + "optional": true, 1117 + "os": [ 1118 + "linux" 1119 + ] 1120 + }, 1121 + "node_modules/@rollup/rollup-linux-x64-musl": { 1122 + "version": "4.57.1", 1123 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", 1124 + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", 1125 + "cpu": [ 1126 + "x64" 1127 + ], 1128 + "dev": true, 1129 + "license": "MIT", 1130 + "optional": true, 1131 + "os": [ 1132 + "linux" 1133 + ] 1134 + }, 1135 + "node_modules/@rollup/rollup-openbsd-x64": { 1136 + "version": "4.57.1", 1137 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", 1138 + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", 1139 + "cpu": [ 1140 + "x64" 1141 + ], 1142 + "dev": true, 1143 + "license": "MIT", 1144 + "optional": true, 1145 + "os": [ 1146 + "openbsd" 1147 + ] 1148 + }, 1149 + "node_modules/@rollup/rollup-openharmony-arm64": { 1150 + "version": "4.57.1", 1151 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", 1152 + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", 1153 + "cpu": [ 1154 + "arm64" 1155 + ], 1156 + "dev": true, 1157 + "license": "MIT", 1158 + "optional": true, 1159 + "os": [ 1160 + "openharmony" 1161 + ] 1162 + }, 1163 + "node_modules/@rollup/rollup-win32-arm64-msvc": { 1164 + "version": "4.57.1", 1165 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", 1166 + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", 1167 + "cpu": [ 1168 + "arm64" 1169 + ], 1170 + "dev": true, 1171 + "license": "MIT", 1172 + "optional": true, 1173 + "os": [ 1174 + "win32" 1175 + ] 1176 + }, 1177 + "node_modules/@rollup/rollup-win32-ia32-msvc": { 1178 + "version": "4.57.1", 1179 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", 1180 + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", 1181 + "cpu": [ 1182 + "ia32" 1183 + ], 1184 + "dev": true, 1185 + "license": "MIT", 1186 + "optional": true, 1187 + "os": [ 1188 + "win32" 1189 + ] 1190 + }, 1191 + "node_modules/@rollup/rollup-win32-x64-gnu": { 1192 + "version": "4.57.1", 1193 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", 1194 + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", 1195 + "cpu": [ 1196 + "x64" 1197 + ], 1198 + "dev": true, 1199 + "license": "MIT", 1200 + "optional": true, 1201 + "os": [ 1202 + "win32" 1203 + ] 1204 + }, 1205 + "node_modules/@rollup/rollup-win32-x64-msvc": { 1206 + "version": "4.57.1", 1207 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", 1208 + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", 1209 + "cpu": [ 1210 + "x64" 1211 + ], 1212 + "dev": true, 1213 + "license": "MIT", 1214 + "optional": true, 1215 + "os": [ 1216 + "win32" 1217 + ] 1218 + }, 1219 + "node_modules/@standard-schema/spec": { 1220 + "version": "1.1.0", 1221 + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", 1222 + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", 1223 + "license": "MIT" 1224 + }, 1225 + "node_modules/@types/bcryptjs": { 1226 + "version": "3.0.0", 1227 + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-3.0.0.tgz", 1228 + "integrity": "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==", 1229 + "deprecated": "This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.", 1230 + "dev": true, 1231 + "license": "MIT", 1232 + "dependencies": { 1233 + "bcryptjs": "*" 1234 + } 1235 + }, 1236 + "node_modules/@types/better-sqlite3": { 1237 + "version": "7.6.13", 1238 + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", 1239 + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", 1240 + "dev": true, 1241 + "license": "MIT", 1242 + "dependencies": { 1243 + "@types/node": "*" 1244 + } 1245 + }, 1246 + "node_modules/@types/bun": { 1247 + "version": "1.3.9", 1248 + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.9.tgz", 1249 + "integrity": "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw==", 1250 + "license": "MIT", 1251 + "dependencies": { 1252 + "bun-types": "1.3.9" 1253 + } 1254 + }, 1255 + "node_modules/@types/chai": { 1256 + "version": "5.2.3", 1257 + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", 1258 + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", 1259 + "dev": true, 1260 + "license": "MIT", 1261 + "dependencies": { 1262 + "@types/deep-eql": "*", 1263 + "assertion-error": "^2.0.1" 1264 + } 1265 + }, 1266 + "node_modules/@types/deep-eql": { 1267 + "version": "4.0.2", 1268 + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", 1269 + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", 1270 + "dev": true, 1271 + "license": "MIT" 1272 + }, 1273 + "node_modules/@types/estree": { 1274 + "version": "1.0.8", 1275 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", 1276 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 1277 + "dev": true, 1278 + "license": "MIT" 1279 + }, 1280 + "node_modules/@types/node": { 1281 + "version": "25.2.3", 1282 + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", 1283 + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", 1284 + "license": "MIT", 1285 + "dependencies": { 1286 + "undici-types": "~7.16.0" 1287 + } 1288 + }, 1289 + "node_modules/@types/ws": { 1290 + "version": "8.18.1", 1291 + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", 1292 + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", 1293 + "dev": true, 1294 + "license": "MIT", 1295 + "dependencies": { 1296 + "@types/node": "*" 1297 + } 1298 + }, 1299 + "node_modules/@vitest/expect": { 1300 + "version": "3.2.4", 1301 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", 1302 + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", 1303 + "dev": true, 1304 + "license": "MIT", 1305 + "dependencies": { 1306 + "@types/chai": "^5.2.2", 1307 + "@vitest/spy": "3.2.4", 1308 + "@vitest/utils": "3.2.4", 1309 + "chai": "^5.2.0", 1310 + "tinyrainbow": "^2.0.0" 1311 + }, 1312 + "funding": { 1313 + "url": "https://opencollective.com/vitest" 1314 + } 1315 + }, 1316 + "node_modules/@vitest/mocker": { 1317 + "version": "3.2.4", 1318 + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", 1319 + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", 1320 + "dev": true, 1321 + "license": "MIT", 1322 + "dependencies": { 1323 + "@vitest/spy": "3.2.4", 1324 + "estree-walker": "^3.0.3", 1325 + "magic-string": "^0.30.17" 1326 + }, 1327 + "funding": { 1328 + "url": "https://opencollective.com/vitest" 1329 + }, 1330 + "peerDependencies": { 1331 + "msw": "^2.4.9", 1332 + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" 1333 + }, 1334 + "peerDependenciesMeta": { 1335 + "msw": { 1336 + "optional": true 1337 + }, 1338 + "vite": { 1339 + "optional": true 1340 + } 1341 + } 1342 + }, 1343 + "node_modules/@vitest/pretty-format": { 1344 + "version": "3.2.4", 1345 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", 1346 + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", 1347 + "dev": true, 1348 + "license": "MIT", 1349 + "dependencies": { 1350 + "tinyrainbow": "^2.0.0" 1351 + }, 1352 + "funding": { 1353 + "url": "https://opencollective.com/vitest" 1354 + } 1355 + }, 1356 + "node_modules/@vitest/runner": { 1357 + "version": "3.2.4", 1358 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", 1359 + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", 1360 + "dev": true, 1361 + "license": "MIT", 1362 + "dependencies": { 1363 + "@vitest/utils": "3.2.4", 1364 + "pathe": "^2.0.3", 1365 + "strip-literal": "^3.0.0" 1366 + }, 1367 + "funding": { 1368 + "url": "https://opencollective.com/vitest" 1369 + } 1370 + }, 1371 + "node_modules/@vitest/snapshot": { 1372 + "version": "3.2.4", 1373 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", 1374 + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", 1375 + "dev": true, 1376 + "license": "MIT", 1377 + "dependencies": { 1378 + "@vitest/pretty-format": "3.2.4", 1379 + "magic-string": "^0.30.17", 1380 + "pathe": "^2.0.3" 1381 + }, 1382 + "funding": { 1383 + "url": "https://opencollective.com/vitest" 1384 + } 1385 + }, 1386 + "node_modules/@vitest/spy": { 1387 + "version": "3.2.4", 1388 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", 1389 + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", 1390 + "dev": true, 1391 + "license": "MIT", 1392 + "dependencies": { 1393 + "tinyspy": "^4.0.3" 1394 + }, 1395 + "funding": { 1396 + "url": "https://opencollective.com/vitest" 1397 + } 1398 + }, 1399 + "node_modules/@vitest/utils": { 1400 + "version": "3.2.4", 1401 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", 1402 + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", 1403 + "dev": true, 1404 + "license": "MIT", 1405 + "dependencies": { 1406 + "@vitest/pretty-format": "3.2.4", 1407 + "loupe": "^3.1.4", 1408 + "tinyrainbow": "^2.0.0" 1409 + }, 1410 + "funding": { 1411 + "url": "https://opencollective.com/vitest" 1412 + } 1413 + }, 1414 + "node_modules/abort-controller": { 1415 + "version": "3.0.0", 1416 + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 1417 + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 1418 + "license": "MIT", 1419 + "dependencies": { 1420 + "event-target-shim": "^5.0.0" 1421 + }, 1422 + "engines": { 1423 + "node": ">=6.5" 1424 + } 1425 + }, 1426 + "node_modules/assertion-error": { 1427 + "version": "2.0.1", 1428 + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", 1429 + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", 1430 + "dev": true, 1431 + "license": "MIT", 1432 + "engines": { 1433 + "node": ">=12" 1434 + } 1435 + }, 1436 + "node_modules/atomic-sleep": { 1437 + "version": "1.0.0", 1438 + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", 1439 + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", 1440 + "license": "MIT", 1441 + "engines": { 1442 + "node": ">=8.0.0" 1443 + } 1444 + }, 1445 + "node_modules/base64-js": { 1446 + "version": "1.5.1", 1447 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 1448 + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 1449 + "funding": [ 1450 + { 1451 + "type": "github", 1452 + "url": "https://github.com/sponsors/feross" 1453 + }, 1454 + { 1455 + "type": "patreon", 1456 + "url": "https://www.patreon.com/feross" 1457 + }, 1458 + { 1459 + "type": "consulting", 1460 + "url": "https://feross.org/support" 1461 + } 1462 + ], 1463 + "license": "MIT" 1464 + }, 1465 + "node_modules/bcryptjs": { 1466 + "version": "3.0.3", 1467 + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", 1468 + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", 1469 + "license": "BSD-3-Clause", 1470 + "bin": { 1471 + "bcrypt": "bin/bcrypt" 1472 + } 1473 + }, 1474 + "node_modules/better-sqlite3": { 1475 + "version": "11.10.0", 1476 + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", 1477 + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", 1478 + "hasInstallScript": true, 1479 + "license": "MIT", 1480 + "dependencies": { 1481 + "bindings": "^1.5.0", 1482 + "prebuild-install": "^7.1.1" 1483 + } 1484 + }, 1485 + "node_modules/bindings": { 1486 + "version": "1.5.0", 1487 + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", 1488 + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 1489 + "license": "MIT", 1490 + "dependencies": { 1491 + "file-uri-to-path": "1.0.0" 1492 + } 1493 + }, 1494 + "node_modules/bl": { 1495 + "version": "4.1.0", 1496 + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 1497 + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 1498 + "license": "MIT", 1499 + "dependencies": { 1500 + "buffer": "^5.5.0", 1501 + "inherits": "^2.0.4", 1502 + "readable-stream": "^3.4.0" 1503 + } 1504 + }, 1505 + "node_modules/bl/node_modules/buffer": { 1506 + "version": "5.7.1", 1507 + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 1508 + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 1509 + "funding": [ 1510 + { 1511 + "type": "github", 1512 + "url": "https://github.com/sponsors/feross" 1513 + }, 1514 + { 1515 + "type": "patreon", 1516 + "url": "https://www.patreon.com/feross" 1517 + }, 1518 + { 1519 + "type": "consulting", 1520 + "url": "https://feross.org/support" 1521 + } 1522 + ], 1523 + "license": "MIT", 1524 + "dependencies": { 1525 + "base64-js": "^1.3.1", 1526 + "ieee754": "^1.1.13" 1527 + } 1528 + }, 1529 + "node_modules/bl/node_modules/readable-stream": { 1530 + "version": "3.6.2", 1531 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 1532 + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 1533 + "license": "MIT", 1534 + "dependencies": { 1535 + "inherits": "^2.0.3", 1536 + "string_decoder": "^1.1.1", 1537 + "util-deprecate": "^1.0.1" 1538 + }, 1539 + "engines": { 1540 + "node": ">= 6" 1541 + } 1542 + }, 1543 + "node_modules/buffer": { 1544 + "version": "6.0.3", 1545 + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", 1546 + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", 1547 + "funding": [ 1548 + { 1549 + "type": "github", 1550 + "url": "https://github.com/sponsors/feross" 1551 + }, 1552 + { 1553 + "type": "patreon", 1554 + "url": "https://www.patreon.com/feross" 1555 + }, 1556 + { 1557 + "type": "consulting", 1558 + "url": "https://feross.org/support" 1559 + } 1560 + ], 1561 + "license": "MIT", 1562 + "dependencies": { 1563 + "base64-js": "^1.3.1", 1564 + "ieee754": "^1.2.1" 1565 + } 1566 + }, 1567 + "node_modules/bun-types": { 1568 + "version": "1.3.9", 1569 + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.9.tgz", 1570 + "integrity": "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg==", 1571 + "license": "MIT", 1572 + "dependencies": { 1573 + "@types/node": "*" 1574 + } 1575 + }, 1576 + "node_modules/cac": { 1577 + "version": "6.7.14", 1578 + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", 1579 + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", 1580 + "dev": true, 1581 + "license": "MIT", 1582 + "engines": { 1583 + "node": ">=8" 1584 + } 1585 + }, 1586 + "node_modules/cborg": { 1587 + "version": "1.10.2", 1588 + "resolved": "https://registry.npmjs.org/cborg/-/cborg-1.10.2.tgz", 1589 + "integrity": "sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==", 1590 + "license": "Apache-2.0", 1591 + "bin": { 1592 + "cborg": "cli.js" 1593 + } 1594 + }, 1595 + "node_modules/chai": { 1596 + "version": "5.3.3", 1597 + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", 1598 + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", 1599 + "dev": true, 1600 + "license": "MIT", 1601 + "dependencies": { 1602 + "assertion-error": "^2.0.1", 1603 + "check-error": "^2.1.1", 1604 + "deep-eql": "^5.0.1", 1605 + "loupe": "^3.1.0", 1606 + "pathval": "^2.0.0" 1607 + }, 1608 + "engines": { 1609 + "node": ">=18" 1610 + } 1611 + }, 1612 + "node_modules/check-error": { 1613 + "version": "2.1.3", 1614 + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", 1615 + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", 1616 + "dev": true, 1617 + "license": "MIT", 1618 + "engines": { 1619 + "node": ">= 16" 1620 + } 1621 + }, 1622 + "node_modules/chownr": { 1623 + "version": "1.1.4", 1624 + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 1625 + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", 1626 + "license": "ISC" 1627 + }, 1628 + "node_modules/debug": { 1629 + "version": "4.4.3", 1630 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 1631 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 1632 + "dev": true, 1633 + "license": "MIT", 1634 + "dependencies": { 1635 + "ms": "^2.1.3" 1636 + }, 1637 + "engines": { 1638 + "node": ">=6.0" 1639 + }, 1640 + "peerDependenciesMeta": { 1641 + "supports-color": { 1642 + "optional": true 1643 + } 1644 + } 1645 + }, 1646 + "node_modules/decompress-response": { 1647 + "version": "6.0.0", 1648 + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 1649 + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 1650 + "license": "MIT", 1651 + "dependencies": { 1652 + "mimic-response": "^3.1.0" 1653 + }, 1654 + "engines": { 1655 + "node": ">=10" 1656 + }, 1657 + "funding": { 1658 + "url": "https://github.com/sponsors/sindresorhus" 1659 + } 1660 + }, 1661 + "node_modules/deep-eql": { 1662 + "version": "5.0.2", 1663 + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", 1664 + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", 1665 + "dev": true, 1666 + "license": "MIT", 1667 + "engines": { 1668 + "node": ">=6" 1669 + } 1670 + }, 1671 + "node_modules/deep-extend": { 1672 + "version": "0.6.0", 1673 + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 1674 + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", 1675 + "license": "MIT", 1676 + "engines": { 1677 + "node": ">=4.0.0" 1678 + } 1679 + }, 1680 + "node_modules/detect-libc": { 1681 + "version": "2.1.2", 1682 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", 1683 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", 1684 + "license": "Apache-2.0", 1685 + "engines": { 1686 + "node": ">=8" 1687 + } 1688 + }, 1689 + "node_modules/end-of-stream": { 1690 + "version": "1.4.5", 1691 + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", 1692 + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", 1693 + "license": "MIT", 1694 + "dependencies": { 1695 + "once": "^1.4.0" 1696 + } 1697 + }, 1698 + "node_modules/es-module-lexer": { 1699 + "version": "1.7.0", 1700 + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", 1701 + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", 1702 + "dev": true, 1703 + "license": "MIT" 1704 + }, 1705 + "node_modules/esbuild": { 1706 + "version": "0.27.3", 1707 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", 1708 + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", 1709 + "dev": true, 1710 + "hasInstallScript": true, 1711 + "license": "MIT", 1712 + "bin": { 1713 + "esbuild": "bin/esbuild" 1714 + }, 1715 + "engines": { 1716 + "node": ">=18" 1717 + }, 1718 + "optionalDependencies": { 1719 + "@esbuild/aix-ppc64": "0.27.3", 1720 + "@esbuild/android-arm": "0.27.3", 1721 + "@esbuild/android-arm64": "0.27.3", 1722 + "@esbuild/android-x64": "0.27.3", 1723 + "@esbuild/darwin-arm64": "0.27.3", 1724 + "@esbuild/darwin-x64": "0.27.3", 1725 + "@esbuild/freebsd-arm64": "0.27.3", 1726 + "@esbuild/freebsd-x64": "0.27.3", 1727 + "@esbuild/linux-arm": "0.27.3", 1728 + "@esbuild/linux-arm64": "0.27.3", 1729 + "@esbuild/linux-ia32": "0.27.3", 1730 + "@esbuild/linux-loong64": "0.27.3", 1731 + "@esbuild/linux-mips64el": "0.27.3", 1732 + "@esbuild/linux-ppc64": "0.27.3", 1733 + "@esbuild/linux-riscv64": "0.27.3", 1734 + "@esbuild/linux-s390x": "0.27.3", 1735 + "@esbuild/linux-x64": "0.27.3", 1736 + "@esbuild/netbsd-arm64": "0.27.3", 1737 + "@esbuild/netbsd-x64": "0.27.3", 1738 + "@esbuild/openbsd-arm64": "0.27.3", 1739 + "@esbuild/openbsd-x64": "0.27.3", 1740 + "@esbuild/openharmony-arm64": "0.27.3", 1741 + "@esbuild/sunos-x64": "0.27.3", 1742 + "@esbuild/win32-arm64": "0.27.3", 1743 + "@esbuild/win32-ia32": "0.27.3", 1744 + "@esbuild/win32-x64": "0.27.3" 1745 + } 1746 + }, 1747 + "node_modules/esm-env": { 1748 + "version": "1.2.2", 1749 + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", 1750 + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", 1751 + "license": "MIT" 1752 + }, 1753 + "node_modules/estree-walker": { 1754 + "version": "3.0.3", 1755 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", 1756 + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", 1757 + "dev": true, 1758 + "license": "MIT", 1759 + "dependencies": { 1760 + "@types/estree": "^1.0.0" 1761 + } 1762 + }, 1763 + "node_modules/event-target-shim": { 1764 + "version": "5.0.1", 1765 + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 1766 + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", 1767 + "license": "MIT", 1768 + "engines": { 1769 + "node": ">=6" 1770 + } 1771 + }, 1772 + "node_modules/events": { 1773 + "version": "3.3.0", 1774 + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", 1775 + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", 1776 + "license": "MIT", 1777 + "engines": { 1778 + "node": ">=0.8.x" 1779 + } 1780 + }, 1781 + "node_modules/expand-template": { 1782 + "version": "2.0.3", 1783 + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 1784 + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", 1785 + "license": "(MIT OR WTFPL)", 1786 + "engines": { 1787 + "node": ">=6" 1788 + } 1789 + }, 1790 + "node_modules/expect-type": { 1791 + "version": "1.3.0", 1792 + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", 1793 + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", 1794 + "dev": true, 1795 + "license": "Apache-2.0", 1796 + "engines": { 1797 + "node": ">=12.0.0" 1798 + } 1799 + }, 1800 + "node_modules/fast-redact": { 1801 + "version": "3.5.0", 1802 + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", 1803 + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", 1804 + "license": "MIT", 1805 + "engines": { 1806 + "node": ">=6" 1807 + } 1808 + }, 1809 + "node_modules/fdir": { 1810 + "version": "6.5.0", 1811 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 1812 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 1813 + "dev": true, 1814 + "license": "MIT", 1815 + "engines": { 1816 + "node": ">=12.0.0" 1817 + }, 1818 + "peerDependencies": { 1819 + "picomatch": "^3 || ^4" 1820 + }, 1821 + "peerDependenciesMeta": { 1822 + "picomatch": { 1823 + "optional": true 1824 + } 1825 + } 1826 + }, 1827 + "node_modules/file-uri-to-path": { 1828 + "version": "1.0.0", 1829 + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 1830 + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", 1831 + "license": "MIT" 1832 + }, 1833 + "node_modules/fs-constants": { 1834 + "version": "1.0.0", 1835 + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 1836 + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", 1837 + "license": "MIT" 1838 + }, 1839 + "node_modules/fsevents": { 1840 + "version": "2.3.3", 1841 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1842 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1843 + "dev": true, 1844 + "hasInstallScript": true, 1845 + "license": "MIT", 1846 + "optional": true, 1847 + "os": [ 1848 + "darwin" 1849 + ], 1850 + "engines": { 1851 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1852 + } 1853 + }, 1854 + "node_modules/get-tsconfig": { 1855 + "version": "4.13.6", 1856 + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", 1857 + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", 1858 + "dev": true, 1859 + "license": "MIT", 1860 + "dependencies": { 1861 + "resolve-pkg-maps": "^1.0.0" 1862 + }, 1863 + "funding": { 1864 + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" 1865 + } 1866 + }, 1867 + "node_modules/github-from-package": { 1868 + "version": "0.0.0", 1869 + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", 1870 + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", 1871 + "license": "MIT" 1872 + }, 1873 + "node_modules/hono": { 1874 + "version": "4.11.9", 1875 + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", 1876 + "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", 1877 + "license": "MIT", 1878 + "engines": { 1879 + "node": ">=16.9.0" 1880 + } 1881 + }, 1882 + "node_modules/ieee754": { 1883 + "version": "1.2.1", 1884 + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 1885 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 1886 + "funding": [ 1887 + { 1888 + "type": "github", 1889 + "url": "https://github.com/sponsors/feross" 1890 + }, 1891 + { 1892 + "type": "patreon", 1893 + "url": "https://www.patreon.com/feross" 1894 + }, 1895 + { 1896 + "type": "consulting", 1897 + "url": "https://feross.org/support" 1898 + } 1899 + ], 1900 + "license": "BSD-3-Clause" 1901 + }, 1902 + "node_modules/inherits": { 1903 + "version": "2.0.4", 1904 + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1905 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1906 + "license": "ISC" 1907 + }, 1908 + "node_modules/ini": { 1909 + "version": "1.3.8", 1910 + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 1911 + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", 1912 + "license": "ISC" 1913 + }, 1914 + "node_modules/iso-datestring-validator": { 1915 + "version": "2.2.2", 1916 + "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", 1917 + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", 1918 + "license": "MIT" 1919 + }, 1920 + "node_modules/jose": { 1921 + "version": "6.1.3", 1922 + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", 1923 + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", 1924 + "license": "MIT", 1925 + "funding": { 1926 + "url": "https://github.com/sponsors/panva" 1927 + } 1928 + }, 1929 + "node_modules/js-tokens": { 1930 + "version": "9.0.1", 1931 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", 1932 + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", 1933 + "dev": true, 1934 + "license": "MIT" 1935 + }, 1936 + "node_modules/loupe": { 1937 + "version": "3.2.1", 1938 + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", 1939 + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", 1940 + "dev": true, 1941 + "license": "MIT" 1942 + }, 1943 + "node_modules/magic-string": { 1944 + "version": "0.30.21", 1945 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", 1946 + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", 1947 + "dev": true, 1948 + "license": "MIT", 1949 + "dependencies": { 1950 + "@jridgewell/sourcemap-codec": "^1.5.5" 1951 + } 1952 + }, 1953 + "node_modules/mimic-response": { 1954 + "version": "3.1.0", 1955 + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 1956 + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", 1957 + "license": "MIT", 1958 + "engines": { 1959 + "node": ">=10" 1960 + }, 1961 + "funding": { 1962 + "url": "https://github.com/sponsors/sindresorhus" 1963 + } 1964 + }, 1965 + "node_modules/minimist": { 1966 + "version": "1.2.8", 1967 + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 1968 + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 1969 + "license": "MIT", 1970 + "funding": { 1971 + "url": "https://github.com/sponsors/ljharb" 1972 + } 1973 + }, 1974 + "node_modules/mkdirp-classic": { 1975 + "version": "0.5.3", 1976 + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 1977 + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", 1978 + "license": "MIT" 1979 + }, 1980 + "node_modules/ms": { 1981 + "version": "2.1.3", 1982 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1983 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1984 + "dev": true, 1985 + "license": "MIT" 1986 + }, 1987 + "node_modules/multiformats": { 1988 + "version": "9.9.0", 1989 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 1990 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 1991 + "license": "(Apache-2.0 AND MIT)" 1992 + }, 1993 + "node_modules/nanoid": { 1994 + "version": "3.3.11", 1995 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 1996 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 1997 + "dev": true, 1998 + "funding": [ 1999 + { 2000 + "type": "github", 2001 + "url": "https://github.com/sponsors/ai" 2002 + } 2003 + ], 2004 + "license": "MIT", 2005 + "bin": { 2006 + "nanoid": "bin/nanoid.cjs" 2007 + }, 2008 + "engines": { 2009 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 2010 + } 2011 + }, 2012 + "node_modules/napi-build-utils": { 2013 + "version": "2.0.0", 2014 + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", 2015 + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", 2016 + "license": "MIT" 2017 + }, 2018 + "node_modules/node-abi": { 2019 + "version": "3.87.0", 2020 + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", 2021 + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", 2022 + "license": "MIT", 2023 + "dependencies": { 2024 + "semver": "^7.3.5" 2025 + }, 2026 + "engines": { 2027 + "node": ">=10" 2028 + } 2029 + }, 2030 + "node_modules/node-gyp-build": { 2031 + "version": "4.8.4", 2032 + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", 2033 + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", 2034 + "license": "MIT", 2035 + "bin": { 2036 + "node-gyp-build": "bin.js", 2037 + "node-gyp-build-optional": "optional.js", 2038 + "node-gyp-build-test": "build-test.js" 2039 + } 2040 + }, 2041 + "node_modules/on-exit-leak-free": { 2042 + "version": "2.1.2", 2043 + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", 2044 + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", 2045 + "license": "MIT", 2046 + "engines": { 2047 + "node": ">=14.0.0" 2048 + } 2049 + }, 2050 + "node_modules/once": { 2051 + "version": "1.4.0", 2052 + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 2053 + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 2054 + "license": "ISC", 2055 + "dependencies": { 2056 + "wrappy": "1" 2057 + } 2058 + }, 2059 + "node_modules/pathe": { 2060 + "version": "2.0.3", 2061 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 2062 + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 2063 + "dev": true, 2064 + "license": "MIT" 2065 + }, 2066 + "node_modules/pathval": { 2067 + "version": "2.0.1", 2068 + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", 2069 + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", 2070 + "dev": true, 2071 + "license": "MIT", 2072 + "engines": { 2073 + "node": ">= 14.16" 2074 + } 2075 + }, 2076 + "node_modules/picocolors": { 2077 + "version": "1.1.1", 2078 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 2079 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 2080 + "license": "ISC" 2081 + }, 2082 + "node_modules/picomatch": { 2083 + "version": "4.0.3", 2084 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 2085 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 2086 + "dev": true, 2087 + "license": "MIT", 2088 + "engines": { 2089 + "node": ">=12" 2090 + }, 2091 + "funding": { 2092 + "url": "https://github.com/sponsors/jonschlinkert" 2093 + } 2094 + }, 2095 + "node_modules/pino": { 2096 + "version": "8.21.0", 2097 + "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", 2098 + "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", 2099 + "license": "MIT", 2100 + "dependencies": { 2101 + "atomic-sleep": "^1.0.0", 2102 + "fast-redact": "^3.1.1", 2103 + "on-exit-leak-free": "^2.1.0", 2104 + "pino-abstract-transport": "^1.2.0", 2105 + "pino-std-serializers": "^6.0.0", 2106 + "process-warning": "^3.0.0", 2107 + "quick-format-unescaped": "^4.0.3", 2108 + "real-require": "^0.2.0", 2109 + "safe-stable-stringify": "^2.3.1", 2110 + "sonic-boom": "^3.7.0", 2111 + "thread-stream": "^2.6.0" 2112 + }, 2113 + "bin": { 2114 + "pino": "bin.js" 2115 + } 2116 + }, 2117 + "node_modules/pino-abstract-transport": { 2118 + "version": "1.2.0", 2119 + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", 2120 + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", 2121 + "license": "MIT", 2122 + "dependencies": { 2123 + "readable-stream": "^4.0.0", 2124 + "split2": "^4.0.0" 2125 + } 2126 + }, 2127 + "node_modules/pino-std-serializers": { 2128 + "version": "6.2.2", 2129 + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", 2130 + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", 2131 + "license": "MIT" 2132 + }, 2133 + "node_modules/postcss": { 2134 + "version": "8.5.6", 2135 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", 2136 + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", 2137 + "dev": true, 2138 + "funding": [ 2139 + { 2140 + "type": "opencollective", 2141 + "url": "https://opencollective.com/postcss/" 2142 + }, 2143 + { 2144 + "type": "tidelift", 2145 + "url": "https://tidelift.com/funding/github/npm/postcss" 2146 + }, 2147 + { 2148 + "type": "github", 2149 + "url": "https://github.com/sponsors/ai" 2150 + } 2151 + ], 2152 + "license": "MIT", 2153 + "dependencies": { 2154 + "nanoid": "^3.3.11", 2155 + "picocolors": "^1.1.1", 2156 + "source-map-js": "^1.2.1" 2157 + }, 2158 + "engines": { 2159 + "node": "^10 || ^12 || >=14" 2160 + } 2161 + }, 2162 + "node_modules/prebuild-install": { 2163 + "version": "7.1.3", 2164 + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", 2165 + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", 2166 + "license": "MIT", 2167 + "dependencies": { 2168 + "detect-libc": "^2.0.0", 2169 + "expand-template": "^2.0.3", 2170 + "github-from-package": "0.0.0", 2171 + "minimist": "^1.2.3", 2172 + "mkdirp-classic": "^0.5.3", 2173 + "napi-build-utils": "^2.0.0", 2174 + "node-abi": "^3.3.0", 2175 + "pump": "^3.0.0", 2176 + "rc": "^1.2.7", 2177 + "simple-get": "^4.0.0", 2178 + "tar-fs": "^2.0.0", 2179 + "tunnel-agent": "^0.6.0" 2180 + }, 2181 + "bin": { 2182 + "prebuild-install": "bin.js" 2183 + }, 2184 + "engines": { 2185 + "node": ">=10" 2186 + } 2187 + }, 2188 + "node_modules/process": { 2189 + "version": "0.11.10", 2190 + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", 2191 + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", 2192 + "license": "MIT", 2193 + "engines": { 2194 + "node": ">= 0.6.0" 2195 + } 2196 + }, 2197 + "node_modules/process-warning": { 2198 + "version": "3.0.0", 2199 + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", 2200 + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", 2201 + "license": "MIT" 2202 + }, 2203 + "node_modules/pump": { 2204 + "version": "3.0.3", 2205 + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", 2206 + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", 2207 + "license": "MIT", 2208 + "dependencies": { 2209 + "end-of-stream": "^1.1.0", 2210 + "once": "^1.3.1" 2211 + } 2212 + }, 2213 + "node_modules/quick-format-unescaped": { 2214 + "version": "4.0.4", 2215 + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", 2216 + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", 2217 + "license": "MIT" 2218 + }, 2219 + "node_modules/rc": { 2220 + "version": "1.2.8", 2221 + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 2222 + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 2223 + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", 2224 + "dependencies": { 2225 + "deep-extend": "^0.6.0", 2226 + "ini": "~1.3.0", 2227 + "minimist": "^1.2.0", 2228 + "strip-json-comments": "~2.0.1" 2229 + }, 2230 + "bin": { 2231 + "rc": "cli.js" 2232 + } 2233 + }, 2234 + "node_modules/readable-stream": { 2235 + "version": "4.7.0", 2236 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", 2237 + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", 2238 + "license": "MIT", 2239 + "dependencies": { 2240 + "abort-controller": "^3.0.0", 2241 + "buffer": "^6.0.3", 2242 + "events": "^3.3.0", 2243 + "process": "^0.11.10", 2244 + "string_decoder": "^1.3.0" 2245 + }, 2246 + "engines": { 2247 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 2248 + } 2249 + }, 2250 + "node_modules/real-require": { 2251 + "version": "0.2.0", 2252 + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", 2253 + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", 2254 + "license": "MIT", 2255 + "engines": { 2256 + "node": ">= 12.13.0" 2257 + } 2258 + }, 2259 + "node_modules/resolve-pkg-maps": { 2260 + "version": "1.0.0", 2261 + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", 2262 + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", 2263 + "dev": true, 2264 + "license": "MIT", 2265 + "funding": { 2266 + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" 2267 + } 2268 + }, 2269 + "node_modules/rollup": { 2270 + "version": "4.57.1", 2271 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", 2272 + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", 2273 + "dev": true, 2274 + "license": "MIT", 2275 + "dependencies": { 2276 + "@types/estree": "1.0.8" 2277 + }, 2278 + "bin": { 2279 + "rollup": "dist/bin/rollup" 2280 + }, 2281 + "engines": { 2282 + "node": ">=18.0.0", 2283 + "npm": ">=8.0.0" 2284 + }, 2285 + "optionalDependencies": { 2286 + "@rollup/rollup-android-arm-eabi": "4.57.1", 2287 + "@rollup/rollup-android-arm64": "4.57.1", 2288 + "@rollup/rollup-darwin-arm64": "4.57.1", 2289 + "@rollup/rollup-darwin-x64": "4.57.1", 2290 + "@rollup/rollup-freebsd-arm64": "4.57.1", 2291 + "@rollup/rollup-freebsd-x64": "4.57.1", 2292 + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", 2293 + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", 2294 + "@rollup/rollup-linux-arm64-gnu": "4.57.1", 2295 + "@rollup/rollup-linux-arm64-musl": "4.57.1", 2296 + "@rollup/rollup-linux-loong64-gnu": "4.57.1", 2297 + "@rollup/rollup-linux-loong64-musl": "4.57.1", 2298 + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", 2299 + "@rollup/rollup-linux-ppc64-musl": "4.57.1", 2300 + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", 2301 + "@rollup/rollup-linux-riscv64-musl": "4.57.1", 2302 + "@rollup/rollup-linux-s390x-gnu": "4.57.1", 2303 + "@rollup/rollup-linux-x64-gnu": "4.57.1", 2304 + "@rollup/rollup-linux-x64-musl": "4.57.1", 2305 + "@rollup/rollup-openbsd-x64": "4.57.1", 2306 + "@rollup/rollup-openharmony-arm64": "4.57.1", 2307 + "@rollup/rollup-win32-arm64-msvc": "4.57.1", 2308 + "@rollup/rollup-win32-ia32-msvc": "4.57.1", 2309 + "@rollup/rollup-win32-x64-gnu": "4.57.1", 2310 + "@rollup/rollup-win32-x64-msvc": "4.57.1", 2311 + "fsevents": "~2.3.2" 2312 + } 2313 + }, 2314 + "node_modules/safe-buffer": { 2315 + "version": "5.2.1", 2316 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 2317 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 2318 + "funding": [ 2319 + { 2320 + "type": "github", 2321 + "url": "https://github.com/sponsors/feross" 2322 + }, 2323 + { 2324 + "type": "patreon", 2325 + "url": "https://www.patreon.com/feross" 2326 + }, 2327 + { 2328 + "type": "consulting", 2329 + "url": "https://feross.org/support" 2330 + } 2331 + ], 2332 + "license": "MIT" 2333 + }, 2334 + "node_modules/safe-stable-stringify": { 2335 + "version": "2.5.0", 2336 + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", 2337 + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", 2338 + "license": "MIT", 2339 + "engines": { 2340 + "node": ">=10" 2341 + } 2342 + }, 2343 + "node_modules/semver": { 2344 + "version": "7.7.4", 2345 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", 2346 + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", 2347 + "license": "ISC", 2348 + "bin": { 2349 + "semver": "bin/semver.js" 2350 + }, 2351 + "engines": { 2352 + "node": ">=10" 2353 + } 2354 + }, 2355 + "node_modules/siginfo": { 2356 + "version": "2.0.0", 2357 + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", 2358 + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", 2359 + "dev": true, 2360 + "license": "ISC" 2361 + }, 2362 + "node_modules/simple-concat": { 2363 + "version": "1.0.1", 2364 + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 2365 + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", 2366 + "funding": [ 2367 + { 2368 + "type": "github", 2369 + "url": "https://github.com/sponsors/feross" 2370 + }, 2371 + { 2372 + "type": "patreon", 2373 + "url": "https://www.patreon.com/feross" 2374 + }, 2375 + { 2376 + "type": "consulting", 2377 + "url": "https://feross.org/support" 2378 + } 2379 + ], 2380 + "license": "MIT" 2381 + }, 2382 + "node_modules/simple-get": { 2383 + "version": "4.0.1", 2384 + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", 2385 + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", 2386 + "funding": [ 2387 + { 2388 + "type": "github", 2389 + "url": "https://github.com/sponsors/feross" 2390 + }, 2391 + { 2392 + "type": "patreon", 2393 + "url": "https://www.patreon.com/feross" 2394 + }, 2395 + { 2396 + "type": "consulting", 2397 + "url": "https://feross.org/support" 2398 + } 2399 + ], 2400 + "license": "MIT", 2401 + "dependencies": { 2402 + "decompress-response": "^6.0.0", 2403 + "once": "^1.3.1", 2404 + "simple-concat": "^1.0.0" 2405 + } 2406 + }, 2407 + "node_modules/sonic-boom": { 2408 + "version": "3.8.1", 2409 + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", 2410 + "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", 2411 + "license": "MIT", 2412 + "dependencies": { 2413 + "atomic-sleep": "^1.0.0" 2414 + } 2415 + }, 2416 + "node_modules/source-map-js": { 2417 + "version": "1.2.1", 2418 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 2419 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 2420 + "dev": true, 2421 + "license": "BSD-3-Clause", 2422 + "engines": { 2423 + "node": ">=0.10.0" 2424 + } 2425 + }, 2426 + "node_modules/split2": { 2427 + "version": "4.2.0", 2428 + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", 2429 + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", 2430 + "license": "ISC", 2431 + "engines": { 2432 + "node": ">= 10.x" 2433 + } 2434 + }, 2435 + "node_modules/stackback": { 2436 + "version": "0.0.2", 2437 + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", 2438 + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", 2439 + "dev": true, 2440 + "license": "MIT" 2441 + }, 2442 + "node_modules/std-env": { 2443 + "version": "3.10.0", 2444 + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", 2445 + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", 2446 + "dev": true, 2447 + "license": "MIT" 2448 + }, 2449 + "node_modules/string_decoder": { 2450 + "version": "1.3.0", 2451 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 2452 + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 2453 + "license": "MIT", 2454 + "dependencies": { 2455 + "safe-buffer": "~5.2.0" 2456 + } 2457 + }, 2458 + "node_modules/strip-json-comments": { 2459 + "version": "2.0.1", 2460 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 2461 + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", 2462 + "license": "MIT", 2463 + "engines": { 2464 + "node": ">=0.10.0" 2465 + } 2466 + }, 2467 + "node_modules/strip-literal": { 2468 + "version": "3.1.0", 2469 + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", 2470 + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", 2471 + "dev": true, 2472 + "license": "MIT", 2473 + "dependencies": { 2474 + "js-tokens": "^9.0.1" 2475 + }, 2476 + "funding": { 2477 + "url": "https://github.com/sponsors/antfu" 2478 + } 2479 + }, 2480 + "node_modules/tar-fs": { 2481 + "version": "2.1.4", 2482 + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", 2483 + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", 2484 + "license": "MIT", 2485 + "dependencies": { 2486 + "chownr": "^1.1.1", 2487 + "mkdirp-classic": "^0.5.2", 2488 + "pump": "^3.0.0", 2489 + "tar-stream": "^2.1.4" 2490 + } 2491 + }, 2492 + "node_modules/tar-stream": { 2493 + "version": "2.2.0", 2494 + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 2495 + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 2496 + "license": "MIT", 2497 + "dependencies": { 2498 + "bl": "^4.0.3", 2499 + "end-of-stream": "^1.4.1", 2500 + "fs-constants": "^1.0.0", 2501 + "inherits": "^2.0.3", 2502 + "readable-stream": "^3.1.1" 2503 + }, 2504 + "engines": { 2505 + "node": ">=6" 2506 + } 2507 + }, 2508 + "node_modules/tar-stream/node_modules/readable-stream": { 2509 + "version": "3.6.2", 2510 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 2511 + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 2512 + "license": "MIT", 2513 + "dependencies": { 2514 + "inherits": "^2.0.3", 2515 + "string_decoder": "^1.1.1", 2516 + "util-deprecate": "^1.0.1" 2517 + }, 2518 + "engines": { 2519 + "node": ">= 6" 2520 + } 2521 + }, 2522 + "node_modules/thread-stream": { 2523 + "version": "2.7.0", 2524 + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", 2525 + "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", 2526 + "license": "MIT", 2527 + "dependencies": { 2528 + "real-require": "^0.2.0" 2529 + } 2530 + }, 2531 + "node_modules/tinybench": { 2532 + "version": "2.9.0", 2533 + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", 2534 + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", 2535 + "dev": true, 2536 + "license": "MIT" 2537 + }, 2538 + "node_modules/tinyexec": { 2539 + "version": "0.3.2", 2540 + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", 2541 + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", 2542 + "dev": true, 2543 + "license": "MIT" 2544 + }, 2545 + "node_modules/tinyglobby": { 2546 + "version": "0.2.15", 2547 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", 2548 + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 2549 + "dev": true, 2550 + "license": "MIT", 2551 + "dependencies": { 2552 + "fdir": "^6.5.0", 2553 + "picomatch": "^4.0.3" 2554 + }, 2555 + "engines": { 2556 + "node": ">=12.0.0" 2557 + }, 2558 + "funding": { 2559 + "url": "https://github.com/sponsors/SuperchupuDev" 2560 + } 2561 + }, 2562 + "node_modules/tinypool": { 2563 + "version": "1.1.1", 2564 + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", 2565 + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", 2566 + "dev": true, 2567 + "license": "MIT", 2568 + "engines": { 2569 + "node": "^18.0.0 || >=20.0.0" 2570 + } 2571 + }, 2572 + "node_modules/tinyrainbow": { 2573 + "version": "2.0.0", 2574 + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", 2575 + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", 2576 + "dev": true, 2577 + "license": "MIT", 2578 + "engines": { 2579 + "node": ">=14.0.0" 2580 + } 2581 + }, 2582 + "node_modules/tinyspy": { 2583 + "version": "4.0.4", 2584 + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", 2585 + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", 2586 + "dev": true, 2587 + "license": "MIT", 2588 + "engines": { 2589 + "node": ">=14.0.0" 2590 + } 2591 + }, 2592 + "node_modules/tslib": { 2593 + "version": "2.8.1", 2594 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 2595 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 2596 + "license": "0BSD" 2597 + }, 2598 + "node_modules/tsx": { 2599 + "version": "4.21.0", 2600 + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", 2601 + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", 2602 + "dev": true, 2603 + "license": "MIT", 2604 + "dependencies": { 2605 + "esbuild": "~0.27.0", 2606 + "get-tsconfig": "^4.7.5" 2607 + }, 2608 + "bin": { 2609 + "tsx": "dist/cli.mjs" 2610 + }, 2611 + "engines": { 2612 + "node": ">=18.0.0" 2613 + }, 2614 + "optionalDependencies": { 2615 + "fsevents": "~2.3.3" 2616 + } 2617 + }, 2618 + "node_modules/tunnel-agent": { 2619 + "version": "0.6.0", 2620 + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 2621 + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", 2622 + "license": "Apache-2.0", 2623 + "dependencies": { 2624 + "safe-buffer": "^5.0.1" 2625 + }, 2626 + "engines": { 2627 + "node": "*" 2628 + } 2629 + }, 2630 + "node_modules/typescript": { 2631 + "version": "5.9.3", 2632 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 2633 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 2634 + "dev": true, 2635 + "license": "Apache-2.0", 2636 + "bin": { 2637 + "tsc": "bin/tsc", 2638 + "tsserver": "bin/tsserver" 2639 + }, 2640 + "engines": { 2641 + "node": ">=14.17" 2642 + } 2643 + }, 2644 + "node_modules/uint8arrays": { 2645 + "version": "3.0.0", 2646 + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", 2647 + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 2648 + "license": "MIT", 2649 + "dependencies": { 2650 + "multiformats": "^9.4.2" 2651 + } 2652 + }, 2653 + "node_modules/undici-types": { 2654 + "version": "7.16.0", 2655 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", 2656 + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", 2657 + "license": "MIT" 2658 + }, 2659 + "node_modules/unicode-segmenter": { 2660 + "version": "0.14.5", 2661 + "resolved": "https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.5.tgz", 2662 + "integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==", 2663 + "license": "MIT" 2664 + }, 2665 + "node_modules/util-deprecate": { 2666 + "version": "1.0.2", 2667 + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 2668 + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 2669 + "license": "MIT" 2670 + }, 2671 + "node_modules/varint": { 2672 + "version": "6.0.0", 2673 + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", 2674 + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", 2675 + "license": "MIT" 2676 + }, 2677 + "node_modules/vite": { 2678 + "version": "7.3.1", 2679 + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", 2680 + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", 2681 + "dev": true, 2682 + "license": "MIT", 2683 + "dependencies": { 2684 + "esbuild": "^0.27.0", 2685 + "fdir": "^6.5.0", 2686 + "picomatch": "^4.0.3", 2687 + "postcss": "^8.5.6", 2688 + "rollup": "^4.43.0", 2689 + "tinyglobby": "^0.2.15" 2690 + }, 2691 + "bin": { 2692 + "vite": "bin/vite.js" 2693 + }, 2694 + "engines": { 2695 + "node": "^20.19.0 || >=22.12.0" 2696 + }, 2697 + "funding": { 2698 + "url": "https://github.com/vitejs/vite?sponsor=1" 2699 + }, 2700 + "optionalDependencies": { 2701 + "fsevents": "~2.3.3" 2702 + }, 2703 + "peerDependencies": { 2704 + "@types/node": "^20.19.0 || >=22.12.0", 2705 + "jiti": ">=1.21.0", 2706 + "less": "^4.0.0", 2707 + "lightningcss": "^1.21.0", 2708 + "sass": "^1.70.0", 2709 + "sass-embedded": "^1.70.0", 2710 + "stylus": ">=0.54.8", 2711 + "sugarss": "^5.0.0", 2712 + "terser": "^5.16.0", 2713 + "tsx": "^4.8.1", 2714 + "yaml": "^2.4.2" 2715 + }, 2716 + "peerDependenciesMeta": { 2717 + "@types/node": { 2718 + "optional": true 2719 + }, 2720 + "jiti": { 2721 + "optional": true 2722 + }, 2723 + "less": { 2724 + "optional": true 2725 + }, 2726 + "lightningcss": { 2727 + "optional": true 2728 + }, 2729 + "sass": { 2730 + "optional": true 2731 + }, 2732 + "sass-embedded": { 2733 + "optional": true 2734 + }, 2735 + "stylus": { 2736 + "optional": true 2737 + }, 2738 + "sugarss": { 2739 + "optional": true 2740 + }, 2741 + "terser": { 2742 + "optional": true 2743 + }, 2744 + "tsx": { 2745 + "optional": true 2746 + }, 2747 + "yaml": { 2748 + "optional": true 2749 + } 2750 + } 2751 + }, 2752 + "node_modules/vite-node": { 2753 + "version": "3.2.4", 2754 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", 2755 + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", 2756 + "dev": true, 2757 + "license": "MIT", 2758 + "dependencies": { 2759 + "cac": "^6.7.14", 2760 + "debug": "^4.4.1", 2761 + "es-module-lexer": "^1.7.0", 2762 + "pathe": "^2.0.3", 2763 + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" 2764 + }, 2765 + "bin": { 2766 + "vite-node": "vite-node.mjs" 2767 + }, 2768 + "engines": { 2769 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 2770 + }, 2771 + "funding": { 2772 + "url": "https://opencollective.com/vitest" 2773 + } 2774 + }, 2775 + "node_modules/vitest": { 2776 + "version": "3.2.4", 2777 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", 2778 + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", 2779 + "dev": true, 2780 + "license": "MIT", 2781 + "dependencies": { 2782 + "@types/chai": "^5.2.2", 2783 + "@vitest/expect": "3.2.4", 2784 + "@vitest/mocker": "3.2.4", 2785 + "@vitest/pretty-format": "^3.2.4", 2786 + "@vitest/runner": "3.2.4", 2787 + "@vitest/snapshot": "3.2.4", 2788 + "@vitest/spy": "3.2.4", 2789 + "@vitest/utils": "3.2.4", 2790 + "chai": "^5.2.0", 2791 + "debug": "^4.4.1", 2792 + "expect-type": "^1.2.1", 2793 + "magic-string": "^0.30.17", 2794 + "pathe": "^2.0.3", 2795 + "picomatch": "^4.0.2", 2796 + "std-env": "^3.9.0", 2797 + "tinybench": "^2.9.0", 2798 + "tinyexec": "^0.3.2", 2799 + "tinyglobby": "^0.2.14", 2800 + "tinypool": "^1.1.1", 2801 + "tinyrainbow": "^2.0.0", 2802 + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", 2803 + "vite-node": "3.2.4", 2804 + "why-is-node-running": "^2.3.0" 2805 + }, 2806 + "bin": { 2807 + "vitest": "vitest.mjs" 2808 + }, 2809 + "engines": { 2810 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 2811 + }, 2812 + "funding": { 2813 + "url": "https://opencollective.com/vitest" 2814 + }, 2815 + "peerDependencies": { 2816 + "@edge-runtime/vm": "*", 2817 + "@types/debug": "^4.1.12", 2818 + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 2819 + "@vitest/browser": "3.2.4", 2820 + "@vitest/ui": "3.2.4", 2821 + "happy-dom": "*", 2822 + "jsdom": "*" 2823 + }, 2824 + "peerDependenciesMeta": { 2825 + "@edge-runtime/vm": { 2826 + "optional": true 2827 + }, 2828 + "@types/debug": { 2829 + "optional": true 2830 + }, 2831 + "@types/node": { 2832 + "optional": true 2833 + }, 2834 + "@vitest/browser": { 2835 + "optional": true 2836 + }, 2837 + "@vitest/ui": { 2838 + "optional": true 2839 + }, 2840 + "happy-dom": { 2841 + "optional": true 2842 + }, 2843 + "jsdom": { 2844 + "optional": true 2845 + } 2846 + } 2847 + }, 2848 + "node_modules/why-is-node-running": { 2849 + "version": "2.3.0", 2850 + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", 2851 + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", 2852 + "dev": true, 2853 + "license": "MIT", 2854 + "dependencies": { 2855 + "siginfo": "^2.0.0", 2856 + "stackback": "0.0.2" 2857 + }, 2858 + "bin": { 2859 + "why-is-node-running": "cli.js" 2860 + }, 2861 + "engines": { 2862 + "node": ">=8" 2863 + } 2864 + }, 2865 + "node_modules/wrappy": { 2866 + "version": "1.0.2", 2867 + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 2868 + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 2869 + "license": "ISC" 2870 + }, 2871 + "node_modules/ws": { 2872 + "version": "8.19.0", 2873 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", 2874 + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", 2875 + "license": "MIT", 2876 + "engines": { 2877 + "node": ">=10.0.0" 2878 + }, 2879 + "peerDependencies": { 2880 + "bufferutil": "^4.0.1", 2881 + "utf-8-validate": ">=5.0.2" 2882 + }, 2883 + "peerDependenciesMeta": { 2884 + "bufferutil": { 2885 + "optional": true 2886 + }, 2887 + "utf-8-validate": { 2888 + "optional": true 2889 + } 2890 + } 2891 + }, 2892 + "node_modules/zod": { 2893 + "version": "3.25.76", 2894 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 2895 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 2896 + "license": "MIT", 2897 + "funding": { 2898 + "url": "https://github.com/sponsors/colinhacks" 2899 + } 2900 + } 2901 + } 2902 + }
+45
package.json
··· 1 + { 2 + "name": "p2pds", 3 + "version": "0.1.0", 4 + "description": "AT Protocol Personal Data Server with P2P capabilities", 5 + "type": "module", 6 + "main": "dist/server.js", 7 + "scripts": { 8 + "dev": "tsx watch src/server.ts", 9 + "build": "tsc", 10 + "start": "node dist/server.js", 11 + "test": "vitest run" 12 + }, 13 + "dependencies": { 14 + "@atcute/atproto": "^3.1.10", 15 + "@atcute/bluesky": "^3.2.14", 16 + "@atcute/cbor": "^2.2.8", 17 + "@atcute/cid": "^2.3.0", 18 + "@atcute/client": "^4.2.0", 19 + "@atcute/identity": "^1.1.3", 20 + "@atcute/identity-resolver": "^1.2.2", 21 + "@atcute/lexicons": "^1.2.6", 22 + "@atcute/tid": "^1.1.1", 23 + "@atproto/crypto": "^0.4.5", 24 + "@atproto/lex-cbor": "^0.0.3", 25 + "@atproto/lex-data": "^0.0.3", 26 + "@atproto/lex-json": "^0.0.11", 27 + "@atproto/repo": "^0.8.12", 28 + "@hono/node-server": "^1.13.8", 29 + "bcryptjs": "^3.0.3", 30 + "better-sqlite3": "^11.8.1", 31 + "hono": "^4.11.3", 32 + "jose": "^6.1.3", 33 + "picocolors": "^1.1.1", 34 + "ws": "^8.18.3" 35 + }, 36 + "devDependencies": { 37 + "@types/bcryptjs": "^3.0.0", 38 + "@types/better-sqlite3": "^7.6.12", 39 + "@types/ws": "^8.18.1", 40 + "tsx": "^4.21.0", 41 + "typescript": "^5.9.3", 42 + "vitest": "^3.0.0" 43 + }, 44 + "license": "MIT" 45 + }
+107
src/blobs.ts
··· 1 + import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; 2 + import { join } from "node:path"; 3 + import { create as createCid, CODEC_RAW, toString as cidToString } from "@atcute/cid"; 4 + 5 + export interface BlobRef { 6 + $type: "blob"; 7 + ref: { $link: string }; 8 + mimeType: string; 9 + size: number; 10 + } 11 + 12 + export interface BlobResult { 13 + bytes: Uint8Array; 14 + mimeType: string; 15 + size: number; 16 + } 17 + 18 + /** 19 + * BlobStore manages blob storage on the local filesystem. 20 + * Blobs are stored with CID-based filenames under DATA_DIR/blobs/{did}/. 21 + */ 22 + export class BlobStore { 23 + private blobDir: string; 24 + 25 + constructor(dataDir: string, private did: string) { 26 + this.blobDir = join(dataDir, "blobs", did); 27 + mkdirSync(this.blobDir, { recursive: true }); 28 + } 29 + 30 + /** 31 + * Upload a blob and return a BlobRef. 32 + */ 33 + async putBlob(bytes: Uint8Array, mimeType: string): Promise<BlobRef> { 34 + const cidObj = await createCid(CODEC_RAW, bytes); 35 + const cidStr = cidToString(cidObj); 36 + 37 + const blobPath = join(this.blobDir, cidStr); 38 + const metaPath = join(this.blobDir, `${cidStr}.meta`); 39 + 40 + writeFileSync(blobPath, bytes); 41 + writeFileSync( 42 + metaPath, 43 + JSON.stringify({ mimeType, size: bytes.length }), 44 + ); 45 + 46 + return { 47 + $type: "blob", 48 + ref: { $link: cidStr }, 49 + mimeType, 50 + size: bytes.length, 51 + }; 52 + } 53 + 54 + /** 55 + * Retrieve a blob by CID string. 56 + */ 57 + getBlob(cid: string): BlobResult | null { 58 + const blobPath = join(this.blobDir, cid); 59 + const metaPath = join(this.blobDir, `${cid}.meta`); 60 + 61 + if (!existsSync(blobPath)) return null; 62 + 63 + const bytes = new Uint8Array(readFileSync(blobPath)); 64 + let mimeType = "application/octet-stream"; 65 + 66 + if (existsSync(metaPath)) { 67 + try { 68 + const meta = JSON.parse(readFileSync(metaPath, "utf-8")); 69 + mimeType = meta.mimeType || mimeType; 70 + } catch { 71 + // Ignore corrupt metadata 72 + } 73 + } 74 + 75 + return { bytes, mimeType, size: bytes.length }; 76 + } 77 + 78 + /** 79 + * Check if a blob exists. 80 + */ 81 + hasBlob(cid: string): boolean { 82 + return existsSync(join(this.blobDir, cid)); 83 + } 84 + 85 + /** 86 + * List all blob CIDs (for listBlobs endpoint). 87 + */ 88 + listBlobs(limit: number = 500, cursor?: string): { cids: string[]; cursor?: string } { 89 + const { readdirSync } = require("node:fs") as typeof import("node:fs"); 90 + const entries = readdirSync(this.blobDir) 91 + .filter((name: string) => !name.endsWith(".meta")) 92 + .sort(); 93 + 94 + let startIdx = 0; 95 + if (cursor) { 96 + const idx = entries.indexOf(cursor); 97 + startIdx = idx >= 0 ? idx + 1 : 0; 98 + } 99 + 100 + const slice = entries.slice(startIdx, startIdx + limit + 1); 101 + const hasMore = slice.length > limit; 102 + const cids = hasMore ? slice.slice(0, limit) : slice; 103 + const nextCursor = hasMore ? cids[cids.length - 1] : undefined; 104 + 105 + return { cids, cursor: nextCursor }; 106 + } 107 + }
+131
src/cbor-compat.ts
··· 1 + /** 2 + * CBOR compatibility layer for migrating from @atproto/lex-cbor to @atcute/cbor. 3 + * 4 + * @atcute/cbor uses lazy wrappers (BytesWrapper, CidLinkWrapper) that are 5 + * compatible with atproto's lex-json format. This layer handles conversion 6 + * of @atproto CID objects to CidLinkWrapper for encoding. 7 + * 8 + * Use toCidLink/fromCidLink to convert between lex-json and raw CID types. 9 + */ 10 + import { 11 + encode as atcuteEncode, 12 + decode as atcuteDecode, 13 + toCidLink, 14 + toBytes, 15 + fromBytes, 16 + isBytes, 17 + type CidLink, 18 + } from "@atcute/cbor"; 19 + import { fromString } from "@atcute/cid"; 20 + import type { CID } from "@atproto/lex-data"; 21 + 22 + /** 23 + * Check if a value is an @atproto CID object. 24 + */ 25 + function isAtprotoCid(value: unknown): value is CID { 26 + if (value === null || typeof value !== "object") { 27 + return false; 28 + } 29 + const obj = value as Record<string | symbol, unknown>; 30 + return "asCID" in obj && obj[Symbol.toStringTag] === "CID"; 31 + } 32 + 33 + /** 34 + * Convert @atproto CID to @atcute CidLink. 35 + */ 36 + function atprotoCidToCidLink(cid: CID): CidLink { 37 + return toCidLink(fromString(cid.toString())); 38 + } 39 + 40 + /** 41 + * Recursively convert @atproto CIDs to @atcute CidLinks for encoding. 42 + */ 43 + function convertCidsForEncode(value: unknown): unknown { 44 + if (value === null || value === undefined) { 45 + return value; 46 + } 47 + 48 + if (typeof value !== "object") { 49 + return value; 50 + } 51 + 52 + // Handle Uint8Array - wrap with toBytes() for @atcute/cbor 53 + if (value instanceof Uint8Array) { 54 + return toBytes(value); 55 + } 56 + 57 + // Convert @atproto CID to @atcute CidLink 58 + if (isAtprotoCid(value)) { 59 + return atprotoCidToCidLink(value); 60 + } 61 + 62 + // Handle arrays 63 + if (Array.isArray(value)) { 64 + return (value as unknown[]).map(convertCidsForEncode); 65 + } 66 + 67 + // Handle plain objects 68 + const proto = Object.getPrototypeOf(value); 69 + if (proto === Object.prototype || proto === null) { 70 + const result: Record<string, unknown> = {}; 71 + for (const [key, val] of Object.entries(value as Record<string, unknown>)) { 72 + result[key] = convertCidsForEncode(val); 73 + } 74 + return result; 75 + } 76 + 77 + return value; 78 + } 79 + 80 + /** 81 + * Encode a value to CBOR, automatically converting @atproto CIDs to CidLinks. 82 + */ 83 + export function encode(value: unknown): Uint8Array { 84 + const converted = convertCidsForEncode(value); 85 + return atcuteEncode(converted); 86 + } 87 + 88 + /** 89 + * Recursively convert @atcute wrappers back to raw types for decoding. 90 + */ 91 + function convertWrappersForDecode(value: unknown): unknown { 92 + if (value === null || value === undefined) { 93 + return value; 94 + } 95 + 96 + if (typeof value !== "object") { 97 + return value; 98 + } 99 + 100 + // Unwrap BytesWrapper to raw Uint8Array 101 + if (isBytes(value)) { 102 + return fromBytes(value); 103 + } 104 + 105 + // CidLinkWrapper is left as-is since it has $link getter for lex-json compat 106 + 107 + // Handle arrays 108 + if (Array.isArray(value)) { 109 + return (value as unknown[]).map(convertWrappersForDecode); 110 + } 111 + 112 + // Handle plain objects 113 + const proto = Object.getPrototypeOf(value); 114 + if (proto === Object.prototype || proto === null) { 115 + const result: Record<string, unknown> = {}; 116 + for (const [key, val] of Object.entries(value as Record<string, unknown>)) { 117 + result[key] = convertWrappersForDecode(val); 118 + } 119 + return result; 120 + } 121 + 122 + return value; 123 + } 124 + 125 + /** 126 + * Decode CBOR bytes. 127 + */ 128 + export function decode(bytes: Uint8Array): unknown { 129 + const decoded = atcuteDecode(bytes); 130 + return convertWrappersForDecode(decoded); 131 + }
+96
src/config.ts
··· 1 + import { readFileSync } from "node:fs"; 2 + import { resolve } from "node:path"; 3 + 4 + export interface Config { 5 + DID: string; 6 + HANDLE: string; 7 + PDS_HOSTNAME: string; 8 + AUTH_TOKEN: string; 9 + SIGNING_KEY: string; 10 + SIGNING_KEY_PUBLIC: string; 11 + JWT_SECRET: string; 12 + PASSWORD_HASH: string; 13 + EMAIL?: string; 14 + DATA_DIR: string; 15 + PORT: number; 16 + } 17 + 18 + const REQUIRED_KEYS = [ 19 + "DID", 20 + "HANDLE", 21 + "PDS_HOSTNAME", 22 + "AUTH_TOKEN", 23 + "SIGNING_KEY", 24 + "SIGNING_KEY_PUBLIC", 25 + "JWT_SECRET", 26 + "PASSWORD_HASH", 27 + ] as const; 28 + 29 + /** 30 + * Load a .env file into process.env (simple key=value parser). 31 + * Skips comments and empty lines. 32 + */ 33 + function loadDotEnv(path: string): void { 34 + let content: string; 35 + try { 36 + content = readFileSync(path, "utf-8"); 37 + } catch { 38 + return; // .env file is optional 39 + } 40 + 41 + for (const line of content.split("\n")) { 42 + const trimmed = line.trim(); 43 + if (!trimmed || trimmed.startsWith("#")) continue; 44 + const eqIdx = trimmed.indexOf("="); 45 + if (eqIdx === -1) continue; 46 + const key = trimmed.slice(0, eqIdx).trim(); 47 + let value = trimmed.slice(eqIdx + 1).trim(); 48 + // Strip surrounding quotes 49 + if ( 50 + (value.startsWith('"') && value.endsWith('"')) || 51 + (value.startsWith("'") && value.endsWith("'")) 52 + ) { 53 + value = value.slice(1, -1); 54 + } 55 + if (!process.env[key]) { 56 + process.env[key] = value; 57 + } 58 + } 59 + } 60 + 61 + /** 62 + * Load and validate configuration from environment variables. 63 + * Optionally loads a .env file first. 64 + */ 65 + export function loadConfig(envPath?: string): Config { 66 + // Load .env file if it exists 67 + const dotenvPath = envPath ?? resolve(process.cwd(), ".env"); 68 + loadDotEnv(dotenvPath); 69 + 70 + // Validate required variables 71 + const missing: string[] = []; 72 + for (const key of REQUIRED_KEYS) { 73 + if (!process.env[key]) { 74 + missing.push(key); 75 + } 76 + } 77 + if (missing.length > 0) { 78 + throw new Error( 79 + `Missing required environment variables: ${missing.join(", ")}`, 80 + ); 81 + } 82 + 83 + return { 84 + DID: process.env.DID!, 85 + HANDLE: process.env.HANDLE!, 86 + PDS_HOSTNAME: process.env.PDS_HOSTNAME!, 87 + AUTH_TOKEN: process.env.AUTH_TOKEN!, 88 + SIGNING_KEY: process.env.SIGNING_KEY!, 89 + SIGNING_KEY_PUBLIC: process.env.SIGNING_KEY_PUBLIC!, 90 + JWT_SECRET: process.env.JWT_SECRET!, 91 + PASSWORD_HASH: process.env.PASSWORD_HASH!, 92 + EMAIL: process.env.EMAIL, 93 + DATA_DIR: process.env.DATA_DIR ?? "./data", 94 + PORT: parseInt(process.env.PORT ?? "3000", 10), 95 + }; 96 + }
+89
src/did-cache.ts
··· 1 + /** 2 + * DID cache using in-memory Map with TTL. 3 + * Replaces Cloudflare Workers Cache API from Cirrus. 4 + */ 5 + 6 + import { defs, type DidDocument } from "@atcute/identity"; 7 + 8 + export interface CacheResult { 9 + did: string; 10 + doc: DidDocument; 11 + updatedAt: number; 12 + stale: boolean; 13 + expired: boolean; 14 + } 15 + 16 + export interface DidCache { 17 + cacheDid( 18 + did: string, 19 + doc: DidDocument, 20 + prevResult?: CacheResult, 21 + ): Promise<void>; 22 + checkCache(did: string): Promise<CacheResult | null>; 23 + refreshCache( 24 + did: string, 25 + getDoc: () => Promise<DidDocument | null>, 26 + prevResult?: CacheResult, 27 + ): Promise<void>; 28 + clearEntry(did: string): Promise<void>; 29 + clear(): Promise<void>; 30 + } 31 + 32 + const STALE_TTL = 60 * 60 * 1000; // 1 hour 33 + const MAX_TTL = 24 * 60 * 60 * 1000; // 24 hours 34 + 35 + interface CacheEntry { 36 + doc: DidDocument; 37 + cachedAt: number; 38 + } 39 + 40 + export class InMemoryDidCache implements DidCache { 41 + private cache = new Map<string, CacheEntry>(); 42 + 43 + async cacheDid(did: string, doc: DidDocument): Promise<void> { 44 + this.cache.set(did, { doc, cachedAt: Date.now() }); 45 + } 46 + 47 + async checkCache(did: string): Promise<CacheResult | null> { 48 + const entry = this.cache.get(did); 49 + if (!entry) return null; 50 + 51 + const now = Date.now(); 52 + const age = now - entry.cachedAt; 53 + 54 + // Validate document 55 + const parsed = defs.didDocument.try(entry.doc); 56 + if (!parsed.ok || parsed.value.id !== did) { 57 + this.cache.delete(did); 58 + return null; 59 + } 60 + 61 + return { 62 + did, 63 + doc: parsed.value, 64 + updatedAt: entry.cachedAt, 65 + stale: age > STALE_TTL, 66 + expired: age > MAX_TTL, 67 + }; 68 + } 69 + 70 + async refreshCache( 71 + did: string, 72 + getDoc: () => Promise<DidDocument | null>, 73 + ): Promise<void> { 74 + // Background refresh (fire and forget) 75 + getDoc().then((doc) => { 76 + if (doc) { 77 + this.cacheDid(did, doc); 78 + } 79 + }); 80 + } 81 + 82 + async clearEntry(did: string): Promise<void> { 83 + this.cache.delete(did); 84 + } 85 + 86 + async clear(): Promise<void> { 87 + this.cache.clear(); 88 + } 89 + }
+88
src/did-resolver.ts
··· 1 + /** 2 + * DID resolution for Node.js 3 + */ 4 + 5 + import { 6 + CompositeDidDocumentResolver, 7 + PlcDidDocumentResolver, 8 + WebDidDocumentResolver, 9 + } from "@atcute/identity-resolver"; 10 + import type { DidDocument } from "@atcute/identity"; 11 + import type { Did } from "@atcute/lexicons/syntax"; 12 + import type { DidCache } from "./did-cache.js"; 13 + 14 + const PLC_DIRECTORY = "https://plc.directory"; 15 + const TIMEOUT_MS = 3000; 16 + 17 + export interface DidResolverOpts { 18 + plcUrl?: string; 19 + timeout?: number; 20 + didCache?: DidCache; 21 + } 22 + 23 + export type { DidDocument }; 24 + 25 + export class DidResolver { 26 + private resolver: CompositeDidDocumentResolver<"plc" | "web">; 27 + private timeout: number; 28 + private cache?: DidCache; 29 + 30 + constructor(opts: DidResolverOpts = {}) { 31 + this.timeout = opts.timeout ?? TIMEOUT_MS; 32 + this.cache = opts.didCache; 33 + 34 + this.resolver = new CompositeDidDocumentResolver({ 35 + methods: { 36 + plc: new PlcDidDocumentResolver({ 37 + apiUrl: opts.plcUrl ?? PLC_DIRECTORY, 38 + }), 39 + web: new WebDidDocumentResolver(), 40 + }, 41 + }); 42 + } 43 + 44 + async resolve(did: string): Promise<DidDocument | null> { 45 + if (this.cache) { 46 + const cached = await this.cache.checkCache(did); 47 + if (cached && !cached.expired) { 48 + if (cached.stale) { 49 + this.cache.refreshCache( 50 + did, 51 + () => this.resolveNoCache(did), 52 + cached, 53 + ); 54 + } 55 + return cached.doc; 56 + } 57 + } 58 + 59 + const doc = await this.resolveNoCache(did); 60 + 61 + if (doc && this.cache) { 62 + await this.cache.cacheDid(did, doc); 63 + } else if (!doc && this.cache) { 64 + await this.cache.clearEntry(did); 65 + } 66 + 67 + return doc; 68 + } 69 + 70 + private async resolveNoCache(did: string): Promise<DidDocument | null> { 71 + const controller = new AbortController(); 72 + const timeoutId = setTimeout(() => controller.abort(), this.timeout); 73 + 74 + try { 75 + const doc = await this.resolver.resolve(did as Did<"plc" | "web">, { 76 + signal: controller.signal, 77 + }); 78 + if (doc.id !== did) { 79 + return null; 80 + } 81 + return doc; 82 + } catch { 83 + return null; 84 + } finally { 85 + clearTimeout(timeoutId); 86 + } 87 + } 88 + }
+141
src/firehose.ts
··· 1 + import { WebSocketServer, WebSocket } from "ws"; 2 + import type { IncomingMessage } from "node:http"; 3 + import { encode as cborEncode } from "./cbor-compat.js"; 4 + import type { RepoManager } from "./repo-manager.js"; 5 + import type { 6 + SeqEvent, 7 + SeqCommitEvent, 8 + SeqIdentityEvent, 9 + } from "./sequencer.js"; 10 + 11 + /** 12 + * Firehose manages WebSocket connections for com.atproto.sync.subscribeRepos. 13 + * Replaces Cloudflare's hibernatable WebSocket API with the `ws` library. 14 + */ 15 + export class Firehose { 16 + private clients = new Set<WebSocket>(); 17 + 18 + constructor(private repoManager: RepoManager) { 19 + // Register as the event handler on the repo manager 20 + this.repoManager.onFirehoseEvent = (event) => { 21 + this.broadcast(event); 22 + }; 23 + } 24 + 25 + /** 26 + * Get current subscriber count. 27 + */ 28 + get subscriberCount(): number { 29 + return this.clients.size; 30 + } 31 + 32 + /** 33 + * Handle a new WebSocket connection. 34 + */ 35 + async handleConnection(ws: WebSocket, req: IncomingMessage): Promise<void> { 36 + this.clients.add(ws); 37 + 38 + // Parse cursor from query string 39 + const url = new URL(req.url ?? "/", "http://localhost"); 40 + const cursorParam = url.searchParams.get("cursor"); 41 + const cursor = cursorParam ? parseInt(cursorParam, 10) : null; 42 + 43 + ws.on("close", () => { 44 + this.clients.delete(ws); 45 + }); 46 + 47 + ws.on("error", (err) => { 48 + console.error("WebSocket error:", err); 49 + this.clients.delete(ws); 50 + }); 51 + 52 + // Backfill if cursor provided 53 + if (cursor !== null) { 54 + await this.backfill(ws, cursor); 55 + } 56 + } 57 + 58 + /** 59 + * Broadcast a firehose event to all connected clients. 60 + */ 61 + private broadcast(event: SeqEvent): void { 62 + const frame = this.encodeEventFrame(event); 63 + 64 + for (const ws of this.clients) { 65 + if (ws.readyState === WebSocket.OPEN) { 66 + try { 67 + ws.send(frame); 68 + } catch (e) { 69 + console.error("Error broadcasting to WebSocket:", e); 70 + this.clients.delete(ws); 71 + } 72 + } 73 + } 74 + } 75 + 76 + /** 77 + * Backfill events from a cursor. 78 + */ 79 + private async backfill(ws: WebSocket, cursor: number): Promise<void> { 80 + const latestSeq = this.repoManager.sequencer.getLatestSeq(); 81 + 82 + if (cursor > latestSeq) { 83 + const frame = this.encodeErrorFrame( 84 + "FutureCursor", 85 + "Cursor is in the future", 86 + ); 87 + ws.send(frame); 88 + ws.close(1008, "FutureCursor"); 89 + return; 90 + } 91 + 92 + const events = await this.repoManager.sequencer.getEventsSince( 93 + cursor, 94 + 1000, 95 + ); 96 + 97 + for (const event of events) { 98 + if (ws.readyState !== WebSocket.OPEN) break; 99 + const frame = this.encodeEventFrame(event); 100 + ws.send(frame); 101 + } 102 + } 103 + 104 + // ============================================ 105 + // Frame Encoding 106 + // ============================================ 107 + 108 + private encodeFrame(header: object, body: object): Uint8Array { 109 + const headerBytes = cborEncode(header); 110 + const bodyBytes = cborEncode(body); 111 + 112 + const frame = new Uint8Array(headerBytes.length + bodyBytes.length); 113 + frame.set(headerBytes, 0); 114 + frame.set(bodyBytes, headerBytes.length); 115 + 116 + return frame; 117 + } 118 + 119 + private encodeCommitFrame(event: SeqCommitEvent): Uint8Array { 120 + const header = { op: 1, t: "#commit" }; 121 + return this.encodeFrame(header, event.event); 122 + } 123 + 124 + private encodeIdentityFrame(event: SeqIdentityEvent): Uint8Array { 125 + const header = { op: 1, t: "#identity" }; 126 + return this.encodeFrame(header, event.event); 127 + } 128 + 129 + private encodeEventFrame(event: SeqEvent): Uint8Array { 130 + if (event.type === "identity") { 131 + return this.encodeIdentityFrame(event); 132 + } 133 + return this.encodeCommitFrame(event); 134 + } 135 + 136 + private encodeErrorFrame(error: string, message: string): Uint8Array { 137 + const header = { op: -1 }; 138 + const body = { error, message }; 139 + return this.encodeFrame(header, body); 140 + } 141 + }
+85
src/format.ts
··· 1 + /** 2 + * Detect content type from file magic bytes. 3 + * Returns the detected MIME type or null if unknown. 4 + */ 5 + export function detectContentType(bytes: Uint8Array): string | null { 6 + // MP4/M4V/MOV - check for ftyp box 7 + if (bytes.length >= 12) { 8 + const ftyp = String.fromCharCode( 9 + bytes[4]!, 10 + bytes[5]!, 11 + bytes[6]!, 12 + bytes[7]!, 13 + ); 14 + if (ftyp === "ftyp") { 15 + const brand = String.fromCharCode( 16 + bytes[8]!, 17 + bytes[9]!, 18 + bytes[10]!, 19 + bytes[11]!, 20 + ); 21 + if ( 22 + brand === "isom" || 23 + brand === "iso2" || 24 + brand === "mp41" || 25 + brand === "mp42" || 26 + brand === "avc1" 27 + ) { 28 + return "video/mp4"; 29 + } 30 + if (brand === "M4V " || brand === "M4VH" || brand === "M4VP") { 31 + return "video/x-m4v"; 32 + } 33 + if (brand === "qt ") { 34 + return "video/quicktime"; 35 + } 36 + return "video/mp4"; 37 + } 38 + } 39 + 40 + // JPEG 41 + if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) { 42 + return "image/jpeg"; 43 + } 44 + 45 + // PNG 46 + if ( 47 + bytes[0] === 0x89 && 48 + bytes[1] === 0x50 && 49 + bytes[2] === 0x4e && 50 + bytes[3] === 0x47 51 + ) { 52 + return "image/png"; 53 + } 54 + 55 + // GIF 56 + if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) { 57 + return "image/gif"; 58 + } 59 + 60 + // WebP 61 + if ( 62 + bytes[0] === 0x52 && 63 + bytes[1] === 0x49 && 64 + bytes[2] === 0x46 && 65 + bytes[3] === 0x46 && 66 + bytes[8] === 0x57 && 67 + bytes[9] === 0x45 && 68 + bytes[10] === 0x42 && 69 + bytes[11] === 0x50 70 + ) { 71 + return "image/webp"; 72 + } 73 + 74 + // WebM 75 + if ( 76 + bytes[0] === 0x1a && 77 + bytes[1] === 0x45 && 78 + bytes[2] === 0xdf && 79 + bytes[3] === 0xa3 80 + ) { 81 + return "video/webm"; 82 + } 83 + 84 + return null; 85 + }
+315
src/index.ts
··· 1 + import { Hono } from "hono"; 2 + import { cors } from "hono/cors"; 3 + import { requireAuth } from "./middleware/auth.js"; 4 + import type { RepoManager } from "./repo-manager.js"; 5 + import type { Firehose } from "./firehose.js"; 6 + import type { Config } from "./config.js"; 7 + import * as sync from "./xrpc/sync.js"; 8 + import * as repo from "./xrpc/repo.js"; 9 + import * as server from "./xrpc/server.js"; 10 + 11 + const VERSION = "0.1.0"; 12 + 13 + /** 14 + * Create the Hono app with all routes. 15 + * The repoManager and firehose are passed in from the server entry point. 16 + */ 17 + export function createApp( 18 + config: Config, 19 + repoManager: RepoManager, 20 + firehose: Firehose, 21 + ) { 22 + const app = new Hono<{ Bindings: Config }>(); 23 + 24 + // Bind config to all requests 25 + app.use("*", async (c, next) => { 26 + // Set config as bindings 27 + Object.assign(c.env, config); 28 + await next(); 29 + }); 30 + 31 + // CORS middleware 32 + app.use( 33 + "*", 34 + cors({ 35 + origin: "*", 36 + allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], 37 + allowHeaders: ["*"], 38 + exposeHeaders: ["Content-Type"], 39 + maxAge: 86400, 40 + }), 41 + ); 42 + 43 + // DID document for did:web resolution 44 + app.get("/.well-known/did.json", (c) => { 45 + const didDocument = { 46 + "@context": [ 47 + "https://www.w3.org/ns/did/v1", 48 + "https://w3id.org/security/multikey/v1", 49 + "https://w3id.org/security/suites/secp256k1-2019/v1", 50 + ], 51 + id: config.DID, 52 + alsoKnownAs: [`at://${config.HANDLE}`], 53 + verificationMethod: [ 54 + { 55 + id: `${config.DID}#atproto`, 56 + type: "Multikey", 57 + controller: config.DID, 58 + publicKeyMultibase: config.SIGNING_KEY_PUBLIC, 59 + }, 60 + ], 61 + service: [ 62 + { 63 + id: "#atproto_pds", 64 + type: "AtprotoPersonalDataServer", 65 + serviceEndpoint: `https://${config.PDS_HOSTNAME}`, 66 + }, 67 + ], 68 + }; 69 + return c.json(didDocument); 70 + }); 71 + 72 + // Handle verification for AT Protocol 73 + app.get("/.well-known/atproto-did", (c) => { 74 + if (config.HANDLE !== config.PDS_HOSTNAME) { 75 + return c.notFound(); 76 + } 77 + return new Response(config.DID, { 78 + headers: { "Content-Type": "text/plain" }, 79 + }); 80 + }); 81 + 82 + // Health check 83 + app.get("/xrpc/_health", (c) => { 84 + try { 85 + repoManager.healthCheck(); 86 + return c.json({ status: "ok", version: VERSION }); 87 + } catch { 88 + return c.json({ status: "unhealthy", version: VERSION }, 503); 89 + } 90 + }); 91 + 92 + // Homepage 93 + app.get("/", (c) => { 94 + const html = `<!DOCTYPE html> 95 + <html lang="en"> 96 + <head> 97 + <meta charset="utf-8"> 98 + <meta name="viewport" content="width=device-width, initial-scale=1"> 99 + <title>P2PDS</title> 100 + <style> 101 + * { margin: 0; padding: 0; box-sizing: border-box; } 102 + body { 103 + min-height: 100vh; 104 + display: flex; 105 + flex-direction: column; 106 + justify-content: center; 107 + align-items: center; 108 + font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace; 109 + background: #f0f0f0; 110 + color: #000; 111 + padding: 2rem; 112 + } 113 + .name { font-size: clamp(1.5rem, 5vw, 3rem); font-weight: 700; letter-spacing: 0.2em; margin: 1rem 0; } 114 + .what { font-size: clamp(0.8rem, 2vw, 1rem); color: #666; max-width: 300px; text-align: center; } 115 + .handle { font-size: clamp(0.9rem, 2.5vw, 1.2rem); margin-top: 2rem; padding: 0.5rem 1rem; border: 2px solid #000; } 116 + .handle a { color: inherit; text-decoration: none; } 117 + .handle a:hover { text-decoration: underline; } 118 + .version { position: fixed; bottom: 1rem; right: 1rem; font-size: 0.7rem; color: #999; } 119 + </style> 120 + </head> 121 + <body> 122 + <div class="name">P2PDS</div> 123 + <div class="what">a personal data server for the atmosphere</div> 124 + <div class="handle"><a href="https://bsky.app/profile/${config.HANDLE}" target="_blank">@${config.HANDLE}</a></div> 125 + <div class="version">v${VERSION}</div> 126 + </body> 127 + </html>`; 128 + return c.html(html); 129 + }); 130 + 131 + // ============================================ 132 + // Sync endpoints (federation) 133 + // ============================================ 134 + app.get("/xrpc/com.atproto.sync.getRepo", (c) => 135 + sync.getRepo(c, repoManager), 136 + ); 137 + app.get("/xrpc/com.atproto.sync.getRepoStatus", (c) => 138 + sync.getRepoStatus(c, repoManager), 139 + ); 140 + app.get("/xrpc/com.atproto.sync.getBlocks", (c) => 141 + sync.getBlocks(c, repoManager), 142 + ); 143 + app.get("/xrpc/com.atproto.sync.getBlob", (c) => 144 + sync.getBlob(c, repoManager), 145 + ); 146 + app.get("/xrpc/com.atproto.sync.listRepos", (c) => 147 + sync.listRepos(c, repoManager), 148 + ); 149 + app.get("/xrpc/com.atproto.sync.listBlobs", (c) => 150 + sync.listBlobs(c, repoManager), 151 + ); 152 + app.get("/xrpc/com.atproto.sync.getRecord", (c) => 153 + sync.getRecord(c, repoManager), 154 + ); 155 + 156 + // WebSocket firehose - handled via ws library upgrade, not Hono 157 + // (see server.ts for WebSocket setup) 158 + 159 + // ============================================ 160 + // Repository operations 161 + // ============================================ 162 + app.use("/xrpc/com.atproto.repo.describeRepo", async (c, next) => { 163 + const requestedRepo = c.req.query("repo"); 164 + if (!requestedRepo || requestedRepo === config.DID) { 165 + return repo.describeRepo(c, repoManager); 166 + } 167 + await next(); 168 + }); 169 + 170 + app.use("/xrpc/com.atproto.repo.getRecord", async (c, next) => { 171 + const requestedRepo = c.req.query("repo"); 172 + if (!requestedRepo || requestedRepo === config.DID) { 173 + return repo.getRecord(c, repoManager); 174 + } 175 + await next(); 176 + }); 177 + 178 + app.use("/xrpc/com.atproto.repo.listRecords", async (c, next) => { 179 + const requestedRepo = c.req.query("repo"); 180 + if (!requestedRepo || requestedRepo === config.DID) { 181 + return repo.listRecords(c, repoManager); 182 + } 183 + await next(); 184 + }); 185 + 186 + // Write operations require authentication 187 + app.post("/xrpc/com.atproto.repo.createRecord", requireAuth, (c) => 188 + repo.createRecord(c, repoManager), 189 + ); 190 + app.post("/xrpc/com.atproto.repo.deleteRecord", requireAuth, (c) => 191 + repo.deleteRecord(c, repoManager), 192 + ); 193 + app.post("/xrpc/com.atproto.repo.uploadBlob", requireAuth, (c) => 194 + repo.uploadBlob(c, repoManager), 195 + ); 196 + app.post("/xrpc/com.atproto.repo.applyWrites", requireAuth, (c) => 197 + repo.applyWrites(c, repoManager), 198 + ); 199 + app.post("/xrpc/com.atproto.repo.putRecord", requireAuth, (c) => 200 + repo.putRecord(c, repoManager), 201 + ); 202 + app.post("/xrpc/com.atproto.repo.importRepo", requireAuth, (c) => 203 + repo.importRepo(c, repoManager), 204 + ); 205 + app.get("/xrpc/com.atproto.repo.listMissingBlobs", requireAuth, (c) => 206 + repo.listMissingBlobs(c, repoManager), 207 + ); 208 + 209 + // ============================================ 210 + // Server identity 211 + // ============================================ 212 + app.get("/xrpc/com.atproto.server.describeServer", server.describeServer); 213 + 214 + // Handle resolution 215 + app.get("/xrpc/com.atproto.identity.resolveHandle", (c) => { 216 + const handle = c.req.query("handle"); 217 + if (handle === config.HANDLE) { 218 + return c.json({ did: config.DID }); 219 + } 220 + return c.json( 221 + { error: "HandleNotFound", message: `Handle not found: ${handle}` }, 222 + 404, 223 + ); 224 + }); 225 + 226 + // ============================================ 227 + // Session management 228 + // ============================================ 229 + app.post("/xrpc/com.atproto.server.createSession", (c) => 230 + server.createSession(c, repoManager), 231 + ); 232 + app.post("/xrpc/com.atproto.server.refreshSession", (c) => 233 + server.refreshSession(c, repoManager), 234 + ); 235 + app.get("/xrpc/com.atproto.server.getSession", (c) => 236 + server.getSession(c, repoManager), 237 + ); 238 + app.post("/xrpc/com.atproto.server.deleteSession", server.deleteSession); 239 + 240 + // ============================================ 241 + // Account lifecycle 242 + // ============================================ 243 + app.get("/xrpc/com.atproto.server.checkAccountStatus", requireAuth, (c) => 244 + server.checkAccountStatus(c, repoManager), 245 + ); 246 + app.post("/xrpc/com.atproto.server.activateAccount", requireAuth, (c) => 247 + server.activateAccount(c, repoManager), 248 + ); 249 + app.post("/xrpc/com.atproto.server.deactivateAccount", requireAuth, (c) => 250 + server.deactivateAccount(c, repoManager), 251 + ); 252 + app.post("/xrpc/gg.mk.experimental.resetMigration", requireAuth, (c) => 253 + server.resetMigration(c, repoManager), 254 + ); 255 + app.post( 256 + "/xrpc/com.atproto.server.requestEmailUpdate", 257 + requireAuth, 258 + server.requestEmailUpdate, 259 + ); 260 + app.post( 261 + "/xrpc/com.atproto.server.requestEmailConfirmation", 262 + requireAuth, 263 + server.requestEmailConfirmation, 264 + ); 265 + app.post("/xrpc/com.atproto.server.updateEmail", requireAuth, (c) => 266 + server.updateEmail(c, repoManager), 267 + ); 268 + 269 + // Service auth 270 + app.get( 271 + "/xrpc/com.atproto.server.getServiceAuth", 272 + requireAuth, 273 + server.getServiceAuth, 274 + ); 275 + 276 + // ============================================ 277 + // Actor preferences 278 + // ============================================ 279 + app.get("/xrpc/app.bsky.actor.getPreferences", requireAuth, async (c) => { 280 + const result = await repoManager.getPreferences(); 281 + return c.json(result); 282 + }); 283 + app.post("/xrpc/app.bsky.actor.putPreferences", requireAuth, async (c) => { 284 + const body = await c.req.json<{ preferences: unknown[] }>(); 285 + await repoManager.putPreferences(body.preferences); 286 + return c.json({}); 287 + }); 288 + 289 + // ============================================ 290 + // Identity events 291 + // ============================================ 292 + app.post( 293 + "/xrpc/gg.mk.experimental.emitIdentityEvent", 294 + requireAuth, 295 + async (c) => { 296 + const result = await repoManager.emitIdentityEvent(config.HANDLE); 297 + return c.json(result); 298 + }, 299 + ); 300 + 301 + // ============================================ 302 + // Firehose status 303 + // ============================================ 304 + app.get( 305 + "/xrpc/gg.mk.experimental.getFirehoseStatus", 306 + requireAuth, 307 + (c) => { 308 + return c.json( 309 + repoManager.getFirehoseStatus(firehose.subscriberCount), 310 + ); 311 + }, 312 + ); 313 + 314 + return app; 315 + }
+107
src/middleware/auth.ts
··· 1 + import type { Context, Next } from "hono"; 2 + import { verifyServiceJwt } from "../service-auth.js"; 3 + import { verifyAccessToken, TokenExpiredError } from "../session.js"; 4 + import type { Config } from "../config.js"; 5 + 6 + export interface AuthInfo { 7 + did: string; 8 + scope: string; 9 + } 10 + 11 + export type AuthVariables = { 12 + auth: AuthInfo; 13 + }; 14 + 15 + export async function requireAuth( 16 + c: Context<{ Bindings: Config; Variables: AuthVariables }>, 17 + next: Next, 18 + ): Promise<Response | void> { 19 + const auth = c.req.header("Authorization"); 20 + 21 + if (!auth) { 22 + return c.json( 23 + { 24 + error: "AuthMissing", 25 + message: "Authorization header required", 26 + }, 27 + 401, 28 + ); 29 + } 30 + 31 + // Handle Bearer tokens (session JWTs, static token, service JWTs) 32 + if (!auth.startsWith("Bearer ")) { 33 + return c.json( 34 + { 35 + error: "AuthMissing", 36 + message: "Invalid authorization scheme", 37 + }, 38 + 401, 39 + ); 40 + } 41 + 42 + const token = auth.slice(7); 43 + 44 + // Try static token first 45 + if (token === c.env.AUTH_TOKEN) { 46 + c.set("auth", { did: c.env.DID, scope: "com.atproto.access" }); 47 + return next(); 48 + } 49 + 50 + const serviceDid = `did:web:${c.env.PDS_HOSTNAME}`; 51 + 52 + // Try session JWT verification (HS256, signed with JWT_SECRET) 53 + try { 54 + const payload = await verifyAccessToken( 55 + token, 56 + c.env.JWT_SECRET, 57 + serviceDid, 58 + ); 59 + 60 + if (payload.sub !== c.env.DID) { 61 + return c.json( 62 + { 63 + error: "AuthenticationRequired", 64 + message: "Invalid access token", 65 + }, 66 + 401, 67 + ); 68 + } 69 + 70 + c.set("auth", { did: payload.sub, scope: payload.scope as string }); 71 + return next(); 72 + } catch (err) { 73 + if (err instanceof TokenExpiredError) { 74 + return c.json( 75 + { 76 + error: "ExpiredToken", 77 + message: err.message, 78 + }, 79 + 400, 80 + ); 81 + } 82 + // Session JWT verification failed, try service JWT 83 + } 84 + 85 + // Try service JWT verification (ES256K, signed with our signing key) 86 + try { 87 + const payload = await verifyServiceJwt( 88 + token, 89 + c.env.SIGNING_KEY, 90 + serviceDid, 91 + c.env.DID, 92 + ); 93 + 94 + c.set("auth", { did: payload.iss, scope: payload.lxm || "atproto" }); 95 + return next(); 96 + } catch { 97 + // Service JWT verification also failed 98 + } 99 + 100 + return c.json( 101 + { 102 + error: "AuthenticationRequired", 103 + message: "Invalid authentication token", 104 + }, 105 + 401, 106 + ); 107 + }
+864
src/repo-manager.ts
··· 1 + import type Database from "better-sqlite3"; 2 + import { 3 + Repo, 4 + WriteOpAction, 5 + BlockMap, 6 + blocksToCarFile, 7 + readCarWithRoot, 8 + getRecords, 9 + type RecordCreateOp, 10 + type RecordUpdateOp, 11 + type RecordDeleteOp, 12 + type RecordWriteOp, 13 + } from "@atproto/repo"; 14 + type RepoRecord = Record<string, unknown>; 15 + import { Secp256k1Keypair } from "@atproto/crypto"; 16 + import { CID, asCid, isBlobRef } from "@atproto/lex-data"; 17 + import { now as tidNow } from "@atcute/tid"; 18 + import { encode as cborEncode } from "./cbor-compat.js"; 19 + import { SqliteRepoStorage } from "./storage.js"; 20 + import { 21 + Sequencer, 22 + type SeqEvent, 23 + type SeqCommitEvent, 24 + type SeqIdentityEvent, 25 + type CommitData, 26 + } from "./sequencer.js"; 27 + import { BlobStore, type BlobRef } from "./blobs.js"; 28 + import { jsonToLex, type JsonValue } from "@atproto/lex-json"; 29 + import type { Config } from "./config.js"; 30 + 31 + /** 32 + * RepoManager - manages a single user's AT Protocol repository. 33 + * 34 + * This is the Node.js equivalent of Cirrus's AccountDurableObject, 35 + * converted from a Cloudflare Durable Object to a plain class. 36 + */ 37 + export class RepoManager { 38 + storage: SqliteRepoStorage; 39 + private repo: Repo | null = null; 40 + private keypair: Secp256k1Keypair | null = null; 41 + sequencer: Sequencer; 42 + blobStore: BlobStore | null = null; 43 + private repoInitialized = false; 44 + 45 + /** Callback invoked when a firehose event is produced */ 46 + onFirehoseEvent?: (event: SeqEvent) => void; 47 + 48 + constructor( 49 + private db: Database.Database, 50 + private config: Config, 51 + ) { 52 + this.storage = new SqliteRepoStorage(db); 53 + this.sequencer = new Sequencer(db); 54 + } 55 + 56 + /** 57 + * Initialize storage schema and optionally the blob store. 58 + */ 59 + init(blobStore?: BlobStore): void { 60 + this.storage.initSchema(true); 61 + if (blobStore) { 62 + this.blobStore = blobStore; 63 + } 64 + } 65 + 66 + /** 67 + * Initialize the Repo instance. Called lazily on first repo access. 68 + */ 69 + private async ensureRepoInitialized(): Promise<void> { 70 + if (this.repoInitialized) return; 71 + 72 + this.keypair = await Secp256k1Keypair.import(this.config.SIGNING_KEY); 73 + 74 + const root = await this.storage.getRoot(); 75 + if (root) { 76 + this.repo = await Repo.load(this.storage, root); 77 + } else { 78 + this.repo = await Repo.create( 79 + this.storage, 80 + this.config.DID, 81 + this.keypair, 82 + ); 83 + } 84 + 85 + this.repoInitialized = true; 86 + } 87 + 88 + async getRepo(): Promise<Repo> { 89 + await this.ensureRepoInitialized(); 90 + return this.repo!; 91 + } 92 + 93 + async getKeypair(): Promise<Secp256k1Keypair> { 94 + await this.ensureRepoInitialized(); 95 + return this.keypair!; 96 + } 97 + 98 + async ensureActive(): Promise<void> { 99 + const isActive = await this.storage.getActive(); 100 + if (!isActive) { 101 + throw new Error( 102 + "AccountDeactivated: Account is deactivated. Call activateAccount to enable writes.", 103 + ); 104 + } 105 + } 106 + 107 + /** 108 + * Get new blocks for the current revision from the database. 109 + */ 110 + private getNewBlocksForRev(rev: string): BlockMap { 111 + const newBlocks = new BlockMap(); 112 + const rows = this.db 113 + .prepare("SELECT cid, bytes FROM blocks WHERE rev = ?") 114 + .all(rev) as Array<{ cid: string; bytes: Buffer }>; 115 + 116 + for (const row of rows) { 117 + const cid = CID.parse(row.cid); 118 + const bytes = new Uint8Array(row.bytes); 119 + newBlocks.set(cid, bytes); 120 + } 121 + return newBlocks; 122 + } 123 + 124 + /** 125 + * Sequence a commit and broadcast to firehose listeners. 126 + */ 127 + private async sequenceAndBroadcast( 128 + prevRev: string, 129 + ops: Array<RecordWriteOp & { cid?: CID | null }>, 130 + ): Promise<void> { 131 + const newBlocks = this.getNewBlocksForRev(this.repo!.commit.rev); 132 + 133 + const commitData: CommitData = { 134 + did: this.repo!.did, 135 + commit: this.repo!.cid, 136 + rev: this.repo!.commit.rev, 137 + since: prevRev, 138 + newBlocks, 139 + ops, 140 + }; 141 + 142 + const event = await this.sequencer.sequenceCommit(commitData); 143 + this.onFirehoseEvent?.(event); 144 + } 145 + 146 + // ============================================ 147 + // Repo Operations 148 + // ============================================ 149 + 150 + async describeRepo(): Promise<{ 151 + did: string; 152 + collections: string[]; 153 + cid: string; 154 + }> { 155 + const repo = await this.getRepo(); 156 + 157 + if (!this.storage.hasCollections() && (await this.storage.getRoot())) { 158 + const seen = new Set<string>(); 159 + for await (const record of repo.walkRecords()) { 160 + if (!seen.has(record.collection)) { 161 + seen.add(record.collection); 162 + this.storage.addCollection(record.collection); 163 + } 164 + } 165 + } 166 + 167 + return { 168 + did: repo.did, 169 + collections: this.storage.getCollections(), 170 + cid: repo.cid.toString(), 171 + }; 172 + } 173 + 174 + async getRecord( 175 + collection: string, 176 + rkey: string, 177 + ): Promise<{ cid: string; record: unknown } | null> { 178 + const repo = await this.getRepo(); 179 + 180 + const dataKey = `${collection}/${rkey}`; 181 + const recordCid = await repo.data.get(dataKey); 182 + if (!recordCid) return null; 183 + 184 + const record = await repo.getRecord(collection, rkey); 185 + if (!record) return null; 186 + 187 + return { 188 + cid: recordCid.toString(), 189 + record: serializeRecord(record), 190 + }; 191 + } 192 + 193 + async listRecords( 194 + collection: string, 195 + opts: { limit: number; cursor?: string; reverse?: boolean }, 196 + ): Promise<{ 197 + records: Array<{ uri: string; cid: string; value: unknown }>; 198 + cursor?: string; 199 + }> { 200 + const repo = await this.getRepo(); 201 + const records = []; 202 + const startFrom = opts.cursor || `${collection}/`; 203 + 204 + for await (const record of repo.walkRecords(startFrom)) { 205 + if (record.collection !== collection) { 206 + if (records.length > 0) break; 207 + continue; 208 + } 209 + 210 + records.push({ 211 + uri: `at://${repo.did}/${record.collection}/${record.rkey}`, 212 + cid: record.cid.toString(), 213 + value: serializeRecord(record.record), 214 + }); 215 + 216 + if (records.length >= opts.limit + 1) break; 217 + } 218 + 219 + if (opts.reverse) { 220 + records.reverse(); 221 + } 222 + 223 + const hasMore = records.length > opts.limit; 224 + const results = hasMore ? records.slice(0, opts.limit) : records; 225 + const cursor = hasMore 226 + ? `${collection}/${results[results.length - 1]?.uri.split("/").pop() ?? ""}` 227 + : undefined; 228 + 229 + return { records: results, cursor }; 230 + } 231 + 232 + async createRecord( 233 + collection: string, 234 + rkey: string | undefined, 235 + record: unknown, 236 + ): Promise<{ 237 + uri: string; 238 + cid: string; 239 + commit: { cid: string; rev: string }; 240 + }> { 241 + await this.ensureActive(); 242 + const repo = await this.getRepo(); 243 + const keypair = await this.getKeypair(); 244 + 245 + const actualRkey = rkey || tidNow(); 246 + const createOp: RecordCreateOp = { 247 + action: WriteOpAction.Create, 248 + collection, 249 + rkey: actualRkey, 250 + record: jsonToLex(record as JsonValue) as RepoRecord, 251 + }; 252 + 253 + const prevRev = repo.commit.rev; 254 + const updatedRepo = await repo.applyWrites([createOp], keypair); 255 + this.repo = updatedRepo; 256 + 257 + const dataKey = `${collection}/${actualRkey}`; 258 + const recordCid = await this.repo.data.get(dataKey); 259 + if (!recordCid) { 260 + throw new Error(`Failed to create record: ${collection}/${actualRkey}`); 261 + } 262 + 263 + this.storage.addCollection(collection); 264 + 265 + const opWithCid = { ...createOp, cid: recordCid }; 266 + await this.sequenceAndBroadcast(prevRev, [opWithCid]); 267 + 268 + return { 269 + uri: `at://${this.repo.did}/${collection}/${actualRkey}`, 270 + cid: recordCid.toString(), 271 + commit: { 272 + cid: this.repo.cid.toString(), 273 + rev: this.repo.commit.rev, 274 + }, 275 + }; 276 + } 277 + 278 + async deleteRecord( 279 + collection: string, 280 + rkey: string, 281 + ): Promise<{ commit: { cid: string; rev: string } } | null> { 282 + await this.ensureActive(); 283 + const repo = await this.getRepo(); 284 + const keypair = await this.getKeypair(); 285 + 286 + const existing = await repo.getRecord(collection, rkey); 287 + if (!existing) return null; 288 + 289 + const deleteOp: RecordDeleteOp = { 290 + action: WriteOpAction.Delete, 291 + collection, 292 + rkey, 293 + }; 294 + 295 + const prevRev = repo.commit.rev; 296 + const updatedRepo = await repo.applyWrites([deleteOp], keypair); 297 + this.repo = updatedRepo; 298 + 299 + await this.sequenceAndBroadcast(prevRev, [deleteOp]); 300 + 301 + return { 302 + commit: { 303 + cid: updatedRepo.cid.toString(), 304 + rev: updatedRepo.commit.rev, 305 + }, 306 + }; 307 + } 308 + 309 + async putRecord( 310 + collection: string, 311 + rkey: string, 312 + record: unknown, 313 + ): Promise<{ 314 + uri: string; 315 + cid: string; 316 + commit: { cid: string; rev: string }; 317 + validationStatus: string; 318 + }> { 319 + await this.ensureActive(); 320 + const repo = await this.getRepo(); 321 + const keypair = await this.getKeypair(); 322 + 323 + const existing = await repo.getRecord(collection, rkey); 324 + const isUpdate = existing !== null; 325 + 326 + const normalizedRecord = jsonToLex(record as JsonValue) as RepoRecord; 327 + const op: RecordWriteOp = isUpdate 328 + ? ({ 329 + action: WriteOpAction.Update, 330 + collection, 331 + rkey, 332 + record: normalizedRecord, 333 + } as RecordUpdateOp) 334 + : ({ 335 + action: WriteOpAction.Create, 336 + collection, 337 + rkey, 338 + record: normalizedRecord, 339 + } as RecordCreateOp); 340 + 341 + const prevRev = repo.commit.rev; 342 + const updatedRepo = await repo.applyWrites([op], keypair); 343 + this.repo = updatedRepo; 344 + 345 + const dataKey = `${collection}/${rkey}`; 346 + const recordCid = await this.repo.data.get(dataKey); 347 + if (!recordCid) { 348 + throw new Error(`Failed to put record: ${collection}/${rkey}`); 349 + } 350 + 351 + this.storage.addCollection(collection); 352 + 353 + const opWithCid = { ...op, cid: recordCid }; 354 + await this.sequenceAndBroadcast(prevRev, [opWithCid]); 355 + 356 + return { 357 + uri: `at://${this.repo.did}/${collection}/${rkey}`, 358 + cid: recordCid.toString(), 359 + commit: { 360 + cid: this.repo.cid.toString(), 361 + rev: this.repo.commit.rev, 362 + }, 363 + validationStatus: "valid", 364 + }; 365 + } 366 + 367 + async applyWrites( 368 + writes: Array<{ 369 + $type: string; 370 + collection: string; 371 + rkey?: string; 372 + value?: unknown; 373 + }>, 374 + ): Promise<{ 375 + commit: { cid: string; rev: string }; 376 + results: Array<{ 377 + $type: string; 378 + uri?: string; 379 + cid?: string; 380 + validationStatus?: string; 381 + }>; 382 + }> { 383 + await this.ensureActive(); 384 + const repo = await this.getRepo(); 385 + const keypair = await this.getKeypair(); 386 + 387 + const ops: RecordWriteOp[] = []; 388 + const results: Array<{ 389 + $type: string; 390 + collection: string; 391 + rkey: string; 392 + action: WriteOpAction; 393 + }> = []; 394 + 395 + for (const write of writes) { 396 + if (write.$type === "com.atproto.repo.applyWrites#create") { 397 + const rkey = write.rkey || tidNow(); 398 + const op: RecordCreateOp = { 399 + action: WriteOpAction.Create, 400 + collection: write.collection, 401 + rkey, 402 + record: jsonToLex(write.value as JsonValue) as RepoRecord, 403 + }; 404 + ops.push(op); 405 + results.push({ 406 + $type: "com.atproto.repo.applyWrites#createResult", 407 + collection: write.collection, 408 + rkey, 409 + action: WriteOpAction.Create, 410 + }); 411 + } else if (write.$type === "com.atproto.repo.applyWrites#update") { 412 + if (!write.rkey) throw new Error("Update requires rkey"); 413 + const op: RecordUpdateOp = { 414 + action: WriteOpAction.Update, 415 + collection: write.collection, 416 + rkey: write.rkey, 417 + record: jsonToLex(write.value as JsonValue) as RepoRecord, 418 + }; 419 + ops.push(op); 420 + results.push({ 421 + $type: "com.atproto.repo.applyWrites#updateResult", 422 + collection: write.collection, 423 + rkey: write.rkey, 424 + action: WriteOpAction.Update, 425 + }); 426 + } else if (write.$type === "com.atproto.repo.applyWrites#delete") { 427 + if (!write.rkey) throw new Error("Delete requires rkey"); 428 + const op: RecordDeleteOp = { 429 + action: WriteOpAction.Delete, 430 + collection: write.collection, 431 + rkey: write.rkey, 432 + }; 433 + ops.push(op); 434 + results.push({ 435 + $type: "com.atproto.repo.applyWrites#deleteResult", 436 + collection: write.collection, 437 + rkey: write.rkey, 438 + action: WriteOpAction.Delete, 439 + }); 440 + } else { 441 + throw new Error(`Unknown write type: ${write.$type}`); 442 + } 443 + } 444 + 445 + const prevRev = repo.commit.rev; 446 + const updatedRepo = await repo.applyWrites(ops, keypair); 447 + this.repo = updatedRepo; 448 + 449 + for (const op of ops) { 450 + if (op.action !== WriteOpAction.Delete) { 451 + this.storage.addCollection(op.collection); 452 + } 453 + } 454 + 455 + const finalResults: Array<{ 456 + $type: string; 457 + uri?: string; 458 + cid?: string; 459 + validationStatus?: string; 460 + }> = []; 461 + const opsWithCids: Array<RecordWriteOp & { cid?: CID | null }> = []; 462 + 463 + for (let i = 0; i < results.length; i++) { 464 + const result = results[i]!; 465 + const op = ops[i]!; 466 + 467 + if (result.action === WriteOpAction.Delete) { 468 + finalResults.push({ $type: result.$type }); 469 + opsWithCids.push(op); 470 + } else { 471 + const dataKey = `${result.collection}/${result.rkey}`; 472 + const recordCid = await this.repo.data.get(dataKey); 473 + finalResults.push({ 474 + $type: result.$type, 475 + uri: `at://${this.repo.did}/${result.collection}/${result.rkey}`, 476 + cid: recordCid?.toString(), 477 + validationStatus: "valid", 478 + }); 479 + opsWithCids.push({ ...op, cid: recordCid }); 480 + } 481 + } 482 + 483 + await this.sequenceAndBroadcast(prevRev, opsWithCids); 484 + 485 + return { 486 + commit: { 487 + cid: this.repo.cid.toString(), 488 + rev: this.repo.commit.rev, 489 + }, 490 + results: finalResults, 491 + }; 492 + } 493 + 494 + // ============================================ 495 + // Repo Export/Import 496 + // ============================================ 497 + 498 + async getRepoStatus(): Promise<{ 499 + did: string; 500 + head: string; 501 + rev: string; 502 + }> { 503 + const repo = await this.getRepo(); 504 + return { 505 + did: repo.did, 506 + head: repo.cid.toString(), 507 + rev: repo.commit.rev, 508 + }; 509 + } 510 + 511 + async getRepoCar(): Promise<Uint8Array> { 512 + const root = await this.storage.getRoot(); 513 + if (!root) throw new Error("No repository root found"); 514 + 515 + const rows = this.db 516 + .prepare("SELECT cid, bytes FROM blocks") 517 + .all() as Array<{ cid: string; bytes: Buffer }>; 518 + 519 + const blocks = new BlockMap(); 520 + for (const row of rows) { 521 + const cid = CID.parse(row.cid); 522 + const bytes = new Uint8Array(row.bytes); 523 + blocks.set(cid, bytes); 524 + } 525 + 526 + return blocksToCarFile(root, blocks); 527 + } 528 + 529 + async getBlocks(cids: string[]): Promise<Uint8Array> { 530 + const root = await this.storage.getRoot(); 531 + if (!root) throw new Error("No repository root found"); 532 + 533 + const blocks = new BlockMap(); 534 + for (const cidStr of cids) { 535 + const cid = CID.parse(cidStr); 536 + const bytes = await this.storage.getBytes(cid); 537 + if (bytes) { 538 + blocks.set(cid, bytes); 539 + } 540 + } 541 + 542 + return blocksToCarFile(root, blocks); 543 + } 544 + 545 + async getRecordProof( 546 + collection: string, 547 + rkey: string, 548 + ): Promise<Uint8Array> { 549 + const root = await this.storage.getRoot(); 550 + if (!root) throw new Error("No repository root found"); 551 + 552 + const carChunks: Uint8Array[] = []; 553 + for await (const chunk of getRecords(this.storage, root, [ 554 + { collection, rkey }, 555 + ])) { 556 + carChunks.push(chunk); 557 + } 558 + 559 + const totalLength = carChunks.reduce((acc, chunk) => acc + chunk.length, 0); 560 + const result = new Uint8Array(totalLength); 561 + let offset = 0; 562 + for (const chunk of carChunks) { 563 + result.set(chunk, offset); 564 + offset += chunk.length; 565 + } 566 + 567 + return result; 568 + } 569 + 570 + async importRepo(carBytes: Uint8Array): Promise<{ 571 + did: string; 572 + rev: string; 573 + cid: string; 574 + }> { 575 + const isActive = await this.storage.getActive(); 576 + const existingRoot = await this.storage.getRoot(); 577 + 578 + if (isActive && existingRoot) { 579 + throw new Error( 580 + "Repository already exists. Cannot import over existing repository.", 581 + ); 582 + } 583 + 584 + if (existingRoot) { 585 + await this.storage.destroy(); 586 + this.repo = null; 587 + this.repoInitialized = false; 588 + } 589 + 590 + const { root: rootCid, blocks } = await readCarWithRoot(carBytes); 591 + 592 + const importRev = tidNow(); 593 + await this.storage.putMany(blocks, importRev); 594 + 595 + this.keypair = await Secp256k1Keypair.import(this.config.SIGNING_KEY); 596 + this.repo = await Repo.load(this.storage, rootCid); 597 + 598 + await this.storage.updateRoot(rootCid, this.repo.commit.rev); 599 + 600 + if (this.repo.did !== this.config.DID) { 601 + await this.storage.destroy(); 602 + throw new Error( 603 + `DID mismatch: CAR file contains DID ${this.repo.did}, but expected ${this.config.DID}`, 604 + ); 605 + } 606 + 607 + this.repoInitialized = true; 608 + 609 + const seenCollections = new Set<string>(); 610 + for await (const record of this.repo.walkRecords()) { 611 + if (!seenCollections.has(record.collection)) { 612 + seenCollections.add(record.collection); 613 + this.storage.addCollection(record.collection); 614 + } 615 + const blobCids = extractBlobCids(record.record); 616 + if (blobCids.length > 0) { 617 + const uri = `at://${this.repo.did}/${record.collection}/${record.rkey}`; 618 + this.storage.addRecordBlobs(uri, blobCids); 619 + } 620 + } 621 + 622 + return { 623 + did: this.repo.did, 624 + rev: this.repo.commit.rev, 625 + cid: rootCid.toString(), 626 + }; 627 + } 628 + 629 + // ============================================ 630 + // Blob Operations 631 + // ============================================ 632 + 633 + async uploadBlob(bytes: Uint8Array, mimeType: string): Promise<BlobRef> { 634 + if (!this.blobStore) { 635 + throw new Error("Blob storage not configured"); 636 + } 637 + 638 + const MAX_BLOB_SIZE = 60 * 1024 * 1024; 639 + if (bytes.length > MAX_BLOB_SIZE) { 640 + throw new Error( 641 + `Blob too large: ${bytes.length} bytes (max ${MAX_BLOB_SIZE})`, 642 + ); 643 + } 644 + 645 + const blobRef = await this.blobStore.putBlob(bytes, mimeType); 646 + this.storage.trackImportedBlob(blobRef.ref.$link, bytes.length, mimeType); 647 + 648 + return blobRef; 649 + } 650 + 651 + // ============================================ 652 + // Preferences 653 + // ============================================ 654 + 655 + async getPreferences(): Promise<{ preferences: unknown[] }> { 656 + const preferences = await this.storage.getPreferences(); 657 + return { preferences }; 658 + } 659 + 660 + async putPreferences(preferences: unknown[]): Promise<void> { 661 + await this.storage.putPreferences(preferences); 662 + } 663 + 664 + // ============================================ 665 + // Account State 666 + // ============================================ 667 + 668 + async getEmail(): Promise<{ email: string | null }> { 669 + return { email: this.storage.getEmail() }; 670 + } 671 + 672 + async updateEmail(email: string): Promise<void> { 673 + this.storage.setEmail(email); 674 + } 675 + 676 + async getActive(): Promise<boolean> { 677 + return this.storage.getActive(); 678 + } 679 + 680 + async activateAccount(): Promise<void> { 681 + await this.storage.setActive(true); 682 + } 683 + 684 + async deactivateAccount(): Promise<void> { 685 + await this.storage.setActive(false); 686 + } 687 + 688 + // ============================================ 689 + // Migration Progress 690 + // ============================================ 691 + 692 + async countBlocks(): Promise<number> { 693 + return this.storage.countBlocks(); 694 + } 695 + 696 + async countRecords(): Promise<number> { 697 + const repo = await this.getRepo(); 698 + let count = 0; 699 + for await (const _record of repo.walkRecords()) { 700 + count++; 701 + } 702 + return count; 703 + } 704 + 705 + async countExpectedBlobs(): Promise<number> { 706 + return this.storage.countExpectedBlobs(); 707 + } 708 + 709 + async countImportedBlobs(): Promise<number> { 710 + return this.storage.countImportedBlobs(); 711 + } 712 + 713 + async listMissingBlobs( 714 + limit: number = 500, 715 + cursor?: string, 716 + ): Promise<{ 717 + blobs: Array<{ cid: string; recordUri: string }>; 718 + cursor?: string; 719 + }> { 720 + return this.storage.listMissingBlobs(limit, cursor); 721 + } 722 + 723 + async resetMigration(): Promise<{ 724 + blocksDeleted: number; 725 + blobsCleared: number; 726 + }> { 727 + const isActive = await this.storage.getActive(); 728 + if (isActive) { 729 + throw new Error( 730 + "AccountActive: Cannot reset migration on an active account. Deactivate first.", 731 + ); 732 + } 733 + 734 + const blocksDeleted = await this.storage.countBlocks(); 735 + const blobsCleared = this.storage.countImportedBlobs(); 736 + 737 + await this.storage.destroy(); 738 + this.storage.clearBlobTracking(); 739 + 740 + this.repo = null; 741 + this.repoInitialized = false; 742 + 743 + return { blocksDeleted, blobsCleared }; 744 + } 745 + 746 + // ============================================ 747 + // Identity Events 748 + // ============================================ 749 + 750 + async emitIdentityEvent(handle: string): Promise<{ seq: number }> { 751 + const time = new Date().toISOString(); 752 + 753 + const result = this.db 754 + .prepare( 755 + `INSERT INTO firehose_events (event_type, payload) 756 + VALUES ('identity', ?)`, 757 + ) 758 + .run(Buffer.alloc(0)); 759 + const seq = Number(result.lastInsertRowid); 760 + 761 + const event: SeqIdentityEvent = { 762 + seq, 763 + type: "identity", 764 + event: { 765 + seq, 766 + did: this.config.DID, 767 + time, 768 + handle, 769 + }, 770 + time, 771 + }; 772 + 773 + this.onFirehoseEvent?.(event); 774 + 775 + return { seq }; 776 + } 777 + 778 + // ============================================ 779 + // Health 780 + // ============================================ 781 + 782 + healthCheck(): { ok: true } { 783 + this.db.prepare("SELECT 1").get(); 784 + return { ok: true }; 785 + } 786 + 787 + getFirehoseStatus(subscriberCount: number): { 788 + subscribers: number; 789 + latestSeq: number | null; 790 + } { 791 + const seq = this.sequencer.getLatestSeq(); 792 + return { 793 + subscribers: subscriberCount, 794 + latestSeq: seq || null, 795 + }; 796 + } 797 + } 798 + 799 + // ============================================ 800 + // Utility Functions 801 + // ============================================ 802 + 803 + /** 804 + * Serialize a record for JSON by converting CID objects to { $link: "..." } format. 805 + */ 806 + function serializeRecord(obj: unknown): unknown { 807 + if (obj === null || obj === undefined) return obj; 808 + 809 + const cid = asCid(obj); 810 + if (cid) { 811 + return { $link: cid.toString() }; 812 + } 813 + 814 + if (obj instanceof Uint8Array) { 815 + let binary = ""; 816 + for (let i = 0; i < obj.length; i++) { 817 + binary += String.fromCharCode(obj[i]!); 818 + } 819 + return { $bytes: btoa(binary) }; 820 + } 821 + 822 + if (Array.isArray(obj)) { 823 + return obj.map(serializeRecord); 824 + } 825 + 826 + if (typeof obj === "object") { 827 + const result: Record<string, unknown> = {}; 828 + for (const [key, value] of Object.entries(obj)) { 829 + result[key] = serializeRecord(value); 830 + } 831 + return result; 832 + } 833 + 834 + return obj; 835 + } 836 + 837 + /** 838 + * Extract blob CIDs from a record by recursively searching for blob references. 839 + */ 840 + function extractBlobCids(obj: unknown): string[] { 841 + const cids: string[] = []; 842 + 843 + function walk(value: unknown): void { 844 + if (value === null || value === undefined) return; 845 + 846 + if (isBlobRef(value)) { 847 + cids.push(value.ref.toString()); 848 + return; 849 + } 850 + 851 + if (Array.isArray(value)) { 852 + for (const item of value) { 853 + walk(item); 854 + } 855 + } else if (typeof value === "object") { 856 + for (const key of Object.keys(value as Record<string, unknown>)) { 857 + walk((value as Record<string, unknown>)[key]); 858 + } 859 + } 860 + } 861 + 862 + walk(obj); 863 + return cids; 864 + }
+224
src/sequencer.ts
··· 1 + import type Database from "better-sqlite3"; 2 + import { encode as cborEncode, decode as cborDecode } from "./cbor-compat.js"; 3 + import { CID } from "@atproto/lex-data"; 4 + import { blocksToCarFile, type BlockMap } from "@atproto/repo"; 5 + import type { RecordWriteOp } from "@atproto/repo"; 6 + 7 + /** 8 + * Commit event payload for the firehose 9 + */ 10 + export interface CommitEvent { 11 + seq: number; 12 + rebase: boolean; 13 + tooBig: boolean; 14 + repo: string; 15 + commit: CID; 16 + rev: string; 17 + since: string | null; 18 + blocks: Uint8Array; 19 + ops: RepoOp[]; 20 + blobs: CID[]; 21 + time: string; 22 + } 23 + 24 + /** 25 + * Identity event payload for the firehose 26 + */ 27 + export interface IdentityEvent { 28 + seq: number; 29 + did: string; 30 + handle: string; 31 + time: string; 32 + } 33 + 34 + /** 35 + * Repository operation in a commit 36 + */ 37 + export interface RepoOp { 38 + action: "create" | "update" | "delete"; 39 + path: string; 40 + cid: CID | null; 41 + } 42 + 43 + /** 44 + * Sequenced commit event wrapper 45 + */ 46 + export interface SeqCommitEvent { 47 + seq: number; 48 + type: "commit"; 49 + event: CommitEvent; 50 + time: string; 51 + } 52 + 53 + /** 54 + * Sequenced identity event wrapper 55 + */ 56 + export interface SeqIdentityEvent { 57 + seq: number; 58 + type: "identity"; 59 + event: IdentityEvent; 60 + time: string; 61 + } 62 + 63 + /** 64 + * Sequenced event (commit or identity) 65 + */ 66 + export type SeqEvent = SeqCommitEvent | SeqIdentityEvent; 67 + 68 + /** 69 + * Data needed to sequence a commit 70 + */ 71 + export interface CommitData { 72 + did: string; 73 + commit: CID; 74 + rev: string; 75 + since: string | null; 76 + newBlocks: BlockMap; 77 + ops: Array<RecordWriteOp & { cid?: CID | null }>; 78 + } 79 + 80 + /** 81 + * Sequencer manages the firehose event log. 82 + * 83 + * Stores commit events in SQLite and provides methods for: 84 + * - Sequencing new commits 85 + * - Backfilling events from a cursor 86 + * - Getting the latest sequence number 87 + */ 88 + export class Sequencer { 89 + private _insertStmt?: Database.Statement; 90 + private _getEventsSinceStmt?: Database.Statement; 91 + private _getLatestSeqStmt?: Database.Statement; 92 + 93 + constructor(private db: Database.Database) {} 94 + 95 + /** Lazily prepare statements (tables must exist first via initSchema) */ 96 + private get insertStmt() { 97 + return (this._insertStmt ??= this.db.prepare( 98 + `INSERT INTO firehose_events (event_type, payload) 99 + VALUES ('commit', ?)`, 100 + )); 101 + } 102 + private get getEventsSinceStmt() { 103 + return (this._getEventsSinceStmt ??= this.db.prepare( 104 + `SELECT seq, event_type, payload, created_at 105 + FROM firehose_events 106 + WHERE seq > ? 107 + ORDER BY seq ASC 108 + LIMIT ?`, 109 + )); 110 + } 111 + private get getLatestSeqStmt() { 112 + return (this._getLatestSeqStmt ??= this.db.prepare( 113 + "SELECT MAX(seq) as seq FROM firehose_events", 114 + )); 115 + } 116 + 117 + /** 118 + * Add a commit to the firehose sequence. 119 + * Returns the complete sequenced event for broadcasting. 120 + */ 121 + async sequenceCommit(data: CommitData): Promise<SeqEvent> { 122 + // Create CAR slice with commit diff 123 + const carBytes = await blocksToCarFile(data.commit, data.newBlocks); 124 + const time = new Date().toISOString(); 125 + 126 + // Build event payload 127 + const eventPayload: Omit<CommitEvent, "seq"> = { 128 + repo: data.did, 129 + commit: data.commit, 130 + rev: data.rev, 131 + since: data.since, 132 + blocks: carBytes, 133 + ops: data.ops.map( 134 + (op): RepoOp => ({ 135 + action: op.action as "create" | "update" | "delete", 136 + path: `${op.collection}/${op.rkey}`, 137 + cid: ("cid" in op && op.cid ? op.cid : null) as CID | null, 138 + }), 139 + ), 140 + rebase: false, 141 + tooBig: carBytes.length > 1_000_000, 142 + blobs: [], 143 + time, 144 + }; 145 + 146 + // Store in SQLite 147 + const payload = cborEncode(eventPayload); 148 + const result = this.insertStmt.run(Buffer.from(payload)); 149 + const seq = Number(result.lastInsertRowid); 150 + 151 + return { 152 + seq, 153 + type: "commit", 154 + event: { 155 + ...eventPayload, 156 + seq, 157 + }, 158 + time, 159 + }; 160 + } 161 + 162 + /** 163 + * Get events from a cursor position. 164 + */ 165 + async getEventsSince(cursor: number, limit = 100): Promise<SeqEvent[]> { 166 + const rows = this.getEventsSinceStmt.all(cursor, limit) as Array<{ 167 + seq: number; 168 + event_type: string; 169 + payload: Buffer; 170 + created_at: string; 171 + }>; 172 + 173 + const events: SeqEvent[] = []; 174 + 175 + for (const row of rows) { 176 + const payload = new Uint8Array(row.payload); 177 + const seq = row.seq; 178 + const time = row.created_at; 179 + 180 + if (row.event_type === "identity") { 181 + if (payload.length === 0) continue; 182 + const decoded = cborDecode(payload) as Omit<IdentityEvent, "seq">; 183 + events.push({ 184 + seq, 185 + type: "identity", 186 + event: { ...decoded, seq }, 187 + time, 188 + }); 189 + } else { 190 + const decoded = cborDecode(payload) as Omit<CommitEvent, "seq">; 191 + events.push({ 192 + seq, 193 + type: "commit", 194 + event: { ...decoded, seq }, 195 + time, 196 + }); 197 + } 198 + } 199 + 200 + return events; 201 + } 202 + 203 + /** 204 + * Get the latest sequence number. 205 + */ 206 + getLatestSeq(): number { 207 + const result = this.getLatestSeqStmt.get() as { 208 + seq: number | null; 209 + }; 210 + return result?.seq ?? 0; 211 + } 212 + 213 + /** 214 + * Prune old events to keep the log from growing indefinitely. 215 + */ 216 + async pruneOldEvents(keepCount = 10000): Promise<void> { 217 + this.db 218 + .prepare( 219 + `DELETE FROM firehose_events 220 + WHERE seq < (SELECT MAX(seq) - ? FROM firehose_events)`, 221 + ) 222 + .run(keepCount); 223 + } 224 + }
+68
src/server.ts
··· 1 + import { getRequestListener } from "@hono/node-server"; 2 + import { createServer } from "node:http"; 3 + import { mkdirSync } from "node:fs"; 4 + import { resolve } from "node:path"; 5 + import Database from "better-sqlite3"; 6 + import { WebSocketServer } from "ws"; 7 + import pc from "picocolors"; 8 + 9 + import { loadConfig } from "./config.js"; 10 + import { RepoManager } from "./repo-manager.js"; 11 + import { BlobStore } from "./blobs.js"; 12 + import { Firehose } from "./firehose.js"; 13 + import { createApp } from "./index.js"; 14 + 15 + // Load configuration 16 + const config = loadConfig(); 17 + 18 + // Ensure data directory exists 19 + const dataDir = resolve(config.DATA_DIR); 20 + mkdirSync(dataDir, { recursive: true }); 21 + 22 + // Initialize SQLite database 23 + const dbPath = resolve(dataDir, "pds.db"); 24 + const db = new Database(dbPath); 25 + db.pragma("journal_mode = WAL"); 26 + db.pragma("synchronous = NORMAL"); 27 + 28 + // Initialize repo manager 29 + const repoManager = new RepoManager(db, config); 30 + const blobStore = new BlobStore(dataDir, config.DID); 31 + repoManager.init(blobStore); 32 + 33 + // Initialize firehose 34 + const firehose = new Firehose(repoManager); 35 + 36 + // Create Hono app 37 + const app = createApp(config, repoManager, firehose); 38 + 39 + // Create HTTP server using @hono/node-server's request listener 40 + const requestListener = getRequestListener(app.fetch); 41 + const httpServer = createServer(requestListener); 42 + 43 + // Set up WebSocket server for firehose 44 + const wss = new WebSocketServer({ noServer: true }); 45 + 46 + httpServer.on("upgrade", (request, socket, head) => { 47 + const url = new URL(request.url ?? "/", `http://localhost:${config.PORT}`); 48 + 49 + if (url.pathname === "/xrpc/com.atproto.sync.subscribeRepos") { 50 + wss.handleUpgrade(request, socket, head, (ws) => { 51 + firehose.handleConnection(ws, request); 52 + }); 53 + } else { 54 + socket.destroy(); 55 + } 56 + }); 57 + 58 + // Start server 59 + httpServer.listen(config.PORT, () => { 60 + console.log( 61 + pc.bold(`\nP2PDS running at `) + 62 + pc.cyan(`http://localhost:${config.PORT}`), 63 + ); 64 + console.log(pc.dim(` DID: ${config.DID}`)); 65 + console.log(pc.dim(` Handle: @${config.HANDLE}`)); 66 + console.log(pc.dim(` Data: ${dataDir}`)); 67 + console.log(); 68 + });
+134
src/service-auth.ts
··· 1 + import { Secp256k1Keypair, randomStr, verifySignature } from "@atproto/crypto"; 2 + 3 + const MINUTE = 60; 4 + const SERVICE_JWT_EXPIRY_SECONDS = 5 * MINUTE; 5 + 6 + let cachedKeypair: Secp256k1Keypair | null = null; 7 + let cachedSigningKey: string | null = null; 8 + 9 + export async function getSigningKeypair( 10 + signingKey: string, 11 + ): Promise<Secp256k1Keypair> { 12 + if (cachedKeypair && cachedSigningKey === signingKey) { 13 + return cachedKeypair; 14 + } 15 + cachedKeypair = await Secp256k1Keypair.import(signingKey); 16 + cachedSigningKey = signingKey; 17 + return cachedKeypair; 18 + } 19 + 20 + export interface ServiceJwtPayload { 21 + iss: string; 22 + aud: string; 23 + exp: number; 24 + iat?: number; 25 + lxm?: string; 26 + jti?: string; 27 + } 28 + 29 + type ServiceJwtParams = { 30 + iss: string; 31 + aud: string; 32 + lxm: string | null; 33 + keypair: Secp256k1Keypair; 34 + }; 35 + 36 + function jsonToB64Url(json: Record<string, unknown>): string { 37 + return Buffer.from(JSON.stringify(json)).toString("base64url"); 38 + } 39 + 40 + function noUndefinedVals<T extends Record<string, unknown>>( 41 + obj: T, 42 + ): Partial<T> { 43 + const result: Partial<T> = {}; 44 + for (const [key, val] of Object.entries(obj)) { 45 + if (val !== undefined) { 46 + result[key as keyof T] = val as T[keyof T]; 47 + } 48 + } 49 + return result; 50 + } 51 + 52 + export async function createServiceJwt( 53 + params: ServiceJwtParams, 54 + ): Promise<string> { 55 + const { iss, aud, keypair } = params; 56 + const iat = Math.floor(Date.now() / 1000); 57 + const exp = iat + SERVICE_JWT_EXPIRY_SECONDS; 58 + const lxm = params.lxm ?? undefined; 59 + const jti = randomStr(16, "hex"); 60 + 61 + const header = { 62 + typ: "JWT", 63 + alg: keypair.jwtAlg, 64 + }; 65 + 66 + const payload = noUndefinedVals({ 67 + iat, 68 + iss, 69 + aud, 70 + exp, 71 + lxm, 72 + jti, 73 + }); 74 + 75 + const toSignStr = `${jsonToB64Url(header)}.${jsonToB64Url(payload as Record<string, unknown>)}`; 76 + const toSign = Buffer.from(toSignStr, "utf8"); 77 + const sig = Buffer.from(await keypair.sign(toSign)); 78 + 79 + return `${toSignStr}.${sig.toString("base64url")}`; 80 + } 81 + 82 + export async function verifyServiceJwt( 83 + token: string, 84 + signingKey: string, 85 + expectedAudience: string, 86 + expectedIssuer: string, 87 + ): Promise<ServiceJwtPayload> { 88 + const parts = token.split("."); 89 + if (parts.length !== 3) { 90 + throw new Error("Invalid JWT format"); 91 + } 92 + 93 + const headerB64 = parts[0]!; 94 + const payloadB64 = parts[1]!; 95 + const signatureB64 = parts[2]!; 96 + 97 + const header = JSON.parse(Buffer.from(headerB64, "base64url").toString()); 98 + if (header.alg !== "ES256K") { 99 + throw new Error(`Unsupported algorithm: ${header.alg}`); 100 + } 101 + 102 + const payload: ServiceJwtPayload = JSON.parse( 103 + Buffer.from(payloadB64, "base64url").toString(), 104 + ); 105 + 106 + const now = Math.floor(Date.now() / 1000); 107 + if (payload.exp && payload.exp < now) { 108 + throw new Error("Token expired"); 109 + } 110 + 111 + if (payload.aud !== expectedAudience) { 112 + throw new Error(`Invalid audience: expected ${expectedAudience}`); 113 + } 114 + 115 + if (payload.iss !== expectedIssuer) { 116 + throw new Error(`Invalid issuer: expected ${expectedIssuer}`); 117 + } 118 + 119 + const keypair = await getSigningKeypair(signingKey); 120 + const msgBytes = new Uint8Array( 121 + Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), 122 + ); 123 + const sigBytes = new Uint8Array(Buffer.from(signatureB64, "base64url")); 124 + 125 + const isValid = await verifySignature(keypair.did(), msgBytes, sigBytes, { 126 + allowMalleableSig: true, 127 + }); 128 + 129 + if (!isValid) { 130 + throw new Error("Invalid signature"); 131 + } 132 + 133 + return payload; 134 + }
+124
src/session.ts
··· 1 + import { SignJWT, jwtVerify, errors, type JWTPayload } from "jose"; 2 + import { compare } from "bcryptjs"; 3 + 4 + export class TokenExpiredError extends Error { 5 + constructor(message = "Token has expired") { 6 + super(message); 7 + this.name = "TokenExpiredError"; 8 + } 9 + } 10 + 11 + const ACCESS_TOKEN_LIFETIME = "120m"; 12 + const REFRESH_TOKEN_LIFETIME = "90d"; 13 + 14 + function createSecretKey(secret: string): Uint8Array { 15 + return new TextEncoder().encode(secret); 16 + } 17 + 18 + export async function createAccessToken( 19 + jwtSecret: string, 20 + userDid: string, 21 + serviceDid: string, 22 + ): Promise<string> { 23 + const secret = createSecretKey(jwtSecret); 24 + 25 + return new SignJWT({ scope: "com.atproto.access" }) 26 + .setProtectedHeader({ alg: "HS256", typ: "at+jwt" }) 27 + .setIssuedAt() 28 + .setAudience(serviceDid) 29 + .setSubject(userDid) 30 + .setExpirationTime(ACCESS_TOKEN_LIFETIME) 31 + .sign(secret); 32 + } 33 + 34 + export async function createRefreshToken( 35 + jwtSecret: string, 36 + userDid: string, 37 + serviceDid: string, 38 + ): Promise<string> { 39 + const secret = createSecretKey(jwtSecret); 40 + const jti = crypto.randomUUID(); 41 + 42 + return new SignJWT({ scope: "com.atproto.refresh" }) 43 + .setProtectedHeader({ alg: "HS256", typ: "refresh+jwt" }) 44 + .setIssuedAt() 45 + .setAudience(serviceDid) 46 + .setSubject(userDid) 47 + .setJti(jti) 48 + .setExpirationTime(REFRESH_TOKEN_LIFETIME) 49 + .sign(secret); 50 + } 51 + 52 + export async function verifyAccessToken( 53 + token: string, 54 + jwtSecret: string, 55 + serviceDid: string, 56 + ): Promise<JWTPayload> { 57 + const secret = createSecretKey(jwtSecret); 58 + 59 + let payload: JWTPayload; 60 + let protectedHeader: { typ?: string }; 61 + 62 + try { 63 + const result = await jwtVerify(token, secret, { 64 + audience: serviceDid, 65 + }); 66 + payload = result.payload; 67 + protectedHeader = result.protectedHeader; 68 + } catch (err) { 69 + if (err instanceof errors.JWTExpired) { 70 + throw new TokenExpiredError(); 71 + } 72 + throw err; 73 + } 74 + 75 + if (protectedHeader.typ !== "at+jwt") { 76 + throw new Error("Invalid token type"); 77 + } 78 + 79 + if (payload.scope !== "com.atproto.access") { 80 + throw new Error("Invalid scope"); 81 + } 82 + 83 + return payload; 84 + } 85 + 86 + export async function verifyRefreshToken( 87 + token: string, 88 + jwtSecret: string, 89 + serviceDid: string, 90 + ): Promise<JWTPayload> { 91 + const secret = createSecretKey(jwtSecret); 92 + 93 + let payload: JWTPayload; 94 + let protectedHeader: { typ?: string }; 95 + 96 + try { 97 + const result = await jwtVerify(token, secret, { 98 + audience: serviceDid, 99 + }); 100 + payload = result.payload; 101 + protectedHeader = result.protectedHeader; 102 + } catch (err) { 103 + if (err instanceof errors.JWTExpired) { 104 + throw new TokenExpiredError(); 105 + } 106 + throw err; 107 + } 108 + 109 + if (protectedHeader.typ !== "refresh+jwt") { 110 + throw new Error("Invalid token type"); 111 + } 112 + 113 + if (payload.scope !== "com.atproto.refresh") { 114 + throw new Error("Invalid scope"); 115 + } 116 + 117 + if (!payload.jti) { 118 + throw new Error("Missing token ID"); 119 + } 120 + 121 + return payload; 122 + } 123 + 124 + export { compare as verifyPassword };
+415
src/storage.ts
··· 1 + import type Database from "better-sqlite3"; 2 + import { CID } from "@atproto/lex-data"; 3 + import { BlockMap, type CommitData } from "@atproto/repo"; 4 + import { ReadableBlockstore, type RepoStorage } from "@atproto/repo"; 5 + 6 + /** 7 + * SQLite-backed repository storage using better-sqlite3. 8 + * 9 + * Implements the RepoStorage interface from @atproto/repo, storing blocks 10 + * in a SQLite database on the local filesystem. 11 + */ 12 + export class SqliteRepoStorage 13 + extends ReadableBlockstore 14 + implements RepoStorage 15 + { 16 + constructor(private db: Database.Database) { 17 + super(); 18 + } 19 + 20 + /** 21 + * Initialize the database schema. Should be called once on startup. 22 + */ 23 + initSchema(initialActive: boolean = true): void { 24 + this.db.exec(` 25 + -- Block storage (MST nodes + record blocks) 26 + CREATE TABLE IF NOT EXISTS blocks ( 27 + cid TEXT PRIMARY KEY, 28 + bytes BLOB NOT NULL, 29 + rev TEXT NOT NULL 30 + ); 31 + 32 + CREATE INDEX IF NOT EXISTS idx_blocks_rev ON blocks(rev); 33 + 34 + -- Repo state (single row) 35 + CREATE TABLE IF NOT EXISTS repo_state ( 36 + id INTEGER PRIMARY KEY CHECK (id = 1), 37 + root_cid TEXT, 38 + rev TEXT, 39 + seq INTEGER NOT NULL DEFAULT 0, 40 + active INTEGER NOT NULL DEFAULT 1, 41 + email TEXT 42 + ); 43 + 44 + -- Initialize with empty state if not exists 45 + INSERT OR IGNORE INTO repo_state (id, root_cid, rev, seq, active) 46 + VALUES (1, NULL, NULL, 0, ${initialActive ? 1 : 0}); 47 + 48 + -- Firehose events (sequenced commit log) 49 + CREATE TABLE IF NOT EXISTS firehose_events ( 50 + seq INTEGER PRIMARY KEY AUTOINCREMENT, 51 + event_type TEXT NOT NULL, 52 + payload BLOB NOT NULL, 53 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 54 + ); 55 + 56 + CREATE INDEX IF NOT EXISTS idx_firehose_created_at ON firehose_events(created_at); 57 + 58 + -- User preferences (single row, stores JSON array) 59 + CREATE TABLE IF NOT EXISTS preferences ( 60 + id INTEGER PRIMARY KEY CHECK (id = 1), 61 + data TEXT NOT NULL DEFAULT '[]' 62 + ); 63 + 64 + -- Initialize with empty preferences array if not exists 65 + INSERT OR IGNORE INTO preferences (id, data) VALUES (1, '[]'); 66 + 67 + -- Track blob references in records (populated during importRepo) 68 + CREATE TABLE IF NOT EXISTS record_blob ( 69 + recordUri TEXT NOT NULL, 70 + blobCid TEXT NOT NULL, 71 + PRIMARY KEY (recordUri, blobCid) 72 + ); 73 + 74 + CREATE INDEX IF NOT EXISTS idx_record_blob_cid ON record_blob(blobCid); 75 + 76 + -- Track successfully imported blobs (populated during uploadBlob) 77 + CREATE TABLE IF NOT EXISTS imported_blobs ( 78 + cid TEXT PRIMARY KEY, 79 + size INTEGER NOT NULL, 80 + mimeType TEXT, 81 + createdAt TEXT NOT NULL DEFAULT (datetime('now')) 82 + ); 83 + 84 + -- Collection name cache (for describeRepo) 85 + CREATE TABLE IF NOT EXISTS collections ( 86 + collection TEXT PRIMARY KEY 87 + ); 88 + `); 89 + } 90 + 91 + async getRoot(): Promise<CID | null> { 92 + const row = this.db 93 + .prepare("SELECT root_cid FROM repo_state WHERE id = 1") 94 + .get() as { root_cid: string | null } | undefined; 95 + if (!row || !row.root_cid) { 96 + return null; 97 + } 98 + return CID.parse(row.root_cid); 99 + } 100 + 101 + async getRev(): Promise<string | null> { 102 + const row = this.db 103 + .prepare("SELECT rev FROM repo_state WHERE id = 1") 104 + .get() as { rev: string | null } | undefined; 105 + return row?.rev ?? null; 106 + } 107 + 108 + async getSeq(): Promise<number> { 109 + const row = this.db 110 + .prepare("SELECT seq FROM repo_state WHERE id = 1") 111 + .get() as { seq: number } | undefined; 112 + return row?.seq ?? 0; 113 + } 114 + 115 + async nextSeq(): Promise<number> { 116 + this.db 117 + .prepare("UPDATE repo_state SET seq = seq + 1 WHERE id = 1") 118 + .run(); 119 + return this.getSeq(); 120 + } 121 + 122 + async getBytes(cid: CID): Promise<Uint8Array | null> { 123 + const row = this.db 124 + .prepare("SELECT bytes FROM blocks WHERE cid = ?") 125 + .get(cid.toString()) as { bytes: Buffer } | undefined; 126 + if (!row || !row.bytes) { 127 + return null; 128 + } 129 + return new Uint8Array(row.bytes); 130 + } 131 + 132 + async has(cid: CID): Promise<boolean> { 133 + const row = this.db 134 + .prepare("SELECT 1 FROM blocks WHERE cid = ? LIMIT 1") 135 + .get(cid.toString()); 136 + return row !== undefined; 137 + } 138 + 139 + async getBlocks(cids: CID[]): Promise<{ blocks: BlockMap; missing: CID[] }> { 140 + const blocks = new BlockMap(); 141 + const missing: CID[] = []; 142 + 143 + for (const cid of cids) { 144 + const bytes = await this.getBytes(cid); 145 + if (bytes) { 146 + blocks.set(cid, bytes); 147 + } else { 148 + missing.push(cid); 149 + } 150 + } 151 + 152 + return { blocks, missing }; 153 + } 154 + 155 + async putBlock(cid: CID, block: Uint8Array, rev: string): Promise<void> { 156 + this.db 157 + .prepare( 158 + "INSERT OR REPLACE INTO blocks (cid, bytes, rev) VALUES (?, ?, ?)", 159 + ) 160 + .run(cid.toString(), Buffer.from(block), rev); 161 + } 162 + 163 + async putMany(blocks: BlockMap, rev: string): Promise<void> { 164 + const stmt = this.db.prepare( 165 + "INSERT OR REPLACE INTO blocks (cid, bytes, rev) VALUES (?, ?, ?)", 166 + ); 167 + const internalMap = (blocks as unknown as { map: Map<string, Uint8Array> }) 168 + .map; 169 + if (internalMap) { 170 + const insertMany = this.db.transaction(() => { 171 + for (const [cidStr, bytes] of internalMap) { 172 + stmt.run(cidStr, Buffer.from(bytes), rev); 173 + } 174 + }); 175 + insertMany(); 176 + } 177 + } 178 + 179 + async updateRoot(cid: CID, rev: string): Promise<void> { 180 + this.db 181 + .prepare("UPDATE repo_state SET root_cid = ?, rev = ? WHERE id = 1") 182 + .run(cid.toString(), rev); 183 + } 184 + 185 + async applyCommit(commit: CommitData): Promise<void> { 186 + const applyTransaction = this.db.transaction(() => { 187 + // Add new blocks 188 + const insertStmt = this.db.prepare( 189 + "INSERT OR REPLACE INTO blocks (cid, bytes, rev) VALUES (?, ?, ?)", 190 + ); 191 + const internalMap = ( 192 + commit.newBlocks as unknown as { map: Map<string, Uint8Array> } 193 + ).map; 194 + if (internalMap) { 195 + for (const [cidStr, bytes] of internalMap) { 196 + insertStmt.run(cidStr, Buffer.from(bytes), commit.rev); 197 + } 198 + } 199 + 200 + // Remove old blocks 201 + const deleteStmt = this.db.prepare( 202 + "DELETE FROM blocks WHERE cid = ?", 203 + ); 204 + const removedSet = ( 205 + commit.removedCids as unknown as { set: Set<string> } 206 + ).set; 207 + if (removedSet) { 208 + for (const cidStr of removedSet) { 209 + deleteStmt.run(cidStr); 210 + } 211 + } 212 + 213 + // Update root 214 + this.db 215 + .prepare( 216 + "UPDATE repo_state SET root_cid = ?, rev = ? WHERE id = 1", 217 + ) 218 + .run(commit.cid.toString(), commit.rev); 219 + }); 220 + applyTransaction(); 221 + } 222 + 223 + async sizeInBytes(): Promise<number> { 224 + const row = this.db 225 + .prepare("SELECT SUM(LENGTH(bytes)) as total FROM blocks") 226 + .get() as { total: number | null } | undefined; 227 + return row?.total ?? 0; 228 + } 229 + 230 + async destroy(): Promise<void> { 231 + this.db.exec("DELETE FROM blocks"); 232 + this.db 233 + .prepare( 234 + "UPDATE repo_state SET root_cid = NULL, rev = NULL WHERE id = 1", 235 + ) 236 + .run(); 237 + } 238 + 239 + async countBlocks(): Promise<number> { 240 + const row = this.db 241 + .prepare("SELECT COUNT(*) as count FROM blocks") 242 + .get() as { count: number }; 243 + return row.count; 244 + } 245 + 246 + async getPreferences(): Promise<unknown[]> { 247 + const row = this.db 248 + .prepare("SELECT data FROM preferences WHERE id = 1") 249 + .get() as { data: string } | undefined; 250 + if (!row?.data) return []; 251 + try { 252 + return JSON.parse(row.data); 253 + } catch { 254 + return []; 255 + } 256 + } 257 + 258 + async putPreferences(preferences: unknown[]): Promise<void> { 259 + this.db 260 + .prepare("UPDATE preferences SET data = ? WHERE id = 1") 261 + .run(JSON.stringify(preferences)); 262 + } 263 + 264 + async getActive(): Promise<boolean> { 265 + const row = this.db 266 + .prepare("SELECT active FROM repo_state WHERE id = 1") 267 + .get() as { active: number } | undefined; 268 + return row ? row.active === 1 : true; 269 + } 270 + 271 + async setActive(active: boolean): Promise<void> { 272 + this.db 273 + .prepare("UPDATE repo_state SET active = ? WHERE id = 1") 274 + .run(active ? 1 : 0); 275 + } 276 + 277 + getEmail(): string | null { 278 + const row = this.db 279 + .prepare("SELECT email FROM repo_state WHERE id = 1") 280 + .get() as { email: string | null } | undefined; 281 + return row?.email ?? null; 282 + } 283 + 284 + setEmail(email: string): void { 285 + this.db 286 + .prepare("UPDATE repo_state SET email = ? WHERE id = 1") 287 + .run(email); 288 + } 289 + 290 + // ============================================ 291 + // Collection Cache Methods 292 + // ============================================ 293 + 294 + getCollections(): string[] { 295 + const rows = this.db 296 + .prepare("SELECT collection FROM collections ORDER BY collection") 297 + .all() as Array<{ collection: string }>; 298 + return rows.map((row) => row.collection); 299 + } 300 + 301 + addCollection(collection: string): void { 302 + this.db 303 + .prepare("INSERT OR IGNORE INTO collections (collection) VALUES (?)") 304 + .run(collection); 305 + } 306 + 307 + hasCollections(): boolean { 308 + const row = this.db 309 + .prepare("SELECT 1 FROM collections LIMIT 1") 310 + .get(); 311 + return row !== undefined; 312 + } 313 + 314 + // ============================================ 315 + // Blob Tracking Methods 316 + // ============================================ 317 + 318 + addRecordBlob(recordUri: string, blobCid: string): void { 319 + this.db 320 + .prepare( 321 + "INSERT OR IGNORE INTO record_blob (recordUri, blobCid) VALUES (?, ?)", 322 + ) 323 + .run(recordUri, blobCid); 324 + } 325 + 326 + addRecordBlobs(recordUri: string, blobCids: string[]): void { 327 + for (const cid of blobCids) { 328 + this.addRecordBlob(recordUri, cid); 329 + } 330 + } 331 + 332 + removeRecordBlobs(recordUri: string): void { 333 + this.db 334 + .prepare("DELETE FROM record_blob WHERE recordUri = ?") 335 + .run(recordUri); 336 + } 337 + 338 + trackImportedBlob(cid: string, size: number, mimeType: string): void { 339 + this.db 340 + .prepare( 341 + "INSERT OR REPLACE INTO imported_blobs (cid, size, mimeType) VALUES (?, ?, ?)", 342 + ) 343 + .run(cid, size, mimeType); 344 + } 345 + 346 + isBlobImported(cid: string): boolean { 347 + const row = this.db 348 + .prepare("SELECT 1 FROM imported_blobs WHERE cid = ? LIMIT 1") 349 + .get(cid); 350 + return row !== undefined; 351 + } 352 + 353 + countExpectedBlobs(): number { 354 + const row = this.db 355 + .prepare( 356 + "SELECT COUNT(DISTINCT blobCid) as count FROM record_blob", 357 + ) 358 + .get() as { count: number }; 359 + return row.count; 360 + } 361 + 362 + countImportedBlobs(): number { 363 + const row = this.db 364 + .prepare("SELECT COUNT(*) as count FROM imported_blobs") 365 + .get() as { count: number }; 366 + return row.count; 367 + } 368 + 369 + listMissingBlobs( 370 + limit: number = 500, 371 + cursor?: string, 372 + ): { blobs: Array<{ cid: string; recordUri: string }>; cursor?: string } { 373 + const blobs: Array<{ cid: string; recordUri: string }> = []; 374 + 375 + const rows = cursor 376 + ? (this.db 377 + .prepare( 378 + `SELECT rb.blobCid, rb.recordUri FROM record_blob rb 379 + LEFT JOIN imported_blobs ib ON rb.blobCid = ib.cid 380 + WHERE ib.cid IS NULL AND rb.blobCid > ? 381 + ORDER BY rb.blobCid 382 + LIMIT ?`, 383 + ) 384 + .all(cursor, limit + 1) as Array<{ 385 + blobCid: string; 386 + recordUri: string; 387 + }>) 388 + : (this.db 389 + .prepare( 390 + `SELECT rb.blobCid, rb.recordUri FROM record_blob rb 391 + LEFT JOIN imported_blobs ib ON rb.blobCid = ib.cid 392 + WHERE ib.cid IS NULL 393 + ORDER BY rb.blobCid 394 + LIMIT ?`, 395 + ) 396 + .all(limit + 1) as Array<{ 397 + blobCid: string; 398 + recordUri: string; 399 + }>); 400 + 401 + for (const row of rows.slice(0, limit)) { 402 + blobs.push({ cid: row.blobCid, recordUri: row.recordUri }); 403 + } 404 + 405 + const hasMore = rows.length > limit; 406 + const nextCursor = hasMore ? blobs[blobs.length - 1]?.cid : undefined; 407 + 408 + return { blobs, cursor: nextCursor }; 409 + } 410 + 411 + clearBlobTracking(): void { 412 + this.db.exec("DELETE FROM record_blob"); 413 + this.db.exec("DELETE FROM imported_blobs"); 414 + } 415 + }
+17
src/types.ts
··· 1 + import type { AuthVariables } from "./middleware/auth.js"; 2 + import type { Config } from "./config.js"; 3 + 4 + /** 5 + * Base app environment with config bindings. 6 + */ 7 + export type AppEnv = { 8 + Bindings: Config; 9 + }; 10 + 11 + /** 12 + * App environment with auth variables. 13 + */ 14 + export type AuthedAppEnv = { 15 + Bindings: Config; 16 + Variables: AuthVariables; 17 + };
+86
src/validation.ts
··· 1 + import { parse, ValidationError, type BaseSchema } from "@atcute/lexicons/validations"; 2 + 3 + import { 4 + AppBskyActorProfile, 5 + AppBskyFeedGenerator, 6 + AppBskyFeedLike, 7 + AppBskyFeedPost, 8 + AppBskyFeedPostgate, 9 + AppBskyFeedRepost, 10 + AppBskyFeedThreadgate, 11 + AppBskyGraphBlock, 12 + AppBskyGraphFollow, 13 + AppBskyGraphList, 14 + AppBskyGraphListblock, 15 + AppBskyGraphListitem, 16 + AppBskyGraphStarterpack, 17 + AppBskyGraphVerification, 18 + AppBskyLabelerService, 19 + } from "@atcute/bluesky"; 20 + 21 + /** 22 + * Map of collection NSID to validation schema. 23 + */ 24 + const recordSchemas: Record<string, BaseSchema> = { 25 + "app.bsky.actor.profile": AppBskyActorProfile.mainSchema, 26 + "app.bsky.feed.generator": AppBskyFeedGenerator.mainSchema, 27 + "app.bsky.feed.like": AppBskyFeedLike.mainSchema, 28 + "app.bsky.feed.post": AppBskyFeedPost.mainSchema, 29 + "app.bsky.feed.postgate": AppBskyFeedPostgate.mainSchema, 30 + "app.bsky.feed.repost": AppBskyFeedRepost.mainSchema, 31 + "app.bsky.feed.threadgate": AppBskyFeedThreadgate.mainSchema, 32 + "app.bsky.graph.block": AppBskyGraphBlock.mainSchema, 33 + "app.bsky.graph.follow": AppBskyGraphFollow.mainSchema, 34 + "app.bsky.graph.list": AppBskyGraphList.mainSchema, 35 + "app.bsky.graph.listblock": AppBskyGraphListblock.mainSchema, 36 + "app.bsky.graph.listitem": AppBskyGraphListitem.mainSchema, 37 + "app.bsky.graph.starterpack": AppBskyGraphStarterpack.mainSchema, 38 + "app.bsky.graph.verification": AppBskyGraphVerification.mainSchema, 39 + "app.bsky.labeler.service": AppBskyLabelerService.mainSchema, 40 + }; 41 + 42 + /** 43 + * Record validator for AT Protocol records. 44 + * Uses optimistic validation: known schemas are validated, unknown are allowed. 45 + */ 46 + export class RecordValidator { 47 + private strictMode: boolean; 48 + 49 + constructor(options: { strict?: boolean } = {}) { 50 + this.strictMode = options.strict ?? false; 51 + } 52 + 53 + validateRecord(collection: string, record: unknown): void { 54 + const schema = recordSchemas[collection]; 55 + 56 + if (!schema) { 57 + if (this.strictMode) { 58 + throw new Error( 59 + `No lexicon schema loaded for collection: ${collection}`, 60 + ); 61 + } 62 + return; 63 + } 64 + 65 + try { 66 + parse(schema, record); 67 + } catch (error) { 68 + if (error instanceof ValidationError) { 69 + throw new Error( 70 + `Lexicon validation failed for ${collection}: ${error.message}`, 71 + ); 72 + } 73 + throw error; 74 + } 75 + } 76 + 77 + hasSchema(collection: string): boolean { 78 + return collection in recordSchemas; 79 + } 80 + 81 + getLoadedSchemas(): string[] { 82 + return Object.keys(recordSchemas); 83 + } 84 + } 85 + 86 + export const validator = new RecordValidator({ strict: false });
+23
src/xrpc/identity.ts
··· 1 + import type { Context } from "hono"; 2 + import type { AuthedAppEnv } from "../types.js"; 3 + 4 + /** 5 + * Resolve handle - return our DID for our handle. 6 + * This is a simplified version; the full Cirrus identity module 7 + * with PLC operations and migration tokens will be added later. 8 + */ 9 + export async function resolveHandle( 10 + c: Context<AuthedAppEnv>, 11 + ): Promise<Response> { 12 + const handle = c.req.query("handle"); 13 + if (handle === c.env.HANDLE) { 14 + return c.json({ did: c.env.DID }); 15 + } 16 + return c.json( 17 + { 18 + error: "HandleNotFound", 19 + message: `Handle not found: ${handle}`, 20 + }, 21 + 404, 22 + ); 23 + }
+513
src/xrpc/repo.ts
··· 1 + import type { Context } from "hono"; 2 + import { isDid } from "@atcute/lexicons/syntax"; 3 + import type { RepoManager } from "../repo-manager.js"; 4 + import type { AppEnv, AuthedAppEnv } from "../types.js"; 5 + import { validator } from "../validation.js"; 6 + import { detectContentType } from "../format.js"; 7 + 8 + function invalidRecordError( 9 + c: Context<AuthedAppEnv>, 10 + err: unknown, 11 + prefix?: string, 12 + ): Response { 13 + const message = err instanceof Error ? err.message : String(err); 14 + return c.json( 15 + { 16 + error: "InvalidRecord", 17 + message: prefix ? `${prefix}: ${message}` : message, 18 + }, 19 + 400, 20 + ); 21 + } 22 + 23 + function checkAccountDeactivatedError( 24 + c: Context<AuthedAppEnv>, 25 + err: unknown, 26 + ): Response | null { 27 + const message = err instanceof Error ? err.message : String(err); 28 + if (message.startsWith("AccountDeactivated:")) { 29 + return c.json( 30 + { 31 + error: "AccountDeactivated", 32 + message: 33 + "Account is deactivated. Call activateAccount to enable writes.", 34 + }, 35 + 403, 36 + ); 37 + } 38 + return null; 39 + } 40 + 41 + export async function describeRepo( 42 + c: Context<AppEnv>, 43 + repoManager: RepoManager, 44 + ): Promise<Response> { 45 + const repo = c.req.query("repo"); 46 + 47 + if (!repo) { 48 + return c.json( 49 + { error: "InvalidRequest", message: "Missing required parameter: repo" }, 50 + 400, 51 + ); 52 + } 53 + 54 + if (!isDid(repo)) { 55 + return c.json( 56 + { error: "InvalidRequest", message: "Invalid DID format" }, 57 + 400, 58 + ); 59 + } 60 + 61 + if (repo !== c.env.DID) { 62 + return c.json( 63 + { error: "RepoNotFound", message: `Repository not found: ${repo}` }, 64 + 404, 65 + ); 66 + } 67 + 68 + const data = await repoManager.describeRepo(); 69 + 70 + return c.json({ 71 + did: c.env.DID, 72 + handle: c.env.HANDLE, 73 + didDoc: { 74 + "@context": ["https://www.w3.org/ns/did/v1"], 75 + id: c.env.DID, 76 + alsoKnownAs: [`at://${c.env.HANDLE}`], 77 + verificationMethod: [ 78 + { 79 + id: `${c.env.DID}#atproto`, 80 + type: "Multikey", 81 + controller: c.env.DID, 82 + publicKeyMultibase: c.env.SIGNING_KEY_PUBLIC, 83 + }, 84 + ], 85 + }, 86 + collections: data.collections, 87 + handleIsCorrect: true, 88 + }); 89 + } 90 + 91 + export async function getRecord( 92 + c: Context<AppEnv>, 93 + repoManager: RepoManager, 94 + ): Promise<Response> { 95 + const repo = c.req.query("repo"); 96 + const collection = c.req.query("collection"); 97 + const rkey = c.req.query("rkey"); 98 + 99 + if (!repo || !collection || !rkey) { 100 + return c.json( 101 + { 102 + error: "InvalidRequest", 103 + message: "Missing required parameters: repo, collection, rkey", 104 + }, 105 + 400, 106 + ); 107 + } 108 + 109 + if (!isDid(repo)) { 110 + return c.json( 111 + { error: "InvalidRequest", message: "Invalid DID format" }, 112 + 400, 113 + ); 114 + } 115 + 116 + if (repo !== c.env.DID) { 117 + return c.json( 118 + { error: "RepoNotFound", message: `Repository not found: ${repo}` }, 119 + 404, 120 + ); 121 + } 122 + 123 + const result = await repoManager.getRecord(collection, rkey); 124 + 125 + if (!result) { 126 + return c.json( 127 + { 128 + error: "RecordNotFound", 129 + message: `Record not found: ${collection}/${rkey}`, 130 + }, 131 + 404, 132 + ); 133 + } 134 + 135 + return c.json({ 136 + uri: `at://${repo}/${collection}/${rkey}`, 137 + cid: result.cid, 138 + value: result.record, 139 + }); 140 + } 141 + 142 + export async function listRecords( 143 + c: Context<AppEnv>, 144 + repoManager: RepoManager, 145 + ): Promise<Response> { 146 + const repo = c.req.query("repo"); 147 + const collection = c.req.query("collection"); 148 + const limitStr = c.req.query("limit"); 149 + const cursor = c.req.query("cursor"); 150 + const reverseStr = c.req.query("reverse"); 151 + 152 + if (!repo || !collection) { 153 + return c.json( 154 + { 155 + error: "InvalidRequest", 156 + message: "Missing required parameters: repo, collection", 157 + }, 158 + 400, 159 + ); 160 + } 161 + 162 + if (!isDid(repo)) { 163 + return c.json( 164 + { error: "InvalidRequest", message: "Invalid DID format" }, 165 + 400, 166 + ); 167 + } 168 + 169 + if (repo !== c.env.DID) { 170 + return c.json( 171 + { error: "RepoNotFound", message: `Repository not found: ${repo}` }, 172 + 404, 173 + ); 174 + } 175 + 176 + const limit = Math.min(limitStr ? Number.parseInt(limitStr, 10) : 50, 100); 177 + const reverse = reverseStr === "true"; 178 + 179 + const result = await repoManager.listRecords(collection, { 180 + limit, 181 + cursor: cursor || undefined, 182 + reverse, 183 + }); 184 + 185 + return c.json(result); 186 + } 187 + 188 + export async function createRecord( 189 + c: Context<AuthedAppEnv>, 190 + repoManager: RepoManager, 191 + ): Promise<Response> { 192 + const body = await c.req.json(); 193 + const { repo, collection, rkey, record } = body; 194 + 195 + if (!repo || !collection || !record) { 196 + return c.json( 197 + { 198 + error: "InvalidRequest", 199 + message: "Missing required parameters: repo, collection, record", 200 + }, 201 + 400, 202 + ); 203 + } 204 + 205 + if (repo !== c.env.DID) { 206 + return c.json( 207 + { error: "InvalidRepo", message: `Invalid repository: ${repo}` }, 208 + 400, 209 + ); 210 + } 211 + 212 + try { 213 + validator.validateRecord(collection, record); 214 + } catch (err) { 215 + return invalidRecordError(c, err); 216 + } 217 + 218 + try { 219 + const result = await repoManager.createRecord(collection, rkey, record); 220 + return c.json(result); 221 + } catch (err) { 222 + const deactivatedError = checkAccountDeactivatedError(c, err); 223 + if (deactivatedError) return deactivatedError; 224 + throw err; 225 + } 226 + } 227 + 228 + export async function deleteRecord( 229 + c: Context<AuthedAppEnv>, 230 + repoManager: RepoManager, 231 + ): Promise<Response> { 232 + const body = await c.req.json(); 233 + const { repo, collection, rkey } = body; 234 + 235 + if (!repo || !collection || !rkey) { 236 + return c.json( 237 + { 238 + error: "InvalidRequest", 239 + message: "Missing required parameters: repo, collection, rkey", 240 + }, 241 + 400, 242 + ); 243 + } 244 + 245 + if (repo !== c.env.DID) { 246 + return c.json( 247 + { error: "InvalidRepo", message: `Invalid repository: ${repo}` }, 248 + 400, 249 + ); 250 + } 251 + 252 + try { 253 + const result = await repoManager.deleteRecord(collection, rkey); 254 + 255 + if (!result) { 256 + return c.json( 257 + { 258 + error: "RecordNotFound", 259 + message: `Record not found: ${collection}/${rkey}`, 260 + }, 261 + 404, 262 + ); 263 + } 264 + 265 + return c.json(result); 266 + } catch (err) { 267 + const deactivatedError = checkAccountDeactivatedError(c, err); 268 + if (deactivatedError) return deactivatedError; 269 + throw err; 270 + } 271 + } 272 + 273 + export async function putRecord( 274 + c: Context<AuthedAppEnv>, 275 + repoManager: RepoManager, 276 + ): Promise<Response> { 277 + const body = await c.req.json(); 278 + const { repo, collection, rkey, record } = body; 279 + 280 + if (!repo || !collection || !rkey || !record) { 281 + return c.json( 282 + { 283 + error: "InvalidRequest", 284 + message: "Missing required parameters: repo, collection, rkey, record", 285 + }, 286 + 400, 287 + ); 288 + } 289 + 290 + if (repo !== c.env.DID) { 291 + return c.json( 292 + { error: "InvalidRepo", message: `Invalid repository: ${repo}` }, 293 + 400, 294 + ); 295 + } 296 + 297 + try { 298 + validator.validateRecord(collection, record); 299 + } catch (err) { 300 + return invalidRecordError(c, err); 301 + } 302 + 303 + try { 304 + const result = await repoManager.putRecord(collection, rkey, record); 305 + return c.json(result); 306 + } catch (err) { 307 + const deactivatedError = checkAccountDeactivatedError(c, err); 308 + if (deactivatedError) return deactivatedError; 309 + return c.json( 310 + { 311 + error: "InvalidRequest", 312 + message: err instanceof Error ? err.message : String(err), 313 + }, 314 + 400, 315 + ); 316 + } 317 + } 318 + 319 + export async function applyWrites( 320 + c: Context<AuthedAppEnv>, 321 + repoManager: RepoManager, 322 + ): Promise<Response> { 323 + const body = await c.req.json(); 324 + const { repo, writes } = body; 325 + 326 + if (!repo || !writes || !Array.isArray(writes)) { 327 + return c.json( 328 + { 329 + error: "InvalidRequest", 330 + message: "Missing required parameters: repo, writes", 331 + }, 332 + 400, 333 + ); 334 + } 335 + 336 + if (repo !== c.env.DID) { 337 + return c.json( 338 + { error: "InvalidRepo", message: `Invalid repository: ${repo}` }, 339 + 400, 340 + ); 341 + } 342 + 343 + if (writes.length > 200) { 344 + return c.json( 345 + { error: "InvalidRequest", message: "Too many writes. Max: 200" }, 346 + 400, 347 + ); 348 + } 349 + 350 + for (let i = 0; i < writes.length; i++) { 351 + const write = writes[i]; 352 + if ( 353 + write.$type === "com.atproto.repo.applyWrites#create" || 354 + write.$type === "com.atproto.repo.applyWrites#update" 355 + ) { 356 + try { 357 + validator.validateRecord(write.collection, write.value); 358 + } catch (err) { 359 + return invalidRecordError(c, err, `Write ${i}`); 360 + } 361 + } 362 + } 363 + 364 + try { 365 + const result = await repoManager.applyWrites(writes); 366 + return c.json(result); 367 + } catch (err) { 368 + const deactivatedError = checkAccountDeactivatedError(c, err); 369 + if (deactivatedError) return deactivatedError; 370 + return c.json( 371 + { 372 + error: "InvalidRequest", 373 + message: err instanceof Error ? err.message : String(err), 374 + }, 375 + 400, 376 + ); 377 + } 378 + } 379 + 380 + export async function uploadBlob( 381 + c: Context<AuthedAppEnv>, 382 + repoManager: RepoManager, 383 + ): Promise<Response> { 384 + let contentType = c.req.header("Content-Type"); 385 + 386 + const bytes = new Uint8Array(await c.req.arrayBuffer()); 387 + if (!contentType || contentType === "*/*") { 388 + contentType = detectContentType(bytes) || "application/octet-stream"; 389 + } 390 + 391 + const MAX_BLOB_SIZE = 60 * 1024 * 1024; 392 + if (bytes.length > MAX_BLOB_SIZE) { 393 + return c.json( 394 + { 395 + error: "BlobTooLarge", 396 + message: `Blob size ${bytes.length} exceeds maximum of ${MAX_BLOB_SIZE} bytes`, 397 + }, 398 + 400, 399 + ); 400 + } 401 + 402 + try { 403 + const blobRef = await repoManager.uploadBlob(bytes, contentType); 404 + return c.json({ blob: blobRef }); 405 + } catch (err) { 406 + if ( 407 + err instanceof Error && 408 + err.message.includes("Blob storage not configured") 409 + ) { 410 + return c.json( 411 + { 412 + error: "ServiceUnavailable", 413 + message: "Blob storage is not configured", 414 + }, 415 + 503, 416 + ); 417 + } 418 + throw err; 419 + } 420 + } 421 + 422 + export async function importRepo( 423 + c: Context<AuthedAppEnv>, 424 + repoManager: RepoManager, 425 + ): Promise<Response> { 426 + const contentType = c.req.header("Content-Type"); 427 + 428 + if (contentType !== "application/vnd.ipld.car") { 429 + return c.json( 430 + { 431 + error: "InvalidRequest", 432 + message: 433 + "Content-Type must be application/vnd.ipld.car for repository import", 434 + }, 435 + 400, 436 + ); 437 + } 438 + 439 + const carBytes = new Uint8Array(await c.req.arrayBuffer()); 440 + 441 + if (carBytes.length === 0) { 442 + return c.json( 443 + { error: "InvalidRequest", message: "Empty CAR file" }, 444 + 400, 445 + ); 446 + } 447 + 448 + const MAX_CAR_SIZE = 100 * 1024 * 1024; 449 + if (carBytes.length > MAX_CAR_SIZE) { 450 + return c.json( 451 + { 452 + error: "RepoTooLarge", 453 + message: `Repository size ${carBytes.length} exceeds maximum of ${MAX_CAR_SIZE} bytes`, 454 + }, 455 + 400, 456 + ); 457 + } 458 + 459 + try { 460 + const result = await repoManager.importRepo(carBytes); 461 + return c.json(result); 462 + } catch (err) { 463 + if (err instanceof Error) { 464 + if (err.message.includes("already exists")) { 465 + return c.json( 466 + { 467 + error: "RepoAlreadyExists", 468 + message: 469 + "Repository already exists. Cannot import over existing data.", 470 + }, 471 + 409, 472 + ); 473 + } 474 + if (err.message.includes("DID mismatch")) { 475 + return c.json( 476 + { error: "InvalidRepo", message: err.message }, 477 + 400, 478 + ); 479 + } 480 + if ( 481 + err.message.includes("no roots") || 482 + err.message.includes("no blocks") || 483 + err.message.includes("Invalid root") 484 + ) { 485 + return c.json( 486 + { 487 + error: "InvalidRepo", 488 + message: `Invalid CAR file: ${err.message}`, 489 + }, 490 + 400, 491 + ); 492 + } 493 + } 494 + throw err; 495 + } 496 + } 497 + 498 + export async function listMissingBlobs( 499 + c: Context<AuthedAppEnv>, 500 + repoManager: RepoManager, 501 + ): Promise<Response> { 502 + const limitStr = c.req.query("limit"); 503 + const cursor = c.req.query("cursor"); 504 + 505 + const limit = limitStr ? Math.min(Number.parseInt(limitStr, 10), 500) : 500; 506 + 507 + const result = await repoManager.listMissingBlobs( 508 + limit, 509 + cursor || undefined, 510 + ); 511 + 512 + return c.json(result); 513 + }
+396
src/xrpc/server.ts
··· 1 + import type { Context } from "hono"; 2 + import type { RepoManager } from "../repo-manager.js"; 3 + import { createServiceJwt, getSigningKeypair } from "../service-auth.js"; 4 + import { 5 + createAccessToken, 6 + createRefreshToken, 7 + verifyPassword, 8 + verifyAccessToken, 9 + verifyRefreshToken, 10 + TokenExpiredError, 11 + } from "../session.js"; 12 + import type { AppEnv, AuthedAppEnv } from "../types.js"; 13 + 14 + export async function describeServer(c: Context<AppEnv>): Promise<Response> { 15 + return c.json({ 16 + did: c.env.DID, 17 + availableUserDomains: [], 18 + inviteCodeRequired: false, 19 + }); 20 + } 21 + 22 + export async function createSession( 23 + c: Context<AppEnv>, 24 + repoManager: RepoManager, 25 + ): Promise<Response> { 26 + const body = await c.req.json<{ 27 + identifier: string; 28 + password: string; 29 + }>(); 30 + 31 + const { identifier, password } = body; 32 + 33 + if (!identifier || !password) { 34 + return c.json( 35 + { error: "InvalidRequest", message: "Missing identifier or password" }, 36 + 400, 37 + ); 38 + } 39 + 40 + if (identifier !== c.env.HANDLE && identifier !== c.env.DID) { 41 + return c.json( 42 + { 43 + error: "AuthenticationRequired", 44 + message: "Invalid identifier or password", 45 + }, 46 + 401, 47 + ); 48 + } 49 + 50 + const passwordValid = await verifyPassword(password, c.env.PASSWORD_HASH); 51 + if (!passwordValid) { 52 + return c.json( 53 + { 54 + error: "AuthenticationRequired", 55 + message: "Invalid identifier or password", 56 + }, 57 + 401, 58 + ); 59 + } 60 + 61 + const serviceDid = `did:web:${c.env.PDS_HOSTNAME}`; 62 + const accessJwt = await createAccessToken( 63 + c.env.JWT_SECRET, 64 + c.env.DID, 65 + serviceDid, 66 + ); 67 + const refreshJwt = await createRefreshToken( 68 + c.env.JWT_SECRET, 69 + c.env.DID, 70 + serviceDid, 71 + ); 72 + 73 + const { email: storedEmail } = await repoManager.getEmail(); 74 + const email = storedEmail || c.env.EMAIL; 75 + 76 + return c.json({ 77 + accessJwt, 78 + refreshJwt, 79 + handle: c.env.HANDLE, 80 + did: c.env.DID, 81 + ...(email ? { email } : {}), 82 + emailConfirmed: true, 83 + active: true, 84 + }); 85 + } 86 + 87 + export async function refreshSession( 88 + c: Context<AppEnv>, 89 + repoManager: RepoManager, 90 + ): Promise<Response> { 91 + const authHeader = c.req.header("Authorization"); 92 + 93 + if (!authHeader?.startsWith("Bearer ")) { 94 + return c.json( 95 + { error: "AuthenticationRequired", message: "Refresh token required" }, 96 + 401, 97 + ); 98 + } 99 + 100 + const token = authHeader.slice(7); 101 + const serviceDid = `did:web:${c.env.PDS_HOSTNAME}`; 102 + 103 + try { 104 + const payload = await verifyRefreshToken( 105 + token, 106 + c.env.JWT_SECRET, 107 + serviceDid, 108 + ); 109 + 110 + if (payload.sub !== c.env.DID) { 111 + return c.json( 112 + { 113 + error: "AuthenticationRequired", 114 + message: "Invalid refresh token", 115 + }, 116 + 401, 117 + ); 118 + } 119 + 120 + const accessJwt = await createAccessToken( 121 + c.env.JWT_SECRET, 122 + c.env.DID, 123 + serviceDid, 124 + ); 125 + const refreshJwt = await createRefreshToken( 126 + c.env.JWT_SECRET, 127 + c.env.DID, 128 + serviceDid, 129 + ); 130 + 131 + const { email: storedEmail } = await repoManager.getEmail(); 132 + const email = storedEmail || c.env.EMAIL; 133 + 134 + return c.json({ 135 + accessJwt, 136 + refreshJwt, 137 + handle: c.env.HANDLE, 138 + did: c.env.DID, 139 + ...(email ? { email } : {}), 140 + emailConfirmed: true, 141 + active: true, 142 + }); 143 + } catch (err) { 144 + if (err instanceof TokenExpiredError) { 145 + return c.json( 146 + { error: "ExpiredToken", message: err.message }, 147 + 400, 148 + ); 149 + } 150 + return c.json( 151 + { 152 + error: "InvalidToken", 153 + message: err instanceof Error ? err.message : "Invalid refresh token", 154 + }, 155 + 400, 156 + ); 157 + } 158 + } 159 + 160 + export async function getSession( 161 + c: Context<AppEnv>, 162 + repoManager: RepoManager, 163 + ): Promise<Response> { 164 + const authHeader = c.req.header("Authorization"); 165 + 166 + if (!authHeader?.startsWith("Bearer ")) { 167 + return c.json( 168 + { error: "AuthenticationRequired", message: "Access token required" }, 169 + 401, 170 + ); 171 + } 172 + 173 + const token = authHeader.slice(7); 174 + const serviceDid = `did:web:${c.env.PDS_HOSTNAME}`; 175 + 176 + if (token === c.env.AUTH_TOKEN) { 177 + const { email: storedEmail } = await repoManager.getEmail(); 178 + const email = storedEmail || c.env.EMAIL; 179 + return c.json({ 180 + handle: c.env.HANDLE, 181 + did: c.env.DID, 182 + ...(email ? { email } : {}), 183 + emailConfirmed: true, 184 + active: true, 185 + }); 186 + } 187 + 188 + try { 189 + const payload = await verifyAccessToken( 190 + token, 191 + c.env.JWT_SECRET, 192 + serviceDid, 193 + ); 194 + 195 + if (payload.sub !== c.env.DID) { 196 + return c.json( 197 + { 198 + error: "AuthenticationRequired", 199 + message: "Invalid access token", 200 + }, 201 + 401, 202 + ); 203 + } 204 + 205 + const { email: storedEmail } = await repoManager.getEmail(); 206 + const email = storedEmail || c.env.EMAIL; 207 + return c.json({ 208 + handle: c.env.HANDLE, 209 + did: c.env.DID, 210 + ...(email ? { email } : {}), 211 + emailConfirmed: true, 212 + active: true, 213 + }); 214 + } catch (err) { 215 + if (err instanceof TokenExpiredError) { 216 + return c.json( 217 + { error: "ExpiredToken", message: err.message }, 218 + 400, 219 + ); 220 + } 221 + return c.json( 222 + { 223 + error: "InvalidToken", 224 + message: err instanceof Error ? err.message : "Invalid access token", 225 + }, 226 + 401, 227 + ); 228 + } 229 + } 230 + 231 + export async function deleteSession(c: Context<AppEnv>): Promise<Response> { 232 + return c.json({}); 233 + } 234 + 235 + export async function checkAccountStatus( 236 + c: Context<AuthedAppEnv>, 237 + repoManager: RepoManager, 238 + ): Promise<Response> { 239 + try { 240 + const status = await repoManager.getRepoStatus(); 241 + const active = await repoManager.getActive(); 242 + 243 + const [repoBlocks, indexedRecords, expectedBlobs, importedBlobs] = 244 + await Promise.all([ 245 + repoManager.countBlocks(), 246 + repoManager.countRecords(), 247 + repoManager.countExpectedBlobs(), 248 + repoManager.countImportedBlobs(), 249 + ]); 250 + 251 + const activated = active || indexedRecords > 0; 252 + 253 + return c.json({ 254 + activated, 255 + active, 256 + validDid: true, 257 + repoCommit: status.head, 258 + repoRev: status.rev, 259 + repoBlocks, 260 + indexedRecords, 261 + privateStateValues: null, 262 + expectedBlobs, 263 + importedBlobs, 264 + }); 265 + } catch { 266 + return c.json({ 267 + activated: false, 268 + active: false, 269 + validDid: true, 270 + repoCommit: null, 271 + repoRev: null, 272 + repoBlocks: 0, 273 + indexedRecords: 0, 274 + privateStateValues: null, 275 + expectedBlobs: 0, 276 + importedBlobs: 0, 277 + }); 278 + } 279 + } 280 + 281 + export async function getServiceAuth( 282 + c: Context<AuthedAppEnv>, 283 + ): Promise<Response> { 284 + const aud = c.req.query("aud"); 285 + const lxm = c.req.query("lxm") || null; 286 + 287 + if (!aud) { 288 + return c.json( 289 + { error: "InvalidRequest", message: "Missing required parameter: aud" }, 290 + 400, 291 + ); 292 + } 293 + 294 + const keypair = await getSigningKeypair(c.env.SIGNING_KEY); 295 + const token = await createServiceJwt({ 296 + iss: c.env.DID, 297 + aud, 298 + lxm, 299 + keypair, 300 + }); 301 + 302 + return c.json({ token }); 303 + } 304 + 305 + export async function activateAccount( 306 + c: Context<AuthedAppEnv>, 307 + repoManager: RepoManager, 308 + ): Promise<Response> { 309 + try { 310 + await repoManager.activateAccount(); 311 + return c.json({ success: true }); 312 + } catch (err) { 313 + return c.json( 314 + { 315 + error: "InternalServerError", 316 + message: err instanceof Error ? err.message : "Unknown error", 317 + }, 318 + 500, 319 + ); 320 + } 321 + } 322 + 323 + export async function deactivateAccount( 324 + c: Context<AuthedAppEnv>, 325 + repoManager: RepoManager, 326 + ): Promise<Response> { 327 + try { 328 + await repoManager.deactivateAccount(); 329 + return c.json({ success: true }); 330 + } catch (err) { 331 + return c.json( 332 + { 333 + error: "InternalServerError", 334 + message: err instanceof Error ? err.message : "Unknown error", 335 + }, 336 + 500, 337 + ); 338 + } 339 + } 340 + 341 + export async function requestEmailUpdate( 342 + c: Context<AuthedAppEnv>, 343 + ): Promise<Response> { 344 + return c.json({ tokenRequired: false }); 345 + } 346 + 347 + export async function requestEmailConfirmation( 348 + c: Context<AuthedAppEnv>, 349 + ): Promise<Response> { 350 + return c.json({}); 351 + } 352 + 353 + export async function updateEmail( 354 + c: Context<AuthedAppEnv>, 355 + repoManager: RepoManager, 356 + ): Promise<Response> { 357 + const body = await c.req.json<{ email: string }>(); 358 + 359 + if (!body.email) { 360 + return c.json( 361 + { error: "InvalidRequest", message: "Missing required field: email" }, 362 + 400, 363 + ); 364 + } 365 + 366 + await repoManager.updateEmail(body.email); 367 + return c.json({}); 368 + } 369 + 370 + export async function resetMigration( 371 + c: Context<AuthedAppEnv>, 372 + repoManager: RepoManager, 373 + ): Promise<Response> { 374 + try { 375 + const result = await repoManager.resetMigration(); 376 + return c.json(result); 377 + } catch (err) { 378 + const message = err instanceof Error ? err.message : "Unknown error"; 379 + 380 + if (message.includes("AccountActive")) { 381 + return c.json( 382 + { 383 + error: "AccountActive", 384 + message: 385 + "Cannot reset migration on an active account. Deactivate first.", 386 + }, 387 + 400, 388 + ); 389 + } 390 + 391 + return c.json( 392 + { error: "InternalServerError", message }, 393 + 500, 394 + ); 395 + } 396 + }
+323
src/xrpc/sync.ts
··· 1 + import type { Context } from "hono"; 2 + import { isDid, isNsid, isRecordKey } from "@atcute/lexicons/syntax"; 3 + import type { RepoManager } from "../repo-manager.js"; 4 + import type { AppEnv } from "../types.js"; 5 + import { detectContentType } from "../format.js"; 6 + 7 + export async function getRepo( 8 + c: Context<AppEnv>, 9 + repoManager: RepoManager, 10 + ): Promise<Response> { 11 + const did = c.req.query("did"); 12 + 13 + if (!did) { 14 + return c.json( 15 + { error: "InvalidRequest", message: "Missing required parameter: did" }, 16 + 400, 17 + ); 18 + } 19 + 20 + if (!isDid(did)) { 21 + return c.json( 22 + { error: "InvalidRequest", message: "Invalid DID format" }, 23 + 400, 24 + ); 25 + } 26 + 27 + if (did !== c.env.DID) { 28 + return c.json( 29 + { error: "RepoNotFound", message: `Repository not found for DID: ${did}` }, 30 + 404, 31 + ); 32 + } 33 + 34 + const carBytes = await repoManager.getRepoCar(); 35 + 36 + return new Response(carBytes, { 37 + status: 200, 38 + headers: { 39 + "Content-Type": "application/vnd.ipld.car", 40 + "Content-Length": carBytes.length.toString(), 41 + }, 42 + }); 43 + } 44 + 45 + export async function getRepoStatus( 46 + c: Context<AppEnv>, 47 + repoManager: RepoManager, 48 + ): Promise<Response> { 49 + const did = c.req.query("did"); 50 + 51 + if (!did) { 52 + return c.json( 53 + { error: "InvalidRequest", message: "Missing required parameter: did" }, 54 + 400, 55 + ); 56 + } 57 + 58 + if (!isDid(did)) { 59 + return c.json( 60 + { error: "InvalidRequest", message: "Invalid DID format" }, 61 + 400, 62 + ); 63 + } 64 + 65 + if (did !== c.env.DID) { 66 + return c.json( 67 + { error: "RepoNotFound", message: `Repository not found for DID: ${did}` }, 68 + 404, 69 + ); 70 + } 71 + 72 + const data = await repoManager.getRepoStatus(); 73 + 74 + return c.json({ 75 + did: data.did, 76 + active: true, 77 + status: "active", 78 + rev: data.rev, 79 + }); 80 + } 81 + 82 + export async function listRepos( 83 + c: Context<AppEnv>, 84 + repoManager: RepoManager, 85 + ): Promise<Response> { 86 + const data = await repoManager.getRepoStatus(); 87 + 88 + return c.json({ 89 + repos: [ 90 + { 91 + did: data.did, 92 + head: data.head, 93 + rev: data.rev, 94 + active: true, 95 + }, 96 + ], 97 + }); 98 + } 99 + 100 + export async function listBlobs( 101 + c: Context<AppEnv>, 102 + repoManager: RepoManager, 103 + ): Promise<Response> { 104 + const did = c.req.query("did"); 105 + 106 + if (!did) { 107 + return c.json( 108 + { error: "InvalidRequest", message: "Missing required parameter: did" }, 109 + 400, 110 + ); 111 + } 112 + 113 + if (!isDid(did)) { 114 + return c.json( 115 + { error: "InvalidRequest", message: "Invalid DID format" }, 116 + 400, 117 + ); 118 + } 119 + 120 + if (did !== c.env.DID) { 121 + return c.json( 122 + { error: "RepoNotFound", message: `Repository not found for DID: ${did}` }, 123 + 404, 124 + ); 125 + } 126 + 127 + if (!repoManager.blobStore) { 128 + return c.json({ cids: [] }); 129 + } 130 + 131 + const cursor = c.req.query("cursor"); 132 + const limit = Math.min(Number(c.req.query("limit")) || 500, 1000); 133 + 134 + const result = repoManager.blobStore.listBlobs(limit, cursor || undefined); 135 + 136 + const response: { cids: string[]; cursor?: string } = { cids: result.cids }; 137 + if (result.cursor) { 138 + response.cursor = result.cursor; 139 + } 140 + 141 + return c.json(response); 142 + } 143 + 144 + export async function getBlocks( 145 + c: Context<AppEnv>, 146 + repoManager: RepoManager, 147 + ): Promise<Response> { 148 + const did = c.req.query("did"); 149 + const cidsParam = c.req.queries("cids"); 150 + 151 + if (!did) { 152 + return c.json( 153 + { error: "InvalidRequest", message: "Missing required parameter: did" }, 154 + 400, 155 + ); 156 + } 157 + 158 + if (!cidsParam || cidsParam.length === 0) { 159 + return c.json( 160 + { error: "InvalidRequest", message: "Missing required parameter: cids" }, 161 + 400, 162 + ); 163 + } 164 + 165 + if (!isDid(did)) { 166 + return c.json( 167 + { error: "InvalidRequest", message: "Invalid DID format" }, 168 + 400, 169 + ); 170 + } 171 + 172 + if (did !== c.env.DID) { 173 + return c.json( 174 + { error: "RepoNotFound", message: `Repository not found for DID: ${did}` }, 175 + 404, 176 + ); 177 + } 178 + 179 + const carBytes = await repoManager.getBlocks(cidsParam); 180 + 181 + return new Response(carBytes, { 182 + status: 200, 183 + headers: { 184 + "Content-Type": "application/vnd.ipld.car", 185 + "Content-Length": carBytes.length.toString(), 186 + }, 187 + }); 188 + } 189 + 190 + export async function getBlob( 191 + c: Context<AppEnv>, 192 + repoManager: RepoManager, 193 + ): Promise<Response> { 194 + const did = c.req.query("did"); 195 + const cid = c.req.query("cid"); 196 + 197 + if (!did || !cid) { 198 + return c.json( 199 + { error: "InvalidRequest", message: "Missing required parameters: did, cid" }, 200 + 400, 201 + ); 202 + } 203 + 204 + if (!isDid(did)) { 205 + return c.json( 206 + { error: "InvalidRequest", message: "Invalid DID format" }, 207 + 400, 208 + ); 209 + } 210 + 211 + if (did !== c.env.DID) { 212 + return c.json( 213 + { error: "RepoNotFound", message: `Repository not found for DID: ${did}` }, 214 + 404, 215 + ); 216 + } 217 + 218 + if (!repoManager.blobStore) { 219 + return c.json( 220 + { error: "ServiceUnavailable", message: "Blob storage is not configured" }, 221 + 503, 222 + ); 223 + } 224 + 225 + const blob = repoManager.blobStore.getBlob(cid); 226 + 227 + if (!blob) { 228 + return c.json( 229 + { error: "BlobNotFound", message: `Blob not found: ${cid}` }, 230 + 404, 231 + ); 232 + } 233 + 234 + let contentType = blob.mimeType; 235 + if (!contentType || contentType === "*/*") { 236 + contentType = 237 + detectContentType(blob.bytes) || "application/octet-stream"; 238 + } 239 + 240 + return new Response(blob.bytes, { 241 + status: 200, 242 + headers: { 243 + "Content-Type": contentType, 244 + "Content-Length": blob.size.toString(), 245 + }, 246 + }); 247 + } 248 + 249 + export async function getRecord( 250 + c: Context<AppEnv>, 251 + repoManager: RepoManager, 252 + ): Promise<Response> { 253 + const did = c.req.query("did"); 254 + const collection = c.req.query("collection"); 255 + const rkey = c.req.query("rkey"); 256 + 257 + if (!did) { 258 + return c.json( 259 + { error: "InvalidRequest", message: "Missing required parameter: did" }, 260 + 400, 261 + ); 262 + } 263 + 264 + if (!collection) { 265 + return c.json( 266 + { error: "InvalidRequest", message: "Missing required parameter: collection" }, 267 + 400, 268 + ); 269 + } 270 + 271 + if (!rkey) { 272 + return c.json( 273 + { error: "InvalidRequest", message: "Missing required parameter: rkey" }, 274 + 400, 275 + ); 276 + } 277 + 278 + if (!isDid(did)) { 279 + return c.json( 280 + { error: "InvalidRequest", message: "Invalid DID format" }, 281 + 400, 282 + ); 283 + } 284 + 285 + if (!isNsid(collection)) { 286 + return c.json( 287 + { error: "InvalidRequest", message: "Invalid collection format (must be NSID)" }, 288 + 400, 289 + ); 290 + } 291 + 292 + if (!isRecordKey(rkey)) { 293 + return c.json( 294 + { error: "InvalidRequest", message: "Invalid rkey format" }, 295 + 400, 296 + ); 297 + } 298 + 299 + if (did !== c.env.DID) { 300 + return c.json( 301 + { error: "RepoNotFound", message: `Repository not found for DID: ${did}` }, 302 + 404, 303 + ); 304 + } 305 + 306 + try { 307 + const carBytes = await repoManager.getRecordProof(collection, rkey); 308 + 309 + return new Response(carBytes, { 310 + status: 200, 311 + headers: { 312 + "Content-Type": "application/vnd.ipld.car", 313 + "Content-Length": carBytes.length.toString(), 314 + }, 315 + }); 316 + } catch (err) { 317 + console.error("Error getting record proof:", err); 318 + return c.json( 319 + { error: "InternalServerError", message: "Failed to get record proof" }, 320 + 500, 321 + ); 322 + } 323 + }
+20
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "NodeNext", 5 + "moduleResolution": "NodeNext", 6 + "lib": ["ES2022"], 7 + "outDir": "dist", 8 + "rootDir": "src", 9 + "strict": true, 10 + "esModuleInterop": true, 11 + "skipLibCheck": true, 12 + "forceConsistentCasingInFileNames": true, 13 + "resolveJsonModule": true, 14 + "declaration": true, 15 + "declarationMap": true, 16 + "sourceMap": true 17 + }, 18 + "include": ["src"], 19 + "exclude": ["node_modules", "dist"] 20 + }