the statusphere demo reworked into a vite/react app in a monorepo

Merge pull request #1 from bluesky-social/divy/oauth-setup

OAuth, sessions, and view setup

authored by

Paul Frazee and committed by
GitHub
e92849c1 321f3435

+832 -40
+5
.env.template
··· 2 2 NODE_ENV="development" # Options: 'development', 'production' 3 3 PORT="8080" # The port your server will listen on 4 4 HOST="localhost" # Hostname for the server 5 + PUBLIC_URL="" # Set when deployed publicly, e.g. "https://mysite.com". Informs OAuth client id. 5 6 6 7 # CORS Settings 7 8 CORS_ORIGIN="http://localhost:*" # Allowed CORS origin, adjust as necessary ··· 9 10 # Rate Limiting 10 11 COMMON_RATE_LIMIT_WINDOW_MS="1000" # Window size for rate limiting (ms) 11 12 COMMON_RATE_LIMIT_MAX_REQUESTS="20" # Max number of requests per window per IP 13 + 14 + # Secrets 15 + # Must set this in production. May be generated with `openssl rand -base64 33` 16 + # COOKIE_SECRET=""
+15
README.md
··· 1 + # AT Protocol Express App 2 + 3 + A demo application covering: 4 + - public firehose ingestion 5 + - identity and login with OAuth 6 + - writing to the network 7 + 8 + ## Getting Started 9 + ### Development 10 + ```sh 11 + pnpm i 12 + cp .env.template .env 13 + pnpm run dev 14 + # Navigate to http://localhost:8080 15 + ```
+11 -12
package.json
··· 18 18 "test": "vitest run" 19 19 }, 20 20 "dependencies": { 21 - "@atproto/lexicon": "^0.4.0", 22 - "@atproto/repo": "^0.4.1", 21 + "@atproto/jwk-jose": "0.1.2-rc.0", 22 + "@atproto/lexicon": "0.4.1-rc.0", 23 + "@atproto/oauth-client-node": "0.0.2-rc.2", 24 + "@atproto/repo": "0.4.2-rc.0", 23 25 "@atproto/syntax": "^0.3.0", 24 - "@atproto/xrpc-server": "^0.5.3", 26 + "@atproto/xrpc-server": "0.5.4-rc.0", 25 27 "better-sqlite3": "^11.1.2", 26 28 "cors": "^2.8.5", 27 29 "dotenv": "^16.4.5", ··· 30 32 "express-rate-limit": "^7.2.0", 31 33 "helmet": "^7.1.0", 32 34 "http-status-codes": "^2.3.0", 35 + "iron-session": "^8.0.2", 33 36 "kysely": "^0.27.4", 34 37 "multiformats": "^9.9.0", 35 38 "pino": "^9.3.2", 36 - "pino-http": "^10.0.0" 39 + "pino-http": "^10.0.0", 40 + "uhtml": "^4.5.9" 37 41 }, 38 42 "devDependencies": { 39 43 "@atproto/lex-cli": "^0.4.1", ··· 45 49 "pino-pretty": "^11.0.0", 46 50 "rimraf": "^5.0.0", 47 51 "supertest": "^7.0.0", 52 + "ts-node": "^10.9.2", 48 53 "tsup": "^8.0.2", 49 54 "tsx": "^4.7.2", 50 55 "typescript": "^5.4.4", ··· 52 57 "vitest": "^2.0.0" 53 58 }, 54 59 "lint-staged": { 55 - "*.{js,ts,cjs,mjs,d.cts,d.mts,json,jsonc}": [ 56 - "biome check --apply --no-errors-on-unmatched" 57 - ] 60 + "*.{js,ts,cjs,mjs,d.cts,d.mts,json,jsonc}": ["biome check --apply --no-errors-on-unmatched"] 58 61 }, 59 62 "tsup": { 60 - "entry": [ 61 - "src", 62 - "!src/**/__tests__/**", 63 - "!src/**/*.test.*" 64 - ], 63 + "entry": ["src", "!src/**/__tests__/**", "!src/**/*.test.*"], 65 64 "splitting": false, 66 65 "sourcemap": true, 67 66 "clean": true
+435 -24
pnpm-lock.yaml
··· 5 5 excludeLinksFromLockfile: false 6 6 7 7 dependencies: 8 + '@atproto/jwk-jose': 9 + specifier: 0.1.2-rc.0 10 + version: 0.1.2-rc.0 8 11 '@atproto/lexicon': 9 - specifier: ^0.4.0 10 - version: 0.4.0 12 + specifier: 0.4.1-rc.0 13 + version: 0.4.1-rc.0 14 + '@atproto/oauth-client-node': 15 + specifier: 0.0.2-rc.2 16 + version: 0.0.2-rc.2 11 17 '@atproto/repo': 12 - specifier: ^0.4.1 13 - version: 0.4.1 18 + specifier: 0.4.2-rc.0 19 + version: 0.4.2-rc.0 14 20 '@atproto/syntax': 15 21 specifier: ^0.3.0 16 22 version: 0.3.0 17 23 '@atproto/xrpc-server': 18 - specifier: ^0.5.3 19 - version: 0.5.3 24 + specifier: 0.5.4-rc.0 25 + version: 0.5.4-rc.0 20 26 better-sqlite3: 21 27 specifier: ^11.1.2 22 28 version: 11.1.2 ··· 41 47 http-status-codes: 42 48 specifier: ^2.3.0 43 49 version: 2.3.0 50 + iron-session: 51 + specifier: ^8.0.2 52 + version: 8.0.2 44 53 kysely: 45 54 specifier: ^0.27.4 46 55 version: 0.27.4 ··· 53 62 pino-http: 54 63 specifier: ^10.0.0 55 64 version: 10.2.0 65 + uhtml: 66 + specifier: ^4.5.9 67 + version: 4.5.9 56 68 57 69 devDependencies: 58 70 '@atproto/lex-cli': ··· 82 94 supertest: 83 95 specifier: ^7.0.0 84 96 version: 7.0.0 97 + ts-node: 98 + specifier: ^10.9.2 99 + version: 10.9.2(@types/node@22.1.0)(typescript@5.5.4) 85 100 tsup: 86 101 specifier: ^8.0.2 87 102 version: 8.2.4(tsx@4.16.5)(typescript@5.5.4) ··· 96 111 version: 4.3.2(typescript@5.5.4) 97 112 vitest: 98 113 specifier: ^2.0.0 99 - version: 2.0.5 114 + version: 2.0.5(@types/node@22.1.0) 100 115 101 116 packages: 102 117 ··· 108 123 '@jridgewell/trace-mapping': 0.3.25 109 124 dev: true 110 125 126 + /@atproto-labs/did-resolver@0.1.2-rc.0: 127 + resolution: {integrity: sha512-5lVxhLG9P1G1XjGXQr7fhk6mBM5vpbCalrfuVXqU5xQADvObLjEtpxpJuLheAacaV2pUMFDml+53ZLYWXCgFIg==} 128 + dependencies: 129 + '@atproto-labs/fetch': 0.1.0 130 + '@atproto-labs/pipe': 0.1.0 131 + '@atproto-labs/simple-store': 0.1.1 132 + '@atproto-labs/simple-store-memory': 0.1.1 133 + '@atproto/did': 0.1.1-rc.0 134 + zod: 3.23.8 135 + dev: false 136 + 137 + /@atproto-labs/fetch-node@0.1.0: 138 + resolution: {integrity: sha512-DUHgaGw8LBqiGg51pUDuWK/alMcmNbpcK7ALzlF2Gw//TNLTsgrj0qY9aEtK+np9rEC+x/o3bN4SGnuQEpgqIg==} 139 + dependencies: 140 + '@atproto-labs/fetch': 0.1.0 141 + '@atproto-labs/pipe': 0.1.0 142 + ipaddr.js: 2.2.0 143 + psl: 1.9.0 144 + undici: 6.19.5 145 + dev: false 146 + 147 + /@atproto-labs/fetch@0.1.0: 148 + resolution: {integrity: sha512-uirja+uA/C4HNk7vayM+AJqsccxQn2wVziUHxbsjJGt/K6Q8ZOKDaEX2+GrcXvpUVcqUKh+94JFjuzH+CAEUlg==} 149 + dependencies: 150 + '@atproto-labs/pipe': 0.1.0 151 + optionalDependencies: 152 + zod: 3.23.8 153 + dev: false 154 + 155 + /@atproto-labs/handle-resolver-node@0.1.2-rc.0: 156 + resolution: {integrity: sha512-wP1c0fqxdhnIQVxFgD3Z6fiToq1ri9ECTCSPoy/1zbNJ+KWrr0V6BSONF/I5MytEbQaICBh8bvZuurvX0OjbNw==} 157 + dependencies: 158 + '@atproto-labs/fetch-node': 0.1.0 159 + '@atproto-labs/handle-resolver': 0.1.2-rc.0 160 + '@atproto/did': 0.1.1-rc.0 161 + dev: false 162 + 163 + /@atproto-labs/handle-resolver@0.1.2-rc.0: 164 + resolution: {integrity: sha512-sxk/Zr1hWyBBcg1HhZ8N/Tw1Iue/6+V6bzu2c8zYhO9VfKgCBp3FFU1/i3MpgR2AlsEqZpcjv6zj4KAnMHiLUg==} 165 + dependencies: 166 + '@atproto-labs/simple-store': 0.1.1 167 + '@atproto-labs/simple-store-memory': 0.1.1 168 + '@atproto/did': 0.1.1-rc.0 169 + zod: 3.23.8 170 + dev: false 171 + 172 + /@atproto-labs/identity-resolver@0.1.2-rc.0: 173 + resolution: {integrity: sha512-4TLjNRbufeGduac3c/No4teJ411qNgyBQck7eY5e2K8XrzS2a/xX/bq3JP91DrvERHiP3yE22PB6ATQkuALgXA==} 174 + dependencies: 175 + '@atproto-labs/did-resolver': 0.1.2-rc.0 176 + '@atproto-labs/handle-resolver': 0.1.2-rc.0 177 + '@atproto/syntax': 0.3.0 178 + dev: false 179 + 180 + /@atproto-labs/pipe@0.1.0: 181 + resolution: {integrity: sha512-ghOqHFyJlQVFPESzlVHjKroP0tPzbmG5Jms0dNI9yLDEfL8xp4OFPWLX4f6T8mRq69wWs4nIDM3sSsFbFqLa1w==} 182 + dev: false 183 + 184 + /@atproto-labs/simple-store-memory@0.1.1: 185 + resolution: {integrity: sha512-PCRqhnZ8NBNBvLku53O56T0lsVOtclfIrQU/rwLCc4+p45/SBPrRYNBi6YFq5rxZbK6Njos9MCmILV/KLQxrWA==} 186 + dependencies: 187 + '@atproto-labs/simple-store': 0.1.1 188 + lru-cache: 10.4.3 189 + dev: false 190 + 191 + /@atproto-labs/simple-store@0.1.1: 192 + resolution: {integrity: sha512-WKILW2b3QbAYKh+w5U2x6p5FqqLl0nAeLwGeDY+KjX01K4Dq3vQTR9b/qNp0jZm48CabPQVrqCv0PPU9LgRRRg==} 193 + dev: false 194 + 195 + /@atproto/api@0.13.0-rc.1: 196 + resolution: {integrity: sha512-h2+M6OoMLnNzqf2KDxsbRkg3/1k2IMWH33PQI31GkiQHIdt3B+MIXvJwXePu0KnMUL/Lvv2Zk01BKiDnjd4LEw==} 197 + dependencies: 198 + '@atproto/common-web': 0.3.0 199 + '@atproto/lexicon': 0.4.1-rc.0 200 + '@atproto/syntax': 0.3.0 201 + '@atproto/xrpc': 0.6.0-rc.0 202 + await-lock: 2.2.2 203 + multiformats: 9.9.0 204 + tlds: 1.254.0 205 + dev: false 206 + 111 207 /@atproto/common-web@0.3.0: 112 208 resolution: {integrity: sha512-67VnV6JJyX+ZWyjV7xFQMypAgDmjVaR9ZCuU/QW+mqlqI7fex2uL4Fv+7/jHadgzhuJHVd6OHOvNn0wR5WZYtA==} 113 209 dependencies: ··· 135 231 uint8arrays: 3.0.0 136 232 dev: false 137 233 234 + /@atproto/did@0.1.1-rc.0: 235 + resolution: {integrity: sha512-rbO6kQv/bKsMGqAqr1M4o7cmJf893gYzabr1CmJ0rr/FNdXHfr0b9s2lRphA6zCS0wPdT4/mw6/LWiCrnBmi9w==} 236 + dependencies: 237 + zod: 3.23.8 238 + dev: false 239 + 240 + /@atproto/jwk-jose@0.1.2-rc.0: 241 + resolution: {integrity: sha512-guqGhgQjOx6OxxDWBENRa30G3CJ91Rqw+5NEwiv4GfhmmM/szS983kZIydmXpySpyyZhGAPZfkOfHai+HrLsXg==} 242 + dependencies: 243 + '@atproto/jwk': 0.1.1 244 + jose: 5.6.3 245 + dev: false 246 + 247 + /@atproto/jwk-webcrypto@0.1.2-rc.0: 248 + resolution: {integrity: sha512-TlLaJulKDWDhXQ8Wujte4l2RPe/Ym+jAnFR/+lwZbcGQHAUsatBMCKzvYVv3TtqXL3B5gIC9ry12+C7oQ5yE/Q==} 249 + dependencies: 250 + '@atproto/jwk': 0.1.1 251 + '@atproto/jwk-jose': 0.1.2-rc.0 252 + dev: false 253 + 254 + /@atproto/jwk@0.1.1: 255 + resolution: {integrity: sha512-6h/bj1APUk7QcV9t/oA6+9DB5NZx9SZru9x+/pV5oHFI9Xz4ZuM5+dq1PfsJV54pZyqdnZ6W6M717cxoC7q7og==} 256 + dependencies: 257 + multiformats: 9.9.0 258 + zod: 3.23.8 259 + dev: false 260 + 138 261 /@atproto/lex-cli@0.4.1: 139 262 resolution: {integrity: sha512-QP9mE8MYzXR2ydhCBb/mtGqKZjqpffqcpZCr7JM4mFOZPvXV8k7OqVP1h+T94JB/tGcGPhB750S6tqUH9VRLVg==} 140 263 hasBin: true ··· 157 280 iso-datestring-validator: 2.2.2 158 281 multiformats: 9.9.0 159 282 zod: 3.23.8 283 + dev: true 160 284 161 - /@atproto/repo@0.4.1: 162 - resolution: {integrity: sha512-DXv/cBwRcAM0KFb4SwafcQBONd0g31QUNLfjTri1bg5adCbX3bxxE4fCPpQM9Qc3+5lcCkTL/EniHW1j3UQjVA==} 285 + /@atproto/lexicon@0.4.1-rc.0: 286 + resolution: {integrity: sha512-CSYO8MWbxTXTLQMEJ1mTXD2pDxIXO2oCK/FVw9T/BeXLMcvwmeVgKAaytd1AGFkapX8IMAAtjBB3cnaltuHwbg==} 287 + dependencies: 288 + '@atproto/common-web': 0.3.0 289 + '@atproto/syntax': 0.3.0 290 + iso-datestring-validator: 2.2.2 291 + multiformats: 9.9.0 292 + zod: 3.23.8 293 + dev: false 294 + 295 + /@atproto/oauth-client-node@0.0.2-rc.2: 296 + resolution: {integrity: sha512-MxR2C84h6XjTB28RpXfctKLvB6Ot68tiOlsOSigeSTKnNJ5SRD2wISz2647P8dxOec81ugMu8wa5BKcZ5Ry7nw==} 297 + dependencies: 298 + '@atproto-labs/did-resolver': 0.1.2-rc.0 299 + '@atproto-labs/handle-resolver-node': 0.1.2-rc.0 300 + '@atproto-labs/simple-store': 0.1.1 301 + '@atproto/did': 0.1.1-rc.0 302 + '@atproto/jwk': 0.1.1 303 + '@atproto/jwk-jose': 0.1.2-rc.0 304 + '@atproto/jwk-webcrypto': 0.1.2-rc.0 305 + '@atproto/oauth-client': 0.1.2-rc.2 306 + '@atproto/oauth-types': 0.1.2-rc.0 307 + dev: false 308 + 309 + /@atproto/oauth-client@0.1.2-rc.2: 310 + resolution: {integrity: sha512-FBYyEKEU1BFoW1ASFzsmw1oOpVPj/nkoR753OZItgNwl9i+Tr4kAA9TqeXGa6Ol3dh7K67oaxHw7DChdEqbtSg==} 311 + dependencies: 312 + '@atproto-labs/did-resolver': 0.1.2-rc.0 313 + '@atproto-labs/fetch': 0.1.0 314 + '@atproto-labs/handle-resolver': 0.1.2-rc.0 315 + '@atproto-labs/identity-resolver': 0.1.2-rc.0 316 + '@atproto-labs/simple-store': 0.1.1 317 + '@atproto-labs/simple-store-memory': 0.1.1 318 + '@atproto/api': 0.13.0-rc.1 319 + '@atproto/did': 0.1.1-rc.0 320 + '@atproto/jwk': 0.1.1 321 + '@atproto/oauth-types': 0.1.2-rc.0 322 + '@atproto/xrpc': 0.6.0-rc.0 323 + multiformats: 9.9.0 324 + zod: 3.23.8 325 + dev: false 326 + 327 + /@atproto/oauth-types@0.1.2-rc.0: 328 + resolution: {integrity: sha512-q/AxPSdLf2xTgC4K1cU35HVl6T4T0LJ/QJmvqXwjpbiNWEqooIQIP9sTp2CqqSLsWpe26z3fIoA3R+oTR1EJsA==} 329 + dependencies: 330 + '@atproto/jwk': 0.1.1 331 + zod: 3.23.8 332 + dev: false 333 + 334 + /@atproto/repo@0.4.2-rc.0: 335 + resolution: {integrity: sha512-y8zXAR23r6qlsTmbzXaBEHYjvlgeNlAKj9eJ6V17JtT+4FVdW246alhsgSsglJ2Uv/e24RC1r90yNJNRxqDzXw==} 163 336 dependencies: 164 337 '@atproto/common': 0.4.1 165 338 '@atproto/common-web': 0.3.0 166 339 '@atproto/crypto': 0.4.0 167 - '@atproto/lexicon': 0.4.0 340 + '@atproto/lexicon': 0.4.1-rc.0 168 341 '@ipld/car': 3.2.4 169 342 '@ipld/dag-cbor': 7.0.3 170 343 multiformats: 9.9.0 ··· 175 348 /@atproto/syntax@0.3.0: 176 349 resolution: {integrity: sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==} 177 350 178 - /@atproto/xrpc-server@0.5.3: 179 - resolution: {integrity: sha512-Gxe5dPDp7mj7E1JaK0yEwGuWot78/HjszHYakqleKp+IXlM+iZxH0N20O+x7b3g7itImuQ2LzH3Zk1jLB0yZjQ==} 351 + /@atproto/xrpc-server@0.5.4-rc.0: 352 + resolution: {integrity: sha512-Vrx1gEoZfJtYoZhSxkbWQsU2r0DuJO/BuvMQGw9Nd66owmF5nPDVvYVd0pJhIDoaSxImTTIEeDWlNNl3WCSBPA==} 180 353 dependencies: 181 354 '@atproto/common': 0.4.1 182 355 '@atproto/crypto': 0.4.0 183 - '@atproto/lexicon': 0.4.0 184 - '@atproto/xrpc': 0.5.0 356 + '@atproto/lexicon': 0.4.1-rc.0 357 + '@atproto/xrpc': 0.6.0-rc.0 185 358 cbor-x: 1.6.0 186 359 express: 4.19.2 187 360 http-errors: 2.0.0 ··· 196 369 - utf-8-validate 197 370 dev: false 198 371 199 - /@atproto/xrpc@0.5.0: 200 - resolution: {integrity: sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==} 372 + /@atproto/xrpc@0.6.0-rc.0: 373 + resolution: {integrity: sha512-TOmynXvbA57Y6KR050UeiDfdzQoAnmgB0zu0qrvhYiu7oeg64fYzvOa7stWxSIP1nhrGqgexxICR1CnOnCEHjg==} 201 374 dependencies: 202 - '@atproto/lexicon': 0.4.0 375 + '@atproto/lexicon': 0.4.1-rc.0 203 376 zod: 3.23.8 204 377 dev: false 205 378 ··· 338 511 requiresBuild: true 339 512 dev: false 340 513 optional: true 514 + 515 + /@cspotcode/source-map-support@0.8.1: 516 + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 517 + engines: {node: '>=12'} 518 + dependencies: 519 + '@jridgewell/trace-mapping': 0.3.9 520 + dev: true 341 521 342 522 /@esbuild/aix-ppc64@0.21.5: 343 523 resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} ··· 819 999 '@jridgewell/sourcemap-codec': 1.5.0 820 1000 dev: true 821 1001 1002 + /@jridgewell/trace-mapping@0.3.9: 1003 + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 1004 + dependencies: 1005 + '@jridgewell/resolve-uri': 3.1.2 1006 + '@jridgewell/sourcemap-codec': 1.5.0 1007 + dev: true 1008 + 822 1009 /@noble/curves@1.4.2: 823 1010 resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==} 824 1011 dependencies: ··· 856 1043 engines: {node: '>=14'} 857 1044 requiresBuild: true 858 1045 dev: true 1046 + optional: true 1047 + 1048 + /@preact/signals-core@1.8.0: 1049 + resolution: {integrity: sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==} 1050 + requiresBuild: true 1051 + dev: false 859 1052 optional: true 860 1053 861 1054 /@rollup/rollup-android-arm-eabi@4.20.0: ··· 995 1188 path-browserify: 1.0.1 996 1189 dev: true 997 1190 1191 + /@tsconfig/node10@1.0.11: 1192 + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} 1193 + dev: true 1194 + 1195 + /@tsconfig/node12@1.0.11: 1196 + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} 1197 + dev: true 1198 + 1199 + /@tsconfig/node14@1.0.3: 1200 + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} 1201 + dev: true 1202 + 1203 + /@tsconfig/node16@1.0.4: 1204 + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} 1205 + dev: true 1206 + 998 1207 /@types/better-sqlite3@7.6.11: 999 1208 resolution: {integrity: sha512-i8KcD3PgGtGBLl3+mMYA8PdKkButvPyARxA7IQAd6qeslht13qxb1zzO8dRCtE7U3IoJS782zDBAeoKiM695kg==} 1000 1209 dependencies: ··· 1124 1333 tinyrainbow: 1.2.0 1125 1334 dev: true 1126 1335 1336 + /@webreflection/signal@2.1.2: 1337 + resolution: {integrity: sha512-0dW0fstQQkIt588JwhDiPS4xgeeQcQnBHn6MVInrBzmFlnLtzoSJL9G7JqdAlZVVi19tfb8R1QisZIT31cgiug==} 1338 + requiresBuild: true 1339 + dev: false 1340 + optional: true 1341 + 1342 + /@webreflection/uparser@0.3.3: 1343 + resolution: {integrity: sha512-XxGfo8jr2eVuvP5lrmwjgMAM7QjtZ0ngFD+dd9Fd3GStcEb4QhLlTiqZYF5O3l5k4sU/V6ZiPrVCzCWXWFEmCw==} 1344 + dependencies: 1345 + domconstants: 1.1.6 1346 + dev: false 1347 + 1127 1348 /abort-controller@3.0.0: 1128 1349 resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} 1129 1350 engines: {node: '>=6.5'} ··· 1138 1359 negotiator: 0.6.3 1139 1360 dev: false 1140 1361 1362 + /acorn-walk@8.3.3: 1363 + resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} 1364 + engines: {node: '>=0.4.0'} 1365 + dependencies: 1366 + acorn: 8.12.1 1367 + dev: true 1368 + 1369 + /acorn@8.12.1: 1370 + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} 1371 + engines: {node: '>=0.4.0'} 1372 + hasBin: true 1373 + dev: true 1374 + 1141 1375 /ansi-escapes@7.0.0: 1142 1376 resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} 1143 1377 engines: {node: '>=18'} ··· 1179 1413 picomatch: 2.3.1 1180 1414 dev: true 1181 1415 1416 + /arg@4.1.3: 1417 + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} 1418 + dev: true 1419 + 1182 1420 /array-flatten@1.1.1: 1183 1421 resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} 1184 1422 dev: false ··· 1204 1442 /atomic-sleep@1.0.0: 1205 1443 resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} 1206 1444 engines: {node: '>=8.0.0'} 1445 + 1446 + /await-lock@2.2.2: 1447 + resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} 1448 + dev: false 1207 1449 1208 1450 /balanced-match@1.0.2: 1209 1451 resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} ··· 1488 1730 vary: 1.1.2 1489 1731 dev: false 1490 1732 1733 + /create-require@1.1.1: 1734 + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} 1735 + dev: true 1736 + 1491 1737 /cross-spawn@7.0.3: 1492 1738 resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 1493 1739 engines: {node: '>= 8'} ··· 1496 1742 shebang-command: 2.0.0 1497 1743 which: 2.0.2 1498 1744 dev: true 1745 + 1746 + /custom-function@1.0.6: 1747 + resolution: {integrity: sha512-styyvwOki/EYr+VBe7/m9xAjq6uKx87SpDKIpFRdTQnofBDSZpBEFc9qJLmaJihjjTeEpAIJ+nz+9fUXj+BPNQ==} 1748 + dev: false 1499 1749 1500 1750 /dateformat@4.6.3: 1501 1751 resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} ··· 1576 1826 wrappy: 1.0.2 1577 1827 dev: true 1578 1828 1829 + /diff@4.0.2: 1830 + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} 1831 + engines: {node: '>=0.3.1'} 1832 + dev: true 1833 + 1579 1834 /dir-glob@3.0.1: 1580 1835 resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 1581 1836 engines: {node: '>=8'} ··· 1583 1838 path-type: 4.0.0 1584 1839 dev: true 1585 1840 1841 + /dom-serializer@2.0.0: 1842 + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} 1843 + dependencies: 1844 + domelementtype: 2.3.0 1845 + domhandler: 5.0.3 1846 + entities: 4.5.0 1847 + dev: false 1848 + 1849 + /domconstants@1.1.6: 1850 + resolution: {integrity: sha512-CuaDrThJ4VM+LyZ4ax8n52k0KbLJZtffyGkuj1WhpTRRcSfcy/9DfOBa68jenhX96oNUTunblSJEUNC4baFdmQ==} 1851 + dev: false 1852 + 1853 + /domelementtype@2.3.0: 1854 + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} 1855 + dev: false 1856 + 1857 + /domhandler@5.0.3: 1858 + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} 1859 + engines: {node: '>= 4'} 1860 + dependencies: 1861 + domelementtype: 2.3.0 1862 + dev: false 1863 + 1864 + /domutils@3.1.0: 1865 + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} 1866 + dependencies: 1867 + dom-serializer: 2.0.0 1868 + domelementtype: 2.3.0 1869 + domhandler: 5.0.3 1870 + dev: false 1871 + 1586 1872 /dotenv@16.4.5: 1587 1873 resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} 1588 1874 engines: {node: '>=12'} ··· 1617 1903 resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} 1618 1904 dependencies: 1619 1905 once: 1.4.0 1906 + 1907 + /entities@4.5.0: 1908 + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 1909 + engines: {node: '>=0.12'} 1910 + dev: false 1620 1911 1621 1912 /envalid@8.0.0: 1622 1913 resolution: {integrity: sha512-PGeYJnJB5naN0ME6SH8nFcDj9HVbLpYIfg1p5lAyM9T4cH2lwtu2fLbozC/bq+HUUOIFxhX/LP0/GmlqPHT4tQ==} ··· 1919 2210 /function-bind@1.1.2: 1920 2211 resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 1921 2212 2213 + /gc-hook@0.3.1: 2214 + resolution: {integrity: sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A==} 2215 + dev: false 2216 + 1922 2217 /get-caller-file@2.0.5: 1923 2218 resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 1924 2219 engines: {node: 6.* || 8.* || >= 10.*} ··· 2044 2339 engines: {node: '>=8'} 2045 2340 dev: true 2046 2341 2342 + /html-escaper@3.0.3: 2343 + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} 2344 + dev: false 2345 + 2346 + /htmlparser2@9.1.0: 2347 + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} 2348 + dependencies: 2349 + domelementtype: 2.3.0 2350 + domhandler: 5.0.3 2351 + domutils: 3.1.0 2352 + entities: 4.5.0 2353 + dev: false 2354 + 2047 2355 /http-errors@2.0.0: 2048 2356 resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} 2049 2357 engines: {node: '>= 0.8'} ··· 2097 2405 engines: {node: '>= 0.10'} 2098 2406 dev: false 2099 2407 2408 + /ipaddr.js@2.2.0: 2409 + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} 2410 + engines: {node: '>= 10'} 2411 + dev: false 2412 + 2413 + /iron-session@8.0.2: 2414 + resolution: {integrity: sha512-p4Yf1moQr6gnCcXu5vCaxVKRKDmR9PZcQDfp7ZOgbsSHUsgaNti6OgDB2BdgxC2aS6V/6Hu4O0wYlj92sbdIJg==} 2415 + dependencies: 2416 + cookie: 0.6.0 2417 + iron-webcrypto: 1.2.1 2418 + uncrypto: 0.1.3 2419 + dev: false 2420 + 2421 + /iron-webcrypto@1.2.1: 2422 + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} 2423 + dev: false 2424 + 2100 2425 /is-binary-path@2.1.0: 2101 2426 resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 2102 2427 engines: {node: '>=8'} ··· 2163 2488 '@pkgjs/parseargs': 0.11.0 2164 2489 dev: true 2165 2490 2491 + /jose@5.6.3: 2492 + resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==} 2493 + dev: false 2494 + 2166 2495 /joycon@3.1.1: 2167 2496 resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} 2168 2497 engines: {node: '>=10'} ··· 2241 2570 2242 2571 /lru-cache@10.4.3: 2243 2572 resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 2244 - dev: true 2245 2573 2246 2574 /magic-string@0.30.11: 2247 2575 resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} ··· 2249 2577 '@jridgewell/sourcemap-codec': 1.5.0 2250 2578 dev: true 2251 2579 2580 + /make-error@1.3.6: 2581 + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} 2582 + dev: true 2583 + 2252 2584 /media-typer@0.3.0: 2253 2585 resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} 2254 2586 engines: {node: '>= 0.6'} ··· 2691 3023 dependencies: 2692 3024 forwarded: 0.2.0 2693 3025 ipaddr.js: 1.9.1 3026 + dev: false 3027 + 3028 + /psl@1.9.0: 3029 + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} 2694 3030 dev: false 2695 3031 2696 3032 /pump@3.0.0: ··· 3211 3547 engines: {node: '>=14.0.0'} 3212 3548 dev: true 3213 3549 3550 + /tlds@1.254.0: 3551 + resolution: {integrity: sha512-YY4ei7K7gPGifqNSrfMaPdqTqiHcwYKUJ7zhLqQOK2ildlGgti5TSwJiXXN1YqG17I2GYZh5cZqv2r5fwBUM+w==} 3552 + hasBin: true 3553 + dev: false 3554 + 3214 3555 /to-regex-range@5.0.1: 3215 3556 resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 3216 3557 engines: {node: '>=8.0'} ··· 3245 3586 code-block-writer: 11.0.3 3246 3587 dev: true 3247 3588 3589 + /ts-node@10.9.2(@types/node@22.1.0)(typescript@5.5.4): 3590 + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} 3591 + hasBin: true 3592 + peerDependencies: 3593 + '@swc/core': '>=1.2.50' 3594 + '@swc/wasm': '>=1.2.50' 3595 + '@types/node': '*' 3596 + typescript: '>=2.7' 3597 + peerDependenciesMeta: 3598 + '@swc/core': 3599 + optional: true 3600 + '@swc/wasm': 3601 + optional: true 3602 + dependencies: 3603 + '@cspotcode/source-map-support': 0.8.1 3604 + '@tsconfig/node10': 1.0.11 3605 + '@tsconfig/node12': 1.0.11 3606 + '@tsconfig/node14': 1.0.3 3607 + '@tsconfig/node16': 1.0.4 3608 + '@types/node': 22.1.0 3609 + acorn: 8.12.1 3610 + acorn-walk: 8.3.3 3611 + arg: 4.1.3 3612 + create-require: 1.1.1 3613 + diff: 4.0.2 3614 + make-error: 1.3.6 3615 + typescript: 5.5.4 3616 + v8-compile-cache-lib: 3.0.1 3617 + yn: 3.1.1 3618 + dev: true 3619 + 3248 3620 /tsconfck@3.1.1(typescript@5.5.4): 3249 3621 resolution: {integrity: sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==} 3250 3622 engines: {node: ^18 || >=20} ··· 3336 3708 hasBin: true 3337 3709 dev: true 3338 3710 3711 + /udomdiff@1.1.0: 3712 + resolution: {integrity: sha512-aqjTs5x/wsShZBkVagdafJkP8S3UMGhkHKszsu1cszjjZ7iOp86+Qb3QOFYh01oWjPMy5ZTuxD6hw5uTKxd+VA==} 3713 + dev: false 3714 + 3715 + /uhtml@4.5.9: 3716 + resolution: {integrity: sha512-WAfIK/E3ZJpaFl0MSzGSB54r7I8Vc8ZyUlOsN8GnLnEaxuioOUyKAS6q/N/xQ5GD9vFFBnx6q+3N3Eq9KNCvTQ==} 3717 + dependencies: 3718 + '@webreflection/uparser': 0.3.3 3719 + custom-function: 1.0.6 3720 + domconstants: 1.1.6 3721 + gc-hook: 0.3.1 3722 + html-escaper: 3.0.3 3723 + htmlparser2: 9.1.0 3724 + udomdiff: 1.1.0 3725 + optionalDependencies: 3726 + '@preact/signals-core': 1.8.0 3727 + '@webreflection/signal': 2.1.2 3728 + dev: false 3729 + 3339 3730 /uint8arrays@3.0.0: 3340 3731 resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==} 3341 3732 dependencies: 3342 3733 multiformats: 9.9.0 3734 + 3735 + /uncrypto@0.1.3: 3736 + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} 3737 + dev: false 3343 3738 3344 3739 /undici-types@6.13.0: 3345 3740 resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==} 3346 3741 dev: true 3347 3742 3743 + /undici@6.19.5: 3744 + resolution: {integrity: sha512-LryC15SWzqQsREHIOUybavaIHF5IoL0dJ9aWWxL/PgT1KfqAW5225FZpDUFlt9xiDMS2/S7DOKhFWA7RLksWdg==} 3745 + engines: {node: '>=18.17'} 3746 + dev: false 3747 + 3348 3748 /unpipe@1.0.0: 3349 3749 resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} 3350 3750 engines: {node: '>= 0.8'} ··· 3359 3759 engines: {node: '>= 0.4.0'} 3360 3760 dev: false 3361 3761 3762 + /v8-compile-cache-lib@3.0.1: 3763 + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} 3764 + dev: true 3765 + 3362 3766 /varint@6.0.0: 3363 3767 resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} 3364 3768 dev: false ··· 3368 3772 engines: {node: '>= 0.8'} 3369 3773 dev: false 3370 3774 3371 - /vite-node@2.0.5: 3775 + /vite-node@2.0.5(@types/node@22.1.0): 3372 3776 resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} 3373 3777 engines: {node: ^18.0.0 || >=20.0.0} 3374 3778 hasBin: true ··· 3377 3781 debug: 4.3.6 3378 3782 pathe: 1.1.2 3379 3783 tinyrainbow: 1.2.0 3380 - vite: 5.3.5 3784 + vite: 5.3.5(@types/node@22.1.0) 3381 3785 transitivePeerDependencies: 3382 3786 - '@types/node' 3383 3787 - less ··· 3405 3809 - typescript 3406 3810 dev: true 3407 3811 3408 - /vite@5.3.5: 3812 + /vite@5.3.5(@types/node@22.1.0): 3409 3813 resolution: {integrity: sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==} 3410 3814 engines: {node: ^18.0.0 || >=20.0.0} 3411 3815 hasBin: true ··· 3433 3837 terser: 3434 3838 optional: true 3435 3839 dependencies: 3840 + '@types/node': 22.1.0 3436 3841 esbuild: 0.21.5 3437 3842 postcss: 8.4.41 3438 3843 rollup: 4.20.0 ··· 3440 3845 fsevents: 2.3.3 3441 3846 dev: true 3442 3847 3443 - /vitest@2.0.5: 3848 + /vitest@2.0.5(@types/node@22.1.0): 3444 3849 resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} 3445 3850 engines: {node: ^18.0.0 || >=20.0.0} 3446 3851 hasBin: true ··· 3466 3871 optional: true 3467 3872 dependencies: 3468 3873 '@ampproject/remapping': 2.3.0 3874 + '@types/node': 22.1.0 3469 3875 '@vitest/expect': 2.0.5 3470 3876 '@vitest/pretty-format': 2.0.5 3471 3877 '@vitest/runner': 2.0.5 ··· 3481 3887 tinybench: 2.9.0 3482 3888 tinypool: 1.0.0 3483 3889 tinyrainbow: 1.2.0 3484 - vite: 5.3.5 3485 - vite-node: 2.0.5 3890 + vite: 5.3.5(@types/node@22.1.0) 3891 + vite-node: 2.0.5(@types/node@22.1.0) 3486 3892 why-is-node-running: 2.3.0 3487 3893 transitivePeerDependencies: 3488 3894 - less ··· 3574 3980 3575 3981 /yesno@0.4.0: 3576 3982 resolution: {integrity: sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA==} 3983 + dev: true 3984 + 3985 + /yn@3.1.1: 3986 + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} 3987 + engines: {node: '>=6'} 3577 3988 dev: true 3578 3989 3579 3990 /zod@3.23.8:
+28
src/auth/client.ts
··· 1 + import { JoseKey } from '@atproto/jwk-jose' 2 + import { NodeOAuthClient } from '@atproto/oauth-client-node' 3 + import type { Database } from '#/db' 4 + import { env } from '#/env' 5 + import { SessionStore, StateStore } from './storage' 6 + 7 + export const createClient = async (db: Database) => { 8 + const publicUrl = env.PUBLIC_URL 9 + const url = publicUrl || `http://127.0.0.1:${env.PORT}` 10 + return new NodeOAuthClient({ 11 + clientMetadata: { 12 + client_name: 'AT Protocol Express App', 13 + client_id: publicUrl 14 + ? `${url}/client-metadata.json` 15 + : `http://localhost?redirect_uri=${encodeURIComponent(`${url}/oauth/callback`)}`, 16 + client_uri: url, 17 + redirect_uris: [`${url}/oauth/callback`], 18 + scope: 'profile offline_access', 19 + grant_types: ['authorization_code', 'refresh_token'], 20 + response_types: ['code'], 21 + application_type: 'web', 22 + token_endpoint_auth_method: 'none', 23 + dpop_bound_access_tokens: true, 24 + }, 25 + stateStore: new StateStore(db), 26 + sessionStore: new SessionStore(db), 27 + }) 28 + }
+35
src/auth/session.ts
··· 1 + 'use server' 2 + 3 + import assert from 'node:assert' 4 + import type { IncomingMessage, ServerResponse } from 'node:http' 5 + import { getIronSession } from 'iron-session' 6 + import { env } from '#/env' 7 + 8 + export type Session = { did: string } 9 + 10 + export async function createSession(req: IncomingMessage, res: ServerResponse<IncomingMessage>, did: string) { 11 + const session = await getSessionRaw(req, res) 12 + assert(!session.did, 'session already exists') 13 + session.did = did 14 + await session.save() 15 + return { did: session.did } 16 + } 17 + 18 + export async function destroySession(req: IncomingMessage, res: ServerResponse<IncomingMessage>) { 19 + const session = await getSessionRaw(req, res) 20 + await session.destroy() 21 + return null 22 + } 23 + 24 + export async function getSession(req: IncomingMessage, res: ServerResponse<IncomingMessage>) { 25 + const session = await getSessionRaw(req, res) 26 + if (!session.did) return null 27 + return { did: session.did } 28 + } 29 + 30 + async function getSessionRaw(req: IncomingMessage, res: ServerResponse<IncomingMessage>) { 31 + return await getIronSession<Session>(req, res, { 32 + cookieName: 'sid', 33 + password: env.COOKIE_SECRET, 34 + }) 35 + }
+47
src/auth/storage.ts
··· 1 + import type { 2 + NodeSavedSession, 3 + NodeSavedSessionStore, 4 + NodeSavedState, 5 + NodeSavedStateStore, 6 + } from '@atproto/oauth-client-node' 7 + import type { Database } from '#/db' 8 + 9 + export class StateStore implements NodeSavedStateStore { 10 + constructor(private db: Database) {} 11 + async get(key: string): Promise<NodeSavedState | undefined> { 12 + const result = await this.db.selectFrom('auth_state').selectAll().where('key', '=', key).executeTakeFirst() 13 + if (!result) return 14 + return JSON.parse(result.state) as NodeSavedState 15 + } 16 + async set(key: string, val: NodeSavedState) { 17 + const state = JSON.stringify(val) 18 + await this.db 19 + .insertInto('auth_state') 20 + .values({ key, state }) 21 + .onConflict((oc) => oc.doUpdateSet({ state })) 22 + .execute() 23 + } 24 + async del(key: string) { 25 + await this.db.deleteFrom('auth_state').where('key', '=', key).execute() 26 + } 27 + } 28 + 29 + export class SessionStore implements NodeSavedSessionStore { 30 + constructor(private db: Database) {} 31 + async get(key: string): Promise<NodeSavedSession | undefined> { 32 + const result = await this.db.selectFrom('auth_session').selectAll().where('key', '=', key).executeTakeFirst() 33 + if (!result) return 34 + return JSON.parse(result.session) as NodeSavedSession 35 + } 36 + async set(key: string, val: NodeSavedSession) { 37 + const session = JSON.stringify(val) 38 + await this.db 39 + .insertInto('auth_session') 40 + .values({ key, session }) 41 + .onConflict((oc) => oc.doUpdateSet({ session })) 42 + .execute() 43 + } 44 + async del(key: string) { 45 + await this.db.deleteFrom('auth_session').where('key', '=', key).execute() 46 + } 47 + }
+2
src/config.ts
··· 1 + import type { OAuthClient } from '@atproto/oauth-client-node' 1 2 import type pino from 'pino' 2 3 import type { Database } from '#/db' 3 4 import type { Ingester } from '#/firehose/ingester' ··· 6 7 db: Database 7 8 ingester: Ingester 8 9 logger: pino.Logger 10 + oauthClient: OAuthClient 9 11 }
+12
src/db/migrations.ts
··· 16 16 .addColumn('text', 'varchar', (col) => col.notNull()) 17 17 .addColumn('indexedAt', 'varchar', (col) => col.notNull()) 18 18 .execute() 19 + await db.schema 20 + .createTable('auth_session') 21 + .addColumn('key', 'varchar', (col) => col.primaryKey()) 22 + .addColumn('session', 'varchar', (col) => col.notNull()) 23 + .execute() 24 + await db.schema 25 + .createTable('auth_state') 26 + .addColumn('key', 'varchar', (col) => col.primaryKey()) 27 + .addColumn('state', 'varchar', (col) => col.notNull()) 28 + .execute() 19 29 }, 20 30 async down(db: Kysely<unknown>) { 31 + await db.schema.dropTable('auth_state').execute() 32 + await db.schema.dropTable('auth_session').execute() 21 33 await db.schema.dropTable('post').execute() 22 34 }, 23 35 }
+16
src/db/schema.ts
··· 1 1 export type DatabaseSchema = { 2 2 post: Post 3 + auth_session: AuthSession 4 + auth_state: AuthState 3 5 } 4 6 5 7 export type Post = { ··· 7 9 text: string 8 10 indexedAt: string 9 11 } 12 + 13 + export type AuthSession = { 14 + key: string 15 + session: AuthSessionJson 16 + } 17 + 18 + export type AuthState = { 19 + key: string 20 + state: AuthStateJson 21 + } 22 + 23 + type AuthStateJson = string 24 + 25 + type AuthSessionJson = string
+2
src/env.ts
··· 7 7 NODE_ENV: str({ devDefault: testOnly('test'), choices: ['development', 'production', 'test'] }), 8 8 HOST: host({ devDefault: testOnly('localhost') }), 9 9 PORT: port({ devDefault: testOnly(3000) }), 10 + PUBLIC_URL: str({}), 11 + COOKIE_SECRET: str({ devDefault: '00000000000000000000000000000000' }), 10 12 CORS_ORIGIN: str({ devDefault: testOnly('http://localhost:3000') }), 11 13 COMMON_RATE_LIMIT_MAX_REQUESTS: num({ devDefault: testOnly(1000) }), 12 14 COMMON_RATE_LIMIT_WINDOW_MS: num({ devDefault: testOnly(1000) }),
+1
src/firehose/ingester.ts
··· 19 19 text: post.text as string, 20 20 indexedAt: new Date().toISOString(), 21 21 }) 22 + .onConflict((oc) => oc.doNothing()) 22 23 .execute() 23 24 } 24 25 }
+46
src/pages/home.ts
··· 1 + import { AtUri } from '@atproto/syntax' 2 + import type { Post } from '#/db/schema' 3 + import { html } from '../view' 4 + import { shell } from './shell' 5 + 6 + type Props = { posts: Post[]; profile?: { displayName?: string; handle: string } } 7 + 8 + export function home(props: Props) { 9 + return shell({ 10 + title: 'Home', 11 + content: content(props), 12 + }) 13 + } 14 + 15 + function content({ posts, profile }: Props) { 16 + return html`<div id="root"> 17 + <h1>Welcome to the Atmosphere</h1> 18 + ${ 19 + profile 20 + ? html`<form action="/logout" method="post"> 21 + <p> 22 + Hi, <b>${profile.displayName || profile.handle}</b>. It's pretty special here. 23 + <button type="submit">Log out.</button> 24 + </p> 25 + </form>` 26 + : html`<p> 27 + It's pretty special here. 28 + <a href="/login">Log in.</a> 29 + </p>` 30 + } 31 + <ul> 32 + ${posts.map((post) => { 33 + return html`<li> 34 + <a href="${toBskyLink(post.uri)}" target="_blank">🔗</a> 35 + ${post.text} 36 + </li>` 37 + })} 38 + </ul> 39 + <a href="/">Give me more</a> 40 + </div>` 41 + } 42 + 43 + function toBskyLink(uriStr: string) { 44 + const uri = new AtUri(uriStr) 45 + return `https://bsky.app/profile/${uri.host}/post/${uri.rkey}` 46 + }
+21
src/pages/login.ts
··· 1 + import { html } from '../view' 2 + import { shell } from './shell' 3 + 4 + type Props = { error?: string } 5 + 6 + export function login(props: Props) { 7 + return shell({ 8 + title: 'Log in', 9 + content: content(props), 10 + }) 11 + } 12 + 13 + function content({ error }: Props) { 14 + return html`<div id="root"> 15 + <form action="/login" method="post"> 16 + <input type="text" name="handle" placeholder="handle" required /> 17 + <button type="submit">Log in.</button> 18 + ${error ? html`<p>Error: <i>${error}</i></p>` : undefined} 19 + </form> 20 + </div>` 21 + }
+13
src/pages/shell.ts
··· 1 + import { type Hole, html } from '../view' 2 + 3 + export function shell({ title, content }: { title: string; content: Hole }) { 4 + return html`<html> 5 + <head> 6 + <title>${title}</title> 7 + <link rel="stylesheet" href="/public/styles.css"> 8 + </head> 9 + <body> 10 + ${content} 11 + </body> 12 + </html>` 13 + }
+35
src/public/styles.css
··· 1 + body { 2 + font-family: Arial, Helvetica, sans-serif; 3 + } 4 + 5 + #root { 6 + padding: 20px; 7 + } 8 + 9 + /* 10 + Josh's Custom CSS Reset 11 + https://www.joshwcomeau.com/css/custom-css-reset/ 12 + */ 13 + *, *::before, *::after { 14 + box-sizing: border-box; 15 + } 16 + * { 17 + margin: 0; 18 + } 19 + body { 20 + line-height: 1.5; 21 + -webkit-font-smoothing: antialiased; 22 + } 23 + img, picture, video, canvas, svg { 24 + display: block; 25 + max-width: 100%; 26 + } 27 + input, button, textarea, select { 28 + font: inherit; 29 + } 30 + p, h1, h2, h3, h4, h5, h6 { 31 + overflow-wrap: break-word; 32 + } 33 + #root, #__next { 34 + isolation: isolate; 35 + }
+82 -2
src/routes/index.ts
··· 1 + import path from 'node:path' 2 + import { OAuthResolverError } from '@atproto/oauth-client-node' 3 + import { isValidHandle } from '@atproto/syntax' 1 4 import express from 'express' 5 + import { createSession, destroySession, getSession } from '#/auth/session' 2 6 import type { AppContext } from '#/config' 7 + import { home } from '#/pages/home' 8 + import { login } from '#/pages/login' 9 + import { page } from '#/view' 3 10 import { handler } from './util' 4 11 5 12 export const createRouter = (ctx: AppContext) => { 6 13 const router = express.Router() 14 + 15 + router.use('/public', express.static(path.join(__dirname, '..', 'public'))) 16 + 17 + router.get( 18 + '/client-metadata.json', 19 + handler((_req, res) => { 20 + return res.json(ctx.oauthClient.clientMetadata) 21 + }), 22 + ) 23 + 24 + router.get( 25 + '/oauth/callback', 26 + handler(async (req, res) => { 27 + const params = new URLSearchParams(req.originalUrl.split('?')[1]) 28 + try { 29 + const { agent } = await ctx.oauthClient.callback(params) 30 + await createSession(req, res, agent.accountDid) 31 + } catch (err) { 32 + ctx.logger.error({ err }, 'oauth callback failed') 33 + return res.redirect('/?error') 34 + } 35 + return res.redirect('/') 36 + }), 37 + ) 38 + 39 + router.get( 40 + '/login', 41 + handler(async (_req, res) => { 42 + return res.type('html').send(page(login({}))) 43 + }), 44 + ) 45 + 46 + router.post( 47 + '/login', 48 + handler(async (req, res) => { 49 + const handle = req.body?.handle 50 + if (typeof handle !== 'string' || !isValidHandle(handle)) { 51 + return res.type('html').send(page(login({ error: 'invalid handle' }))) 52 + } 53 + try { 54 + const url = await ctx.oauthClient.authorize(handle) 55 + return res.redirect(url.toString()) 56 + } catch (err) { 57 + ctx.logger.error({ err }, 'oauth authorize failed') 58 + return res.type('html').send( 59 + page( 60 + login({ 61 + error: err instanceof OAuthResolverError ? err.message : "couldn't initiate login", 62 + }), 63 + ), 64 + ) 65 + } 66 + }), 67 + ) 68 + 69 + router.post( 70 + '/logout', 71 + handler(async (req, res) => { 72 + await destroySession(req, res) 73 + return res.redirect('/') 74 + }), 75 + ) 7 76 8 77 router.get( 9 78 '/', 10 79 handler(async (req, res) => { 80 + const session = await getSession(req, res) 81 + const agent = 82 + session && 83 + (await ctx.oauthClient.restore(session.did).catch(async (err) => { 84 + ctx.logger.warn({ err }, 'oauth restore failed') 85 + await destroySession(req, res) 86 + return null 87 + })) 11 88 const posts = await ctx.db.selectFrom('post').selectAll().orderBy('indexedAt', 'desc').limit(10).execute() 12 - const postTexts = posts.map((row) => row.text) 13 - res.json(postTexts) 89 + if (!agent) { 90 + return res.type('html').send(page(home({ posts }))) 91 + } 92 + const { data: profile } = await agent.getProfile({ actor: session.did }) 93 + return res.type('html').send(page(home({ posts, profile }))) 14 94 }), 15 95 ) 16 96
+13 -1
src/server.ts
··· 11 11 import errorHandler from '#/middleware/errorHandler' 12 12 import requestLogger from '#/middleware/requestLogger' 13 13 import { createRouter } from '#/routes' 14 + import { createClient } from './auth/client' 14 15 import type { AppContext } from './config' 15 16 16 17 export class Server { ··· 27 28 const db = createDb(':memory:') 28 29 await migrateToLatest(db) 29 30 const ingester = new Ingester(db) 31 + const oauthClient = await createClient(db) 30 32 ingester.start() 31 33 const ctx = { 32 34 db, 33 35 ingester, 34 36 logger, 37 + oauthClient, 35 38 } 36 39 37 40 const app: Express = express() ··· 46 49 app.use(express.json()) 47 50 app.use(express.urlencoded({ extended: true })) 48 51 app.use(cors({ origin: env.CORS_ORIGIN, credentials: true })) 49 - app.use(helmet()) 52 + app.use( 53 + helmet({ 54 + contentSecurityPolicy: { 55 + directives: { 56 + // allow oauth redirect when submitting login form 57 + formAction: null, 58 + }, 59 + }, 60 + }), 61 + ) 50 62 51 63 // Request logging 52 64 app.use(requestLogger)
+12
src/view.ts
··· 1 + // @ts-ignore 2 + import ssr from 'uhtml/ssr' 3 + import type initSSR from 'uhtml/types/init-ssr' 4 + import type { Hole } from 'uhtml/types/keyed' 5 + 6 + export type { Hole } 7 + 8 + export const { html }: ReturnType<typeof initSSR> = ssr() 9 + 10 + export function page(hole: Hole) { 11 + return `<!DOCTYPE html>\n${hole.toDOM().toString()}` 12 + }
+1 -1
tsconfig.json
··· 6 6 "paths": { 7 7 "#/*": ["src/*"] 8 8 }, 9 - "moduleResolution": "Node", 9 + "moduleResolution": "Node10", 10 10 "outDir": "dist", 11 11 "importsNotUsedAsValues": "remove", 12 12 "strict": true,