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

Configure Feed

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

setup oauth client, login ui and endpoints

+536 -27
+7
.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="https://localhost:8080" 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 + # openssl rand -base64 33 16 + COOKIE_SECRET="" 17 + # openssl ecparam -name prime256v1 -genkey | openssl pkcs8 -topk8 -nocrypt | openssl base64 -A 18 + PRIVATE_KEY_ES256_B64=""
+6 -3
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",
+226 -17
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 ··· 114 123 '@jridgewell/trace-mapping': 0.3.25 115 124 dev: true 116 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 + 117 207 /@atproto/common-web@0.3.0: 118 208 resolution: {integrity: sha512-67VnV6JJyX+ZWyjV7xFQMypAgDmjVaR9ZCuU/QW+mqlqI7fex2uL4Fv+7/jHadgzhuJHVd6OHOvNn0wR5WZYtA==} 119 209 dependencies: ··· 141 231 uint8arrays: 3.0.0 142 232 dev: false 143 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 + 144 261 /@atproto/lex-cli@0.4.1: 145 262 resolution: {integrity: sha512-QP9mE8MYzXR2ydhCBb/mtGqKZjqpffqcpZCr7JM4mFOZPvXV8k7OqVP1h+T94JB/tGcGPhB750S6tqUH9VRLVg==} 146 263 hasBin: true ··· 163 280 iso-datestring-validator: 2.2.2 164 281 multiformats: 9.9.0 165 282 zod: 3.23.8 283 + dev: true 166 284 167 - /@atproto/repo@0.4.1: 168 - 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==} 169 336 dependencies: 170 337 '@atproto/common': 0.4.1 171 338 '@atproto/common-web': 0.3.0 172 339 '@atproto/crypto': 0.4.0 173 - '@atproto/lexicon': 0.4.0 340 + '@atproto/lexicon': 0.4.1-rc.0 174 341 '@ipld/car': 3.2.4 175 342 '@ipld/dag-cbor': 7.0.3 176 343 multiformats: 9.9.0 ··· 181 348 /@atproto/syntax@0.3.0: 182 349 resolution: {integrity: sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==} 183 350 184 - /@atproto/xrpc-server@0.5.3: 185 - resolution: {integrity: sha512-Gxe5dPDp7mj7E1JaK0yEwGuWot78/HjszHYakqleKp+IXlM+iZxH0N20O+x7b3g7itImuQ2LzH3Zk1jLB0yZjQ==} 351 + /@atproto/xrpc-server@0.5.4-rc.0: 352 + resolution: {integrity: sha512-Vrx1gEoZfJtYoZhSxkbWQsU2r0DuJO/BuvMQGw9Nd66owmF5nPDVvYVd0pJhIDoaSxImTTIEeDWlNNl3WCSBPA==} 186 353 dependencies: 187 354 '@atproto/common': 0.4.1 188 355 '@atproto/crypto': 0.4.0 189 - '@atproto/lexicon': 0.4.0 190 - '@atproto/xrpc': 0.5.0 356 + '@atproto/lexicon': 0.4.1-rc.0 357 + '@atproto/xrpc': 0.6.0-rc.0 191 358 cbor-x: 1.6.0 192 359 express: 4.19.2 193 360 http-errors: 2.0.0 ··· 202 369 - utf-8-validate 203 370 dev: false 204 371 205 - /@atproto/xrpc@0.5.0: 206 - resolution: {integrity: sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==} 372 + /@atproto/xrpc@0.6.0-rc.0: 373 + resolution: {integrity: sha512-TOmynXvbA57Y6KR050UeiDfdzQoAnmgB0zu0qrvhYiu7oeg64fYzvOa7stWxSIP1nhrGqgexxICR1CnOnCEHjg==} 207 374 dependencies: 208 - '@atproto/lexicon': 0.4.0 375 + '@atproto/lexicon': 0.4.1-rc.0 209 376 zod: 3.23.8 210 377 dev: false 211 378 ··· 1276 1443 resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} 1277 1444 engines: {node: '>=8.0.0'} 1278 1445 1446 + /await-lock@2.2.2: 1447 + resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} 1448 + dev: false 1449 + 1279 1450 /balanced-match@1.0.2: 1280 1451 resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 1281 1452 dev: true ··· 2234 2405 engines: {node: '>= 0.10'} 2235 2406 dev: false 2236 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 + 2237 2425 /is-binary-path@2.1.0: 2238 2426 resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 2239 2427 engines: {node: '>=8'} ··· 2299 2487 optionalDependencies: 2300 2488 '@pkgjs/parseargs': 0.11.0 2301 2489 dev: true 2490 + 2491 + /jose@5.6.3: 2492 + resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==} 2493 + dev: false 2302 2494 2303 2495 /joycon@3.1.1: 2304 2496 resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} ··· 2378 2570 2379 2571 /lru-cache@10.4.3: 2380 2572 resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 2381 - dev: true 2382 2573 2383 2574 /magic-string@0.30.11: 2384 2575 resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} ··· 2834 3025 ipaddr.js: 1.9.1 2835 3026 dev: false 2836 3027 3028 + /psl@1.9.0: 3029 + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} 3030 + dev: false 3031 + 2837 3032 /pump@3.0.0: 2838 3033 resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} 2839 3034 dependencies: ··· 3352 3547 engines: {node: '>=14.0.0'} 3353 3548 dev: true 3354 3549 3550 + /tlds@1.254.0: 3551 + resolution: {integrity: sha512-YY4ei7K7gPGifqNSrfMaPdqTqiHcwYKUJ7zhLqQOK2ildlGgti5TSwJiXXN1YqG17I2GYZh5cZqv2r5fwBUM+w==} 3552 + hasBin: true 3553 + dev: false 3554 + 3355 3555 /to-regex-range@5.0.1: 3356 3556 resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 3357 3557 engines: {node: '>=8.0'} ··· 3532 3732 dependencies: 3533 3733 multiformats: 9.9.0 3534 3734 3735 + /uncrypto@0.1.3: 3736 + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} 3737 + dev: false 3738 + 3535 3739 /undici-types@6.13.0: 3536 3740 resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==} 3537 3741 dev: true 3742 + 3743 + /undici@6.19.5: 3744 + resolution: {integrity: sha512-LryC15SWzqQsREHIOUybavaIHF5IoL0dJ9aWWxL/PgT1KfqAW5225FZpDUFlt9xiDMS2/S7DOKhFWA7RLksWdg==} 3745 + engines: {node: '>=18.17'} 3746 + dev: false 3538 3747 3539 3748 /unpipe@1.0.0: 3540 3749 resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
+43
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 url = env.PUBLIC_URL 9 + const privateKeyPKCS8 = Buffer.from(env.PRIVATE_KEY_ES256_B64, 'base64').toString() 10 + const privateKey = await JoseKey.fromImportable(privateKeyPKCS8, 'key1') 11 + return new NodeOAuthClient({ 12 + // This object will be used to build the payload of the /client-metadata.json 13 + // endpoint metadata, exposing the client metadata to the OAuth server. 14 + clientMetadata: { 15 + // Must be a URL that will be exposing this metadata 16 + client_id: `${url}/client-metadata.json`, 17 + client_uri: url, 18 + client_name: 'ATProto Express App', 19 + jwks_uri: `${url}/jwks.json`, 20 + logo_uri: `${url}/logo.png`, 21 + tos_uri: `${url}/tos`, 22 + policy_uri: `${url}/policy`, 23 + redirect_uris: [`${url}/oauth/callback`], 24 + token_endpoint_auth_signing_alg: 'ES256', 25 + scope: 'profile email offline_access', 26 + grant_types: ['authorization_code', 'refresh_token'], 27 + response_types: ['code'], 28 + application_type: 'web', 29 + token_endpoint_auth_method: 'private_key_jwt', 30 + dpop_bound_access_tokens: true, 31 + }, 32 + 33 + // Used to authenticate the client to the token endpoint. Will be used to 34 + // build the jwks object to be exposed on the "jwks_uri" endpoint. 35 + keyset: [privateKey], 36 + 37 + // Interface to store authorization state data (during authorization flows) 38 + stateStore: new StateStore(db), 39 + 40 + // Interface to store authenticated session data 41 + sessionStore: new SessionStore(db), 42 + }) 43 + }
+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
+3
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({ devDefault: testOnly('http://localhost:3000') }), 11 + COOKIE_SECRET: str(), 12 + PRIVATE_KEY_ES256_B64: str(), 10 13 CORS_ORIGIN: str({ devDefault: testOnly('http://localhost:3000') }), 11 14 COMMON_RATE_LIMIT_MAX_REQUESTS: num({ devDefault: testOnly(1000) }), 12 15 COMMON_RATE_LIMIT_WINDOW_MS: num({ devDefault: testOnly(1000) }),
+19 -5
src/pages/home.ts
··· 3 3 import { html } from '../view' 4 4 import { shell } from './shell' 5 5 6 - export function home(posts: Post[]) { 6 + type Props = { posts: Post[]; profile?: { displayName?: string; handle: string } } 7 + 8 + export function home(props: Props) { 7 9 return shell({ 8 10 title: 'Home', 9 - content: content(posts), 11 + content: content(props), 10 12 }) 11 13 } 12 14 13 - function content(posts: Post[]) { 15 + function content({ posts, profile }: Props) { 14 16 return html`<div> 15 - <h1>Welcome to My Page</h1> 16 - <p>It's pretty special here.</p> 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">Login.</a> 29 + </p>` 30 + } 17 31 <ul> 18 32 ${posts.map((post) => { 19 33 return html`<li>
+23
src/pages/login.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 = { error?: string } 7 + 8 + export function login(props: Props) { 9 + return shell({ 10 + title: 'Login', 11 + content: content(props), 12 + }) 13 + } 14 + 15 + function content({ error }: Props) { 16 + return html`<div> 17 + <form action="/login" method="post"> 18 + <input type="text" name="handle" placeholder="handle" required /> 19 + <button type="submit">Log in.</button> 20 + ${error ? html`<p>Error: <i>${error}</i></p>` : undefined} 21 + </form> 22 + </div>` 23 + }
+84 -1
src/routes/index.ts
··· 1 + import { OAuthResolverError } from '@atproto/oauth-client-node' 2 + import { isValidHandle } from '@atproto/syntax' 1 3 import express from 'express' 4 + import { createSession, destroySession, getSession } from '#/auth/session' 2 5 import type { AppContext } from '#/config' 3 6 import { home } from '#/pages/home' 7 + import { login } from '#/pages/login' 4 8 import { page } from '#/view' 5 9 import { handler } from './util' 6 10 ··· 8 12 const router = express.Router() 9 13 10 14 router.get( 15 + '/jwks.json', 16 + handler((_req, res) => { 17 + return res.json(ctx.oauthClient.jwks) 18 + }), 19 + ) 20 + 21 + router.get( 22 + '/client-metadata.json', 23 + handler((_req, res) => { 24 + return res.json(ctx.oauthClient.clientMetadata) 25 + }), 26 + ) 27 + 28 + router.get( 29 + '/oauth/callback', 30 + handler(async (req, res) => { 31 + const params = new URLSearchParams(req.originalUrl.split('?')[1]) 32 + try { 33 + const { agent } = await ctx.oauthClient.callback(params) 34 + await createSession(req, res, agent.accountDid) 35 + } catch (err) { 36 + ctx.logger.error({ err }, 'oauth callback failed') 37 + return res.redirect('/?error') 38 + } 39 + return res.redirect('/') 40 + }), 41 + ) 42 + 43 + router.get( 44 + '/login', 45 + handler(async (_req, res) => { 46 + return res.type('html').send(page(login({}))) 47 + }), 48 + ) 49 + 50 + router.post( 51 + '/login', 52 + handler(async (req, res) => { 53 + const handle = req.body?.handle 54 + if (typeof handle !== 'string' || !isValidHandle(handle)) { 55 + return res.type('html').send(page(login({ error: 'invalid handle' }))) 56 + } 57 + try { 58 + const url = await ctx.oauthClient.authorize(handle) 59 + return res.redirect(url.toString()) 60 + } catch (err) { 61 + ctx.logger.error({ err }, 'oauth authorize failed') 62 + return res.type('html').send( 63 + page( 64 + login({ 65 + error: err instanceof OAuthResolverError ? err.message : "couldn't initiate login", 66 + }), 67 + ), 68 + ) 69 + } 70 + }), 71 + ) 72 + 73 + router.post( 74 + '/logout', 75 + handler(async (req, res) => { 76 + await destroySession(req, res) 77 + return res.redirect('/') 78 + }), 79 + ) 80 + 81 + router.get( 11 82 '/', 12 83 handler(async (req, res) => { 84 + const session = await getSession(req, res) 85 + const agent = 86 + session && 87 + (await ctx.oauthClient.restore(session.did).catch(async (err) => { 88 + ctx.logger.warn({ err }, 'oauth restore failed') 89 + await destroySession(req, res) 90 + return null 91 + })) 13 92 const posts = await ctx.db.selectFrom('post').selectAll().orderBy('indexedAt', 'desc').limit(10).execute() 14 - return res.type('html').send(page(home(posts))) 93 + if (!agent) { 94 + return res.type('html').send(page(home({ posts }))) 95 + } 96 + const { data: profile } = await agent.getProfile({ actor: session.did }) 97 + return res.type('html').send(page(home({ posts, profile }))) 15 98 }), 16 99 ) 17 100
+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)