A lexicon-driven AppView for ATProto. happyview.dev
backfill firehose jetstream atproto appview oauth lexicon
8
fork

Configure Feed

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

feat: add ATProto OAuth login via handle

Trezy fe98dad9 3f27acce

+601 -116
+366
web/package-lock.json
··· 8 8 "name": "web", 9 9 "version": "0.1.0", 10 10 "dependencies": { 11 + "@atproto/oauth-client-browser": "^0.3.40", 11 12 "@dnd-kit/core": "^6.3.1", 12 13 "@dnd-kit/modifiers": "^9.0.0", 13 14 "@dnd-kit/sortable": "^10.0.0", ··· 77 78 "nup": "bin/nup.mjs" 78 79 } 79 80 }, 81 + "node_modules/@atproto-labs/did-resolver": { 82 + "version": "0.2.6", 83 + "resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.2.6.tgz", 84 + "integrity": "sha512-2K1bC04nI2fmgNcvof+yA28IhGlpWn2JKYlPa7To9JTKI45FINCGkQSGiL2nyXlyzDJJ34fZ1aq6/IRFIOIiqg==", 85 + "license": "MIT", 86 + "dependencies": { 87 + "@atproto-labs/fetch": "0.2.3", 88 + "@atproto-labs/pipe": "0.1.1", 89 + "@atproto-labs/simple-store": "0.3.0", 90 + "@atproto-labs/simple-store-memory": "0.1.4", 91 + "@atproto/did": "0.3.0", 92 + "zod": "^3.23.8" 93 + } 94 + }, 95 + "node_modules/@atproto-labs/did-resolver/node_modules/zod": { 96 + "version": "3.25.76", 97 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 98 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 99 + "license": "MIT", 100 + "funding": { 101 + "url": "https://github.com/sponsors/colinhacks" 102 + } 103 + }, 104 + "node_modules/@atproto-labs/fetch": { 105 + "version": "0.2.3", 106 + "resolved": "https://registry.npmjs.org/@atproto-labs/fetch/-/fetch-0.2.3.tgz", 107 + "integrity": "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==", 108 + "license": "MIT", 109 + "dependencies": { 110 + "@atproto-labs/pipe": "0.1.1" 111 + } 112 + }, 113 + "node_modules/@atproto-labs/handle-resolver": { 114 + "version": "0.3.6", 115 + "resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver/-/handle-resolver-0.3.6.tgz", 116 + "integrity": "sha512-qnSTXvOBNj1EHhp2qTWSX8MS5q3AwYU5LKlt5fBvSbCjgmTr2j0URHCv+ydrwO55KvsojIkTMgeMOh4YuY4fCA==", 117 + "license": "MIT", 118 + "dependencies": { 119 + "@atproto-labs/simple-store": "0.3.0", 120 + "@atproto-labs/simple-store-memory": "0.1.4", 121 + "@atproto/did": "0.3.0", 122 + "zod": "^3.23.8" 123 + } 124 + }, 125 + "node_modules/@atproto-labs/handle-resolver/node_modules/zod": { 126 + "version": "3.25.76", 127 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 128 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 129 + "license": "MIT", 130 + "funding": { 131 + "url": "https://github.com/sponsors/colinhacks" 132 + } 133 + }, 134 + "node_modules/@atproto-labs/identity-resolver": { 135 + "version": "0.3.6", 136 + "resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.3.6.tgz", 137 + "integrity": "sha512-qoWqBDRobln0NR8L8dQjSp79E0chGkBhibEgxQa2f9WD+JbJdjQ0YvwwO5yeQn05pJoJmAwmI2wyJ45zjU7aWg==", 138 + "license": "MIT", 139 + "dependencies": { 140 + "@atproto-labs/did-resolver": "0.2.6", 141 + "@atproto-labs/handle-resolver": "0.3.6" 142 + } 143 + }, 144 + "node_modules/@atproto-labs/pipe": { 145 + "version": "0.1.1", 146 + "resolved": "https://registry.npmjs.org/@atproto-labs/pipe/-/pipe-0.1.1.tgz", 147 + "integrity": "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==", 148 + "license": "MIT" 149 + }, 150 + "node_modules/@atproto-labs/simple-store": { 151 + "version": "0.3.0", 152 + "resolved": "https://registry.npmjs.org/@atproto-labs/simple-store/-/simple-store-0.3.0.tgz", 153 + "integrity": "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ==", 154 + "license": "MIT" 155 + }, 156 + "node_modules/@atproto-labs/simple-store-memory": { 157 + "version": "0.1.4", 158 + "resolved": "https://registry.npmjs.org/@atproto-labs/simple-store-memory/-/simple-store-memory-0.1.4.tgz", 159 + "integrity": "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw==", 160 + "license": "MIT", 161 + "dependencies": { 162 + "@atproto-labs/simple-store": "0.3.0", 163 + "lru-cache": "^10.2.0" 164 + } 165 + }, 166 + "node_modules/@atproto-labs/simple-store-memory/node_modules/lru-cache": { 167 + "version": "10.4.3", 168 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", 169 + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", 170 + "license": "ISC" 171 + }, 172 + "node_modules/@atproto/common-web": { 173 + "version": "0.4.16", 174 + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.16.tgz", 175 + "integrity": "sha512-Ufvaff5JgxUyUyTAG0/3o7ltpy3lnZ1DvLjyAnvAf+hHfiK7OMQg+8byr+orN+KP9MtIQaRTsCgYPX+PxMKUoA==", 176 + "license": "MIT", 177 + "dependencies": { 178 + "@atproto/lex-data": "^0.0.11", 179 + "@atproto/lex-json": "^0.0.11", 180 + "@atproto/syntax": "^0.4.3", 181 + "zod": "^3.23.8" 182 + } 183 + }, 184 + "node_modules/@atproto/common-web/node_modules/zod": { 185 + "version": "3.25.76", 186 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 187 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 188 + "license": "MIT", 189 + "funding": { 190 + "url": "https://github.com/sponsors/colinhacks" 191 + } 192 + }, 193 + "node_modules/@atproto/did": { 194 + "version": "0.3.0", 195 + "resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.3.0.tgz", 196 + "integrity": "sha512-raUPzUGegtW/6OxwCmM8bhZvuIMzxG5t9oWsth6Tp91Kb5fTnHV2h/KKNF1C82doeA4BdXCErTyg7ISwLbQkzA==", 197 + "license": "MIT", 198 + "dependencies": { 199 + "zod": "^3.23.8" 200 + } 201 + }, 202 + "node_modules/@atproto/did/node_modules/zod": { 203 + "version": "3.25.76", 204 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 205 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 206 + "license": "MIT", 207 + "funding": { 208 + "url": "https://github.com/sponsors/colinhacks" 209 + } 210 + }, 211 + "node_modules/@atproto/jwk": { 212 + "version": "0.6.0", 213 + "resolved": "https://registry.npmjs.org/@atproto/jwk/-/jwk-0.6.0.tgz", 214 + "integrity": "sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw==", 215 + "license": "MIT", 216 + "dependencies": { 217 + "multiformats": "^9.9.0", 218 + "zod": "^3.23.8" 219 + } 220 + }, 221 + "node_modules/@atproto/jwk-jose": { 222 + "version": "0.1.11", 223 + "resolved": "https://registry.npmjs.org/@atproto/jwk-jose/-/jwk-jose-0.1.11.tgz", 224 + "integrity": "sha512-i4Fnr2sTBYmMmHXl7NJh8GrCH+tDQEVWrcDMDnV5DjJfkgT17wIqvojIw9SNbSL4Uf0OtfEv6AgG0A+mgh8b5Q==", 225 + "license": "MIT", 226 + "dependencies": { 227 + "@atproto/jwk": "0.6.0", 228 + "jose": "^5.2.0" 229 + } 230 + }, 231 + "node_modules/@atproto/jwk-jose/node_modules/jose": { 232 + "version": "5.10.0", 233 + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", 234 + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", 235 + "license": "MIT", 236 + "funding": { 237 + "url": "https://github.com/sponsors/panva" 238 + } 239 + }, 240 + "node_modules/@atproto/jwk-webcrypto": { 241 + "version": "0.2.0", 242 + "resolved": "https://registry.npmjs.org/@atproto/jwk-webcrypto/-/jwk-webcrypto-0.2.0.tgz", 243 + "integrity": "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg==", 244 + "license": "MIT", 245 + "dependencies": { 246 + "@atproto/jwk": "0.6.0", 247 + "@atproto/jwk-jose": "0.1.11", 248 + "zod": "^3.23.8" 249 + } 250 + }, 251 + "node_modules/@atproto/jwk-webcrypto/node_modules/zod": { 252 + "version": "3.25.76", 253 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 254 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 255 + "license": "MIT", 256 + "funding": { 257 + "url": "https://github.com/sponsors/colinhacks" 258 + } 259 + }, 260 + "node_modules/@atproto/jwk/node_modules/zod": { 261 + "version": "3.25.76", 262 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 263 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 264 + "license": "MIT", 265 + "funding": { 266 + "url": "https://github.com/sponsors/colinhacks" 267 + } 268 + }, 269 + "node_modules/@atproto/lex-data": { 270 + "version": "0.0.11", 271 + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.11.tgz", 272 + "integrity": "sha512-4+KTtHdqwlhiTKA7D4SACea4jprsNpCQsNALW09wsZ6IHhCDGO5tr1cmV+QnLYe3G3mu1E1yXHXbPUHrUUDT/A==", 273 + "license": "MIT", 274 + "dependencies": { 275 + "multiformats": "^9.9.0", 276 + "tslib": "^2.8.1", 277 + "uint8arrays": "3.0.0", 278 + "unicode-segmenter": "^0.14.0" 279 + } 280 + }, 281 + "node_modules/@atproto/lex-json": { 282 + "version": "0.0.11", 283 + "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.11.tgz", 284 + "integrity": "sha512-2IExAoQ4KsR5fyPa1JjIvtR316PvdgRH/l3BVGLBd3cSxM3m5MftIv1B6qZ9HjNiK60SgkWp0mi9574bTNDhBQ==", 285 + "license": "MIT", 286 + "dependencies": { 287 + "@atproto/lex-data": "^0.0.11", 288 + "tslib": "^2.8.1" 289 + } 290 + }, 291 + "node_modules/@atproto/lexicon": { 292 + "version": "0.6.1", 293 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.1.tgz", 294 + "integrity": "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw==", 295 + "license": "MIT", 296 + "dependencies": { 297 + "@atproto/common-web": "^0.4.13", 298 + "@atproto/syntax": "^0.4.3", 299 + "iso-datestring-validator": "^2.2.2", 300 + "multiformats": "^9.9.0", 301 + "zod": "^3.23.8" 302 + } 303 + }, 304 + "node_modules/@atproto/lexicon/node_modules/zod": { 305 + "version": "3.25.76", 306 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 307 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 308 + "license": "MIT", 309 + "funding": { 310 + "url": "https://github.com/sponsors/colinhacks" 311 + } 312 + }, 313 + "node_modules/@atproto/oauth-client": { 314 + "version": "0.5.14", 315 + "resolved": "https://registry.npmjs.org/@atproto/oauth-client/-/oauth-client-0.5.14.tgz", 316 + "integrity": "sha512-sPH+vcdq9maTEAhJI0HzmFcFAMrkCS19np+RUssNkX6kS8Xr3OYr57tvYRCbkcnIyYTfYcxKQgpwHKx3RVEaYw==", 317 + "license": "MIT", 318 + "dependencies": { 319 + "@atproto-labs/did-resolver": "0.2.6", 320 + "@atproto-labs/fetch": "0.2.3", 321 + "@atproto-labs/handle-resolver": "0.3.6", 322 + "@atproto-labs/identity-resolver": "0.3.6", 323 + "@atproto-labs/simple-store": "0.3.0", 324 + "@atproto-labs/simple-store-memory": "0.1.4", 325 + "@atproto/did": "0.3.0", 326 + "@atproto/jwk": "0.6.0", 327 + "@atproto/oauth-types": "0.6.2", 328 + "@atproto/xrpc": "0.7.7", 329 + "core-js": "^3", 330 + "multiformats": "^9.9.0", 331 + "zod": "^3.23.8" 332 + } 333 + }, 334 + "node_modules/@atproto/oauth-client-browser": { 335 + "version": "0.3.40", 336 + "resolved": "https://registry.npmjs.org/@atproto/oauth-client-browser/-/oauth-client-browser-0.3.40.tgz", 337 + "integrity": "sha512-AlvHf1DYFRHw+J8uALUMhpclTbUTTkvLqzQeGBdAXxSGP8OefsOXXaSiY5Mh6zAYxek95/ypxYzNYncasgnMWg==", 338 + "license": "MIT", 339 + "dependencies": { 340 + "@atproto-labs/did-resolver": "0.2.6", 341 + "@atproto-labs/handle-resolver": "0.3.6", 342 + "@atproto-labs/simple-store": "0.3.0", 343 + "@atproto/did": "0.3.0", 344 + "@atproto/jwk": "0.6.0", 345 + "@atproto/jwk-webcrypto": "0.2.0", 346 + "@atproto/oauth-client": "0.5.14", 347 + "@atproto/oauth-types": "0.6.2", 348 + "core-js": "^3" 349 + } 350 + }, 351 + "node_modules/@atproto/oauth-client/node_modules/zod": { 352 + "version": "3.25.76", 353 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 354 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 355 + "license": "MIT", 356 + "funding": { 357 + "url": "https://github.com/sponsors/colinhacks" 358 + } 359 + }, 360 + "node_modules/@atproto/oauth-types": { 361 + "version": "0.6.2", 362 + "resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.6.2.tgz", 363 + "integrity": "sha512-2cuboM4RQBCYR8NQC5uGRkW6KgCgKyq/B5/+tnMmWZYtZGVUQvsUWQHK/ZiMCnVXbcDNtc/RIEJQJDZ8FXMoxg==", 364 + "license": "MIT", 365 + "dependencies": { 366 + "@atproto/did": "0.3.0", 367 + "@atproto/jwk": "0.6.0", 368 + "zod": "^3.23.8" 369 + } 370 + }, 371 + "node_modules/@atproto/oauth-types/node_modules/zod": { 372 + "version": "3.25.76", 373 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 374 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 375 + "license": "MIT", 376 + "funding": { 377 + "url": "https://github.com/sponsors/colinhacks" 378 + } 379 + }, 380 + "node_modules/@atproto/syntax": { 381 + "version": "0.4.3", 382 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", 383 + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", 384 + "license": "MIT", 385 + "dependencies": { 386 + "tslib": "^2.8.1" 387 + } 388 + }, 389 + "node_modules/@atproto/xrpc": { 390 + "version": "0.7.7", 391 + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.7.tgz", 392 + "integrity": "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==", 393 + "license": "MIT", 394 + "dependencies": { 395 + "@atproto/lexicon": "^0.6.0", 396 + "zod": "^3.23.8" 397 + } 398 + }, 399 + "node_modules/@atproto/xrpc/node_modules/zod": { 400 + "version": "3.25.76", 401 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 402 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 403 + "license": "MIT", 404 + "funding": { 405 + "url": "https://github.com/sponsors/colinhacks" 406 + } 407 + }, 80 408 "node_modules/@babel/code-frame": { 81 409 "version": "7.29.0", 82 410 "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", ··· 5554 5882 "node": ">=6.6.0" 5555 5883 } 5556 5884 }, 5885 + "node_modules/core-js": { 5886 + "version": "3.48.0", 5887 + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", 5888 + "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", 5889 + "hasInstallScript": true, 5890 + "license": "MIT", 5891 + "funding": { 5892 + "type": "opencollective", 5893 + "url": "https://opencollective.com/core-js" 5894 + } 5895 + }, 5557 5896 "node_modules/cors": { 5558 5897 "version": "2.8.6", 5559 5898 "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", ··· 8345 8684 "dev": true, 8346 8685 "license": "ISC" 8347 8686 }, 8687 + "node_modules/iso-datestring-validator": { 8688 + "version": "2.2.2", 8689 + "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", 8690 + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", 8691 + "license": "MIT" 8692 + }, 8348 8693 "node_modules/iterator.prototype": { 8349 8694 "version": "1.1.5", 8350 8695 "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", ··· 9145 9490 "type": "opencollective", 9146 9491 "url": "https://opencollective.com/express" 9147 9492 } 9493 + }, 9494 + "node_modules/multiformats": { 9495 + "version": "9.9.0", 9496 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 9497 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 9498 + "license": "(Apache-2.0 AND MIT)" 9148 9499 }, 9149 9500 "node_modules/mute-stream": { 9150 9501 "version": "2.0.0", ··· 11680 12031 "typescript": ">=4.8.4 <6.0.0" 11681 12032 } 11682 12033 }, 12034 + "node_modules/uint8arrays": { 12035 + "version": "3.0.0", 12036 + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", 12037 + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 12038 + "license": "MIT", 12039 + "dependencies": { 12040 + "multiformats": "^9.4.2" 12041 + } 12042 + }, 11683 12043 "node_modules/unbox-primitive": { 11684 12044 "version": "1.1.0", 11685 12045 "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", ··· 11704 12064 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 11705 12065 "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 11706 12066 "dev": true, 12067 + "license": "MIT" 12068 + }, 12069 + "node_modules/unicode-segmenter": { 12070 + "version": "0.14.5", 12071 + "resolved": "https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.5.tgz", 12072 + "integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==", 11707 12073 "license": "MIT" 11708 12074 }, 11709 12075 "node_modules/unicorn-magic": {
+1
web/package.json
··· 9 9 "lint": "eslint" 10 10 }, 11 11 "dependencies": { 12 + "@atproto/oauth-client-browser": "^0.3.40", 12 13 "@dnd-kit/core": "^6.3.1", 13 14 "@dnd-kit/modifiers": "^9.0.0", 14 15 "@dnd-kit/sortable": "^10.0.0",
+8 -10
web/src/app/(dashboard)/admins/page.tsx
··· 28 28 } from "@/components/ui/table" 29 29 30 30 export default function AdminsPage() { 31 - const { token } = useAuth() 31 + const { getToken } = useAuth() 32 32 const [admins, setAdmins] = useState<AdminSummary[]>([]) 33 33 const [error, setError] = useState<string | null>(null) 34 34 35 35 const load = useCallback(() => { 36 - if (!token) return 37 - getAdmins(token).then(setAdmins).catch((e) => setError(e.message)) 38 - }, [token]) 36 + getAdmins(getToken).then(setAdmins).catch((e) => setError(e.message)) 37 + }, [getToken]) 39 38 40 39 useEffect(() => { 41 40 load() 42 41 }, [load]) 43 42 44 43 async function handleDelete(id: string) { 45 - if (!token) return 46 44 try { 47 - await deleteAdmin(token, id) 45 + await deleteAdmin(getToken, id) 48 46 load() 49 47 } catch (e: unknown) { 50 48 setError(e instanceof Error ? e.message : String(e)) ··· 59 57 60 58 <div className="flex items-center justify-between"> 61 59 <h2 className="text-lg font-semibold">Admin Users</h2> 62 - <AddAdminDialog token={token!} onSuccess={load} /> 60 + <AddAdminDialog getToken={getToken} onSuccess={load} /> 63 61 </div> 64 62 65 63 <div className="rounded-lg border"> ··· 116 114 } 117 115 118 116 function AddAdminDialog({ 119 - token, 117 + getToken, 120 118 onSuccess, 121 119 }: { 122 - token: string 120 + getToken: () => Promise<string | null> 123 121 onSuccess: () => void 124 122 }) { 125 123 const [did, setDid] = useState("") ··· 129 127 async function handleAdd() { 130 128 setError(null) 131 129 try { 132 - await addAdmin(token, { did }) 130 + await addAdmin(getToken, { did }) 133 131 setDid("") 134 132 setOpen(false) 135 133 onSuccess()
+7 -8
web/src/app/(dashboard)/backfill/page.tsx
··· 46 46 } 47 47 48 48 export default function BackfillPage() { 49 - const { token } = useAuth() 49 + const { getToken } = useAuth() 50 50 const [jobs, setJobs] = useState<BackfillJob[]>([]) 51 51 const [error, setError] = useState<string | null>(null) 52 52 53 53 const load = useCallback(() => { 54 - if (!token) return 55 - getBackfillJobs(token).then(setJobs).catch((e) => setError(e.message)) 56 - }, [token]) 54 + getBackfillJobs(getToken).then(setJobs).catch((e) => setError(e.message)) 55 + }, [getToken]) 57 56 58 57 useEffect(() => { 59 58 load() ··· 77 76 78 77 <div className="flex items-center justify-between"> 79 78 <h2 className="text-lg font-semibold">Backfill Jobs</h2> 80 - <CreateDialog token={token!} onSuccess={load} /> 79 + <CreateDialog getToken={getToken} onSuccess={load} /> 81 80 </div> 82 81 83 82 <div className="rounded-lg border"> ··· 144 143 } 145 144 146 145 function CreateDialog({ 147 - token, 146 + getToken, 148 147 onSuccess, 149 148 }: { 150 - token: string 149 + getToken: () => Promise<string | null> 151 150 onSuccess: () => void 152 151 }) { 153 152 const [collection, setCollection] = useState("") ··· 158 157 async function handleCreate() { 159 158 setError(null) 160 159 try { 161 - await createBackfillJob(token, { 160 + await createBackfillJob(getToken, { 162 161 collection: collection || undefined, 163 162 did: did || undefined, 164 163 })
+4 -4
web/src/app/(dashboard)/layout.tsx
··· 12 12 }: { 13 13 children: React.ReactNode 14 14 }) { 15 - const { token } = useAuth() 15 + const { did } = useAuth() 16 16 const router = useRouter() 17 17 18 18 useEffect(() => { 19 - if (!token) { 19 + if (!did) { 20 20 router.replace("/login") 21 21 } 22 - }, [token, router]) 22 + }, [did, router]) 23 23 24 - if (!token) return null 24 + if (!did) return null 25 25 26 26 return ( 27 27 <SidebarProvider
+9 -12
web/src/app/(dashboard)/lexicons/page.tsx
··· 38 38 import { Textarea } from "@/components/ui/textarea" 39 39 40 40 export default function LexiconsPage() { 41 - const { token } = useAuth() 41 + const { getToken } = useAuth() 42 42 const [lexicons, setLexicons] = useState<LexiconSummary[]>([]) 43 43 const [error, setError] = useState<string | null>(null) 44 44 const [viewLexicon, setViewLexicon] = useState<LexiconDetail | null>(null) 45 45 46 46 const load = useCallback(() => { 47 - if (!token) return 48 - getLexicons(token).then(setLexicons).catch((e) => setError(e.message)) 49 - }, [token]) 47 + getLexicons(getToken).then(setLexicons).catch((e) => setError(e.message)) 48 + }, [getToken]) 50 49 51 50 useEffect(() => { 52 51 load() 53 52 }, [load]) 54 53 55 54 async function handleView(id: string) { 56 - if (!token) return 57 55 try { 58 - const detail = await getLexicon(token, id) 56 + const detail = await getLexicon(getToken, id) 59 57 setViewLexicon(detail) 60 58 } catch (e: unknown) { 61 59 setError(e instanceof Error ? e.message : String(e)) ··· 63 61 } 64 62 65 63 async function handleDelete(id: string) { 66 - if (!token) return 67 64 try { 68 - await deleteLexicon(token, id) 65 + await deleteLexicon(getToken, id) 69 66 load() 70 67 } catch (e: unknown) { 71 68 setError(e instanceof Error ? e.message : String(e)) ··· 80 77 81 78 <div className="flex items-center justify-between"> 82 79 <h2 className="text-lg font-semibold">Uploaded Lexicons</h2> 83 - <UploadDialog token={token!} onSuccess={load} /> 80 + <UploadDialog getToken={getToken} onSuccess={load} /> 84 81 </div> 85 82 86 83 <div className="rounded-lg border"> ··· 157 154 } 158 155 159 156 function UploadDialog({ 160 - token, 157 + getToken, 161 158 onSuccess, 162 159 }: { 163 - token: string 160 + getToken: () => Promise<string | null> 164 161 onSuccess: () => void 165 162 }) { 166 163 const [json, setJson] = useState("") ··· 174 171 setError(null) 175 172 try { 176 173 const lexiconJson = JSON.parse(json) 177 - await uploadLexicon(token, { 174 + await uploadLexicon(getToken, { 178 175 lexicon_json: lexiconJson, 179 176 backfill, 180 177 target_collection: targetCollection || undefined,
+8 -10
web/src/app/(dashboard)/network-lexicons/page.tsx
··· 33 33 } from "@/components/ui/table" 34 34 35 35 export default function NetworkLexiconsPage() { 36 - const { token } = useAuth() 36 + const { getToken } = useAuth() 37 37 const [items, setItems] = useState<NetworkLexiconSummary[]>([]) 38 38 const [error, setError] = useState<string | null>(null) 39 39 40 40 const load = useCallback(() => { 41 - if (!token) return 42 - getNetworkLexicons(token).then(setItems).catch((e) => setError(e.message)) 43 - }, [token]) 41 + getNetworkLexicons(getToken).then(setItems).catch((e) => setError(e.message)) 42 + }, [getToken]) 44 43 45 44 useEffect(() => { 46 45 load() 47 46 }, [load]) 48 47 49 48 async function handleDelete(nsid: string) { 50 - if (!token) return 51 49 try { 52 - await deleteNetworkLexicon(token, nsid) 50 + await deleteNetworkLexicon(getToken, nsid) 53 51 load() 54 52 } catch (e: unknown) { 55 53 setError(e instanceof Error ? e.message : String(e)) ··· 64 62 65 63 <div className="flex items-center justify-between"> 66 64 <h2 className="text-lg font-semibold">Tracked Network Lexicons</h2> 67 - <AddDialog token={token!} onSuccess={load} /> 65 + <AddDialog getToken={getToken} onSuccess={load} /> 68 66 </div> 69 67 70 68 <div className="rounded-lg border"> ··· 125 123 } 126 124 127 125 function AddDialog({ 128 - token, 126 + getToken, 129 127 onSuccess, 130 128 }: { 131 - token: string 129 + getToken: () => Promise<string | null> 132 130 onSuccess: () => void 133 131 }) { 134 132 const [nsid, setNsid] = useState("") ··· 139 137 async function handleAdd() { 140 138 setError(null) 141 139 try { 142 - await addNetworkLexicon(token, { 140 + await addNetworkLexicon(getToken, { 143 141 nsid, 144 142 target_collection: targetCollection || undefined, 145 143 })
+3 -4
web/src/app/(dashboard)/page.tsx
··· 21 21 } from "@/components/ui/table" 22 22 23 23 export default function DashboardPage() { 24 - const { token } = useAuth() 24 + const { getToken } = useAuth() 25 25 const [stats, setStats] = useState<StatsResponse | null>(null) 26 26 const [error, setError] = useState<string | null>(null) 27 27 28 28 useEffect(() => { 29 - if (!token) return 30 - getStats(token).then(setStats).catch((e) => setError(e.message)) 31 - }, [token]) 29 + getStats(getToken).then(setStats).catch((e) => setError(e.message)) 30 + }, [getToken]) 32 31 33 32 return ( 34 33 <>
+4 -4
web/src/app/login/page.tsx
··· 6 6 import { useAuth } from "@/lib/auth-context" 7 7 8 8 export default function LoginPage() { 9 - const { token } = useAuth() 9 + const { did } = useAuth() 10 10 const router = useRouter() 11 11 12 12 useEffect(() => { 13 - if (token) router.replace("/") 14 - }, [token, router]) 13 + if (did) router.replace("/") 14 + }, [did, router]) 15 15 16 - if (token) return null 16 + if (did) return null 17 17 18 18 return ( 19 19 <div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
+26 -16
web/src/components/login-form.tsx
··· 1 1 "use client" 2 2 3 3 import { useState } from "react" 4 - import { useRouter } from "next/navigation" 5 4 6 5 import { cn } from "@/lib/utils" 7 6 import { useAuth } from "@/lib/auth-context" ··· 18 17 className, 19 18 ...props 20 19 }: React.ComponentProps<"div">) { 21 - const [token, setToken] = useState("") 20 + const [handle, setHandle] = useState("") 21 + const [loading, setLoading] = useState(false) 22 + const [error, setError] = useState<string | null>(null) 22 23 const { login } = useAuth() 23 - const router = useRouter() 24 24 25 - function handleSubmit(e: React.FormEvent) { 25 + async function handleSubmit(e: React.FormEvent) { 26 26 e.preventDefault() 27 - if (!token.trim()) return 28 - login(token.trim()) 29 - router.push("/") 27 + if (!handle.trim()) return 28 + setLoading(true) 29 + setError(null) 30 + try { 31 + await login(handle.trim()) 32 + } catch (e: unknown) { 33 + setError(e instanceof Error ? e.message : "Login failed") 34 + setLoading(false) 35 + } 30 36 } 31 37 32 38 return ( ··· 36 42 <div className="flex flex-col items-center gap-2 text-center"> 37 43 <h1 className="text-xl font-bold">HappyView Admin</h1> 38 44 <FieldDescription> 39 - Enter your access token to manage your AppView. 45 + Sign in with your ATProto account to manage your AppView. 40 46 </FieldDescription> 41 47 </div> 48 + {error && ( 49 + <p className="text-destructive text-center text-sm">{error}</p> 50 + )} 42 51 <Field> 43 - <FieldLabel htmlFor="token">Access Token</FieldLabel> 52 + <FieldLabel htmlFor="handle">Handle</FieldLabel> 44 53 <Input 45 - id="token" 46 - type="password" 47 - placeholder="eyJ..." 48 - value={token} 49 - onChange={(e) => setToken(e.target.value)} 54 + id="handle" 55 + type="text" 56 + placeholder="you.bsky.social" 57 + value={handle} 58 + onChange={(e) => setHandle(e.target.value)} 50 59 required 60 + disabled={loading} 51 61 /> 52 62 </Field> 53 63 <Field> 54 - <Button type="submit" className="w-full"> 55 - Sign in 64 + <Button type="submit" className="w-full" disabled={loading}> 65 + {loading ? "Signing in..." : "Sign in"} 56 66 </Button> 57 67 </Field> 58 68 </FieldGroup>
+39 -27
web/src/lib/api.ts
··· 8 8 9 9 async function apiFetch<T = unknown>( 10 10 path: string, 11 - token: string, 11 + getToken: () => Promise<string | null>, 12 12 options?: RequestInit 13 13 ): Promise<T> { 14 + const token = await getToken() 15 + if (!token) throw new ApiError(401, "Not authenticated") 16 + 14 17 const headers: Record<string, string> = { 15 18 Authorization: `Bearer ${token}`, 16 19 } ··· 46 49 collections: CollectionStat[] 47 50 } 48 51 49 - export function getStats(token: string) { 50 - return apiFetch<StatsResponse>("/admin/stats", token) 52 + export function getStats(getToken: () => Promise<string | null>) { 53 + return apiFetch<StatsResponse>("/admin/stats", getToken) 51 54 } 52 55 53 56 // Lexicons ··· 65 68 lexicon_json: Record<string, unknown> 66 69 } 67 70 68 - export function getLexicons(token: string) { 69 - return apiFetch<LexiconSummary[]>("/admin/lexicons", token) 71 + export function getLexicons(getToken: () => Promise<string | null>) { 72 + return apiFetch<LexiconSummary[]>("/admin/lexicons", getToken) 70 73 } 71 74 72 - export function getLexicon(token: string, id: string) { 73 - return apiFetch<LexiconDetail>(`/admin/lexicons/${encodeURIComponent(id)}`, token) 75 + export function getLexicon(getToken: () => Promise<string | null>, id: string) { 76 + return apiFetch<LexiconDetail>( 77 + `/admin/lexicons/${encodeURIComponent(id)}`, 78 + getToken 79 + ) 74 80 } 75 81 76 82 export function uploadLexicon( 77 - token: string, 83 + getToken: () => Promise<string | null>, 78 84 body: { 79 85 lexicon_json: unknown 80 86 backfill?: boolean ··· 82 88 action?: string 83 89 } 84 90 ) { 85 - return apiFetch<{ id: string; revision: number }>("/admin/lexicons", token, { 91 + return apiFetch<{ id: string; revision: number }>("/admin/lexicons", getToken, { 86 92 method: "POST", 87 93 body: JSON.stringify(body), 88 94 }) 89 95 } 90 96 91 - export function deleteLexicon(token: string, id: string) { 92 - return apiFetch(`/admin/lexicons/${encodeURIComponent(id)}`, token, { 97 + export function deleteLexicon(getToken: () => Promise<string | null>, id: string) { 98 + return apiFetch(`/admin/lexicons/${encodeURIComponent(id)}`, getToken, { 93 99 method: "DELETE", 94 100 }) 95 101 } ··· 103 109 created_at: string 104 110 } 105 111 106 - export function getNetworkLexicons(token: string) { 107 - return apiFetch<NetworkLexiconSummary[]>("/admin/network-lexicons", token) 112 + export function getNetworkLexicons(getToken: () => Promise<string | null>) { 113 + return apiFetch<NetworkLexiconSummary[]>("/admin/network-lexicons", getToken) 108 114 } 109 115 110 116 export function addNetworkLexicon( 111 - token: string, 117 + getToken: () => Promise<string | null>, 112 118 body: { nsid: string; target_collection?: string } 113 119 ) { 114 120 return apiFetch<{ nsid: string; authority_did: string; revision: number }>( 115 121 "/admin/network-lexicons", 116 - token, 122 + getToken, 117 123 { method: "POST", body: JSON.stringify(body) } 118 124 ) 119 125 } 120 126 121 - export function deleteNetworkLexicon(token: string, nsid: string) { 127 + export function deleteNetworkLexicon( 128 + getToken: () => Promise<string | null>, 129 + nsid: string 130 + ) { 122 131 return apiFetch( 123 132 `/admin/network-lexicons/${encodeURIComponent(nsid)}`, 124 - token, 133 + getToken, 125 134 { method: "DELETE" } 126 135 ) 127 136 } ··· 141 150 created_at: string 142 151 } 143 152 144 - export function getBackfillJobs(token: string) { 145 - return apiFetch<BackfillJob[]>("/admin/backfill/status", token) 153 + export function getBackfillJobs(getToken: () => Promise<string | null>) { 154 + return apiFetch<BackfillJob[]>("/admin/backfill/status", getToken) 146 155 } 147 156 148 157 export function createBackfillJob( 149 - token: string, 158 + getToken: () => Promise<string | null>, 150 159 body: { collection?: string; did?: string } 151 160 ) { 152 - return apiFetch<{ id: string; status: string }>("/admin/backfill", token, { 161 + return apiFetch<{ id: string; status: string }>("/admin/backfill", getToken, { 153 162 method: "POST", 154 163 body: JSON.stringify(body), 155 164 }) ··· 163 172 last_used_at: string | null 164 173 } 165 174 166 - export function getAdmins(token: string) { 167 - return apiFetch<AdminSummary[]>("/admin/admins", token) 175 + export function getAdmins(getToken: () => Promise<string | null>) { 176 + return apiFetch<AdminSummary[]>("/admin/admins", getToken) 168 177 } 169 178 170 - export function addAdmin(token: string, body: { did: string }) { 171 - return apiFetch<{ id: string; did: string }>("/admin/admins", token, { 179 + export function addAdmin( 180 + getToken: () => Promise<string | null>, 181 + body: { did: string } 182 + ) { 183 + return apiFetch<{ id: string; did: string }>("/admin/admins", getToken, { 172 184 method: "POST", 173 185 body: JSON.stringify(body), 174 186 }) 175 187 } 176 188 177 - export function deleteAdmin(token: string, id: string) { 178 - return apiFetch(`/admin/admins/${encodeURIComponent(id)}`, token, { 189 + export function deleteAdmin(getToken: () => Promise<string | null>, id: string) { 190 + return apiFetch(`/admin/admins/${encodeURIComponent(id)}`, getToken, { 179 191 method: "DELETE", 180 192 }) 181 193 }
+126 -21
web/src/lib/auth-context.tsx
··· 1 1 "use client" 2 2 3 - import { createContext, useCallback, useContext, useEffect, useState } from "react" 3 + import { 4 + createContext, 5 + useCallback, 6 + useContext, 7 + useEffect, 8 + useRef, 9 + useState, 10 + } from "react" 11 + import type { BrowserOAuthClient, OAuthSession } from "@atproto/oauth-client-browser" 4 12 5 13 interface AuthContextType { 6 - token: string | null 7 - login: (token: string) => void 8 - logout: () => void 14 + did: string | null 15 + getToken: () => Promise<string | null> 16 + login: (handle: string) => Promise<void> 17 + logout: () => Promise<void> 18 + loading: boolean 19 + error: string | null 9 20 } 10 21 11 22 const AuthContext = createContext<AuthContextType>({ 12 - token: null, 13 - login: () => {}, 14 - logout: () => {}, 23 + did: null, 24 + getToken: async () => null, 25 + login: async () => {}, 26 + logout: async () => {}, 27 + loading: true, 28 + error: null, 15 29 }) 16 30 17 31 export function AuthProvider({ children }: { children: React.ReactNode }) { 18 - const [token, setToken] = useState<string | null>(null) 19 - const [loaded, setLoaded] = useState(false) 32 + const [session, setSession] = useState<OAuthSession | null>(null) 33 + const [loading, setLoading] = useState(true) 34 + const [error, setError] = useState<string | null>(null) 35 + const clientRef = useRef<BrowserOAuthClient | null>(null) 20 36 21 37 useEffect(() => { 22 - const stored = localStorage.getItem("happyview_token") 23 - if (stored) setToken(stored) 24 - setLoaded(true) 38 + let cancelled = false 39 + 40 + async function init() { 41 + try { 42 + const { 43 + BrowserOAuthClient: Client, 44 + atprotoLoopbackClientMetadata, 45 + buildAtprotoLoopbackClientId, 46 + } = await import("@atproto/oauth-client-browser") 47 + 48 + const isLocalhost = 49 + window.location.hostname === "localhost" || 50 + window.location.hostname === "127.0.0.1" 51 + 52 + let client: InstanceType<typeof Client> 53 + 54 + if (isLocalhost) { 55 + const port = window.location.port 56 + ? `:${window.location.port}` 57 + : "" 58 + const clientId = buildAtprotoLoopbackClientId({ 59 + redirect_uris: [`http://127.0.0.1${port}/`], 60 + }) 61 + client = new Client({ 62 + handleResolver: "https://bsky.social", 63 + clientMetadata: atprotoLoopbackClientMetadata(clientId), 64 + }) 65 + } else { 66 + client = await Client.load({ 67 + clientId: `${window.location.origin}/oauth/client-metadata.json`, 68 + handleResolver: "https://bsky.social", 69 + }) 70 + } 71 + 72 + clientRef.current = client 73 + 74 + const result = await client.init() 75 + if (!cancelled && result?.session) { 76 + setSession(result.session) 77 + } 78 + } catch (e) { 79 + if (!cancelled) { 80 + console.error("OAuth init error:", e) 81 + setError(e instanceof Error ? e.message : String(e)) 82 + } 83 + } finally { 84 + if (!cancelled) setLoading(false) 85 + } 86 + } 87 + 88 + init() 89 + return () => { 90 + cancelled = true 91 + } 25 92 }, []) 26 93 27 - const login = useCallback((t: string) => { 28 - localStorage.setItem("happyview_token", t) 29 - setToken(t) 94 + const getToken = useCallback(async (): Promise<string | null> => { 95 + if (!session) return null 96 + try { 97 + // Access the protected getTokenSet method to extract the raw access 98 + // token. The admin API validates tokens via AIP's userinfo endpoint 99 + // using plain Bearer auth, so we need the raw JWT. 100 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 101 + const tokenSet = await (session as any).getTokenSet("auto") 102 + return tokenSet.access_token 103 + } catch { 104 + return null 105 + } 106 + }, [session]) 107 + 108 + const login = useCallback(async (handle: string) => { 109 + const client = clientRef.current 110 + if (!client) return 111 + setError(null) 112 + try { 113 + await client.signIn(handle, { 114 + scope: "atproto", 115 + }) 116 + } catch (e) { 117 + setError(e instanceof Error ? e.message : String(e)) 118 + throw e 119 + } 30 120 }, []) 31 121 32 - const logout = useCallback(() => { 33 - localStorage.removeItem("happyview_token") 34 - setToken(null) 35 - }, []) 122 + const logout = useCallback(async () => { 123 + if (session) { 124 + try { 125 + await session.signOut() 126 + } catch { 127 + // Ignore sign-out errors 128 + } 129 + } 130 + setSession(null) 131 + }, [session]) 36 132 37 - if (!loaded) return null 133 + if (loading) return null 38 134 39 135 return ( 40 - <AuthContext.Provider value={{ token, login, logout }}> 136 + <AuthContext.Provider 137 + value={{ 138 + did: session?.did ?? null, 139 + getToken, 140 + login, 141 + logout, 142 + loading, 143 + error, 144 + }} 145 + > 41 146 {children} 42 147 </AuthContext.Provider> 43 148 )