[READ-ONLY] Mirror of https://github.com/flo-bit/contrail. atproto backend in a bottle flo-bit.dev/contrail/
0

Configure Feed

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

many updates, such wow

Florian (Apr 22, 2026, 7:32 PM +0200) cd0d31e4 a2a7287d

+12658 -1115
+4
package.json
··· 35 35 "types": "./dist/publish.d.ts", 36 36 "import": "./dist/publish.js" 37 37 }, 38 + "./sync": { 39 + "types": "./dist/sync/index.d.ts", 40 + "import": "./dist/sync/index.js" 41 + }, 38 42 "./spaces-lexicon-templates/*.json": "./spaces-lexicon-templates/*.json", 39 43 "./community-lexicon-templates/*.json": "./community-lexicon-templates/*.json", 40 44 "./realtime-lexicon-templates/*.json": "./realtime-lexicon-templates/*.json"
+3004 -6
pnpm-lock.yaml
··· 73 73 specifier: ^4.63.0 74 74 version: 4.63.0(@cloudflare/workers-types@4.20260205.0) 75 75 76 + examples/sveltekit-group-chat: 77 + dependencies: 78 + '@atcute/jetstream': 79 + specifier: ^1.1.2 80 + version: 1.1.2 81 + '@atmo-dev/contrail': 82 + specifier: workspace:* 83 + version: link:../.. 84 + '@foxui/core': 85 + specifier: ^0.9.1 86 + version: 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) 87 + '@foxui/social': 88 + specifier: ^0.8.10 89 + version: 0.8.10(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) 90 + '@foxui/time': 91 + specifier: ^0.8.5 92 + version: 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) 93 + valibot: 94 + specifier: ^1.3.1 95 + version: 1.3.1(typescript@6.0.3) 96 + devDependencies: 97 + '@atcute/atproto': 98 + specifier: ^3.1.10 99 + version: 3.1.10 100 + '@atcute/bluesky': 101 + specifier: ^3.3.0 102 + version: 3.3.3 103 + '@atcute/client': 104 + specifier: ^4.2.1 105 + version: 4.2.1 106 + '@atcute/identity-resolver': 107 + specifier: ^1.2.2 108 + version: 1.2.2(@atcute/identity@1.1.4) 109 + '@atcute/lex-cli': 110 + specifier: ^2.5.3 111 + version: 2.5.3 112 + '@atcute/lexicon-doc': 113 + specifier: ^2.1.2 114 + version: 2.1.2 115 + '@atcute/lexicons': 116 + specifier: ^1.2.9 117 + version: 1.3.0 118 + '@atcute/oauth-node-client': 119 + specifier: ^1.1.0 120 + version: 1.1.0 121 + '@atcute/tid': 122 + specifier: ^1.1.2 123 + version: 1.1.2 124 + '@cloudflare/workers-types': 125 + specifier: ^4.20260317.1 126 + version: 4.20260422.1 127 + '@eslint/compat': 128 + specifier: ^2.0.3 129 + version: 2.0.5(eslint@10.2.1(jiti@2.6.1)) 130 + '@eslint/js': 131 + specifier: ^10.0.1 132 + version: 10.0.1(eslint@10.2.1(jiti@2.6.1)) 133 + '@sveltejs/adapter-cloudflare': 134 + specifier: ^7.2.8 135 + version: 7.2.8(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.84.1(@cloudflare/workers-types@4.20260422.1)) 136 + '@sveltejs/kit': 137 + specifier: ^2.55.0 138 + version: 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 139 + '@sveltejs/vite-plugin-svelte': 140 + specifier: ^7.0.0 141 + version: 7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 142 + '@tailwindcss/forms': 143 + specifier: ^0.5.11 144 + version: 0.5.11(tailwindcss@4.2.4) 145 + '@tailwindcss/vite': 146 + specifier: ^4.2.2 147 + version: 4.2.4(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 148 + bits-ui: 149 + specifier: ^2.16.4 150 + version: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 151 + eslint: 152 + specifier: ^10.1.0 153 + version: 10.2.1(jiti@2.6.1) 154 + eslint-config-prettier: 155 + specifier: ^10.1.8 156 + version: 10.1.8(eslint@10.2.1(jiti@2.6.1)) 157 + eslint-plugin-svelte: 158 + specifier: ^3.16.0 159 + version: 3.17.1(eslint@10.2.1(jiti@2.6.1))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 160 + globals: 161 + specifier: ^17.4.0 162 + version: 17.5.0 163 + prettier: 164 + specifier: ^3.8.1 165 + version: 3.8.1 166 + prettier-plugin-svelte: 167 + specifier: ^3.5.1 168 + version: 3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 169 + prettier-plugin-tailwindcss: 170 + specifier: ^0.7.2 171 + version: 0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0)))(prettier@3.8.1) 172 + svelte: 173 + specifier: ^5.55.0 174 + version: 5.55.4(@typescript-eslint/types@8.59.0) 175 + svelte-check: 176 + specifier: ^4.4.5 177 + version: 4.4.6(picomatch@4.0.4)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3) 178 + tailwindcss: 179 + specifier: ^4.2.2 180 + version: 4.2.4 181 + tsx: 182 + specifier: ^4.21.0 183 + version: 4.21.0 184 + typescript: 185 + specifier: ^6.0.2 186 + version: 6.0.3 187 + typescript-eslint: 188 + specifier: ^8.57.2 189 + version: 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) 190 + vite: 191 + specifier: ^8.0.3 192 + version: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) 193 + wrangler: 194 + specifier: ^4.78.0 195 + version: 4.84.1(@cloudflare/workers-types@4.20260422.1) 196 + 76 197 packages: 77 198 78 199 '@atcute/atproto@3.1.10': 79 200 resolution: {integrity: sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ==} 201 + 202 + '@atcute/atproto@3.1.11': 203 + resolution: {integrity: sha512-yh+ASvA+iHHQij6UeHEKp2+rwvFvQR8A6/5Dk/xvqDslIikWEFx9VlprNwm/clQIPl2bLuQg+LHS8uY9o5nFTA==} 204 + 205 + '@atcute/bluesky-richtext-segmenter@3.0.0': 206 + resolution: {integrity: sha512-NhZTUKtFpeBBbILwAcxj5u4RobIoHOmGw3CAaaEFNebKYSvmTecrXJ7XufHw5DFOUdr8SiKXQVRQxGAxulMNWg==} 207 + 208 + '@atcute/bluesky@3.3.3': 209 + resolution: {integrity: sha512-R1VyEmbvIUhub6ONnt7sm6FcRoKJISWf+I0qraXtk59KLmNCsJYMX8fLdIcdv5o1i9XtDg8/Um/wqLrl84IuFg==} 80 210 81 211 '@atcute/car@5.1.1': 82 212 resolution: {integrity: sha512-MeRUJNXYgAHrJZw7mMoZJb9xIqv3LZLQw90rRRAVAo8SGNdICwyqe6Bf2LGesX73QM04MBuYO6Kqhvold3TFfg==} ··· 129 259 '@atcute/multibase@1.2.0': 130 260 resolution: {integrity: sha512-ZK2GRra+qIYq9nNuQB52m2ul0hOmCQEtPobGfTSUxm7pF0OGEkWGkWHugFhNEDVzHzTwPxHp6VGotdZFue4lYQ==} 131 261 262 + '@atcute/oauth-crypto@0.1.0': 263 + resolution: {integrity: sha512-qZYDCNLF/4B6AndYT1rsQelN8621AC5u/sL5PHvlr/qqAbmmUwCBGjEgRSyZtHE1AqD60VNiSMlOgAuEQTSl3w==} 264 + 265 + '@atcute/oauth-keyset@0.1.0': 266 + resolution: {integrity: sha512-+wqT/+I5Lg9VzKnKY3g88+N45xbq+wsdT6bHDGqCVa2u57gRvolFF4dY+weMfc/OX641BIZO6/o+zFtKBsMQnQ==} 267 + 268 + '@atcute/oauth-node-client@1.1.0': 269 + resolution: {integrity: sha512-xCp/VfjtvTeKscKR/oI2hdMTp1/DaF/7ll8b6yZOCgbKlVDDfhCn5mmKNVARGTNaoywxrXG3XffbWCIx3/E87w==} 270 + 271 + '@atcute/oauth-types@0.1.1': 272 + resolution: {integrity: sha512-u+3KMjse3Uc/9hDyilu1QVN7IpcnjVXgRzhddzBB8Uh6wePHNVBDdi9wQvFTVVA3zmxtMJVptXRyLLg6Ou9bqg==} 273 + 132 274 '@atcute/repo@0.1.3': 133 275 resolution: {integrity: sha512-kN4gkrkQgJwI5xkVQ9zSdI8ULR47uH48EdJdjMvKAWsSDu8zKYd8ZQ6n778qa6o3WYEq02v9QVf7pzeU+3fYVA==} 276 + 277 + '@atcute/tid@1.1.2': 278 + resolution: {integrity: sha512-bmPuOX/TOfcm/vsK9vM98spjkcx2wgd9S2PeK5oLgEr8IbNRPq7iMCAPzOL1nu5XAW3LlkOYQEbYRcw5vcQ37w==} 279 + 280 + '@atcute/time-ms@1.3.2': 281 + resolution: {integrity: sha512-F+qOyR9pO55g1d/QmN+Gr+fimoUQQLusdGSB6pjV0wW5KPILR4oQ4e2ZhWzqUbeHLAgWvgoTTMsMDdz62Xa2tg==} 134 282 135 283 '@atcute/uint8array@1.1.1': 136 284 resolution: {integrity: sha512-3LsC8XB8TKe9q/5hOA5sFuzGaIFdJZJNewC5OKa3o/eU6+K7JR6see9Zy2JbQERNVnRl11EzbNov1efgLMAs4g==} ··· 226 374 workerd: 227 375 optional: true 228 376 377 + '@cloudflare/unenv-preset@2.16.0': 378 + resolution: {integrity: sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==} 379 + peerDependencies: 380 + unenv: 2.0.0-rc.24 381 + workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 382 + peerDependenciesMeta: 383 + workerd: 384 + optional: true 385 + 229 386 '@cloudflare/workerd-darwin-64@1.20260205.0': 230 387 resolution: {integrity: sha512-ToOItqcirmWPwR+PtT+Q4bdjTn/63ZxhJKEfW4FNn7FxMTS1Tw5dml0T0mieOZbCpcvY8BdvPKFCSlJuI8IVHQ==} 388 + engines: {node: '>=16'} 389 + cpu: [x64] 390 + os: [darwin] 391 + 392 + '@cloudflare/workerd-darwin-64@1.20260421.1': 393 + resolution: {integrity: sha512-DLU5ZTZ1VHeZZnj0PuVJEMHKGisfLe2XShyImP5P/PPj/m/t7CLEJmPiI7FMxvT7ynArkckJl7m+Z5x7u4Kkdw==} 231 394 engines: {node: '>=16'} 232 395 cpu: [x64] 233 396 os: [darwin] ··· 238 401 cpu: [arm64] 239 402 os: [darwin] 240 403 404 + '@cloudflare/workerd-darwin-arm64@1.20260421.1': 405 + resolution: {integrity: sha512-Trotq3xRAkIcpC505WoxM8+kIH4JIvOJCNuRatyHcz9uF5S+ukgiVUFUlM+GIjw1uCM/Bda2St+vSniX1RZdpw==} 406 + engines: {node: '>=16'} 407 + cpu: [arm64] 408 + os: [darwin] 409 + 241 410 '@cloudflare/workerd-linux-64@1.20260205.0': 242 411 resolution: {integrity: sha512-rz9jBzazIA18RHY+osa19hvsPfr0LZI1AJzIjC6UqkKKphcTpHBEQ25Xt8cIA34ivMIqeENpYnnmpDFesLkfcQ==} 412 + engines: {node: '>=16'} 413 + cpu: [x64] 414 + os: [linux] 415 + 416 + '@cloudflare/workerd-linux-64@1.20260421.1': 417 + resolution: {integrity: sha512-938QjUv0z+QqK6BAvgwX/lCIZ2b224ZXoXtGTbhyNVMhB+mt4Dj24cj9qca4ekNXjVM7uTKp1yOHZO97fVSacw==} 243 418 engines: {node: '>=16'} 244 419 cpu: [x64] 245 420 os: [linux] ··· 250 425 cpu: [arm64] 251 426 os: [linux] 252 427 428 + '@cloudflare/workerd-linux-arm64@1.20260421.1': 429 + resolution: {integrity: sha512-YI4+mLfwnJcKJ+iPyxzx+tp2Jy4o29BxBPSQGZxl/AZyvZ9eTKsmNZmtjEiT4i3O/M0tdO/B/d9ESDHbRCs2rQ==} 430 + engines: {node: '>=16'} 431 + cpu: [arm64] 432 + os: [linux] 433 + 253 434 '@cloudflare/workerd-windows-64@1.20260205.0': 254 435 resolution: {integrity: sha512-SMPW5jCZYOG7XFIglSlsgN8ivcl0pCrSAYxCwxtWvZ88whhcDB/aISNtiQiDZujPH8tIo2hE5dEkxW7tGEwc3A==} 255 436 engines: {node: '>=16'} 256 437 cpu: [x64] 257 438 os: [win32] 258 439 440 + '@cloudflare/workerd-windows-64@1.20260421.1': 441 + resolution: {integrity: sha512-q1SFgwlNH9lFmw74vh7EJbJtduo92Nx51mNOfd3/u6pux6AldcwRviYzKEEv3FEbtv6OBB7J8D5f8vtZj7Z6Sg==} 442 + engines: {node: '>=16'} 443 + cpu: [x64] 444 + os: [win32] 445 + 259 446 '@cloudflare/workers-types@4.20260205.0': 260 447 resolution: {integrity: sha512-LTnpvcodmiuMwxmbrO2Fd0+Avbm2UVLLJxT8J2pRWPfoM44gmbIecXwOPZmDAMeadKWrBsQ+B0sloQAhUu5fpA==} 448 + 449 + '@cloudflare/workers-types@4.20260422.1': 450 + resolution: {integrity: sha512-kI4baXLJloST4J/PxAfhz3azy4TPC7L2wcX6nusOzSNJJD/L5CSsivSJobgkYaMG5yspzRFQUQlCdPza0nTHbQ==} 261 451 262 452 '@cspotcode/source-map-support@0.8.1': 263 453 resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} ··· 281 471 cpu: [ppc64] 282 472 os: [aix] 283 473 474 + '@esbuild/aix-ppc64@0.27.3': 475 + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} 476 + engines: {node: '>=18'} 477 + cpu: [ppc64] 478 + os: [aix] 479 + 284 480 '@esbuild/android-arm64@0.27.0': 285 481 resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} 482 + engines: {node: '>=18'} 483 + cpu: [arm64] 484 + os: [android] 485 + 486 + '@esbuild/android-arm64@0.27.3': 487 + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} 286 488 engines: {node: '>=18'} 287 489 cpu: [arm64] 288 490 os: [android] ··· 293 495 cpu: [arm] 294 496 os: [android] 295 497 498 + '@esbuild/android-arm@0.27.3': 499 + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} 500 + engines: {node: '>=18'} 501 + cpu: [arm] 502 + os: [android] 503 + 296 504 '@esbuild/android-x64@0.27.0': 297 505 resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} 506 + engines: {node: '>=18'} 507 + cpu: [x64] 508 + os: [android] 509 + 510 + '@esbuild/android-x64@0.27.3': 511 + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} 298 512 engines: {node: '>=18'} 299 513 cpu: [x64] 300 514 os: [android] ··· 305 519 cpu: [arm64] 306 520 os: [darwin] 307 521 522 + '@esbuild/darwin-arm64@0.27.3': 523 + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} 524 + engines: {node: '>=18'} 525 + cpu: [arm64] 526 + os: [darwin] 527 + 308 528 '@esbuild/darwin-x64@0.27.0': 309 529 resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} 530 + engines: {node: '>=18'} 531 + cpu: [x64] 532 + os: [darwin] 533 + 534 + '@esbuild/darwin-x64@0.27.3': 535 + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} 310 536 engines: {node: '>=18'} 311 537 cpu: [x64] 312 538 os: [darwin] ··· 317 543 cpu: [arm64] 318 544 os: [freebsd] 319 545 546 + '@esbuild/freebsd-arm64@0.27.3': 547 + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} 548 + engines: {node: '>=18'} 549 + cpu: [arm64] 550 + os: [freebsd] 551 + 320 552 '@esbuild/freebsd-x64@0.27.0': 321 553 resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} 554 + engines: {node: '>=18'} 555 + cpu: [x64] 556 + os: [freebsd] 557 + 558 + '@esbuild/freebsd-x64@0.27.3': 559 + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} 322 560 engines: {node: '>=18'} 323 561 cpu: [x64] 324 562 os: [freebsd] ··· 329 567 cpu: [arm64] 330 568 os: [linux] 331 569 570 + '@esbuild/linux-arm64@0.27.3': 571 + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} 572 + engines: {node: '>=18'} 573 + cpu: [arm64] 574 + os: [linux] 575 + 332 576 '@esbuild/linux-arm@0.27.0': 333 577 resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} 578 + engines: {node: '>=18'} 579 + cpu: [arm] 580 + os: [linux] 581 + 582 + '@esbuild/linux-arm@0.27.3': 583 + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} 334 584 engines: {node: '>=18'} 335 585 cpu: [arm] 336 586 os: [linux] ··· 341 591 cpu: [ia32] 342 592 os: [linux] 343 593 594 + '@esbuild/linux-ia32@0.27.3': 595 + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} 596 + engines: {node: '>=18'} 597 + cpu: [ia32] 598 + os: [linux] 599 + 344 600 '@esbuild/linux-loong64@0.27.0': 345 601 resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} 602 + engines: {node: '>=18'} 603 + cpu: [loong64] 604 + os: [linux] 605 + 606 + '@esbuild/linux-loong64@0.27.3': 607 + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} 346 608 engines: {node: '>=18'} 347 609 cpu: [loong64] 348 610 os: [linux] ··· 353 615 cpu: [mips64el] 354 616 os: [linux] 355 617 618 + '@esbuild/linux-mips64el@0.27.3': 619 + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} 620 + engines: {node: '>=18'} 621 + cpu: [mips64el] 622 + os: [linux] 623 + 356 624 '@esbuild/linux-ppc64@0.27.0': 357 625 resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} 626 + engines: {node: '>=18'} 627 + cpu: [ppc64] 628 + os: [linux] 629 + 630 + '@esbuild/linux-ppc64@0.27.3': 631 + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} 358 632 engines: {node: '>=18'} 359 633 cpu: [ppc64] 360 634 os: [linux] ··· 365 639 cpu: [riscv64] 366 640 os: [linux] 367 641 642 + '@esbuild/linux-riscv64@0.27.3': 643 + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} 644 + engines: {node: '>=18'} 645 + cpu: [riscv64] 646 + os: [linux] 647 + 368 648 '@esbuild/linux-s390x@0.27.0': 369 649 resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} 650 + engines: {node: '>=18'} 651 + cpu: [s390x] 652 + os: [linux] 653 + 654 + '@esbuild/linux-s390x@0.27.3': 655 + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} 370 656 engines: {node: '>=18'} 371 657 cpu: [s390x] 372 658 os: [linux] ··· 377 663 cpu: [x64] 378 664 os: [linux] 379 665 666 + '@esbuild/linux-x64@0.27.3': 667 + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} 668 + engines: {node: '>=18'} 669 + cpu: [x64] 670 + os: [linux] 671 + 380 672 '@esbuild/netbsd-arm64@0.27.0': 381 673 resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} 674 + engines: {node: '>=18'} 675 + cpu: [arm64] 676 + os: [netbsd] 677 + 678 + '@esbuild/netbsd-arm64@0.27.3': 679 + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} 382 680 engines: {node: '>=18'} 383 681 cpu: [arm64] 384 682 os: [netbsd] ··· 389 687 cpu: [x64] 390 688 os: [netbsd] 391 689 690 + '@esbuild/netbsd-x64@0.27.3': 691 + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} 692 + engines: {node: '>=18'} 693 + cpu: [x64] 694 + os: [netbsd] 695 + 392 696 '@esbuild/openbsd-arm64@0.27.0': 393 697 resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} 698 + engines: {node: '>=18'} 699 + cpu: [arm64] 700 + os: [openbsd] 701 + 702 + '@esbuild/openbsd-arm64@0.27.3': 703 + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} 394 704 engines: {node: '>=18'} 395 705 cpu: [arm64] 396 706 os: [openbsd] ··· 401 711 cpu: [x64] 402 712 os: [openbsd] 403 713 714 + '@esbuild/openbsd-x64@0.27.3': 715 + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} 716 + engines: {node: '>=18'} 717 + cpu: [x64] 718 + os: [openbsd] 719 + 404 720 '@esbuild/openharmony-arm64@0.27.0': 405 721 resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} 722 + engines: {node: '>=18'} 723 + cpu: [arm64] 724 + os: [openharmony] 725 + 726 + '@esbuild/openharmony-arm64@0.27.3': 727 + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} 406 728 engines: {node: '>=18'} 407 729 cpu: [arm64] 408 730 os: [openharmony] ··· 413 735 cpu: [x64] 414 736 os: [sunos] 415 737 738 + '@esbuild/sunos-x64@0.27.3': 739 + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} 740 + engines: {node: '>=18'} 741 + cpu: [x64] 742 + os: [sunos] 743 + 416 744 '@esbuild/win32-arm64@0.27.0': 417 745 resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} 746 + engines: {node: '>=18'} 747 + cpu: [arm64] 748 + os: [win32] 749 + 750 + '@esbuild/win32-arm64@0.27.3': 751 + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} 418 752 engines: {node: '>=18'} 419 753 cpu: [arm64] 420 754 os: [win32] ··· 425 759 cpu: [ia32] 426 760 os: [win32] 427 761 762 + '@esbuild/win32-ia32@0.27.3': 763 + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} 764 + engines: {node: '>=18'} 765 + cpu: [ia32] 766 + os: [win32] 767 + 428 768 '@esbuild/win32-x64@0.27.0': 429 769 resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} 430 770 engines: {node: '>=18'} 431 771 cpu: [x64] 432 772 os: [win32] 773 + 774 + '@esbuild/win32-x64@0.27.3': 775 + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} 776 + engines: {node: '>=18'} 777 + cpu: [x64] 778 + os: [win32] 779 + 780 + '@eslint-community/eslint-utils@4.9.1': 781 + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} 782 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 783 + peerDependencies: 784 + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 785 + 786 + '@eslint-community/regexpp@4.12.2': 787 + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} 788 + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 789 + 790 + '@eslint/compat@2.0.5': 791 + resolution: {integrity: sha512-IbHDbHJfkVNv6xjlET8AIVo/K1NQt7YT4Rp6ok/clyBGcpRx1l6gv0Rq3vBvYfPJIZt6ODf66Zq08FJNDpnzgg==} 792 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 793 + peerDependencies: 794 + eslint: ^8.40 || 9 || 10 795 + peerDependenciesMeta: 796 + eslint: 797 + optional: true 798 + 799 + '@eslint/config-array@0.23.5': 800 + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} 801 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 802 + 803 + '@eslint/config-helpers@0.5.5': 804 + resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} 805 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 806 + 807 + '@eslint/core@1.2.1': 808 + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} 809 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 810 + 811 + '@eslint/js@10.0.1': 812 + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} 813 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 814 + peerDependencies: 815 + eslint: ^10.0.0 816 + peerDependenciesMeta: 817 + eslint: 818 + optional: true 819 + 820 + '@eslint/object-schema@3.0.5': 821 + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} 822 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 823 + 824 + '@eslint/plugin-kit@0.7.1': 825 + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} 826 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 827 + 828 + '@floating-ui/core@1.7.5': 829 + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} 830 + 831 + '@floating-ui/dom@1.7.6': 832 + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} 833 + 834 + '@floating-ui/utils@0.2.11': 835 + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} 836 + 837 + '@fontsource-variable/geist-mono@5.2.7': 838 + resolution: {integrity: sha512-ZKlZ5sjtalb2TwXKs400mAGDlt/+2ENLNySPx0wTz3bP3mWARCsUW+rpxzZc7e05d2qGch70pItt3K4qttbIYA==} 839 + 840 + '@fontsource-variable/geist@5.2.8': 841 + resolution: {integrity: sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==} 842 + 843 + '@foxui/core@0.9.1': 844 + resolution: {integrity: sha512-GnMegqx5gz71U85FK85ayEEHuD3bvpNET8HeNsJU1Gj+OVkKKvUY9q/XqmCY3tsfEqY0Mv+RM1iQSnsZn5LZ6w==} 845 + peerDependencies: 846 + svelte: '>=5' 847 + tailwindcss: '>=3' 848 + 849 + '@foxui/social@0.8.10': 850 + resolution: {integrity: sha512-d9FB1BjWFbvldNlrXogdJU/a90dDdNqvBWGAkLRK2i7iiBAaWkKceWL3xc8N5+79+hfD8Ydg4ptCk3wx3bkguw==} 851 + peerDependencies: 852 + svelte: '>=5' 853 + tailwindcss: '>=3' 854 + 855 + '@foxui/text@0.8.5': 856 + resolution: {integrity: sha512-K7XsJGWdyRdXlRxjD8Z4TQ9xy7T/rVrB/z+U0YGGIjIER1DQGLawPwBQ5WXYQeNCBS1ab8mG67uFZw2sA5aFPQ==} 857 + peerDependencies: 858 + svelte: '>=5' 859 + tailwindcss: '>=3' 860 + 861 + '@foxui/time@0.8.5': 862 + resolution: {integrity: sha512-InXhgKZW4URArJWW87hblhHMAoNii0iueKCxei72SCvlABZ/y5QoX/ewhrgk7/47qbfrlgyqBzshDtF60kTqcg==} 863 + peerDependencies: 864 + svelte: '>=5' 865 + tailwindcss: '>=3' 866 + 867 + '@humanfs/core@0.19.2': 868 + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} 869 + engines: {node: '>=18.18.0'} 870 + 871 + '@humanfs/node@0.16.8': 872 + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} 873 + engines: {node: '>=18.18.0'} 874 + 875 + '@humanfs/types@0.15.0': 876 + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} 877 + engines: {node: '>=18.18.0'} 878 + 879 + '@humanwhocodes/module-importer@1.0.1': 880 + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} 881 + engines: {node: '>=12.22'} 882 + 883 + '@humanwhocodes/retry@0.4.3': 884 + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} 885 + engines: {node: '>=18.18'} 433 886 434 887 '@img/colour@1.0.0': 435 888 resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} ··· 577 1030 '@types/node': 578 1031 optional: true 579 1032 1033 + '@internationalized/date@3.12.1': 1034 + resolution: {integrity: sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==} 1035 + 1036 + '@jis3r/icons@2.7.0': 1037 + resolution: {integrity: sha512-RXee+W1AoWFHtFliLZ7SQxVopAezmCH0xU45HKTvvpDQ+6Y3g6tUHvKOK16J52TM8YsN4WDnsiOmcwau+bHmwg==} 1038 + peerDependencies: 1039 + svelte: ^5.0.0 1040 + 580 1041 '@jridgewell/gen-mapping@0.3.13': 581 1042 resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} 1043 + 1044 + '@jridgewell/remapping@2.3.5': 1045 + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} 582 1046 583 1047 '@jridgewell/resolve-uri@3.1.2': 584 1048 resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} ··· 592 1056 593 1057 '@jridgewell/trace-mapping@0.3.9': 594 1058 resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 1059 + 1060 + '@lucide/svelte@1.8.0': 1061 + resolution: {integrity: sha512-+zYQUKqEOVP5lxbGmxL1OVgGMQtRK91eIJ0bR+3Cr1ts4oQEsQfxyzzd5X47psJlblAuGFrl2xm4YuATjR9oaA==} 1062 + peerDependencies: 1063 + svelte: ^5 595 1064 596 1065 '@manypkg/find-root@1.1.0': 597 1066 resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} ··· 626 1095 resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 627 1096 engines: {node: '>= 8'} 628 1097 1098 + '@number-flow/svelte@0.4.0': 1099 + resolution: {integrity: sha512-9tnowrlZlBV3IVe3Gm1V7yXSf4Ugag2k7iW45xqb04HXSa1ApEImopvGWAjJpHDvS849o+UCb0YH461Mtde9lA==} 1100 + peerDependencies: 1101 + svelte: ^4 || ^5 1102 + 629 1103 '@optique/core@0.6.11': 630 1104 resolution: {integrity: sha512-GVLFihzBA1j78NFlkU5N1Lu0jRqET0k6Z66WK8VQKG/a3cxmCInVGSKMIdQG8i6pgC8wD5OizF6Y3QMztmhAxg==} 631 1105 engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} ··· 636 1110 637 1111 '@oxc-project/types@0.124.0': 638 1112 resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} 1113 + 1114 + '@polka/url@1.0.0-next.29': 1115 + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} 639 1116 640 1117 '@poppinss/colors@4.1.6': 641 1118 resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} ··· 873 1350 '@standard-schema/spec@1.1.0': 874 1351 resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 875 1352 1353 + '@sveltejs/acorn-typescript@1.0.9': 1354 + resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} 1355 + peerDependencies: 1356 + acorn: ^8.9.0 1357 + 1358 + '@sveltejs/adapter-cloudflare@7.2.8': 1359 + resolution: {integrity: sha512-bIdhY/Fi4AQmqiBdQVKnafH1h9Gw+xbCvHyUu4EouC8rJOU02zwhi14k/FDhQ0mJF1iblIu3m8UNQ8GpGIvIOQ==} 1360 + peerDependencies: 1361 + '@sveltejs/kit': ^2.0.0 1362 + wrangler: ^4.0.0 1363 + 1364 + '@sveltejs/kit@2.57.1': 1365 + resolution: {integrity: sha512-VRdSbB96cI1EnRh09CqmnQqP/YJvET5buj8S6k7CxaJqBJD4bw4fRKDjcarAj/eX9k2eHifQfDH8NtOh+ZxxPw==} 1366 + engines: {node: '>=18.13'} 1367 + hasBin: true 1368 + peerDependencies: 1369 + '@opentelemetry/api': ^1.0.0 1370 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 1371 + svelte: ^4.0.0 || ^5.0.0-next.0 1372 + typescript: ^5.3.3 || ^6.0.0 1373 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 1374 + peerDependenciesMeta: 1375 + '@opentelemetry/api': 1376 + optional: true 1377 + typescript: 1378 + optional: true 1379 + 1380 + '@sveltejs/vite-plugin-svelte@7.0.0': 1381 + resolution: {integrity: sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==} 1382 + engines: {node: ^20.19 || ^22.12 || >=24} 1383 + peerDependencies: 1384 + svelte: ^5.46.4 1385 + vite: ^8.0.0-beta.7 || ^8.0.0 1386 + 1387 + '@swc/helpers@0.5.21': 1388 + resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} 1389 + 1390 + '@tailwindcss/forms@0.5.11': 1391 + resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==} 1392 + peerDependencies: 1393 + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' 1394 + 1395 + '@tailwindcss/node@4.2.4': 1396 + resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==} 1397 + 1398 + '@tailwindcss/oxide-android-arm64@4.2.4': 1399 + resolution: {integrity: sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==} 1400 + engines: {node: '>= 20'} 1401 + cpu: [arm64] 1402 + os: [android] 1403 + 1404 + '@tailwindcss/oxide-darwin-arm64@4.2.4': 1405 + resolution: {integrity: sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==} 1406 + engines: {node: '>= 20'} 1407 + cpu: [arm64] 1408 + os: [darwin] 1409 + 1410 + '@tailwindcss/oxide-darwin-x64@4.2.4': 1411 + resolution: {integrity: sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==} 1412 + engines: {node: '>= 20'} 1413 + cpu: [x64] 1414 + os: [darwin] 1415 + 1416 + '@tailwindcss/oxide-freebsd-x64@4.2.4': 1417 + resolution: {integrity: sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==} 1418 + engines: {node: '>= 20'} 1419 + cpu: [x64] 1420 + os: [freebsd] 1421 + 1422 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': 1423 + resolution: {integrity: sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==} 1424 + engines: {node: '>= 20'} 1425 + cpu: [arm] 1426 + os: [linux] 1427 + 1428 + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': 1429 + resolution: {integrity: sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==} 1430 + engines: {node: '>= 20'} 1431 + cpu: [arm64] 1432 + os: [linux] 1433 + 1434 + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': 1435 + resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} 1436 + engines: {node: '>= 20'} 1437 + cpu: [arm64] 1438 + os: [linux] 1439 + 1440 + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': 1441 + resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} 1442 + engines: {node: '>= 20'} 1443 + cpu: [x64] 1444 + os: [linux] 1445 + 1446 + '@tailwindcss/oxide-linux-x64-musl@4.2.4': 1447 + resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} 1448 + engines: {node: '>= 20'} 1449 + cpu: [x64] 1450 + os: [linux] 1451 + 1452 + '@tailwindcss/oxide-wasm32-wasi@4.2.4': 1453 + resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} 1454 + engines: {node: '>=14.0.0'} 1455 + cpu: [wasm32] 1456 + bundledDependencies: 1457 + - '@napi-rs/wasm-runtime' 1458 + - '@emnapi/core' 1459 + - '@emnapi/runtime' 1460 + - '@tybys/wasm-util' 1461 + - '@emnapi/wasi-threads' 1462 + - tslib 1463 + 1464 + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': 1465 + resolution: {integrity: sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==} 1466 + engines: {node: '>= 20'} 1467 + cpu: [arm64] 1468 + os: [win32] 1469 + 1470 + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': 1471 + resolution: {integrity: sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==} 1472 + engines: {node: '>= 20'} 1473 + cpu: [x64] 1474 + os: [win32] 1475 + 1476 + '@tailwindcss/oxide@4.2.4': 1477 + resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==} 1478 + engines: {node: '>= 20'} 1479 + 1480 + '@tailwindcss/vite@4.2.4': 1481 + resolution: {integrity: sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==} 1482 + peerDependencies: 1483 + vite: ^5.2.0 || ^6 || ^7 || ^8 1484 + 1485 + '@tiptap/core@3.22.4': 1486 + resolution: {integrity: sha512-vGIGm/HpqLg8EAAQXQ+koV+/S828OEpzocfWcPOwo1u2QUVf9dQG47Yy6JJ8zFFaJwfv4dBcOXli+7BrJwsxDQ==} 1487 + peerDependencies: 1488 + '@tiptap/pm': 3.22.4 1489 + 1490 + '@tiptap/extension-blockquote@3.22.4': 1491 + resolution: {integrity: sha512-7/61kNPbGFhMgM//zMknD0pSb69rGdRIkpulXOWS1JBrFHkH6hjZDfrOETNzgKkO+NlmzVl9rXSTv0xauS3lzA==} 1492 + peerDependencies: 1493 + '@tiptap/core': 3.22.4 1494 + 1495 + '@tiptap/extension-bold@3.22.4': 1496 + resolution: {integrity: sha512-jIaPKfNOQu2lhpbLDvtwlQqM+mjF+Kk+auHpzYjBnsuwUli1Cl5ZOau7RH+rru/SQvZe1DtpQlANujDywugZAA==} 1497 + peerDependencies: 1498 + '@tiptap/core': 3.22.4 1499 + 1500 + '@tiptap/extension-bubble-menu@3.22.4': 1501 + resolution: {integrity: sha512-v4pux5Ql3THAEjaLMY4ldtdy/Xy2qU7PJLBkq8ugLp8qicaKC+tpqxp6sGif4vLIjz7Ap5hurRbTNbXzszyyHA==} 1502 + peerDependencies: 1503 + '@tiptap/core': 3.22.4 1504 + '@tiptap/pm': 3.22.4 1505 + 1506 + '@tiptap/extension-bullet-list@3.22.4': 1507 + resolution: {integrity: sha512-TB+d3fGcTixYjO7coKqTr1mGTJuqr8hjDCPUFgzuvKyJnBhqWITmBzQ/8CLq4rr6mihgGURbD3N+xkQuPAKFiw==} 1508 + peerDependencies: 1509 + '@tiptap/extension-list': 3.22.4 1510 + 1511 + '@tiptap/extension-code-block-lowlight@3.22.4': 1512 + resolution: {integrity: sha512-Gw01buMOF3EMaQHFADXMw53vvDaUcHC8ERTjw4IlwM33CGGDRQmn5cwxUvvhImyrc2FzzE2VmB5KC11PrtfORA==} 1513 + peerDependencies: 1514 + '@tiptap/core': 3.22.4 1515 + '@tiptap/extension-code-block': 3.22.4 1516 + '@tiptap/pm': 3.22.4 1517 + highlight.js: ^11 1518 + lowlight: ^2 || ^3 1519 + 1520 + '@tiptap/extension-code-block@3.22.4': 1521 + resolution: {integrity: sha512-MEurzNXfMET3rhjpoPJYUgMfxTdTqbzT9+ToFrqNGAHocdXVm6m1hhO2frVC7fEtHPnxXKsn0Z3NUbCRkRTLuA==} 1522 + peerDependencies: 1523 + '@tiptap/core': 3.22.4 1524 + '@tiptap/pm': 3.22.4 1525 + 1526 + '@tiptap/extension-code@3.22.4': 1527 + resolution: {integrity: sha512-cnbxmVhAcc7X3G81QUYEmKP0ve2hRmvAiFXBuuv9RUtQlBiRnzmhHoJOMgkX0CsMR7+8kMRpTfeDUYq2xp5s5w==} 1528 + peerDependencies: 1529 + '@tiptap/core': 3.22.4 1530 + 1531 + '@tiptap/extension-document@3.22.4': 1532 + resolution: {integrity: sha512-XQKla1+703FqQJC48tPDVgt9ucGiFbIEmQdOg5L5o07z9a6/NzuaZAc+1zJ7NxcUZzy+z6wBn1PrVMTiqiSXlw==} 1533 + peerDependencies: 1534 + '@tiptap/core': 3.22.4 1535 + 1536 + '@tiptap/extension-dropcursor@3.22.4': 1537 + resolution: {integrity: sha512-N9/yMDC35jJp0V/naL0+6gi4gUDUIcPpWEzFdCDWUSYBA8mt41c1kI1ZU7UTKYIBzTClenhYHRc2XKZxxx0+LQ==} 1538 + peerDependencies: 1539 + '@tiptap/extensions': 3.22.4 1540 + 1541 + '@tiptap/extension-floating-menu@3.22.4': 1542 + resolution: {integrity: sha512-DFuyYxgaZPgxum5z1yvJPbfYCvDdO8geXsdyqt0qYYdiat3aGE4ncJhiLRIFDhSHBhaZg5eCgu/YPYAN6jZnrA==} 1543 + peerDependencies: 1544 + '@floating-ui/dom': ^1.0.0 1545 + '@tiptap/core': 3.22.4 1546 + '@tiptap/pm': 3.22.4 1547 + 1548 + '@tiptap/extension-gapcursor@3.22.4': 1549 + resolution: {integrity: sha512-UYBEUj3SFpKINIE7AdzcyeS3xICK+ee+YLBbuqNXyHStYChjJOohzJehqiqhjR16A88KQQ+ZjgyDcItKGygSog==} 1550 + peerDependencies: 1551 + '@tiptap/extensions': 3.22.4 1552 + 1553 + '@tiptap/extension-hard-break@3.22.4': 1554 + resolution: {integrity: sha512-xq+a4dE7T6VwApCkh/yU3p30gn3F8g8Arb9CyEZm58/WIJUIGvHSTjDdHmvU16+kiWSBg+wOOsaFHhYjJjxcKA==} 1555 + peerDependencies: 1556 + '@tiptap/core': 3.22.4 1557 + 1558 + '@tiptap/extension-heading@3.22.4': 1559 + resolution: {integrity: sha512-TUaj5f0Ir5qy9HKKt2ocnwfXKpZDYeHgbbP9gshKFzdq5PLe1RbIgkjfy6bnoI865cYjmPYWRjcT7XsKyIcb9Q==} 1560 + peerDependencies: 1561 + '@tiptap/core': 3.22.4 1562 + 1563 + '@tiptap/extension-history@3.22.4': 1564 + resolution: {integrity: sha512-yk1RrBJty4F3Os+w6zIS3nOSOKy+iIax3UyXwCKiP+JI4nsTlYvgQTgTtT9Wqa14ZrypasV8gAuQeTNf/spFLg==} 1565 + peerDependencies: 1566 + '@tiptap/extensions': 3.22.4 1567 + 1568 + '@tiptap/extension-horizontal-rule@3.22.4': 1569 + resolution: {integrity: sha512-cCI1HekGQwhY/MbgaKQ0R/7HcH5ZM1oFAyI/J72QGLC0XnF403S/OXoHMuBWr1mCu8hNiQWCzeNRJUty0iytNw==} 1570 + peerDependencies: 1571 + '@tiptap/core': 3.22.4 1572 + '@tiptap/pm': 3.22.4 1573 + 1574 + '@tiptap/extension-image@3.22.4': 1575 + resolution: {integrity: sha512-ZDc+fLaratTQ4IgnKcJJwfUgUgpcHjbZSBi6UQAILJwkflMy1Zxj8mpbma5P934nLSI+uDnR5ret6ZZLNITKhA==} 1576 + peerDependencies: 1577 + '@tiptap/core': 3.22.4 1578 + 1579 + '@tiptap/extension-italic@3.22.4': 1580 + resolution: {integrity: sha512-fVSDx5AYXgDI3v2zZIqb7V8EewthwM2NJ/ZCX+XaxRsqNEpnjVhgHs7UlvDqK1wj2OJ6zmUNjPtVlAFRxwT+HQ==} 1581 + peerDependencies: 1582 + '@tiptap/core': 3.22.4 1583 + 1584 + '@tiptap/extension-link@3.22.4': 1585 + resolution: {integrity: sha512-uoP3yus02uwGPVzW2QaEPJWVIrUb/r5nKm6c8DiJv9fNSX1+gykZZMg42c6GwRFLZ/vyfWjVCbAE03VMUqafgA==} 1586 + peerDependencies: 1587 + '@tiptap/core': 3.22.4 1588 + '@tiptap/pm': 3.22.4 1589 + 1590 + '@tiptap/extension-list-item@3.22.4': 1591 + resolution: {integrity: sha512-H659KXTvggSypIDWSOJBZ37jh9pKjQriDDvYPYvOZCdfij0D0hsDXN/wXoypArneUkoBdgruHfTtMkFOaQlgkw==} 1592 + peerDependencies: 1593 + '@tiptap/extension-list': 3.22.4 1594 + 1595 + '@tiptap/extension-list-keymap@3.22.4': 1596 + resolution: {integrity: sha512-t/zhker4oIS78AIGYDdFFfZC6zSBlszfD7z/zqFLGCg5PHNNgkZK5hKj6Vyix6D2SapRn/ajnx+8mhbKIUH5eA==} 1597 + peerDependencies: 1598 + '@tiptap/extension-list': 3.22.4 1599 + 1600 + '@tiptap/extension-list@3.22.4': 1601 + resolution: {integrity: sha512-Xe8UFvvHmyp/c/TJsFwlwU9CWACYbBirNsluJ3U1+H8BTu1wqdrT/AXR5uIXeyCl5kiWKgX5q71eHWbYFOrqrg==} 1602 + peerDependencies: 1603 + '@tiptap/core': 3.22.4 1604 + '@tiptap/pm': 3.22.4 1605 + 1606 + '@tiptap/extension-ordered-list@3.22.4': 1607 + resolution: {integrity: sha512-w77hPVf7pcHt97vfrybg/l0t5CimCd4y75OJKuHuo3CfgM5xbUP/gaPNMDyLLe7MYole/UHi/XvG3XjgzqTzAw==} 1608 + peerDependencies: 1609 + '@tiptap/extension-list': 3.22.4 1610 + 1611 + '@tiptap/extension-paragraph@3.22.4': 1612 + resolution: {integrity: sha512-de6dFkIhigiENESY6rNJ3yTVS/337ybfP30dNPudTwGe9oAu9ZCS+04j6QCvXSjhlI3ULiv7wiSHqrP26Gd+Hw==} 1613 + peerDependencies: 1614 + '@tiptap/core': 3.22.4 1615 + 1616 + '@tiptap/extension-placeholder@3.22.4': 1617 + resolution: {integrity: sha512-Z3wtWL+KufwkC7CkJge5enAxx4q8C3oOYixme02snY9zfjX3V/1pjAmEfP4wxScgM5GIuTEJ83B9Yz3wRzPA6Q==} 1618 + peerDependencies: 1619 + '@tiptap/extensions': 3.22.4 1620 + 1621 + '@tiptap/extension-strike@3.22.4': 1622 + resolution: {integrity: sha512-aRHWQj42HiailXSC9LkKYM3jWMcSeGwOjbqM4PiuxQZmHVDRFmeHkfJItOdn2cSHaO0vuEVK+TvrWUWsBFi3pg==} 1623 + peerDependencies: 1624 + '@tiptap/core': 3.22.4 1625 + 1626 + '@tiptap/extension-text@3.22.4': 1627 + resolution: {integrity: sha512-mM69uUW5cSxIhyEpWXi/YcfyupcJMDLCPEfYi62awH0iOP/LRoCv/nHjJq4Hyj/KxRJbe8HKwIUnqaCUf7m5Pg==} 1628 + peerDependencies: 1629 + '@tiptap/core': 3.22.4 1630 + 1631 + '@tiptap/extension-typography@3.22.4': 1632 + resolution: {integrity: sha512-LXcJbhLl7m8jQ+VFj8u6T5p8ylZLv8WUUZQal7JF4KgDLBhPGiS0O2HXXzcRfjuVzT+erqakwybnS+0bkTGcsQ==} 1633 + peerDependencies: 1634 + '@tiptap/core': 3.22.4 1635 + 1636 + '@tiptap/extension-underline@3.22.4': 1637 + resolution: {integrity: sha512-08kGdbhIrA6h10GWXqOkqIveaBj5tmxclK208/nUIAlonI9hPd739vu7fmVtpnmqCnSSNpoRtU4u6Gj5at0ZpA==} 1638 + peerDependencies: 1639 + '@tiptap/core': 3.22.4 1640 + 1641 + '@tiptap/extensions@3.22.4': 1642 + resolution: {integrity: sha512-fOe8VptJvLPs32bNdUYo8SRyljwqKNQVXWW056VoXIc5en/59OdJlJQVeHI0jRRciH3MtrqODi/gfJR0VHNZ8A==} 1643 + peerDependencies: 1644 + '@tiptap/core': 3.22.4 1645 + '@tiptap/pm': 3.22.4 1646 + 1647 + '@tiptap/markdown@3.22.4': 1648 + resolution: {integrity: sha512-gcoLOq5TBntw13QdeWMy7yc2X+b9yTplcFb5kpMQNgNoxlu0+jlX4LiBR+E6lOV+m+AtkprHHltTYyKG23bdOw==} 1649 + peerDependencies: 1650 + '@tiptap/core': 3.22.4 1651 + '@tiptap/pm': 3.22.4 1652 + 1653 + '@tiptap/pm@3.22.4': 1654 + resolution: {integrity: sha512-hj8Qka6WcHRllHUdeSjDnq2XaisUo4KsoGJc1WcFpoa1Yd+OeD861zUMnV7DFVGdZRy45Obht0CUYJpXQ4yA4w==} 1655 + 1656 + '@tiptap/starter-kit@3.22.4': 1657 + resolution: {integrity: sha512-qWjw+vfdin1rzMRpRU4cC5tLTwMJtUpXeQukv+6mOqqvhptuwuZBjUHImVEJaSPoHXS7+1ut+nTnrLyWyEuE5Q==} 1658 + 1659 + '@tiptap/suggestion@3.22.4': 1660 + resolution: {integrity: sha512-1buvLZemITTeKmPf2wGFWvvhRFKjdQ+JgMqc67xBraOKeDd8wQi1e2XlhCYAtlVMm5f6j+qlLC/MvwuHI2jHeQ==} 1661 + peerDependencies: 1662 + '@tiptap/core': 3.22.4 1663 + '@tiptap/pm': 3.22.4 1664 + 876 1665 '@tybys/wasm-util@0.10.1': 877 1666 resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} 878 1667 879 1668 '@types/chai@5.2.3': 880 1669 resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} 881 1670 1671 + '@types/cookie@0.6.0': 1672 + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} 1673 + 882 1674 '@types/deep-eql@4.0.2': 883 1675 resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} 884 1676 1677 + '@types/esrecurse@4.3.1': 1678 + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} 1679 + 885 1680 '@types/estree@1.0.8': 886 1681 resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 1682 + 1683 + '@types/hast@3.0.4': 1684 + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} 1685 + 1686 + '@types/json-schema@7.0.15': 1687 + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 887 1688 888 1689 '@types/node@12.20.55': 889 1690 resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} ··· 893 1694 894 1695 '@types/pg@8.20.0': 895 1696 resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} 1697 + 1698 + '@types/trusted-types@2.0.7': 1699 + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} 1700 + 1701 + '@types/unist@3.0.3': 1702 + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} 1703 + 1704 + '@typescript-eslint/eslint-plugin@8.59.0': 1705 + resolution: {integrity: sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==} 1706 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 1707 + peerDependencies: 1708 + '@typescript-eslint/parser': ^8.59.0 1709 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 1710 + typescript: '>=4.8.4 <6.1.0' 1711 + 1712 + '@typescript-eslint/parser@8.59.0': 1713 + resolution: {integrity: sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==} 1714 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 1715 + peerDependencies: 1716 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 1717 + typescript: '>=4.8.4 <6.1.0' 1718 + 1719 + '@typescript-eslint/project-service@8.59.0': 1720 + resolution: {integrity: sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==} 1721 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 1722 + peerDependencies: 1723 + typescript: '>=4.8.4 <6.1.0' 1724 + 1725 + '@typescript-eslint/scope-manager@8.59.0': 1726 + resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==} 1727 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 1728 + 1729 + '@typescript-eslint/tsconfig-utils@8.59.0': 1730 + resolution: {integrity: sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==} 1731 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 1732 + peerDependencies: 1733 + typescript: '>=4.8.4 <6.1.0' 1734 + 1735 + '@typescript-eslint/type-utils@8.59.0': 1736 + resolution: {integrity: sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==} 1737 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 1738 + peerDependencies: 1739 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 1740 + typescript: '>=4.8.4 <6.1.0' 1741 + 1742 + '@typescript-eslint/types@8.59.0': 1743 + resolution: {integrity: sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==} 1744 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 1745 + 1746 + '@typescript-eslint/typescript-estree@8.59.0': 1747 + resolution: {integrity: sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==} 1748 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 1749 + peerDependencies: 1750 + typescript: '>=4.8.4 <6.1.0' 1751 + 1752 + '@typescript-eslint/utils@8.59.0': 1753 + resolution: {integrity: sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==} 1754 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 1755 + peerDependencies: 1756 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 1757 + typescript: '>=4.8.4 <6.1.0' 1758 + 1759 + '@typescript-eslint/visitor-keys@8.59.0': 1760 + resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} 1761 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 1762 + 1763 + '@use-gesture/core@10.3.1': 1764 + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} 1765 + 1766 + '@use-gesture/vanilla@10.3.1': 1767 + resolution: {integrity: sha512-lT4scGLu59ovA3zmtUonukAGcA0AdOOh+iwNDS05Bsu7Lq9aZToDHhI6D8Q2qvsVraovtsLLYwPrWdG/noMAKw==} 896 1768 897 1769 '@vitest/expect@4.1.0': 898 1770 resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} ··· 923 1795 '@vitest/utils@4.1.0': 924 1796 resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} 925 1797 1798 + acorn-jsx@5.3.2: 1799 + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 1800 + peerDependencies: 1801 + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 1802 + 926 1803 acorn@8.16.0: 927 1804 resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} 928 1805 engines: {node: '>=0.4.0'} 929 1806 hasBin: true 1807 + 1808 + ajv@6.14.0: 1809 + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} 930 1810 931 1811 ansi-colors@4.1.3: 932 1812 resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} ··· 945 1825 argparse@2.0.1: 946 1826 resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 947 1827 1828 + aria-query@5.3.1: 1829 + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} 1830 + engines: {node: '>= 0.4'} 1831 + 948 1832 array-union@2.1.0: 949 1833 resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 950 1834 engines: {node: '>=8'} ··· 953 1837 resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 954 1838 engines: {node: '>=12'} 955 1839 1840 + axobject-query@4.1.0: 1841 + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} 1842 + engines: {node: '>= 0.4'} 1843 + 1844 + balanced-match@4.0.4: 1845 + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} 1846 + engines: {node: 18 || 20 || >=22} 1847 + 956 1848 better-path-resolve@1.0.0: 957 1849 resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} 958 1850 engines: {node: '>=4'} 959 1851 1852 + bits-ui@2.18.0: 1853 + resolution: {integrity: sha512-GLOBZRVy3hxNHIQ2MpD/+5aK9KcBFZRhUJtZ1UDABXdlVR4K6zFpgt4T+Rwuhf2sQzlc6yK1q/DprHPjwT4Pjw==} 1854 + engines: {node: '>=20'} 1855 + peerDependencies: 1856 + '@internationalized/date': ^3.8.1 1857 + svelte: ^5.33.0 1858 + 960 1859 blake3-wasm@2.1.5: 961 1860 resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 1861 + 1862 + brace-expansion@5.0.5: 1863 + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} 1864 + engines: {node: 18 || 20 || >=22} 962 1865 963 1866 braces@3.0.3: 964 1867 resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} ··· 985 1888 resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} 986 1889 engines: {node: '>= 14.16.0'} 987 1890 1891 + clsx@2.1.1: 1892 + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} 1893 + engines: {node: '>=6'} 1894 + 988 1895 commander@4.1.1: 989 1896 resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 990 1897 engines: {node: '>= 6'} ··· 999 1906 convert-source-map@2.0.0: 1000 1907 resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 1001 1908 1909 + cookie@0.6.0: 1910 + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} 1911 + engines: {node: '>= 0.6'} 1912 + 1002 1913 cookie@1.1.1: 1003 1914 resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} 1004 1915 engines: {node: '>=18'} 1005 1916 1917 + core-js@3.49.0: 1918 + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} 1919 + 1006 1920 cross-spawn@7.0.6: 1007 1921 resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 1008 1922 engines: {node: '>= 8'} 1923 + 1924 + cssesc@3.0.0: 1925 + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 1926 + engines: {node: '>=4'} 1927 + hasBin: true 1928 + 1929 + custom-event-polyfill@1.0.7: 1930 + resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==} 1009 1931 1010 1932 debug@4.4.3: 1011 1933 resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} ··· 1016 1938 supports-color: 1017 1939 optional: true 1018 1940 1941 + deep-is@0.1.4: 1942 + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 1943 + 1944 + deepmerge@4.3.1: 1945 + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 1946 + engines: {node: '>=0.10.0'} 1947 + 1948 + dequal@2.0.3: 1949 + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} 1950 + engines: {node: '>=6'} 1951 + 1019 1952 detect-indent@6.1.0: 1020 1953 resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} 1021 1954 engines: {node: '>=8'} ··· 1024 1957 resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 1025 1958 engines: {node: '>=8'} 1026 1959 1960 + devalue@5.7.1: 1961 + resolution: {integrity: sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==} 1962 + 1963 + devlop@1.1.0: 1964 + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} 1965 + 1027 1966 dir-glob@3.0.1: 1028 1967 resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 1029 1968 engines: {node: '>=8'} 1969 + 1970 + dompurify@3.4.1: 1971 + resolution: {integrity: sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==} 1972 + 1973 + emoji-picker-element@1.29.1: 1974 + resolution: {integrity: sha512-TOiHzu9Dqib3x4MwcAi3wi3RdyT4SoeB4b15AvH1ks4SBwTl7DeebhZ0d3x6dNi4XfNU7IGRZ7NBQllj0RqwrQ==} 1975 + 1976 + enhanced-resolve@5.20.1: 1977 + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} 1978 + engines: {node: '>=10.13.0'} 1030 1979 1031 1980 enquirer@2.4.1: 1032 1981 resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} ··· 1043 1992 engines: {node: '>=18'} 1044 1993 hasBin: true 1045 1994 1995 + esbuild@0.27.3: 1996 + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} 1997 + engines: {node: '>=18'} 1998 + hasBin: true 1999 + 2000 + escape-string-regexp@4.0.0: 2001 + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 2002 + engines: {node: '>=10'} 2003 + 2004 + eslint-config-prettier@10.1.8: 2005 + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} 2006 + hasBin: true 2007 + peerDependencies: 2008 + eslint: '>=7.0.0' 2009 + 2010 + eslint-plugin-svelte@3.17.1: 2011 + resolution: {integrity: sha512-NyiXHtS3Ni7e532RBwS9OXlMKDIrENg3gY+/+ODjZzQx2xhU3NlJ+nIl1a93iUUQeiJL3lS8KLmY+W8hklzweQ==} 2012 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 2013 + peerDependencies: 2014 + eslint: ^8.57.1 || ^9.0.0 || ^10.0.0 2015 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 2016 + peerDependenciesMeta: 2017 + svelte: 2018 + optional: true 2019 + 2020 + eslint-scope@8.4.0: 2021 + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} 2022 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 2023 + 2024 + eslint-scope@9.1.2: 2025 + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} 2026 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 2027 + 2028 + eslint-visitor-keys@3.4.3: 2029 + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} 2030 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 2031 + 2032 + eslint-visitor-keys@4.2.1: 2033 + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} 2034 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 2035 + 2036 + eslint-visitor-keys@5.0.1: 2037 + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} 2038 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 2039 + 2040 + eslint@10.2.1: 2041 + resolution: {integrity: sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==} 2042 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 2043 + hasBin: true 2044 + peerDependencies: 2045 + jiti: '*' 2046 + peerDependenciesMeta: 2047 + jiti: 2048 + optional: true 2049 + 1046 2050 esm-env@1.2.2: 1047 2051 resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 2052 + 2053 + espree@10.4.0: 2054 + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} 2055 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 2056 + 2057 + espree@11.2.0: 2058 + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} 2059 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 1048 2060 1049 2061 esprima@4.0.1: 1050 2062 resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} 1051 2063 engines: {node: '>=4'} 1052 2064 hasBin: true 1053 2065 2066 + esquery@1.7.0: 2067 + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} 2068 + engines: {node: '>=0.10'} 2069 + 2070 + esrap@2.2.5: 2071 + resolution: {integrity: sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==} 2072 + peerDependencies: 2073 + '@typescript-eslint/types': ^8.2.0 2074 + peerDependenciesMeta: 2075 + '@typescript-eslint/types': 2076 + optional: true 2077 + 2078 + esrecurse@4.3.0: 2079 + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 2080 + engines: {node: '>=4.0'} 2081 + 2082 + estraverse@5.3.0: 2083 + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 2084 + engines: {node: '>=4.0'} 2085 + 1054 2086 estree-walker@3.0.3: 1055 2087 resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 2088 + 2089 + esutils@2.0.3: 2090 + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 2091 + engines: {node: '>=0.10.0'} 1056 2092 1057 2093 event-target-polyfill@0.0.4: 1058 2094 resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==} ··· 1064 2100 extendable-error@0.1.7: 1065 2101 resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} 1066 2102 2103 + fast-deep-equal@3.1.3: 2104 + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 2105 + 1067 2106 fast-glob@3.3.3: 1068 2107 resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} 1069 2108 engines: {node: '>=8.6.0'} 2109 + 2110 + fast-json-stable-stringify@2.1.0: 2111 + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 2112 + 2113 + fast-levenshtein@2.0.6: 2114 + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 1070 2115 1071 2116 fastq@1.20.1: 1072 2117 resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} ··· 1080 2125 picomatch: 1081 2126 optional: true 1082 2127 2128 + file-entry-cache@8.0.0: 2129 + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} 2130 + engines: {node: '>=16.0.0'} 2131 + 1083 2132 fill-range@7.1.1: 1084 2133 resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 1085 2134 engines: {node: '>=8'} ··· 1088 2137 resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} 1089 2138 engines: {node: '>=8'} 1090 2139 2140 + find-up@5.0.0: 2141 + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 2142 + engines: {node: '>=10'} 2143 + 1091 2144 fix-dts-default-cjs-exports@1.0.1: 1092 2145 resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} 2146 + 2147 + flat-cache@4.0.1: 2148 + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} 2149 + engines: {node: '>=16'} 2150 + 2151 + flatted@3.4.2: 2152 + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} 1093 2153 1094 2154 fs-extra@7.0.1: 1095 2155 resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} ··· 1111 2171 resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 1112 2172 engines: {node: '>= 6'} 1113 2173 2174 + glob-parent@6.0.2: 2175 + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 2176 + engines: {node: '>=10.13.0'} 2177 + 2178 + globals@16.5.0: 2179 + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} 2180 + engines: {node: '>=18'} 2181 + 2182 + globals@17.5.0: 2183 + resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==} 2184 + engines: {node: '>=18'} 2185 + 1114 2186 globby@11.1.0: 1115 2187 resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} 1116 2188 engines: {node: '>=10'} 1117 2189 1118 2190 graceful-fs@4.2.11: 1119 2191 resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 2192 + 2193 + highlight.js@11.11.1: 2194 + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} 2195 + engines: {node: '>=12.0.0'} 2196 + 2197 + hls.js@1.6.16: 2198 + resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} 1120 2199 1121 2200 hono@4.12.8: 1122 2201 resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} ··· 1134 2213 resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 1135 2214 engines: {node: '>= 4'} 1136 2215 2216 + ignore@7.0.5: 2217 + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} 2218 + engines: {node: '>= 4'} 2219 + 2220 + imurmurhash@0.1.4: 2221 + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 2222 + engines: {node: '>=0.8.19'} 2223 + 2224 + inline-style-parser@0.2.7: 2225 + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} 2226 + 2227 + is-emoji-supported@0.0.5: 2228 + resolution: {integrity: sha512-WOlXUhDDHxYqcSmFZis+xWhhqXiK2SU0iYiqmth5Ip0FHLZQAt9rKL5ahnilE8/86WH8tZ3bmNNNC+bTzamqlw==} 2229 + 1137 2230 is-extglob@2.1.1: 1138 2231 resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 1139 2232 engines: {node: '>=0.10.0'} ··· 1145 2238 is-number@7.0.0: 1146 2239 resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 1147 2240 engines: {node: '>=0.12.0'} 2241 + 2242 + is-reference@3.0.3: 2243 + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} 1148 2244 1149 2245 is-subdir@1.2.0: 1150 2246 resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} ··· 1173 2269 resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} 1174 2270 hasBin: true 1175 2271 2272 + json-buffer@3.0.1: 2273 + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 2274 + 2275 + json-schema-traverse@0.4.1: 2276 + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 2277 + 2278 + json-stable-stringify-without-jsonify@1.0.1: 2279 + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 2280 + 1176 2281 jsonfile@4.0.0: 1177 2282 resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} 2283 + 2284 + keyv@4.5.4: 2285 + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 1178 2286 1179 2287 kleur@4.1.5: 1180 2288 resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} 1181 2289 engines: {node: '>=6'} 2290 + 2291 + known-css-properties@0.37.0: 2292 + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} 2293 + 2294 + levn@0.4.1: 2295 + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 2296 + engines: {node: '>= 0.8.0'} 1182 2297 1183 2298 lightningcss-android-arm64@1.32.0: 1184 2299 resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} ··· 1250 2365 resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} 1251 2366 engines: {node: '>= 12.0.0'} 1252 2367 2368 + lilconfig@2.1.0: 2369 + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} 2370 + engines: {node: '>=10'} 2371 + 1253 2372 lilconfig@3.1.3: 1254 2373 resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} 1255 2374 engines: {node: '>=14'} ··· 1257 2376 lines-and-columns@1.2.4: 1258 2377 resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 1259 2378 2379 + linkifyjs@4.3.2: 2380 + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} 2381 + 1260 2382 load-tsconfig@0.2.5: 1261 2383 resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} 1262 2384 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 2385 + 2386 + loadjs@4.3.0: 2387 + resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==} 2388 + 2389 + locate-character@3.0.0: 2390 + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} 1263 2391 1264 2392 locate-path@5.0.0: 1265 2393 resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} 1266 2394 engines: {node: '>=8'} 1267 2395 2396 + locate-path@6.0.0: 2397 + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 2398 + engines: {node: '>=10'} 2399 + 1268 2400 lodash.startcase@4.4.0: 1269 2401 resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} 1270 2402 2403 + lowlight@3.3.0: 2404 + resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} 2405 + 2406 + lz-string@1.5.0: 2407 + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} 2408 + hasBin: true 2409 + 1271 2410 magic-string@0.30.21: 1272 2411 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 2412 + 2413 + marked@17.0.6: 2414 + resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==} 2415 + engines: {node: '>= 20'} 2416 + hasBin: true 1273 2417 1274 2418 merge2@1.4.1: 1275 2419 resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} ··· 1279 2423 resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 1280 2424 engines: {node: '>=8.6'} 1281 2425 2426 + mini-svg-data-uri@1.4.4: 2427 + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} 2428 + hasBin: true 2429 + 1282 2430 miniflare@4.20260205.0: 1283 2431 resolution: {integrity: sha512-jG1TknEDeFqcq/z5gsOm1rKeg4cNG7ruWxEuiPxl3pnQumavxo8kFpeQC6XKVpAhh2PI9ODGyIYlgd77sTHl5g==} 1284 2432 engines: {node: '>=18.0.0'} 1285 2433 hasBin: true 1286 2434 2435 + miniflare@4.20260421.0: 2436 + resolution: {integrity: sha512-7ZkNQ7brgQ2hh5ha9iQCDUjxBkLvuiG2VdDns9esRL8O8lXg+MoP6E0dO1rtp+ZY2I+vV1tPWr6td5IojkewLw==} 2437 + engines: {node: '>=18.0.0'} 2438 + hasBin: true 2439 + 2440 + minimatch@10.2.5: 2441 + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} 2442 + engines: {node: 18 || 20 || >=22} 2443 + 1287 2444 mlly@1.8.2: 1288 2445 resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} 2446 + 2447 + mode-watcher@1.1.0: 2448 + resolution: {integrity: sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==} 2449 + peerDependencies: 2450 + svelte: ^5.27.0 1289 2451 1290 2452 mri@1.2.0: 1291 2453 resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} 1292 2454 engines: {node: '>=4'} 2455 + 2456 + mrmime@2.0.1: 2457 + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} 2458 + engines: {node: '>=10'} 1293 2459 1294 2460 ms@2.1.3: 1295 2461 resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} ··· 1307 2473 engines: {node: ^18 || >=20} 1308 2474 hasBin: true 1309 2475 2476 + natural-compare@1.4.0: 2477 + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 2478 + 2479 + number-flow@0.6.0: 2480 + resolution: {integrity: sha512-K8flNq2Wqus53vjp/btVo3qXFkagF8dIdYavreBfE7hlvFFG/b1HMGEH6nZL+mlrJ+4lbLP9OmPv3t2rmRkpSQ==} 2481 + 1310 2482 object-assign@4.1.1: 1311 2483 resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 1312 2484 engines: {node: '>=0.10.0'} 1313 2485 1314 2486 obug@2.1.1: 1315 2487 resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} 2488 + 2489 + optionator@0.9.4: 2490 + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} 2491 + engines: {node: '>= 0.8.0'} 2492 + 2493 + orderedmap@2.1.1: 2494 + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} 1316 2495 1317 2496 outdent@0.5.0: 1318 2497 resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} ··· 1325 2504 resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} 1326 2505 engines: {node: '>=6'} 1327 2506 2507 + p-limit@3.1.0: 2508 + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 2509 + engines: {node: '>=10'} 2510 + 1328 2511 p-locate@4.1.0: 1329 2512 resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} 1330 2513 engines: {node: '>=8'} 2514 + 2515 + p-locate@5.0.0: 2516 + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 2517 + engines: {node: '>=10'} 1331 2518 1332 2519 p-map@2.1.0: 1333 2520 resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} ··· 1421 2608 pkg-types@1.3.1: 1422 2609 resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} 1423 2610 2611 + plyr@3.8.4: 2612 + resolution: {integrity: sha512-DrzLbK9Wol3zeiuZCleD9aUOl0KAaBHR9H6WVVVYPZ4Ya+LYxUFTgSF1jooHcMQCv96Ws96wCaZzIoP3bES8pQ==} 2613 + 2614 + postcss-load-config@3.1.4: 2615 + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} 2616 + engines: {node: '>= 10'} 2617 + peerDependencies: 2618 + postcss: '>=8.0.9' 2619 + ts-node: '>=9.0.0' 2620 + peerDependenciesMeta: 2621 + postcss: 2622 + optional: true 2623 + ts-node: 2624 + optional: true 2625 + 1424 2626 postcss-load-config@6.0.1: 1425 2627 resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} 1426 2628 engines: {node: '>= 18'} ··· 1438 2640 optional: true 1439 2641 yaml: 1440 2642 optional: true 2643 + 2644 + postcss-safe-parser@7.0.1: 2645 + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} 2646 + engines: {node: '>=18.0'} 2647 + peerDependencies: 2648 + postcss: ^8.4.31 2649 + 2650 + postcss-scss@4.0.9: 2651 + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} 2652 + engines: {node: '>=12.0'} 2653 + peerDependencies: 2654 + postcss: ^8.4.29 2655 + 2656 + postcss-selector-parser@7.1.1: 2657 + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} 2658 + engines: {node: '>=4'} 1441 2659 1442 2660 postcss@8.5.8: 1443 2661 resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} ··· 1459 2677 resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} 1460 2678 engines: {node: '>=0.10.0'} 1461 2679 2680 + prelude-ls@1.2.1: 2681 + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 2682 + engines: {node: '>= 0.8.0'} 2683 + 2684 + prettier-plugin-svelte@3.5.1: 2685 + resolution: {integrity: sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==} 2686 + peerDependencies: 2687 + prettier: ^3.0.0 2688 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 2689 + 2690 + prettier-plugin-tailwindcss@0.7.2: 2691 + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} 2692 + engines: {node: '>=20.19'} 2693 + peerDependencies: 2694 + '@ianvs/prettier-plugin-sort-imports': '*' 2695 + '@prettier/plugin-hermes': '*' 2696 + '@prettier/plugin-oxc': '*' 2697 + '@prettier/plugin-pug': '*' 2698 + '@shopify/prettier-plugin-liquid': '*' 2699 + '@trivago/prettier-plugin-sort-imports': '*' 2700 + '@zackad/prettier-plugin-twig': '*' 2701 + prettier: ^3.0 2702 + prettier-plugin-astro: '*' 2703 + prettier-plugin-css-order: '*' 2704 + prettier-plugin-jsdoc: '*' 2705 + prettier-plugin-marko: '*' 2706 + prettier-plugin-multiline-arrays: '*' 2707 + prettier-plugin-organize-attributes: '*' 2708 + prettier-plugin-organize-imports: '*' 2709 + prettier-plugin-sort-imports: '*' 2710 + prettier-plugin-svelte: '*' 2711 + peerDependenciesMeta: 2712 + '@ianvs/prettier-plugin-sort-imports': 2713 + optional: true 2714 + '@prettier/plugin-hermes': 2715 + optional: true 2716 + '@prettier/plugin-oxc': 2717 + optional: true 2718 + '@prettier/plugin-pug': 2719 + optional: true 2720 + '@shopify/prettier-plugin-liquid': 2721 + optional: true 2722 + '@trivago/prettier-plugin-sort-imports': 2723 + optional: true 2724 + '@zackad/prettier-plugin-twig': 2725 + optional: true 2726 + prettier-plugin-astro: 2727 + optional: true 2728 + prettier-plugin-css-order: 2729 + optional: true 2730 + prettier-plugin-jsdoc: 2731 + optional: true 2732 + prettier-plugin-marko: 2733 + optional: true 2734 + prettier-plugin-multiline-arrays: 2735 + optional: true 2736 + prettier-plugin-organize-attributes: 2737 + optional: true 2738 + prettier-plugin-organize-imports: 2739 + optional: true 2740 + prettier-plugin-sort-imports: 2741 + optional: true 2742 + prettier-plugin-svelte: 2743 + optional: true 2744 + 1462 2745 prettier@2.8.8: 1463 2746 resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} 1464 2747 engines: {node: '>=10.13.0'} ··· 1469 2752 engines: {node: '>=14'} 1470 2753 hasBin: true 1471 2754 2755 + prosemirror-changeset@2.4.1: 2756 + resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==} 2757 + 2758 + prosemirror-commands@1.7.1: 2759 + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} 2760 + 2761 + prosemirror-dropcursor@1.8.2: 2762 + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} 2763 + 2764 + prosemirror-gapcursor@1.4.1: 2765 + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} 2766 + 2767 + prosemirror-history@1.5.0: 2768 + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} 2769 + 2770 + prosemirror-keymap@1.2.3: 2771 + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} 2772 + 2773 + prosemirror-model@1.25.4: 2774 + resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} 2775 + 2776 + prosemirror-schema-list@1.5.1: 2777 + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} 2778 + 2779 + prosemirror-state@1.4.4: 2780 + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} 2781 + 2782 + prosemirror-tables@1.8.5: 2783 + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} 2784 + 2785 + prosemirror-transform@1.12.0: 2786 + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} 2787 + 2788 + prosemirror-view@1.41.8: 2789 + resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==} 2790 + 2791 + punycode@2.3.1: 2792 + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 2793 + engines: {node: '>=6'} 2794 + 1472 2795 quansync@0.2.11: 1473 2796 resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} 1474 2797 1475 2798 queue-microtask@1.2.3: 1476 2799 resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 2800 + 2801 + rangetouch@2.0.1: 2802 + resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==} 1477 2803 1478 2804 read-yaml-file@1.1.0: 1479 2805 resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} ··· 1482 2808 readdirp@4.1.2: 1483 2809 resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} 1484 2810 engines: {node: '>= 14.18.0'} 2811 + 2812 + regexparam@3.0.0: 2813 + resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} 2814 + engines: {node: '>=8'} 1485 2815 1486 2816 resolve-from@5.0.0: 1487 2817 resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} ··· 1504 2834 engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1505 2835 hasBin: true 1506 2836 2837 + rope-sequence@1.3.4: 2838 + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} 2839 + 1507 2840 run-parallel@1.2.0: 1508 2841 resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 2842 + 2843 + runed@0.23.4: 2844 + resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==} 2845 + peerDependencies: 2846 + svelte: ^5.7.0 2847 + 2848 + runed@0.25.0: 2849 + resolution: {integrity: sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==} 2850 + peerDependencies: 2851 + svelte: ^5.7.0 2852 + 2853 + runed@0.28.0: 2854 + resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==} 2855 + peerDependencies: 2856 + svelte: ^5.7.0 2857 + 2858 + runed@0.35.1: 2859 + resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} 2860 + peerDependencies: 2861 + '@sveltejs/kit': ^2.21.0 2862 + svelte: ^5.7.0 2863 + peerDependenciesMeta: 2864 + '@sveltejs/kit': 2865 + optional: true 2866 + 2867 + sade@1.8.1: 2868 + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} 2869 + engines: {node: '>=6'} 1509 2870 1510 2871 safer-buffer@2.1.2: 1511 2872 resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} ··· 1514 2875 resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} 1515 2876 engines: {node: '>=10'} 1516 2877 hasBin: true 2878 + 2879 + set-cookie-parser@3.1.0: 2880 + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} 1517 2881 1518 2882 sharp@0.34.5: 1519 2883 resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} ··· 1533 2897 signal-exit@4.1.0: 1534 2898 resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 1535 2899 engines: {node: '>=14'} 2900 + 2901 + sirv@3.0.2: 2902 + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} 2903 + engines: {node: '>=18'} 1536 2904 1537 2905 slash@3.0.0: 1538 2906 resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} ··· 1570 2938 resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} 1571 2939 engines: {node: '>=4'} 1572 2940 2941 + style-to-object@1.0.14: 2942 + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} 2943 + 1573 2944 sucrase@3.35.1: 1574 2945 resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} 1575 2946 engines: {node: '>=16 || 14 >=14.17'} ··· 1578 2949 supports-color@10.2.2: 1579 2950 resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} 1580 2951 engines: {node: '>=18'} 2952 + 2953 + svelte-check@4.4.6: 2954 + resolution: {integrity: sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==} 2955 + engines: {node: '>= 18.0.0'} 2956 + hasBin: true 2957 + peerDependencies: 2958 + svelte: ^4.0.0 || ^5.0.0-next.0 2959 + typescript: '>=5.0.0' 2960 + 2961 + svelte-eslint-parser@1.6.0: 2962 + resolution: {integrity: sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw==} 2963 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.30.3} 2964 + peerDependencies: 2965 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 2966 + peerDependenciesMeta: 2967 + svelte: 2968 + optional: true 2969 + 2970 + svelte-sonner@1.1.0: 2971 + resolution: {integrity: sha512-3lYM6ZIqWe+p9vwwWHGWP/ZdvHiUtzURsud2quIxivrX4rvpXh6i+geBGn0m3JS6KwW6W8VgbOl3xQMcDuh6gg==} 2972 + peerDependencies: 2973 + svelte: ^5.0.0 2974 + 2975 + svelte-tiptap@3.0.1: 2976 + resolution: {integrity: sha512-Vi3kVGOd01f7mslOxGbJB7z2QavdvH+6WffhB+Y5fleTiZaW0YWqIboyO2u/uh4BQeosiINmmuRJ+Qwb7mYP+A==} 2977 + peerDependencies: 2978 + '@floating-ui/dom': ^1.0.0 2979 + '@tiptap/core': ^3.0.0 2980 + '@tiptap/extension-bubble-menu': ^3.0.0 2981 + '@tiptap/extension-floating-menu': ^3.0.0 2982 + '@tiptap/pm': ^3.0.0 2983 + svelte: ^5.0.0 2984 + 2985 + svelte-toolbelt@0.10.6: 2986 + resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} 2987 + engines: {node: '>=18', pnpm: '>=8.7.0'} 2988 + peerDependencies: 2989 + svelte: ^5.30.2 2990 + 2991 + svelte-toolbelt@0.7.1: 2992 + resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==} 2993 + engines: {node: '>=18', pnpm: '>=8.7.0'} 2994 + peerDependencies: 2995 + svelte: ^5.0.0 2996 + 2997 + svelte@5.55.4: 2998 + resolution: {integrity: sha512-q8DFohk6vUswSng95IZb9nzWJnbINZsK7OiM1snAa3qCjJBL0ZQpvMyAaVXjUukdM75J/m8UE8xwqat8Ors/zQ==} 2999 + engines: {node: '>=18'} 3000 + 3001 + tabbable@6.4.0: 3002 + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} 3003 + 3004 + tailwind-merge@3.5.0: 3005 + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} 3006 + 3007 + tailwind-variants@3.2.2: 3008 + resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==} 3009 + engines: {node: '>=16.x', pnpm: '>=7.x'} 3010 + peerDependencies: 3011 + tailwind-merge: '>=3.0.0' 3012 + tailwindcss: '*' 3013 + peerDependenciesMeta: 3014 + tailwind-merge: 3015 + optional: true 3016 + 3017 + tailwindcss@4.2.4: 3018 + resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==} 3019 + 3020 + tapable@2.3.3: 3021 + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} 3022 + engines: {node: '>=6'} 1581 3023 1582 3024 term-size@2.2.1: 1583 3025 resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} ··· 1612 3054 resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1613 3055 engines: {node: '>=8.0'} 1614 3056 3057 + totalist@3.0.1: 3058 + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} 3059 + engines: {node: '>=6'} 3060 + 1615 3061 tree-kill@1.2.2: 1616 3062 resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 1617 3063 hasBin: true 3064 + 3065 + ts-api-utils@2.5.0: 3066 + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} 3067 + engines: {node: '>=18.12'} 3068 + peerDependencies: 3069 + typescript: '>=4.8.4' 1618 3070 1619 3071 ts-interface-checker@0.1.13: 1620 3072 resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} ··· 1646 3098 engines: {node: '>=18.0.0'} 1647 3099 hasBin: true 1648 3100 3101 + type-check@0.4.0: 3102 + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 3103 + engines: {node: '>= 0.8.0'} 3104 + 1649 3105 type-fest@4.41.0: 1650 3106 resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} 1651 3107 engines: {node: '>=16'} 1652 3108 3109 + typescript-eslint@8.59.0: 3110 + resolution: {integrity: sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==} 3111 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 3112 + peerDependencies: 3113 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 3114 + typescript: '>=4.8.4 <6.1.0' 3115 + 1653 3116 typescript@5.9.3: 1654 3117 resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 3118 + engines: {node: '>=14.17'} 3119 + hasBin: true 3120 + 3121 + typescript@6.0.3: 3122 + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} 1655 3123 engines: {node: '>=14.17'} 1656 3124 hasBin: true 1657 3125 ··· 1665 3133 resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} 1666 3134 engines: {node: '>=20.18.1'} 1667 3135 3136 + undici@7.24.8: 3137 + resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} 3138 + engines: {node: '>=20.18.1'} 3139 + 1668 3140 unenv@2.0.0-rc.24: 1669 3141 resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} 1670 3142 ··· 1674 3146 universalify@0.1.2: 1675 3147 resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} 1676 3148 engines: {node: '>= 4.0.0'} 3149 + 3150 + uri-js@4.4.1: 3151 + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 3152 + 3153 + url-polyfill@1.1.14: 3154 + resolution: {integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==} 3155 + 3156 + util-deprecate@1.0.2: 3157 + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 3158 + 3159 + valibot@1.3.1: 3160 + resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==} 3161 + peerDependencies: 3162 + typescript: '>=5' 3163 + peerDependenciesMeta: 3164 + typescript: 3165 + optional: true 1677 3166 1678 3167 vite@8.0.8: 1679 3168 resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} ··· 1718 3207 yaml: 1719 3208 optional: true 1720 3209 3210 + vitefu@1.1.3: 3211 + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} 3212 + peerDependencies: 3213 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 3214 + peerDependenciesMeta: 3215 + vite: 3216 + optional: true 3217 + 1721 3218 vitest@4.1.0: 1722 3219 resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} 1723 3220 engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} ··· 1753 3250 jsdom: 1754 3251 optional: true 1755 3252 3253 + w3c-keyname@2.2.8: 3254 + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} 3255 + 3256 + web-haptics@0.0.6: 3257 + resolution: {integrity: sha512-eCzcf1LDi20+Fr0x9V3OkX92k0gxEQXaHajmhXHitsnk6SxPeshv8TBtBRqxyst8HI1uf2FyFVE7QS3jo1gkrw==} 3258 + peerDependencies: 3259 + react: '>=18' 3260 + react-dom: '>=18' 3261 + svelte: '>=4' 3262 + vue: '>=3' 3263 + peerDependenciesMeta: 3264 + react: 3265 + optional: true 3266 + react-dom: 3267 + optional: true 3268 + svelte: 3269 + optional: true 3270 + vue: 3271 + optional: true 3272 + 1756 3273 which@2.0.2: 1757 3274 resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1758 3275 engines: {node: '>= 8'} ··· 1763 3280 engines: {node: '>=8'} 1764 3281 hasBin: true 1765 3282 3283 + word-wrap@1.2.5: 3284 + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} 3285 + engines: {node: '>=0.10.0'} 3286 + 1766 3287 workerd@1.20260205.0: 1767 3288 resolution: {integrity: sha512-CcMH5clHwrH8VlY7yWS9C/G/C8g9czIz1yU3akMSP9Z3CkEMFSoC3GGdj5G7Alw/PHEeez1+1IrlYger4pwu+w==} 1768 3289 engines: {node: '>=16'} 1769 3290 hasBin: true 3291 + 3292 + workerd@1.20260421.1: 3293 + resolution: {integrity: sha512-zTYD+xFR4d7TUCxsyl7FTPth9a8CDgk8pM7xUWbJxo0SGUx+2e5C7Q5LrramBZwmuAErtzXmOjlQ15PtkPAhZA==} 3294 + engines: {node: '>=16'} 3295 + hasBin: true 3296 + 3297 + worktop@0.8.0-next.18: 3298 + resolution: {integrity: sha512-+TvsA6VAVoMC3XDKR5MoC/qlLqDixEfOBysDEKnPIPou/NvoPWCAuXHXMsswwlvmEuvX56lQjvELLyLuzTKvRw==} 3299 + engines: {node: '>=12'} 1770 3300 1771 3301 wrangler@4.63.0: 1772 3302 resolution: {integrity: sha512-+R04jF7Eb8K3KRMSgoXpcIdLb8GC62eoSGusYh1pyrSMm/10E0hbKkd7phMJO4HxXc6R7mOHC5SSoX9eof30Uw==} ··· 1774 3304 hasBin: true 1775 3305 peerDependencies: 1776 3306 '@cloudflare/workers-types': ^4.20260205.0 3307 + peerDependenciesMeta: 3308 + '@cloudflare/workers-types': 3309 + optional: true 3310 + 3311 + wrangler@4.84.1: 3312 + resolution: {integrity: sha512-Xe1S/Bik7pNdtdJ+asHsEZC2dX9k3WxYn2BbxFtOrrLVxN/LKi750zsrjX41jSAk00M/O1l7jzyQV4sQqw8ftg==} 3313 + engines: {node: '>=20.3.0'} 3314 + hasBin: true 3315 + peerDependencies: 3316 + '@cloudflare/workers-types': ^4.20260421.1 1777 3317 peerDependenciesMeta: 1778 3318 '@cloudflare/workers-types': 1779 3319 optional: true ··· 1794 3334 resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} 1795 3335 engines: {node: '>=0.4'} 1796 3336 3337 + yaml@1.10.3: 3338 + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} 3339 + engines: {node: '>= 6'} 3340 + 3341 + yocto-queue@0.1.0: 3342 + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 3343 + engines: {node: '>=10'} 3344 + 1797 3345 yocto-queue@1.2.2: 1798 3346 resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} 1799 3347 engines: {node: '>=12.20'} ··· 1804 3352 youch@4.1.0-beta.10: 1805 3353 resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} 1806 3354 3355 + zimmerframe@1.1.4: 3356 + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} 3357 + 1807 3358 snapshots: 1808 3359 1809 3360 '@atcute/atproto@3.1.10': 1810 3361 dependencies: 1811 3362 '@atcute/lexicons': 1.2.9 3363 + 3364 + '@atcute/atproto@3.1.11': 3365 + dependencies: 3366 + '@atcute/lexicons': 1.3.0 3367 + 3368 + '@atcute/bluesky-richtext-segmenter@3.0.0': {} 3369 + 3370 + '@atcute/bluesky@3.3.3': 3371 + dependencies: 3372 + '@atcute/atproto': 3.1.11 3373 + '@atcute/lexicons': 1.3.0 1812 3374 1813 3375 '@atcute/car@5.1.1': 1814 3376 dependencies: ··· 1888 3450 '@atcute/identity': 1.1.4 1889 3451 '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) 1890 3452 '@atcute/lexicon-doc': 2.1.2 1891 - '@atcute/lexicons': 1.2.9 3453 + '@atcute/lexicons': 1.3.0 1892 3454 '@atcute/repo': 0.1.3 1893 3455 '@atcute/util-fetch': 1.0.5 1894 3456 '@badrap/valita': 0.4.6 ··· 1917 3479 dependencies: 1918 3480 '@atcute/uint8array': 1.1.1 1919 3481 3482 + '@atcute/oauth-crypto@0.1.0': 3483 + dependencies: 3484 + '@atcute/multibase': 1.2.0 3485 + '@atcute/uint8array': 1.1.1 3486 + '@badrap/valita': 0.4.6 3487 + nanoid: 5.1.7 3488 + 3489 + '@atcute/oauth-keyset@0.1.0': 3490 + dependencies: 3491 + '@atcute/oauth-crypto': 0.1.0 3492 + 3493 + '@atcute/oauth-node-client@1.1.0': 3494 + dependencies: 3495 + '@atcute/client': 4.2.1 3496 + '@atcute/identity': 1.1.4 3497 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) 3498 + '@atcute/lexicons': 1.3.0 3499 + '@atcute/oauth-crypto': 0.1.0 3500 + '@atcute/oauth-keyset': 0.1.0 3501 + '@atcute/oauth-types': 0.1.1 3502 + '@atcute/util-fetch': 1.0.5 3503 + '@badrap/valita': 0.4.6 3504 + nanoid: 5.1.7 3505 + 3506 + '@atcute/oauth-types@0.1.1': 3507 + dependencies: 3508 + '@atcute/identity': 1.1.4 3509 + '@atcute/lexicons': 1.3.0 3510 + '@atcute/oauth-keyset': 0.1.0 3511 + '@badrap/valita': 0.4.6 3512 + 1920 3513 '@atcute/repo@0.1.3': 1921 3514 dependencies: 1922 3515 '@atcute/car': 5.1.1 1923 3516 '@atcute/cbor': 2.3.2 1924 3517 '@atcute/cid': 2.4.1 1925 3518 '@atcute/crypto': 2.4.1 1926 - '@atcute/lexicons': 1.2.9 3519 + '@atcute/lexicons': 1.3.0 1927 3520 '@atcute/mst': 1.0.0 1928 3521 '@atcute/uint8array': 1.1.1 3522 + 3523 + '@atcute/tid@1.1.2': 3524 + dependencies: 3525 + '@atcute/time-ms': 1.3.2 3526 + 3527 + '@atcute/time-ms@1.3.2': {} 1929 3528 1930 3529 '@atcute/uint8array@1.1.1': {} 1931 3530 ··· 2110 3709 optionalDependencies: 2111 3710 workerd: 1.20260205.0 2112 3711 3712 + '@cloudflare/unenv-preset@2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260421.1)': 3713 + dependencies: 3714 + unenv: 2.0.0-rc.24 3715 + optionalDependencies: 3716 + workerd: 1.20260421.1 3717 + 2113 3718 '@cloudflare/workerd-darwin-64@1.20260205.0': 3719 + optional: true 3720 + 3721 + '@cloudflare/workerd-darwin-64@1.20260421.1': 2114 3722 optional: true 2115 3723 2116 3724 '@cloudflare/workerd-darwin-arm64@1.20260205.0': 2117 3725 optional: true 2118 3726 3727 + '@cloudflare/workerd-darwin-arm64@1.20260421.1': 3728 + optional: true 3729 + 2119 3730 '@cloudflare/workerd-linux-64@1.20260205.0': 3731 + optional: true 3732 + 3733 + '@cloudflare/workerd-linux-64@1.20260421.1': 2120 3734 optional: true 2121 3735 2122 3736 '@cloudflare/workerd-linux-arm64@1.20260205.0': 2123 3737 optional: true 2124 3738 3739 + '@cloudflare/workerd-linux-arm64@1.20260421.1': 3740 + optional: true 3741 + 2125 3742 '@cloudflare/workerd-windows-64@1.20260205.0': 2126 3743 optional: true 2127 3744 3745 + '@cloudflare/workerd-windows-64@1.20260421.1': 3746 + optional: true 3747 + 2128 3748 '@cloudflare/workers-types@4.20260205.0': {} 3749 + 3750 + '@cloudflare/workers-types@4.20260422.1': {} 2129 3751 2130 3752 '@cspotcode/source-map-support@0.8.1': 2131 3753 dependencies: ··· 2155 3777 '@esbuild/aix-ppc64@0.27.0': 2156 3778 optional: true 2157 3779 3780 + '@esbuild/aix-ppc64@0.27.3': 3781 + optional: true 3782 + 2158 3783 '@esbuild/android-arm64@0.27.0': 3784 + optional: true 3785 + 3786 + '@esbuild/android-arm64@0.27.3': 2159 3787 optional: true 2160 3788 2161 3789 '@esbuild/android-arm@0.27.0': 2162 3790 optional: true 2163 3791 3792 + '@esbuild/android-arm@0.27.3': 3793 + optional: true 3794 + 2164 3795 '@esbuild/android-x64@0.27.0': 3796 + optional: true 3797 + 3798 + '@esbuild/android-x64@0.27.3': 2165 3799 optional: true 2166 3800 2167 3801 '@esbuild/darwin-arm64@0.27.0': 2168 3802 optional: true 2169 3803 3804 + '@esbuild/darwin-arm64@0.27.3': 3805 + optional: true 3806 + 2170 3807 '@esbuild/darwin-x64@0.27.0': 3808 + optional: true 3809 + 3810 + '@esbuild/darwin-x64@0.27.3': 2171 3811 optional: true 2172 3812 2173 3813 '@esbuild/freebsd-arm64@0.27.0': 2174 3814 optional: true 2175 3815 3816 + '@esbuild/freebsd-arm64@0.27.3': 3817 + optional: true 3818 + 2176 3819 '@esbuild/freebsd-x64@0.27.0': 3820 + optional: true 3821 + 3822 + '@esbuild/freebsd-x64@0.27.3': 2177 3823 optional: true 2178 3824 2179 3825 '@esbuild/linux-arm64@0.27.0': 2180 3826 optional: true 2181 3827 3828 + '@esbuild/linux-arm64@0.27.3': 3829 + optional: true 3830 + 2182 3831 '@esbuild/linux-arm@0.27.0': 3832 + optional: true 3833 + 3834 + '@esbuild/linux-arm@0.27.3': 2183 3835 optional: true 2184 3836 2185 3837 '@esbuild/linux-ia32@0.27.0': 2186 3838 optional: true 2187 3839 3840 + '@esbuild/linux-ia32@0.27.3': 3841 + optional: true 3842 + 2188 3843 '@esbuild/linux-loong64@0.27.0': 3844 + optional: true 3845 + 3846 + '@esbuild/linux-loong64@0.27.3': 2189 3847 optional: true 2190 3848 2191 3849 '@esbuild/linux-mips64el@0.27.0': 2192 3850 optional: true 2193 3851 3852 + '@esbuild/linux-mips64el@0.27.3': 3853 + optional: true 3854 + 2194 3855 '@esbuild/linux-ppc64@0.27.0': 3856 + optional: true 3857 + 3858 + '@esbuild/linux-ppc64@0.27.3': 2195 3859 optional: true 2196 3860 2197 3861 '@esbuild/linux-riscv64@0.27.0': 2198 3862 optional: true 2199 3863 3864 + '@esbuild/linux-riscv64@0.27.3': 3865 + optional: true 3866 + 2200 3867 '@esbuild/linux-s390x@0.27.0': 3868 + optional: true 3869 + 3870 + '@esbuild/linux-s390x@0.27.3': 2201 3871 optional: true 2202 3872 2203 3873 '@esbuild/linux-x64@0.27.0': 2204 3874 optional: true 2205 3875 3876 + '@esbuild/linux-x64@0.27.3': 3877 + optional: true 3878 + 2206 3879 '@esbuild/netbsd-arm64@0.27.0': 3880 + optional: true 3881 + 3882 + '@esbuild/netbsd-arm64@0.27.3': 2207 3883 optional: true 2208 3884 2209 3885 '@esbuild/netbsd-x64@0.27.0': 2210 3886 optional: true 2211 3887 3888 + '@esbuild/netbsd-x64@0.27.3': 3889 + optional: true 3890 + 2212 3891 '@esbuild/openbsd-arm64@0.27.0': 3892 + optional: true 3893 + 3894 + '@esbuild/openbsd-arm64@0.27.3': 2213 3895 optional: true 2214 3896 2215 3897 '@esbuild/openbsd-x64@0.27.0': 2216 3898 optional: true 2217 3899 3900 + '@esbuild/openbsd-x64@0.27.3': 3901 + optional: true 3902 + 2218 3903 '@esbuild/openharmony-arm64@0.27.0': 3904 + optional: true 3905 + 3906 + '@esbuild/openharmony-arm64@0.27.3': 2219 3907 optional: true 2220 3908 2221 3909 '@esbuild/sunos-x64@0.27.0': 2222 3910 optional: true 2223 3911 3912 + '@esbuild/sunos-x64@0.27.3': 3913 + optional: true 3914 + 2224 3915 '@esbuild/win32-arm64@0.27.0': 3916 + optional: true 3917 + 3918 + '@esbuild/win32-arm64@0.27.3': 2225 3919 optional: true 2226 3920 2227 3921 '@esbuild/win32-ia32@0.27.0': 2228 3922 optional: true 2229 3923 3924 + '@esbuild/win32-ia32@0.27.3': 3925 + optional: true 3926 + 2230 3927 '@esbuild/win32-x64@0.27.0': 2231 3928 optional: true 3929 + 3930 + '@esbuild/win32-x64@0.27.3': 3931 + optional: true 3932 + 3933 + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.1(jiti@2.6.1))': 3934 + dependencies: 3935 + eslint: 10.2.1(jiti@2.6.1) 3936 + eslint-visitor-keys: 3.4.3 3937 + 3938 + '@eslint-community/regexpp@4.12.2': {} 3939 + 3940 + '@eslint/compat@2.0.5(eslint@10.2.1(jiti@2.6.1))': 3941 + dependencies: 3942 + '@eslint/core': 1.2.1 3943 + optionalDependencies: 3944 + eslint: 10.2.1(jiti@2.6.1) 3945 + 3946 + '@eslint/config-array@0.23.5': 3947 + dependencies: 3948 + '@eslint/object-schema': 3.0.5 3949 + debug: 4.4.3 3950 + minimatch: 10.2.5 3951 + transitivePeerDependencies: 3952 + - supports-color 3953 + 3954 + '@eslint/config-helpers@0.5.5': 3955 + dependencies: 3956 + '@eslint/core': 1.2.1 3957 + 3958 + '@eslint/core@1.2.1': 3959 + dependencies: 3960 + '@types/json-schema': 7.0.15 3961 + 3962 + '@eslint/js@10.0.1(eslint@10.2.1(jiti@2.6.1))': 3963 + optionalDependencies: 3964 + eslint: 10.2.1(jiti@2.6.1) 3965 + 3966 + '@eslint/object-schema@3.0.5': {} 3967 + 3968 + '@eslint/plugin-kit@0.7.1': 3969 + dependencies: 3970 + '@eslint/core': 1.2.1 3971 + levn: 0.4.1 3972 + 3973 + '@floating-ui/core@1.7.5': 3974 + dependencies: 3975 + '@floating-ui/utils': 0.2.11 3976 + 3977 + '@floating-ui/dom@1.7.6': 3978 + dependencies: 3979 + '@floating-ui/core': 1.7.5 3980 + '@floating-ui/utils': 0.2.11 3981 + 3982 + '@floating-ui/utils@0.2.11': {} 3983 + 3984 + '@fontsource-variable/geist-mono@5.2.7': {} 3985 + 3986 + '@fontsource-variable/geist@5.2.8': {} 3987 + 3988 + '@foxui/core@0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': 3989 + dependencies: 3990 + '@fontsource-variable/geist': 5.2.8 3991 + '@fontsource-variable/geist-mono': 5.2.7 3992 + '@jis3r/icons': 2.7.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 3993 + '@lucide/svelte': 1.8.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 3994 + '@number-flow/svelte': 0.4.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 3995 + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 3996 + clsx: 2.1.1 3997 + dompurify: 3.4.1 3998 + mode-watcher: 1.1.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 3999 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 4000 + svelte-sonner: 1.1.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 4001 + tailwind-merge: 3.5.0 4002 + tailwind-variants: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.4) 4003 + tailwindcss: 4.2.4 4004 + web-haptics: 0.0.6(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 4005 + transitivePeerDependencies: 4006 + - '@internationalized/date' 4007 + - '@sveltejs/kit' 4008 + - react 4009 + - react-dom 4010 + - vue 4011 + 4012 + '@foxui/social@0.8.10(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': 4013 + dependencies: 4014 + '@atcute/bluesky': 3.3.3 4015 + '@atcute/bluesky-richtext-segmenter': 3.0.0 4016 + '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) 4017 + '@foxui/text': 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) 4018 + '@foxui/time': 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) 4019 + '@use-gesture/vanilla': 10.3.1 4020 + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 4021 + emoji-picker-element: 1.29.1 4022 + hls.js: 1.6.16 4023 + is-emoji-supported: 0.0.5 4024 + plyr: 3.8.4 4025 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 4026 + tailwindcss: 4.2.4 4027 + transitivePeerDependencies: 4028 + - '@internationalized/date' 4029 + - '@sveltejs/kit' 4030 + - '@tiptap/extension-code-block' 4031 + - '@tiptap/extensions' 4032 + - highlight.js 4033 + - react 4034 + - react-dom 4035 + - vue 4036 + 4037 + '@foxui/text@0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': 4038 + dependencies: 4039 + '@floating-ui/dom': 1.7.6 4040 + '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) 4041 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4042 + '@tiptap/extension-bubble-menu': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4043 + '@tiptap/extension-code-block-lowlight': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(highlight.js@11.11.1)(lowlight@3.3.0) 4044 + '@tiptap/extension-document': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4045 + '@tiptap/extension-dropcursor': 3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) 4046 + '@tiptap/extension-floating-menu': 3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4047 + '@tiptap/extension-history': 3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) 4048 + '@tiptap/extension-image': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4049 + '@tiptap/extension-link': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4050 + '@tiptap/extension-paragraph': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4051 + '@tiptap/extension-placeholder': 3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) 4052 + '@tiptap/extension-strike': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4053 + '@tiptap/extension-text': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4054 + '@tiptap/extension-typography': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4055 + '@tiptap/extension-underline': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4056 + '@tiptap/markdown': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4057 + '@tiptap/pm': 3.22.4 4058 + '@tiptap/starter-kit': 3.22.4 4059 + '@tiptap/suggestion': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4060 + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 4061 + lowlight: 3.3.0 4062 + state: link:@tiptap/pm/state 4063 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 4064 + svelte-tiptap: 3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/extension-bubble-menu@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extension-floating-menu@3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 4065 + tailwindcss: 4.2.4 4066 + transitivePeerDependencies: 4067 + - '@internationalized/date' 4068 + - '@sveltejs/kit' 4069 + - '@tiptap/extension-code-block' 4070 + - '@tiptap/extensions' 4071 + - highlight.js 4072 + - react 4073 + - react-dom 4074 + - vue 4075 + 4076 + '@foxui/time@0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': 4077 + dependencies: 4078 + '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) 4079 + '@number-flow/svelte': 0.4.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 4080 + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 4081 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 4082 + tailwindcss: 4.2.4 4083 + transitivePeerDependencies: 4084 + - '@internationalized/date' 4085 + - '@sveltejs/kit' 4086 + - react 4087 + - react-dom 4088 + - vue 4089 + 4090 + '@humanfs/core@0.19.2': 4091 + dependencies: 4092 + '@humanfs/types': 0.15.0 4093 + 4094 + '@humanfs/node@0.16.8': 4095 + dependencies: 4096 + '@humanfs/core': 0.19.2 4097 + '@humanfs/types': 0.15.0 4098 + '@humanwhocodes/retry': 0.4.3 4099 + 4100 + '@humanfs/types@0.15.0': {} 4101 + 4102 + '@humanwhocodes/module-importer@1.0.1': {} 4103 + 4104 + '@humanwhocodes/retry@0.4.3': {} 2232 4105 2233 4106 '@img/colour@1.0.0': {} 2234 4107 ··· 2333 4206 optionalDependencies: 2334 4207 '@types/node': 25.5.0 2335 4208 4209 + '@internationalized/date@3.12.1': 4210 + dependencies: 4211 + '@swc/helpers': 0.5.21 4212 + 4213 + '@jis3r/icons@2.7.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))': 4214 + dependencies: 4215 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 4216 + 2336 4217 '@jridgewell/gen-mapping@0.3.13': 2337 4218 dependencies: 2338 4219 '@jridgewell/sourcemap-codec': 1.5.5 4220 + '@jridgewell/trace-mapping': 0.3.31 4221 + 4222 + '@jridgewell/remapping@2.3.5': 4223 + dependencies: 4224 + '@jridgewell/gen-mapping': 0.3.13 2339 4225 '@jridgewell/trace-mapping': 0.3.31 2340 4226 2341 4227 '@jridgewell/resolve-uri@3.1.2': {} ··· 2351 4237 dependencies: 2352 4238 '@jridgewell/resolve-uri': 3.1.2 2353 4239 '@jridgewell/sourcemap-codec': 1.5.5 4240 + 4241 + '@lucide/svelte@1.8.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))': 4242 + dependencies: 4243 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 2354 4244 2355 4245 '@manypkg/find-root@1.1.0': 2356 4246 dependencies: ··· 2395 4285 '@nodelib/fs.scandir': 2.1.5 2396 4286 fastq: 1.20.1 2397 4287 4288 + '@number-flow/svelte@0.4.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))': 4289 + dependencies: 4290 + esm-env: 1.2.2 4291 + number-flow: 0.6.0 4292 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 4293 + 2398 4294 '@optique/core@0.6.11': {} 2399 4295 2400 4296 '@optique/run@0.6.11': ··· 2402 4298 '@optique/core': 0.6.11 2403 4299 2404 4300 '@oxc-project/types@0.124.0': {} 4301 + 4302 + '@polka/url@1.0.0-next.29': {} 2405 4303 2406 4304 '@poppinss/colors@4.1.6': 2407 4305 dependencies: ··· 2547 4445 2548 4446 '@standard-schema/spec@1.1.0': {} 2549 4447 4448 + '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': 4449 + dependencies: 4450 + acorn: 8.16.0 4451 + 4452 + '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.84.1(@cloudflare/workers-types@4.20260422.1))': 4453 + dependencies: 4454 + '@cloudflare/workers-types': 4.20260422.1 4455 + '@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 4456 + worktop: 0.8.0-next.18 4457 + wrangler: 4.84.1(@cloudflare/workers-types@4.20260422.1) 4458 + 4459 + '@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': 4460 + dependencies: 4461 + '@standard-schema/spec': 1.1.0 4462 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) 4463 + '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 4464 + '@types/cookie': 0.6.0 4465 + acorn: 8.16.0 4466 + cookie: 0.6.0 4467 + devalue: 5.7.1 4468 + esm-env: 1.2.2 4469 + kleur: 4.1.5 4470 + magic-string: 0.30.21 4471 + mrmime: 2.0.1 4472 + set-cookie-parser: 3.1.0 4473 + sirv: 3.0.2 4474 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 4475 + vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) 4476 + optionalDependencies: 4477 + typescript: 6.0.3 4478 + 4479 + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': 4480 + dependencies: 4481 + deepmerge: 4.3.1 4482 + magic-string: 0.30.21 4483 + obug: 2.1.1 4484 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 4485 + vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) 4486 + vitefu: 1.1.3(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 4487 + 4488 + '@swc/helpers@0.5.21': 4489 + dependencies: 4490 + tslib: 2.8.1 4491 + 4492 + '@tailwindcss/forms@0.5.11(tailwindcss@4.2.4)': 4493 + dependencies: 4494 + mini-svg-data-uri: 1.4.4 4495 + tailwindcss: 4.2.4 4496 + 4497 + '@tailwindcss/node@4.2.4': 4498 + dependencies: 4499 + '@jridgewell/remapping': 2.3.5 4500 + enhanced-resolve: 5.20.1 4501 + jiti: 2.6.1 4502 + lightningcss: 1.32.0 4503 + magic-string: 0.30.21 4504 + source-map-js: 1.2.1 4505 + tailwindcss: 4.2.4 4506 + 4507 + '@tailwindcss/oxide-android-arm64@4.2.4': 4508 + optional: true 4509 + 4510 + '@tailwindcss/oxide-darwin-arm64@4.2.4': 4511 + optional: true 4512 + 4513 + '@tailwindcss/oxide-darwin-x64@4.2.4': 4514 + optional: true 4515 + 4516 + '@tailwindcss/oxide-freebsd-x64@4.2.4': 4517 + optional: true 4518 + 4519 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': 4520 + optional: true 4521 + 4522 + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': 4523 + optional: true 4524 + 4525 + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': 4526 + optional: true 4527 + 4528 + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': 4529 + optional: true 4530 + 4531 + '@tailwindcss/oxide-linux-x64-musl@4.2.4': 4532 + optional: true 4533 + 4534 + '@tailwindcss/oxide-wasm32-wasi@4.2.4': 4535 + optional: true 4536 + 4537 + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': 4538 + optional: true 4539 + 4540 + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': 4541 + optional: true 4542 + 4543 + '@tailwindcss/oxide@4.2.4': 4544 + optionalDependencies: 4545 + '@tailwindcss/oxide-android-arm64': 4.2.4 4546 + '@tailwindcss/oxide-darwin-arm64': 4.2.4 4547 + '@tailwindcss/oxide-darwin-x64': 4.2.4 4548 + '@tailwindcss/oxide-freebsd-x64': 4.2.4 4549 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.4 4550 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.4 4551 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.4 4552 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.4 4553 + '@tailwindcss/oxide-linux-x64-musl': 4.2.4 4554 + '@tailwindcss/oxide-wasm32-wasi': 4.2.4 4555 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 4556 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 4557 + 4558 + '@tailwindcss/vite@4.2.4(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': 4559 + dependencies: 4560 + '@tailwindcss/node': 4.2.4 4561 + '@tailwindcss/oxide': 4.2.4 4562 + tailwindcss: 4.2.4 4563 + vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) 4564 + 4565 + '@tiptap/core@3.22.4(@tiptap/pm@3.22.4)': 4566 + dependencies: 4567 + '@tiptap/pm': 3.22.4 4568 + 4569 + '@tiptap/extension-blockquote@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': 4570 + dependencies: 4571 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4572 + 4573 + '@tiptap/extension-bold@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': 4574 + dependencies: 4575 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4576 + 4577 + '@tiptap/extension-bubble-menu@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': 4578 + dependencies: 4579 + '@floating-ui/dom': 1.7.6 4580 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4581 + '@tiptap/pm': 3.22.4 4582 + 4583 + '@tiptap/extension-bullet-list@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': 4584 + dependencies: 4585 + '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4586 + 4587 + '@tiptap/extension-code-block-lowlight@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(highlight.js@11.11.1)(lowlight@3.3.0)': 4588 + dependencies: 4589 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4590 + '@tiptap/extension-code-block': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4591 + '@tiptap/pm': 3.22.4 4592 + highlight.js: 11.11.1 4593 + lowlight: 3.3.0 4594 + 4595 + '@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': 4596 + dependencies: 4597 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4598 + '@tiptap/pm': 3.22.4 4599 + 4600 + '@tiptap/extension-code@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': 4601 + dependencies: 4602 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4603 + 4604 + '@tiptap/extension-document@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': 4605 + dependencies: 4606 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4607 + 4608 + '@tiptap/extension-dropcursor@3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': 4609 + dependencies: 4610 + '@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4611 + 4612 + '@tiptap/extension-floating-menu@3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': 4613 + dependencies: 4614 + '@floating-ui/dom': 1.7.6 4615 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4616 + '@tiptap/pm': 3.22.4 4617 + 4618 + '@tiptap/extension-gapcursor@3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': 4619 + dependencies: 4620 + '@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4621 + 4622 + '@tiptap/extension-hard-break@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': 4623 + dependencies: 4624 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4625 + 4626 + '@tiptap/extension-heading@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': 4627 + dependencies: 4628 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4629 + 4630 + '@tiptap/extension-history@3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': 4631 + dependencies: 4632 + '@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4633 + 4634 + '@tiptap/extension-horizontal-rule@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': 4635 + dependencies: 4636 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4637 + '@tiptap/pm': 3.22.4 4638 + 4639 + '@tiptap/extension-image@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': 4640 + dependencies: 4641 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4642 + 4643 + '@tiptap/extension-italic@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': 4644 + dependencies: 4645 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4646 + 4647 + '@tiptap/extension-link@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': 4648 + dependencies: 4649 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4650 + '@tiptap/pm': 3.22.4 4651 + linkifyjs: 4.3.2 4652 + 4653 + '@tiptap/extension-list-item@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': 4654 + dependencies: 4655 + '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4656 + 4657 + '@tiptap/extension-list-keymap@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': 4658 + dependencies: 4659 + '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4660 + 4661 + '@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': 4662 + dependencies: 4663 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4664 + '@tiptap/pm': 3.22.4 4665 + 4666 + '@tiptap/extension-ordered-list@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': 4667 + dependencies: 4668 + '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4669 + 4670 + '@tiptap/extension-paragraph@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': 4671 + dependencies: 4672 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4673 + 4674 + '@tiptap/extension-placeholder@3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': 4675 + dependencies: 4676 + '@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4677 + 4678 + '@tiptap/extension-strike@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': 4679 + dependencies: 4680 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4681 + 4682 + '@tiptap/extension-text@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': 4683 + dependencies: 4684 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4685 + 4686 + '@tiptap/extension-typography@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': 4687 + dependencies: 4688 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4689 + 4690 + '@tiptap/extension-underline@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': 4691 + dependencies: 4692 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4693 + 4694 + '@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': 4695 + dependencies: 4696 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4697 + '@tiptap/pm': 3.22.4 4698 + 4699 + '@tiptap/markdown@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': 4700 + dependencies: 4701 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4702 + '@tiptap/pm': 3.22.4 4703 + marked: 17.0.6 4704 + 4705 + '@tiptap/pm@3.22.4': 4706 + dependencies: 4707 + prosemirror-changeset: 2.4.1 4708 + prosemirror-commands: 1.7.1 4709 + prosemirror-dropcursor: 1.8.2 4710 + prosemirror-gapcursor: 1.4.1 4711 + prosemirror-history: 1.5.0 4712 + prosemirror-keymap: 1.2.3 4713 + prosemirror-model: 1.25.4 4714 + prosemirror-schema-list: 1.5.1 4715 + prosemirror-state: 1.4.4 4716 + prosemirror-tables: 1.8.5 4717 + prosemirror-transform: 1.12.0 4718 + prosemirror-view: 1.41.8 4719 + 4720 + '@tiptap/starter-kit@3.22.4': 4721 + dependencies: 4722 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4723 + '@tiptap/extension-blockquote': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4724 + '@tiptap/extension-bold': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4725 + '@tiptap/extension-bullet-list': 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) 4726 + '@tiptap/extension-code': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4727 + '@tiptap/extension-code-block': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4728 + '@tiptap/extension-document': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4729 + '@tiptap/extension-dropcursor': 3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) 4730 + '@tiptap/extension-gapcursor': 3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) 4731 + '@tiptap/extension-hard-break': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4732 + '@tiptap/extension-heading': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4733 + '@tiptap/extension-horizontal-rule': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4734 + '@tiptap/extension-italic': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4735 + '@tiptap/extension-link': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4736 + '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4737 + '@tiptap/extension-list-item': 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) 4738 + '@tiptap/extension-list-keymap': 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) 4739 + '@tiptap/extension-ordered-list': 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) 4740 + '@tiptap/extension-paragraph': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4741 + '@tiptap/extension-strike': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4742 + '@tiptap/extension-text': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4743 + '@tiptap/extension-underline': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) 4744 + '@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 4745 + '@tiptap/pm': 3.22.4 4746 + 4747 + '@tiptap/suggestion@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': 4748 + dependencies: 4749 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 4750 + '@tiptap/pm': 3.22.4 4751 + 2550 4752 '@tybys/wasm-util@0.10.1': 2551 4753 dependencies: 2552 4754 tslib: 2.8.1 ··· 2557 4759 '@types/deep-eql': 4.0.2 2558 4760 assertion-error: 2.0.1 2559 4761 4762 + '@types/cookie@0.6.0': {} 4763 + 2560 4764 '@types/deep-eql@4.0.2': {} 2561 4765 4766 + '@types/esrecurse@4.3.1': {} 4767 + 2562 4768 '@types/estree@1.0.8': {} 4769 + 4770 + '@types/hast@3.0.4': 4771 + dependencies: 4772 + '@types/unist': 3.0.3 4773 + 4774 + '@types/json-schema@7.0.15': {} 2563 4775 2564 4776 '@types/node@12.20.55': {} 2565 4777 ··· 2572 4784 '@types/node': 25.5.0 2573 4785 pg-protocol: 1.13.0 2574 4786 pg-types: 2.2.0 4787 + 4788 + '@types/trusted-types@2.0.7': {} 4789 + 4790 + '@types/unist@3.0.3': {} 4791 + 4792 + '@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)': 4793 + dependencies: 4794 + '@eslint-community/regexpp': 4.12.2 4795 + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) 4796 + '@typescript-eslint/scope-manager': 8.59.0 4797 + '@typescript-eslint/type-utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) 4798 + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) 4799 + '@typescript-eslint/visitor-keys': 8.59.0 4800 + eslint: 10.2.1(jiti@2.6.1) 4801 + ignore: 7.0.5 4802 + natural-compare: 1.4.0 4803 + ts-api-utils: 2.5.0(typescript@6.0.3) 4804 + typescript: 6.0.3 4805 + transitivePeerDependencies: 4806 + - supports-color 4807 + 4808 + '@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)': 4809 + dependencies: 4810 + '@typescript-eslint/scope-manager': 8.59.0 4811 + '@typescript-eslint/types': 8.59.0 4812 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) 4813 + '@typescript-eslint/visitor-keys': 8.59.0 4814 + debug: 4.4.3 4815 + eslint: 10.2.1(jiti@2.6.1) 4816 + typescript: 6.0.3 4817 + transitivePeerDependencies: 4818 + - supports-color 4819 + 4820 + '@typescript-eslint/project-service@8.59.0(typescript@6.0.3)': 4821 + dependencies: 4822 + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3) 4823 + '@typescript-eslint/types': 8.59.0 4824 + debug: 4.4.3 4825 + typescript: 6.0.3 4826 + transitivePeerDependencies: 4827 + - supports-color 4828 + 4829 + '@typescript-eslint/scope-manager@8.59.0': 4830 + dependencies: 4831 + '@typescript-eslint/types': 8.59.0 4832 + '@typescript-eslint/visitor-keys': 8.59.0 4833 + 4834 + '@typescript-eslint/tsconfig-utils@8.59.0(typescript@6.0.3)': 4835 + dependencies: 4836 + typescript: 6.0.3 4837 + 4838 + '@typescript-eslint/type-utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)': 4839 + dependencies: 4840 + '@typescript-eslint/types': 8.59.0 4841 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) 4842 + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) 4843 + debug: 4.4.3 4844 + eslint: 10.2.1(jiti@2.6.1) 4845 + ts-api-utils: 2.5.0(typescript@6.0.3) 4846 + typescript: 6.0.3 4847 + transitivePeerDependencies: 4848 + - supports-color 4849 + 4850 + '@typescript-eslint/types@8.59.0': {} 4851 + 4852 + '@typescript-eslint/typescript-estree@8.59.0(typescript@6.0.3)': 4853 + dependencies: 4854 + '@typescript-eslint/project-service': 8.59.0(typescript@6.0.3) 4855 + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3) 4856 + '@typescript-eslint/types': 8.59.0 4857 + '@typescript-eslint/visitor-keys': 8.59.0 4858 + debug: 4.4.3 4859 + minimatch: 10.2.5 4860 + semver: 7.7.4 4861 + tinyglobby: 0.2.15 4862 + ts-api-utils: 2.5.0(typescript@6.0.3) 4863 + typescript: 6.0.3 4864 + transitivePeerDependencies: 4865 + - supports-color 4866 + 4867 + '@typescript-eslint/utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)': 4868 + dependencies: 4869 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) 4870 + '@typescript-eslint/scope-manager': 8.59.0 4871 + '@typescript-eslint/types': 8.59.0 4872 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) 4873 + eslint: 10.2.1(jiti@2.6.1) 4874 + typescript: 6.0.3 4875 + transitivePeerDependencies: 4876 + - supports-color 4877 + 4878 + '@typescript-eslint/visitor-keys@8.59.0': 4879 + dependencies: 4880 + '@typescript-eslint/types': 8.59.0 4881 + eslint-visitor-keys: 5.0.1 4882 + 4883 + '@use-gesture/core@10.3.1': {} 4884 + 4885 + '@use-gesture/vanilla@10.3.1': 4886 + dependencies: 4887 + '@use-gesture/core': 10.3.1 2575 4888 2576 4889 '@vitest/expect@4.1.0': 2577 4890 dependencies: ··· 2614 4927 convert-source-map: 2.0.0 2615 4928 tinyrainbow: 3.1.0 2616 4929 4930 + acorn-jsx@5.3.2(acorn@8.16.0): 4931 + dependencies: 4932 + acorn: 8.16.0 4933 + 2617 4934 acorn@8.16.0: {} 4935 + 4936 + ajv@6.14.0: 4937 + dependencies: 4938 + fast-deep-equal: 3.1.3 4939 + fast-json-stable-stringify: 2.1.0 4940 + json-schema-traverse: 0.4.1 4941 + uri-js: 4.4.1 2618 4942 2619 4943 ansi-colors@4.1.3: {} 2620 4944 ··· 2628 4952 2629 4953 argparse@2.0.1: {} 2630 4954 4955 + aria-query@5.3.1: {} 4956 + 2631 4957 array-union@2.1.0: {} 2632 4958 2633 4959 assertion-error@2.0.1: {} 4960 + 4961 + axobject-query@4.1.0: {} 4962 + 4963 + balanced-match@4.0.4: {} 2634 4964 2635 4965 better-path-resolve@1.0.0: 2636 4966 dependencies: 2637 4967 is-windows: 1.0.2 2638 4968 4969 + bits-ui@2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)): 4970 + dependencies: 4971 + '@floating-ui/core': 1.7.5 4972 + '@floating-ui/dom': 1.7.6 4973 + '@internationalized/date': 3.12.1 4974 + esm-env: 1.2.2 4975 + runed: 0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 4976 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 4977 + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 4978 + tabbable: 6.4.0 4979 + transitivePeerDependencies: 4980 + - '@sveltejs/kit' 4981 + 2639 4982 blake3-wasm@2.1.5: {} 4983 + 4984 + brace-expansion@5.0.5: 4985 + dependencies: 4986 + balanced-match: 4.0.4 2640 4987 2641 4988 braces@3.0.3: 2642 4989 dependencies: ··· 2657 5004 dependencies: 2658 5005 readdirp: 4.1.2 2659 5006 5007 + clsx@2.1.1: {} 5008 + 2660 5009 commander@4.1.1: {} 2661 5010 2662 5011 confbox@0.1.8: {} ··· 2665 5014 2666 5015 convert-source-map@2.0.0: {} 2667 5016 5017 + cookie@0.6.0: {} 5018 + 2668 5019 cookie@1.1.1: {} 5020 + 5021 + core-js@3.49.0: {} 2669 5022 2670 5023 cross-spawn@7.0.6: 2671 5024 dependencies: ··· 2673 5026 shebang-command: 2.0.0 2674 5027 which: 2.0.2 2675 5028 5029 + cssesc@3.0.0: {} 5030 + 5031 + custom-event-polyfill@1.0.7: {} 5032 + 2676 5033 debug@4.4.3: 2677 5034 dependencies: 2678 5035 ms: 2.1.3 5036 + 5037 + deep-is@0.1.4: {} 5038 + 5039 + deepmerge@4.3.1: {} 5040 + 5041 + dequal@2.0.3: {} 2679 5042 2680 5043 detect-indent@6.1.0: {} 2681 5044 2682 5045 detect-libc@2.1.2: {} 2683 5046 5047 + devalue@5.7.1: {} 5048 + 5049 + devlop@1.1.0: 5050 + dependencies: 5051 + dequal: 2.0.3 5052 + 2684 5053 dir-glob@3.0.1: 2685 5054 dependencies: 2686 5055 path-type: 4.0.0 5056 + 5057 + dompurify@3.4.1: 5058 + optionalDependencies: 5059 + '@types/trusted-types': 2.0.7 5060 + 5061 + emoji-picker-element@1.29.1: {} 5062 + 5063 + enhanced-resolve@5.20.1: 5064 + dependencies: 5065 + graceful-fs: 4.2.11 5066 + tapable: 2.3.3 2687 5067 2688 5068 enquirer@2.4.1: 2689 5069 dependencies: ··· 2723 5103 '@esbuild/win32-ia32': 0.27.0 2724 5104 '@esbuild/win32-x64': 0.27.0 2725 5105 5106 + esbuild@0.27.3: 5107 + optionalDependencies: 5108 + '@esbuild/aix-ppc64': 0.27.3 5109 + '@esbuild/android-arm': 0.27.3 5110 + '@esbuild/android-arm64': 0.27.3 5111 + '@esbuild/android-x64': 0.27.3 5112 + '@esbuild/darwin-arm64': 0.27.3 5113 + '@esbuild/darwin-x64': 0.27.3 5114 + '@esbuild/freebsd-arm64': 0.27.3 5115 + '@esbuild/freebsd-x64': 0.27.3 5116 + '@esbuild/linux-arm': 0.27.3 5117 + '@esbuild/linux-arm64': 0.27.3 5118 + '@esbuild/linux-ia32': 0.27.3 5119 + '@esbuild/linux-loong64': 0.27.3 5120 + '@esbuild/linux-mips64el': 0.27.3 5121 + '@esbuild/linux-ppc64': 0.27.3 5122 + '@esbuild/linux-riscv64': 0.27.3 5123 + '@esbuild/linux-s390x': 0.27.3 5124 + '@esbuild/linux-x64': 0.27.3 5125 + '@esbuild/netbsd-arm64': 0.27.3 5126 + '@esbuild/netbsd-x64': 0.27.3 5127 + '@esbuild/openbsd-arm64': 0.27.3 5128 + '@esbuild/openbsd-x64': 0.27.3 5129 + '@esbuild/openharmony-arm64': 0.27.3 5130 + '@esbuild/sunos-x64': 0.27.3 5131 + '@esbuild/win32-arm64': 0.27.3 5132 + '@esbuild/win32-ia32': 0.27.3 5133 + '@esbuild/win32-x64': 0.27.3 5134 + 5135 + escape-string-regexp@4.0.0: {} 5136 + 5137 + eslint-config-prettier@10.1.8(eslint@10.2.1(jiti@2.6.1)): 5138 + dependencies: 5139 + eslint: 10.2.1(jiti@2.6.1) 5140 + 5141 + eslint-plugin-svelte@3.17.1(eslint@10.2.1(jiti@2.6.1))(svelte@5.55.4(@typescript-eslint/types@8.59.0)): 5142 + dependencies: 5143 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) 5144 + '@jridgewell/sourcemap-codec': 1.5.5 5145 + eslint: 10.2.1(jiti@2.6.1) 5146 + esutils: 2.0.3 5147 + globals: 16.5.0 5148 + known-css-properties: 0.37.0 5149 + postcss: 8.5.8 5150 + postcss-load-config: 3.1.4(postcss@8.5.8) 5151 + postcss-safe-parser: 7.0.1(postcss@8.5.8) 5152 + semver: 7.7.4 5153 + svelte-eslint-parser: 1.6.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 5154 + optionalDependencies: 5155 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 5156 + transitivePeerDependencies: 5157 + - ts-node 5158 + 5159 + eslint-scope@8.4.0: 5160 + dependencies: 5161 + esrecurse: 4.3.0 5162 + estraverse: 5.3.0 5163 + 5164 + eslint-scope@9.1.2: 5165 + dependencies: 5166 + '@types/esrecurse': 4.3.1 5167 + '@types/estree': 1.0.8 5168 + esrecurse: 4.3.0 5169 + estraverse: 5.3.0 5170 + 5171 + eslint-visitor-keys@3.4.3: {} 5172 + 5173 + eslint-visitor-keys@4.2.1: {} 5174 + 5175 + eslint-visitor-keys@5.0.1: {} 5176 + 5177 + eslint@10.2.1(jiti@2.6.1): 5178 + dependencies: 5179 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) 5180 + '@eslint-community/regexpp': 4.12.2 5181 + '@eslint/config-array': 0.23.5 5182 + '@eslint/config-helpers': 0.5.5 5183 + '@eslint/core': 1.2.1 5184 + '@eslint/plugin-kit': 0.7.1 5185 + '@humanfs/node': 0.16.8 5186 + '@humanwhocodes/module-importer': 1.0.1 5187 + '@humanwhocodes/retry': 0.4.3 5188 + '@types/estree': 1.0.8 5189 + ajv: 6.14.0 5190 + cross-spawn: 7.0.6 5191 + debug: 4.4.3 5192 + escape-string-regexp: 4.0.0 5193 + eslint-scope: 9.1.2 5194 + eslint-visitor-keys: 5.0.1 5195 + espree: 11.2.0 5196 + esquery: 1.7.0 5197 + esutils: 2.0.3 5198 + fast-deep-equal: 3.1.3 5199 + file-entry-cache: 8.0.0 5200 + find-up: 5.0.0 5201 + glob-parent: 6.0.2 5202 + ignore: 5.3.2 5203 + imurmurhash: 0.1.4 5204 + is-glob: 4.0.3 5205 + json-stable-stringify-without-jsonify: 1.0.1 5206 + minimatch: 10.2.5 5207 + natural-compare: 1.4.0 5208 + optionator: 0.9.4 5209 + optionalDependencies: 5210 + jiti: 2.6.1 5211 + transitivePeerDependencies: 5212 + - supports-color 5213 + 2726 5214 esm-env@1.2.2: {} 2727 5215 5216 + espree@10.4.0: 5217 + dependencies: 5218 + acorn: 8.16.0 5219 + acorn-jsx: 5.3.2(acorn@8.16.0) 5220 + eslint-visitor-keys: 4.2.1 5221 + 5222 + espree@11.2.0: 5223 + dependencies: 5224 + acorn: 8.16.0 5225 + acorn-jsx: 5.3.2(acorn@8.16.0) 5226 + eslint-visitor-keys: 5.0.1 5227 + 2728 5228 esprima@4.0.1: {} 5229 + 5230 + esquery@1.7.0: 5231 + dependencies: 5232 + estraverse: 5.3.0 5233 + 5234 + esrap@2.2.5(@typescript-eslint/types@8.59.0): 5235 + dependencies: 5236 + '@jridgewell/sourcemap-codec': 1.5.5 5237 + optionalDependencies: 5238 + '@typescript-eslint/types': 8.59.0 5239 + 5240 + esrecurse@4.3.0: 5241 + dependencies: 5242 + estraverse: 5.3.0 5243 + 5244 + estraverse@5.3.0: {} 2729 5245 2730 5246 estree-walker@3.0.3: 2731 5247 dependencies: 2732 5248 '@types/estree': 1.0.8 5249 + 5250 + esutils@2.0.3: {} 2733 5251 2734 5252 event-target-polyfill@0.0.4: {} 2735 5253 2736 5254 expect-type@1.3.0: {} 2737 5255 2738 5256 extendable-error@0.1.7: {} 5257 + 5258 + fast-deep-equal@3.1.3: {} 2739 5259 2740 5260 fast-glob@3.3.3: 2741 5261 dependencies: ··· 2745 5265 merge2: 1.4.1 2746 5266 micromatch: 4.0.8 2747 5267 5268 + fast-json-stable-stringify@2.1.0: {} 5269 + 5270 + fast-levenshtein@2.0.6: {} 5271 + 2748 5272 fastq@1.20.1: 2749 5273 dependencies: 2750 5274 reusify: 1.1.0 ··· 2752 5276 fdir@6.5.0(picomatch@4.0.4): 2753 5277 optionalDependencies: 2754 5278 picomatch: 4.0.4 5279 + 5280 + file-entry-cache@8.0.0: 5281 + dependencies: 5282 + flat-cache: 4.0.1 2755 5283 2756 5284 fill-range@7.1.1: 2757 5285 dependencies: ··· 2762 5290 locate-path: 5.0.0 2763 5291 path-exists: 4.0.0 2764 5292 5293 + find-up@5.0.0: 5294 + dependencies: 5295 + locate-path: 6.0.0 5296 + path-exists: 4.0.0 5297 + 2765 5298 fix-dts-default-cjs-exports@1.0.1: 2766 5299 dependencies: 2767 5300 magic-string: 0.30.21 2768 5301 mlly: 1.8.2 2769 5302 rollup: 4.60.1 5303 + 5304 + flat-cache@4.0.1: 5305 + dependencies: 5306 + flatted: 3.4.2 5307 + keyv: 4.5.4 5308 + 5309 + flatted@3.4.2: {} 2770 5310 2771 5311 fs-extra@7.0.1: 2772 5312 dependencies: ··· 2791 5331 dependencies: 2792 5332 is-glob: 4.0.3 2793 5333 5334 + glob-parent@6.0.2: 5335 + dependencies: 5336 + is-glob: 4.0.3 5337 + 5338 + globals@16.5.0: {} 5339 + 5340 + globals@17.5.0: {} 5341 + 2794 5342 globby@11.1.0: 2795 5343 dependencies: 2796 5344 array-union: 2.1.0 ··· 2802 5350 2803 5351 graceful-fs@4.2.11: {} 2804 5352 5353 + highlight.js@11.11.1: {} 5354 + 5355 + hls.js@1.6.16: {} 5356 + 2805 5357 hono@4.12.8: {} 2806 5358 2807 5359 human-id@4.1.3: {} ··· 2812 5364 2813 5365 ignore@5.3.2: {} 2814 5366 5367 + ignore@7.0.5: {} 5368 + 5369 + imurmurhash@0.1.4: {} 5370 + 5371 + inline-style-parser@0.2.7: {} 5372 + 5373 + is-emoji-supported@0.0.5: {} 5374 + 2815 5375 is-extglob@2.1.1: {} 2816 5376 2817 5377 is-glob@4.0.3: ··· 2819 5379 is-extglob: 2.1.1 2820 5380 2821 5381 is-number@7.0.0: {} 5382 + 5383 + is-reference@3.0.3: 5384 + dependencies: 5385 + '@types/estree': 1.0.8 2822 5386 2823 5387 is-subdir@1.2.0: 2824 5388 dependencies: ··· 2828 5392 2829 5393 isexe@2.0.0: {} 2830 5394 2831 - jiti@2.6.1: 2832 - optional: true 5395 + jiti@2.6.1: {} 2833 5396 2834 5397 joycon@3.1.1: {} 2835 5398 ··· 2842 5405 dependencies: 2843 5406 argparse: 2.0.1 2844 5407 5408 + json-buffer@3.0.1: {} 5409 + 5410 + json-schema-traverse@0.4.1: {} 5411 + 5412 + json-stable-stringify-without-jsonify@1.0.1: {} 5413 + 2845 5414 jsonfile@4.0.0: 2846 5415 optionalDependencies: 2847 5416 graceful-fs: 4.2.11 2848 5417 5418 + keyv@4.5.4: 5419 + dependencies: 5420 + json-buffer: 3.0.1 5421 + 2849 5422 kleur@4.1.5: {} 5423 + 5424 + known-css-properties@0.37.0: {} 5425 + 5426 + levn@0.4.1: 5427 + dependencies: 5428 + prelude-ls: 1.2.1 5429 + type-check: 0.4.0 2850 5430 2851 5431 lightningcss-android-arm64@1.32.0: 2852 5432 optional: true ··· 2897 5477 lightningcss-win32-arm64-msvc: 1.32.0 2898 5478 lightningcss-win32-x64-msvc: 1.32.0 2899 5479 5480 + lilconfig@2.1.0: {} 5481 + 2900 5482 lilconfig@3.1.3: {} 2901 5483 2902 5484 lines-and-columns@1.2.4: {} 2903 5485 5486 + linkifyjs@4.3.2: {} 5487 + 2904 5488 load-tsconfig@0.2.5: {} 5489 + 5490 + loadjs@4.3.0: {} 5491 + 5492 + locate-character@3.0.0: {} 2905 5493 2906 5494 locate-path@5.0.0: 2907 5495 dependencies: 2908 5496 p-locate: 4.1.0 2909 5497 5498 + locate-path@6.0.0: 5499 + dependencies: 5500 + p-locate: 5.0.0 5501 + 2910 5502 lodash.startcase@4.4.0: {} 5503 + 5504 + lowlight@3.3.0: 5505 + dependencies: 5506 + '@types/hast': 3.0.4 5507 + devlop: 1.1.0 5508 + highlight.js: 11.11.1 5509 + 5510 + lz-string@1.5.0: {} 2911 5511 2912 5512 magic-string@0.30.21: 2913 5513 dependencies: 2914 5514 '@jridgewell/sourcemap-codec': 1.5.5 5515 + 5516 + marked@17.0.6: {} 2915 5517 2916 5518 merge2@1.4.1: {} 2917 5519 ··· 2919 5521 dependencies: 2920 5522 braces: 3.0.3 2921 5523 picomatch: 2.3.2 5524 + 5525 + mini-svg-data-uri@1.4.4: {} 2922 5526 2923 5527 miniflare@4.20260205.0: 2924 5528 dependencies: ··· 2932 5536 - bufferutil 2933 5537 - utf-8-validate 2934 5538 5539 + miniflare@4.20260421.0: 5540 + dependencies: 5541 + '@cspotcode/source-map-support': 0.8.1 5542 + sharp: 0.34.5 5543 + undici: 7.24.8 5544 + workerd: 1.20260421.1 5545 + ws: 8.18.0 5546 + youch: 4.1.0-beta.10 5547 + transitivePeerDependencies: 5548 + - bufferutil 5549 + - utf-8-validate 5550 + 5551 + minimatch@10.2.5: 5552 + dependencies: 5553 + brace-expansion: 5.0.5 5554 + 2935 5555 mlly@1.8.2: 2936 5556 dependencies: 2937 5557 acorn: 8.16.0 ··· 2939 5559 pkg-types: 1.3.1 2940 5560 ufo: 1.6.3 2941 5561 5562 + mode-watcher@1.1.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): 5563 + dependencies: 5564 + runed: 0.25.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 5565 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 5566 + svelte-toolbelt: 0.7.1(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 5567 + 2942 5568 mri@1.2.0: {} 5569 + 5570 + mrmime@2.0.1: {} 2943 5571 2944 5572 ms@2.1.3: {} 2945 5573 ··· 2953 5581 2954 5582 nanoid@5.1.7: {} 2955 5583 5584 + natural-compare@1.4.0: {} 5585 + 5586 + number-flow@0.6.0: 5587 + dependencies: 5588 + esm-env: 1.2.2 5589 + 2956 5590 object-assign@4.1.1: {} 2957 5591 2958 5592 obug@2.1.1: {} 5593 + 5594 + optionator@0.9.4: 5595 + dependencies: 5596 + deep-is: 0.1.4 5597 + fast-levenshtein: 2.0.6 5598 + levn: 0.4.1 5599 + prelude-ls: 1.2.1 5600 + type-check: 0.4.0 5601 + word-wrap: 1.2.5 5602 + 5603 + orderedmap@2.1.1: {} 2959 5604 2960 5605 outdent@0.5.0: {} 2961 5606 ··· 2967 5612 dependencies: 2968 5613 p-try: 2.2.0 2969 5614 5615 + p-limit@3.1.0: 5616 + dependencies: 5617 + yocto-queue: 0.1.0 5618 + 2970 5619 p-locate@4.1.0: 2971 5620 dependencies: 2972 5621 p-limit: 2.3.0 5622 + 5623 + p-locate@5.0.0: 5624 + dependencies: 5625 + p-limit: 3.1.0 2973 5626 2974 5627 p-map@2.1.0: {} 2975 5628 ··· 3046 5699 mlly: 1.8.2 3047 5700 pathe: 2.0.3 3048 5701 5702 + plyr@3.8.4: 5703 + dependencies: 5704 + core-js: 3.49.0 5705 + custom-event-polyfill: 1.0.7 5706 + loadjs: 4.3.0 5707 + rangetouch: 2.0.1 5708 + url-polyfill: 1.1.14 5709 + 5710 + postcss-load-config@3.1.4(postcss@8.5.8): 5711 + dependencies: 5712 + lilconfig: 2.1.0 5713 + yaml: 1.10.3 5714 + optionalDependencies: 5715 + postcss: 8.5.8 5716 + 3049 5717 postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0): 3050 5718 dependencies: 3051 5719 lilconfig: 3.1.3 ··· 3053 5721 jiti: 2.6.1 3054 5722 postcss: 8.5.8 3055 5723 tsx: 4.21.0 5724 + 5725 + postcss-safe-parser@7.0.1(postcss@8.5.8): 5726 + dependencies: 5727 + postcss: 8.5.8 5728 + 5729 + postcss-scss@4.0.9(postcss@8.5.8): 5730 + dependencies: 5731 + postcss: 8.5.8 5732 + 5733 + postcss-selector-parser@7.1.1: 5734 + dependencies: 5735 + cssesc: 3.0.0 5736 + util-deprecate: 1.0.2 3056 5737 3057 5738 postcss@8.5.8: 3058 5739 dependencies: ··· 3070 5751 dependencies: 3071 5752 xtend: 4.0.2 3072 5753 5754 + prelude-ls@1.2.1: {} 5755 + 5756 + prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0)): 5757 + dependencies: 5758 + prettier: 3.8.1 5759 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 5760 + 5761 + prettier-plugin-tailwindcss@0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0)))(prettier@3.8.1): 5762 + dependencies: 5763 + prettier: 3.8.1 5764 + optionalDependencies: 5765 + prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 5766 + 3073 5767 prettier@2.8.8: {} 3074 5768 3075 5769 prettier@3.8.1: {} 3076 5770 5771 + prosemirror-changeset@2.4.1: 5772 + dependencies: 5773 + prosemirror-transform: 1.12.0 5774 + 5775 + prosemirror-commands@1.7.1: 5776 + dependencies: 5777 + prosemirror-model: 1.25.4 5778 + prosemirror-state: 1.4.4 5779 + prosemirror-transform: 1.12.0 5780 + 5781 + prosemirror-dropcursor@1.8.2: 5782 + dependencies: 5783 + prosemirror-state: 1.4.4 5784 + prosemirror-transform: 1.12.0 5785 + prosemirror-view: 1.41.8 5786 + 5787 + prosemirror-gapcursor@1.4.1: 5788 + dependencies: 5789 + prosemirror-keymap: 1.2.3 5790 + prosemirror-model: 1.25.4 5791 + prosemirror-state: 1.4.4 5792 + prosemirror-view: 1.41.8 5793 + 5794 + prosemirror-history@1.5.0: 5795 + dependencies: 5796 + prosemirror-state: 1.4.4 5797 + prosemirror-transform: 1.12.0 5798 + prosemirror-view: 1.41.8 5799 + rope-sequence: 1.3.4 5800 + 5801 + prosemirror-keymap@1.2.3: 5802 + dependencies: 5803 + prosemirror-state: 1.4.4 5804 + w3c-keyname: 2.2.8 5805 + 5806 + prosemirror-model@1.25.4: 5807 + dependencies: 5808 + orderedmap: 2.1.1 5809 + 5810 + prosemirror-schema-list@1.5.1: 5811 + dependencies: 5812 + prosemirror-model: 1.25.4 5813 + prosemirror-state: 1.4.4 5814 + prosemirror-transform: 1.12.0 5815 + 5816 + prosemirror-state@1.4.4: 5817 + dependencies: 5818 + prosemirror-model: 1.25.4 5819 + prosemirror-transform: 1.12.0 5820 + prosemirror-view: 1.41.8 5821 + 5822 + prosemirror-tables@1.8.5: 5823 + dependencies: 5824 + prosemirror-keymap: 1.2.3 5825 + prosemirror-model: 1.25.4 5826 + prosemirror-state: 1.4.4 5827 + prosemirror-transform: 1.12.0 5828 + prosemirror-view: 1.41.8 5829 + 5830 + prosemirror-transform@1.12.0: 5831 + dependencies: 5832 + prosemirror-model: 1.25.4 5833 + 5834 + prosemirror-view@1.41.8: 5835 + dependencies: 5836 + prosemirror-model: 1.25.4 5837 + prosemirror-state: 1.4.4 5838 + prosemirror-transform: 1.12.0 5839 + 5840 + punycode@2.3.1: {} 5841 + 3077 5842 quansync@0.2.11: {} 3078 5843 3079 5844 queue-microtask@1.2.3: {} 5845 + 5846 + rangetouch@2.0.1: {} 3080 5847 3081 5848 read-yaml-file@1.1.0: 3082 5849 dependencies: ··· 3086 5853 strip-bom: 3.0.0 3087 5854 3088 5855 readdirp@4.1.2: {} 5856 + 5857 + regexparam@3.0.0: {} 3089 5858 3090 5859 resolve-from@5.0.0: {} 3091 5860 ··· 3145 5914 '@rollup/rollup-win32-x64-msvc': 4.60.1 3146 5915 fsevents: 2.3.3 3147 5916 5917 + rope-sequence@1.3.4: {} 5918 + 3148 5919 run-parallel@1.2.0: 3149 5920 dependencies: 3150 5921 queue-microtask: 1.2.3 3151 5922 5923 + runed@0.23.4(svelte@5.55.4(@typescript-eslint/types@8.59.0)): 5924 + dependencies: 5925 + esm-env: 1.2.2 5926 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 5927 + 5928 + runed@0.25.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): 5929 + dependencies: 5930 + esm-env: 1.2.2 5931 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 5932 + 5933 + runed@0.28.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): 5934 + dependencies: 5935 + esm-env: 1.2.2 5936 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 5937 + 5938 + runed@0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)): 5939 + dependencies: 5940 + dequal: 2.0.3 5941 + esm-env: 1.2.2 5942 + lz-string: 1.5.0 5943 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 5944 + optionalDependencies: 5945 + '@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 5946 + 5947 + sade@1.8.1: 5948 + dependencies: 5949 + mri: 1.2.0 5950 + 3152 5951 safer-buffer@2.1.2: {} 3153 5952 3154 5953 semver@7.7.4: {} 5954 + 5955 + set-cookie-parser@3.1.0: {} 3155 5956 3156 5957 sharp@0.34.5: 3157 5958 dependencies: ··· 3194 5995 3195 5996 signal-exit@4.1.0: {} 3196 5997 5998 + sirv@3.0.2: 5999 + dependencies: 6000 + '@polka/url': 1.0.0-next.29 6001 + mrmime: 2.0.1 6002 + totalist: 3.0.1 6003 + 3197 6004 slash@3.0.0: {} 3198 6005 3199 6006 source-map-js@1.2.1: {} ··· 3219 6026 3220 6027 strip-bom@3.0.0: {} 3221 6028 6029 + style-to-object@1.0.14: 6030 + dependencies: 6031 + inline-style-parser: 0.2.7 6032 + 3222 6033 sucrase@3.35.1: 3223 6034 dependencies: 3224 6035 '@jridgewell/gen-mapping': 0.3.13 ··· 3230 6041 ts-interface-checker: 0.1.13 3231 6042 3232 6043 supports-color@10.2.2: {} 6044 + 6045 + svelte-check@4.4.6(picomatch@4.0.4)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3): 6046 + dependencies: 6047 + '@jridgewell/trace-mapping': 0.3.31 6048 + chokidar: 4.0.3 6049 + fdir: 6.5.0(picomatch@4.0.4) 6050 + picocolors: 1.1.1 6051 + sade: 1.8.1 6052 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 6053 + typescript: 6.0.3 6054 + transitivePeerDependencies: 6055 + - picomatch 6056 + 6057 + svelte-eslint-parser@1.6.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): 6058 + dependencies: 6059 + eslint-scope: 8.4.0 6060 + eslint-visitor-keys: 4.2.1 6061 + espree: 10.4.0 6062 + postcss: 8.5.8 6063 + postcss-scss: 4.0.9(postcss@8.5.8) 6064 + postcss-selector-parser: 7.1.1 6065 + semver: 7.7.4 6066 + optionalDependencies: 6067 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 6068 + 6069 + svelte-sonner@1.1.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): 6070 + dependencies: 6071 + runed: 0.28.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 6072 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 6073 + 6074 + svelte-tiptap@3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/extension-bubble-menu@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extension-floating-menu@3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(svelte@5.55.4(@typescript-eslint/types@8.59.0)): 6075 + dependencies: 6076 + '@floating-ui/dom': 1.7.6 6077 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) 6078 + '@tiptap/extension-bubble-menu': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 6079 + '@tiptap/extension-floating-menu': 3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) 6080 + '@tiptap/pm': 3.22.4 6081 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 6082 + 6083 + svelte-toolbelt@0.10.6(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)): 6084 + dependencies: 6085 + clsx: 2.1.1 6086 + runed: 0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 6087 + style-to-object: 1.0.14 6088 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 6089 + transitivePeerDependencies: 6090 + - '@sveltejs/kit' 6091 + 6092 + svelte-toolbelt@0.7.1(svelte@5.55.4(@typescript-eslint/types@8.59.0)): 6093 + dependencies: 6094 + clsx: 2.1.1 6095 + runed: 0.23.4(svelte@5.55.4(@typescript-eslint/types@8.59.0)) 6096 + style-to-object: 1.0.14 6097 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 6098 + 6099 + svelte@5.55.4(@typescript-eslint/types@8.59.0): 6100 + dependencies: 6101 + '@jridgewell/remapping': 2.3.5 6102 + '@jridgewell/sourcemap-codec': 1.5.5 6103 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) 6104 + '@types/estree': 1.0.8 6105 + '@types/trusted-types': 2.0.7 6106 + acorn: 8.16.0 6107 + aria-query: 5.3.1 6108 + axobject-query: 4.1.0 6109 + clsx: 2.1.1 6110 + devalue: 5.7.1 6111 + esm-env: 1.2.2 6112 + esrap: 2.2.5(@typescript-eslint/types@8.59.0) 6113 + is-reference: 3.0.3 6114 + locate-character: 3.0.0 6115 + magic-string: 0.30.21 6116 + zimmerframe: 1.1.4 6117 + transitivePeerDependencies: 6118 + - '@typescript-eslint/types' 6119 + 6120 + tabbable@6.4.0: {} 6121 + 6122 + tailwind-merge@3.5.0: {} 6123 + 6124 + tailwind-variants@3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.4): 6125 + dependencies: 6126 + tailwindcss: 4.2.4 6127 + optionalDependencies: 6128 + tailwind-merge: 3.5.0 6129 + 6130 + tailwindcss@4.2.4: {} 6131 + 6132 + tapable@2.3.3: {} 3233 6133 3234 6134 term-size@2.2.1: {} 3235 6135 ··· 3258 6158 dependencies: 3259 6159 is-number: 7.0.0 3260 6160 6161 + totalist@3.0.1: {} 6162 + 3261 6163 tree-kill@1.2.2: {} 6164 + 6165 + ts-api-utils@2.5.0(typescript@6.0.3): 6166 + dependencies: 6167 + typescript: 6.0.3 3262 6168 3263 6169 ts-interface-checker@0.1.13: {} 3264 6170 3265 - tslib@2.8.1: 3266 - optional: true 6171 + tslib@2.8.1: {} 3267 6172 3268 6173 tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3): 3269 6174 dependencies: ··· 3300 6205 optionalDependencies: 3301 6206 fsevents: 2.3.3 3302 6207 6208 + type-check@0.4.0: 6209 + dependencies: 6210 + prelude-ls: 1.2.1 6211 + 3303 6212 type-fest@4.41.0: {} 3304 6213 6214 + typescript-eslint@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3): 6215 + dependencies: 6216 + '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) 6217 + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) 6218 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) 6219 + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) 6220 + eslint: 10.2.1(jiti@2.6.1) 6221 + typescript: 6.0.3 6222 + transitivePeerDependencies: 6223 + - supports-color 6224 + 3305 6225 typescript@5.9.3: {} 6226 + 6227 + typescript@6.0.3: {} 3306 6228 3307 6229 ufo@1.6.3: {} 3308 6230 3309 6231 undici-types@7.18.2: {} 3310 6232 3311 6233 undici@7.18.2: {} 6234 + 6235 + undici@7.24.8: {} 3312 6236 3313 6237 unenv@2.0.0-rc.24: 3314 6238 dependencies: ··· 3317 6241 unicode-segmenter@0.14.5: {} 3318 6242 3319 6243 universalify@0.1.2: {} 6244 + 6245 + uri-js@4.4.1: 6246 + dependencies: 6247 + punycode: 2.3.1 6248 + 6249 + url-polyfill@1.1.14: {} 6250 + 6251 + util-deprecate@1.0.2: {} 6252 + 6253 + valibot@1.3.1(typescript@6.0.3): 6254 + optionalDependencies: 6255 + typescript: 6.0.3 3320 6256 3321 6257 vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0): 3322 6258 dependencies: ··· 3331 6267 fsevents: 2.3.3 3332 6268 jiti: 2.6.1 3333 6269 tsx: 4.21.0 6270 + 6271 + vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0): 6272 + dependencies: 6273 + lightningcss: 1.32.0 6274 + picomatch: 4.0.4 6275 + postcss: 8.5.8 6276 + rolldown: 1.0.0-rc.15 6277 + tinyglobby: 0.2.15 6278 + optionalDependencies: 6279 + '@types/node': 25.5.0 6280 + esbuild: 0.27.3 6281 + fsevents: 2.3.3 6282 + jiti: 2.6.1 6283 + tsx: 4.21.0 6284 + 6285 + vitefu@1.1.3(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)): 6286 + optionalDependencies: 6287 + vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) 3334 6288 3335 6289 vitest@4.1.0(@types/node@25.5.0)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0)): 3336 6290 dependencies: ··· 3359 6313 transitivePeerDependencies: 3360 6314 - msw 3361 6315 6316 + w3c-keyname@2.2.8: {} 6317 + 6318 + web-haptics@0.0.6(svelte@5.55.4(@typescript-eslint/types@8.59.0)): 6319 + optionalDependencies: 6320 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) 6321 + 3362 6322 which@2.0.2: 3363 6323 dependencies: 3364 6324 isexe: 2.0.0 ··· 3368 6328 siginfo: 2.0.0 3369 6329 stackback: 0.0.2 3370 6330 6331 + word-wrap@1.2.5: {} 6332 + 3371 6333 workerd@1.20260205.0: 3372 6334 optionalDependencies: 3373 6335 '@cloudflare/workerd-darwin-64': 1.20260205.0 ··· 3375 6337 '@cloudflare/workerd-linux-64': 1.20260205.0 3376 6338 '@cloudflare/workerd-linux-arm64': 1.20260205.0 3377 6339 '@cloudflare/workerd-windows-64': 1.20260205.0 6340 + 6341 + workerd@1.20260421.1: 6342 + optionalDependencies: 6343 + '@cloudflare/workerd-darwin-64': 1.20260421.1 6344 + '@cloudflare/workerd-darwin-arm64': 1.20260421.1 6345 + '@cloudflare/workerd-linux-64': 1.20260421.1 6346 + '@cloudflare/workerd-linux-arm64': 1.20260421.1 6347 + '@cloudflare/workerd-windows-64': 1.20260421.1 6348 + 6349 + worktop@0.8.0-next.18: 6350 + dependencies: 6351 + mrmime: 2.0.1 6352 + regexparam: 3.0.0 3378 6353 3379 6354 wrangler@4.63.0(@cloudflare/workers-types@4.20260205.0): 3380 6355 dependencies: ··· 3393 6368 - bufferutil 3394 6369 - utf-8-validate 3395 6370 6371 + wrangler@4.84.1(@cloudflare/workers-types@4.20260422.1): 6372 + dependencies: 6373 + '@cloudflare/kv-asset-handler': 0.4.2 6374 + '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260421.1) 6375 + blake3-wasm: 2.1.5 6376 + esbuild: 0.27.3 6377 + miniflare: 4.20260421.0 6378 + path-to-regexp: 6.3.0 6379 + unenv: 2.0.0-rc.24 6380 + workerd: 1.20260421.1 6381 + optionalDependencies: 6382 + '@cloudflare/workers-types': 4.20260422.1 6383 + fsevents: 2.3.3 6384 + transitivePeerDependencies: 6385 + - bufferutil 6386 + - utf-8-validate 6387 + 3396 6388 ws@8.18.0: {} 3397 6389 3398 6390 xtend@4.0.2: {} 6391 + 6392 + yaml@1.10.3: {} 6393 + 6394 + yocto-queue@0.1.0: {} 3399 6395 3400 6396 yocto-queue@1.2.2: {} 3401 6397 ··· 3411 6407 '@speed-highlight/core': 1.2.14 3412 6408 cookie: 1.1.1 3413 6409 youch-core: 0.3.3 6410 + 6411 + zimmerframe@1.1.4: {}
+3
pnpm-workspace.yaml
··· 1 + packages: 2 + - "examples/sveltekit-group-chat" 3 + 1 4 ignoredBuiltDependencies: 2 5 - esbuild 3 6 - sharp
+1
tsup.config.ts
··· 6 6 "src/server.ts", 7 7 "src/generate.ts", 8 8 "src/publish.ts", 9 + "src/sync/index.ts", 9 10 "src/adapters/sqlite.ts", 10 11 "src/adapters/postgres.ts", 11 12 ],
+16
community-lexicon-templates/defs.json
··· 55 55 "did": { "type": "string", "format": "did" }, 56 56 "addedAt": { "type": "integer" } 57 57 } 58 + }, 59 + "inviteView": { 60 + "type": "object", 61 + "required": ["tokenHash", "spaceUri", "accessLevel", "createdBy", "createdAt", "usedCount"], 62 + "properties": { 63 + "tokenHash": { "type": "string", "description": "SHA-256 of the raw token. Stable id for list/revoke; never grants access on its own." }, 64 + "spaceUri": { "type": "string", "format": "at-uri" }, 65 + "accessLevel": { "type": "ref", "ref": "#accessLevel" }, 66 + "createdBy": { "type": "string", "format": "did" }, 67 + "createdAt": { "type": "integer" }, 68 + "expiresAt": { "type": "integer", "description": "Unix ms. Omitted for no expiry." }, 69 + "maxUses": { "type": "integer", "description": "Omitted for unlimited." }, 70 + "usedCount": { "type": "integer" }, 71 + "revokedAt": { "type": "integer", "description": "Unix ms. Omitted if not revoked." }, 72 + "note": { "type": "string" } 73 + } 58 74 } 59 75 } 60 76 }
+4 -1
docs/community-spec-mapping.md
··· 36 36 | Owner | `owner`; only meaningful in `$admin` for owner-management | ✅ | Post's level 8 | 37 37 | Push model for membership lists | Reconciler writes `spaces_members` after each change | ➕ | Post doesn't specify a sync direction; push keeps spaces read-path zero-overhead | 38 38 | Cross-community / cross-arbiter delegation | _same-contrail only_ | ❌ | v1 constraint. Private-membership federation is deferred — see [community.md § Deferred work] | 39 - | Invites as a separate service | _not implemented_ | ❌ | Post proposes an optional add-on; contrail has spaces invites to draw from when this lands | 39 + | Invites as a separate service | `community.invite.*` (create/list/revoke/redeem) | ➕ | Built-in — tokens pre-sign a grant, redeemer gets the encoded access level. SHA-256 at rest, atomic redeem. | 40 40 | Writing records under the arbiter's account | `community.space.putRecord` (in-space) + `community.putRecord` (public) | ✅ | In-space: `admin+`. Public: `member+` in `$publishers` — routes through adopted community's PDS | 41 41 | Public membership-list flag | _none_ | ❌ | Post allows spaces to expose membership publicly. Ruled out for cross-instance federation (privacy) | 42 42 | Space credential (from arbiter) | _none_ | ⚠️ | Covered by the spaces module's service-auth story, not the community module | ··· 63 63 ### Membership 64 64 - `community.space.grant` · `community.space.revoke` · `community.space.setAccessLevel` 65 65 - `community.space.listMembers` (`?flatten=true` for the resolved DID list) · `community.space.resync` 66 + 67 + ### Invites 68 + - `community.invite.create` · `community.invite.list` · `community.invite.revoke` · `community.invite.redeem` 66 69 67 70 ### Publishing 68 71 - `community.putRecord` · `community.deleteRecord` (public records via the community's PDS; adopted only)
+34
src/generate.ts
··· 690 690 ...hydrateDefs, ...refDefs, ...profileDefs(), 691 691 }, 692 692 }); 693 + 694 + // watchRecords — realtime subscription over the same query shape. 695 + // Only emitted when realtime is configured. SSE-only (subscription 696 + // lexicon doesn't cleanly model custom event framing, so we describe it 697 + // as a query with a streaming output and document the event kinds.) 698 + if (config.realtime) { 699 + writeLexicon(`${ns}.${shortName}.watchRecords`, { 700 + lexicon: 1, 701 + id: `${ns}.${shortName}.watchRecords`, 702 + defs: { 703 + main: { 704 + type: "query", 705 + description: 706 + `Subscribe to a live ${collection} query. Returns Server-Sent Events: ` + 707 + `\`snapshot.start\`, \`snapshot.record\` (N of), \`snapshot.end\`, ` + 708 + `then \`record.created\` / \`record.deleted\` as records enter/leave the result, ` + 709 + `plus a final \`member.removed\` if the caller loses access mid-stream. ` + 710 + `v1 requires spaceUri; cross-space watch is deferred.`, 711 + parameters: { type: "params", properties: listParams }, 712 + output: { 713 + encoding: "text/event-stream", 714 + schema: { type: "object", properties: {} }, 715 + }, 716 + errors: [ 717 + { name: "InvalidRequest" }, 718 + { name: "AuthRequired" }, 719 + { name: "Forbidden" }, 720 + { name: "NotFound" }, 721 + { name: "NotSupported" }, 722 + ], 723 + }, 724 + }, 725 + }); 726 + } 693 727 } 694 728 695 729 // --- getRecord ---
+41
src/index.ts
··· 35 35 SpacesConfig, 36 36 AppPolicy, 37 37 AppPolicyMode, 38 + AuthOverrideResult, 38 39 SpaceRow, 39 40 SpaceMemberRow, 40 41 StoredRecord, ··· 50 51 export { HostedAdapter } from "./core/spaces/adapter"; 51 52 export { nextTid } from "./core/spaces/tid"; 52 53 export { generateInviteToken, hashInviteToken } from "./core/spaces/invite-token"; 54 + export { 55 + MemoryBlobAdapter, 56 + R2BlobAdapter, 57 + blobKey, 58 + } from "./core/spaces/blob-adapter"; 59 + export type { BlobAdapter, BlobUploadMeta, R2BucketLike } from "./core/spaces/blob-adapter"; 60 + export type { SpacesBlobsConfig, BlobMetaRow } from "./core/spaces/types"; 53 61 54 62 // Realtime 55 63 export type { ··· 72 80 collectionTopic, 73 81 communityTopic, 74 82 spaceTopic, 83 + registerRealtimeRoutes, 75 84 } from "./core/realtime"; 85 + export type { 86 + DurableObjectId, 87 + DurableObjectNamespace, 88 + DurableObjectStub, 89 + DurableObjectState, 90 + } from "./core/realtime/durable-object"; 91 + 92 + // Community 93 + export { 94 + CommunityAdapter, 95 + CredentialCipher, 96 + registerCommunityRoutes, 97 + ACCESS_LEVELS, 98 + RESERVED_KEYS, 99 + isAccessLevel, 100 + isReservedKey, 101 + rankOf, 102 + resolveEffectiveLevel, 103 + flattenEffectiveMembers, 104 + wouldCycle, 105 + reconcile, 106 + } from "./core/community"; 107 + export type { 108 + CommunityConfig, 109 + CommunityMode, 110 + CommunityRow, 111 + CommunityInviteRow, 112 + CreateCommunityInviteInput, 113 + AccessLevel, 114 + AccessLevelRow, 115 + ReservedKey, 116 + } from "./core/community";
+45
community-lexicon-templates/invite/create.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.community.invite.create", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create an invite for a community-owned space. Caller must have manager+ on the target space and cannot grant a higher level than their own. Returns the raw token once; only its SHA-256 is stored.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["spaceUri", "accessLevel"], 13 + "properties": { 14 + "spaceUri": { "type": "string", "format": "at-uri" }, 15 + "accessLevel": { "type": "ref", "ref": "tools.atmo.community.defs#accessLevel" }, 16 + "expiresAt": { "type": "integer", "description": "Unix ms. Omit for no expiry." }, 17 + "maxUses": { "type": "integer", "minimum": 1, "description": "Omit for unlimited redemptions." }, 18 + "note": { "type": "string", "maxLength": 500 } 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "required": ["token", "tokenHash", "spaceUri", "accessLevel", "createdAt"], 27 + "properties": { 28 + "token": { "type": "string", "description": "Raw token. Shown once — cannot be retrieved later." }, 29 + "tokenHash": { "type": "string" }, 30 + "spaceUri": { "type": "string", "format": "at-uri" }, 31 + "accessLevel": { "type": "ref", "ref": "tools.atmo.community.defs#accessLevel" }, 32 + "expiresAt": { "type": "integer" }, 33 + "maxUses": { "type": "integer" }, 34 + "createdAt": { "type": "integer" } 35 + } 36 + } 37 + }, 38 + "errors": [ 39 + { "name": "InvalidRequest" }, 40 + { "name": "Forbidden" }, 41 + { "name": "NotFound" } 42 + ] 43 + } 44 + } 45 + }
+35
community-lexicon-templates/invite/list.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.community.invite.list", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List invites for a community-owned space. Caller must have manager+ on the space. Raw tokens are never returned — tokenHash is the stable id.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["spaceUri"], 11 + "properties": { 12 + "spaceUri": { "type": "string", "format": "at-uri" }, 13 + "includeRevoked": { "type": "boolean", "default": false } 14 + } 15 + }, 16 + "output": { 17 + "encoding": "application/json", 18 + "schema": { 19 + "type": "object", 20 + "required": ["invites"], 21 + "properties": { 22 + "invites": { 23 + "type": "array", 24 + "items": { "type": "ref", "ref": "tools.atmo.community.defs#inviteView" } 25 + } 26 + } 27 + } 28 + }, 29 + "errors": [ 30 + { "name": "Forbidden" }, 31 + { "name": "NotFound" } 32 + ] 33 + } 34 + } 35 + }
+37
community-lexicon-templates/invite/redeem.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.community.invite.redeem", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Redeem a community invite. The caller's DID (from service-auth) is granted the access level encoded in the invite. Atomic — a token is consumed only if the invite is currently usable.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["token"], 13 + "properties": { 14 + "token": { "type": "string" } 15 + } 16 + } 17 + }, 18 + "output": { 19 + "encoding": "application/json", 20 + "schema": { 21 + "type": "object", 22 + "required": ["spaceUri", "accessLevel", "communityDid"], 23 + "properties": { 24 + "spaceUri": { "type": "string", "format": "at-uri" }, 25 + "accessLevel": { "type": "ref", "ref": "tools.atmo.community.defs#accessLevel" }, 26 + "communityDid": { "type": "string", "format": "did" } 27 + } 28 + } 29 + }, 30 + "errors": [ 31 + { "name": "InvalidRequest" }, 32 + { "name": "InvalidToken" }, 33 + { "name": "NotFound" } 34 + ] 35 + } 36 + } 37 + }
+34
community-lexicon-templates/invite/revoke.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.community.invite.revoke", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Revoke an invite. Caller must be the invite's creator or have manager+ on the target space.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["tokenHash"], 13 + "properties": { 14 + "tokenHash": { "type": "string" } 15 + } 16 + } 17 + }, 18 + "output": { 19 + "encoding": "application/json", 20 + "schema": { 21 + "type": "object", 22 + "required": ["ok"], 23 + "properties": { 24 + "ok": { "type": "boolean" } 25 + } 26 + } 27 + }, 28 + "errors": [ 29 + { "name": "NotFound" }, 30 + { "name": "Forbidden" } 31 + ] 32 + } 33 + } 34 + }
+16
examples/sveltekit-group-chat/.env.example
··· 4 4 5 5 # Set to your tunnel URL to use a confidential client in dev 6 6 # OAUTH_PUBLIC_URL=https://your-tunnel.trycloudflare.com 7 + 8 + # 32-byte base64 secret used to envelope-encrypt community signing keys. 9 + # Generate with: openssl rand -base64 32 10 + COMMUNITY_MASTER_KEY= 11 + 12 + # 32-byte base64 secret used to HMAC-sign realtime subscription tickets. 13 + # Generate with: openssl rand -base64 32 14 + REALTIME_TICKET_SECRET= 15 + 16 + # Service DID for JWT verification (aud claim). did:web:<your-host> for prod. 17 + SERVICE_DID=did:web:localhost 18 + 19 + # Dev-only: trust the HMAC-signed session cookie instead of atproto service-auth 20 + # JWTs, so the loopback OAuth client can reach contrail without a tunnel. NEVER 21 + # set this in production — it turns the cookie into the only auth signal. 22 + DEV_AUTH=1
+84 -44
examples/sveltekit-group-chat/README.md
··· 1 - # sveltekit + contrail + atproto OAuth 1 + # sveltekit-group-chat 2 2 3 - A sveltekit example app on cloudflare workers with self-hosted atproto record indexing via [contrail](https://github.com/flo-bit/contrail), fully typed queries, and OAuth authentication. 3 + A Discord-like demo built on top of [contrail](https://github.com/flo-bit/contrail) — SvelteKit + Cloudflare Workers + D1, OAuth, spaces, communities, and realtime (SSE). 4 + 5 + ## What this exercises 6 + 7 + - **`community`** — mint a fresh `did:plc` per server, tiered access levels (member / manager / admin / owner), space → space delegation. 8 + - **`spaces`** — permissioned per-channel record tables, membership pushed in by the community reconciler. 9 + - **`realtime`** — single SSE connection subscribed to `community:<did>`, fanning out per-channel events with unread dots derived client-side. 10 + - Three record collections — `tools.atmo.chat.server`, `tools.atmo.chat.channel`, `tools.atmo.chat.message` — with unified `listRecords` for "every server / channel I can see" in one call. 4 11 5 12 ## Setup 6 13 7 14 ```sh 8 15 pnpm install 16 + cp .env.example .env 17 + # fill in COMMUNITY_MASTER_KEY and REALTIME_TICKET_SECRET with 18 + # openssl rand -base64 32 19 + pnpm generate:pull # emits lexicons + src/lib/atproto/generated-methods.ts 9 20 pnpm dev 10 21 ``` 11 22 12 - Dev mode uses a loopback OAuth client — no keys or Cloudflare setup needed. 23 + Re-run `pnpm generate` whenever you add/remove collections or toggle contrail 24 + modules in `src/lib/contrail/config.ts`: the OAuth scope list is derived from 25 + it, so the consent screen only asks for permissions you actually use. 13 26 14 - ## Config 27 + Dev mode uses a loopback OAuth client — no Cloudflare setup needed. The realtime Durable Object runs locally via miniflare. 15 28 16 - Define which AT Protocol collections to index in `src/lib/contrail/config.ts`: 29 + ### Auth in dev vs prod 17 30 18 - ```ts 19 - import type { ContrailConfig } from '@atmo-dev/contrail'; 31 + Contrail normally auths every write with an atproto **service-auth JWT**. The browser delegates minting those JWTs to the SvelteKit worker, which calls `com.atproto.server.getServiceAuth` on the user's PDS. 20 32 21 - export const config: ContrailConfig = { 22 - namespace: 'statusphere.app', 23 - collections: { 24 - status: { // short name → URL path segment 25 - collection: 'xyz.statusphere.status', // full NSID of the record type 26 - queryable: { 27 - status: {}, // equality filter (?status=...) 28 - createdAt: { type: 'range' } // range filter (?createdAtMin=...&createdAtMax=...) 29 - } 30 - } 31 - } 32 - }; 33 + bsky.social **refuses** that call for loopback OAuth clients, so out of the box a plain `pnpm dev` couldn't talk to contrail without a [cloudflared tunnel](#with-a-tunnel). 34 + 35 + To skip the tunnel this example ships a **dev auth bypass** gated on `DEV_AUTH=1` in `.env`: 36 + 37 + - Contrail's auth middleware trusts the HMAC-signed `did` session cookie the OAuth flow already sets, and uses its DID as the authenticated caller. 38 + - The cookie is HMAC-signed with `COOKIE_SECRET`, so only this worker can mint a valid one. 39 + - All access-level checks (admin+, manager+, etc.) still apply — the bypass only replaces the JWT step. 40 + 41 + **Never set `DEV_AUTH=1` in production** — that would make the cookie the sole auth signal for third-party clients. 42 + 43 + ### With a tunnel 44 + 45 + If you want to exercise the real JWT path in dev, run a tunnel and drop `DEV_AUTH`: 46 + 47 + ```sh 48 + pnpm tunnel # prints https://xxx.trycloudflare.com 49 + # in .env: set OAUTH_PUBLIC_URL to the tunnel URL and remove DEV_AUTH 50 + pnpm dev 51 + # open the tunnel URL (not localhost) 33 52 ``` 34 53 35 - After changing the config, run `pnpm generate:pull` to regenerate lexicons and types. 54 + ## How the pieces connect 36 55 37 - Run `pnpm sync` to backfill existing records from the network. 56 + ``` 57 + ┌────────────┐ service-auth JWT ┌─────────────────┐ contrail XRPCs ┌───────────┐ 58 + │ browser │ ─────────────────► │ SvelteKit worker │ ─────────────────► │ contrail │ 59 + └────────────┘ └─────────────────┘ └───────────┘ 60 + │ │ │ 61 + │ EventSource /xrpc/.../realtime.subscribe?ticket=... ◄──────── realtime DO │ 62 + └───────────────────────────────────────────────────────────────────────────┘ 63 + ``` 38 64 39 - Wrangler bindings (`wrangler.jsonc`): 65 + - The browser can't mint service-auth JWTs itself — it delegates to the SvelteKit worker, which uses the user's OAuth session to call `com.atproto.server.getServiceAuth` on their PDS. The resulting JWT is scoped to one `lxm`. 66 + - Realtime tickets are minted the same way, then the browser holds an `EventSource` open against the subscribe endpoint. 40 67 41 - - **D1** (`DB`) — contrail's database 42 - - **KV** (`OAUTH_SESSIONS`, `OAUTH_STATES`) — OAuth session storage 43 - - **Vars** — `CRON_SECRET`, `OAUTH_PUBLIC_URL`, `CLIENT_ASSERTION_KEY`, `COOKIE_SECRET` 68 + ## Data model 69 + 70 + | Record | Lives in | Author | 71 + |---|---|---| 72 + | `tools.atmo.chat.server` | community's `members` space, rkey `self` | community DID (via `community.space.putRecord`) | 73 + | `tools.atmo.chat.channel` | channel's own space, rkey `self` | community DID | 74 + | `tools.atmo.chat.message` | channel's own space, rkey = TID | user DID | 75 + 76 + Only admins (`admin+` in the target space) can write the server and channel records, and the community DID is the author. Reads filter by `actor=<communityDid>` so spoof records from random members are silently ignored. 77 + 78 + ## Feature map 79 + 80 + | Route | What it does | 81 + |---|---| 82 + | `/` | "My servers" — unified `server.listRecords` over every space I'm in | 83 + | `/new` | Mint community, bootstrap `members` role-space, write server record, show recovery key once | 84 + | `/c/[communityDid]` | Redirects to the first channel you can see | 85 + | `/c/[communityDid]/[channelKey]` | Chat view — messages via `space.listRecords` + realtime | 86 + | `/c/[communityDid]/settings/members` | Add, promote, demote, revoke members | 87 + 88 + ## Private channels 89 + 90 + New-channel modal offers a visibility toggle: 91 + 92 + - **Public** — grants the `members` role-space `member` access on the new channel. Reconciler fans out to everyone. 93 + - **Private** — grants each picked DID directly. Also grants `members` (so they can see the server header) but not automatic access to other channels. 44 94 45 95 ## Deploy 46 96 47 97 ```sh 48 - npx wrangler d1 create statusphere 49 - # Add database_id to wrangler.jsonc 98 + npx wrangler d1 create group-chat 99 + # add the database_id to wrangler.jsonc 100 + npx wrangler secret put COMMUNITY_MASTER_KEY 101 + npx wrangler secret put REALTIME_TICKET_SECRET 50 102 51 103 pnpm build 52 104 npx wrangler deploy 53 105 ``` 54 106 55 - ## How it works 107 + ## Known gaps 56 108 57 - **Contrail** indexes AT Protocol records into D1 via Jetstream (cron, every minute). When a user posts, `contrail.notify()` indexes it immediately. 109 + This is a demo. It deliberately skips: 58 110 59 - **Typed queries** use `@atcute/client` with an in-process handler — full type safety, zero HTTP overhead: 60 - 61 - ```ts 62 - const client = getClient(platform!.env.DB); 63 - const res = await client.get('statusphere.app.status.listRecords', { 64 - params: { limit: 50, profiles: true } // typed params 65 - }); 66 - res.data.records // typed response 67 - ``` 68 - 69 - Types are generated from contrail's config via `pnpm generate:pull`, which produces lexicon JSON and TypeScript types that register with `@atcute/client`. 70 - 71 - **Scheduled ingestion** works around SvelteKit's lack of `scheduled` export support ([sveltejs/kit#4841](https://github.com/sveltejs/kit/issues/4841)) by appending a handler post-build that self-calls `/api/cron`. 72 - 73 - **OAuth** uses `@atcute/oauth-node-client` with KV-backed sessions and HMAC-signed cookies. 111 + - **`adopt` mode** — only `mint` is wired up. 112 + - **`$publishers`** / community-authored public posts. 113 + - Threads, reactions, image uploads, rich text, typing indicators. 74 114 75 115 ## License 76 116
+2 -2
examples/sveltekit-group-chat/package.json
··· 1 1 { 2 - "name": "svelte-cloudflare-statusphere", 2 + "name": "sveltekit-group-chat", 3 3 "private": true, 4 4 "version": "0.1.0", 5 5 "type": "module", ··· 59 59 "license": "MIT", 60 60 "dependencies": { 61 61 "@atcute/jetstream": "^1.1.2", 62 - "@atmo-dev/contrail": "^0.1.1", 62 + "@atmo-dev/contrail": "workspace:*", 63 63 "@foxui/core": "^0.9.1", 64 64 "@foxui/social": "^0.8.10", 65 65 "@foxui/time": "^0.8.5",
+36 -9
examples/sveltekit-group-chat/wrangler.jsonc
··· 1 1 { 2 2 "$schema": "node_modules/wrangler/config-schema.json", 3 - "name": "svelte-atproto-oauth-cloudflare-workers", 3 + "name": "sveltekit-group-chat", 4 4 "main": ".svelte-kit/cloudflare/_worker.js", 5 5 "compatibility_date": "2025-12-25", 6 - "compatibility_flags": ["nodejs_compat_v2"], 6 + "compatibility_flags": [ 7 + "nodejs_compat_v2" 8 + ], 7 9 "assets": { 8 10 "binding": "ASSETS", 9 11 "directory": ".svelte-kit/cloudflare" ··· 12 14 "enabled": true 13 15 }, 14 16 "vars": { 15 - "OAUTH_PUBLIC_URL": "https://statusphere.atmo.tools" 17 + "OAUTH_PUBLIC_URL": "https://chat.atmo.tools", 18 + "SERVICE_DID": "did:web:chat.atmo.tools" 16 19 }, 17 20 "d1_databases": [ 18 21 { 19 22 "binding": "DB", 20 - "database_name": "statusphere", 21 - "database_id": "REPLACE_WITH_D1_DATABASE_ID" 23 + "database_name": "group-chat", 24 + "database_id": "576a1971-7b78-4bc2-95f0-f83b360d3279" 22 25 } 23 26 ], 24 27 "triggers": { 25 - "crons": ["*/1 * * * *"] 28 + "crons": [ 29 + "*/1 * * * *" 30 + ] 26 31 }, 27 32 "kv_namespaces": [ 28 33 { 29 34 "binding": "OAUTH_SESSIONS", 30 - "id": "c44fc97274b84f5a97a8399b1977ad0d" 35 + "id": "852448049fd74f0bb61448f0e1dd2d77" 31 36 }, 32 37 { 33 38 "binding": "OAUTH_STATES", 34 - "id": "115d8089d9ca4592a3a5e4a7d9146610" 39 + "id": "3ef76633a67d46f3aa3a7b619e0730de" 40 + } 41 + ], 42 + "durable_objects": { 43 + "bindings": [ 44 + { 45 + "name": "REALTIME", 46 + "class_name": "RealtimePubSubDO" 47 + } 48 + ] 49 + }, 50 + "migrations": [ 51 + { 52 + "tag": "v1", 53 + "new_sqlite_classes": [ 54 + "RealtimePubSubDO" 55 + ] 56 + } 57 + ], 58 + "r2_buckets": [ 59 + { 60 + "binding": "BLOBS", 61 + "bucket_name": "group-chat-blobs" 35 62 } 36 63 ] 37 - } 64 + }
+403
src/sync/index.ts
··· 1 + /** Client-side sync engine over `watchRecords`. 2 + * 3 + * Consumes the event stream emitted by a contrail `<ns>.<collection>.watchRecords` 4 + * endpoint and maintains a keyed, reactive-ish record store. The core is 5 + * framework-agnostic — you get a subscribable store; wrap it in your 6 + * framework's reactive primitives. 7 + * 8 + * Two transports: 9 + * - `sse` (default) — opens an EventSource against `url`. Simplest. 10 + * - `ws` — two-step: HTTP GET to `url&mode=ws` for the snapshot + wsUrl, 11 + * then WebSocket upgrade. On Cloudflare this routes the live stream 12 + * through a Durable Object with hibernation, so idle connections cost 13 + * near-zero at scale. */ 14 + 15 + export interface WatchRecord { 16 + uri: string; 17 + did: string; 18 + rkey: string; 19 + collection: string; 20 + record: Record<string, unknown>; 21 + time_us?: number; 22 + indexed_at?: number; 23 + cid?: string | null; 24 + /** Set when the record originates from a per-space table. */ 25 + _space?: string; 26 + /** Additional hydrated relations / references populated server-side. */ 27 + [k: string]: unknown; 28 + } 29 + 30 + export interface WatchStoreOptions { 31 + /** Fully-qualified URL of the `watchRecords` endpoint, including query params. */ 32 + url: string; 33 + /** Transport. Default: 'sse'. Use 'ws' on Cloudflare for DO-terminated 34 + * long-lived subscriptions that hibernate while idle. */ 35 + transport?: "sse" | "ws"; 36 + /** Optional: provide a ticket string (for `?ticket=...`-style auth). 37 + * When omitted, the connection is opened without an explicit token — 38 + * rely on cookies / bearer tokens your fetch policy sends automatically. */ 39 + fetchTicket?: () => Promise<string>; 40 + /** Custom compare. Default: sort by `time_us` descending (newest first), 41 + * tie-breaking by rkey. */ 42 + compareRecords?: (a: WatchRecord, b: WatchRecord) => number; 43 + /** Reconnect on error with backoff. Default: true, 1s → 30s exponential. */ 44 + reconnect?: boolean; 45 + /** Optional logger for debug output. */ 46 + logger?: { 47 + log?: (...args: unknown[]) => void; 48 + warn?: (...args: unknown[]) => void; 49 + error?: (...args: unknown[]) => void; 50 + }; 51 + } 52 + 53 + export type WatchStoreStatus = 54 + | "idle" 55 + | "connecting" 56 + | "snapshot" 57 + | "live" 58 + | "reconnecting" 59 + | "closed"; 60 + 61 + export interface WatchStore { 62 + readonly records: ReadonlyArray<WatchRecord>; 63 + readonly status: WatchStoreStatus; 64 + readonly error: Error | null; 65 + /** Subscribe to changes. Called once with the current state on subscribe. */ 66 + subscribe(listener: (state: WatchStoreState) => void): () => void; 67 + /** Open the connection and begin consuming. Safe to call multiple times. */ 68 + start(): void; 69 + /** Close the connection and clear the store. */ 70 + stop(): void; 71 + } 72 + 73 + export interface WatchStoreState { 74 + records: ReadonlyArray<WatchRecord>; 75 + status: WatchStoreStatus; 76 + error: Error | null; 77 + } 78 + 79 + // --------------------------------------------------------------------------- 80 + 81 + const defaultCompare = (a: WatchRecord, b: WatchRecord): number => { 82 + const at = a.time_us ?? 0; 83 + const bt = b.time_us ?? 0; 84 + if (at !== bt) return bt - at; 85 + return a.rkey < b.rkey ? 1 : a.rkey > b.rkey ? -1 : 0; 86 + }; 87 + 88 + export function createWatchStore(options: WatchStoreOptions): WatchStore { 89 + const compare = options.compareRecords ?? defaultCompare; 90 + const reconnect = options.reconnect !== false; 91 + const transport = options.transport ?? "sse"; 92 + const log = options.logger ?? {}; 93 + 94 + const byKey = new Map<string, WatchRecord>(); 95 + let sorted: WatchRecord[] = []; 96 + let status: WatchStoreStatus = "idle"; 97 + let error: Error | null = null; 98 + const listeners = new Set<(state: WatchStoreState) => void>(); 99 + 100 + let es: EventSource | null = null; 101 + let ws: WebSocket | null = null; 102 + let started = false; 103 + let stopped = false; 104 + let backoffMs = 1000; 105 + 106 + const stateSnapshot = (): WatchStoreState => ({ records: sorted, status, error }); 107 + 108 + const notify = () => { 109 + const s = stateSnapshot(); 110 + for (const l of listeners) l(s); 111 + }; 112 + 113 + const resort = () => { 114 + sorted = Array.from(byKey.values()).sort(compare); 115 + }; 116 + 117 + const setStatus = (next: WatchStoreStatus, nextError: Error | null = null) => { 118 + status = next; 119 + error = nextError; 120 + notify(); 121 + }; 122 + 123 + const key = (r: { did?: string; rkey?: string; uri?: string }): string => { 124 + if (r.uri) return r.uri; 125 + if (r.did && r.rkey) return `${r.did}/${r.rkey}`; 126 + throw new Error("record must have uri or did+rkey"); 127 + }; 128 + 129 + const applySnapshotRecord = (record: WatchRecord) => { 130 + byKey.set(key(record), record); 131 + }; 132 + 133 + const applyCreated = (record: WatchRecord) => { 134 + byKey.set(key(record), record); 135 + resort(); 136 + notify(); 137 + }; 138 + 139 + const applyDeleted = (info: { uri?: string; did?: string; rkey?: string }) => { 140 + const k = key(info); 141 + if (!byKey.delete(k)) return; 142 + resort(); 143 + notify(); 144 + }; 145 + 146 + const applyHydrationAdded = ( 147 + parentUri: string, 148 + relation: string, 149 + child: WatchRecord 150 + ) => { 151 + const parent = byKey.get(parentUri); 152 + if (!parent) return; 153 + const existing = 154 + ((parent as Record<string, unknown>)[relation] as WatchRecord[] | undefined) ?? []; 155 + if (existing.some((c) => c.rkey === child.rkey)) return; 156 + (parent as Record<string, unknown>)[relation] = [...existing, child]; 157 + resort(); 158 + notify(); 159 + }; 160 + 161 + const applyHydrationRemoved = ( 162 + parentUri: string, 163 + relation: string, 164 + childRkey: string 165 + ) => { 166 + const parent = byKey.get(parentUri); 167 + if (!parent) return; 168 + const existing = (parent as Record<string, unknown>)[relation] as 169 + | WatchRecord[] 170 + | undefined; 171 + if (!existing) return; 172 + const filtered = existing.filter((c) => c.rkey !== childRkey); 173 + if (filtered.length === existing.length) return; 174 + (parent as Record<string, unknown>)[relation] = filtered; 175 + resort(); 176 + notify(); 177 + }; 178 + 179 + // Dispatch a single decoded event envelope to the store. Shared by both 180 + // transports — SSE event handlers and WS message handlers both funnel here. 181 + type IncomingMessage = 182 + | { kind: "snapshot.start"; data: unknown } 183 + | { kind: "snapshot.record"; data: { record: WatchRecord } } 184 + | { kind: "snapshot.end"; data: unknown } 185 + | { kind: "record.created"; data: { record: WatchRecord } } 186 + | { kind: "record.deleted"; data: { uri?: string; did?: string; rkey?: string } } 187 + | { 188 + kind: "hydration.added"; 189 + data: { parentUri: string; relation: string; child: WatchRecord }; 190 + } 191 + | { 192 + kind: "hydration.removed"; 193 + data: { parentUri: string; relation: string; childRkey: string }; 194 + } 195 + | { kind: "member.removed"; data: unknown }; 196 + 197 + const handleMessage = (msg: IncomingMessage) => { 198 + switch (msg.kind) { 199 + case "snapshot.start": 200 + setStatus("snapshot"); 201 + backoffMs = 1000; 202 + break; 203 + case "snapshot.record": 204 + applySnapshotRecord(msg.data.record); 205 + break; 206 + case "snapshot.end": 207 + resort(); 208 + setStatus("live"); 209 + break; 210 + case "record.created": 211 + applyCreated(msg.data.record); 212 + break; 213 + case "record.deleted": 214 + applyDeleted(msg.data); 215 + break; 216 + case "hydration.added": 217 + applyHydrationAdded(msg.data.parentUri, msg.data.relation, msg.data.child); 218 + break; 219 + case "hydration.removed": 220 + applyHydrationRemoved(msg.data.parentUri, msg.data.relation, msg.data.childRkey); 221 + break; 222 + case "member.removed": 223 + byKey.clear(); 224 + sorted = []; 225 + setStatus("closed"); 226 + closeConnections(); 227 + break; 228 + } 229 + }; 230 + 231 + const closeConnections = () => { 232 + if (es) { 233 + es.close(); 234 + es = null; 235 + } 236 + if (ws) { 237 + try { 238 + ws.close(); 239 + } catch { 240 + /* ignore */ 241 + } 242 + ws = null; 243 + } 244 + }; 245 + 246 + const openSse = async () => { 247 + let url = options.url; 248 + if (options.fetchTicket) { 249 + const ticket = await options.fetchTicket(); 250 + const sep = url.includes("?") ? "&" : "?"; 251 + url += `${sep}ticket=${encodeURIComponent(ticket)}`; 252 + } 253 + const source = new EventSource(url); 254 + es = source; 255 + 256 + const on = (kind: IncomingMessage["kind"]) => 257 + source.addEventListener(kind, (e) => { 258 + try { 259 + const data = JSON.parse((e as MessageEvent).data); 260 + handleMessage({ kind, data } as IncomingMessage); 261 + } catch (err) { 262 + log.warn?.(`${kind} parse failed`, err); 263 + } 264 + }); 265 + 266 + on("snapshot.start"); 267 + on("snapshot.record"); 268 + on("snapshot.end"); 269 + on("record.created"); 270 + on("record.deleted"); 271 + on("hydration.added"); 272 + on("hydration.removed"); 273 + on("member.removed"); 274 + 275 + source.addEventListener("error", () => { 276 + if (source.readyState === EventSource.CLOSED) { 277 + if (stopped) return; 278 + setStatus("reconnecting", new Error("stream closed")); 279 + scheduleReconnect(); 280 + } 281 + }); 282 + }; 283 + 284 + const openWs = async () => { 285 + // Step 1: snapshot + handshake. 286 + let snapshotUrl = options.url; 287 + const sep = snapshotUrl.includes("?") ? "&" : "?"; 288 + snapshotUrl += `${sep}mode=ws`; 289 + if (options.fetchTicket) { 290 + const ticket = await options.fetchTicket(); 291 + snapshotUrl += `&ticket=${encodeURIComponent(ticket)}`; 292 + } 293 + const res = await fetch(snapshotUrl, { headers: { accept: "application/json" } }); 294 + if (!res.ok) throw new Error(`snapshot fetch failed (${res.status})`); 295 + const handshake = (await res.json()) as { 296 + transport: "ws"; 297 + snapshot: { records: WatchRecord[]; cursor?: string }; 298 + wsUrl: string; 299 + /** Watch-scoped ticket the server embeds in `wsUrl` for WS auth. 300 + * Opaque to the client; passed back verbatim on upgrade. */ 301 + ticket?: string; 302 + /** Unix ms captured by the server before the snapshot ran. The 303 + * server embeds this in wsUrl as `?sinceTs=...` so the DO can 304 + * replay events from the race window on connect. */ 305 + sinceTs?: number; 306 + }; 307 + 308 + // Apply snapshot in-order. 309 + setStatus("snapshot"); 310 + for (const record of handshake.snapshot.records) applySnapshotRecord(record); 311 + resort(); 312 + backoffMs = 1000; 313 + 314 + // Step 2: open the WS. `wsUrl` is a relative path from the server. 315 + const wsHref = new URL(handshake.wsUrl, options.url); 316 + wsHref.protocol = wsHref.protocol === "https:" ? "wss:" : "ws:"; 317 + const socket = new WebSocket(wsHref.toString()); 318 + ws = socket; 319 + 320 + socket.addEventListener("open", () => { 321 + setStatus("live"); 322 + }); 323 + 324 + socket.addEventListener("message", (e) => { 325 + try { 326 + const parsed = JSON.parse((e as MessageEvent).data as string); 327 + if (!parsed || typeof parsed !== "object" || !parsed.kind) return; 328 + handleMessage(parsed as IncomingMessage); 329 + } catch (err) { 330 + log.warn?.("ws message parse failed", err); 331 + } 332 + }); 333 + 334 + socket.addEventListener("close", () => { 335 + if (stopped) return; 336 + setStatus("reconnecting", new Error("ws closed")); 337 + scheduleReconnect(); 338 + }); 339 + 340 + socket.addEventListener("error", (err) => { 341 + log.warn?.("ws error", err); 342 + }); 343 + }; 344 + 345 + const openOnce = async () => { 346 + setStatus("connecting"); 347 + try { 348 + if (transport === "ws") await openWs(); 349 + else await openSse(); 350 + } catch (err) { 351 + setStatus( 352 + "reconnecting", 353 + err instanceof Error ? err : new Error(String(err)) 354 + ); 355 + scheduleReconnect(); 356 + } 357 + }; 358 + 359 + const scheduleReconnect = () => { 360 + if (!reconnect || stopped) { 361 + setStatus("closed"); 362 + return; 363 + } 364 + const delay = Math.min(backoffMs, 30_000); 365 + backoffMs = Math.min(backoffMs * 2, 30_000); 366 + setTimeout(() => { 367 + if (stopped) return; 368 + byKey.clear(); 369 + sorted = []; 370 + void openOnce(); 371 + }, delay); 372 + }; 373 + 374 + return { 375 + get records() { 376 + return sorted; 377 + }, 378 + get status() { 379 + return status; 380 + }, 381 + get error() { 382 + return error; 383 + }, 384 + subscribe(listener) { 385 + listeners.add(listener); 386 + listener(stateSnapshot()); 387 + return () => listeners.delete(listener); 388 + }, 389 + start() { 390 + if (started) return; 391 + started = true; 392 + stopped = false; 393 + void openOnce(); 394 + }, 395 + stop() { 396 + stopped = true; 397 + closeConnections(); 398 + byKey.clear(); 399 + sorted = []; 400 + setStatus("closed"); 401 + } 402 + }; 403 + }
+1 -1
examples/sveltekit-group-chat/lexicons-pulled/README.md
··· 2 2 3 3 this directory contains lexicon documents pulled from the following sources: 4 4 5 - - atproto (nsids: app.bsky.actor.profile, xyz.statusphere.status) 5 + - atproto (nsids: app.bsky.actor.profile, tools.atmo.chat.channel, tools.atmo.chat.message, tools.atmo.chat.server)
+9 -6
examples/sveltekit-group-chat/scripts/append-scheduled.ts
··· 1 1 /** 2 - * Post-build script: appends a `scheduled` handler to the SvelteKit worker output. 3 - * 4 - * SvelteKit's adapter-cloudflare doesn't support the `scheduled` export natively 5 - * (see https://github.com/sveltejs/kit/issues/4841). This script patches the 6 - * generated _worker.js to add one that self-calls the /api/cron endpoint. 2 + * Post-build script: 3 + * - appends a `scheduled` handler to the SvelteKit worker output 4 + * (adapter-cloudflare doesn't support scheduled exports natively: 5 + * https://github.com/sveltejs/kit/issues/4841) 6 + * - re-exports the realtime Durable Object class so wrangler's class_name binding resolves. 7 7 */ 8 8 import { readFileSync, writeFileSync } from 'fs'; 9 9 import { join, dirname } from 'path'; ··· 16 16 17 17 code += ` 18 18 // --- Appended by scripts/append-scheduled.ts --- 19 + import { RealtimePubSubDO as __RealtimePubSubDO } from "@atmo-dev/contrail"; 20 + export { __RealtimePubSubDO as RealtimePubSubDO }; 21 + 19 22 worker_default.scheduled = async function (event, env, ctx) { 20 23 const req = new Request('http://localhost/api/cron', { 21 24 method: 'POST', ··· 26 29 `; 27 30 28 31 writeFileSync(workerPath, code); 29 - console.log('Appended scheduled handler to _worker.js'); 32 + console.log('Appended scheduled handler + RealtimePubSubDO re-export to _worker.js');
+27 -4
examples/sveltekit-group-chat/scripts/generate.ts
··· 1 1 import { join, dirname } from 'path'; 2 2 import { fileURLToPath } from 'url'; 3 - import { config } from '../src/lib/contrail/config'; 4 - import { generateLexicons } from '@atmo-dev/contrail/generate'; 3 + import { writeFileSync } from 'fs'; 4 + import { baseConfig } from '../src/lib/contrail/config'; 5 + import { generateLexicons, extractXrpcMethods } from '@atmo-dev/contrail/generate'; 5 6 6 7 const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); 7 8 8 - generateLexicons({ 9 - config, 9 + // Use a config that has spaces/community/realtime declared so the generator 10 + // emits the full surface (space.*, community.*, realtime.*) alongside the 11 + // per-collection endpoints. Values are stubs — only the shape matters for codegen. 12 + const configForGen = { 13 + ...baseConfig, 14 + spaces: { type: 'tools.atmo.chat.space', serviceDid: 'did:web:localhost' }, 15 + community: { masterKey: new Uint8Array(32), serviceDid: 'did:web:localhost' }, 16 + realtime: { ticketSecret: new Uint8Array(32) } 17 + }; 18 + 19 + const generated = generateLexicons({ 20 + config: configForGen, 10 21 rootDir: ROOT_DIR, 11 22 outputDir: join(ROOT_DIR, 'lexicons-generated'), 12 23 writeRuntimeFiles: true 13 24 }); 25 + 26 + // Emit the sorted XRPC method list as a TS module. settings.ts imports it and 27 + // feeds it into `scope.rpc({ lxm: [...], aud })` so the OAuth consent screen 28 + // requests exactly the methods contrail exposes (no more, no less). 29 + const methods = extractXrpcMethods(generated); 30 + const out = `// Auto-generated by scripts/generate.ts. Do not edit by hand. 31 + // Source of truth: the set of XRPC methods contrail emits for this namespace. 32 + 33 + export const xrpcMethods = ${JSON.stringify(methods, null, '\t')} as const; 34 + `; 35 + writeFileSync(join(ROOT_DIR, 'src/lib/atproto/generated-methods.ts'), out); 36 + console.log(`wrote generated-methods.ts (${methods.length} methods)`);
+7
examples/sveltekit-group-chat/src/app.d.ts
··· 3 3 import type { OAuthSession } from '@atcute/oauth-node-client'; 4 4 import type { Client } from '@atcute/client'; 5 5 import type { Did } from '@atcute/lexicons'; 6 + import type { DurableObjectNamespace } from '@atmo-dev/contrail'; 6 7 7 8 declare global { 8 9 namespace App { ··· 24 25 PROFILE_CACHE?: KVNamespace; 25 26 DB: D1Database; 26 27 CRON_SECRET: string; 28 + COMMUNITY_MASTER_KEY: string; 29 + REALTIME_TICKET_SECRET: string; 30 + SERVICE_DID: string; 31 + DEV_AUTH?: string; 32 + REALTIME: DurableObjectNamespace; 33 + BLOBS?: R2Bucket; 27 34 }; 28 35 } 29 36 }
+1
examples/sveltekit-group-chat/src/hooks.server.ts
··· 1 1 import type { Handle } from '@sveltejs/kit'; 2 + import './lexicon-types'; // register XRPCQueries/XRPCProcedures augmentations 2 3 import { restoreSession } from '$lib/atproto/server/session'; 3 4 4 5 export const handle: Handle = async ({ event, resolve }) => {
examples/sveltekit-group-chat/static/og.png

This is a binary file and will not be displayed.

+114 -1
src/core/community/adapter.ts
··· 1 1 import type { Database } from "../types"; 2 - import type { AccessLevel, AccessLevelRow, CommunityMode, CommunityRow } from "./types"; 2 + import type { 3 + AccessLevel, 4 + AccessLevelRow, 5 + CommunityInviteRow, 6 + CommunityMode, 7 + CommunityRow, 8 + CreateCommunityInviteInput, 9 + } from "./types"; 3 10 4 11 function toNum(v: unknown): number { 5 12 return typeof v === "string" ? Number(v) : (v as number); ··· 286 293 .bind(spaceUri) 287 294 .run(); 288 295 } 296 + 297 + // ---- Invites ----------------------------------------------------------- 298 + 299 + async createInvite(input: CreateCommunityInviteInput): Promise<CommunityInviteRow> { 300 + const now = Date.now(); 301 + await this.db 302 + .prepare( 303 + `INSERT INTO community_invites 304 + (token_hash, space_uri, access_level, created_by, created_at, expires_at, max_uses, used_count, note) 305 + VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)` 306 + ) 307 + .bind( 308 + input.tokenHash, 309 + input.spaceUri, 310 + input.accessLevel, 311 + input.createdBy, 312 + now, 313 + input.expiresAt, 314 + input.maxUses, 315 + input.note 316 + ) 317 + .run(); 318 + return { 319 + tokenHash: input.tokenHash, 320 + spaceUri: input.spaceUri, 321 + accessLevel: input.accessLevel, 322 + createdBy: input.createdBy, 323 + createdAt: now, 324 + expiresAt: input.expiresAt, 325 + maxUses: input.maxUses, 326 + usedCount: 0, 327 + revokedAt: null, 328 + note: input.note, 329 + }; 330 + } 331 + 332 + async getInvite(tokenHash: string): Promise<CommunityInviteRow | null> { 333 + const row = await this.db 334 + .prepare(`SELECT * FROM community_invites WHERE token_hash = ?`) 335 + .bind(tokenHash) 336 + .first<any>(); 337 + return row ? mapCommunityInviteRow(row) : null; 338 + } 339 + 340 + async listInvites( 341 + spaceUri: string, 342 + options: { includeRevoked?: boolean } = {} 343 + ): Promise<CommunityInviteRow[]> { 344 + const sql = options.includeRevoked 345 + ? `SELECT * FROM community_invites WHERE space_uri = ? ORDER BY created_at DESC` 346 + : `SELECT * FROM community_invites WHERE space_uri = ? AND revoked_at IS NULL ORDER BY created_at DESC`; 347 + const { results } = await this.db.prepare(sql).bind(spaceUri).all<any>(); 348 + return results.map(mapCommunityInviteRow); 349 + } 350 + 351 + async revokeInvite(tokenHash: string): Promise<boolean> { 352 + const res = await this.db 353 + .prepare( 354 + `UPDATE community_invites SET revoked_at = ? WHERE token_hash = ? AND revoked_at IS NULL` 355 + ) 356 + .bind(Date.now(), tokenHash) 357 + .run(); 358 + const changes = (res as any)?.changes ?? (res as any)?.meta?.changes ?? 0; 359 + return Number(changes) > 0; 360 + } 361 + 362 + /** Atomically consume one "use" from an invite. Returns the updated row if 363 + * the invite is currently usable, null otherwise (expired, revoked, 364 + * exhausted, or missing). The router calls this, then issues a grant if 365 + * the return is non-null. */ 366 + async redeemInvite(tokenHash: string, now: number): Promise<CommunityInviteRow | null> { 367 + const res = await this.db 368 + .prepare( 369 + `UPDATE community_invites 370 + SET used_count = used_count + 1 371 + WHERE token_hash = ? 372 + AND revoked_at IS NULL 373 + AND (expires_at IS NULL OR expires_at > ?) 374 + AND (max_uses IS NULL OR used_count < max_uses)` 375 + ) 376 + .bind(tokenHash, now) 377 + .run(); 378 + const changes = (res as any)?.changes ?? (res as any)?.meta?.changes ?? 0; 379 + if (Number(changes) === 0) return null; 380 + 381 + const row = await this.db 382 + .prepare(`SELECT * FROM community_invites WHERE token_hash = ?`) 383 + .bind(tokenHash) 384 + .first<any>(); 385 + return row ? mapCommunityInviteRow(row) : null; 386 + } 387 + } 388 + 389 + function mapCommunityInviteRow(row: any): CommunityInviteRow { 390 + return { 391 + tokenHash: row.token_hash, 392 + spaceUri: row.space_uri, 393 + accessLevel: row.access_level as AccessLevel, 394 + createdBy: row.created_by, 395 + createdAt: toNum(row.created_at), 396 + expiresAt: row.expires_at == null ? null : toNum(row.expires_at), 397 + maxUses: row.max_uses == null ? null : Number(row.max_uses), 398 + usedCount: Number(row.used_count ?? 0), 399 + revokedAt: row.revoked_at == null ? null : toNum(row.revoked_at), 400 + note: row.note ?? null, 401 + }; 289 402 }
+2
src/core/community/index.ts
··· 5 5 AccessLevel, 6 6 AccessLevelRow, 7 7 CommunityConfig, 8 + CommunityInviteRow, 8 9 CommunityMode, 9 10 CommunityRow, 11 + CreateCommunityInviteInput, 10 12 ReservedKey, 11 13 } from "./types"; 12 14 export {
+43 -5
src/core/community/plc.ts
··· 48 48 return "did:key:z" + base58btcEncode(combined); 49 49 } 50 50 51 + /** Order of the P-256 curve (n). */ 52 + const P256_N = BigInt( 53 + "0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551" 54 + ); 55 + const P256_N_HALF = P256_N >> 1n; 56 + 51 57 async function signBytes(privateJwk: JsonWebKey, bytes: Uint8Array): Promise<Uint8Array> { 52 58 const key = await crypto.subtle.importKey( 53 59 "jwk", ··· 56 62 false, 57 63 ["sign"] 58 64 ); 59 - const sig = await crypto.subtle.sign( 60 - { name: "ECDSA", hash: "SHA-256" }, 61 - key, 62 - bytes as BufferSource 65 + const sig = new Uint8Array( 66 + await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, bytes as BufferSource) 63 67 ); 64 - return new Uint8Array(sig); 68 + // Web Crypto returns IEEE P1363 form: r || s (32 bytes each for P-256). 69 + // atproto/PLC (and most modern ECDSA consumers) require low-S — i.e. 70 + // s must be in the lower half of the curve order. Flip high-S signatures 71 + // by replacing s with n - s. This yields an equivalent valid signature. 72 + return normalizeLowS(sig); 73 + } 74 + 75 + function normalizeLowS(sig: Uint8Array): Uint8Array { 76 + if (sig.length !== 64) return sig; // not a P-256 P1363 sig — don't touch 77 + const r = sig.slice(0, 32); 78 + const sBytes = sig.slice(32); 79 + const s = bytesToBigInt(sBytes); 80 + if (s <= P256_N_HALF) return sig; 81 + const sLow = P256_N - s; 82 + const sLowBytes = bigIntToBytes(sLow, 32); 83 + const out = new Uint8Array(64); 84 + out.set(r, 0); 85 + out.set(sLowBytes, 32); 86 + return out; 87 + } 88 + 89 + function bytesToBigInt(b: Uint8Array): bigint { 90 + let v = 0n; 91 + for (const byte of b) v = (v << 8n) | BigInt(byte); 92 + return v; 93 + } 94 + 95 + function bigIntToBytes(v: bigint, length: number): Uint8Array { 96 + const out = new Uint8Array(length); 97 + let x = v; 98 + for (let i = length - 1; i >= 0; i--) { 99 + out[i] = Number(x & 0xffn); 100 + x >>= 8n; 101 + } 102 + return out; 65 103 } 66 104 67 105 // ============================================================================
+164
src/core/community/router.ts
··· 25 25 RESERVED_KEYS, 26 26 } from "./types"; 27 27 import type { ServiceJwtVerifier } from "@atcute/xrpc-server/auth"; 28 + import { 29 + generateInviteToken, 30 + hashInviteToken, 31 + } from "../spaces/invite-token"; 28 32 29 33 export interface CommunityRoutesOptions { 30 34 /** Override auth middleware for tests. */ ··· 970 974 identifier, 971 975 }); 972 976 return c.json({ ok: true }); 977 + }); 978 + 979 + // ========================================================================== 980 + // Invites — pre-signed grants for community-owned spaces. Admin/manager 981 + // creates; any authenticated user with the raw token redeems once. 982 + // ========================================================================== 983 + 984 + app.post(`/xrpc/${NS}.invite.create`, auth, async (c) => { 985 + const sa = getAuth(c); 986 + const body = (await c.req.json().catch(() => null)) as 987 + | { 988 + spaceUri?: string; 989 + accessLevel?: string; 990 + expiresAt?: number; 991 + maxUses?: number; 992 + note?: string; 993 + } 994 + | null; 995 + if (!body?.spaceUri || !body.accessLevel) { 996 + return c.json( 997 + { error: "InvalidRequest", message: "spaceUri and accessLevel required" }, 998 + 400 999 + ); 1000 + } 1001 + if (!isAccessLevel(body.accessLevel)) { 1002 + return c.json({ error: "InvalidRequest", message: "invalid accessLevel" }, 400); 1003 + } 1004 + const space = await spaces.getSpace(body.spaceUri); 1005 + if (!space) return c.json({ error: "NotFound" }, 404); 1006 + const communityRow = await community.getCommunity(space.ownerDid); 1007 + if (!communityRow) { 1008 + return c.json({ error: "InvalidRequest", reason: "not-community-owned" }, 400); 1009 + } 1010 + 1011 + // Caller must have at least manager on the target space, and cannot create 1012 + // an invite that confers a higher level than their own. 1013 + const callerLevel = await resolveEffectiveLevel(community, body.spaceUri, sa.issuer); 1014 + if (!callerLevel || rankOf(callerLevel) < rankOf("manager")) { 1015 + return c.json({ error: "Forbidden", reason: "manager-required" }, 403); 1016 + } 1017 + if (rankOf(body.accessLevel) > rankOf(callerLevel)) { 1018 + return c.json({ error: "Forbidden", reason: "cannot-grant-higher-than-self" }, 403); 1019 + } 1020 + 1021 + const token = generateInviteToken(); 1022 + const tokenHash = await hashInviteToken(token); 1023 + 1024 + const row = await community.createInvite({ 1025 + spaceUri: body.spaceUri, 1026 + tokenHash, 1027 + accessLevel: body.accessLevel, 1028 + createdBy: sa.issuer, 1029 + expiresAt: body.expiresAt ?? null, 1030 + maxUses: body.maxUses ?? null, 1031 + note: body.note ?? null, 1032 + }); 1033 + 1034 + // Raw token only returned once. tokenHash is the stable ID for list/revoke. 1035 + return c.json({ 1036 + token, 1037 + tokenHash, 1038 + spaceUri: row.spaceUri, 1039 + accessLevel: row.accessLevel, 1040 + expiresAt: row.expiresAt, 1041 + maxUses: row.maxUses, 1042 + createdAt: row.createdAt, 1043 + }); 1044 + }); 1045 + 1046 + app.get(`/xrpc/${NS}.invite.list`, auth, async (c) => { 1047 + const sa = getAuth(c); 1048 + const spaceUri = c.req.query("spaceUri"); 1049 + const includeRevoked = c.req.query("includeRevoked") === "true"; 1050 + if (!spaceUri) { 1051 + return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); 1052 + } 1053 + const space = await spaces.getSpace(spaceUri); 1054 + if (!space) return c.json({ error: "NotFound" }, 404); 1055 + 1056 + const callerLevel = await resolveEffectiveLevel(community, spaceUri, sa.issuer); 1057 + if (!callerLevel || rankOf(callerLevel) < rankOf("manager")) { 1058 + return c.json({ error: "Forbidden", reason: "manager-required" }, 403); 1059 + } 1060 + 1061 + const rows = await community.listInvites(spaceUri, { includeRevoked }); 1062 + return c.json({ 1063 + invites: rows.map((r) => ({ 1064 + tokenHash: r.tokenHash, 1065 + spaceUri: r.spaceUri, 1066 + accessLevel: r.accessLevel, 1067 + createdBy: r.createdBy, 1068 + createdAt: r.createdAt, 1069 + expiresAt: r.expiresAt, 1070 + maxUses: r.maxUses, 1071 + usedCount: r.usedCount, 1072 + revokedAt: r.revokedAt, 1073 + note: r.note, 1074 + })), 1075 + }); 1076 + }); 1077 + 1078 + app.post(`/xrpc/${NS}.invite.revoke`, auth, async (c) => { 1079 + const sa = getAuth(c); 1080 + const body = (await c.req.json().catch(() => null)) as 1081 + | { tokenHash?: string } 1082 + | null; 1083 + if (!body?.tokenHash) { 1084 + return c.json({ error: "InvalidRequest", message: "tokenHash required" }, 400); 1085 + } 1086 + const invite = await community.getInvite(body.tokenHash); 1087 + if (!invite) return c.json({ error: "NotFound" }, 404); 1088 + 1089 + // Revoker: either the invite's creator, or manager+ on the target space. 1090 + let allowed = invite.createdBy === sa.issuer; 1091 + if (!allowed) { 1092 + const level = await resolveEffectiveLevel(community, invite.spaceUri, sa.issuer); 1093 + allowed = !!level && rankOf(level) >= rankOf("manager"); 1094 + } 1095 + if (!allowed) { 1096 + return c.json({ error: "Forbidden", reason: "creator-or-manager-required" }, 403); 1097 + } 1098 + await community.revokeInvite(body.tokenHash); 1099 + return c.json({ ok: true }); 1100 + }); 1101 + 1102 + app.post(`/xrpc/${NS}.invite.redeem`, auth, async (c) => { 1103 + const sa = getAuth(c); 1104 + const body = (await c.req.json().catch(() => null)) as { token?: string } | null; 1105 + if (!body?.token) { 1106 + return c.json({ error: "InvalidRequest", message: "token required" }, 400); 1107 + } 1108 + const tokenHash = await hashInviteToken(body.token); 1109 + 1110 + // Atomic consume — returns null if the token is expired/revoked/exhausted. 1111 + const invite = await community.redeemInvite(tokenHash, Date.now()); 1112 + if (!invite) { 1113 + return c.json({ error: "InvalidToken", reason: "token-invalid-or-exhausted" }, 410); 1114 + } 1115 + 1116 + const space = await spaces.getSpace(invite.spaceUri); 1117 + if (!space) { 1118 + return c.json({ error: "NotFound", reason: "space-not-found" }, 404); 1119 + } 1120 + 1121 + // The token itself is the authorization: the creator (who had manager+) 1122 + // pre-signed "anyone with this token gets level X on this space". Grant 1123 + // directly, attributing to the creator so audit trails make sense. 1124 + await community.grant({ 1125 + spaceUri: invite.spaceUri, 1126 + subjectDid: sa.issuer, 1127 + accessLevel: invite.accessLevel, 1128 + grantedBy: invite.createdBy, 1129 + }); 1130 + await reconcile(community, spaces, invite.spaceUri, invite.createdBy); 1131 + 1132 + return c.json({ 1133 + spaceUri: invite.spaceUri, 1134 + accessLevel: invite.accessLevel, 1135 + communityDid: space.ownerDid, 1136 + }); 973 1137 }); 974 1138 } 975 1139
+14
src/core/community/schema.ts
··· 30 30 `CREATE INDEX IF NOT EXISTS idx_cal_subject ON community_access_levels(subject)`, 31 31 `CREATE INDEX IF NOT EXISTS idx_cal_subject_space ON community_access_levels(subject) 32 32 WHERE subject_kind = 'space'`, 33 + 34 + `CREATE TABLE IF NOT EXISTS community_invites ( 35 + token_hash TEXT PRIMARY KEY NOT NULL, 36 + space_uri TEXT NOT NULL, 37 + access_level TEXT NOT NULL, 38 + created_by TEXT NOT NULL, 39 + created_at ${dialect.bigintType} NOT NULL, 40 + expires_at ${dialect.bigintType}, 41 + max_uses INTEGER, 42 + used_count INTEGER NOT NULL DEFAULT 0, 43 + revoked_at ${dialect.bigintType}, 44 + note TEXT 45 + )`, 46 + `CREATE INDEX IF NOT EXISTS idx_community_invites_space ON community_invites(space_uri, created_at DESC)`, 33 47 ]; 34 48 } 35 49
+26
src/core/community/types.ts
··· 59 59 grantedAt: number; 60 60 } 61 61 62 + /** A pre-signed grant for a community-owned space. An admin/manager creates 63 + * one; anyone with the raw token can redeem it once to get the specified 64 + * access level. Storage keeps only the SHA-256 of the token. */ 65 + export interface CommunityInviteRow { 66 + tokenHash: string; 67 + spaceUri: string; 68 + accessLevel: AccessLevel; 69 + createdBy: string; 70 + createdAt: number; 71 + expiresAt: number | null; 72 + maxUses: number | null; 73 + usedCount: number; 74 + revokedAt: number | null; 75 + note: string | null; 76 + } 77 + 78 + export interface CreateCommunityInviteInput { 79 + spaceUri: string; 80 + tokenHash: string; 81 + accessLevel: AccessLevel; 82 + createdBy: string; 83 + expiresAt: number | null; 84 + maxUses: number | null; 85 + note: string | null; 86 + } 87 + 62 88 /** Key prefix for reserved community-owned spaces. */ 63 89 export const RESERVED_KEYS = ["$admin", "$publishers"] as const; 64 90 export type ReservedKey = (typeof RESERVED_KEYS)[number];
+294 -21
src/core/realtime/durable-object.ts
··· 15 15 * the DO trusts anything that reaches it. */ 16 16 17 17 import type { PubSub, RealtimeEvent } from "./types"; 18 + import { translateForQuery, type TranslatedEnvelope } from "./query-filter"; 19 + type TranslatedEvent = TranslatedEnvelope; 20 + 21 + /** Query spec attached to a WS subscriber, used to filter events before 22 + * delivery. Shape matches what the Worker's `watchRecords` handler builds; 23 + * forwarded to the DO via trusted internal headers on the WS upgrade. */ 24 + export interface SubscriberQuerySpec { 25 + /** NSID of the primary collection the client is watching. */ 26 + collection: string; 27 + /** Space URI this subscription is scoped to. Events outside are dropped. */ 28 + spaceUri: string; 29 + /** Hydrated relations. Keyed by relName — value is the child collection 30 + * NSID and the field on the child record that references the parent. */ 31 + hydrate?: Record<string, { childCollection: string; matchField: string }>; 32 + } 18 33 19 34 // ---- Minimal structural typings so we don't depend on @cloudflare/workers-types 20 35 // at the library level. Callers on Workers will have proper types. ··· 72 87 return pullIterator(stub, signal); 73 88 }, 74 89 }; 90 + } 91 + 92 + /** Forward an incoming browser WS upgrade (or SSE GET) through to the DO 93 + * that owns this topic, attaching a query-filter spec that the DO will use 94 + * to decide what to deliver. The Worker must verify auth + spec validity 95 + * before calling this — the DO trusts the headers. */ 96 + async forwardSubscribe( 97 + topic: string, 98 + request: Request, 99 + opts: { 100 + did?: string; 101 + querySpec?: SubscriberQuerySpec; 102 + /** Unix ms. DO replays any buffered event with ts > sinceTs before 103 + * going live — closes the snapshot→WS race window on the client. */ 104 + sinceTs?: number; 105 + } = {} 106 + ): Promise<Response> { 107 + const headers = new Headers(request.headers); 108 + if (opts.querySpec) { 109 + headers.set("X-Contrail-Query-Spec", JSON.stringify(opts.querySpec)); 110 + } 111 + const url = new URL("https://do/subscribe"); 112 + if (opts.did) url.searchParams.set("did", opts.did); 113 + if (opts.sinceTs && opts.sinceTs > 0) { 114 + url.searchParams.set("sinceTs", String(opts.sinceTs)); 115 + } 116 + return this.stub(topic).fetch(url.toString(), { 117 + method: "GET", 118 + headers 119 + }); 75 120 } 76 121 } 77 122 ··· 150 195 * This class intentionally avoids the `DurableObject` base class so we don't 151 196 * have to depend on @cloudflare/workers-types at the library level — users 152 197 * wire it up directly in their Worker entry. */ 198 + /** Rolling buffer of recent events, used to close the snapshot→WS race: 199 + * when a new subscriber connects with `?sinceTs=X`, replay any buffered 200 + * event with `event.ts > X` before going live. Bounded by count + age so 201 + * memory stays small. */ 202 + const RECENT_BUFFER_MS = 15_000; 203 + const RECENT_BUFFER_MAX = 500; 204 + 153 205 export class RealtimePubSubDO { 206 + private readonly recentEvents: RealtimeEvent[] = []; 207 + 154 208 constructor( 155 209 protected readonly state: DurableObjectState, 156 210 _env?: unknown 157 211 ) {} 212 + 213 + private pushRecent(event: RealtimeEvent): void { 214 + this.recentEvents.push(event); 215 + const cutoff = Date.now() - RECENT_BUFFER_MS; 216 + while ( 217 + this.recentEvents.length > RECENT_BUFFER_MAX || 218 + (this.recentEvents.length > 0 && this.recentEvents[0]!.ts < cutoff) 219 + ) { 220 + this.recentEvents.shift(); 221 + } 222 + } 158 223 159 224 /** Worker entry delegates `fetch` to this method. */ 160 225 async fetch(request: Request): Promise<Response> { ··· 171 236 } 172 237 if (request.method === "GET" && url.pathname === "/subscribe") { 173 238 const did = url.searchParams.get("did") ?? undefined; 239 + const sinceTsRaw = url.searchParams.get("sinceTs"); 240 + const sinceTs = sinceTsRaw ? Number(sinceTsRaw) : 0; 241 + // Optional query-filter spec, forwarded by the Worker after it has 242 + // verified the caller's auth + access. Parsed once here; the parsed 243 + // object is serialized into the WS attachment so the DO can filter 244 + // events on publish without re-parsing. 245 + let querySpec: SubscriberQuerySpec | undefined; 246 + const rawSpec = request.headers.get("X-Contrail-Query-Spec"); 247 + if (rawSpec) { 248 + try { 249 + querySpec = JSON.parse(rawSpec) as SubscriberQuerySpec; 250 + } catch { 251 + return new Response( 252 + JSON.stringify({ error: "InvalidRequest", message: "bad X-Contrail-Query-Spec" }), 253 + { status: 400 } 254 + ); 255 + } 256 + } 257 + 174 258 if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") { 175 259 const Pair = (globalThis as unknown as { WebSocketPair?: any }).WebSocketPair; 176 260 if (!Pair) return new Response("websockets require Workers", { status: 426 }); 177 261 const pair = new Pair(); 178 - this.acceptWebSocketSubscriber(pair[1], did); 262 + this.acceptWebSocketSubscriber(pair[1], did, querySpec); 263 + if (sinceTs > 0) this.replayRecentTo(pair[1], sinceTs); 179 264 return new Response(null, { 180 265 status: 101, 181 - // @ts-expect-error Workers-specific init field 266 + // Workers-specific init field 182 267 webSocket: pair[0], 183 - }); 268 + } as ResponseInit & { webSocket: unknown }); 184 269 } 185 - return this.openSseResponse(did); 270 + return this.openSseResponse(did, querySpec, sinceTs); 186 271 } 187 272 return new Response("not found", { status: 404 }); 188 273 } 189 274 190 275 /** Fan-out an event to every connected subscriber (WS + SSE). 191 - * Public so tests + advanced callers can skip the HTTP layer. */ 276 + * Public so tests + advanced callers can skip the HTTP layer. 277 + * 278 + * If a subscriber has attached a `querySpec`, we translate the raw event 279 + * into 0–1 watchRecords-shaped events (record.created, record.deleted, 280 + * hydration.added, hydration.removed) and deliver only those. Otherwise 281 + * the raw event is delivered as-is (topic-firehose behaviour for the 282 + * `realtime.subscribe` endpoint). */ 192 283 publishEvent(event: RealtimeEvent): void { 193 - const payload = JSON.stringify(event); 194 - const frame = `event: ${event.kind}\ndata: ${payload}\n\n`; 284 + // Buffer first so a subscriber connecting mid-publish (race-window 285 + // replay) can pick up this event too once they provide their sinceTs. 286 + this.pushRecent(event); 287 + 288 + const rawPayload = JSON.stringify(event); 289 + const rawFrame = `event: ${event.kind}\ndata: ${rawPayload}\n\n`; 195 290 196 291 for (const ws of this.state.getWebSockets()) { 197 - try { 198 - ws.send(payload); 199 - } catch { 200 - /* ignore — socket may be closing */ 201 - } 202 292 const attachment = getAttachment(ws); 293 + 294 + if (attachment?.querySpec) { 295 + const translated = translateForQuery(event, attachment); 296 + if (translated) this.writeSubscriberState(ws, attachment, translated); 297 + for (const msg of translated ?? []) { 298 + try { 299 + ws.send(JSON.stringify(msg)); 300 + } catch { 301 + /* ignore */ 302 + } 303 + } 304 + } else { 305 + try { 306 + ws.send(rawPayload); 307 + } catch { 308 + /* ignore */ 309 + } 310 + } 311 + 203 312 if ( 204 313 event.kind === "member.removed" && 205 314 attachment?.did && ··· 212 321 } 213 322 } 214 323 } 324 + 215 325 for (const entry of this.sseControllers) { 216 - try { 217 - entry.controller.enqueue(this.encoder.encode(frame)); 218 - } catch { 219 - /* drop; cleanup happens on the subscribe-side */ 326 + if (entry.querySpec) { 327 + const translated = translateForQuery(event, entry); 328 + if (translated) this.writeSubscriberStateForSse(entry, translated); 329 + for (const msg of translated ?? []) { 330 + try { 331 + entry.controller.enqueue( 332 + this.encoder.encode(`event: ${msg.kind}\ndata: ${JSON.stringify(msg.data)}\n\n`) 333 + ); 334 + } catch { 335 + /* drop */ 336 + } 337 + } 338 + } else { 339 + try { 340 + entry.controller.enqueue(this.encoder.encode(rawFrame)); 341 + } catch { 342 + /* drop; cleanup happens on the subscribe-side */ 343 + } 220 344 } 345 + 221 346 if ( 222 347 event.kind === "member.removed" && 223 348 entry.did && ··· 232 357 } 233 358 } 234 359 235 - /** Register a server-side WebSocket as a subscriber. Wires the DID attachment. */ 236 - acceptWebSocketSubscriber(serverWs: any, did?: string): void { 360 + /** Register a server-side WebSocket as a subscriber. Wires the DID + 361 + * optional query spec into the hibernation attachment so this DO can 362 + * filter and route events after going to sleep. */ 363 + acceptWebSocketSubscriber( 364 + serverWs: any, 365 + did?: string, 366 + querySpec?: SubscriberQuerySpec 367 + ): void { 237 368 this.state.acceptWebSocket(serverWs, did ? [did] : undefined); 238 - if (did) setAttachment(serverWs, { did }); 369 + if (did || querySpec) { 370 + setAttachment(serverWs, { 371 + did, 372 + querySpec, 373 + parentUris: [], 374 + childToParent: {} 375 + }); 376 + } 239 377 } 240 378 241 379 /** Open an SSE subscriber; returns the streaming Response. */ 242 - openSseResponse(did?: string): Response { 380 + openSseResponse( 381 + did?: string, 382 + querySpec?: SubscriberQuerySpec, 383 + sinceTs = 0 384 + ): Response { 243 385 let entry: SseEntry; 244 386 const stream = new ReadableStream<Uint8Array>({ 245 387 start: (controller) => { 246 - entry = { controller, did }; 388 + entry = { 389 + controller, 390 + did, 391 + querySpec, 392 + parentUris: new Set(), 393 + childToParent: new Map() 394 + }; 247 395 this.sseControllers.add(entry); 248 396 controller.enqueue(this.encoder.encode(`: open\n\n`)); 397 + if (sinceTs > 0) this.replayRecentToSse(entry, sinceTs); 249 398 }, 250 399 cancel: () => { 251 400 this.sseControllers.delete(entry); ··· 261 410 }); 262 411 } 263 412 413 + /** Replay buffered events with ts > sinceTs through this subscriber's 414 + * query-spec filter. Called once, synchronously, on WS connect. */ 415 + private replayRecentTo(ws: any, sinceTs: number): void { 416 + const attachment = getAttachment(ws); 417 + for (const event of this.recentEvents) { 418 + if (event.ts <= sinceTs) continue; 419 + if (attachment?.querySpec) { 420 + const translated = translateForQuery(event, attachment); 421 + if (translated) this.writeSubscriberState(ws, attachment, translated); 422 + for (const msg of translated ?? []) { 423 + try { 424 + ws.send(JSON.stringify(msg)); 425 + } catch { 426 + /* ignore */ 427 + } 428 + } 429 + } else { 430 + try { 431 + ws.send(JSON.stringify(event)); 432 + } catch { 433 + /* ignore */ 434 + } 435 + } 436 + } 437 + } 438 + 439 + private replayRecentToSse(entry: SseEntry, sinceTs: number): void { 440 + for (const event of this.recentEvents) { 441 + if (event.ts <= sinceTs) continue; 442 + if (entry.querySpec) { 443 + const translated = translateForQuery(event, entry); 444 + if (translated) this.writeSubscriberStateForSse(entry, translated); 445 + for (const msg of translated ?? []) { 446 + try { 447 + entry.controller.enqueue( 448 + this.encoder.encode(`event: ${msg.kind}\ndata: ${JSON.stringify(msg.data)}\n\n`) 449 + ); 450 + } catch { 451 + /* drop */ 452 + } 453 + } 454 + } else { 455 + try { 456 + entry.controller.enqueue( 457 + this.encoder.encode(`event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`) 458 + ); 459 + } catch { 460 + /* drop */ 461 + } 462 + } 463 + } 464 + } 465 + 466 + /** Update the persisted WS attachment state after we've decided which 467 + * events to forward. Keeps the parent/child tracking tables warm across 468 + * hibernation. */ 469 + private writeSubscriberState( 470 + ws: any, 471 + attachment: WsAttachment, 472 + translated: TranslatedEvent[] 473 + ): void { 474 + let dirty = false; 475 + for (const msg of translated) { 476 + if (msg.kind === "record.created" && msg.data.record?.uri) { 477 + attachment.parentUris = Array.from( 478 + new Set([...(attachment.parentUris ?? []), msg.data.record.uri]) 479 + ); 480 + dirty = true; 481 + } else if (msg.kind === "record.deleted" && msg.data.uri) { 482 + const before = attachment.parentUris ?? []; 483 + attachment.parentUris = before.filter((u) => u !== msg.data.uri); 484 + if (attachment.parentUris.length !== before.length) dirty = true; 485 + } else if (msg.kind === "hydration.added" && msg.data.child?.rkey) { 486 + attachment.childToParent = { 487 + ...(attachment.childToParent ?? {}), 488 + [msg.data.child.rkey]: { 489 + parentUri: msg.data.parentUri, 490 + relName: msg.data.relation 491 + } 492 + }; 493 + dirty = true; 494 + } else if (msg.kind === "hydration.removed" && msg.data.childRkey) { 495 + const next = { ...(attachment.childToParent ?? {}) }; 496 + if (next[msg.data.childRkey]) { 497 + delete next[msg.data.childRkey]; 498 + attachment.childToParent = next; 499 + dirty = true; 500 + } 501 + } 502 + } 503 + if (dirty) setAttachment(ws, attachment); 504 + } 505 + 506 + private writeSubscriberStateForSse( 507 + entry: SseEntry, 508 + translated: TranslatedEvent[] 509 + ): void { 510 + for (const msg of translated) { 511 + if (msg.kind === "record.created" && msg.data.record?.uri) { 512 + entry.parentUris?.add(msg.data.record.uri); 513 + } else if (msg.kind === "record.deleted" && msg.data.uri) { 514 + entry.parentUris?.delete(msg.data.uri); 515 + } else if (msg.kind === "hydration.added" && msg.data.child?.rkey) { 516 + entry.childToParent?.set(msg.data.child.rkey, { 517 + parentUri: msg.data.parentUri, 518 + relName: msg.data.relation 519 + }); 520 + } else if (msg.kind === "hydration.removed" && msg.data.childRkey) { 521 + entry.childToParent?.delete(msg.data.childRkey); 522 + } 523 + } 524 + } 525 + 264 526 private readonly sseControllers = new Set<SseEntry>(); 265 527 private readonly encoder = new TextEncoder(); 266 528 } ··· 268 530 interface SseEntry { 269 531 controller: ReadableStreamDefaultController<Uint8Array>; 270 532 did: string | undefined; 533 + querySpec?: SubscriberQuerySpec; 534 + parentUris?: Set<string>; 535 + childToParent?: Map<string, { parentUri: string; relName: string }>; 271 536 } 272 537 273 538 interface WsAttachment { 274 539 did?: string; 540 + querySpec?: SubscriberQuerySpec; 541 + /** URIs of primary records currently in this subscriber's result set. */ 542 + parentUris?: string[]; 543 + /** childRkey → parent info, for routing child delete events. */ 544 + childToParent?: Record<string, { parentUri: string; relName: string }>; 275 545 } 276 546 277 547 function setAttachment(ws: any, attachment: WsAttachment): void { ··· 292 562 } 293 563 return (ws.__attachment as WsAttachment | undefined) ?? null; 294 564 } 565 + 566 + // Query-spec filtering lives in ./query-filter so the Worker can reuse it for 567 + // non-DO (InMemoryPubSub) watchRecords paths without bundling the whole DO.
+5
src/core/realtime/publishing-adapter.ts
··· 55 55 getRecord: inner.getRecord.bind(inner), 56 56 listRecords: inner.listRecords.bind(inner), 57 57 listCollections: inner.listCollections.bind(inner), 58 + putBlobMeta: inner.putBlobMeta.bind(inner), 59 + getBlobMeta: inner.getBlobMeta.bind(inner), 60 + listBlobMeta: inner.listBlobMeta.bind(inner), 61 + deleteBlobMeta: inner.deleteBlobMeta.bind(inner), 62 + findOrphanBlobs: inner.findOrphanBlobs.bind(inner), 58 63 59 64 async addMember(spaceUri, did, addedBy) { 60 65 await inner.addMember(spaceUri, did, addedBy);
+235
src/core/realtime/query-filter.ts
··· 1 + /** Shared query-spec → event-translation logic. 2 + * 3 + * Used by: 4 + * - the Durable Object's WS publish path (per-subscriber filter after hibernation) 5 + * - the Worker's SSE / Worker-terminated WS path (in-process filter) 6 + * 7 + * Given a raw RealtimeEvent and a SubscriberQuerySpec, returns the list of 8 + * `{kind, data}` envelopes to send to the subscriber, or `null` if the 9 + * subscriber has no spec (i.e. raw-firehose mode). */ 10 + 11 + import type { RealtimeEvent } from "./types"; 12 + import type { SubscriberQuerySpec } from "./durable-object"; 13 + 14 + export type TranslatedEnvelope = 15 + | { 16 + kind: "record.created"; 17 + data: { 18 + record: { 19 + uri: string; 20 + did: string; 21 + rkey: string; 22 + collection: string; 23 + cid: string | null | undefined; 24 + record: Record<string, unknown>; 25 + time_us: number; 26 + indexed_at: number; 27 + _space: string; 28 + }; 29 + }; 30 + } 31 + | { kind: "record.deleted"; data: { uri: string; did: string; rkey: string } } 32 + | { 33 + kind: "hydration.added"; 34 + data: { 35 + parentUri: string; 36 + relation: string; 37 + child: { 38 + uri: string; 39 + did: string; 40 + rkey: string; 41 + collection: string; 42 + cid: string | null | undefined; 43 + record: Record<string, unknown>; 44 + _space: string; 45 + }; 46 + }; 47 + } 48 + | { 49 + kind: "hydration.removed"; 50 + data: { 51 + parentUri: string; 52 + relation: string; 53 + childRkey: string; 54 + childDid?: string; 55 + }; 56 + }; 57 + 58 + export interface SubscriberView { 59 + querySpec?: SubscriberQuerySpec; 60 + parentUris?: Set<string> | string[]; 61 + childToParent?: 62 + | Map<string, { parentUri: string; relName: string }> 63 + | Record<string, { parentUri: string; relName: string }>; 64 + } 65 + 66 + export function translateForQuery( 67 + event: RealtimeEvent, 68 + sub: SubscriberView 69 + ): TranslatedEnvelope[] | null { 70 + const spec = sub.querySpec; 71 + if (!spec) return null; 72 + if (event.kind !== "record.created" && event.kind !== "record.deleted") return []; 73 + if (event.payload.spaceUri !== spec.spaceUri) return []; 74 + 75 + const primaryUri = `at://${event.payload.authorDid}/${event.payload.collection}/${event.payload.rkey}`; 76 + 77 + if (event.payload.collection === spec.collection) { 78 + if (event.kind === "record.created") { 79 + return [ 80 + { 81 + kind: "record.created", 82 + data: { 83 + record: { 84 + uri: primaryUri, 85 + did: event.payload.authorDid, 86 + rkey: event.payload.rkey, 87 + collection: event.payload.collection, 88 + cid: event.payload.cid, 89 + record: event.payload.record, 90 + time_us: event.ts * 1000, 91 + indexed_at: event.ts, 92 + _space: spec.spaceUri 93 + } 94 + } 95 + } 96 + ]; 97 + } 98 + return [ 99 + { 100 + kind: "record.deleted", 101 + data: { 102 + uri: primaryUri, 103 + did: event.payload.authorDid, 104 + rkey: event.payload.rkey 105 + } 106 + } 107 + ]; 108 + } 109 + 110 + if (!spec.hydrate) return []; 111 + for (const [relName, rel] of Object.entries(spec.hydrate)) { 112 + if (rel.childCollection !== event.payload.collection) continue; 113 + if (event.kind === "record.created") { 114 + const parentUri = getNestedValue( 115 + event.payload.record as Record<string, unknown>, 116 + rel.matchField 117 + ); 118 + if (typeof parentUri !== "string") continue; 119 + if (!hasParent(sub.parentUris, parentUri)) continue; 120 + return [ 121 + { 122 + kind: "hydration.added", 123 + data: { 124 + parentUri, 125 + relation: relName, 126 + child: { 127 + uri: primaryUri, 128 + did: event.payload.authorDid, 129 + rkey: event.payload.rkey, 130 + collection: event.payload.collection, 131 + cid: event.payload.cid, 132 + record: event.payload.record, 133 + _space: spec.spaceUri 134 + } 135 + } 136 + } 137 + ]; 138 + } 139 + const info = getChildInfo(sub.childToParent, event.payload.rkey); 140 + if (!info || info.relName !== relName) continue; 141 + return [ 142 + { 143 + kind: "hydration.removed", 144 + data: { 145 + parentUri: info.parentUri, 146 + relation: relName, 147 + childRkey: event.payload.rkey, 148 + childDid: event.payload.authorDid 149 + } 150 + } 151 + ]; 152 + } 153 + return []; 154 + } 155 + 156 + export function applyEnvelopesToSubscriber( 157 + subscriber: SubscriberView, 158 + envs: TranslatedEnvelope[] 159 + ): void { 160 + for (const msg of envs) { 161 + if (msg.kind === "record.created") { 162 + ensureParentSet(subscriber).add(msg.data.record.uri); 163 + } else if (msg.kind === "record.deleted") { 164 + const set = subscriber.parentUris; 165 + if (set instanceof Set) set.delete(msg.data.uri); 166 + else if (Array.isArray(set)) { 167 + const idx = set.indexOf(msg.data.uri); 168 + if (idx >= 0) set.splice(idx, 1); 169 + } 170 + } else if (msg.kind === "hydration.added") { 171 + ensureChildMap(subscriber).set(msg.data.child.rkey, { 172 + parentUri: msg.data.parentUri, 173 + relName: msg.data.relation 174 + }); 175 + } else if (msg.kind === "hydration.removed") { 176 + const map = subscriber.childToParent; 177 + if (map instanceof Map) map.delete(msg.data.childRkey); 178 + else if (map && typeof map === "object") { 179 + delete (map as Record<string, unknown>)[msg.data.childRkey]; 180 + } 181 + } 182 + } 183 + } 184 + 185 + function ensureParentSet(sub: SubscriberView): Set<string> { 186 + if (sub.parentUris instanceof Set) return sub.parentUris; 187 + const set = new Set<string>(sub.parentUris ?? []); 188 + sub.parentUris = set; 189 + return set; 190 + } 191 + 192 + function ensureChildMap( 193 + sub: SubscriberView 194 + ): Map<string, { parentUri: string; relName: string }> { 195 + if (sub.childToParent instanceof Map) return sub.childToParent; 196 + const map = new Map<string, { parentUri: string; relName: string }>(); 197 + if (sub.childToParent && typeof sub.childToParent === "object") { 198 + for (const [k, v] of Object.entries(sub.childToParent)) map.set(k, v); 199 + } 200 + sub.childToParent = map; 201 + return map; 202 + } 203 + 204 + function hasParent( 205 + parents: Set<string> | string[] | undefined, 206 + uri: string 207 + ): boolean { 208 + if (!parents) return false; 209 + if (parents instanceof Set) return parents.has(uri); 210 + return parents.includes(uri); 211 + } 212 + 213 + function getChildInfo( 214 + map: 215 + | Map<string, { parentUri: string; relName: string }> 216 + | Record<string, { parentUri: string; relName: string }> 217 + | undefined, 218 + rkey: string 219 + ): { parentUri: string; relName: string } | undefined { 220 + if (!map) return undefined; 221 + if (map instanceof Map) return map.get(rkey); 222 + return map[rkey]; 223 + } 224 + 225 + function getNestedValue( 226 + obj: Record<string, unknown>, 227 + path: string 228 + ): unknown { 229 + let cur: unknown = obj; 230 + for (const key of path.split(".")) { 231 + if (cur == null || typeof cur !== "object") return undefined; 232 + cur = (cur as Record<string, unknown>)[key]; 233 + } 234 + return cur; 235 + }
+17 -1
src/core/realtime/ticket.ts
··· 18 18 exp: number; 19 19 /** Unix ms — useful for debugging; ignored on verify. */ 20 20 iat: number; 21 + /** Optional: query-scoped watchRecords spec this ticket authorizes. Present 22 + * when the ticket was minted from a watchRecords handshake. The server 23 + * trusts the signed spec on upgrade and forwards it to the DO. */ 24 + querySpec?: TicketQuerySpec; 25 + } 26 + 27 + export interface TicketQuerySpec { 28 + collection: string; 29 + spaceUri: string; 30 + hydrate?: Record<string, { childCollection: string; matchField: string }>; 21 31 } 22 32 23 33 function normalizeSecret(secret: Uint8Array | string): Uint8Array { ··· 107 117 ); 108 118 } 109 119 110 - async sign(input: { topics: string[]; did: string; ttlMs: number }): Promise<string> { 120 + async sign(input: { 121 + topics: string[]; 122 + did: string; 123 + ttlMs: number; 124 + querySpec?: TicketQuerySpec; 125 + }): Promise<string> { 111 126 const now = Date.now(); 112 127 const payload: TicketPayload = { 113 128 topics: input.topics, 114 129 did: input.did, 115 130 exp: now + input.ttlMs, 116 131 iat: now, 132 + ...(input.querySpec ? { querySpec: input.querySpec } : {}), 117 133 }; 118 134 const payloadPart = b64urlFromString(JSON.stringify(payload)); 119 135 const sig = await crypto.subtle.sign(
+543 -20
src/core/router/collection.ts
··· 21 21 import { hashInviteToken } from "../spaces/invite-token"; 22 22 import type { SpacesContext } from "."; 23 23 import type { Nsid } from "@atcute/lexicons"; 24 + import type { RealtimeEvent } from "../realtime/types"; 25 + import { sseResponse } from "../realtime/sse"; 26 + import { spaceTopic } from "../realtime/types"; 27 + import type { SubscriberQuerySpec } from "../realtime/durable-object"; 28 + import { DurableObjectPubSub } from "../realtime/durable-object"; 29 + import { TicketSigner, type TicketQuerySpec } from "../realtime/ticket"; 30 + import { parseHydrateParams } from "./hydrate"; 31 + import { getRelationField, getNestedValue, nsidForShortName } from "../types"; 32 + 33 + /** Shared implementation of the watchRecords snapshot+live loop. Called by 34 + * both transport branches (SSE and Worker-terminated WS). The caller owns 35 + * the actual socket/stream and provides a `send(kind, data)` closure. */ 36 + async function runQueryStream(opts: { 37 + send: (kind: string, data: unknown) => void; 38 + abort: AbortController; 39 + spaceUri: string; 40 + callerDid: string | undefined; 41 + params: URLSearchParams; 42 + db: Database; 43 + config: ContrailConfig; 44 + collection: string; 45 + colNsid: string; 46 + pubsub: import("../realtime/types").PubSub; 47 + relations: Record<string, import("../types").RelationConfig>; 48 + references: Record<string, import("../types").ReferenceConfig>; 49 + childCollectionMap: Map< 50 + string, 51 + { relName: string; matchField: string; matchMode: "uri" | "did" } 52 + >; 53 + }): Promise<void> { 54 + const { 55 + send, 56 + abort, 57 + spaceUri, 58 + callerDid, 59 + params, 60 + db, 61 + config, 62 + collection, 63 + colNsid, 64 + pubsub, 65 + relations, 66 + references, 67 + childCollectionMap 68 + } = opts; 69 + 70 + const hydrateSpec = parseHydrateParams(params, relations, references); 71 + const trackHydration = Object.keys(hydrateSpec.relations).length > 0; 72 + const parentUris = new Set<string>(); 73 + const parentDids = new Set<string>(); 74 + const childToParent = new Map<string, { parentUri: string; relName: string }>(); 75 + 76 + const primaryUri = (payload: { authorDid: string; collection: string; rkey: string }) => 77 + `at://${payload.authorDid}/${payload.collection}/${payload.rkey}`; 78 + 79 + const handleChildEvent = (event: RealtimeEvent) => { 80 + if (!trackHydration) return; 81 + if (event.kind !== "record.created" && event.kind !== "record.deleted") return; 82 + const meta = childCollectionMap.get(event.payload.collection); 83 + if (!meta) return; 84 + if (!(hydrateSpec.relations as Record<string, number>)[meta.relName]) return; 85 + if (event.payload.spaceUri !== spaceUri) return; 86 + 87 + if (event.kind === "record.created") { 88 + const matched = getNestedValue(event.payload.record, meta.matchField); 89 + if (matched == null) return; 90 + const parent = 91 + meta.matchMode === "did" 92 + ? parentDids.has(String(matched)) 93 + ? `at://${String(matched)}/${colNsid}/_` 94 + : null 95 + : parentUris.has(String(matched)) 96 + ? String(matched) 97 + : null; 98 + if (!parent) return; 99 + childToParent.set(event.payload.rkey, { 100 + parentUri: parent, 101 + relName: meta.relName 102 + }); 103 + send("hydration.added", { 104 + parentUri: parent, 105 + relation: meta.relName, 106 + child: { 107 + uri: primaryUri(event.payload), 108 + did: event.payload.authorDid, 109 + rkey: event.payload.rkey, 110 + collection: event.payload.collection, 111 + cid: event.payload.cid, 112 + record: event.payload.record, 113 + _space: spaceUri 114 + } 115 + }); 116 + } else { 117 + const info = childToParent.get(event.payload.rkey); 118 + if (!info) return; 119 + childToParent.delete(event.payload.rkey); 120 + send("hydration.removed", { 121 + parentUri: info.parentUri, 122 + relation: info.relName, 123 + childRkey: event.payload.rkey, 124 + childDid: event.payload.authorDid 125 + }); 126 + } 127 + }; 128 + 129 + const handleLive = (event: RealtimeEvent) => { 130 + if (abort.signal.aborted) return; 131 + if (event.kind === "member.removed" && event.payload.did === callerDid) { 132 + send("member.removed", event.payload); 133 + abort.abort(); 134 + return; 135 + } 136 + if (event.kind !== "record.created" && event.kind !== "record.deleted") return; 137 + if (event.payload.spaceUri !== spaceUri) return; 138 + 139 + if (event.payload.collection !== colNsid) { 140 + handleChildEvent(event); 141 + return; 142 + } 143 + 144 + const nowUs = event.ts * 1000; 145 + const uri = primaryUri(event.payload); 146 + if (event.kind === "record.created") { 147 + parentUris.add(uri); 148 + parentDids.add(event.payload.authorDid); 149 + send("record.created", { 150 + record: { 151 + uri, 152 + did: event.payload.authorDid, 153 + rkey: event.payload.rkey, 154 + collection: event.payload.collection, 155 + cid: event.payload.cid, 156 + record: event.payload.record, 157 + time_us: nowUs, 158 + indexed_at: event.ts, 159 + _space: spaceUri 160 + } 161 + }); 162 + } else { 163 + parentUris.delete(uri); 164 + send("record.deleted", { 165 + uri, 166 + did: event.payload.authorDid, 167 + rkey: event.payload.rkey 168 + }); 169 + } 170 + }; 171 + 172 + const topic = spaceTopic(spaceUri); 173 + const iter = pubsub.subscribe(topic, abort.signal); 174 + 175 + const buffered: RealtimeEvent[] = []; 176 + let snapshotDone = false; 177 + 178 + const pump = (async () => { 179 + try { 180 + for await (const event of iter) { 181 + if (abort.signal.aborted) break; 182 + if (!snapshotDone) buffered.push(event); 183 + else handleLive(event); 184 + } 185 + } catch { 186 + /* aborted or errored */ 187 + } 188 + })(); 189 + 190 + try { 191 + send("snapshot.start", { spaceUri, collection: colNsid }); 192 + const result = await runPipeline(db, config, collection, params, undefined, [spaceUri]); 193 + for (const record of result.records) { 194 + if (abort.signal.aborted) break; 195 + if (typeof record.uri === "string") parentUris.add(record.uri); 196 + if (typeof record.did === "string") parentDids.add(record.did); 197 + for (const [relName] of Object.entries(hydrateSpec.relations)) { 198 + const hydratedGroups = (record as Record<string, unknown>)[relName]; 199 + if (!hydratedGroups) continue; 200 + const flat: Array<{ rkey?: string }> = Array.isArray(hydratedGroups) 201 + ? (hydratedGroups as Array<{ rkey?: string }>) 202 + : (Object.values(hydratedGroups as Record<string, unknown>).flat() as Array<{ 203 + rkey?: string; 204 + }>); 205 + for (const child of flat) { 206 + if (child?.rkey) { 207 + childToParent.set(child.rkey, { 208 + parentUri: record.uri as string, 209 + relName 210 + }); 211 + } 212 + } 213 + } 214 + send("snapshot.record", { record }); 215 + } 216 + send("snapshot.end", { cursor: result.cursor }); 217 + snapshotDone = true; 218 + for (const event of buffered) handleLive(event); 219 + } catch (err) { 220 + send("error", { 221 + message: err instanceof Error ? err.message : String(err) 222 + }); 223 + abort.abort(); 224 + } 225 + 226 + await pump.catch(() => {}); 227 + } 24 228 25 229 export async function runPipeline( 26 230 db: Database, ··· 194 398 app: Hono, 195 399 db: Database, 196 400 config: ContrailConfig, 197 - spacesCtx?: SpacesContext | null 401 + spacesCtx?: SpacesContext | null, 402 + options: { pubsub?: import("../realtime/types").PubSub | null } = {} 198 403 ): void { 199 404 const ns = config.namespace; 405 + const pubsub = options.pubsub ?? null; 200 406 201 407 /** When a per-collection endpoint receives `?spaceUri=...`, verify the JWT, 202 408 * resolve membership, run the space ACL, and return the caller DID if allowed. ··· 242 448 } 243 449 244 450 const nsid = new URL(c.req.url).pathname.match(/\/xrpc\/([^?]+)/)?.[1] as Nsid | null; 245 - const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid); 451 + const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid, { 452 + authOverride: config.spaces?.authOverride, 453 + }); 246 454 if (!auth) { 247 455 return c.json( 248 456 { error: "AuthRequired", message: "spaceUri requires a valid service-auth JWT or read-grant invite token" }, ··· 277 485 if (spaceUri) { 278 486 const gated = await gateSpaceAccess(c, spaceUri, "read"); 279 487 if (gated instanceof Response) return gated; 280 - // ACL passed — dispatch directly to the spaces adapter. 281 - const nsid = colConfig.collection; 282 - const list = await spacesCtx!.adapter.listRecords(spaceUri, nsid, { 283 - byUser: params.get("byUser") ?? undefined, 284 - cursor: params.get("cursor") ?? undefined, 285 - limit: params.get("limit") ? Number(params.get("limit")) : undefined, 286 - }); 287 - return c.json(list); 488 + // Route through runPipeline with a single-element space list so the 489 + // full filter / sort / hydrate / reference surface works on per-space 490 + // queries too, not just on the cross-space union path. 491 + try { 492 + const result = await runPipeline(db, config, collection, params, undefined, [spaceUri]); 493 + return c.json(result); 494 + } catch (e: any) { 495 + if (e.message === "Could not resolve actor") { 496 + return c.json({ error: e.message }, 400); 497 + } 498 + throw e; 499 + } 288 500 } 289 501 290 - // Union path: when Authorization is present, verify the JWT and fold 291 - // in records from spaces the caller is a member of. 502 + // Union path: when Authorization is present (or the dev-mode override 503 + // can supply claims without one), verify and fold in records from 504 + // spaces the caller is a member of. 292 505 let spaceUris: string[] | undefined; 293 - if (spacesCtx && c.req.header("Authorization")) { 506 + const hasAuthHeader = !!c.req.header("Authorization"); 507 + const hasOverride = !!config.spaces?.authOverride; 508 + if (spacesCtx && (hasAuthHeader || hasOverride)) { 294 509 const nsid = new URL(c.req.url).pathname.match(/\/xrpc\/([^?]+)/)?.[1] as Nsid | null; 295 - const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid); 296 - if (!auth) { 510 + const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid, { 511 + authOverride: config.spaces?.authOverride, 512 + }); 513 + if (auth) { 514 + const { spaces } = await spacesCtx.adapter.listSpaces({ 515 + memberDid: auth.issuer, 516 + limit: 200, 517 + }); 518 + spaceUris = spaces.map((s) => s.uri); 519 + } else if (hasAuthHeader) { 520 + // Had an auth header but it was invalid — reject rather than 521 + // silently downgrading to public results. 297 522 return c.json( 298 523 { error: "AuthRequired", message: "invalid service-auth JWT" }, 299 524 401 300 525 ); 301 526 } 302 - const { spaces } = await spacesCtx.adapter.listSpaces({ 303 - memberDid: auth.issuer, 304 - limit: 200, 305 - }); 306 - spaceUris = spaces.map((s) => s.uri); 307 527 } 308 528 309 529 try { ··· 316 536 throw e; 317 537 } 318 538 }); 539 + 540 + // Streaming variant — same query shape, SSE'd forever. Opted in via the 541 + // presence of the realtime module; no explicit method config needed. 542 + if (pubsub && spacesCtx) { 543 + const colNsid = colConfig.collection; 544 + const relations = colConfig.relations ?? {}; 545 + const references = colConfig.references ?? {}; 546 + // Map child-NSID → { relName, matchField } so we can route child 547 + // events to hydration deltas without re-parsing config per event. 548 + const childCollectionMap = new Map< 549 + string, 550 + { relName: string; matchField: string; matchMode: "uri" | "did" } 551 + >(); 552 + for (const [relName, rel] of Object.entries(relations)) { 553 + const childNsid = nsidForShortName(config, rel.collection) ?? rel.collection; 554 + childCollectionMap.set(childNsid, { 555 + relName, 556 + matchField: getRelationField(rel), 557 + matchMode: rel.match ?? "uri" 558 + }); 559 + } 560 + 561 + // TicketSigner for watch-scoped tickets. Minted on `mode=ws` handshake 562 + // so the subsequent WS upgrade can auth with just `?ticket=...` (no 563 + // cookie or JWT needed — enables cross-origin + stateless clients). 564 + const ticketSigner = config.realtime?.ticketSecret 565 + ? new TicketSigner(config.realtime.ticketSecret) 566 + : null; 567 + const ticketTtl = config.realtime?.ticketTtlMs ?? 120_000; 568 + 569 + app.get(`/xrpc/${ns}.${collection}.watchRecords`, async (c) => { 570 + const params = new URL(c.req.url).searchParams; 571 + const spaceUri = params.get("spaceUri"); 572 + if (!spaceUri) { 573 + return c.json( 574 + { error: "InvalidRequest", message: "spaceUri required (cross-space watch is deferred)" }, 575 + 400 576 + ); 577 + } 578 + 579 + // Try ticket-auth first: if a valid watch ticket scoped to this 580 + // spaceUri/collection is present, use it and skip the JWT gate. 581 + let callerDid: string | undefined; 582 + let querySpec: SubscriberQuerySpec; 583 + let ticketSpec: SubscriberQuerySpec | null = null; 584 + 585 + const providedTicket = params.get("ticket"); 586 + if (providedTicket && ticketSigner) { 587 + const payload = await ticketSigner.verify(providedTicket); 588 + if (payload?.querySpec) { 589 + const ts = payload.querySpec; 590 + if ( 591 + ts.collection === colNsid && 592 + ts.spaceUri === spaceUri && 593 + payload.topics.includes(spaceTopic(spaceUri)) 594 + ) { 595 + callerDid = payload.did; 596 + ticketSpec = { 597 + collection: ts.collection, 598 + spaceUri: ts.spaceUri, 599 + ...(ts.hydrate ? { hydrate: ts.hydrate } : {}) 600 + }; 601 + } 602 + } 603 + } 604 + 605 + if (!ticketSpec) { 606 + const gated = await gateSpaceAccess(c, spaceUri, "read"); 607 + if (gated instanceof Response) return gated; 608 + callerDid = "callerDid" in gated ? gated.callerDid : undefined; 609 + } 610 + 611 + // Build the query spec the DO will filter events against. Prefer 612 + // the ticket's spec when present (guarantees parity with what the 613 + // client asked for at handshake time, no param drift). 614 + const hydrateSpec = parseHydrateParams(params, relations, references); 615 + querySpec = ticketSpec ?? { 616 + collection: colNsid, 617 + spaceUri, 618 + ...(Object.keys(hydrateSpec.relations).length > 0 619 + ? { 620 + hydrate: Object.fromEntries( 621 + Object.entries(hydrateSpec.relations).map(([relName]) => { 622 + const rel = relations[relName]!; 623 + const childNsid = 624 + nsidForShortName(config, rel.collection) ?? rel.collection; 625 + return [ 626 + relName, 627 + { childCollection: childNsid, matchField: getRelationField(rel) } 628 + ]; 629 + }) 630 + ) 631 + } 632 + : {}) 633 + }; 634 + 635 + // Upgrade-to-WS path — forward directly to the DO with the spec, 636 + // so the DO terminates the socket and hibernates when idle. 637 + // Requires snapshot to be fetched separately (see `mode=ws` JSON 638 + // handshake below) or accepted as lossy-on-connect for a plain WS 639 + // upgrade. 640 + const isUpgrade = c.req.header("Upgrade")?.toLowerCase() === "websocket"; 641 + const isWsMode = params.get("mode") === "ws"; 642 + 643 + if (isWsMode && !isUpgrade) { 644 + // Handshake: return snapshot + a ticket the client uses to 645 + // upgrade. Ticket carries the (did, topic, querySpec) signed 646 + // so the WS-upgrade route skips any other auth. 647 + try { 648 + // Capture a server-side timestamp BEFORE running the snapshot. 649 + // Any event published after this moment will have ts > sinceTs 650 + // and be replayed by the DO on WS connect — so the client 651 + // never misses events during the snapshot→WS gap. 652 + const sinceTs = Date.now(); 653 + const result = await runPipeline( 654 + db, 655 + config, 656 + collection, 657 + params, 658 + undefined, 659 + [spaceUri] 660 + ); 661 + let ticket: string | undefined; 662 + if (ticketSigner && callerDid) { 663 + ticket = await ticketSigner.sign({ 664 + topics: [spaceTopic(spaceUri)], 665 + did: callerDid, 666 + ttlMs: ticketTtl, 667 + querySpec: querySpec as TicketQuerySpec 668 + }); 669 + } 670 + const wsUrl = (() => { 671 + const u = new URL(c.req.url); 672 + u.searchParams.delete("mode"); 673 + if (ticket) u.searchParams.set("ticket", ticket); 674 + u.searchParams.set("sinceTs", String(sinceTs)); 675 + return u.pathname + u.search; 676 + })(); 677 + return c.json({ 678 + transport: "ws", 679 + snapshot: { records: result.records, cursor: result.cursor }, 680 + querySpec, 681 + ticket, 682 + ticketTtlMs: ticketTtl, 683 + sinceTs, 684 + wsUrl 685 + }); 686 + } catch (err) { 687 + return c.json( 688 + { error: "SnapshotFailed", message: err instanceof Error ? err.message : String(err) }, 689 + 500 690 + ); 691 + } 692 + } 693 + 694 + if (isUpgrade && pubsub instanceof DurableObjectPubSub) { 695 + // Forward the WS upgrade to the DO. The DO owns the socket from 696 + // here and hibernates when idle. Replays any events buffered 697 + // since the handshake `sinceTs` so the client closes the gap. 698 + const sinceTsParam = params.get("sinceTs"); 699 + const sinceTs = sinceTsParam ? Number(sinceTsParam) : 0; 700 + return pubsub.forwardSubscribe(spaceTopic(spaceUri), c.req.raw, { 701 + did: callerDid, 702 + querySpec, 703 + sinceTs: Number.isFinite(sinceTs) ? sinceTs : 0 704 + }); 705 + } 706 + 707 + const ac = new AbortController(); 708 + const reqSignal = c.req.raw.signal; 709 + if (reqSignal) { 710 + if (reqSignal.aborted) ac.abort(); 711 + else reqSignal.addEventListener("abort", () => ac.abort(), { once: true }); 712 + } 713 + 714 + // Worker-terminated WebSocket — used when pubsub isn't DO-backed 715 + // (dev InMemoryPubSub). Same query-filter loop as SSE; different 716 + // transport. Runs in the same isolate so no cost benefit, but 717 + // matches the prod protocol. 718 + if (isUpgrade) { 719 + const WsPair = (globalThis as unknown as { WebSocketPair?: any }).WebSocketPair; 720 + if (!WsPair) { 721 + return c.json( 722 + { error: "NotSupported", reason: "websockets-require-workers-runtime" }, 723 + 426 724 + ); 725 + } 726 + const pair = new WsPair(); 727 + const clientWs = pair[0] as WebSocket; 728 + const serverWs = pair[1] as WebSocket & { accept?: () => void }; 729 + serverWs.accept?.(); 730 + 731 + const sendWs = (kind: string, data: unknown) => { 732 + try { 733 + serverWs.send(JSON.stringify({ kind, data })); 734 + } catch { 735 + ac.abort(); 736 + } 737 + }; 738 + serverWs.addEventListener?.("close", () => ac.abort()); 739 + serverWs.addEventListener?.("error", () => ac.abort()); 740 + 741 + void runQueryStream({ 742 + send: sendWs, 743 + abort: ac, 744 + spaceUri, 745 + callerDid, 746 + params, 747 + db, 748 + config, 749 + collection, 750 + colNsid, 751 + pubsub, 752 + relations, 753 + references, 754 + childCollectionMap 755 + }).finally(() => { 756 + try { 757 + serverWs.close(); 758 + } catch { 759 + /* ignore */ 760 + } 761 + }); 762 + 763 + return new Response(null, { 764 + status: 101, 765 + webSocket: clientWs 766 + } as ResponseInit & { webSocket: unknown }); 767 + } 768 + 769 + // SSE fallback. 770 + const encoder = new TextEncoder(); 771 + const stream = new ReadableStream<Uint8Array>({ 772 + start(controller) { 773 + let closed = false; 774 + const close = () => { 775 + if (closed) return; 776 + closed = true; 777 + try { 778 + controller.close(); 779 + } catch { 780 + /* already closed */ 781 + } 782 + }; 783 + ac.signal.addEventListener("abort", close, { once: true }); 784 + 785 + const send = (kind: string, data: unknown) => { 786 + if (closed) return; 787 + try { 788 + controller.enqueue( 789 + encoder.encode(`event: ${kind}\ndata: ${JSON.stringify(data)}\n\n`) 790 + ); 791 + } catch { 792 + close(); 793 + } 794 + }; 795 + 796 + const keepalive = setInterval(() => { 797 + if (closed) return; 798 + try { 799 + controller.enqueue(encoder.encode(`: keepalive\n\n`)); 800 + } catch { 801 + close(); 802 + } 803 + }, 15_000); 804 + ac.signal.addEventListener( 805 + "abort", 806 + () => clearInterval(keepalive), 807 + { once: true } 808 + ); 809 + 810 + void runQueryStream({ 811 + send, 812 + abort: ac, 813 + spaceUri, 814 + callerDid, 815 + params, 816 + db, 817 + config, 818 + collection, 819 + colNsid, 820 + pubsub, 821 + relations, 822 + references, 823 + childCollectionMap 824 + }).finally(() => close()); 825 + }, 826 + cancel() { 827 + ac.abort(); 828 + }, 829 + }); 830 + 831 + return new Response(stream, { 832 + status: 200, 833 + headers: { 834 + "content-type": "text/event-stream", 835 + "cache-control": "no-cache, no-transform", 836 + connection: "keep-alive", 837 + "x-accel-buffering": "no", 838 + }, 839 + }); 840 + }); 841 + } 319 842 } 320 843 321 844 if (!methods.includes("getRecord")) {
+5 -3
src/core/router/index.ts
··· 108 108 } 109 109 110 110 registerAdminRoutes(app, db, config); 111 - registerCollectionRoutes(app, db, config, spacesCtx); 111 + registerCollectionRoutes(app, db, config, spacesCtx, { pubsub: realtimePubsub }); 112 112 registerFeedRoutes(app, db, config); 113 113 registerNotifyRoute(app, db, config); 114 114 registerSpacesRoutes(app, spacesDb, config, options.spaces, spacesCtx); 115 + 116 + const authOverride = config.spaces?.authOverride; 115 117 116 118 if (config.community && spacesCtx) { 117 119 // Community routes reuse the spaces service-auth middleware (same JWT verifier). 118 120 const authMiddleware = 119 121 options.community?.authMiddleware ?? 120 122 options.spaces?.authMiddleware ?? 121 - createServiceAuthMiddleware(spacesCtx.verifier); 123 + createServiceAuthMiddleware(spacesCtx.verifier, { authOverride }); 122 124 registerCommunityRoutes( 123 125 app, 124 126 spacesDb, ··· 132 134 const authMiddleware = 133 135 options.realtime?.authMiddleware ?? 134 136 options.spaces?.authMiddleware ?? 135 - createServiceAuthMiddleware(spacesCtx.verifier); 137 + createServiceAuthMiddleware(spacesCtx.verifier, { authOverride }); 136 138 const communityAdapter = config.community ? new CommunityAdapter(spacesDb) : null; 137 139 registerRealtimeRoutes(app, config, spacesCtx.adapter, communityAdapter, { 138 140 authMiddleware,
+51 -5
src/core/spaces/auth.ts
··· 43 43 44 44 /** Hono middleware that verifies the Authorization: Bearer <JWT> as an atproto 45 45 * service-auth token. On success, attaches the decoded claims to c.var.serviceAuth. 46 - * Expected Nsid method is taken from the route pattern (last segment after /xrpc/). */ 46 + * Expected Nsid method is taken from the route pattern (last segment after /xrpc/). 47 + * 48 + * If `authOverride` is provided and returns non-null claims, the JWT check is 49 + * skipped and the override's claims are attached instead — used to paper over 50 + * bsky.social's loopback-client restriction on `getServiceAuth` during dev. 51 + * The override must be gated in the caller (e.g. an env flag) since it 52 + * bypasses cryptographic verification. */ 47 53 export function createServiceAuthMiddleware( 48 - verifier: ServiceJwtVerifier 54 + verifier: ServiceJwtVerifier, 55 + options: { 56 + authOverride?: ( 57 + req: Request 58 + ) => Promise<import("./types").AuthOverrideResult | null> | import("./types").AuthOverrideResult | null; 59 + } = {} 49 60 ): MiddlewareHandler { 50 61 return async (c, next) => { 62 + const lxm = extractLxmFromPath(c); 63 + 64 + if (options.authOverride) { 65 + const override = await options.authOverride(c.req.raw); 66 + if (override) { 67 + c.set("serviceAuth", { 68 + issuer: override.issuer, 69 + audience: override.audience ?? "", 70 + lxm: override.lxm ?? lxm ?? undefined, 71 + clientId: override.clientId, 72 + } satisfies ServiceAuth); 73 + await next(); 74 + return; 75 + } 76 + } 77 + 51 78 const header = c.req.header("Authorization"); 52 79 if (!header || !header.startsWith("Bearer ")) { 53 80 return c.json({ error: "AuthRequired", message: "Missing bearer token" }, 401); 54 81 } 55 82 const token = header.slice(7).trim(); 56 - const lxm = extractLxmFromPath(c); 57 83 58 84 const result = await verifier.verify(token, { lxm }); 59 85 if (!result.ok) { ··· 85 111 86 112 /** Verify a request's Authorization: Bearer token out-of-band (e.g. from a 87 113 * route handler that doesn't always require auth). Returns the claims on 88 - * success, or null if missing/invalid. */ 114 + * success, or null if missing/invalid. 115 + * 116 + * If `authOverride` is provided and returns claims, the JWT path is skipped 117 + * and those claims are returned — same bypass as the middleware-level auth. */ 89 118 export async function verifyServiceAuthRequest( 90 119 verifier: ServiceJwtVerifier, 91 120 request: Request, 92 - lxm?: Nsid | null 121 + lxm?: Nsid | null, 122 + options: { 123 + authOverride?: ( 124 + req: Request 125 + ) => Promise<import("./types").AuthOverrideResult | null> | import("./types").AuthOverrideResult | null; 126 + } = {} 93 127 ): Promise<ServiceAuth | null> { 128 + if (options.authOverride) { 129 + const override = await options.authOverride(request); 130 + if (override) { 131 + return { 132 + issuer: override.issuer, 133 + audience: override.audience ?? "", 134 + lxm: override.lxm ?? lxm ?? undefined, 135 + clientId: override.clientId, 136 + }; 137 + } 138 + } 139 + 94 140 const header = request.headers.get("Authorization"); 95 141 if (!header || !header.startsWith("Bearer ")) return null; 96 142 const token = header.slice(7).trim();
+4 -1
src/core/spaces/router.ts
··· 46 46 const adapter = options.adapter ?? ctx?.adapter ?? new HostedAdapter(db, config); 47 47 const verifier = ctx?.verifier ?? buildVerifier(spacesConfig); 48 48 const auth = 49 - options.authMiddleware ?? createServiceAuthMiddleware(verifier); 49 + options.authMiddleware ?? 50 + createServiceAuthMiddleware(verifier, { 51 + authOverride: spacesConfig.authOverride, 52 + }); 50 53 51 54 /** Read-route auth: skip the JWT middleware when an `?inviteToken=` is 52 55 * present so anonymous bearer reads don't 401 before the route handler can
+18
src/core/spaces/types.ts
··· 37 37 resolver?: DidDocumentResolver; 38 38 /** Blob-upload backend. When omitted, blob XRPCs are not exposed. */ 39 39 blobs?: SpacesBlobsConfig; 40 + /** Dev-only auth bypass. Runs BEFORE JWT verification. If it returns 41 + * non-null claims, those are used as the authenticated caller and the 42 + * JWT check is skipped. Intended for local development where bsky.social 43 + * rejects `getServiceAuth` for loopback OAuth clients; pair with a 44 + * signed-cookie session check so only your own browser can trigger it. 45 + * Never set in production. */ 46 + authOverride?: ( 47 + req: Request 48 + ) => Promise<AuthOverrideResult | null> | AuthOverrideResult | null; 49 + } 50 + 51 + /** Claims returned by an `authOverride`. Matches the ServiceAuth shape the 52 + * normal JWT path attaches to the request. */ 53 + export interface AuthOverrideResult { 54 + issuer: string; 55 + audience?: string; 56 + lxm?: string; 57 + clientId?: string; 40 58 } 41 59 42 60 export interface SpaceRow {
+25 -11
examples/sveltekit-group-chat/src/routes/+layout.server.ts
··· 1 1 import type { LayoutServerLoad } from './$types'; 2 - import { getServerClient } from '$lib/contrail'; 3 - import { extractProfile } from '$lib/contrail/client'; 2 + import { dispatch } from '$lib/contrail'; 3 + 4 + interface Profile { 5 + handle?: string; 6 + displayName?: string; 7 + avatar?: string; 8 + } 4 9 5 10 export const load: LayoutServerLoad = async ({ locals, platform }) => { 6 11 if (!locals.did || !locals.client) { ··· 8 13 } 9 14 10 15 try { 11 - const client = getServerClient(platform!.env.DB); 12 - const res = await client.get('statusphere.app.getProfile', { 13 - params: { actor: locals.did } 14 - }); 15 - 16 + // tools.atmo.chat.getProfile is unauthenticated. 17 + const req = new Request( 18 + `http://localhost/xrpc/tools.atmo.chat.getProfile?actor=${encodeURIComponent(locals.did)}` 19 + ); 20 + const res = await dispatch(req, platform!.env); 16 21 if (!res.ok) return { did: locals.did, profile: null }; 17 - 18 - return { 19 - did: locals.did, 20 - profile: extractProfile(res.data) 22 + const data = (await res.json()) as { 23 + profiles?: Array<{ 24 + did: string; 25 + handle?: string | null; 26 + record?: { displayName?: string; avatar?: string }; 27 + }>; 21 28 }; 29 + const entry = data.profiles?.[0]; 30 + const profile: Profile = { 31 + handle: entry?.handle ?? undefined, 32 + displayName: entry?.record?.displayName, 33 + avatar: entry?.record?.avatar 34 + }; 35 + return { did: locals.did, profile }; 22 36 } catch { 23 37 return { did: locals.did, profile: null }; 24 38 }
+19 -8
examples/sveltekit-group-chat/src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 2 import '../app.css'; 3 - import { AtprotoLoginModal } from '@foxui/social'; 4 - import { login, signup } from '$lib/atproto'; 5 3 import { Head, ThemeToggle } from '@foxui/core'; 6 4 import LoginModal from '$lib/atproto/ui/LoginModal.svelte'; 5 + import { profiles } from '$lib/rooms/profiles.svelte'; 7 6 8 - let { children } = $props(); 7 + let { data, children } = $props(); 8 + 9 + // Seed the profile cache with the logged-in user so their own handle shows 10 + // up everywhere (members list, chat messages) without a separate fetch. 11 + $effect(() => { 12 + if (data.did && data.profile) { 13 + profiles[data.did] = { 14 + did: data.did, 15 + handle: data.profile.handle, 16 + displayName: data.profile.displayName, 17 + avatar: data.profile.avatar 18 + }; 19 + } 20 + }); 9 21 </script> 10 22 11 23 {@render children()} 12 24 13 25 <LoginModal /> 14 26 15 - <ThemeToggle class="absolute top-2 left-2" /> 27 + <ThemeToggle class="absolute top-2 right-2" /> 16 28 17 29 <Head 18 - title="statusphere" 19 - emojiFavicon="🥳" 20 - description="svelte + cloudflare workers statusphere" 21 - image="/og.png" 30 + title="chat demo" 31 + emojiFavicon="💬" 32 + description="contrail chat demo" 22 33 />
+60 -32
examples/sveltekit-group-chat/src/routes/+page.server.ts
··· 1 - import { getServerClient } from '$lib/contrail'; 2 - import { extractProfile, type Profile } from '$lib/contrail/client'; 3 1 import type { PageServerLoad } from './$types'; 2 + import type { Client } from '@atcute/client'; 3 + import { authedFetch } from '$lib/rooms/server'; 4 + import { parseSpaceUri } from '$lib/rooms/uri'; 4 5 5 - export const load: PageServerLoad = async ({ platform }) => { 6 + interface ServerEntry { 7 + communityDid: string; 8 + name: string; 9 + description?: string; 10 + iconUrl?: string; 11 + createdAt: string; 12 + membersUri: string; 13 + } 14 + 15 + export const load: PageServerLoad = async ({ locals, platform }) => { 16 + if (!locals.did || !locals.client) { 17 + return { loggedIn: false, servers: [] as ServerEntry[] }; 18 + } 19 + 20 + const ctx = { 21 + env: platform!.env, 22 + client: locals.client as Client, 23 + did: locals.did as string 24 + }; 25 + 6 26 try { 7 - const client = getServerClient(platform!.env.DB); 8 - 9 - const res = await client.get('statusphere.app.status.listRecords', { 10 - params: { limit: 50, profiles: true, sort: 'createdAt', order: 'desc' } 11 - }); 12 - 13 - if (!res.ok) return { statuses: [], profiles: {} }; 14 - 15 - const records = res.data.records; 16 - const statuses = records 17 - .map((r) => { 18 - const record = r.record as { status: string; createdAt: string }; 19 - return { 20 - did: r.did, 21 - rkey: r.rkey, 22 - status: record.status, 23 - createdAt: record.createdAt 27 + const data = await authedFetch<{ 28 + records: Array<{ 29 + did: string; 30 + rkey: string; 31 + record: { 32 + communityDid?: string; 33 + name?: string; 34 + description?: string; 35 + createdAt?: string; 36 + icon?: { ref?: { $link?: string } }; 24 37 }; 25 - }) 26 - .filter((s) => !isNaN(new Date(s.createdAt).getTime())); 38 + space?: string; 39 + }>; 40 + }>(ctx, 'tools.atmo.chat.server.listRecords', { query: { limit: '50' } }); 27 41 28 - const profiles: Record<string, Profile> = {}; 29 - if (res.data.profiles) { 30 - for (const p of res.data.profiles) { 31 - profiles[p.did] = extractProfile(p); 32 - } 42 + const servers: ServerEntry[] = []; 43 + for (const r of data.records) { 44 + if (!r.space) continue; 45 + const parsed = parseSpaceUri(r.space); 46 + if (!parsed) continue; 47 + if (r.did !== parsed.communityDid) continue; 48 + if (parsed.key !== 'members') continue; 49 + if (r.rkey !== 'self') continue; 50 + const rec = r.record; 51 + if (!rec?.communityDid || !rec.name || !rec.createdAt) continue; 52 + const iconCid = rec.icon?.ref?.$link; 53 + servers.push({ 54 + communityDid: rec.communityDid, 55 + name: rec.name, 56 + description: rec.description, 57 + iconUrl: iconCid 58 + ? `/api/blob?spaceUri=${encodeURIComponent(r.space)}&cid=${encodeURIComponent(iconCid)}` 59 + : undefined, 60 + createdAt: rec.createdAt, 61 + membersUri: r.space 62 + }); 33 63 } 64 + servers.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1)); 34 65 35 - return { statuses, profiles }; 66 + return { loggedIn: true, servers }; 36 67 } catch { 37 - return { 38 - statuses: [] as { did: string; rkey: string; status: string; createdAt: string }[], 39 - profiles: {} as Record<string, Profile> 40 - }; 68 + return { loggedIn: true, servers: [] as ServerEntry[] }; 41 69 } 42 70 };
+59 -171
examples/sveltekit-group-chat/src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { flip } from 'svelte/animate'; 3 - import { scale } from 'svelte/transition'; 4 - import { onMount } from 'svelte'; 5 - import { SvelteSet } from 'svelte/reactivity'; 6 - import { user, logout } from '$lib/atproto'; 7 - import { Button } from '@foxui/core'; 8 - import { GithubCorner, PopoverEmojiPicker } from '@foxui/social'; 9 - import { RelativeTime } from '@foxui/time'; 10 - import { JetstreamSubscription } from '@atcute/jetstream'; 11 - 12 - import { createTID } from '$lib/atproto/methods'; 13 - import { putRecord } from '$lib/atproto/server/repo.remote'; 14 - import { emojiToNotoAnimatedWebp } from '$lib/emojis'; 2 + import { goto } from '$app/navigation'; 3 + import { Button, Heading } from '@foxui/core'; 15 4 import { atProtoLoginModalState } from '$lib/atproto/ui/LoginModal.svelte'; 16 - import { getClient, extractProfile } from '$lib/contrail/client'; 5 + import { user, logout } from '$lib/atproto'; 6 + import { RelativeTime } from '@foxui/time'; 17 7 18 8 let { data } = $props(); 19 - 20 - let open = $state(false); 21 - let localStatuses = $state<{ did: string; rkey: string; status: string; createdAt: string }[]>( 22 - [] 23 - ); 24 - let liveStatuses = $state<{ did: string; rkey: string; status: string; createdAt: string }[]>([]); 25 - 26 - // Deduplication set seeded from server data 27 - // svelte-ignore state_referenced_locally 28 - let seenKeys = new SvelteSet<string>(data.statuses.map((s) => `${s.did}-${s.rkey}`)); 29 - 30 - // Client-side profile cache seeded from server data 31 - // svelte-ignore state_referenced_locally 32 - let profiles = $state<Record<string, { handle: string; displayName?: string; avatar?: string }>>({ 33 - ...data.profiles 34 - }); 35 - 36 - const contrailClient = getClient(); 37 - 38 - async function fetchProfile(did: string) { 39 - if (profiles[did]) return; 40 - try { 41 - const res = await contrailClient.get('statusphere.app.getProfile', { 42 - params: { actor: did } 43 - }); 44 - profiles[did] = extractProfile(res.data); 45 - } catch { 46 - // ignore fetch errors 47 - } 48 - } 49 - 50 - let allStatuses = $derived([...localStatuses, ...liveStatuses, ...data.statuses]); 51 - 52 - // Jetstream subscription 53 - onMount(() => { 54 - const subscription = new JetstreamSubscription({ 55 - url: 'wss://jetstream1.us-east.bsky.network', 56 - wantedCollections: ['xyz.statusphere.status'] 57 - }); 58 - 59 - const iterator = subscription[Symbol.asyncIterator](); 60 - 61 - (async () => { 62 - try { 63 - while (true) { 64 - const { value: event, done } = await iterator.next(); 65 - if (done) break; 66 - 67 - if (event.kind !== 'commit') continue; 68 - if (event.commit.operation !== 'create') continue; 69 - 70 - const { did } = event; 71 - const { rkey, record } = event.commit as { 72 - rkey: string; 73 - record: { status: string; createdAt: string }; 74 - }; 75 - const key = `${did}-${rkey}`; 76 - 77 - if (seenKeys.has(key)) continue; 78 - seenKeys.add(key); 79 - 80 - await fetchProfile(did); 81 - 82 - liveStatuses = [ 83 - { did, rkey, status: record.status, createdAt: record.createdAt }, 84 - ...liveStatuses 85 - ]; 86 - } 87 - } catch { 88 - // subscription closed or errored 89 - } 90 - })(); 91 - 92 - return () => { 93 - iterator.return!(); 94 - }; 95 - }); 96 9 </script> 97 10 98 - <div class="mx-auto max-w-xl px-4 my-16"> 99 - <h1 class="mb-4 text-3xl font-bold">svelte + cloudflare workers statusphere</h1> 100 - 101 - <GithubCorner href="https://github.com/flo-bit/svelte-cloudflare-statusphere" /> 11 + <div class="mx-auto my-16 max-w-2xl px-4"> 12 + <div class="mb-8 flex items-center justify-between"> 13 + <Heading>group chat</Heading> 14 + {#if user.isLoggedIn} 15 + <div class="flex items-center gap-2"> 16 + <span class="text-base-500 text-sm">{user.profile?.handle ?? user.did}</span> 17 + <Button variant="ghost" size="sm" onclick={() => logout()}>Sign out</Button> 18 + </div> 19 + {:else} 20 + <Button size="sm" onclick={() => (atProtoLoginModalState.open = true)}>Sign in</Button> 21 + {/if} 22 + </div> 102 23 103 24 {#if !user.isLoggedIn} 104 - <Button class="my-4" size="lg" onclick={() => atProtoLoginModalState.open = true} 105 - >Login to post a status</Button 106 - > 25 + <p class="text-base-500"> 26 + Sign in with your atproto account to create a server or join one you've been invited to. 27 + </p> 107 28 {:else} 108 - <div class="my-8 flex items-center gap-2"> 109 - <PopoverEmojiPicker 110 - onpicked={async (emoji) => { 111 - const rkey = createTID(); 112 - const createdAt = new Date().toISOString(); 113 - const key = `${user.did!}-${rkey}`; 114 - seenKeys.add(key); 115 - localStatuses = [ 116 - { did: user.did!, rkey, status: emoji.unicode, createdAt }, 117 - ...localStatuses 118 - ]; 119 - open = false; 120 - await putRecord({ 121 - rkey, 122 - collection: 'xyz.statusphere.status', 123 - record: { 124 - status: emoji.unicode, 125 - createdAt 126 - } 127 - }); 128 - }} 129 - bind:open 130 - > 131 - {#snippet child({ props })} 132 - <Button size="lg" {...props}>Post a status</Button> 133 - {/snippet} 134 - </PopoverEmojiPicker> 135 - <Button variant="ghost" onclick={() => logout()}>Sign Out</Button> 29 + <div class="mb-6 flex items-center justify-between"> 30 + <h2 class="text-lg font-semibold">Your servers</h2> 31 + <Button size="sm" onclick={() => goto('/new')}>+ New server</Button> 136 32 </div> 137 - {/if} 138 33 139 - {#if allStatuses.length > 0} 140 - <ul class="mt-4"> 141 - {#each allStatuses as status, i (`${status.did}-${status.rkey}`)} 142 - {@const profile = 143 - profiles[status.did] ?? 144 - (status.did === user.did && data.profile 145 - ? { displayName: data.profile.displayName, handle: data.profile.handle } 146 - : null)} 147 - {@const animated = emojiToNotoAnimatedWebp(status.status)} 148 - <li class="flex items-center gap-3" animate:flip={{ duration: 300 }}> 149 - <div class="flex flex-col items-center"> 150 - <div 151 - class="bg-base-200 dark:bg-base-950/50 border-base-400/50 dark:border-base-800 flex h-12 w-12 items-center inset-shadow-xs inset-shadow-base-800/10 dark:inset-shadow-black/60 justify-center rounded-full border text-2xl" 34 + {#if data.servers.length === 0} 35 + <div class="text-base-500 rounded-2xl border border-dashed p-8 text-center text-sm"> 36 + No servers yet. Create one to get started. 37 + </div> 38 + {:else} 39 + <ul class="grid gap-3 sm:grid-cols-2"> 40 + {#each data.servers as server (server.communityDid)} 41 + <li> 42 + <a 43 + class="bg-base-100 dark:bg-base-800 hover:bg-base-200 dark:hover:bg-base-700 block rounded-2xl p-4 transition" 44 + href={`/c/${encodeURIComponent(server.communityDid)}`} 152 45 > 153 - {#if animated} 154 - {#if i === 0} 155 - <img in:scale={{ duration: 300 }} src={animated} alt={status.status} class="h-7 w-7" /> 46 + <div class="flex items-center gap-3"> 47 + {#if server.iconUrl} 48 + <img 49 + src={server.iconUrl} 50 + alt="" 51 + class="bg-base-200 dark:bg-base-700 size-10 shrink-0 rounded-full object-cover" 52 + /> 156 53 {:else} 157 - <img src={animated} alt={status.status} class="h-7 w-7" /> 54 + <div 55 + class="bg-base-200 dark:bg-base-700 text-base-500 flex size-10 shrink-0 items-center justify-center rounded-full text-sm font-semibold" 56 + > 57 + {server.name.slice(0, 1).toUpperCase()} 58 + </div> 158 59 {/if} 159 - {:else if i === 0} 160 - <span in:scale={{ duration: 300 }}>{status.status}</span> 161 - {:else} 162 - <span>{status.status}</span> 60 + <div class="min-w-0"> 61 + <div class="truncate font-semibold">{server.name}</div> 62 + <div class="text-base-400 text-xs"> 63 + <RelativeTime date={new Date(server.createdAt)} locale="en-US" /> 64 + </div> 65 + </div> 66 + </div> 67 + {#if server.description} 68 + <div class="text-base-500 mt-2 line-clamp-2 text-sm">{server.description}</div> 163 69 {/if} 164 - </div> 165 - {#if i < allStatuses.length - 1} 166 - <div class="bg-base-400/50 dark:bg-base-800 min-h-3 w-px grow"></div> 167 - {/if} 168 - </div> 169 - <div class="flex items-center gap-1.5 pb-3"> 170 - {#if profile} 171 - <span class="text-accent-500 text-sm font-medium" 172 - >{profile.displayName || profile.handle}</span 173 - > 174 - {:else} 175 - <span class="text-base-400 dark:text-base-500 text-sm" 176 - >{status.did.slice(0, 20)}...</span 177 - > 178 - {/if} 179 - <span class="text-base-400 dark:text-base-500 text-sm">&middot;</span> 180 - <span class="text-base-400 dark:text-base-500 text-sm"> 181 - <RelativeTime date={new Date(status.createdAt)} locale="en-US" /> 182 - </span> 183 - </div> 184 - </li> 185 - {/each} 186 - </ul> 70 + </a> 71 + </li> 72 + {/each} 73 + </ul> 74 + {/if} 187 75 {/if} 188 76 </div>
-27
examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getCursor.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "statusphere.app.getCursor", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Get the current cursor position", 8 - "output": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "properties": { 13 - "time_us": { 14 - "type": "integer" 15 - }, 16 - "date": { 17 - "type": "string" 18 - }, 19 - "seconds_ago": { 20 - "type": "integer" 21 - } 22 - } 23 - } 24 - } 25 - } 26 - } 27 - }
-51
examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getOverview.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "statusphere.app.getOverview", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Get an overview of all indexed collections", 8 - "output": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "required": [ 13 - "total_records", 14 - "collections" 15 - ], 16 - "properties": { 17 - "total_records": { 18 - "type": "integer" 19 - }, 20 - "collections": { 21 - "type": "array", 22 - "items": { 23 - "type": "ref", 24 - "ref": "#collectionStats" 25 - } 26 - } 27 - } 28 - } 29 - } 30 - }, 31 - "collectionStats": { 32 - "type": "object", 33 - "required": [ 34 - "collection", 35 - "records", 36 - "unique_users" 37 - ], 38 - "properties": { 39 - "collection": { 40 - "type": "string" 41 - }, 42 - "records": { 43 - "type": "integer" 44 - }, 45 - "unique_users": { 46 - "type": "integer" 47 - } 48 - } 49 - } 50 - } 51 - }
-137
examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getProfile.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "statusphere.app.getProfile", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Get a user's profiles by DID or handle", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "actor" 12 - ], 13 - "properties": { 14 - "actor": { 15 - "type": "string", 16 - "format": "at-identifier", 17 - "description": "DID or handle of the user" 18 - } 19 - } 20 - }, 21 - "output": { 22 - "encoding": "application/json", 23 - "schema": { 24 - "type": "object", 25 - "required": [ 26 - "profiles" 27 - ], 28 - "properties": { 29 - "profiles": { 30 - "type": "array", 31 - "items": { 32 - "type": "ref", 33 - "ref": "#profileEntry" 34 - } 35 - } 36 - } 37 - } 38 - } 39 - }, 40 - "profileEntry": { 41 - "type": "object", 42 - "required": [ 43 - "did" 44 - ], 45 - "properties": { 46 - "did": { 47 - "type": "string", 48 - "format": "did" 49 - }, 50 - "handle": { 51 - "type": "string" 52 - }, 53 - "uri": { 54 - "type": "string", 55 - "format": "at-uri" 56 - }, 57 - "collection": { 58 - "type": "string", 59 - "format": "nsid" 60 - }, 61 - "rkey": { 62 - "type": "string" 63 - }, 64 - "cid": { 65 - "type": "string" 66 - }, 67 - "record": { 68 - "type": "ref", 69 - "ref": "#appBskyActorProfile" 70 - } 71 - } 72 - }, 73 - "appBskyActorProfile": { 74 - "type": "object", 75 - "properties": { 76 - "avatar": { 77 - "type": "blob", 78 - "accept": [ 79 - "image/png", 80 - "image/jpeg" 81 - ], 82 - "maxSize": 1000000, 83 - "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 84 - }, 85 - "banner": { 86 - "type": "blob", 87 - "accept": [ 88 - "image/png", 89 - "image/jpeg" 90 - ], 91 - "maxSize": 1000000, 92 - "description": "Larger horizontal image to display behind profile view." 93 - }, 94 - "labels": { 95 - "refs": [ 96 - "com.atproto.label.defs#selfLabels" 97 - ], 98 - "type": "union", 99 - "description": "Self-label values, specific to the Bluesky application, on the overall account." 100 - }, 101 - "website": { 102 - "type": "string", 103 - "format": "uri" 104 - }, 105 - "pronouns": { 106 - "type": "string", 107 - "maxLength": 200, 108 - "description": "Free-form pronouns text.", 109 - "maxGraphemes": 20 110 - }, 111 - "createdAt": { 112 - "type": "string", 113 - "format": "datetime" 114 - }, 115 - "pinnedPost": { 116 - "ref": "com.atproto.repo.strongRef", 117 - "type": "ref" 118 - }, 119 - "description": { 120 - "type": "string", 121 - "maxLength": 2560, 122 - "description": "Free-form profile description text.", 123 - "maxGraphemes": 256 124 - }, 125 - "displayName": { 126 - "type": "string", 127 - "maxLength": 640, 128 - "maxGraphemes": 64 129 - }, 130 - "joinedViaStarterPack": { 131 - "ref": "com.atproto.repo.strongRef", 132 - "type": "ref" 133 - } 134 - } 135 - } 136 - } 137 - }
-59
examples/sveltekit-group-chat/lexicons-generated/statusphere/app/notifyOfUpdate.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "statusphere.app.notifyOfUpdate", 4 - "defs": { 5 - "main": { 6 - "type": "procedure", 7 - "description": "Notify of a record change for immediate indexing. Fetches the record from the user's PDS and indexes (or deletes) it.", 8 - "input": { 9 - "encoding": "application/json", 10 - "schema": { 11 - "type": "object", 12 - "properties": { 13 - "uri": { 14 - "type": "string", 15 - "format": "at-uri", 16 - "description": "Single AT URI to fetch and index" 17 - }, 18 - "uris": { 19 - "type": "array", 20 - "items": { 21 - "type": "string", 22 - "format": "at-uri" 23 - }, 24 - "maxLength": 25, 25 - "description": "Batch of AT URIs to fetch and index (max 25)" 26 - } 27 - } 28 - } 29 - }, 30 - "output": { 31 - "encoding": "application/json", 32 - "schema": { 33 - "type": "object", 34 - "required": [ 35 - "indexed", 36 - "deleted" 37 - ], 38 - "properties": { 39 - "indexed": { 40 - "type": "integer", 41 - "description": "Number of records created or updated" 42 - }, 43 - "deleted": { 44 - "type": "integer", 45 - "description": "Number of records deleted (not found on PDS)" 46 - }, 47 - "errors": { 48 - "type": "array", 49 - "items": { 50 - "type": "string" 51 - }, 52 - "description": "Errors for individual URIs that could not be processed" 53 - } 54 - } 55 - } 56 - } 57 - } 58 - } 59 - }
-26
examples/sveltekit-group-chat/lexicons-generated/statusphere/app/permissionSet.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "statusphere.app.permissionSet", 4 - "defs": { 5 - "main": { 6 - "type": "permission-set", 7 - "title": "statusphere.app", 8 - "description": "All XRPC methods exposed by the statusphere.app service.", 9 - "permissions": [ 10 - { 11 - "type": "permission", 12 - "resource": "rpc", 13 - "inheritAud": true, 14 - "lxm": [ 15 - "statusphere.app.getCursor", 16 - "statusphere.app.getOverview", 17 - "statusphere.app.getProfile", 18 - "statusphere.app.notifyOfUpdate", 19 - "statusphere.app.status.getRecord", 20 - "statusphere.app.status.listRecords" 21 - ] 22 - } 23 - ] 24 - } 25 - } 26 - }
-27
examples/sveltekit-group-chat/lexicons-pulled/xyz/statusphere/status.json
··· 1 - { 2 - "id": "xyz.statusphere.status", 3 - "defs": { 4 - "main": { 5 - "key": "tid", 6 - "type": "record", 7 - "record": { 8 - "type": "object", 9 - "required": ["status", "createdAt"], 10 - "properties": { 11 - "status": { 12 - "type": "string", 13 - "maxLength": 32, 14 - "minLength": 1, 15 - "maxGraphemes": 1 16 - }, 17 - "createdAt": { 18 - "type": "string", 19 - "format": "datetime" 20 - } 21 - } 22 - } 23 - } 24 - }, 25 - "$type": "com.atproto.lexicon.schema", 26 - "lexicon": 1 27 - }
+58
examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts
··· 1 + // Auto-generated by scripts/generate.ts. Do not edit by hand. 2 + // Source of truth: the set of XRPC methods contrail emits for this namespace. 3 + 4 + export const xrpcMethods = [ 5 + "tools.atmo.chat.channel.getRecord", 6 + "tools.atmo.chat.channel.listRecords", 7 + "tools.atmo.chat.community.adopt", 8 + "tools.atmo.chat.community.delete", 9 + "tools.atmo.chat.community.deleteRecord", 10 + "tools.atmo.chat.community.getHealth", 11 + "tools.atmo.chat.community.invite.create", 12 + "tools.atmo.chat.community.invite.list", 13 + "tools.atmo.chat.community.invite.redeem", 14 + "tools.atmo.chat.community.invite.revoke", 15 + "tools.atmo.chat.community.list", 16 + "tools.atmo.chat.community.mint", 17 + "tools.atmo.chat.community.putRecord", 18 + "tools.atmo.chat.community.reauth", 19 + "tools.atmo.chat.community.space.create", 20 + "tools.atmo.chat.community.space.delete", 21 + "tools.atmo.chat.community.space.deleteRecord", 22 + "tools.atmo.chat.community.space.grant", 23 + "tools.atmo.chat.community.space.listMembers", 24 + "tools.atmo.chat.community.space.putRecord", 25 + "tools.atmo.chat.community.space.resync", 26 + "tools.atmo.chat.community.space.revoke", 27 + "tools.atmo.chat.community.space.setAccessLevel", 28 + "tools.atmo.chat.community.whoami", 29 + "tools.atmo.chat.getCursor", 30 + "tools.atmo.chat.getOverview", 31 + "tools.atmo.chat.getProfile", 32 + "tools.atmo.chat.message.getRecord", 33 + "tools.atmo.chat.message.listRecords", 34 + "tools.atmo.chat.notifyOfUpdate", 35 + "tools.atmo.chat.realtime.subscribe", 36 + "tools.atmo.chat.realtime.ticket", 37 + "tools.atmo.chat.server.getRecord", 38 + "tools.atmo.chat.server.listRecords", 39 + "tools.atmo.chat.space.addMember", 40 + "tools.atmo.chat.space.createSpace", 41 + "tools.atmo.chat.space.deleteRecord", 42 + "tools.atmo.chat.space.getBlob", 43 + "tools.atmo.chat.space.getRecord", 44 + "tools.atmo.chat.space.getSpace", 45 + "tools.atmo.chat.space.invite.create", 46 + "tools.atmo.chat.space.invite.list", 47 + "tools.atmo.chat.space.invite.redeem", 48 + "tools.atmo.chat.space.invite.revoke", 49 + "tools.atmo.chat.space.leaveSpace", 50 + "tools.atmo.chat.space.listBlobs", 51 + "tools.atmo.chat.space.listMembers", 52 + "tools.atmo.chat.space.listRecords", 53 + "tools.atmo.chat.space.listSpaces", 54 + "tools.atmo.chat.space.putRecord", 55 + "tools.atmo.chat.space.removeMember", 56 + "tools.atmo.chat.space.uploadBlob", 57 + "tools.atmo.chat.space.whoami" 58 + ] as const;
+21 -5
examples/sveltekit-group-chat/src/lib/atproto/settings.ts
··· 1 1 import { dev } from '$app/environment'; 2 2 import { scope } from '@atcute/oauth-node-client'; 3 + import { xrpcMethods } from './generated-methods'; 3 4 4 - // writable collections 5 - export const collections = ['xyz.statusphere.status', 'social.atmo.test.blob'] as const; 5 + // Writable collections on the USER's PDS. This app writes nothing to the user's 6 + // PDS — every record lives in contrail spaces, authored by the user DID via 7 + // space.putRecord or by the community DID via community.space.putRecord. 8 + // Keep the list empty; repo scope is omitted below. 9 + export const collections = [] as const; 6 10 7 11 export type AllowedCollection = (typeof collections)[number]; 8 12 9 - // OAuth scope — add scope.blob(), scope.rpc(), etc. as needed 13 + // OAuth scope. 14 + // 15 + // We ask for: 16 + // - `atproto` — baseline identity scope. 17 + // - `rpc` for every XRPC method contrail emits in this namespace. The list 18 + // is auto-generated by `pnpm generate` (source: contrail's permissionSet), 19 + // so adding/removing collections or toggling modules automatically updates 20 + // the consent prompt. 21 + // - `aud: '*'` — we don't couple the scope to a specific service DID so one 22 + // OAuth client metadata works in dev and prod. The PDS still only mints 23 + // JWTs for the exact `aud` the app asks for at getServiceAuth time. 10 24 export const scopes = [ 11 25 'atproto', 12 - scope.repo({ collection: [...collections] }), 13 - scope.blob({ accept: ['image/*'] }) 26 + scope.rpc({ 27 + aud: '*', 28 + lxm: [...xrpcMethods] as `${string}.${string}.${string}`[] 29 + }) 14 30 ]; 15 31 16 32 // set to false to disable signup
+22 -5
examples/sveltekit-group-chat/src/lib/contrail/config.ts
··· 1 1 import type { ContrailConfig } from '@atmo-dev/contrail'; 2 2 3 - export const config: ContrailConfig = { 4 - namespace: 'statusphere.app', 3 + /** Env-independent contrail config. Spaces/community/realtime are enabled per-request 4 + * from `getContrail(env)` in `./index.ts`, since those modules need bindings or secrets 5 + * that live on `platform.env`. */ 6 + export const namespace = 'tools.atmo.chat'; 7 + 8 + export const baseConfig: ContrailConfig = { 9 + namespace, 5 10 collections: { 6 - status: { 7 - collection: 'xyz.statusphere.status', 11 + server: { 12 + collection: 'tools.atmo.chat.server', 8 13 queryable: { 9 - status: {}, 14 + communityDid: {} 15 + } 16 + }, 17 + channel: { 18 + collection: 'tools.atmo.chat.channel', 19 + queryable: { 20 + communityDid: {}, 21 + visibility: {} 22 + } 23 + }, 24 + message: { 25 + collection: 'tools.atmo.chat.message', 26 + queryable: { 10 27 createdAt: { type: 'range' } 11 28 } 12 29 }
+115 -18
examples/sveltekit-group-chat/src/lib/contrail/index.ts
··· 1 - import { Contrail } from '@atmo-dev/contrail'; 1 + import { 2 + Contrail, 3 + DurableObjectPubSub, 4 + InMemoryPubSub, 5 + MemoryBlobAdapter, 6 + R2BlobAdapter, 7 + type BlobAdapter, 8 + type ContrailConfig, 9 + type DurableObjectNamespace, 10 + type PubSub, 11 + type R2BucketLike 12 + } from '@atmo-dev/contrail'; 2 13 import { createHandler } from '@atmo-dev/contrail/server'; 3 14 import { Client } from '@atcute/client'; 4 - import { config } from './config'; 15 + import { baseConfig } from './config'; 16 + import { getSignedCookieFromRequest } from '$lib/atproto/server/signed-cookie'; 5 17 6 - export const contrail = new Contrail(config); 18 + type Env = App.Platform['env']; 7 19 8 - let initialized = false; 9 - 10 - export async function ensureInit(db: D1Database) { 11 - if (!initialized) { 12 - await contrail.init(db); 13 - initialized = true; 14 - } 20 + interface Bundle { 21 + contrail: Contrail; 22 + handle: (req: Request, db?: unknown) => unknown; 23 + ready: Promise<void>; 15 24 } 16 25 17 - const handle = createHandler(contrail); 26 + let cached: { env: Env; bundle: Bundle } | null = null; 18 27 19 - /** 20 - * Server-side: fully typed @atcute/client that routes through contrail in-process. 21 - * No HTTP roundtrip — calls createHandler directly. 22 - */ 23 - export function getServerClient(db: D1Database) { 28 + function build(env: Env): Bundle { 29 + const devAuth = env.DEV_AUTH === '1'; 30 + 31 + // In dev the DO class isn't wired (vite runs the worker directly, skipping 32 + // the post-build re-export), so fall back to InMemoryPubSub. Single-isolate 33 + // is fine for one-user dev; production uses the DO so publishes fan out 34 + // across isolates. 35 + const pubsub: PubSub = devAuth 36 + ? new InMemoryPubSub() 37 + : new DurableObjectPubSub(env.REALTIME as DurableObjectNamespace); 38 + 39 + // Blob storage: in dev, an in-isolate memory adapter (bytes reset on 40 + // restart). In prod, bind an R2 bucket as `env.BLOBS` and it'll be used. 41 + const blobAdapter: BlobAdapter = env.BLOBS 42 + ? new R2BlobAdapter(env.BLOBS as R2BucketLike) 43 + : new MemoryBlobAdapter(); 44 + 45 + const config: ContrailConfig = { 46 + ...baseConfig, 47 + spaces: { 48 + type: 'tools.atmo.chat.space', 49 + serviceDid: env.SERVICE_DID, 50 + blobs: { 51 + adapter: blobAdapter, 52 + maxSize: 2 * 1024 * 1024, 53 + accept: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'] 54 + }, 55 + // Dev-only bypass: trust the HMAC-signed `did` cookie the OAuth flow 56 + // sets, in lieu of a service-auth JWT. bsky.social rejects 57 + // `getServiceAuth` for loopback clients, so without this the demo 58 + // would need a tunnel. NEVER set DEV_AUTH=1 in prod. 59 + authOverride: devAuth 60 + ? (req: Request) => { 61 + // Accept either the HMAC-signed session cookie (from the 62 + // browser via /xrpc/...) or an X-Dev-Did header (from the 63 + // server-side remote helpers, which construct synthetic 64 + // requests and already trust the session themselves). 65 + const headerDid = req.headers.get('x-dev-did'); 66 + const did = headerDid ?? getSignedCookieFromRequest(req, 'did'); 67 + if (!did) return null; 68 + return { 69 + issuer: did, 70 + audience: env.SERVICE_DID 71 + }; 72 + } 73 + : undefined 74 + }, 75 + community: { 76 + masterKey: env.COMMUNITY_MASTER_KEY, 77 + serviceDid: env.SERVICE_DID 78 + }, 79 + realtime: { 80 + ticketSecret: env.REALTIME_TICKET_SECRET, 81 + pubsub 82 + } 83 + }; 84 + 85 + const contrail = new Contrail(config); 86 + const handle = createHandler(contrail); 87 + const ready = contrail.init(env.DB); 88 + 89 + return { contrail, handle: handle as Bundle['handle'], ready }; 90 + } 91 + 92 + function getBundle(env: Env): Bundle { 93 + if (cached && cached.env === env) return cached.bundle; 94 + const bundle = build(env); 95 + cached = { env, bundle }; 96 + return bundle; 97 + } 98 + 99 + /** Get the shared contrail instance, bound to this worker's env. */ 100 + export async function getContrail(env: Env): Promise<Contrail> { 101 + const b = getBundle(env); 102 + await b.ready; 103 + return b.contrail; 104 + } 105 + 106 + /** Dispatch a Request through contrail's XRPC handler. */ 107 + export async function dispatch(req: Request, env: Env): Promise<Response> { 108 + const b = getBundle(env); 109 + await b.ready; 110 + return (await b.handle(req, env.DB)) as Response; 111 + } 112 + 113 + /** Server-side typed @atcute client that routes through contrail in-process. 114 + * When `asDid` is set, every request carries an Authorization header identifying 115 + * the caller — this is used only for server-originated bootstrap calls (e.g. the 116 + * community mint flow) where we already have the user's DID from the OAuth session 117 + * but want to bypass the atproto PDS round-trip. For normal user actions, route 118 + * through the user's OAuth client instead. */ 119 + export function getServerClient(env: Env): Client { 24 120 return new Client({ 25 121 handler: async (pathname, init) => { 26 - await ensureInit(db); 122 + const b = getBundle(env); 123 + await b.ready; 27 124 const url = new URL(pathname, 'http://localhost'); 28 - return handle(new Request(url, init), db) as Promise<Response>; 125 + return (await b.handle(new Request(url, init), env.DB)) as Response; 29 126 } 30 127 }); 31 128 }
+65
examples/sveltekit-group-chat/src/lib/rooms/invites.remote.ts
··· 1 + /** Thin SvelteKit remote-command wrappers around contrail's community.invite.* 2 + * XRPCs. Contrail owns the token storage, atomic redemption, and access-level 3 + * authorization checks. */ 4 + 5 + import { error } from '@sveltejs/kit'; 6 + import { command, getRequestEvent } from '$app/server'; 7 + import * as v from 'valibot'; 8 + import type { Client } from '@atcute/client'; 9 + import { 10 + createCommunityInvite, 11 + redeemCommunityInvite, 12 + revokeCommunityInvite 13 + } from './server'; 14 + 15 + function requireAuthed() { 16 + const event = getRequestEvent(); 17 + const env = event.platform?.env; 18 + const client = event.locals.client; 19 + const did = event.locals.did; 20 + if (!env) error(500, 'platform env unavailable'); 21 + if (!client || !did) error(401, 'Not authenticated'); 22 + return { env, client: client as Client, did: did as string }; 23 + } 24 + 25 + const AccessLevel = v.picklist(['member', 'manager', 'admin', 'owner']); 26 + 27 + const CreateInviteInput = v.object({ 28 + spaceUri: v.pipe(v.string(), v.minLength(1)), 29 + accessLevel: v.optional(AccessLevel), 30 + expiresInMinutes: v.optional(v.pipe(v.number(), v.minValue(1), v.maxValue(60 * 24 * 90))), 31 + maxUses: v.optional(v.pipe(v.number(), v.minValue(1), v.maxValue(10000))), 32 + note: v.optional(v.pipe(v.string(), v.maxLength(500))) 33 + }); 34 + 35 + export const createInvite = command(CreateInviteInput, async (input) => { 36 + const ctx = requireAuthed(); 37 + const expiresAt = 38 + input.expiresInMinutes != null ? Date.now() + input.expiresInMinutes * 60_000 : undefined; 39 + const res = await createCommunityInvite(ctx, { 40 + spaceUri: input.spaceUri, 41 + accessLevel: input.accessLevel ?? 'member', 42 + expiresAt, 43 + maxUses: input.maxUses, 44 + note: input.note 45 + }); 46 + return { token: res.token, tokenHash: res.tokenHash }; 47 + }); 48 + 49 + const RevokeInviteInput = v.object({ 50 + tokenHash: v.pipe(v.string(), v.minLength(1)) 51 + }); 52 + 53 + export const revokeInvite = command(RevokeInviteInput, async (input) => { 54 + const ctx = requireAuthed(); 55 + return revokeCommunityInvite(ctx, { tokenHash: input.tokenHash }); 56 + }); 57 + 58 + const RedeemInviteInput = v.object({ 59 + token: v.pipe(v.string(), v.minLength(8)) 60 + }); 61 + 62 + export const redeemInvite = command(RedeemInviteInput, async (input) => { 63 + const ctx = requireAuthed(); 64 + return redeemCommunityInvite(ctx, { token: input.token }); 65 + });
+70
examples/sveltekit-group-chat/src/lib/rooms/profiles.svelte.ts
··· 1 + /** Browser-side profile cache. DIDs → handle / displayName / avatar. 2 + * Lazy: first read triggers a `<ns>.getProfile` fetch in the background; 3 + * the reactive store updates when the fetch resolves, so templates reading 4 + * the display string rerender automatically. 5 + * 6 + * Kept dead simple — no eviction, no refresh. A single user session touches 7 + * at most a handful of DIDs, so a plain record is fine. */ 8 + 9 + import { untrack } from 'svelte'; 10 + 11 + export interface ProfileEntry { 12 + did: string; 13 + handle?: string; 14 + displayName?: string; 15 + avatar?: string; 16 + /** true if we tried and failed — stops us from retrying forever. */ 17 + failed?: boolean; 18 + } 19 + 20 + export const profiles = $state<Record<string, ProfileEntry>>({}); 21 + const inFlight = new Set<string>(); 22 + 23 + /** Request a profile fetch if we don't have it. Safe to call from templates. */ 24 + export function ensureProfile(did: string): void { 25 + if (!did) return; 26 + if (untrack(() => profiles[did] || inFlight.has(did))) return; 27 + inFlight.add(did); 28 + void (async () => { 29 + try { 30 + const res = await fetch( 31 + `/xrpc/tools.atmo.chat.getProfile?actor=${encodeURIComponent(did)}` 32 + ); 33 + if (!res.ok) throw new Error(`getProfile ${res.status}`); 34 + const data = (await res.json()) as { 35 + profiles?: Array<{ 36 + did: string; 37 + handle?: string | null; 38 + record?: { displayName?: string; avatar?: string }; 39 + }>; 40 + }; 41 + const entry = data.profiles?.[0]; 42 + profiles[did] = { 43 + did, 44 + handle: entry?.handle ?? undefined, 45 + displayName: entry?.record?.displayName, 46 + avatar: entry?.record?.avatar 47 + }; 48 + } catch { 49 + profiles[did] = { did, failed: true }; 50 + } finally { 51 + inFlight.delete(did); 52 + } 53 + })(); 54 + } 55 + 56 + /** Display name for a DID. Returns handle if known, else a truncated DID. 57 + * Calling this from a template also kicks off a background fetch. */ 58 + export function displayName(did: string): string { 59 + ensureProfile(did); 60 + const p = profiles[did]; 61 + if (p?.displayName) return p.displayName; 62 + if (p?.handle) return p.handle; 63 + return did.length > 14 ? `${did.slice(0, 14)}…` : did; 64 + } 65 + 66 + /** Handle for a DID, if known. Returns null when we don't have it yet. */ 67 + export function handleOf(did: string): string | null { 68 + ensureProfile(did); 69 + return profiles[did]?.handle ?? null; 70 + }
+216
examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts
··· 1 + /** Browser-side realtime: single EventSource against 2 + * `<ns>.realtime.subscribe?ticket=…`, keyed on `community:<did>`. 3 + * - auto-renews the ticket before expiry 4 + * - forwards `record.created` into `channelMessages` 5 + * - bumps `unread` counts for channels that aren't open 6 + * - invalidates the current page (channel list) when new channel records arrive 7 + */ 8 + 9 + import { untrack } from 'svelte'; 10 + import { invalidateAll } from '$app/navigation'; 11 + import { bumpUnread } from './unread.svelte'; 12 + 13 + async function fetchTicket( 14 + topic: string 15 + ): Promise<{ ticket: string; topics: string[]; expiresAt: number }> { 16 + const res = await fetch('/api/ticket', { 17 + method: 'POST', 18 + headers: { 'Content-Type': 'application/json' }, 19 + body: JSON.stringify({ topic }) 20 + }); 21 + if (!res.ok) throw new Error(`ticket mint failed: ${res.status}`); 22 + return res.json() as Promise<{ ticket: string; topics: string[]; expiresAt: number }>; 23 + } 24 + 25 + export interface ChatMessage { 26 + rkey: string; 27 + authorDid: string; 28 + text: string; 29 + createdAt: string; 30 + replyTo?: string; 31 + } 32 + 33 + // ---- message store per-space --------------------------------------------- 34 + 35 + const byChannel = $state<Record<string, ChatMessage[]>>({}); 36 + 37 + export const channelMessages = { 38 + /** Reactive — safe to read in templates / $derived. */ 39 + get(spaceUri: string): ChatMessage[] { 40 + return byChannel[spaceUri] ?? []; 41 + }, 42 + /** Seed the store from a server-rendered initial message list. Merges with 43 + * any realtime events that arrived before hydration. Fully untracked so 44 + * calling this from a `$effect` doesn't re-enter the effect. */ 45 + seed(spaceUri: string, messages: ChatMessage[]) { 46 + untrack(() => { 47 + const existing = byChannel[spaceUri]; 48 + if (!existing) { 49 + byChannel[spaceUri] = [...messages]; 50 + return; 51 + } 52 + const seen = new Set(existing.map((m) => m.rkey)); 53 + const merged = [...existing]; 54 + for (const m of messages) { 55 + if (!seen.has(m.rkey)) { 56 + seen.add(m.rkey); 57 + merged.push(m); 58 + } 59 + } 60 + merged.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1)); 61 + byChannel[spaceUri] = merged; 62 + }); 63 + }, 64 + append(spaceUri: string, m: ChatMessage) { 65 + untrack(() => { 66 + const arr = byChannel[spaceUri] ?? []; 67 + if (arr.some((x) => x.rkey === m.rkey)) return; 68 + byChannel[spaceUri] = [...arr, m].sort((a, b) => 69 + a.createdAt < b.createdAt ? -1 : 1 70 + ); 71 + }); 72 + }, 73 + remove(spaceUri: string, rkey: string) { 74 + untrack(() => { 75 + const arr = byChannel[spaceUri]; 76 + if (!arr) return; 77 + byChannel[spaceUri] = arr.filter((m) => m.rkey !== rkey); 78 + }); 79 + } 80 + }; 81 + 82 + // ---- connection ---------------------------------------------------------- 83 + 84 + interface Connection { 85 + close(): void; 86 + } 87 + 88 + let current: { key: string; conn: Connection } | null = null; 89 + 90 + export function connectCommunityRealtime(communityDid: string): () => void { 91 + const key = `community:${communityDid}`; 92 + // Re-use if already connected. 93 + if (current?.key === key) return current.conn.close; 94 + 95 + current?.conn.close(); 96 + 97 + let closed = false; 98 + let es: EventSource | null = null; 99 + let renewTimer: ReturnType<typeof setTimeout> | null = null; 100 + 101 + const handleEvent = (rawData: string, kind: string) => { 102 + let ev: { 103 + topic: string; 104 + kind: string; 105 + payload: Record<string, unknown>; 106 + ts: number; 107 + }; 108 + try { 109 + ev = JSON.parse(rawData); 110 + } catch { 111 + return; 112 + } 113 + if (kind === 'record.created') { 114 + const p = ev.payload as { 115 + spaceUri: string; 116 + collection: string; 117 + authorDid: string; 118 + rkey: string; 119 + record: Record<string, unknown>; 120 + createdAt: number | string; 121 + }; 122 + if (p.collection === 'tools.atmo.chat.message') { 123 + const rec = p.record as { text?: string; createdAt?: string; replyTo?: string }; 124 + if (rec.text && rec.createdAt) { 125 + channelMessages.append(p.spaceUri, { 126 + rkey: p.rkey, 127 + authorDid: p.authorDid, 128 + text: rec.text, 129 + createdAt: rec.createdAt, 130 + replyTo: rec.replyTo 131 + }); 132 + // Bump unread if we're not currently on this channel. 133 + if (!isCurrentChannel(p.spaceUri)) { 134 + bumpUnread(p.spaceUri, rec.createdAt); 135 + } 136 + } 137 + } else if (p.collection === 'tools.atmo.chat.channel') { 138 + void invalidateAll(); 139 + } 140 + } else if (kind === 'record.deleted') { 141 + const p = ev.payload as { spaceUri: string; collection: string; rkey: string }; 142 + if (p.collection === 'tools.atmo.chat.message') { 143 + channelMessages.remove(p.spaceUri, p.rkey); 144 + } else if (p.collection === 'tools.atmo.chat.channel') { 145 + void invalidateAll(); 146 + } 147 + } else if (kind === 'member.added' || kind === 'member.removed') { 148 + void invalidateAll(); 149 + } 150 + }; 151 + 152 + async function open() { 153 + if (closed) return; 154 + let ticketRes: { ticket: string; topics: string[]; expiresAt: number }; 155 + try { 156 + ticketRes = await fetchTicket(key); 157 + } catch (err) { 158 + console.warn('realtime ticket mint failed', err); 159 + // backoff + retry 160 + renewTimer = setTimeout(open, 5000); 161 + return; 162 + } 163 + 164 + const url = `/xrpc/tools.atmo.chat.realtime.subscribe?ticket=${encodeURIComponent( 165 + ticketRes.ticket 166 + )}`; 167 + es = new EventSource(url); 168 + 169 + es.addEventListener('record.created', (e) => handleEvent((e as MessageEvent).data, 'record.created')); 170 + es.addEventListener('record.deleted', (e) => handleEvent((e as MessageEvent).data, 'record.deleted')); 171 + es.addEventListener('member.added', (e) => handleEvent((e as MessageEvent).data, 'member.added')); 172 + es.addEventListener('member.removed', (e) => handleEvent((e as MessageEvent).data, 'member.removed')); 173 + 174 + es.addEventListener('error', () => { 175 + // Browser retries automatically; only act if the stream is closed. 176 + if (es?.readyState === EventSource.CLOSED && !closed) { 177 + renewTimer = setTimeout(open, 3000); 178 + } 179 + }); 180 + 181 + // Renew ~20s before expiry. 182 + const ttl = Math.max(ticketRes.expiresAt - Date.now() - 20_000, 30_000); 183 + renewTimer = setTimeout(() => { 184 + es?.close(); 185 + es = null; 186 + void open(); 187 + }, ttl); 188 + } 189 + 190 + void open(); 191 + 192 + const conn: Connection = { 193 + close() { 194 + closed = true; 195 + if (renewTimer) clearTimeout(renewTimer); 196 + es?.close(); 197 + if (current?.key === key) current = null; 198 + } 199 + }; 200 + 201 + current = { key, conn }; 202 + return () => conn.close(); 203 + } 204 + 205 + // ---- helpers ------------------------------------------------------------- 206 + 207 + function isCurrentChannel(spaceUri: string): boolean { 208 + if (typeof window === 'undefined') return false; 209 + return currentChannelSpaceUri === spaceUri; 210 + } 211 + 212 + let currentChannelSpaceUri: string | null = null; 213 + 214 + export function setCurrentChannel(spaceUri: string | null) { 215 + currentChannelSpaceUri = spaceUri; 216 + }
+218
examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts
··· 1 + /** SvelteKit remote functions the browser can invoke directly. They run on 2 + * the server, so they have access to `locals.client` (OAuth session) and 3 + * `platform.env` (D1, DO, secrets). */ 4 + 5 + import { error } from '@sveltejs/kit'; 6 + import { command, getRequestEvent } from '$app/server'; 7 + import * as v from 'valibot'; 8 + import type { Client } from '@atcute/client'; 9 + import { 10 + createSpace, 11 + communityPutRecord, 12 + grantAccess, 13 + mintCommunity, 14 + spacePutRecord, 15 + revokeAccess, 16 + setAccessLevel, 17 + uploadBlob 18 + } from './server'; 19 + import { buildMembersUri } from './uri'; 20 + 21 + function requireAuthed() { 22 + const event = getRequestEvent(); 23 + const env = event.platform?.env; 24 + const client = event.locals.client; 25 + const did = event.locals.did; 26 + if (!env) error(500, 'platform env unavailable'); 27 + if (!client || !did) error(401, 'Not authenticated'); 28 + return { env, client: client as Client, did: did as string }; 29 + } 30 + 31 + // --------------------------------------------------------------------------- 32 + // createCommunity — mint + bootstrap members space + write server record. 33 + // Returns { communityDid, recoveryKey }. The UI must show recoveryKey once. 34 + // --------------------------------------------------------------------------- 35 + 36 + const CreateCommunityInput = v.object({ 37 + name: v.pipe(v.string(), v.minLength(1), v.maxLength(128)), 38 + description: v.optional(v.pipe(v.string(), v.maxLength(2000))), 39 + handle: v.optional(v.pipe(v.string(), v.maxLength(256))), 40 + /** Optional icon blob ref. The bytes must already have been uploaded to 41 + * the community's members space via /api/blob-upload before this call — 42 + * but the members space doesn't exist yet at mint time, so the upload 43 + * flow is: createCommunity first, then separately upload+update. For 44 + * convenience, this endpoint also accepts an icon that was uploaded 45 + * *during* the same request — it stitches it in by reuploading into the 46 + * members space after creation. */ 47 + icon: v.optional( 48 + v.object({ 49 + /** Raw bytes as a base64 string — small enough to inline. */ 50 + bytes: v.string(), 51 + mimeType: v.string() 52 + }) 53 + ) 54 + }); 55 + 56 + export const createCommunity = command(CreateCommunityInput, async (input) => { 57 + const ctx = requireAuthed(); 58 + 59 + const mint = await mintCommunity(ctx, input.handle ? { handle: input.handle } : {}); 60 + const communityDid = mint.communityDid; 61 + 62 + const space = await createSpace(ctx, { communityDid, key: 'members' }); 63 + const membersUri = space.space.uri; 64 + 65 + // If an icon was provided, upload it to the members space first, then 66 + // include its blob ref on the server record. 67 + let iconRef: import('./server').BlobRef | undefined; 68 + if (input.icon) { 69 + const bytes = Uint8Array.from(atob(input.icon.bytes), (c) => c.charCodeAt(0)); 70 + iconRef = await uploadBlob(ctx, { 71 + spaceUri: membersUri, 72 + mimeType: input.icon.mimeType, 73 + bytes 74 + }); 75 + } 76 + 77 + await communityPutRecord(ctx, { 78 + spaceUri: membersUri, 79 + collection: 'tools.atmo.chat.server', 80 + rkey: 'self', 81 + record: { 82 + communityDid, 83 + name: input.name, 84 + description: input.description, 85 + ...(iconRef ? { icon: iconRef } : {}), 86 + createdAt: new Date().toISOString() 87 + } 88 + }); 89 + 90 + return { 91 + communityDid, 92 + membersUri, 93 + recoveryKey: mint.recoveryKey 94 + }; 95 + }); 96 + 97 + // --------------------------------------------------------------------------- 98 + // createChannel — new channel space + optional members-delegation (public) or 99 + // direct DID grants (private) + channel record. 100 + // --------------------------------------------------------------------------- 101 + 102 + const CreateChannelInput = v.object({ 103 + communityDid: v.pipe(v.string(), v.minLength(1)), 104 + name: v.pipe(v.string(), v.minLength(1), v.maxLength(128)), 105 + topic: v.optional(v.pipe(v.string(), v.maxLength(512))), 106 + visibility: v.picklist(['public', 'private']), 107 + /** For private channels: DIDs to grant direct member access. */ 108 + memberDids: v.optional(v.array(v.pipe(v.string(), v.minLength(1)))) 109 + }); 110 + 111 + export const createChannel = command(CreateChannelInput, async (input) => { 112 + const ctx = requireAuthed(); 113 + 114 + const created = await createSpace(ctx, { communityDid: input.communityDid }); 115 + const channelUri = created.space.uri; 116 + const channelKey = created.space.key; 117 + 118 + if (input.visibility === 'public') { 119 + await grantAccess(ctx, { 120 + spaceUri: channelUri, 121 + subject: { spaceUri: buildMembersUri(input.communityDid) }, 122 + accessLevel: 'member' 123 + }); 124 + } else { 125 + for (const did of input.memberDids ?? []) { 126 + // Ensure the user is in `members` too so they get the server header. 127 + // Ignore duplicates; grant is upsert. 128 + await grantAccess(ctx, { 129 + spaceUri: buildMembersUri(input.communityDid), 130 + subject: { did }, 131 + accessLevel: 'member' 132 + }).catch(() => {}); 133 + await grantAccess(ctx, { 134 + spaceUri: channelUri, 135 + subject: { did }, 136 + accessLevel: 'member' 137 + }); 138 + } 139 + } 140 + 141 + await communityPutRecord(ctx, { 142 + spaceUri: channelUri, 143 + collection: 'tools.atmo.chat.channel', 144 + rkey: 'self', 145 + record: { 146 + communityDid: input.communityDid, 147 + name: input.name, 148 + topic: input.topic, 149 + visibility: input.visibility, 150 + createdAt: new Date().toISOString() 151 + } 152 + }); 153 + 154 + return { channelUri, channelKey }; 155 + }); 156 + 157 + // --------------------------------------------------------------------------- 158 + // postMessage — append a chat message to a channel. 159 + // --------------------------------------------------------------------------- 160 + 161 + const PostMessageInput = v.object({ 162 + spaceUri: v.pipe(v.string(), v.minLength(1)), 163 + text: v.pipe(v.string(), v.minLength(1), v.maxLength(4000)), 164 + replyTo: v.optional(v.pipe(v.string(), v.minLength(1))) 165 + }); 166 + 167 + export const postMessage = command(PostMessageInput, async (input) => { 168 + const ctx = requireAuthed(); 169 + const res = await spacePutRecord(ctx, { 170 + spaceUri: input.spaceUri, 171 + collection: 'tools.atmo.chat.message', 172 + record: { 173 + text: input.text, 174 + createdAt: new Date().toISOString(), 175 + ...(input.replyTo ? { replyTo: input.replyTo } : {}) 176 + } 177 + }); 178 + return res; 179 + }); 180 + 181 + // --------------------------------------------------------------------------- 182 + // grantMember / revokeMember / setMemberLevel — used in settings UI. 183 + // --------------------------------------------------------------------------- 184 + 185 + const GrantMemberInput = v.object({ 186 + spaceUri: v.pipe(v.string(), v.minLength(1)), 187 + did: v.pipe(v.string(), v.minLength(1)), 188 + accessLevel: v.picklist(['member', 'manager', 'admin', 'owner']) 189 + }); 190 + 191 + export const grantMember = command(GrantMemberInput, async (input) => { 192 + const ctx = requireAuthed(); 193 + return grantAccess(ctx, { 194 + spaceUri: input.spaceUri, 195 + subject: { did: input.did }, 196 + accessLevel: input.accessLevel 197 + }); 198 + }); 199 + 200 + const RevokeMemberInput = v.object({ 201 + spaceUri: v.pipe(v.string(), v.minLength(1)), 202 + did: v.pipe(v.string(), v.minLength(1)) 203 + }); 204 + 205 + export const revokeMember = command(RevokeMemberInput, async (input) => { 206 + const ctx = requireAuthed(); 207 + return revokeAccess(ctx, { spaceUri: input.spaceUri, subject: { did: input.did } }); 208 + }); 209 + 210 + export const setMemberLevel = command(GrantMemberInput, async (input) => { 211 + const ctx = requireAuthed(); 212 + return setAccessLevel(ctx, { 213 + spaceUri: input.spaceUri, 214 + subject: { did: input.did }, 215 + accessLevel: input.accessLevel 216 + }); 217 + }); 218 +
+340
examples/sveltekit-group-chat/src/lib/rooms/server.ts
··· 1 + /** Server-side helpers for calling contrail's authenticated XRPCs. 2 + * 3 + * Contrail's community/space/realtime endpoints authenticate via atproto 4 + * service-auth JWTs. The caller's OAuth session on their PDS can mint those 5 + * JWTs via `com.atproto.server.getServiceAuth`. This module wraps that dance: 6 + * mint → dispatch → return the typed body. 7 + */ 8 + 9 + import type { Client } from '@atcute/client'; 10 + import { dispatch } from '$lib/contrail'; 11 + 12 + type Env = App.Platform['env']; 13 + 14 + interface AuthedCallContext { 15 + env: Env; 16 + /** Caller's OAuth-authenticated atproto client (talks to their PDS). */ 17 + client: Client; 18 + /** Caller's DID. */ 19 + did: string; 20 + } 21 + 22 + /** Authenticated fetch for page loaders — mints a service-auth JWT 23 + * (or takes the dev bypass) and dispatches to contrail in-process. 24 + * Returns the parsed JSON body, or throws with status + payload. */ 25 + export async function authedFetch<T = unknown>( 26 + ctx: AuthedCallContext, 27 + method: string, 28 + opts: { query?: Record<string, string>; body?: unknown } = {} 29 + ): Promise<T> { 30 + return callContrail<T>(ctx, method, opts); 31 + } 32 + 33 + async function mintServiceAuth( 34 + ctx: AuthedCallContext, 35 + lxm: string, 36 + expSeconds = 60 37 + ): Promise<string> { 38 + // Dev bypass: the contrail authOverride trusts our signed session cookie 39 + // instead of a real JWT, so we don't need to hit the PDS. Returns a sentinel 40 + // the override ignores (any string is fine — it's never verified). 41 + if (ctx.env.DEV_AUTH === '1') { 42 + return 'dev'; 43 + } 44 + const res = await ctx.client.get('com.atproto.server.getServiceAuth', { 45 + params: { 46 + aud: ctx.env.SERVICE_DID as `did:${string}:${string}`, 47 + lxm: lxm as `${string}.${string}.${string}`, 48 + exp: Math.floor(Date.now() / 1000) + expSeconds 49 + } 50 + }); 51 + if (!res.ok) { 52 + throw new Error(`getServiceAuth failed: ${JSON.stringify(res.data)}`); 53 + } 54 + const data = res.data as { token: string }; 55 + return data.token; 56 + } 57 + 58 + async function callContrail<T = unknown>( 59 + ctx: AuthedCallContext, 60 + method: string, 61 + opts: { body?: unknown; query?: Record<string, string>; httpMethod?: 'GET' | 'POST' } 62 + ): Promise<T> { 63 + const jwt = await mintServiceAuth(ctx, method); 64 + const url = new URL(`http://localhost/xrpc/${method}`); 65 + for (const [k, v] of Object.entries(opts.query ?? {})) { 66 + url.searchParams.set(k, v); 67 + } 68 + const httpMethod = opts.httpMethod ?? (opts.body === undefined ? 'GET' : 'POST'); 69 + const devAuth = ctx.env.DEV_AUTH === '1'; 70 + const headers: Record<string, string> = { 71 + Authorization: `Bearer ${jwt}`, 72 + ...(opts.body !== undefined ? { 'Content-Type': 'application/json' } : {}) 73 + }; 74 + if (devAuth) headers['X-Dev-Did'] = ctx.did; 75 + const req = new Request(url, { 76 + method: httpMethod, 77 + headers, 78 + body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined 79 + }); 80 + const res = await dispatch(req, ctx.env); 81 + const text = await res.text(); 82 + const json = text ? JSON.parse(text) : {}; 83 + if (!res.ok) { 84 + const err = new Error(`${method} failed (${res.status}): ${text}`); 85 + (err as Error & { status: number; payload: unknown }).status = res.status; 86 + (err as Error & { status: number; payload: unknown }).payload = json; 87 + throw err; 88 + } 89 + return json as T; 90 + } 91 + 92 + // --- community lifecycle ---------------------------------------------------- 93 + 94 + export async function mintCommunity( 95 + ctx: AuthedCallContext, 96 + input: { handle?: string } = {} 97 + ): Promise<{ communityDid: string; recoveryKey: unknown }> { 98 + return callContrail(ctx, 'tools.atmo.chat.community.mint', { body: input }); 99 + } 100 + 101 + // --- space (channel / role) lifecycle -------------------------------------- 102 + 103 + export async function createSpace( 104 + ctx: AuthedCallContext, 105 + input: { communityDid: string; key?: string } 106 + ): Promise<{ space: { uri: string; key: string; ownerDid: string } }> { 107 + return callContrail(ctx, 'tools.atmo.chat.community.space.create', { body: input }); 108 + } 109 + 110 + export async function grantAccess( 111 + ctx: AuthedCallContext, 112 + input: { 113 + spaceUri: string; 114 + subject: { did?: string; spaceUri?: string }; 115 + accessLevel: 'member' | 'manager' | 'admin' | 'owner'; 116 + } 117 + ): Promise<{ ok: true }> { 118 + return callContrail(ctx, 'tools.atmo.chat.community.space.grant', { body: input }); 119 + } 120 + 121 + export async function revokeAccess( 122 + ctx: AuthedCallContext, 123 + input: { spaceUri: string; subject: { did?: string; spaceUri?: string } } 124 + ): Promise<{ ok: true }> { 125 + return callContrail(ctx, 'tools.atmo.chat.community.space.revoke', { body: input }); 126 + } 127 + 128 + export async function setAccessLevel( 129 + ctx: AuthedCallContext, 130 + input: { 131 + spaceUri: string; 132 + subject: { did?: string; spaceUri?: string }; 133 + accessLevel: 'member' | 'manager' | 'admin' | 'owner'; 134 + } 135 + ): Promise<{ ok: true }> { 136 + return callContrail(ctx, 'tools.atmo.chat.community.space.setAccessLevel', { body: input }); 137 + } 138 + 139 + // --- community records (authored by community DID) ------------------------- 140 + 141 + export async function communityPutRecord( 142 + ctx: AuthedCallContext, 143 + input: { 144 + spaceUri: string; 145 + collection: string; 146 + rkey?: string; 147 + record: Record<string, unknown>; 148 + } 149 + ): Promise<{ rkey: string; authorDid: string; createdAt: number }> { 150 + return callContrail(ctx, 'tools.atmo.chat.community.space.putRecord', { body: input }); 151 + } 152 + 153 + // --- user records in a space ----------------------------------------------- 154 + 155 + export async function spacePutRecord( 156 + ctx: AuthedCallContext, 157 + input: { 158 + spaceUri: string; 159 + collection: string; 160 + rkey?: string; 161 + record: Record<string, unknown>; 162 + } 163 + ): Promise<{ rkey: string; authorDid: string; createdAt: number }> { 164 + return callContrail(ctx, 'tools.atmo.chat.space.putRecord', { body: input }); 165 + } 166 + 167 + export async function spaceListRecords( 168 + ctx: AuthedCallContext, 169 + query: { spaceUri: string; collection: string; limit?: number; cursor?: string } 170 + ): Promise<{ 171 + records: Array<{ 172 + spaceUri: string; 173 + collection: string; 174 + authorDid: string; 175 + rkey: string; 176 + cid: string | null; 177 + record: Record<string, unknown>; 178 + createdAt: number; 179 + }>; 180 + cursor?: string; 181 + }> { 182 + const q: Record<string, string> = { 183 + spaceUri: query.spaceUri, 184 + collection: query.collection 185 + }; 186 + if (query.limit) q.limit = String(query.limit); 187 + if (query.cursor) q.cursor = query.cursor; 188 + return callContrail(ctx, 'tools.atmo.chat.space.listRecords', { query: q }); 189 + } 190 + 191 + // --- members --------------------------------------------------------------- 192 + 193 + export async function listSpaceMembers( 194 + ctx: AuthedCallContext, 195 + query: { spaceUri: string; flatten?: boolean } 196 + ): Promise< 197 + | { members: Array<{ did: string; addedAt: number }> } 198 + | { 199 + rows: Array<{ 200 + subject: { did?: string; spaceUri?: string }; 201 + accessLevel: string; 202 + grantedBy: string; 203 + grantedAt: number; 204 + }>; 205 + } 206 + > { 207 + const q: Record<string, string> = { spaceUri: query.spaceUri }; 208 + if (query.flatten) q.flatten = 'true'; 209 + return callContrail(ctx, 'tools.atmo.chat.community.space.listMembers', { query: q }); 210 + } 211 + 212 + export async function whoami( 213 + ctx: AuthedCallContext, 214 + query: { spaceUri: string } 215 + ): Promise<{ spaceUri: string; accessLevel: string | null }> { 216 + return callContrail(ctx, 'tools.atmo.chat.community.whoami', { query }); 217 + } 218 + 219 + // --- realtime -------------------------------------------------------------- 220 + 221 + export async function getRealtimeTicket( 222 + ctx: AuthedCallContext, 223 + input: { topic: string } 224 + ): Promise<{ ticket: string; topics: string[]; expiresAt: number }> { 225 + return callContrail(ctx, 'tools.atmo.chat.realtime.ticket', { body: input }); 226 + } 227 + 228 + // --- invites --------------------------------------------------------------- 229 + 230 + export interface InviteView { 231 + tokenHash: string; 232 + spaceUri: string; 233 + accessLevel: 'member' | 'manager' | 'admin' | 'owner'; 234 + createdBy: string; 235 + createdAt: number; 236 + expiresAt: number | null; 237 + maxUses: number | null; 238 + usedCount: number; 239 + revokedAt: number | null; 240 + note: string | null; 241 + } 242 + 243 + export async function createCommunityInvite( 244 + ctx: AuthedCallContext, 245 + input: { 246 + spaceUri: string; 247 + accessLevel: 'member' | 'manager' | 'admin' | 'owner'; 248 + expiresAt?: number; 249 + maxUses?: number; 250 + note?: string; 251 + } 252 + ): Promise<{ 253 + token: string; 254 + tokenHash: string; 255 + spaceUri: string; 256 + accessLevel: string; 257 + expiresAt: number | null; 258 + maxUses: number | null; 259 + createdAt: number; 260 + }> { 261 + return callContrail(ctx, 'tools.atmo.chat.community.invite.create', { body: input }); 262 + } 263 + 264 + export async function listCommunityInvites( 265 + ctx: AuthedCallContext, 266 + query: { spaceUri: string; includeRevoked?: boolean } 267 + ): Promise<{ invites: InviteView[] }> { 268 + const q: Record<string, string> = { spaceUri: query.spaceUri }; 269 + if (query.includeRevoked) q.includeRevoked = 'true'; 270 + return callContrail(ctx, 'tools.atmo.chat.community.invite.list', { query: q }); 271 + } 272 + 273 + export async function revokeCommunityInvite( 274 + ctx: AuthedCallContext, 275 + input: { tokenHash: string } 276 + ): Promise<{ ok: true }> { 277 + return callContrail(ctx, 'tools.atmo.chat.community.invite.revoke', { body: input }); 278 + } 279 + 280 + export async function redeemCommunityInvite( 281 + ctx: AuthedCallContext, 282 + input: { token: string } 283 + ): Promise<{ spaceUri: string; accessLevel: string; communityDid: string }> { 284 + return callContrail(ctx, 'tools.atmo.chat.community.invite.redeem', { body: input }); 285 + } 286 + 287 + // --- blobs ----------------------------------------------------------------- 288 + 289 + export interface BlobRef { 290 + $type: 'blob'; 291 + ref: { $link: string }; 292 + mimeType: string; 293 + size: number; 294 + } 295 + 296 + /** Upload raw bytes to a space's blob store. Unlike the other helpers this one 297 + * uses a binary body (the raw bytes) rather than JSON, so we build the 298 + * Request ourselves. Returns the blob ref for use in a record field. */ 299 + export async function uploadBlob( 300 + ctx: AuthedCallContext, 301 + input: { spaceUri: string; mimeType: string; bytes: Uint8Array } 302 + ): Promise<BlobRef> { 303 + const method = 'tools.atmo.chat.space.uploadBlob'; 304 + const jwt = ctx.env.DEV_AUTH === '1' ? 'dev' : await mintServiceAuthForUpload(ctx, method); 305 + const url = new URL(`http://localhost/xrpc/${method}`); 306 + url.searchParams.set('spaceUri', input.spaceUri); 307 + const headers: Record<string, string> = { 308 + Authorization: `Bearer ${jwt}`, 309 + 'Content-Type': input.mimeType, 310 + 'Content-Length': String(input.bytes.byteLength) 311 + }; 312 + if (ctx.env.DEV_AUTH === '1') headers['X-Dev-Did'] = ctx.did; 313 + 314 + const req = new Request(url, { 315 + method: 'POST', 316 + headers, 317 + body: new Blob([new Uint8Array(input.bytes)], { type: input.mimeType }) 318 + }); 319 + const res = await dispatch(req, ctx.env); 320 + const text = await res.text(); 321 + const json = text ? JSON.parse(text) : {}; 322 + if (!res.ok) throw new Error(`${method} failed (${res.status}): ${text}`); 323 + return (json as { blob: BlobRef }).blob; 324 + } 325 + 326 + async function mintServiceAuthForUpload( 327 + ctx: AuthedCallContext, 328 + lxm: string, 329 + expSeconds = 60 330 + ): Promise<string> { 331 + const res = await ctx.client.get('com.atproto.server.getServiceAuth', { 332 + params: { 333 + aud: ctx.env.SERVICE_DID as `did:${string}:${string}`, 334 + lxm: lxm as `${string}.${string}.${string}`, 335 + exp: Math.floor(Date.now() / 1000) + expSeconds 336 + } 337 + }); 338 + if (!res.ok) throw new Error('getServiceAuth failed'); 339 + return (res.data as { token: string }).token; 340 + }
+72
examples/sveltekit-group-chat/src/lib/rooms/unread.svelte.ts
··· 1 + /** Per-space unread tracker. 2 + * - `counts[spaceUri]` is the number of record.created events received since 3 + * the space was last opened. 4 + * - `lastRead[spaceUri]` is the iso timestamp of the last "open" event, 5 + * persisted to localStorage so cold loads can compare against existing 6 + * messages' createdAt to derive initial dot state. 7 + * 8 + * Reset counts call on channel open via `markLastRead()` / `resetUnread()`. */ 9 + 10 + import { untrack } from 'svelte'; 11 + 12 + const LS_KEY = 'rooms:lastRead:v1'; 13 + 14 + function loadLastRead(): Record<string, string> { 15 + if (typeof localStorage === 'undefined') return {}; 16 + try { 17 + const raw = localStorage.getItem(LS_KEY); 18 + if (!raw) return {}; 19 + const parsed = JSON.parse(raw); 20 + return typeof parsed === 'object' && parsed ? parsed : {}; 21 + } catch { 22 + return {}; 23 + } 24 + } 25 + 26 + function persistLastRead(v: Record<string, string>) { 27 + if (typeof localStorage === 'undefined') return; 28 + try { 29 + localStorage.setItem(LS_KEY, JSON.stringify(v)); 30 + } catch { 31 + // ignore quota errors 32 + } 33 + } 34 + 35 + export const unread = $state<{ 36 + counts: Record<string, number>; 37 + lastRead: Record<string, string>; 38 + }>({ 39 + counts: {}, 40 + lastRead: loadLastRead() 41 + }); 42 + 43 + /** Called when a record.created event for a space arrives while the space is 44 + * not the active one. Increments the unread counter. 45 + * 46 + * Whole body is wrapped in `untrack` so that a caller running inside a 47 + * `$effect` does NOT subscribe to any of the `unread.*` state we read here. 48 + * Writes still notify real dependents — we just stop the current effect from 49 + * subscribing to state it's about to mutate and re-reading it afterwards. */ 50 + export function bumpUnread(spaceUri: string, createdAt?: string) { 51 + untrack(() => { 52 + const last = unread.lastRead[spaceUri]; 53 + if (createdAt && last && createdAt <= last) return; 54 + unread.counts[spaceUri] = (unread.counts[spaceUri] ?? 0) + 1; 55 + }); 56 + } 57 + 58 + /** Called when a channel view mounts or receives new messages while focused. */ 59 + export function markLastRead(spaceUri: string) { 60 + untrack(() => { 61 + const next = { ...unread.lastRead, [spaceUri]: new Date().toISOString() }; 62 + unread.counts[spaceUri] = 0; 63 + unread.lastRead = next; 64 + persistLastRead(next); 65 + }); 66 + } 67 + 68 + export function resetUnread(spaceUri: string) { 69 + untrack(() => { 70 + if (unread.counts[spaceUri]) unread.counts[spaceUri] = 0; 71 + }); 72 + }
+23
examples/sveltekit-group-chat/src/lib/rooms/uri.ts
··· 1 + /** Helpers for assembling space URIs in the `tools.atmo.chat` namespace. */ 2 + 3 + const SPACE_TYPE = 'tools.atmo.chat.space'; 4 + 5 + export function buildSpaceUri(communityDid: string, key: string): string { 6 + return `at://${communityDid}/${SPACE_TYPE}/${key}`; 7 + } 8 + 9 + export function buildMembersUri(communityDid: string): string { 10 + return buildSpaceUri(communityDid, 'members'); 11 + } 12 + 13 + export function buildAdminUri(communityDid: string): string { 14 + return buildSpaceUri(communityDid, '$admin'); 15 + } 16 + 17 + const SPACE_URI_RE = new RegExp(`^at://([^/]+)/${SPACE_TYPE.replace(/\./g, '\\.')}/([^/]+)$`); 18 + 19 + export function parseSpaceUri(uri: string): { communityDid: string; key: string } | null { 20 + const m = SPACE_URI_RE.exec(uri); 21 + if (!m) return null; 22 + return { communityDid: m[1]!, key: m[2]! }; 23 + }
+56
examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts
··· 1 + /** Svelte 5 binding over `@atmo-dev/contrail/sync`. Turns the framework-agnostic 2 + * subscribable store into a reactive `$state` object that Svelte effects can 3 + * observe in the usual way. 4 + * 5 + * Usage: 6 + * const query = createWatchQuery({ 7 + * url: `/xrpc/tools.atmo.chat.message.watchRecords?spaceUri=${encodeURIComponent(spaceUri)}`, 8 + * }); 9 + * // template: {#each query.records as r (r.uri)} ... 10 + * // teardown: $effect(() => () => query.stop()); 11 + */ 12 + 13 + import { createWatchStore, type WatchRecord, type WatchStoreOptions, type WatchStoreStatus } from '@atmo-dev/contrail/sync'; 14 + 15 + export interface WatchQuery { 16 + readonly records: ReadonlyArray<WatchRecord>; 17 + readonly status: WatchStoreStatus; 18 + readonly error: Error | null; 19 + stop(): void; 20 + } 21 + 22 + export function createWatchQuery(options: WatchStoreOptions): WatchQuery { 23 + const state = $state<{ 24 + records: ReadonlyArray<WatchRecord>; 25 + status: WatchStoreStatus; 26 + error: Error | null; 27 + }>({ 28 + records: [], 29 + status: 'idle', 30 + error: null 31 + }); 32 + 33 + const store = createWatchStore(options); 34 + const unsub = store.subscribe((s) => { 35 + state.records = s.records; 36 + state.status = s.status; 37 + state.error = s.error; 38 + }); 39 + store.start(); 40 + 41 + return { 42 + get records() { 43 + return state.records; 44 + }, 45 + get status() { 46 + return state.status; 47 + }, 48 + get error() { 49 + return state.error; 50 + }, 51 + stop() { 52 + unsub(); 53 + store.stop(); 54 + } 55 + }; 56 + }
+155
examples/sveltekit-group-chat/src/routes/new/+page.svelte
··· 1 + <script lang="ts"> 2 + import { goto } from '$app/navigation'; 3 + import { resolve } from '$app/paths'; 4 + import { Button, Heading, Input } from '@foxui/core'; 5 + import { user } from '$lib/atproto'; 6 + import { createCommunity } from '$lib/rooms/rooms.remote'; 7 + 8 + let name = $state(''); 9 + let description = $state(''); 10 + let iconFile = $state<File | null>(null); 11 + let iconPreview = $state<string | null>(null); 12 + let submitting = $state(false); 13 + let errorMsg = $state<string | null>(null); 14 + let result = $state<{ communityDid: string; recoveryKey: unknown } | null>(null); 15 + 16 + function onIconPicked(e: Event) { 17 + const input = e.currentTarget as HTMLInputElement; 18 + const file = input.files?.[0] ?? null; 19 + if (!file) { 20 + iconFile = null; 21 + iconPreview = null; 22 + return; 23 + } 24 + if (file.size > 1_000_000) { 25 + errorMsg = 'Icon must be under 1 MB.'; 26 + input.value = ''; 27 + return; 28 + } 29 + iconFile = file; 30 + iconPreview = URL.createObjectURL(file); 31 + } 32 + 33 + async function fileToBase64(file: File): Promise<string> { 34 + const buf = await file.arrayBuffer(); 35 + let binary = ''; 36 + const bytes = new Uint8Array(buf); 37 + for (const b of bytes) binary += String.fromCharCode(b); 38 + return btoa(binary); 39 + } 40 + 41 + async function submit(e: SubmitEvent) { 42 + e.preventDefault(); 43 + if (!name.trim()) return; 44 + submitting = true; 45 + errorMsg = null; 46 + try { 47 + const icon = iconFile 48 + ? { bytes: await fileToBase64(iconFile), mimeType: iconFile.type } 49 + : undefined; 50 + const res = await createCommunity({ 51 + name: name.trim(), 52 + description: description.trim() || undefined, 53 + icon 54 + }); 55 + result = { communityDid: res.communityDid, recoveryKey: res.recoveryKey }; 56 + } catch (err) { 57 + errorMsg = err instanceof Error ? err.message : String(err); 58 + } finally { 59 + submitting = false; 60 + } 61 + } 62 + 63 + function copyRecovery() { 64 + if (!result) return; 65 + void navigator.clipboard.writeText(JSON.stringify(result.recoveryKey, null, 2)); 66 + } 67 + </script> 68 + 69 + <div class="mx-auto my-16 max-w-lg px-4"> 70 + {#if !user.isLoggedIn} 71 + <p class="text-base-500">Please sign in first.</p> 72 + {:else if result} 73 + <Heading>Server created</Heading> 74 + <p class="text-base-500 mt-2 text-sm"> 75 + This is the one and only chance to save your recovery key. If lost, ownership of this server 76 + cannot be recovered. 77 + </p> 78 + 79 + <pre 80 + class="bg-base-100 dark:bg-base-900 my-4 overflow-x-auto rounded-2xl border p-4 text-xs">{JSON.stringify( 81 + result.recoveryKey, 82 + null, 83 + 2 84 + )}</pre> 85 + 86 + <div class="flex gap-2"> 87 + <Button onclick={copyRecovery}>Copy recovery key</Button> 88 + <Button 89 + variant="secondary" 90 + onclick={() => 91 + goto( 92 + resolve('/c/[communityDid]', { 93 + communityDid: result!.communityDid 94 + }) 95 + )} 96 + > 97 + I've saved it — continue 98 + </Button> 99 + </div> 100 + {:else} 101 + <Heading>New server</Heading> 102 + <form onsubmit={submit} class="mt-6 flex flex-col gap-4"> 103 + <label class="flex flex-col gap-1 text-sm"> 104 + Name 105 + <Input bind:value={name} placeholder="Contrail rooms" maxlength={128} required /> 106 + </label> 107 + <label class="flex flex-col gap-1 text-sm"> 108 + Description (optional) 109 + <Input bind:value={description} placeholder="a place for things" maxlength={2000} /> 110 + </label> 111 + <div class="flex items-center gap-3"> 112 + <label 113 + class="bg-base-200 dark:bg-base-800 flex size-16 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-full border" 114 + > 115 + {#if iconPreview} 116 + <img src={iconPreview} alt="icon" class="h-full w-full object-cover" /> 117 + {:else} 118 + <span class="text-base-500 text-xs">icon</span> 119 + {/if} 120 + <input 121 + type="file" 122 + accept="image/png,image/jpeg,image/webp,image/gif" 123 + class="hidden" 124 + onchange={onIconPicked} 125 + /> 126 + </label> 127 + <div class="flex flex-col gap-1 text-xs"> 128 + <span class="text-base-500">Server icon (optional)</span> 129 + <span class="text-base-400">PNG, JPG, WEBP, or GIF. Up to 1 MB.</span> 130 + {#if iconFile} 131 + <button 132 + type="button" 133 + class="text-red-500 hover:underline" 134 + onclick={() => { 135 + iconFile = null; 136 + iconPreview = null; 137 + }} 138 + > 139 + Remove 140 + </button> 141 + {/if} 142 + </div> 143 + </div> 144 + {#if errorMsg} 145 + <div class="rounded-xl border border-red-500 p-2 text-sm text-red-500">{errorMsg}</div> 146 + {/if} 147 + <div class="mt-2 flex gap-2"> 148 + <Button type="submit" disabled={submitting || !name.trim()}> 149 + {submitting ? 'Creating…' : 'Create server'} 150 + </Button> 151 + <Button type="button" variant="ghost" onclick={() => goto(resolve('/'))}>Cancel</Button> 152 + </div> 153 + </form> 154 + {/if} 155 + </div>
-170
examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/getRecord.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "statusphere.app.status.getRecord", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Get a single xyz.statusphere.status record by AT URI", 8 - "parameters": { 9 - "type": "params", 10 - "required": [ 11 - "uri" 12 - ], 13 - "properties": { 14 - "uri": { 15 - "type": "string", 16 - "format": "at-uri", 17 - "description": "AT URI of the record" 18 - }, 19 - "profiles": { 20 - "type": "boolean", 21 - "description": "Include profile + identity info keyed by DID" 22 - } 23 - } 24 - }, 25 - "output": { 26 - "encoding": "application/json", 27 - "schema": { 28 - "type": "object", 29 - "required": [ 30 - "uri", 31 - "did", 32 - "collection", 33 - "rkey", 34 - "time_us" 35 - ], 36 - "properties": { 37 - "uri": { 38 - "type": "string", 39 - "format": "at-uri" 40 - }, 41 - "did": { 42 - "type": "string", 43 - "format": "did" 44 - }, 45 - "collection": { 46 - "type": "string", 47 - "format": "nsid" 48 - }, 49 - "rkey": { 50 - "type": "string" 51 - }, 52 - "cid": { 53 - "type": "string" 54 - }, 55 - "record": { 56 - "type": "ref", 57 - "ref": "xyz.statusphere.status#main" 58 - }, 59 - "time_us": { 60 - "type": "integer" 61 - }, 62 - "profiles": { 63 - "type": "array", 64 - "items": { 65 - "type": "ref", 66 - "ref": "#profileEntry" 67 - } 68 - } 69 - } 70 - } 71 - } 72 - }, 73 - "profileEntry": { 74 - "type": "object", 75 - "required": [ 76 - "did" 77 - ], 78 - "properties": { 79 - "did": { 80 - "type": "string", 81 - "format": "did" 82 - }, 83 - "handle": { 84 - "type": "string" 85 - }, 86 - "uri": { 87 - "type": "string", 88 - "format": "at-uri" 89 - }, 90 - "collection": { 91 - "type": "string", 92 - "format": "nsid" 93 - }, 94 - "rkey": { 95 - "type": "string" 96 - }, 97 - "cid": { 98 - "type": "string" 99 - }, 100 - "record": { 101 - "type": "ref", 102 - "ref": "#appBskyActorProfile" 103 - } 104 - } 105 - }, 106 - "appBskyActorProfile": { 107 - "type": "object", 108 - "properties": { 109 - "avatar": { 110 - "type": "blob", 111 - "accept": [ 112 - "image/png", 113 - "image/jpeg" 114 - ], 115 - "maxSize": 1000000, 116 - "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 117 - }, 118 - "banner": { 119 - "type": "blob", 120 - "accept": [ 121 - "image/png", 122 - "image/jpeg" 123 - ], 124 - "maxSize": 1000000, 125 - "description": "Larger horizontal image to display behind profile view." 126 - }, 127 - "labels": { 128 - "refs": [ 129 - "com.atproto.label.defs#selfLabels" 130 - ], 131 - "type": "union", 132 - "description": "Self-label values, specific to the Bluesky application, on the overall account." 133 - }, 134 - "website": { 135 - "type": "string", 136 - "format": "uri" 137 - }, 138 - "pronouns": { 139 - "type": "string", 140 - "maxLength": 200, 141 - "description": "Free-form pronouns text.", 142 - "maxGraphemes": 20 143 - }, 144 - "createdAt": { 145 - "type": "string", 146 - "format": "datetime" 147 - }, 148 - "pinnedPost": { 149 - "ref": "com.atproto.repo.strongRef", 150 - "type": "ref" 151 - }, 152 - "description": { 153 - "type": "string", 154 - "maxLength": 2560, 155 - "description": "Free-form profile description text.", 156 - "maxGraphemes": 256 157 - }, 158 - "displayName": { 159 - "type": "string", 160 - "maxLength": 640, 161 - "maxGraphemes": 64 162 - }, 163 - "joinedViaStarterPack": { 164 - "ref": "com.atproto.repo.strongRef", 165 - "type": "ref" 166 - } 167 - } 168 - } 169 - } 170 - }
-222
examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/listRecords.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "statusphere.app.status.listRecords", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Query xyz.statusphere.status records with filters", 8 - "parameters": { 9 - "type": "params", 10 - "properties": { 11 - "limit": { 12 - "type": "integer", 13 - "minimum": 1, 14 - "maximum": 200, 15 - "default": 50 16 - }, 17 - "cursor": { 18 - "type": "string" 19 - }, 20 - "actor": { 21 - "type": "string", 22 - "format": "at-identifier", 23 - "description": "Filter by DID or handle (triggers on-demand backfill)" 24 - }, 25 - "profiles": { 26 - "type": "boolean", 27 - "description": "Include profile + identity info keyed by DID" 28 - }, 29 - "status": { 30 - "type": "string", 31 - "description": "Filter by status" 32 - }, 33 - "createdAtMin": { 34 - "type": "string", 35 - "description": "Minimum value for createdAt" 36 - }, 37 - "createdAtMax": { 38 - "type": "string", 39 - "description": "Maximum value for createdAt" 40 - }, 41 - "sort": { 42 - "type": "string", 43 - "knownValues": [ 44 - "status", 45 - "createdAt" 46 - ], 47 - "description": "Field to sort by (default: time_us)" 48 - }, 49 - "order": { 50 - "type": "string", 51 - "knownValues": [ 52 - "asc", 53 - "desc" 54 - ], 55 - "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" 56 - } 57 - } 58 - }, 59 - "output": { 60 - "encoding": "application/json", 61 - "schema": { 62 - "type": "object", 63 - "required": [ 64 - "records" 65 - ], 66 - "properties": { 67 - "records": { 68 - "type": "array", 69 - "items": { 70 - "type": "ref", 71 - "ref": "#record" 72 - } 73 - }, 74 - "cursor": { 75 - "type": "string" 76 - }, 77 - "profiles": { 78 - "type": "array", 79 - "items": { 80 - "type": "ref", 81 - "ref": "#profileEntry" 82 - } 83 - } 84 - } 85 - } 86 - } 87 - }, 88 - "record": { 89 - "type": "object", 90 - "required": [ 91 - "uri", 92 - "did", 93 - "collection", 94 - "rkey", 95 - "time_us" 96 - ], 97 - "properties": { 98 - "uri": { 99 - "type": "string", 100 - "format": "at-uri" 101 - }, 102 - "did": { 103 - "type": "string", 104 - "format": "did" 105 - }, 106 - "collection": { 107 - "type": "string", 108 - "format": "nsid" 109 - }, 110 - "rkey": { 111 - "type": "string" 112 - }, 113 - "cid": { 114 - "type": "string" 115 - }, 116 - "record": { 117 - "type": "ref", 118 - "ref": "xyz.statusphere.status#main" 119 - }, 120 - "time_us": { 121 - "type": "integer" 122 - } 123 - } 124 - }, 125 - "profileEntry": { 126 - "type": "object", 127 - "required": [ 128 - "did" 129 - ], 130 - "properties": { 131 - "did": { 132 - "type": "string", 133 - "format": "did" 134 - }, 135 - "handle": { 136 - "type": "string" 137 - }, 138 - "uri": { 139 - "type": "string", 140 - "format": "at-uri" 141 - }, 142 - "collection": { 143 - "type": "string", 144 - "format": "nsid" 145 - }, 146 - "rkey": { 147 - "type": "string" 148 - }, 149 - "cid": { 150 - "type": "string" 151 - }, 152 - "record": { 153 - "type": "ref", 154 - "ref": "#appBskyActorProfile" 155 - } 156 - } 157 - }, 158 - "appBskyActorProfile": { 159 - "type": "object", 160 - "properties": { 161 - "avatar": { 162 - "type": "blob", 163 - "accept": [ 164 - "image/png", 165 - "image/jpeg" 166 - ], 167 - "maxSize": 1000000, 168 - "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 169 - }, 170 - "banner": { 171 - "type": "blob", 172 - "accept": [ 173 - "image/png", 174 - "image/jpeg" 175 - ], 176 - "maxSize": 1000000, 177 - "description": "Larger horizontal image to display behind profile view." 178 - }, 179 - "labels": { 180 - "refs": [ 181 - "com.atproto.label.defs#selfLabels" 182 - ], 183 - "type": "union", 184 - "description": "Self-label values, specific to the Bluesky application, on the overall account." 185 - }, 186 - "website": { 187 - "type": "string", 188 - "format": "uri" 189 - }, 190 - "pronouns": { 191 - "type": "string", 192 - "maxLength": 200, 193 - "description": "Free-form pronouns text.", 194 - "maxGraphemes": 20 195 - }, 196 - "createdAt": { 197 - "type": "string", 198 - "format": "datetime" 199 - }, 200 - "pinnedPost": { 201 - "ref": "com.atproto.repo.strongRef", 202 - "type": "ref" 203 - }, 204 - "description": { 205 - "type": "string", 206 - "maxLength": 2560, 207 - "description": "Free-form profile description text.", 208 - "maxGraphemes": 256 209 - }, 210 - "displayName": { 211 - "type": "string", 212 - "maxLength": 640, 213 - "maxGraphemes": 64 214 - }, 215 - "joinedViaStarterPack": { 216 - "ref": "com.atproto.repo.strongRef", 217 - "type": "ref" 218 - } 219 - } 220 - } 221 - } 222 - }
+27
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getCursor.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.getCursor", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get the current cursor position", 8 + "output": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "properties": { 13 + "time_us": { 14 + "type": "integer" 15 + }, 16 + "date": { 17 + "type": "string" 18 + }, 19 + "seconds_ago": { 20 + "type": "integer" 21 + } 22 + } 23 + } 24 + } 25 + } 26 + } 27 + }
+51
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getOverview.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.getOverview", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get an overview of all indexed collections", 8 + "output": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "total_records", 14 + "collections" 15 + ], 16 + "properties": { 17 + "total_records": { 18 + "type": "integer" 19 + }, 20 + "collections": { 21 + "type": "array", 22 + "items": { 23 + "type": "ref", 24 + "ref": "#collectionStats" 25 + } 26 + } 27 + } 28 + } 29 + } 30 + }, 31 + "collectionStats": { 32 + "type": "object", 33 + "required": [ 34 + "collection", 35 + "records", 36 + "unique_users" 37 + ], 38 + "properties": { 39 + "collection": { 40 + "type": "string" 41 + }, 42 + "records": { 43 + "type": "integer" 44 + }, 45 + "unique_users": { 46 + "type": "integer" 47 + } 48 + } 49 + } 50 + } 51 + }
+137
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getProfile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.getProfile", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a user's profiles by DID or handle", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "actor" 12 + ], 13 + "properties": { 14 + "actor": { 15 + "type": "string", 16 + "format": "at-identifier", 17 + "description": "DID or handle of the user" 18 + } 19 + } 20 + }, 21 + "output": { 22 + "encoding": "application/json", 23 + "schema": { 24 + "type": "object", 25 + "required": [ 26 + "profiles" 27 + ], 28 + "properties": { 29 + "profiles": { 30 + "type": "array", 31 + "items": { 32 + "type": "ref", 33 + "ref": "#profileEntry" 34 + } 35 + } 36 + } 37 + } 38 + } 39 + }, 40 + "profileEntry": { 41 + "type": "object", 42 + "required": [ 43 + "did" 44 + ], 45 + "properties": { 46 + "did": { 47 + "type": "string", 48 + "format": "did" 49 + }, 50 + "handle": { 51 + "type": "string" 52 + }, 53 + "uri": { 54 + "type": "string", 55 + "format": "at-uri" 56 + }, 57 + "collection": { 58 + "type": "string", 59 + "format": "nsid" 60 + }, 61 + "rkey": { 62 + "type": "string" 63 + }, 64 + "cid": { 65 + "type": "string" 66 + }, 67 + "record": { 68 + "type": "ref", 69 + "ref": "#appBskyActorProfile" 70 + } 71 + } 72 + }, 73 + "appBskyActorProfile": { 74 + "type": "object", 75 + "properties": { 76 + "avatar": { 77 + "type": "blob", 78 + "accept": [ 79 + "image/png", 80 + "image/jpeg" 81 + ], 82 + "maxSize": 1000000, 83 + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 84 + }, 85 + "banner": { 86 + "type": "blob", 87 + "accept": [ 88 + "image/png", 89 + "image/jpeg" 90 + ], 91 + "maxSize": 1000000, 92 + "description": "Larger horizontal image to display behind profile view." 93 + }, 94 + "labels": { 95 + "refs": [ 96 + "com.atproto.label.defs#selfLabels" 97 + ], 98 + "type": "union", 99 + "description": "Self-label values, specific to the Bluesky application, on the overall account." 100 + }, 101 + "website": { 102 + "type": "string", 103 + "format": "uri" 104 + }, 105 + "pronouns": { 106 + "type": "string", 107 + "maxLength": 200, 108 + "description": "Free-form pronouns text.", 109 + "maxGraphemes": 20 110 + }, 111 + "createdAt": { 112 + "type": "string", 113 + "format": "datetime" 114 + }, 115 + "pinnedPost": { 116 + "ref": "com.atproto.repo.strongRef", 117 + "type": "ref" 118 + }, 119 + "description": { 120 + "type": "string", 121 + "maxLength": 2560, 122 + "description": "Free-form profile description text.", 123 + "maxGraphemes": 256 124 + }, 125 + "displayName": { 126 + "type": "string", 127 + "maxLength": 640, 128 + "maxGraphemes": 64 129 + }, 130 + "joinedViaStarterPack": { 131 + "ref": "com.atproto.repo.strongRef", 132 + "type": "ref" 133 + } 134 + } 135 + } 136 + } 137 + }
+59
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/notifyOfUpdate.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.notifyOfUpdate", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Notify of a record change for immediate indexing. Fetches the record from the user's PDS and indexes (or deletes) it.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "properties": { 13 + "uri": { 14 + "type": "string", 15 + "format": "at-uri", 16 + "description": "Single AT URI to fetch and index" 17 + }, 18 + "uris": { 19 + "type": "array", 20 + "items": { 21 + "type": "string", 22 + "format": "at-uri" 23 + }, 24 + "maxLength": 25, 25 + "description": "Batch of AT URIs to fetch and index (max 25)" 26 + } 27 + } 28 + } 29 + }, 30 + "output": { 31 + "encoding": "application/json", 32 + "schema": { 33 + "type": "object", 34 + "required": [ 35 + "indexed", 36 + "deleted" 37 + ], 38 + "properties": { 39 + "indexed": { 40 + "type": "integer", 41 + "description": "Number of records created or updated" 42 + }, 43 + "deleted": { 44 + "type": "integer", 45 + "description": "Number of records deleted (not found on PDS)" 46 + }, 47 + "errors": { 48 + "type": "array", 49 + "items": { 50 + "type": "string" 51 + }, 52 + "description": "Errors for individual URIs that could not be processed" 53 + } 54 + } 55 + } 56 + } 57 + } 58 + } 59 + }
+73
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.permissionSet", 4 + "defs": { 5 + "main": { 6 + "type": "permission-set", 7 + "title": "tools.atmo.chat", 8 + "description": "All XRPC methods exposed by the tools.atmo.chat service.", 9 + "permissions": [ 10 + { 11 + "type": "permission", 12 + "resource": "rpc", 13 + "aud": "*", 14 + "lxm": [ 15 + "tools.atmo.chat.channel.getRecord", 16 + "tools.atmo.chat.channel.listRecords", 17 + "tools.atmo.chat.community.adopt", 18 + "tools.atmo.chat.community.delete", 19 + "tools.atmo.chat.community.deleteRecord", 20 + "tools.atmo.chat.community.getHealth", 21 + "tools.atmo.chat.community.invite.create", 22 + "tools.atmo.chat.community.invite.list", 23 + "tools.atmo.chat.community.invite.redeem", 24 + "tools.atmo.chat.community.invite.revoke", 25 + "tools.atmo.chat.community.list", 26 + "tools.atmo.chat.community.mint", 27 + "tools.atmo.chat.community.putRecord", 28 + "tools.atmo.chat.community.reauth", 29 + "tools.atmo.chat.community.space.create", 30 + "tools.atmo.chat.community.space.delete", 31 + "tools.atmo.chat.community.space.deleteRecord", 32 + "tools.atmo.chat.community.space.grant", 33 + "tools.atmo.chat.community.space.listMembers", 34 + "tools.atmo.chat.community.space.putRecord", 35 + "tools.atmo.chat.community.space.resync", 36 + "tools.atmo.chat.community.space.revoke", 37 + "tools.atmo.chat.community.space.setAccessLevel", 38 + "tools.atmo.chat.community.whoami", 39 + "tools.atmo.chat.getCursor", 40 + "tools.atmo.chat.getOverview", 41 + "tools.atmo.chat.getProfile", 42 + "tools.atmo.chat.message.getRecord", 43 + "tools.atmo.chat.message.listRecords", 44 + "tools.atmo.chat.notifyOfUpdate", 45 + "tools.atmo.chat.realtime.subscribe", 46 + "tools.atmo.chat.realtime.ticket", 47 + "tools.atmo.chat.server.getRecord", 48 + "tools.atmo.chat.server.listRecords", 49 + "tools.atmo.chat.space.addMember", 50 + "tools.atmo.chat.space.createSpace", 51 + "tools.atmo.chat.space.deleteRecord", 52 + "tools.atmo.chat.space.getBlob", 53 + "tools.atmo.chat.space.getRecord", 54 + "tools.atmo.chat.space.getSpace", 55 + "tools.atmo.chat.space.invite.create", 56 + "tools.atmo.chat.space.invite.list", 57 + "tools.atmo.chat.space.invite.redeem", 58 + "tools.atmo.chat.space.invite.revoke", 59 + "tools.atmo.chat.space.leaveSpace", 60 + "tools.atmo.chat.space.listBlobs", 61 + "tools.atmo.chat.space.listMembers", 62 + "tools.atmo.chat.space.listRecords", 63 + "tools.atmo.chat.space.listSpaces", 64 + "tools.atmo.chat.space.putRecord", 65 + "tools.atmo.chat.space.removeMember", 66 + "tools.atmo.chat.space.uploadBlob", 67 + "tools.atmo.chat.space.whoami" 68 + ] 69 + } 70 + ] 71 + } 72 + } 73 + }
+24
examples/sveltekit-group-chat/lexicons/tools/atmo/chat/channel.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.channel", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "literal:self", 8 + "record": { 9 + "type": "object", 10 + "required": ["communityDid", "name", "visibility", "createdAt"], 11 + "properties": { 12 + "communityDid": { "type": "string", "format": "did" }, 13 + "name": { "type": "string", "maxLength": 128 }, 14 + "topic": { "type": "string", "maxLength": 512 }, 15 + "visibility": { 16 + "type": "string", 17 + "knownValues": ["public", "private"] 18 + }, 19 + "createdAt": { "type": "string", "format": "datetime" } 20 + } 21 + } 22 + } 23 + } 24 + }
+19
examples/sveltekit-group-chat/lexicons/tools/atmo/chat/message.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.message", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "tid", 8 + "record": { 9 + "type": "object", 10 + "required": ["text", "createdAt"], 11 + "properties": { 12 + "text": { "type": "string", "maxLength": 4000 }, 13 + "createdAt": { "type": "string", "format": "datetime" }, 14 + "replyTo": { "type": "string", "description": "rkey of the parent message in the same channel" } 15 + } 16 + } 17 + } 18 + } 19 + }
+25
examples/sveltekit-group-chat/lexicons/tools/atmo/chat/server.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.server", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "literal:self", 8 + "record": { 9 + "type": "object", 10 + "required": ["communityDid", "name", "createdAt"], 11 + "properties": { 12 + "communityDid": { "type": "string", "format": "did" }, 13 + "name": { "type": "string", "maxLength": 128 }, 14 + "description": { "type": "string", "maxLength": 2000 }, 15 + "icon": { 16 + "type": "blob", 17 + "accept": ["image/png", "image/jpeg", "image/webp", "image/gif"], 18 + "maxSize": 1000000 19 + }, 20 + "createdAt": { "type": "string", "format": "datetime" } 21 + } 22 + } 23 + } 24 + } 25 + }
+4 -5
examples/sveltekit-group-chat/src/lib/atproto/server/repo.remote.ts
··· 2 2 import { command, getRequestEvent } from '$app/server'; 3 3 import * as v from 'valibot'; 4 4 import { collections } from '../settings'; 5 - import { contrail, ensureInit } from '$lib/contrail'; 5 + import { getContrail } from '$lib/contrail'; 6 6 7 7 // Validate collection format and check against allowed list from settings 8 8 const collectionSchema = v.pipe( ··· 35 35 36 36 // Immediately index the new/updated record in contrail 37 37 const { platform } = getRequestEvent(); 38 - const db = platform?.env?.DB; 39 - if (db) { 40 - await ensureInit(db); 41 - await contrail.notify(response.data.uri, db).catch(() => {}); 38 + if (platform?.env) { 39 + const contrail = await getContrail(platform.env); 40 + await contrail.notify(response.data.uri, platform.env.DB).catch(() => {}); 42 41 } 43 42 44 43 return response.data;
+37
examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts
··· 67 67 const signed = `${value}${SEPARATOR}${sig}`; 68 68 cookies.set(name, signed, options); 69 69 } 70 + 71 + /** Verify + read a signed cookie from a raw Request's Cookie header. 72 + * Used from places that don't have a SvelteKit `Cookies` instance 73 + * (e.g. contrail auth override). */ 74 + export function getSignedCookieFromRequest(req: Request, name: string): string | null { 75 + const header = req.headers.get('cookie'); 76 + if (!header) return null; 77 + const pairs = header.split(/;\s*/); 78 + let raw: string | null = null; 79 + for (const pair of pairs) { 80 + const eq = pair.indexOf('='); 81 + if (eq === -1) continue; 82 + const key = pair.slice(0, eq); 83 + if (key === name) { 84 + raw = decodeURIComponent(pair.slice(eq + 1)); 85 + break; 86 + } 87 + } 88 + if (!raw) return null; 89 + 90 + const idx = raw.lastIndexOf(SEPARATOR); 91 + if (idx === -1) return null; 92 + const value = raw.slice(0, idx); 93 + const sig = raw.slice(idx + 1); 94 + 95 + let expected: Uint8Array; 96 + let got: Uint8Array; 97 + try { 98 + expected = hmacSha256(value); 99 + got = fromBase64Url(sig); 100 + } catch { 101 + return null; 102 + } 103 + 104 + if (got.length !== expected.length || !timingSafeEqual(got, expected)) return null; 105 + return value; 106 + }
+23
examples/sveltekit-group-chat/src/routes/api/blob-upload/+server.ts
··· 1 + import { error, json } from '@sveltejs/kit'; 2 + import type { RequestHandler } from './$types'; 3 + import type { Client } from '@atcute/client'; 4 + import { uploadBlob } from '$lib/rooms/server'; 5 + 6 + /** POST raw bytes (body = bytes) with ?spaceUri=<uri>&mimeType=<type>. 7 + * Returns the atproto blob ref. 8 + * Plain +server.ts so we can pass binary without remote-function JSON schema, 9 + * and so there's no auto-revalidation. */ 10 + export const POST: RequestHandler = async ({ request, url, locals, platform }) => { 11 + if (!locals.did || !locals.client) error(401, 'Not authenticated'); 12 + const spaceUri = url.searchParams.get('spaceUri'); 13 + const mimeType = url.searchParams.get('mimeType') ?? request.headers.get('content-type'); 14 + if (!spaceUri) error(400, 'spaceUri required'); 15 + if (!mimeType) error(400, 'mimeType required'); 16 + 17 + const bytes = new Uint8Array(await request.arrayBuffer()); 18 + const blob = await uploadBlob( 19 + { env: platform!.env, client: locals.client as Client, did: locals.did as string }, 20 + { spaceUri, mimeType, bytes } 21 + ); 22 + return json(blob); 23 + };
+46
examples/sveltekit-group-chat/src/routes/api/blob/+server.ts
··· 1 + import { error } from '@sveltejs/kit'; 2 + import type { RequestHandler } from './$types'; 3 + import { dispatch } from '$lib/contrail'; 4 + import { getSignedCookie } from '$lib/atproto/server/signed-cookie'; 5 + 6 + /** GET ?spaceUri=<uri>&cid=<cid> → blob bytes. 7 + * Proxies through contrail's space.getBlob endpoint with the caller's 8 + * (cookie-based in dev / JWT in prod) auth attached. */ 9 + export const GET: RequestHandler = async ({ url, locals, platform, request, cookies }) => { 10 + const spaceUri = url.searchParams.get('spaceUri'); 11 + const cid = url.searchParams.get('cid'); 12 + if (!spaceUri || !cid) error(400, 'spaceUri and cid required'); 13 + 14 + const target = new URL('http://localhost/xrpc/tools.atmo.chat.space.getBlob'); 15 + target.searchParams.set('spaceUri', spaceUri); 16 + target.searchParams.set('cid', cid); 17 + 18 + // Forward the session cookie so dev auth override works; in prod the 19 + // caller's OAuth session can mint a real JWT — but for <img src> we can't 20 + // attach headers from the browser, so we mint server-side. 21 + const headers: Record<string, string> = {}; 22 + if (platform!.env.DEV_AUTH === '1') { 23 + const did = getSignedCookie(cookies, 'did'); 24 + if (did) headers['X-Dev-Did'] = did; 25 + } else { 26 + if (!locals.client || !locals.did) error(401, 'Not authenticated'); 27 + const authRes = await locals.client.get('com.atproto.server.getServiceAuth', { 28 + params: { 29 + aud: platform!.env.SERVICE_DID as `did:${string}:${string}`, 30 + lxm: 'tools.atmo.chat.space.getBlob' as `${string}.${string}.${string}`, 31 + exp: Math.floor(Date.now() / 1000) + 120 32 + } 33 + }); 34 + if (authRes.ok) headers.Authorization = `Bearer ${(authRes.data as { token: string }).token}`; 35 + } 36 + 37 + const proxied = await dispatch(new Request(target, { headers }), platform!.env); 38 + // Pass through status + body + content-type. 39 + return new Response(proxied.body, { 40 + status: proxied.status, 41 + headers: { 42 + 'content-type': proxied.headers.get('content-type') ?? 'application/octet-stream', 43 + 'cache-control': 'private, max-age=3600' 44 + } 45 + }); 46 + };
+3 -4
examples/sveltekit-group-chat/src/routes/api/cron/+server.ts
··· 1 - import { contrail, ensureInit } from '$lib/contrail'; 1 + import { getContrail } from '$lib/contrail'; 2 2 import type { RequestHandler } from './$types'; 3 3 4 4 export const POST: RequestHandler = async ({ request, platform }) => { ··· 7 7 return new Response('Unauthorized', { status: 401 }); 8 8 } 9 9 10 - const db = platform!.env.DB; 11 - await ensureInit(db); 12 - await contrail.ingest({}, db); 10 + const contrail = await getContrail(platform!.env); 11 + await contrail.ingest({}, platform!.env.DB); 13 12 14 13 return new Response('OK'); 15 14 };
+24
examples/sveltekit-group-chat/src/routes/api/ticket/+server.ts
··· 1 + import { error, json } from '@sveltejs/kit'; 2 + import type { RequestHandler } from './$types'; 3 + import type { Client } from '@atcute/client'; 4 + import { getRealtimeTicket } from '$lib/rooms/server'; 5 + 6 + /** POST { topic } → realtime ticket. 7 + * Kept as a plain +server.ts endpoint rather than a remote command so that 8 + * calling it doesn't trigger SvelteKit's auto-revalidation (which would 9 + * cycle with the layout effect that opens the EventSource). */ 10 + export const POST: RequestHandler = async ({ request, locals, platform }) => { 11 + if (!locals.did || !locals.client) error(401, 'Not authenticated'); 12 + const body = (await request.json().catch(() => null)) as { topic?: string } | null; 13 + if (!body?.topic) error(400, 'topic required'); 14 + 15 + const res = await getRealtimeTicket( 16 + { 17 + env: platform!.env, 18 + client: locals.client as Client, 19 + did: locals.did as string 20 + }, 21 + { topic: body.topic } 22 + ); 23 + return json(res); 24 + };
+130
examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts
··· 1 + import type { LayoutServerLoad } from './$types'; 2 + import { redirect } from '@sveltejs/kit'; 3 + import type { Client } from '@atcute/client'; 4 + import { authedFetch } from '$lib/rooms/server'; 5 + import { parseSpaceUri, buildAdminUri } from '$lib/rooms/uri'; 6 + 7 + interface ServerMeta { 8 + communityDid: string; 9 + name: string; 10 + description?: string; 11 + iconUrl?: string; 12 + membersUri: string; 13 + } 14 + 15 + interface ChannelMeta { 16 + spaceUri: string; 17 + key: string; 18 + name: string; 19 + topic?: string; 20 + visibility: 'public' | 'private'; 21 + createdAt: string; 22 + } 23 + 24 + export const load: LayoutServerLoad = async ({ locals, params, platform }) => { 25 + if (!locals.did || !locals.client) { 26 + throw redirect(302, '/'); 27 + } 28 + const communityDid = decodeURIComponent(params.communityDid); 29 + const ctx = { 30 + env: platform!.env, 31 + client: locals.client as Client, 32 + did: locals.did as string 33 + }; 34 + 35 + // --- fetch server record ------------------------------------------------- 36 + let server: ServerMeta | null = null; 37 + try { 38 + const data = await authedFetch<{ 39 + records: Array<{ 40 + did: string; 41 + rkey: string; 42 + record: { 43 + communityDid?: string; 44 + name?: string; 45 + description?: string; 46 + icon?: { ref?: { $link?: string } }; 47 + }; 48 + space?: string; 49 + }>; 50 + }>(ctx, 'tools.atmo.chat.server.listRecords', { 51 + query: { actor: communityDid, limit: '1' } 52 + }); 53 + const row = data.records.find( 54 + (r) => 55 + r.space && 56 + r.did === communityDid && 57 + r.rkey === 'self' && 58 + r.record?.communityDid === communityDid 59 + ); 60 + if (row?.record?.name) { 61 + const iconCid = row.record.icon?.ref?.$link; 62 + server = { 63 + communityDid, 64 + name: row.record.name, 65 + description: row.record.description, 66 + iconUrl: iconCid 67 + ? `/api/blob?spaceUri=${encodeURIComponent(row.space!)}&cid=${encodeURIComponent(iconCid)}` 68 + : undefined, 69 + membersUri: row.space! 70 + }; 71 + } 72 + } catch { 73 + // fallthrough — server stays null, UI shows "server" fallback 74 + } 75 + 76 + // --- fetch channels ------------------------------------------------------ 77 + const channels: ChannelMeta[] = []; 78 + try { 79 + const data = await authedFetch<{ 80 + records: Array<{ 81 + did: string; 82 + rkey: string; 83 + record: { 84 + communityDid?: string; 85 + name?: string; 86 + topic?: string; 87 + visibility?: 'public' | 'private'; 88 + createdAt?: string; 89 + }; 90 + space?: string; 91 + }>; 92 + }>(ctx, 'tools.atmo.chat.channel.listRecords', { 93 + query: { actor: communityDid, limit: '100' } 94 + }); 95 + for (const r of data.records) { 96 + if (!r.space || r.did !== communityDid || r.rkey !== 'self') continue; 97 + if (r.record?.communityDid !== communityDid) continue; 98 + if (!r.record.name || !r.record.visibility || !r.record.createdAt) continue; 99 + const parsed = parseSpaceUri(r.space); 100 + if (!parsed || parsed.key.startsWith('$') || parsed.key === 'members') continue; 101 + channels.push({ 102 + spaceUri: r.space, 103 + key: parsed.key, 104 + name: r.record.name, 105 + topic: r.record.topic, 106 + visibility: r.record.visibility, 107 + createdAt: r.record.createdAt 108 + }); 109 + } 110 + channels.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1)); 111 + } catch { 112 + // fallthrough 113 + } 114 + 115 + // --- caller's access level on $admin ----------------------------------- 116 + let isAdmin = false; 117 + try { 118 + const adminUri = buildAdminUri(communityDid); 119 + const d = await authedFetch<{ accessLevel: string | null }>( 120 + ctx, 121 + 'tools.atmo.chat.community.whoami', 122 + { query: { spaceUri: adminUri } } 123 + ); 124 + isAdmin = d.accessLevel === 'admin' || d.accessLevel === 'owner'; 125 + } catch { 126 + // stay false 127 + } 128 + 129 + return { communityDid, server, channels, isAdmin, myDid: locals.did }; 130 + };
+269
examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/state'; 3 + import { goto, invalidateAll } from '$app/navigation'; 4 + import { resolve } from '$app/paths'; 5 + import { Button, Input, Modal, Heading, Sidebar, Navbar } from '@foxui/core'; 6 + import { user } from '$lib/atproto'; 7 + import { createChannel } from '$lib/rooms/rooms.remote'; 8 + import { unread, resetUnread } from '$lib/rooms/unread.svelte'; 9 + import { connectCommunityRealtime } from '$lib/rooms/realtime.svelte'; 10 + 11 + let { data, children } = $props(); 12 + 13 + let newChannelOpen = $state(false); 14 + let newChannelName = $state(''); 15 + let newChannelTopic = $state(''); 16 + let newChannelPrivate = $state(false); 17 + let privateDids = $state(''); 18 + let creating = $state(false); 19 + let createError = $state<string | null>(null); 20 + 21 + let currentChannelKey = $derived(page.params.channelKey ?? null); 22 + let currentChannel = $derived( 23 + currentChannelKey ? (data.channels.find((c) => c.key === currentChannelKey) ?? null) : null 24 + ); 25 + 26 + async function submitNewChannel(e: SubmitEvent) { 27 + e.preventDefault(); 28 + if (!newChannelName.trim()) return; 29 + creating = true; 30 + createError = null; 31 + try { 32 + const dids = privateDids 33 + .split(/[\s,]+/) 34 + .map((s) => s.trim()) 35 + .filter(Boolean); 36 + const res = await createChannel({ 37 + communityDid: data.communityDid, 38 + name: newChannelName.trim(), 39 + topic: newChannelTopic.trim() || undefined, 40 + visibility: newChannelPrivate ? 'private' : 'public', 41 + memberDids: newChannelPrivate ? dids : undefined 42 + }); 43 + newChannelOpen = false; 44 + newChannelName = ''; 45 + newChannelTopic = ''; 46 + privateDids = ''; 47 + newChannelPrivate = false; 48 + // Same-layout navigation wouldn't rerun the loader that populates 49 + // `data.channels`, so force it before we navigate to the new channel. 50 + await invalidateAll(); 51 + await goto( 52 + resolve('/c/[communityDid]/[channelKey]', { 53 + communityDid: data.communityDid, 54 + channelKey: res.channelKey 55 + }) 56 + ); 57 + } catch (err) { 58 + createError = err instanceof Error ? err.message : String(err); 59 + } finally { 60 + creating = false; 61 + } 62 + } 63 + 64 + $effect(() => { 65 + if (!user.isLoggedIn) return; 66 + const disconnect = connectCommunityRealtime(data.communityDid); 67 + return disconnect; 68 + }); 69 + 70 + $effect(() => { 71 + if (currentChannelKey) { 72 + const ch = data.channels.find((c) => c.key === currentChannelKey); 73 + if (ch) resetUnread(ch.spaceUri); 74 + } 75 + }); 76 + 77 + function closeMobileSidebar() { 78 + const el = typeof document !== 'undefined' && document.getElementById('mobile-menu'); 79 + if (el && 'hidePopover' in el) (el as HTMLElement).hidePopover(); 80 + } 81 + </script> 82 + 83 + <Navbar hasSidebar> 84 + <div class="flex min-w-0 items-center gap-2"> 85 + <button 86 + type="button" 87 + popovertarget="mobile-menu" 88 + class="text-base-500 hover:text-base-700 dark:hover:text-base-200 -ml-1 p-1 lg:hidden" 89 + aria-label="open sidebar" 90 + > 91 + <svg 92 + xmlns="http://www.w3.org/2000/svg" 93 + viewBox="0 0 24 24" 94 + fill="currentColor" 95 + class="size-5" 96 + > 97 + <path 98 + fill-rule="evenodd" 99 + d="M3 9a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 9Zm0 6.75a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75Z" 100 + clip-rule="evenodd" 101 + /> 102 + </svg> 103 + </button> 104 + 105 + {#if currentChannel} 106 + <div class="flex min-w-0 items-baseline gap-2"> 107 + <span class="text-base-500">{currentChannel.visibility === 'private' ? '🔒' : '#'}</span> 108 + <span class="truncate font-semibold">{currentChannel.name}</span> 109 + {#if currentChannel.topic} 110 + <span class="text-base-500 hidden truncate text-sm sm:inline"> 111 + {currentChannel.topic} 112 + </span> 113 + {/if} 114 + </div> 115 + {:else} 116 + <span class="truncate font-semibold">{data.server?.name ?? 'server'}</span> 117 + {/if} 118 + </div> 119 + </Navbar> 120 + 121 + <Sidebar> 122 + <div class="flex h-full flex-col"> 123 + <div class="border-base-200 dark:border-base-300/10 border-b p-4"> 124 + <a 125 + href={resolve('/')} 126 + onclick={closeMobileSidebar} 127 + class="text-base-500 text-xs hover:underline" 128 + > 129 + ← all servers 130 + </a> 131 + <div class="mt-2 flex items-center gap-2"> 132 + {#if data.server?.iconUrl} 133 + <img 134 + src={data.server.iconUrl} 135 + alt="" 136 + class="bg-base-200 dark:bg-base-800 size-8 shrink-0 rounded-full object-cover" 137 + /> 138 + {/if} 139 + <div class="truncate font-semibold">{data.server?.name ?? 'server'}</div> 140 + </div> 141 + {#if data.server?.description} 142 + <div class="text-base-500 mt-1 line-clamp-2 text-xs">{data.server.description}</div> 143 + {/if} 144 + </div> 145 + 146 + <nav class="flex-1 overflow-y-auto p-2"> 147 + <div class="mb-2 flex items-center justify-between px-2"> 148 + <span class="text-base-500 text-xs font-semibold uppercase">channels</span> 149 + {#if data.isAdmin} 150 + <button 151 + class="text-base-500 hover:text-base-700 dark:hover:text-base-200 text-sm leading-none" 152 + aria-label="new channel" 153 + onclick={() => (newChannelOpen = true)} 154 + > 155 + + 156 + </button> 157 + {/if} 158 + </div> 159 + 160 + {#if data.channels.length === 0} 161 + <div class="text-base-500 px-2 py-1 text-xs">no channels yet</div> 162 + {:else} 163 + <ul class="flex flex-col gap-0.5"> 164 + {#each data.channels as ch (ch.spaceUri)} 165 + {@const active = ch.key === currentChannelKey} 166 + {@const hasUnread = !active && (unread.counts[ch.spaceUri] ?? 0) > 0} 167 + <li> 168 + <Button 169 + variant="ghost" 170 + size="sm" 171 + href={resolve('/c/[communityDid]/[channelKey]', { 172 + communityDid: data.communityDid, 173 + channelKey: ch.key 174 + })} 175 + onclick={closeMobileSidebar} 176 + class="w-full justify-between font-normal backdrop-blur-none {active 177 + ? 'font-semibold' 178 + : 'opacity-70 hover:opacity-100'}" 179 + > 180 + <span class="flex min-w-0 items-center gap-1.5 truncate"> 181 + <span class="text-base-500">{ch.visibility === 'private' ? '🔒' : '#'}</span> 182 + <span class="truncate">{ch.name}</span> 183 + </span> 184 + {#if hasUnread} 185 + <span 186 + class="bg-accent-500 ml-2 inline-block h-2 w-2 shrink-0 rounded-full" 187 + aria-label="unread" 188 + ></span> 189 + {/if} 190 + </Button> 191 + </li> 192 + {/each} 193 + </ul> 194 + {/if} 195 + </nav> 196 + 197 + <div class="border-base-200 dark:border-base-300/10 flex flex-col gap-1 border-t p-2"> 198 + {#if data.isAdmin} 199 + <a 200 + href={resolve('/c/[communityDid]/settings/members', { 201 + communityDid: data.communityDid 202 + })} 203 + onclick={closeMobileSidebar} 204 + class="hover:bg-base-200/50 dark:hover:bg-base-800/50 block rounded-lg px-2 py-1 text-sm" 205 + > 206 + Members 207 + </a> 208 + <a 209 + href={resolve('/c/[communityDid]/settings/invites', { 210 + communityDid: data.communityDid 211 + })} 212 + onclick={closeMobileSidebar} 213 + class="hover:bg-base-200/50 dark:hover:bg-base-800/50 block rounded-lg px-2 py-1 text-sm" 214 + > 215 + Invites 216 + </a> 217 + {/if} 218 + <div class="text-base-500 truncate px-2 py-1 text-xs"> 219 + {user.profile?.handle ?? user.did} 220 + </div> 221 + </div> 222 + </div> 223 + </Sidebar> 224 + 225 + <main class="flex h-dvh min-w-0 flex-col overflow-hidden pt-18 lg:ml-74"> 226 + {@render children?.()} 227 + </main> 228 + 229 + <Modal bind:open={newChannelOpen}> 230 + <div class="flex flex-col gap-3"> 231 + <Heading>new channel</Heading> 232 + <form onsubmit={submitNewChannel} class="flex flex-col gap-3"> 233 + <label class="flex flex-col gap-1 text-sm"> 234 + Name 235 + <Input bind:value={newChannelName} placeholder="general" maxlength={128} required /> 236 + </label> 237 + <label class="flex flex-col gap-1 text-sm"> 238 + Topic (optional) 239 + <Input bind:value={newChannelTopic} maxlength={512} /> 240 + </label> 241 + <label class="flex items-center gap-2 text-sm"> 242 + <input type="checkbox" bind:checked={newChannelPrivate} /> 243 + Private — only specific people can see this 244 + </label> 245 + {#if newChannelPrivate} 246 + <label class="flex flex-col gap-1 text-sm"> 247 + Member DIDs (comma or newline separated) 248 + <textarea 249 + class="bg-base-100 dark:bg-base-900 rounded-xl border px-3 py-2 text-sm" 250 + rows="3" 251 + bind:value={privateDids} 252 + placeholder="did:plc:..." 253 + ></textarea> 254 + </label> 255 + {/if} 256 + {#if createError} 257 + <div class="rounded-xl border border-red-500 p-2 text-xs text-red-500">{createError}</div> 258 + {/if} 259 + <div class="flex gap-2"> 260 + <Button type="submit" disabled={creating || !newChannelName.trim()}> 261 + {creating ? 'Creating…' : 'Create'} 262 + </Button> 263 + <Button type="button" variant="ghost" onclick={() => (newChannelOpen = false)}> 264 + Cancel 265 + </Button> 266 + </div> 267 + </form> 268 + </div> 269 + </Modal>
+27
examples/sveltekit-group-chat/src/routes/c/[communityDid]/+page.svelte
··· 1 + <script lang="ts"> 2 + import { goto } from '$app/navigation'; 3 + let { data } = $props(); 4 + 5 + $effect(() => { 6 + if (data.channels.length > 0) { 7 + void goto(`/c/${encodeURIComponent(data.communityDid)}/${data.channels[0].key}`, { 8 + replaceState: true 9 + }); 10 + } 11 + }); 12 + </script> 13 + 14 + <div class="flex flex-1 items-center justify-center p-8"> 15 + {#if data.channels.length === 0} 16 + <div class="text-base-500 text-center"> 17 + <p>No channels yet.</p> 18 + {#if data.isAdmin} 19 + <p class="mt-2 text-sm">Click <b>+</b> in the sidebar to create one.</p> 20 + {:else} 21 + <p class="mt-2 text-sm">Waiting for an admin to create a channel.</p> 22 + {/if} 23 + </div> 24 + {:else} 25 + <span class="text-base-500 text-sm">loading…</span> 26 + {/if} 27 + </div>
+56
examples/sveltekit-group-chat/src/routes/join/[token]/+page.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/state'; 3 + import { goto } from '$app/navigation'; 4 + import { resolve } from '$app/paths'; 5 + import { Button, Heading } from '@foxui/core'; 6 + import { user } from '$lib/atproto'; 7 + import { atProtoLoginModalState } from '$lib/atproto/ui/LoginModal.svelte'; 8 + import { redeemInvite } from '$lib/rooms/invites.remote'; 9 + 10 + const token = $derived(page.params.token!); 11 + 12 + let redeeming = $state(false); 13 + let err = $state<string | null>(null); 14 + 15 + async function accept() { 16 + redeeming = true; 17 + err = null; 18 + try { 19 + const res = await redeemInvite({ token }); 20 + await goto( 21 + resolve('/c/[communityDid]', { communityDid: res.communityDid }) 22 + ); 23 + } catch (e) { 24 + err = e instanceof Error ? e.message : String(e); 25 + } finally { 26 + redeeming = false; 27 + } 28 + } 29 + </script> 30 + 31 + <div class="mx-auto my-24 max-w-md px-4"> 32 + <Heading>Join server</Heading> 33 + 34 + {#if !user.isLoggedIn} 35 + <p class="text-base-500 mt-3 text-sm"> 36 + Sign in with your atproto account to accept this invite. 37 + </p> 38 + <div class="mt-4"> 39 + <Button onclick={() => (atProtoLoginModalState.open = true)}>Sign in</Button> 40 + </div> 41 + {:else} 42 + <p class="text-base-500 mt-3 text-sm"> 43 + You're signed in as <span class="font-medium">{user.profile?.handle ?? user.did}</span>. 44 + Accepting will add you to the server's members. 45 + </p> 46 + <div class="mt-6 flex gap-2"> 47 + <Button onclick={accept} disabled={redeeming}> 48 + {redeeming ? 'Joining…' : 'Accept invite'} 49 + </Button> 50 + <Button variant="ghost" onclick={() => goto(resolve('/'))}>Cancel</Button> 51 + </div> 52 + {#if err} 53 + <div class="mt-4 rounded-xl border border-red-500 p-2 text-sm text-red-500">{err}</div> 54 + {/if} 55 + {/if} 56 + </div>
+2 -7
examples/sveltekit-group-chat/src/routes/xrpc/[...path]/+server.ts
··· 1 - import { createHandler } from '@atmo-dev/contrail/server'; 2 - import { contrail, ensureInit } from '$lib/contrail'; 1 + import { dispatch } from '$lib/contrail'; 3 2 import type { RequestHandler } from './$types'; 4 3 5 - const handle = createHandler(contrail); 6 - 7 4 async function handler(request: Request, platform: App.Platform | undefined) { 8 - const db = platform!.env.DB; 9 - await ensureInit(db); 10 - return handle(request, db) as Promise<Response>; 5 + return dispatch(request, platform!.env); 11 6 } 12 7 13 8 export const GET: RequestHandler = async ({ request, platform }) => handler(request, platform);
+184
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/getRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.channel.getRecord", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a single tools.atmo.chat.channel record by AT URI", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "uri" 12 + ], 13 + "properties": { 14 + "uri": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "AT URI of the record" 18 + }, 19 + "profiles": { 20 + "type": "boolean", 21 + "description": "Include profile + identity info keyed by DID" 22 + }, 23 + "spaceUri": { 24 + "type": "string", 25 + "format": "at-uri", 26 + "description": "If set, fetch from this permissioned space (requires service-auth JWT or a read-grant invite token)." 27 + }, 28 + "inviteToken": { 29 + "type": "string", 30 + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." 31 + } 32 + } 33 + }, 34 + "output": { 35 + "encoding": "application/json", 36 + "schema": { 37 + "type": "object", 38 + "required": [ 39 + "uri", 40 + "did", 41 + "collection", 42 + "rkey", 43 + "time_us" 44 + ], 45 + "properties": { 46 + "uri": { 47 + "type": "string", 48 + "format": "at-uri" 49 + }, 50 + "did": { 51 + "type": "string", 52 + "format": "did" 53 + }, 54 + "collection": { 55 + "type": "string", 56 + "format": "nsid" 57 + }, 58 + "rkey": { 59 + "type": "string" 60 + }, 61 + "cid": { 62 + "type": "string" 63 + }, 64 + "record": { 65 + "type": "ref", 66 + "ref": "tools.atmo.chat.channel#main" 67 + }, 68 + "time_us": { 69 + "type": "integer" 70 + }, 71 + "space": { 72 + "type": "string", 73 + "format": "at-uri", 74 + "description": "Present when the record was read from a permissioned space; its value is the space URI." 75 + }, 76 + "profiles": { 77 + "type": "array", 78 + "items": { 79 + "type": "ref", 80 + "ref": "#profileEntry" 81 + } 82 + } 83 + } 84 + } 85 + } 86 + }, 87 + "profileEntry": { 88 + "type": "object", 89 + "required": [ 90 + "did" 91 + ], 92 + "properties": { 93 + "did": { 94 + "type": "string", 95 + "format": "did" 96 + }, 97 + "handle": { 98 + "type": "string" 99 + }, 100 + "uri": { 101 + "type": "string", 102 + "format": "at-uri" 103 + }, 104 + "collection": { 105 + "type": "string", 106 + "format": "nsid" 107 + }, 108 + "rkey": { 109 + "type": "string" 110 + }, 111 + "cid": { 112 + "type": "string" 113 + }, 114 + "record": { 115 + "type": "ref", 116 + "ref": "#appBskyActorProfile" 117 + } 118 + } 119 + }, 120 + "appBskyActorProfile": { 121 + "type": "object", 122 + "properties": { 123 + "avatar": { 124 + "type": "blob", 125 + "accept": [ 126 + "image/png", 127 + "image/jpeg" 128 + ], 129 + "maxSize": 1000000, 130 + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 131 + }, 132 + "banner": { 133 + "type": "blob", 134 + "accept": [ 135 + "image/png", 136 + "image/jpeg" 137 + ], 138 + "maxSize": 1000000, 139 + "description": "Larger horizontal image to display behind profile view." 140 + }, 141 + "labels": { 142 + "refs": [ 143 + "com.atproto.label.defs#selfLabels" 144 + ], 145 + "type": "union", 146 + "description": "Self-label values, specific to the Bluesky application, on the overall account." 147 + }, 148 + "website": { 149 + "type": "string", 150 + "format": "uri" 151 + }, 152 + "pronouns": { 153 + "type": "string", 154 + "maxLength": 200, 155 + "description": "Free-form pronouns text.", 156 + "maxGraphemes": 20 157 + }, 158 + "createdAt": { 159 + "type": "string", 160 + "format": "datetime" 161 + }, 162 + "pinnedPost": { 163 + "ref": "com.atproto.repo.strongRef", 164 + "type": "ref" 165 + }, 166 + "description": { 167 + "type": "string", 168 + "maxLength": 2560, 169 + "description": "Free-form profile description text.", 170 + "maxGraphemes": 256 171 + }, 172 + "displayName": { 173 + "type": "string", 174 + "maxLength": 640, 175 + "maxGraphemes": 64 176 + }, 177 + "joinedViaStarterPack": { 178 + "ref": "com.atproto.repo.strongRef", 179 + "type": "ref" 180 + } 181 + } 182 + } 183 + } 184 + }
+256
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/listRecords.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.channel.listRecords", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Query tools.atmo.chat.channel records with filters", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "limit": { 12 + "type": "integer", 13 + "minimum": 1, 14 + "maximum": 200, 15 + "default": 50 16 + }, 17 + "cursor": { 18 + "type": "string" 19 + }, 20 + "actor": { 21 + "type": "string", 22 + "format": "at-identifier", 23 + "description": "Filter by DID or handle (triggers on-demand backfill)" 24 + }, 25 + "profiles": { 26 + "type": "boolean", 27 + "description": "Include profile + identity info keyed by DID" 28 + }, 29 + "spaceUri": { 30 + "type": "string", 31 + "format": "at-uri", 32 + "description": "If set, query records inside this permissioned space (requires service-auth JWT or a read-grant invite token)." 33 + }, 34 + "byUser": { 35 + "type": "string", 36 + "format": "did", 37 + "description": "Only used with spaceUri — filter to records authored by this DID." 38 + }, 39 + "inviteToken": { 40 + "type": "string", 41 + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." 42 + }, 43 + "communityDid": { 44 + "type": "string", 45 + "description": "Filter by communityDid" 46 + }, 47 + "name": { 48 + "type": "string", 49 + "description": "Filter by name" 50 + }, 51 + "topic": { 52 + "type": "string", 53 + "description": "Filter by topic" 54 + }, 55 + "visibility": { 56 + "type": "string", 57 + "description": "Filter by visibility" 58 + }, 59 + "createdAtMin": { 60 + "type": "string", 61 + "description": "Minimum value for createdAt" 62 + }, 63 + "createdAtMax": { 64 + "type": "string", 65 + "description": "Maximum value for createdAt" 66 + }, 67 + "sort": { 68 + "type": "string", 69 + "knownValues": [ 70 + "communityDid", 71 + "name", 72 + "topic", 73 + "visibility", 74 + "createdAt" 75 + ], 76 + "description": "Field to sort by (default: time_us)" 77 + }, 78 + "order": { 79 + "type": "string", 80 + "knownValues": [ 81 + "asc", 82 + "desc" 83 + ], 84 + "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" 85 + } 86 + } 87 + }, 88 + "output": { 89 + "encoding": "application/json", 90 + "schema": { 91 + "type": "object", 92 + "required": [ 93 + "records" 94 + ], 95 + "properties": { 96 + "records": { 97 + "type": "array", 98 + "items": { 99 + "type": "ref", 100 + "ref": "#record" 101 + } 102 + }, 103 + "cursor": { 104 + "type": "string" 105 + }, 106 + "profiles": { 107 + "type": "array", 108 + "items": { 109 + "type": "ref", 110 + "ref": "#profileEntry" 111 + } 112 + } 113 + } 114 + } 115 + } 116 + }, 117 + "record": { 118 + "type": "object", 119 + "required": [ 120 + "uri", 121 + "did", 122 + "collection", 123 + "rkey", 124 + "time_us" 125 + ], 126 + "properties": { 127 + "uri": { 128 + "type": "string", 129 + "format": "at-uri" 130 + }, 131 + "did": { 132 + "type": "string", 133 + "format": "did" 134 + }, 135 + "collection": { 136 + "type": "string", 137 + "format": "nsid" 138 + }, 139 + "rkey": { 140 + "type": "string" 141 + }, 142 + "cid": { 143 + "type": "string" 144 + }, 145 + "record": { 146 + "type": "ref", 147 + "ref": "tools.atmo.chat.channel#main" 148 + }, 149 + "time_us": { 150 + "type": "integer" 151 + }, 152 + "space": { 153 + "type": "string", 154 + "format": "at-uri", 155 + "description": "Present when the record was read from a permissioned space; its value is the space URI." 156 + } 157 + } 158 + }, 159 + "profileEntry": { 160 + "type": "object", 161 + "required": [ 162 + "did" 163 + ], 164 + "properties": { 165 + "did": { 166 + "type": "string", 167 + "format": "did" 168 + }, 169 + "handle": { 170 + "type": "string" 171 + }, 172 + "uri": { 173 + "type": "string", 174 + "format": "at-uri" 175 + }, 176 + "collection": { 177 + "type": "string", 178 + "format": "nsid" 179 + }, 180 + "rkey": { 181 + "type": "string" 182 + }, 183 + "cid": { 184 + "type": "string" 185 + }, 186 + "record": { 187 + "type": "ref", 188 + "ref": "#appBskyActorProfile" 189 + } 190 + } 191 + }, 192 + "appBskyActorProfile": { 193 + "type": "object", 194 + "properties": { 195 + "avatar": { 196 + "type": "blob", 197 + "accept": [ 198 + "image/png", 199 + "image/jpeg" 200 + ], 201 + "maxSize": 1000000, 202 + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 203 + }, 204 + "banner": { 205 + "type": "blob", 206 + "accept": [ 207 + "image/png", 208 + "image/jpeg" 209 + ], 210 + "maxSize": 1000000, 211 + "description": "Larger horizontal image to display behind profile view." 212 + }, 213 + "labels": { 214 + "refs": [ 215 + "com.atproto.label.defs#selfLabels" 216 + ], 217 + "type": "union", 218 + "description": "Self-label values, specific to the Bluesky application, on the overall account." 219 + }, 220 + "website": { 221 + "type": "string", 222 + "format": "uri" 223 + }, 224 + "pronouns": { 225 + "type": "string", 226 + "maxLength": 200, 227 + "description": "Free-form pronouns text.", 228 + "maxGraphemes": 20 229 + }, 230 + "createdAt": { 231 + "type": "string", 232 + "format": "datetime" 233 + }, 234 + "pinnedPost": { 235 + "ref": "com.atproto.repo.strongRef", 236 + "type": "ref" 237 + }, 238 + "description": { 239 + "type": "string", 240 + "maxLength": 2560, 241 + "description": "Free-form profile description text.", 242 + "maxGraphemes": 256 243 + }, 244 + "displayName": { 245 + "type": "string", 246 + "maxLength": 640, 247 + "maxGraphemes": 64 248 + }, 249 + "joinedViaStarterPack": { 250 + "ref": "com.atproto.repo.strongRef", 251 + "type": "ref" 252 + } 253 + } 254 + } 255 + } 256 + }
+56
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/adopt.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.adopt", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Adopt an existing ATProto account as a community identity. The provided app password is verified by creating a session, then stored encrypted. Creates the reserved `$admin` and `$publishers` spaces with the caller as `owner` in both.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "identifier", 14 + "appPassword" 15 + ], 16 + "properties": { 17 + "identifier": { 18 + "type": "string", 19 + "description": "Handle or DID of the account to adopt." 20 + }, 21 + "appPassword": { 22 + "type": "string", 23 + "description": "App password for the account; verified by createSession and stored encrypted." 24 + } 25 + } 26 + } 27 + }, 28 + "output": { 29 + "encoding": "application/json", 30 + "schema": { 31 + "type": "object", 32 + "required": [ 33 + "communityDid" 34 + ], 35 + "properties": { 36 + "communityDid": { 37 + "type": "string", 38 + "format": "did" 39 + } 40 + } 41 + } 42 + }, 43 + "errors": [ 44 + { 45 + "name": "InvalidRequest" 46 + }, 47 + { 48 + "name": "Unauthorized" 49 + }, 50 + { 51 + "name": "AlreadyExists" 52 + } 53 + ] 54 + } 55 + } 56 + }
+185
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.defs", 4 + "description": "Shared types for community-module XRPC methods.", 5 + "defs": { 6 + "accessLevel": { 7 + "type": "string", 8 + "knownValues": [ 9 + "member", 10 + "manager", 11 + "admin", 12 + "owner" 13 + ], 14 + "description": "Module-internal access levels that govern community operations on a given space. Ordered: member < manager < admin < owner." 15 + }, 16 + "communityView": { 17 + "type": "object", 18 + "required": [ 19 + "did", 20 + "mode", 21 + "createdAt" 22 + ], 23 + "properties": { 24 + "did": { 25 + "type": "string", 26 + "format": "did" 27 + }, 28 + "mode": { 29 + "type": "string", 30 + "knownValues": [ 31 + "adopt", 32 + "mint" 33 + ] 34 + }, 35 + "identifier": { 36 + "type": "string", 37 + "description": "Handle or DID at adoption time (adopt only)." 38 + }, 39 + "createdAt": { 40 + "type": "integer" 41 + } 42 + } 43 + }, 44 + "spaceView": { 45 + "type": "object", 46 + "required": [ 47 + "uri", 48 + "ownerDid", 49 + "type", 50 + "key", 51 + "serviceDid", 52 + "createdAt" 53 + ], 54 + "properties": { 55 + "uri": { 56 + "type": "string", 57 + "format": "at-uri" 58 + }, 59 + "ownerDid": { 60 + "type": "string", 61 + "format": "did" 62 + }, 63 + "type": { 64 + "type": "string", 65 + "format": "nsid" 66 + }, 67 + "key": { 68 + "type": "string" 69 + }, 70 + "serviceDid": { 71 + "type": "string" 72 + }, 73 + "createdAt": { 74 + "type": "integer" 75 + } 76 + } 77 + }, 78 + "subject": { 79 + "type": "object", 80 + "description": "Exactly one of `did` or `spaceUri` must be set.", 81 + "properties": { 82 + "did": { 83 + "type": "string", 84 + "format": "did" 85 + }, 86 + "spaceUri": { 87 + "type": "string", 88 + "format": "at-uri" 89 + } 90 + } 91 + }, 92 + "memberRow": { 93 + "type": "object", 94 + "required": [ 95 + "subject", 96 + "accessLevel", 97 + "grantedBy", 98 + "grantedAt" 99 + ], 100 + "properties": { 101 + "subject": { 102 + "type": "ref", 103 + "ref": "#subject" 104 + }, 105 + "accessLevel": { 106 + "type": "ref", 107 + "ref": "#accessLevel" 108 + }, 109 + "grantedBy": { 110 + "type": "string", 111 + "format": "did" 112 + }, 113 + "grantedAt": { 114 + "type": "integer" 115 + } 116 + } 117 + }, 118 + "flatMember": { 119 + "type": "object", 120 + "required": [ 121 + "did", 122 + "addedAt" 123 + ], 124 + "properties": { 125 + "did": { 126 + "type": "string", 127 + "format": "did" 128 + }, 129 + "addedAt": { 130 + "type": "integer" 131 + } 132 + } 133 + }, 134 + "inviteView": { 135 + "type": "object", 136 + "required": [ 137 + "tokenHash", 138 + "spaceUri", 139 + "accessLevel", 140 + "createdBy", 141 + "createdAt", 142 + "usedCount" 143 + ], 144 + "properties": { 145 + "tokenHash": { 146 + "type": "string", 147 + "description": "SHA-256 of the raw token. Stable id for list/revoke; never grants access on its own." 148 + }, 149 + "spaceUri": { 150 + "type": "string", 151 + "format": "at-uri" 152 + }, 153 + "accessLevel": { 154 + "type": "ref", 155 + "ref": "#accessLevel" 156 + }, 157 + "createdBy": { 158 + "type": "string", 159 + "format": "did" 160 + }, 161 + "createdAt": { 162 + "type": "integer" 163 + }, 164 + "expiresAt": { 165 + "type": "integer", 166 + "description": "Unix ms. Omitted for no expiry." 167 + }, 168 + "maxUses": { 169 + "type": "integer", 170 + "description": "Omitted for unlimited." 171 + }, 172 + "usedCount": { 173 + "type": "integer" 174 + }, 175 + "revokedAt": { 176 + "type": "integer", 177 + "description": "Unix ms. Omitted if not revoked." 178 + }, 179 + "note": { 180 + "type": "string" 181 + } 182 + } 183 + } 184 + } 185 + }
+47
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/delete.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.delete", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Soft-delete a community. Also soft-deletes all spaces owned by the community. Caller must have `owner` in the community's `$admin` space.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "communityDid" 14 + ], 15 + "properties": { 16 + "communityDid": { 17 + "type": "string", 18 + "format": "did" 19 + } 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "required": [ 28 + "ok" 29 + ], 30 + "properties": { 31 + "ok": { 32 + "type": "boolean" 33 + } 34 + } 35 + } 36 + }, 37 + "errors": [ 38 + { 39 + "name": "NotFound" 40 + }, 41 + { 42 + "name": "Forbidden" 43 + } 44 + ] 45 + } 46 + } 47 + }
+65
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/deleteRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.deleteRecord", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Delete a public record authored by the community from the community's PDS. Adopted communities only. Caller must be `member` or higher in the community's `$publishers` space.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "communityDid", 14 + "collection", 15 + "rkey" 16 + ], 17 + "properties": { 18 + "communityDid": { 19 + "type": "string", 20 + "format": "did" 21 + }, 22 + "collection": { 23 + "type": "string", 24 + "format": "nsid" 25 + }, 26 + "rkey": { 27 + "type": "string" 28 + } 29 + } 30 + } 31 + }, 32 + "output": { 33 + "encoding": "application/json", 34 + "schema": { 35 + "type": "object", 36 + "required": [ 37 + "ok" 38 + ], 39 + "properties": { 40 + "ok": { 41 + "type": "boolean" 42 + } 43 + } 44 + } 45 + }, 46 + "errors": [ 47 + { 48 + "name": "InvalidRequest" 49 + }, 50 + { 51 + "name": "NotFound" 52 + }, 53 + { 54 + "name": "Forbidden" 55 + }, 56 + { 57 + "name": "NotSupported" 58 + }, 59 + { 60 + "name": "UpstreamFailure" 61 + } 62 + ] 63 + } 64 + } 65 + }
+49
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/getHealth.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.getHealth", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Check whether stored credentials for the community are still usable. Adopted: attempts a live session creation. Minted: verifies the signing key can be decrypted. Caller must have at least `member` in the community's `$admin` space.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "communityDid" 12 + ], 13 + "properties": { 14 + "communityDid": { 15 + "type": "string", 16 + "format": "did" 17 + } 18 + } 19 + }, 20 + "output": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "required": [ 25 + "status" 26 + ], 27 + "properties": { 28 + "status": { 29 + "type": "string", 30 + "knownValues": [ 31 + "healthy", 32 + "degraded", 33 + "expired" 34 + ] 35 + } 36 + } 37 + } 38 + }, 39 + "errors": [ 40 + { 41 + "name": "NotFound" 42 + }, 43 + { 44 + "name": "Forbidden" 45 + } 46 + ] 47 + } 48 + } 49 + }
+38
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/list.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.list", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List communities where the actor has any access level in any community-owned space. Defaults to the JWT issuer when `actor` is omitted.", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "actor": { 12 + "type": "string", 13 + "format": "did", 14 + "description": "DID to query (defaults to the JWT issuer)." 15 + } 16 + } 17 + }, 18 + "output": { 19 + "encoding": "application/json", 20 + "schema": { 21 + "type": "object", 22 + "required": [ 23 + "communities" 24 + ], 25 + "properties": { 26 + "communities": { 27 + "type": "array", 28 + "items": { 29 + "type": "ref", 30 + "ref": "tools.atmo.chat.community.defs#communityView" 31 + } 32 + } 33 + } 34 + } 35 + } 36 + } 37 + } 38 + }
+51
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/mint.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.mint", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Mint a fresh did:plc for a new community. Contrail generates three P-256 keypairs (signing, contrail rotation, creator rotation), submits a genesis op to the PLC directory, and stores the signing + contrail-rotation keys encrypted. The creator's rotation key is returned once as a recovery secret and never stored.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "properties": { 13 + "handle": { 14 + "type": "string", 15 + "description": "Optional handle to include in alsoKnownAs (as at://<handle>)." 16 + }, 17 + "pdsEndpoint": { 18 + "type": "string", 19 + "description": "Optional PDS endpoint to include as the atproto_pds service." 20 + } 21 + } 22 + } 23 + }, 24 + "output": { 25 + "encoding": "application/json", 26 + "schema": { 27 + "type": "object", 28 + "required": [ 29 + "communityDid", 30 + "recoveryKey" 31 + ], 32 + "properties": { 33 + "communityDid": { 34 + "type": "string", 35 + "format": "did" 36 + }, 37 + "recoveryKey": { 38 + "type": "unknown", 39 + "description": "Creator's rotation key as a private JWK. Shown once — cannot be retrieved later." 40 + } 41 + } 42 + } 43 + }, 44 + "errors": [ 45 + { 46 + "name": "UpstreamFailure" 47 + } 48 + ] 49 + } 50 + } 51 + }
+73
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/putRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.putRecord", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Publish a public record authored by the community via the community's PDS. Adopted communities only. Caller must be `member` or higher in the community's `$publishers` space.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "communityDid", 14 + "collection", 15 + "record" 16 + ], 17 + "properties": { 18 + "communityDid": { 19 + "type": "string", 20 + "format": "did" 21 + }, 22 + "collection": { 23 + "type": "string", 24 + "format": "nsid" 25 + }, 26 + "rkey": { 27 + "type": "string" 28 + }, 29 + "record": { 30 + "type": "unknown" 31 + }, 32 + "validate": { 33 + "type": "boolean" 34 + } 35 + } 36 + } 37 + }, 38 + "output": { 39 + "encoding": "application/json", 40 + "schema": { 41 + "type": "object", 42 + "properties": { 43 + "uri": { 44 + "type": "string", 45 + "format": "at-uri" 46 + }, 47 + "cid": { 48 + "type": "string", 49 + "format": "cid" 50 + } 51 + } 52 + } 53 + }, 54 + "errors": [ 55 + { 56 + "name": "InvalidRequest" 57 + }, 58 + { 59 + "name": "NotFound" 60 + }, 61 + { 62 + "name": "Forbidden" 63 + }, 64 + { 65 + "name": "NotSupported" 66 + }, 67 + { 68 + "name": "UpstreamFailure" 69 + } 70 + ] 71 + } 72 + } 73 + }
+60
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/reauth.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.reauth", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Replace the stored app password for an adopted community. Caller must have `owner` access in the community's `$admin` space.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "communityDid", 14 + "appPassword" 15 + ], 16 + "properties": { 17 + "communityDid": { 18 + "type": "string", 19 + "format": "did" 20 + }, 21 + "appPassword": { 22 + "type": "string" 23 + } 24 + } 25 + } 26 + }, 27 + "output": { 28 + "encoding": "application/json", 29 + "schema": { 30 + "type": "object", 31 + "required": [ 32 + "ok" 33 + ], 34 + "properties": { 35 + "ok": { 36 + "type": "boolean" 37 + } 38 + } 39 + } 40 + }, 41 + "errors": [ 42 + { 43 + "name": "InvalidRequest" 44 + }, 45 + { 46 + "name": "NotFound" 47 + }, 48 + { 49 + "name": "Forbidden" 50 + }, 51 + { 52 + "name": "Unauthorized" 53 + }, 54 + { 55 + "name": "NotSupported" 56 + } 57 + ] 58 + } 59 + } 60 + }
+42
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/whoami.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.whoami", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Report the JWT issuer's effective access level in a space (resolving delegated memberships).", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri" 12 + ], 13 + "properties": { 14 + "spaceUri": { 15 + "type": "string", 16 + "format": "at-uri" 17 + } 18 + } 19 + }, 20 + "output": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "required": [ 25 + "spaceUri" 26 + ], 27 + "properties": { 28 + "spaceUri": { 29 + "type": "string", 30 + "format": "at-uri" 31 + }, 32 + "accessLevel": { 33 + "type": "ref", 34 + "ref": "tools.atmo.chat.community.defs#accessLevel", 35 + "description": "Null when caller has no resolvable access." 36 + } 37 + } 38 + } 39 + } 40 + } 41 + } 42 + }
+184
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/getRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.message.getRecord", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a single tools.atmo.chat.message record by AT URI", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "uri" 12 + ], 13 + "properties": { 14 + "uri": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "AT URI of the record" 18 + }, 19 + "profiles": { 20 + "type": "boolean", 21 + "description": "Include profile + identity info keyed by DID" 22 + }, 23 + "spaceUri": { 24 + "type": "string", 25 + "format": "at-uri", 26 + "description": "If set, fetch from this permissioned space (requires service-auth JWT or a read-grant invite token)." 27 + }, 28 + "inviteToken": { 29 + "type": "string", 30 + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." 31 + } 32 + } 33 + }, 34 + "output": { 35 + "encoding": "application/json", 36 + "schema": { 37 + "type": "object", 38 + "required": [ 39 + "uri", 40 + "did", 41 + "collection", 42 + "rkey", 43 + "time_us" 44 + ], 45 + "properties": { 46 + "uri": { 47 + "type": "string", 48 + "format": "at-uri" 49 + }, 50 + "did": { 51 + "type": "string", 52 + "format": "did" 53 + }, 54 + "collection": { 55 + "type": "string", 56 + "format": "nsid" 57 + }, 58 + "rkey": { 59 + "type": "string" 60 + }, 61 + "cid": { 62 + "type": "string" 63 + }, 64 + "record": { 65 + "type": "ref", 66 + "ref": "tools.atmo.chat.message#main" 67 + }, 68 + "time_us": { 69 + "type": "integer" 70 + }, 71 + "space": { 72 + "type": "string", 73 + "format": "at-uri", 74 + "description": "Present when the record was read from a permissioned space; its value is the space URI." 75 + }, 76 + "profiles": { 77 + "type": "array", 78 + "items": { 79 + "type": "ref", 80 + "ref": "#profileEntry" 81 + } 82 + } 83 + } 84 + } 85 + } 86 + }, 87 + "profileEntry": { 88 + "type": "object", 89 + "required": [ 90 + "did" 91 + ], 92 + "properties": { 93 + "did": { 94 + "type": "string", 95 + "format": "did" 96 + }, 97 + "handle": { 98 + "type": "string" 99 + }, 100 + "uri": { 101 + "type": "string", 102 + "format": "at-uri" 103 + }, 104 + "collection": { 105 + "type": "string", 106 + "format": "nsid" 107 + }, 108 + "rkey": { 109 + "type": "string" 110 + }, 111 + "cid": { 112 + "type": "string" 113 + }, 114 + "record": { 115 + "type": "ref", 116 + "ref": "#appBskyActorProfile" 117 + } 118 + } 119 + }, 120 + "appBskyActorProfile": { 121 + "type": "object", 122 + "properties": { 123 + "avatar": { 124 + "type": "blob", 125 + "accept": [ 126 + "image/png", 127 + "image/jpeg" 128 + ], 129 + "maxSize": 1000000, 130 + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 131 + }, 132 + "banner": { 133 + "type": "blob", 134 + "accept": [ 135 + "image/png", 136 + "image/jpeg" 137 + ], 138 + "maxSize": 1000000, 139 + "description": "Larger horizontal image to display behind profile view." 140 + }, 141 + "labels": { 142 + "refs": [ 143 + "com.atproto.label.defs#selfLabels" 144 + ], 145 + "type": "union", 146 + "description": "Self-label values, specific to the Bluesky application, on the overall account." 147 + }, 148 + "website": { 149 + "type": "string", 150 + "format": "uri" 151 + }, 152 + "pronouns": { 153 + "type": "string", 154 + "maxLength": 200, 155 + "description": "Free-form pronouns text.", 156 + "maxGraphemes": 20 157 + }, 158 + "createdAt": { 159 + "type": "string", 160 + "format": "datetime" 161 + }, 162 + "pinnedPost": { 163 + "ref": "com.atproto.repo.strongRef", 164 + "type": "ref" 165 + }, 166 + "description": { 167 + "type": "string", 168 + "maxLength": 2560, 169 + "description": "Free-form profile description text.", 170 + "maxGraphemes": 256 171 + }, 172 + "displayName": { 173 + "type": "string", 174 + "maxLength": 640, 175 + "maxGraphemes": 64 176 + }, 177 + "joinedViaStarterPack": { 178 + "ref": "com.atproto.repo.strongRef", 179 + "type": "ref" 180 + } 181 + } 182 + } 183 + } 184 + }
+246
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/listRecords.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.message.listRecords", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Query tools.atmo.chat.message records with filters", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "limit": { 12 + "type": "integer", 13 + "minimum": 1, 14 + "maximum": 200, 15 + "default": 50 16 + }, 17 + "cursor": { 18 + "type": "string" 19 + }, 20 + "actor": { 21 + "type": "string", 22 + "format": "at-identifier", 23 + "description": "Filter by DID or handle (triggers on-demand backfill)" 24 + }, 25 + "profiles": { 26 + "type": "boolean", 27 + "description": "Include profile + identity info keyed by DID" 28 + }, 29 + "spaceUri": { 30 + "type": "string", 31 + "format": "at-uri", 32 + "description": "If set, query records inside this permissioned space (requires service-auth JWT or a read-grant invite token)." 33 + }, 34 + "byUser": { 35 + "type": "string", 36 + "format": "did", 37 + "description": "Only used with spaceUri — filter to records authored by this DID." 38 + }, 39 + "inviteToken": { 40 + "type": "string", 41 + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." 42 + }, 43 + "text": { 44 + "type": "string", 45 + "description": "Filter by text" 46 + }, 47 + "createdAtMin": { 48 + "type": "string", 49 + "description": "Minimum value for createdAt" 50 + }, 51 + "createdAtMax": { 52 + "type": "string", 53 + "description": "Maximum value for createdAt" 54 + }, 55 + "replyTo": { 56 + "type": "string", 57 + "description": "Filter by replyTo" 58 + }, 59 + "sort": { 60 + "type": "string", 61 + "knownValues": [ 62 + "text", 63 + "createdAt", 64 + "replyTo" 65 + ], 66 + "description": "Field to sort by (default: time_us)" 67 + }, 68 + "order": { 69 + "type": "string", 70 + "knownValues": [ 71 + "asc", 72 + "desc" 73 + ], 74 + "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" 75 + } 76 + } 77 + }, 78 + "output": { 79 + "encoding": "application/json", 80 + "schema": { 81 + "type": "object", 82 + "required": [ 83 + "records" 84 + ], 85 + "properties": { 86 + "records": { 87 + "type": "array", 88 + "items": { 89 + "type": "ref", 90 + "ref": "#record" 91 + } 92 + }, 93 + "cursor": { 94 + "type": "string" 95 + }, 96 + "profiles": { 97 + "type": "array", 98 + "items": { 99 + "type": "ref", 100 + "ref": "#profileEntry" 101 + } 102 + } 103 + } 104 + } 105 + } 106 + }, 107 + "record": { 108 + "type": "object", 109 + "required": [ 110 + "uri", 111 + "did", 112 + "collection", 113 + "rkey", 114 + "time_us" 115 + ], 116 + "properties": { 117 + "uri": { 118 + "type": "string", 119 + "format": "at-uri" 120 + }, 121 + "did": { 122 + "type": "string", 123 + "format": "did" 124 + }, 125 + "collection": { 126 + "type": "string", 127 + "format": "nsid" 128 + }, 129 + "rkey": { 130 + "type": "string" 131 + }, 132 + "cid": { 133 + "type": "string" 134 + }, 135 + "record": { 136 + "type": "ref", 137 + "ref": "tools.atmo.chat.message#main" 138 + }, 139 + "time_us": { 140 + "type": "integer" 141 + }, 142 + "space": { 143 + "type": "string", 144 + "format": "at-uri", 145 + "description": "Present when the record was read from a permissioned space; its value is the space URI." 146 + } 147 + } 148 + }, 149 + "profileEntry": { 150 + "type": "object", 151 + "required": [ 152 + "did" 153 + ], 154 + "properties": { 155 + "did": { 156 + "type": "string", 157 + "format": "did" 158 + }, 159 + "handle": { 160 + "type": "string" 161 + }, 162 + "uri": { 163 + "type": "string", 164 + "format": "at-uri" 165 + }, 166 + "collection": { 167 + "type": "string", 168 + "format": "nsid" 169 + }, 170 + "rkey": { 171 + "type": "string" 172 + }, 173 + "cid": { 174 + "type": "string" 175 + }, 176 + "record": { 177 + "type": "ref", 178 + "ref": "#appBskyActorProfile" 179 + } 180 + } 181 + }, 182 + "appBskyActorProfile": { 183 + "type": "object", 184 + "properties": { 185 + "avatar": { 186 + "type": "blob", 187 + "accept": [ 188 + "image/png", 189 + "image/jpeg" 190 + ], 191 + "maxSize": 1000000, 192 + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 193 + }, 194 + "banner": { 195 + "type": "blob", 196 + "accept": [ 197 + "image/png", 198 + "image/jpeg" 199 + ], 200 + "maxSize": 1000000, 201 + "description": "Larger horizontal image to display behind profile view." 202 + }, 203 + "labels": { 204 + "refs": [ 205 + "com.atproto.label.defs#selfLabels" 206 + ], 207 + "type": "union", 208 + "description": "Self-label values, specific to the Bluesky application, on the overall account." 209 + }, 210 + "website": { 211 + "type": "string", 212 + "format": "uri" 213 + }, 214 + "pronouns": { 215 + "type": "string", 216 + "maxLength": 200, 217 + "description": "Free-form pronouns text.", 218 + "maxGraphemes": 20 219 + }, 220 + "createdAt": { 221 + "type": "string", 222 + "format": "datetime" 223 + }, 224 + "pinnedPost": { 225 + "ref": "com.atproto.repo.strongRef", 226 + "type": "ref" 227 + }, 228 + "description": { 229 + "type": "string", 230 + "maxLength": 2560, 231 + "description": "Free-form profile description text.", 232 + "maxGraphemes": 256 233 + }, 234 + "displayName": { 235 + "type": "string", 236 + "maxLength": 640, 237 + "maxGraphemes": 64 238 + }, 239 + "joinedViaStarterPack": { 240 + "ref": "com.atproto.repo.strongRef", 241 + "type": "ref" 242 + } 243 + } 244 + } 245 + } 246 + }
+42
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/realtime/subscribe.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.realtime.subscribe", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Open a streaming subscription. Responds with Server-Sent Events by default, or upgrades to WebSocket when the request carries `Upgrade: websocket`. Auth via `?ticket=` (browser) or `Authorization: Bearer <JWT>` (server-side). The stream closes with a final `member.removed` event if the caller loses access mid-stream.", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "ticket": { 12 + "type": "string", 13 + "description": "Subscription ticket from `.ticket`. Required if no Authorization header." 14 + }, 15 + "topic": { 16 + "type": "string", 17 + "description": "Optional: restrict the stream to a single topic (must be in the ticket). Required on the JWT path." 18 + }, 19 + "collection": { 20 + "type": "string", 21 + "format": "nsid", 22 + "description": "Optional: drop `record.*` events whose collection doesn't match." 23 + } 24 + } 25 + }, 26 + "errors": [ 27 + { 28 + "name": "AuthRequired" 29 + }, 30 + { 31 + "name": "Forbidden" 32 + }, 33 + { 34 + "name": "InvalidRequest" 35 + }, 36 + { 37 + "name": "NotFound" 38 + } 39 + ] 40 + } 41 + } 42 + }
+64
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/realtime/ticket.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.realtime.ticket", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Mint a short-lived subscription ticket for the given topic. The caller's access is checked and — for `community:<did>` topics — expanded to the concrete list of `space:<uri>` topics the caller can see. Server-side callers may skip tickets and use their JWT directly on `.subscribe`.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "topic" 14 + ], 15 + "properties": { 16 + "topic": { 17 + "type": "string", 18 + "description": "Requested topic. One of `space:<uri>`, `community:<did>`, `actor:<did>` (self-only in v1)." 19 + } 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "required": [ 28 + "ticket", 29 + "topics", 30 + "expiresAt" 31 + ], 32 + "properties": { 33 + "ticket": { 34 + "type": "string", 35 + "description": "HMAC-signed opaque token. Passed as `?ticket=` on `.subscribe`." 36 + }, 37 + "topics": { 38 + "type": "array", 39 + "items": { 40 + "type": "string" 41 + }, 42 + "description": "Concrete delivery topics authorized by this ticket." 43 + }, 44 + "expiresAt": { 45 + "type": "integer", 46 + "description": "Unix ms." 47 + } 48 + } 49 + } 50 + }, 51 + "errors": [ 52 + { 53 + "name": "Forbidden" 54 + }, 55 + { 56 + "name": "NotFound" 57 + }, 58 + { 59 + "name": "NotSupported" 60 + } 61 + ] 62 + } 63 + } 64 + }
+184
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/getRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.server.getRecord", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a single tools.atmo.chat.server record by AT URI", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "uri" 12 + ], 13 + "properties": { 14 + "uri": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "AT URI of the record" 18 + }, 19 + "profiles": { 20 + "type": "boolean", 21 + "description": "Include profile + identity info keyed by DID" 22 + }, 23 + "spaceUri": { 24 + "type": "string", 25 + "format": "at-uri", 26 + "description": "If set, fetch from this permissioned space (requires service-auth JWT or a read-grant invite token)." 27 + }, 28 + "inviteToken": { 29 + "type": "string", 30 + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." 31 + } 32 + } 33 + }, 34 + "output": { 35 + "encoding": "application/json", 36 + "schema": { 37 + "type": "object", 38 + "required": [ 39 + "uri", 40 + "did", 41 + "collection", 42 + "rkey", 43 + "time_us" 44 + ], 45 + "properties": { 46 + "uri": { 47 + "type": "string", 48 + "format": "at-uri" 49 + }, 50 + "did": { 51 + "type": "string", 52 + "format": "did" 53 + }, 54 + "collection": { 55 + "type": "string", 56 + "format": "nsid" 57 + }, 58 + "rkey": { 59 + "type": "string" 60 + }, 61 + "cid": { 62 + "type": "string" 63 + }, 64 + "record": { 65 + "type": "ref", 66 + "ref": "tools.atmo.chat.server#main" 67 + }, 68 + "time_us": { 69 + "type": "integer" 70 + }, 71 + "space": { 72 + "type": "string", 73 + "format": "at-uri", 74 + "description": "Present when the record was read from a permissioned space; its value is the space URI." 75 + }, 76 + "profiles": { 77 + "type": "array", 78 + "items": { 79 + "type": "ref", 80 + "ref": "#profileEntry" 81 + } 82 + } 83 + } 84 + } 85 + } 86 + }, 87 + "profileEntry": { 88 + "type": "object", 89 + "required": [ 90 + "did" 91 + ], 92 + "properties": { 93 + "did": { 94 + "type": "string", 95 + "format": "did" 96 + }, 97 + "handle": { 98 + "type": "string" 99 + }, 100 + "uri": { 101 + "type": "string", 102 + "format": "at-uri" 103 + }, 104 + "collection": { 105 + "type": "string", 106 + "format": "nsid" 107 + }, 108 + "rkey": { 109 + "type": "string" 110 + }, 111 + "cid": { 112 + "type": "string" 113 + }, 114 + "record": { 115 + "type": "ref", 116 + "ref": "#appBskyActorProfile" 117 + } 118 + } 119 + }, 120 + "appBskyActorProfile": { 121 + "type": "object", 122 + "properties": { 123 + "avatar": { 124 + "type": "blob", 125 + "accept": [ 126 + "image/png", 127 + "image/jpeg" 128 + ], 129 + "maxSize": 1000000, 130 + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 131 + }, 132 + "banner": { 133 + "type": "blob", 134 + "accept": [ 135 + "image/png", 136 + "image/jpeg" 137 + ], 138 + "maxSize": 1000000, 139 + "description": "Larger horizontal image to display behind profile view." 140 + }, 141 + "labels": { 142 + "refs": [ 143 + "com.atproto.label.defs#selfLabels" 144 + ], 145 + "type": "union", 146 + "description": "Self-label values, specific to the Bluesky application, on the overall account." 147 + }, 148 + "website": { 149 + "type": "string", 150 + "format": "uri" 151 + }, 152 + "pronouns": { 153 + "type": "string", 154 + "maxLength": 200, 155 + "description": "Free-form pronouns text.", 156 + "maxGraphemes": 20 157 + }, 158 + "createdAt": { 159 + "type": "string", 160 + "format": "datetime" 161 + }, 162 + "pinnedPost": { 163 + "ref": "com.atproto.repo.strongRef", 164 + "type": "ref" 165 + }, 166 + "description": { 167 + "type": "string", 168 + "maxLength": 2560, 169 + "description": "Free-form profile description text.", 170 + "maxGraphemes": 256 171 + }, 172 + "displayName": { 173 + "type": "string", 174 + "maxLength": 640, 175 + "maxGraphemes": 64 176 + }, 177 + "joinedViaStarterPack": { 178 + "ref": "com.atproto.repo.strongRef", 179 + "type": "ref" 180 + } 181 + } 182 + } 183 + } 184 + }
+251
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/listRecords.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.server.listRecords", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Query tools.atmo.chat.server records with filters", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "limit": { 12 + "type": "integer", 13 + "minimum": 1, 14 + "maximum": 200, 15 + "default": 50 16 + }, 17 + "cursor": { 18 + "type": "string" 19 + }, 20 + "actor": { 21 + "type": "string", 22 + "format": "at-identifier", 23 + "description": "Filter by DID or handle (triggers on-demand backfill)" 24 + }, 25 + "profiles": { 26 + "type": "boolean", 27 + "description": "Include profile + identity info keyed by DID" 28 + }, 29 + "spaceUri": { 30 + "type": "string", 31 + "format": "at-uri", 32 + "description": "If set, query records inside this permissioned space (requires service-auth JWT or a read-grant invite token)." 33 + }, 34 + "byUser": { 35 + "type": "string", 36 + "format": "did", 37 + "description": "Only used with spaceUri — filter to records authored by this DID." 38 + }, 39 + "inviteToken": { 40 + "type": "string", 41 + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." 42 + }, 43 + "communityDid": { 44 + "type": "string", 45 + "description": "Filter by communityDid" 46 + }, 47 + "name": { 48 + "type": "string", 49 + "description": "Filter by name" 50 + }, 51 + "description": { 52 + "type": "string", 53 + "description": "Filter by description" 54 + }, 55 + "createdAtMin": { 56 + "type": "string", 57 + "description": "Minimum value for createdAt" 58 + }, 59 + "createdAtMax": { 60 + "type": "string", 61 + "description": "Maximum value for createdAt" 62 + }, 63 + "sort": { 64 + "type": "string", 65 + "knownValues": [ 66 + "communityDid", 67 + "name", 68 + "description", 69 + "createdAt" 70 + ], 71 + "description": "Field to sort by (default: time_us)" 72 + }, 73 + "order": { 74 + "type": "string", 75 + "knownValues": [ 76 + "asc", 77 + "desc" 78 + ], 79 + "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" 80 + } 81 + } 82 + }, 83 + "output": { 84 + "encoding": "application/json", 85 + "schema": { 86 + "type": "object", 87 + "required": [ 88 + "records" 89 + ], 90 + "properties": { 91 + "records": { 92 + "type": "array", 93 + "items": { 94 + "type": "ref", 95 + "ref": "#record" 96 + } 97 + }, 98 + "cursor": { 99 + "type": "string" 100 + }, 101 + "profiles": { 102 + "type": "array", 103 + "items": { 104 + "type": "ref", 105 + "ref": "#profileEntry" 106 + } 107 + } 108 + } 109 + } 110 + } 111 + }, 112 + "record": { 113 + "type": "object", 114 + "required": [ 115 + "uri", 116 + "did", 117 + "collection", 118 + "rkey", 119 + "time_us" 120 + ], 121 + "properties": { 122 + "uri": { 123 + "type": "string", 124 + "format": "at-uri" 125 + }, 126 + "did": { 127 + "type": "string", 128 + "format": "did" 129 + }, 130 + "collection": { 131 + "type": "string", 132 + "format": "nsid" 133 + }, 134 + "rkey": { 135 + "type": "string" 136 + }, 137 + "cid": { 138 + "type": "string" 139 + }, 140 + "record": { 141 + "type": "ref", 142 + "ref": "tools.atmo.chat.server#main" 143 + }, 144 + "time_us": { 145 + "type": "integer" 146 + }, 147 + "space": { 148 + "type": "string", 149 + "format": "at-uri", 150 + "description": "Present when the record was read from a permissioned space; its value is the space URI." 151 + } 152 + } 153 + }, 154 + "profileEntry": { 155 + "type": "object", 156 + "required": [ 157 + "did" 158 + ], 159 + "properties": { 160 + "did": { 161 + "type": "string", 162 + "format": "did" 163 + }, 164 + "handle": { 165 + "type": "string" 166 + }, 167 + "uri": { 168 + "type": "string", 169 + "format": "at-uri" 170 + }, 171 + "collection": { 172 + "type": "string", 173 + "format": "nsid" 174 + }, 175 + "rkey": { 176 + "type": "string" 177 + }, 178 + "cid": { 179 + "type": "string" 180 + }, 181 + "record": { 182 + "type": "ref", 183 + "ref": "#appBskyActorProfile" 184 + } 185 + } 186 + }, 187 + "appBskyActorProfile": { 188 + "type": "object", 189 + "properties": { 190 + "avatar": { 191 + "type": "blob", 192 + "accept": [ 193 + "image/png", 194 + "image/jpeg" 195 + ], 196 + "maxSize": 1000000, 197 + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 198 + }, 199 + "banner": { 200 + "type": "blob", 201 + "accept": [ 202 + "image/png", 203 + "image/jpeg" 204 + ], 205 + "maxSize": 1000000, 206 + "description": "Larger horizontal image to display behind profile view." 207 + }, 208 + "labels": { 209 + "refs": [ 210 + "com.atproto.label.defs#selfLabels" 211 + ], 212 + "type": "union", 213 + "description": "Self-label values, specific to the Bluesky application, on the overall account." 214 + }, 215 + "website": { 216 + "type": "string", 217 + "format": "uri" 218 + }, 219 + "pronouns": { 220 + "type": "string", 221 + "maxLength": 200, 222 + "description": "Free-form pronouns text.", 223 + "maxGraphemes": 20 224 + }, 225 + "createdAt": { 226 + "type": "string", 227 + "format": "datetime" 228 + }, 229 + "pinnedPost": { 230 + "ref": "com.atproto.repo.strongRef", 231 + "type": "ref" 232 + }, 233 + "description": { 234 + "type": "string", 235 + "maxLength": 2560, 236 + "description": "Free-form profile description text.", 237 + "maxGraphemes": 256 238 + }, 239 + "displayName": { 240 + "type": "string", 241 + "maxLength": 640, 242 + "maxGraphemes": 64 243 + }, 244 + "joinedViaStarterPack": { 245 + "ref": "com.atproto.repo.strongRef", 246 + "type": "ref" 247 + } 248 + } 249 + } 250 + } 251 + }
+52
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/addMember.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.addMember", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Add a member to a space. Caller must be the space owner.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "did" 15 + ], 16 + "properties": { 17 + "spaceUri": { 18 + "type": "string", 19 + "format": "at-uri" 20 + }, 21 + "did": { 22 + "type": "string", 23 + "format": "did" 24 + } 25 + } 26 + } 27 + }, 28 + "output": { 29 + "encoding": "application/json", 30 + "schema": { 31 + "type": "object", 32 + "required": [ 33 + "ok" 34 + ], 35 + "properties": { 36 + "ok": { 37 + "type": "boolean" 38 + } 39 + } 40 + } 41 + }, 42 + "errors": [ 43 + { 44 + "name": "NotFound" 45 + }, 46 + { 47 + "name": "Forbidden" 48 + } 49 + ] 50 + } 51 + } 52 + }
+55
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/createSpace.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.createSpace", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create a new space owned by the JWT issuer. The caller is added as an owner-perm member.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "properties": { 13 + "type": { 14 + "type": "string", 15 + "format": "nsid", 16 + "description": "Space type NSID. Defaults to the service's configured type." 17 + }, 18 + "key": { 19 + "type": "string", 20 + "description": "Space key. Auto-generated (TID) if omitted." 21 + }, 22 + "appPolicyRef": { 23 + "type": "string", 24 + "format": "at-uri" 25 + }, 26 + "appPolicy": { 27 + "type": "ref", 28 + "ref": "tools.atmo.chat.space.defs#appPolicy" 29 + } 30 + } 31 + } 32 + }, 33 + "output": { 34 + "encoding": "application/json", 35 + "schema": { 36 + "type": "object", 37 + "required": [ 38 + "space" 39 + ], 40 + "properties": { 41 + "space": { 42 + "type": "ref", 43 + "ref": "tools.atmo.chat.space.defs#spaceView" 44 + } 45 + } 46 + } 47 + }, 48 + "errors": [ 49 + { 50 + "name": "AlreadyExists" 51 + } 52 + ] 53 + } 54 + } 55 + }
+210
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.defs", 4 + "description": "Shared types for permissioned-space XRPC methods.", 5 + "defs": { 6 + "spaceView": { 7 + "type": "object", 8 + "required": [ 9 + "uri", 10 + "ownerDid", 11 + "type", 12 + "key", 13 + "serviceDid", 14 + "createdAt" 15 + ], 16 + "properties": { 17 + "uri": { 18 + "type": "string", 19 + "format": "at-uri" 20 + }, 21 + "ownerDid": { 22 + "type": "string", 23 + "format": "did" 24 + }, 25 + "type": { 26 + "type": "string", 27 + "format": "nsid" 28 + }, 29 + "key": { 30 + "type": "string" 31 + }, 32 + "serviceDid": { 33 + "type": "string" 34 + }, 35 + "appPolicyRef": { 36 + "type": "string", 37 + "format": "at-uri" 38 + }, 39 + "createdAt": { 40 + "type": "integer" 41 + }, 42 + "appPolicy": { 43 + "type": "ref", 44 + "ref": "#appPolicy", 45 + "description": "Owner-only" 46 + } 47 + } 48 + }, 49 + "memberView": { 50 + "type": "object", 51 + "required": [ 52 + "did", 53 + "addedAt" 54 + ], 55 + "properties": { 56 + "did": { 57 + "type": "string", 58 + "format": "did" 59 + }, 60 + "addedAt": { 61 + "type": "integer" 62 + }, 63 + "addedBy": { 64 + "type": "string", 65 + "format": "did" 66 + } 67 + } 68 + }, 69 + "recordView": { 70 + "type": "object", 71 + "required": [ 72 + "spaceUri", 73 + "collection", 74 + "authorDid", 75 + "rkey", 76 + "record", 77 + "createdAt" 78 + ], 79 + "properties": { 80 + "spaceUri": { 81 + "type": "string", 82 + "format": "at-uri" 83 + }, 84 + "collection": { 85 + "type": "string", 86 + "format": "nsid" 87 + }, 88 + "authorDid": { 89 + "type": "string", 90 + "format": "did" 91 + }, 92 + "rkey": { 93 + "type": "string" 94 + }, 95 + "cid": { 96 + "type": "string", 97 + "format": "cid" 98 + }, 99 + "record": { 100 + "type": "unknown" 101 + }, 102 + "createdAt": { 103 + "type": "integer" 104 + } 105 + } 106 + }, 107 + "appPolicy": { 108 + "type": "object", 109 + "required": [ 110 + "mode", 111 + "apps" 112 + ], 113 + "properties": { 114 + "mode": { 115 + "type": "string", 116 + "knownValues": [ 117 + "allow", 118 + "deny" 119 + ], 120 + "description": "'allow' = default-allow with apps[] as denylist; 'deny' = default-deny with apps[] as allowlist." 121 + }, 122 + "apps": { 123 + "type": "array", 124 + "items": { 125 + "type": "string" 126 + } 127 + } 128 + } 129 + }, 130 + "blobInfo": { 131 + "type": "object", 132 + "required": [ 133 + "cid", 134 + "mimeType", 135 + "size", 136 + "authorDid", 137 + "createdAt" 138 + ], 139 + "properties": { 140 + "cid": { 141 + "type": "string", 142 + "format": "cid" 143 + }, 144 + "mimeType": { 145 + "type": "string" 146 + }, 147 + "size": { 148 + "type": "integer" 149 + }, 150 + "authorDid": { 151 + "type": "string", 152 + "format": "did" 153 + }, 154 + "createdAt": { 155 + "type": "integer" 156 + } 157 + } 158 + }, 159 + "inviteView": { 160 + "type": "object", 161 + "required": [ 162 + "tokenHash", 163 + "spaceUri", 164 + "kind", 165 + "usedCount", 166 + "createdBy", 167 + "createdAt" 168 + ], 169 + "properties": { 170 + "tokenHash": { 171 + "type": "string" 172 + }, 173 + "spaceUri": { 174 + "type": "string", 175 + "format": "at-uri" 176 + }, 177 + "kind": { 178 + "type": "string", 179 + "knownValues": [ 180 + "join", 181 + "read", 182 + "read-join" 183 + ] 184 + }, 185 + "expiresAt": { 186 + "type": "integer" 187 + }, 188 + "maxUses": { 189 + "type": "integer" 190 + }, 191 + "usedCount": { 192 + "type": "integer" 193 + }, 194 + "createdBy": { 195 + "type": "string", 196 + "format": "did" 197 + }, 198 + "createdAt": { 199 + "type": "integer" 200 + }, 201 + "revokedAt": { 202 + "type": "integer" 203 + }, 204 + "note": { 205 + "type": "string" 206 + } 207 + } 208 + } 209 + } 210 + }
+56
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/deleteRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.deleteRecord", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Delete a record from a space. Callers can delete their own records; the space owner can delete any record (via a separate admin path).", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "collection", 15 + "rkey" 16 + ], 17 + "properties": { 18 + "spaceUri": { 19 + "type": "string", 20 + "format": "at-uri" 21 + }, 22 + "collection": { 23 + "type": "string", 24 + "format": "nsid" 25 + }, 26 + "rkey": { 27 + "type": "string" 28 + } 29 + } 30 + } 31 + }, 32 + "output": { 33 + "encoding": "application/json", 34 + "schema": { 35 + "type": "object", 36 + "required": [ 37 + "ok" 38 + ], 39 + "properties": { 40 + "ok": { 41 + "type": "boolean" 42 + } 43 + } 44 + } 45 + }, 46 + "errors": [ 47 + { 48 + "name": "NotFound" 49 + }, 50 + { 51 + "name": "Forbidden" 52 + } 53 + ] 54 + } 55 + } 56 + }
+42
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getBlob.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.getBlob", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Read a blob from a space. Requires read access via a service-auth JWT or a read-grant invite token.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri", 12 + "cid" 13 + ], 14 + "properties": { 15 + "spaceUri": { 16 + "type": "string", 17 + "format": "at-uri" 18 + }, 19 + "cid": { 20 + "type": "string", 21 + "format": "cid" 22 + }, 23 + "inviteToken": { 24 + "type": "string", 25 + "description": "Read-grant invite token for anonymous bearer access." 26 + } 27 + } 28 + }, 29 + "output": { 30 + "encoding": "*/*" 31 + }, 32 + "errors": [ 33 + { 34 + "name": "NotFound" 35 + }, 36 + { 37 + "name": "Forbidden" 38 + } 39 + ] 40 + } 41 + } 42 + }
+63
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.getRecord", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a single record from a space.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri", 12 + "collection", 13 + "author", 14 + "rkey" 15 + ], 16 + "properties": { 17 + "spaceUri": { 18 + "type": "string", 19 + "format": "at-uri" 20 + }, 21 + "collection": { 22 + "type": "string", 23 + "format": "nsid" 24 + }, 25 + "author": { 26 + "type": "string", 27 + "format": "did" 28 + }, 29 + "rkey": { 30 + "type": "string" 31 + }, 32 + "inviteToken": { 33 + "type": "string", 34 + "description": "Read-grant invite token. When supplied, replaces JWT auth for this read." 35 + } 36 + } 37 + }, 38 + "output": { 39 + "encoding": "application/json", 40 + "schema": { 41 + "type": "object", 42 + "required": [ 43 + "record" 44 + ], 45 + "properties": { 46 + "record": { 47 + "type": "ref", 48 + "ref": "tools.atmo.chat.space.defs#recordView" 49 + } 50 + } 51 + } 52 + }, 53 + "errors": [ 54 + { 55 + "name": "NotFound" 56 + }, 57 + { 58 + "name": "Forbidden" 59 + } 60 + ] 61 + } 62 + } 63 + }
+49
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getSpace.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.getSpace", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get metadata for a single space. Caller must be a member, the owner, or hold a read-grant invite token.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "uri" 12 + ], 13 + "properties": { 14 + "uri": { 15 + "type": "string", 16 + "format": "at-uri" 17 + }, 18 + "inviteToken": { 19 + "type": "string", 20 + "description": "Read-grant invite token. When supplied, replaces JWT auth for this read." 21 + } 22 + } 23 + }, 24 + "output": { 25 + "encoding": "application/json", 26 + "schema": { 27 + "type": "object", 28 + "required": [ 29 + "space" 30 + ], 31 + "properties": { 32 + "space": { 33 + "type": "ref", 34 + "ref": "tools.atmo.chat.space.defs#spaceView" 35 + } 36 + } 37 + } 38 + }, 39 + "errors": [ 40 + { 41 + "name": "NotFound" 42 + }, 43 + { 44 + "name": "Forbidden" 45 + } 46 + ] 47 + } 48 + } 49 + }
+48
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/leaveSpace.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.leaveSpace", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Remove the caller from a space's member list. The owner cannot leave — they must delete the space instead.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri" 14 + ], 15 + "properties": { 16 + "spaceUri": { 17 + "type": "string", 18 + "format": "at-uri" 19 + } 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "required": [ 28 + "ok" 29 + ], 30 + "properties": { 31 + "ok": { 32 + "type": "boolean" 33 + } 34 + } 35 + } 36 + }, 37 + "errors": [ 38 + { 39 + "name": "NotFound" 40 + }, 41 + { 42 + "name": "InvalidRequest", 43 + "description": "Raised if the caller is the space owner." 44 + } 45 + ] 46 + } 47 + } 48 + }
+57
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listBlobs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.listBlobs", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List blob metadata for a space. Members only.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri" 12 + ], 13 + "properties": { 14 + "spaceUri": { 15 + "type": "string", 16 + "format": "at-uri" 17 + }, 18 + "byUser": { 19 + "type": "string", 20 + "format": "did", 21 + "description": "Only blobs uploaded by this DID." 22 + }, 23 + "limit": { 24 + "type": "integer", 25 + "minimum": 1, 26 + "maximum": 200, 27 + "default": 50 28 + }, 29 + "cursor": { 30 + "type": "string" 31 + } 32 + } 33 + }, 34 + "output": { 35 + "encoding": "application/json", 36 + "schema": { 37 + "type": "object", 38 + "required": [ 39 + "blobs" 40 + ], 41 + "properties": { 42 + "blobs": { 43 + "type": "array", 44 + "items": { 45 + "type": "ref", 46 + "ref": "tools.atmo.chat.space.defs#blobInfo" 47 + } 48 + }, 49 + "cursor": { 50 + "type": "string" 51 + } 52 + } 53 + } 54 + } 55 + } 56 + } 57 + }
+48
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listMembers.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.listMembers", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List members of a space. Caller must be a member or the owner.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri" 12 + ], 13 + "properties": { 14 + "spaceUri": { 15 + "type": "string", 16 + "format": "at-uri" 17 + } 18 + } 19 + }, 20 + "output": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "required": [ 25 + "members" 26 + ], 27 + "properties": { 28 + "members": { 29 + "type": "array", 30 + "items": { 31 + "type": "ref", 32 + "ref": "tools.atmo.chat.space.defs#memberView" 33 + } 34 + } 35 + } 36 + } 37 + }, 38 + "errors": [ 39 + { 40 + "name": "NotFound" 41 + }, 42 + { 43 + "name": "Forbidden" 44 + } 45 + ] 46 + } 47 + } 48 + }
+74
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listRecords.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.listRecords", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List records of a given collection within a space. Access is governed by the space's collection policy.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri", 12 + "collection" 13 + ], 14 + "properties": { 15 + "spaceUri": { 16 + "type": "string", 17 + "format": "at-uri" 18 + }, 19 + "collection": { 20 + "type": "string", 21 + "format": "nsid" 22 + }, 23 + "byUser": { 24 + "type": "string", 25 + "format": "did", 26 + "description": "Only return records authored by this DID." 27 + }, 28 + "cursor": { 29 + "type": "string" 30 + }, 31 + "limit": { 32 + "type": "integer", 33 + "minimum": 1, 34 + "maximum": 200, 35 + "default": 50 36 + }, 37 + "inviteToken": { 38 + "type": "string", 39 + "description": "Read-grant invite token. When supplied, replaces JWT auth for this read." 40 + } 41 + } 42 + }, 43 + "output": { 44 + "encoding": "application/json", 45 + "schema": { 46 + "type": "object", 47 + "required": [ 48 + "records" 49 + ], 50 + "properties": { 51 + "records": { 52 + "type": "array", 53 + "items": { 54 + "type": "ref", 55 + "ref": "tools.atmo.chat.space.defs#recordView" 56 + } 57 + }, 58 + "cursor": { 59 + "type": "string" 60 + } 61 + } 62 + } 63 + }, 64 + "errors": [ 65 + { 66 + "name": "NotFound" 67 + }, 68 + { 69 + "name": "Forbidden" 70 + } 71 + ] 72 + } 73 + } 74 + }
+62
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listSpaces.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.listSpaces", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List spaces the caller has access to. Default scope is 'member' (spaces the caller is a member of, including owned); 'owner' lists only spaces the caller owns. When scope='member', the optional 'owner' param narrows to spaces owned by that DID — useful for listing all channels in a specific community the caller can access.", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "scope": { 12 + "type": "string", 13 + "knownValues": [ 14 + "member", 15 + "owner" 16 + ], 17 + "default": "member" 18 + }, 19 + "type": { 20 + "type": "string", 21 + "format": "nsid" 22 + }, 23 + "owner": { 24 + "type": "string", 25 + "format": "did", 26 + "description": "With scope=member, filter to spaces owned by this DID. Ignored when scope=owner." 27 + }, 28 + "cursor": { 29 + "type": "string" 30 + }, 31 + "limit": { 32 + "type": "integer", 33 + "minimum": 1, 34 + "maximum": 200, 35 + "default": 50 36 + } 37 + } 38 + }, 39 + "output": { 40 + "encoding": "application/json", 41 + "schema": { 42 + "type": "object", 43 + "required": [ 44 + "spaces" 45 + ], 46 + "properties": { 47 + "spaces": { 48 + "type": "array", 49 + "items": { 50 + "type": "ref", 51 + "ref": "tools.atmo.chat.space.defs#spaceView" 52 + } 53 + }, 54 + "cursor": { 55 + "type": "string" 56 + } 57 + } 58 + } 59 + } 60 + } 61 + } 62 + }
+68
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/putRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.putRecord", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Write a record into a space. The author is always the JWT issuer. If rkey is omitted, a TID is generated.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "collection", 15 + "record" 16 + ], 17 + "properties": { 18 + "spaceUri": { 19 + "type": "string", 20 + "format": "at-uri" 21 + }, 22 + "collection": { 23 + "type": "string", 24 + "format": "nsid" 25 + }, 26 + "rkey": { 27 + "type": "string" 28 + }, 29 + "record": { 30 + "type": "unknown" 31 + } 32 + } 33 + } 34 + }, 35 + "output": { 36 + "encoding": "application/json", 37 + "schema": { 38 + "type": "object", 39 + "required": [ 40 + "rkey", 41 + "authorDid", 42 + "createdAt" 43 + ], 44 + "properties": { 45 + "rkey": { 46 + "type": "string" 47 + }, 48 + "authorDid": { 49 + "type": "string", 50 + "format": "did" 51 + }, 52 + "createdAt": { 53 + "type": "integer" 54 + } 55 + } 56 + } 57 + }, 58 + "errors": [ 59 + { 60 + "name": "NotFound" 61 + }, 62 + { 63 + "name": "Forbidden" 64 + } 65 + ] 66 + } 67 + } 68 + }
+55
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/removeMember.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.removeMember", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Remove a member from a space. Owner only. Cannot remove the owner.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "did" 15 + ], 16 + "properties": { 17 + "spaceUri": { 18 + "type": "string", 19 + "format": "at-uri" 20 + }, 21 + "did": { 22 + "type": "string", 23 + "format": "did" 24 + } 25 + } 26 + } 27 + }, 28 + "output": { 29 + "encoding": "application/json", 30 + "schema": { 31 + "type": "object", 32 + "required": [ 33 + "ok" 34 + ], 35 + "properties": { 36 + "ok": { 37 + "type": "boolean" 38 + } 39 + } 40 + } 41 + }, 42 + "errors": [ 43 + { 44 + "name": "NotFound" 45 + }, 46 + { 47 + "name": "Forbidden" 48 + }, 49 + { 50 + "name": "InvalidRequest" 51 + } 52 + ] 53 + } 54 + } 55 + }
+50
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/uploadBlob.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.uploadBlob", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Upload a blob into a space. Returns a standard atproto BlobRef that records in this space can then reference. The record will be rejected at putRecord time if it references a blob that was not uploaded to the same space.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri" 12 + ], 13 + "properties": { 14 + "spaceUri": { 15 + "type": "string", 16 + "format": "at-uri" 17 + } 18 + } 19 + }, 20 + "input": { 21 + "encoding": "*/*" 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "required": [ 28 + "blob" 29 + ], 30 + "properties": { 31 + "blob": { 32 + "type": "blob" 33 + } 34 + } 35 + } 36 + }, 37 + "errors": [ 38 + { 39 + "name": "Forbidden" 40 + }, 41 + { 42 + "name": "BlobTooLarge" 43 + }, 44 + { 45 + "name": "InvalidMimeType" 46 + } 47 + ] 48 + } 49 + } 50 + }
+45
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/whoami.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.whoami", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Report the caller's relationship to a space: whether they are the owner and/or a member. Useful for clients to avoid a listMembers roundtrip.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri" 12 + ], 13 + "properties": { 14 + "spaceUri": { 15 + "type": "string", 16 + "format": "at-uri" 17 + } 18 + } 19 + }, 20 + "output": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "required": [ 25 + "isOwner", 26 + "isMember" 27 + ], 28 + "properties": { 29 + "isOwner": { 30 + "type": "boolean" 31 + }, 32 + "isMember": { 33 + "type": "boolean" 34 + } 35 + } 36 + } 37 + }, 38 + "errors": [ 39 + { 40 + "name": "NotFound" 41 + } 42 + ] 43 + } 44 + } 45 + }
+10
examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts
··· 1 + import type { PageServerLoad } from './$types'; 2 + import { error } from '@sveltejs/kit'; 3 + import { buildSpaceUri } from '$lib/rooms/uri'; 4 + 5 + export const load: PageServerLoad = async ({ locals, params }) => { 6 + if (!locals.did || !locals.client) error(401, 'Not authenticated'); 7 + const communityDid = decodeURIComponent(params.communityDid); 8 + const spaceUri = buildSpaceUri(communityDid, params.channelKey); 9 + return { spaceUri, channelKey: params.channelKey, myDid: locals.did }; 10 + };
+144
examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte
··· 1 + <script lang="ts"> 2 + import { tick } from 'svelte'; 3 + import { Button, Input, Navbar } from '@foxui/core'; 4 + import { RelativeTime } from '@foxui/time'; 5 + import { postMessage } from '$lib/rooms/rooms.remote'; 6 + import { setCurrentChannel } from '$lib/rooms/realtime.svelte'; 7 + import { markLastRead } from '$lib/rooms/unread.svelte'; 8 + import { displayName } from '$lib/rooms/profiles.svelte'; 9 + import { createWatchQuery } from '$lib/rooms/watch.svelte'; 10 + import type { WatchRecord } from '@atmo-dev/contrail/sync'; 11 + import { dev } from '$app/environment'; 12 + 13 + let { data } = $props(); 14 + 15 + let channel = $derived(data.channels.find((c) => c.key === data.channelKey)); 16 + let channelName = $derived(channel?.name ?? data.channelKey); 17 + 18 + // Live message feed via contrail's watchRecords subscription. The engine 19 + // handles the snapshot + live merge; we just render its `records` array. 20 + // Sorted newest-first by default — flip to oldest-first for chat. 21 + let query = $derived.by(() => 22 + createWatchQuery({ 23 + url: `/xrpc/tools.atmo.chat.message.watchRecords?spaceUri=${encodeURIComponent(data.spaceUri)}&limit=50`, 24 + // In prod the server has a DurableObjectPubSub — open a DO-terminated 25 + // WS so idle connections hibernate (near-zero cost at CF scale). 26 + // In dev the server uses InMemoryPubSub (one isolate), which only 27 + // speaks SSE, so downgrade. 28 + transport: dev ? 'sse' : 'ws', 29 + compareRecords: (a: WatchRecord, b: WatchRecord) => 30 + (a.time_us ?? 0) - (b.time_us ?? 0) 31 + }) 32 + ); 33 + 34 + // Teardown on channel change. 35 + $effect(() => { 36 + return () => query.stop(); 37 + }); 38 + 39 + // Unread / current-channel bookkeeping. 40 + $effect(() => { 41 + setCurrentChannel(data.spaceUri); 42 + markLastRead(data.spaceUri); 43 + return () => setCurrentChannel(null); 44 + }); 45 + 46 + // Project records to the shape the existing template expects. 47 + let messages = $derived( 48 + query.records.map((r) => { 49 + const rec = r.record as { text?: string; createdAt?: string; replyTo?: string }; 50 + return { 51 + rkey: r.rkey, 52 + authorDid: r.did, 53 + text: rec.text ?? '', 54 + createdAt: rec.createdAt ?? '', 55 + replyTo: rec.replyTo 56 + }; 57 + }) 58 + ); 59 + 60 + let scrollEl: HTMLDivElement | null = $state(null); 61 + let text = $state(''); 62 + let sending = $state(false); 63 + let sendErr = $state<string | null>(null); 64 + 65 + $effect(() => { 66 + void messages.length; 67 + tick().then(() => { 68 + if (scrollEl) scrollEl.scrollTop = scrollEl.scrollHeight; 69 + }); 70 + }); 71 + 72 + async function send(e: SubmitEvent) { 73 + e.preventDefault(); 74 + const t = text.trim(); 75 + if (!t) return; 76 + sending = true; 77 + sendErr = null; 78 + try { 79 + await postMessage({ spaceUri: data.spaceUri, text: t }); 80 + text = ''; 81 + } catch (err) { 82 + sendErr = err instanceof Error ? err.message : String(err); 83 + } finally { 84 + sending = false; 85 + } 86 + } 87 + 88 + $effect(() => { 89 + if (messages.length > 0) { 90 + markLastRead(data.spaceUri); 91 + } 92 + }); 93 + </script> 94 + 95 + <div class="flex min-h-0 flex-1 flex-col pb-20"> 96 + <div bind:this={scrollEl} class="flex-1 overflow-y-auto px-4 py-6"> 97 + {#if query.status === 'connecting' || query.status === 'snapshot'} 98 + <div class="text-base-500 text-center text-sm">Loading…</div> 99 + {:else if messages.length === 0} 100 + <div class="text-base-500 text-center text-sm">No messages yet. Say hi.</div> 101 + {:else} 102 + <ul class="flex flex-col gap-2"> 103 + {#each messages as m (m.rkey)} 104 + <li 105 + class="max-w-2xl {m.authorDid === data.myDid ? 'self-end text-right' : 'self-start'}" 106 + > 107 + <div class="text-base-500 text-xs"> 108 + <span class="font-medium">{displayName(m.authorDid)}</span> 109 + · 110 + <RelativeTime date={new Date(m.createdAt)} locale="en-US" /> 111 + </div> 112 + <div 113 + class="{m.authorDid === data.myDid 114 + ? 'bg-accent-500 text-white' 115 + : 'bg-base-200 dark:bg-base-800'} mt-1 inline-block rounded-2xl px-3 py-2 text-sm whitespace-pre-wrap" 116 + > 117 + {m.text} 118 + </div> 119 + </li> 120 + {/each} 121 + </ul> 122 + {/if} 123 + </div> 124 + </div> 125 + 126 + <Navbar hasSidebar class="top-auto bottom-1"> 127 + <form onsubmit={send} class="flex w-full items-center gap-2"> 128 + <Input 129 + class="flex-1" 130 + maxlength={4000} 131 + placeholder={`message #${channelName}`} 132 + bind:value={text} 133 + /> 134 + <Button type="submit" disabled={sending || !text.trim()}> 135 + {sending ? '…' : 'Send'} 136 + </Button> 137 + </form> 138 + </Navbar> 139 + 140 + {#if sendErr} 141 + <div class="fixed right-4 bottom-20 z-50 rounded-xl border border-red-500 bg-white p-2 text-xs text-red-500 dark:bg-black"> 142 + {sendErr} 143 + </div> 144 + {/if}
+93
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/create.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.invite.create", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create an invite for a community-owned space. Caller must have manager+ on the target space and cannot grant a higher level than their own. Returns the raw token once; only its SHA-256 is stored.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "accessLevel" 15 + ], 16 + "properties": { 17 + "spaceUri": { 18 + "type": "string", 19 + "format": "at-uri" 20 + }, 21 + "accessLevel": { 22 + "type": "ref", 23 + "ref": "tools.atmo.chat.community.defs#accessLevel" 24 + }, 25 + "expiresAt": { 26 + "type": "integer", 27 + "description": "Unix ms. Omit for no expiry." 28 + }, 29 + "maxUses": { 30 + "type": "integer", 31 + "minimum": 1, 32 + "description": "Omit for unlimited redemptions." 33 + }, 34 + "note": { 35 + "type": "string", 36 + "maxLength": 500 37 + } 38 + } 39 + } 40 + }, 41 + "output": { 42 + "encoding": "application/json", 43 + "schema": { 44 + "type": "object", 45 + "required": [ 46 + "token", 47 + "tokenHash", 48 + "spaceUri", 49 + "accessLevel", 50 + "createdAt" 51 + ], 52 + "properties": { 53 + "token": { 54 + "type": "string", 55 + "description": "Raw token. Shown once — cannot be retrieved later." 56 + }, 57 + "tokenHash": { 58 + "type": "string" 59 + }, 60 + "spaceUri": { 61 + "type": "string", 62 + "format": "at-uri" 63 + }, 64 + "accessLevel": { 65 + "type": "ref", 66 + "ref": "tools.atmo.chat.community.defs#accessLevel" 67 + }, 68 + "expiresAt": { 69 + "type": "integer" 70 + }, 71 + "maxUses": { 72 + "type": "integer" 73 + }, 74 + "createdAt": { 75 + "type": "integer" 76 + } 77 + } 78 + } 79 + }, 80 + "errors": [ 81 + { 82 + "name": "InvalidRequest" 83 + }, 84 + { 85 + "name": "Forbidden" 86 + }, 87 + { 88 + "name": "NotFound" 89 + } 90 + ] 91 + } 92 + } 93 + }
+52
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/list.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.invite.list", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List invites for a community-owned space. Caller must have manager+ on the space. Raw tokens are never returned — tokenHash is the stable id.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri" 12 + ], 13 + "properties": { 14 + "spaceUri": { 15 + "type": "string", 16 + "format": "at-uri" 17 + }, 18 + "includeRevoked": { 19 + "type": "boolean", 20 + "default": false 21 + } 22 + } 23 + }, 24 + "output": { 25 + "encoding": "application/json", 26 + "schema": { 27 + "type": "object", 28 + "required": [ 29 + "invites" 30 + ], 31 + "properties": { 32 + "invites": { 33 + "type": "array", 34 + "items": { 35 + "type": "ref", 36 + "ref": "tools.atmo.chat.community.defs#inviteView" 37 + } 38 + } 39 + } 40 + } 41 + }, 42 + "errors": [ 43 + { 44 + "name": "Forbidden" 45 + }, 46 + { 47 + "name": "NotFound" 48 + } 49 + ] 50 + } 51 + } 52 + }
+60
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/redeem.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.invite.redeem", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Redeem a community invite. The caller's DID (from service-auth) is granted the access level encoded in the invite. Atomic — a token is consumed only if the invite is currently usable.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "token" 14 + ], 15 + "properties": { 16 + "token": { 17 + "type": "string" 18 + } 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "required": [ 27 + "spaceUri", 28 + "accessLevel", 29 + "communityDid" 30 + ], 31 + "properties": { 32 + "spaceUri": { 33 + "type": "string", 34 + "format": "at-uri" 35 + }, 36 + "accessLevel": { 37 + "type": "ref", 38 + "ref": "tools.atmo.chat.community.defs#accessLevel" 39 + }, 40 + "communityDid": { 41 + "type": "string", 42 + "format": "did" 43 + } 44 + } 45 + } 46 + }, 47 + "errors": [ 48 + { 49 + "name": "InvalidRequest" 50 + }, 51 + { 52 + "name": "InvalidToken" 53 + }, 54 + { 55 + "name": "NotFound" 56 + } 57 + ] 58 + } 59 + } 60 + }
+46
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/revoke.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.invite.revoke", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Revoke an invite. Caller must be the invite's creator or have manager+ on the target space.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "tokenHash" 14 + ], 15 + "properties": { 16 + "tokenHash": { 17 + "type": "string" 18 + } 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "required": [ 27 + "ok" 28 + ], 29 + "properties": { 30 + "ok": { 31 + "type": "boolean" 32 + } 33 + } 34 + } 35 + }, 36 + "errors": [ 37 + { 38 + "name": "NotFound" 39 + }, 40 + { 41 + "name": "Forbidden" 42 + } 43 + ] 44 + } 45 + } 46 + }
+58
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/create.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.space.create", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create a community-owned space (group/role/channel). Caller needs `admin` or higher in the community's `$admin` space. Reserved keys (`$admin`, `$publishers`, …) are rejected here.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "communityDid" 14 + ], 15 + "properties": { 16 + "communityDid": { 17 + "type": "string", 18 + "format": "did" 19 + }, 20 + "key": { 21 + "type": "string", 22 + "description": "Space key. Auto-generated if omitted." 23 + } 24 + } 25 + } 26 + }, 27 + "output": { 28 + "encoding": "application/json", 29 + "schema": { 30 + "type": "object", 31 + "required": [ 32 + "space" 33 + ], 34 + "properties": { 35 + "space": { 36 + "type": "ref", 37 + "ref": "tools.atmo.chat.community.defs#spaceView" 38 + } 39 + } 40 + } 41 + }, 42 + "errors": [ 43 + { 44 + "name": "InvalidRequest" 45 + }, 46 + { 47 + "name": "NotFound" 48 + }, 49 + { 50 + "name": "Forbidden" 51 + }, 52 + { 53 + "name": "AlreadyExists" 54 + } 55 + ] 56 + } 57 + } 58 + }
+47
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/delete.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.space.delete", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Soft-delete a community-owned space. Caller must be `owner` on the space, or `admin`+ in the community's `$admin` space. Reserved spaces cannot be deleted.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri" 14 + ], 15 + "properties": { 16 + "spaceUri": { 17 + "type": "string", 18 + "format": "at-uri" 19 + } 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "required": [ 28 + "ok" 29 + ], 30 + "properties": { 31 + "ok": { 32 + "type": "boolean" 33 + } 34 + } 35 + } 36 + }, 37 + "errors": [ 38 + { 39 + "name": "NotFound" 40 + }, 41 + { 42 + "name": "Forbidden" 43 + } 44 + ] 45 + } 46 + } 47 + }
+59
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/deleteRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.space.deleteRecord", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Delete an in-space record authored by the community DID. Caller must be `admin` or higher in the target space.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "collection", 15 + "rkey" 16 + ], 17 + "properties": { 18 + "spaceUri": { 19 + "type": "string", 20 + "format": "at-uri" 21 + }, 22 + "collection": { 23 + "type": "string", 24 + "format": "nsid" 25 + }, 26 + "rkey": { 27 + "type": "string" 28 + } 29 + } 30 + } 31 + }, 32 + "output": { 33 + "encoding": "application/json", 34 + "schema": { 35 + "type": "object", 36 + "required": [ 37 + "ok" 38 + ], 39 + "properties": { 40 + "ok": { 41 + "type": "boolean" 42 + } 43 + } 44 + } 45 + }, 46 + "errors": [ 47 + { 48 + "name": "InvalidRequest" 49 + }, 50 + { 51 + "name": "NotFound" 52 + }, 53 + { 54 + "name": "Forbidden" 55 + } 56 + ] 57 + } 58 + } 59 + }
+60
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/grant.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.space.grant", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Grant or upsert a subject's access level on a space. Caller needs `manager` or higher; the granted level cannot exceed the caller's own. A subject can be a DID (member) or another space (delegated membership).", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "subject", 15 + "accessLevel" 16 + ], 17 + "properties": { 18 + "spaceUri": { 19 + "type": "string", 20 + "format": "at-uri" 21 + }, 22 + "subject": { 23 + "type": "ref", 24 + "ref": "tools.atmo.chat.community.defs#subject" 25 + }, 26 + "accessLevel": { 27 + "type": "ref", 28 + "ref": "tools.atmo.chat.community.defs#accessLevel" 29 + } 30 + } 31 + } 32 + }, 33 + "output": { 34 + "encoding": "application/json", 35 + "schema": { 36 + "type": "object", 37 + "required": [ 38 + "ok" 39 + ], 40 + "properties": { 41 + "ok": { 42 + "type": "boolean" 43 + } 44 + } 45 + } 46 + }, 47 + "errors": [ 48 + { 49 + "name": "InvalidRequest" 50 + }, 51 + { 52 + "name": "NotFound" 53 + }, 54 + { 55 + "name": "Forbidden" 56 + } 57 + ] 58 + } 59 + } 60 + }
+58
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/listMembers.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.space.listMembers", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List members of a community-owned space. By default returns the raw access-level rows; with `flatten=true` returns the flattened DID list (delegated memberships resolved).", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri" 12 + ], 13 + "properties": { 14 + "spaceUri": { 15 + "type": "string", 16 + "format": "at-uri" 17 + }, 18 + "flatten": { 19 + "type": "boolean", 20 + "description": "If true, return the flat DID list instead of raw rows." 21 + } 22 + } 23 + }, 24 + "output": { 25 + "encoding": "application/json", 26 + "schema": { 27 + "type": "object", 28 + "properties": { 29 + "rows": { 30 + "type": "array", 31 + "items": { 32 + "type": "ref", 33 + "ref": "tools.atmo.chat.community.defs#memberRow" 34 + }, 35 + "description": "Present when flatten=false." 36 + }, 37 + "members": { 38 + "type": "array", 39 + "items": { 40 + "type": "ref", 41 + "ref": "tools.atmo.chat.community.defs#flatMember" 42 + }, 43 + "description": "Present when flatten=true." 44 + } 45 + } 46 + } 47 + }, 48 + "errors": [ 49 + { 50 + "name": "NotFound" 51 + }, 52 + { 53 + "name": "Forbidden" 54 + } 55 + ] 56 + } 57 + } 58 + }
+71
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/putRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.space.putRecord", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Write an in-space record authored by the community DID (rather than the caller). Caller must be `admin` or higher in the target space.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "collection", 15 + "record" 16 + ], 17 + "properties": { 18 + "spaceUri": { 19 + "type": "string", 20 + "format": "at-uri" 21 + }, 22 + "collection": { 23 + "type": "string", 24 + "format": "nsid" 25 + }, 26 + "rkey": { 27 + "type": "string" 28 + }, 29 + "record": { 30 + "type": "unknown" 31 + } 32 + } 33 + } 34 + }, 35 + "output": { 36 + "encoding": "application/json", 37 + "schema": { 38 + "type": "object", 39 + "required": [ 40 + "rkey", 41 + "authorDid", 42 + "createdAt" 43 + ], 44 + "properties": { 45 + "rkey": { 46 + "type": "string" 47 + }, 48 + "authorDid": { 49 + "type": "string", 50 + "format": "did" 51 + }, 52 + "createdAt": { 53 + "type": "integer" 54 + } 55 + } 56 + } 57 + }, 58 + "errors": [ 59 + { 60 + "name": "InvalidRequest" 61 + }, 62 + { 63 + "name": "NotFound" 64 + }, 65 + { 66 + "name": "Forbidden" 67 + } 68 + ] 69 + } 70 + } 71 + }
+47
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/resync.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.space.resync", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Manually recompute `spaces_members` for a community-owned space. Useful after a crash mid-reconcile leaves stale membership. Caller must have `admin` or higher.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri" 14 + ], 15 + "properties": { 16 + "spaceUri": { 17 + "type": "string", 18 + "format": "at-uri" 19 + } 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "required": [ 28 + "ok" 29 + ], 30 + "properties": { 31 + "ok": { 32 + "type": "boolean" 33 + } 34 + } 35 + } 36 + }, 37 + "errors": [ 38 + { 39 + "name": "NotFound" 40 + }, 41 + { 42 + "name": "Forbidden" 43 + } 44 + ] 45 + } 46 + } 47 + }
+55
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/revoke.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.space.revoke", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Remove a subject's access grant from a space. Caller must have `manager` or higher and outrank the subject's current level.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "subject" 15 + ], 16 + "properties": { 17 + "spaceUri": { 18 + "type": "string", 19 + "format": "at-uri" 20 + }, 21 + "subject": { 22 + "type": "ref", 23 + "ref": "tools.atmo.chat.community.defs#subject" 24 + } 25 + } 26 + } 27 + }, 28 + "output": { 29 + "encoding": "application/json", 30 + "schema": { 31 + "type": "object", 32 + "required": [ 33 + "ok" 34 + ], 35 + "properties": { 36 + "ok": { 37 + "type": "boolean" 38 + } 39 + } 40 + } 41 + }, 42 + "errors": [ 43 + { 44 + "name": "InvalidRequest" 45 + }, 46 + { 47 + "name": "NotFound" 48 + }, 49 + { 50 + "name": "Forbidden" 51 + } 52 + ] 53 + } 54 + } 55 + }
+60
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/setAccessLevel.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.community.space.setAccessLevel", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Change the access level of an existing grant. Caller must outrank both the old and new level.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "subject", 15 + "accessLevel" 16 + ], 17 + "properties": { 18 + "spaceUri": { 19 + "type": "string", 20 + "format": "at-uri" 21 + }, 22 + "subject": { 23 + "type": "ref", 24 + "ref": "tools.atmo.chat.community.defs#subject" 25 + }, 26 + "accessLevel": { 27 + "type": "ref", 28 + "ref": "tools.atmo.chat.community.defs#accessLevel" 29 + } 30 + } 31 + } 32 + }, 33 + "output": { 34 + "encoding": "application/json", 35 + "schema": { 36 + "type": "object", 37 + "required": [ 38 + "ok" 39 + ], 40 + "properties": { 41 + "ok": { 42 + "type": "boolean" 43 + } 44 + } 45 + } 46 + }, 47 + "errors": [ 48 + { 49 + "name": "InvalidRequest" 50 + }, 51 + { 52 + "name": "NotFound" 53 + }, 54 + { 55 + "name": "Forbidden" 56 + } 57 + ] 58 + } 59 + } 60 + }
+76
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/create.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.invite.create", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create an invite for a space. Caller must be the space owner. Returns the raw token once; only the hash is stored.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri" 14 + ], 15 + "properties": { 16 + "spaceUri": { 17 + "type": "string", 18 + "format": "at-uri" 19 + }, 20 + "kind": { 21 + "type": "string", 22 + "knownValues": [ 23 + "join", 24 + "read", 25 + "read-join" 26 + ], 27 + "default": "join", 28 + "description": "join: redeem to become a member. read: bearer-only read access, no membership. read-join: anonymous read + signed-in redeem to join." 29 + }, 30 + "expiresAt": { 31 + "type": "integer", 32 + "description": "Unix ms timestamp. Omit for no expiry." 33 + }, 34 + "maxUses": { 35 + "type": "integer", 36 + "minimum": 1, 37 + "description": "Caps join redemptions only — read-token reads are unlimited. Omit for unlimited joins." 38 + }, 39 + "note": { 40 + "type": "string", 41 + "maxLength": 500 42 + } 43 + } 44 + } 45 + }, 46 + "output": { 47 + "encoding": "application/json", 48 + "schema": { 49 + "type": "object", 50 + "required": [ 51 + "token", 52 + "invite" 53 + ], 54 + "properties": { 55 + "token": { 56 + "type": "string", 57 + "description": "Raw token. Shown once — cannot be retrieved later." 58 + }, 59 + "invite": { 60 + "type": "ref", 61 + "ref": "tools.atmo.chat.space.defs#inviteView" 62 + } 63 + } 64 + } 65 + }, 66 + "errors": [ 67 + { 68 + "name": "NotFound" 69 + }, 70 + { 71 + "name": "Forbidden" 72 + } 73 + ] 74 + } 75 + } 76 + }
+52
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/list.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.invite.list", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List invites for a space. Owner only.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri" 12 + ], 13 + "properties": { 14 + "spaceUri": { 15 + "type": "string", 16 + "format": "at-uri" 17 + }, 18 + "includeRevoked": { 19 + "type": "boolean", 20 + "default": false 21 + } 22 + } 23 + }, 24 + "output": { 25 + "encoding": "application/json", 26 + "schema": { 27 + "type": "object", 28 + "required": [ 29 + "invites" 30 + ], 31 + "properties": { 32 + "invites": { 33 + "type": "array", 34 + "items": { 35 + "type": "ref", 36 + "ref": "tools.atmo.chat.space.defs#inviteView" 37 + } 38 + } 39 + } 40 + } 41 + }, 42 + "errors": [ 43 + { 44 + "name": "NotFound" 45 + }, 46 + { 47 + "name": "Forbidden" 48 + } 49 + ] 50 + } 51 + } 52 + }
+44
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/redeem.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.invite.redeem", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Redeem an invite token. The JWT issuer becomes a member of the space.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "token" 14 + ], 15 + "properties": { 16 + "token": { 17 + "type": "string" 18 + } 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "required": [ 27 + "spaceUri" 28 + ], 29 + "properties": { 30 + "spaceUri": { 31 + "type": "string", 32 + "format": "at-uri" 33 + } 34 + } 35 + } 36 + }, 37 + "errors": [ 38 + { 39 + "name": "InvalidInvite" 40 + } 41 + ] 42 + } 43 + } 44 + }
+51
examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/revoke.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.atmo.chat.space.invite.revoke", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Revoke an invite. Owner only. Invites are identified by their tokenHash (visible in list output).", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "tokenHash" 15 + ], 16 + "properties": { 17 + "spaceUri": { 18 + "type": "string", 19 + "format": "at-uri" 20 + }, 21 + "tokenHash": { 22 + "type": "string" 23 + } 24 + } 25 + } 26 + }, 27 + "output": { 28 + "encoding": "application/json", 29 + "schema": { 30 + "type": "object", 31 + "required": [ 32 + "ok" 33 + ], 34 + "properties": { 35 + "ok": { 36 + "type": "boolean" 37 + } 38 + } 39 + } 40 + }, 41 + "errors": [ 42 + { 43 + "name": "NotFound" 44 + }, 45 + { 46 + "name": "Forbidden" 47 + } 48 + ] 49 + } 50 + } 51 + }
+50
examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.server.ts
··· 1 + import type { PageServerLoad } from './$types'; 2 + import { error } from '@sveltejs/kit'; 3 + import type { Client } from '@atcute/client'; 4 + import { listCommunityInvites } from '$lib/rooms/server'; 5 + import { buildMembersUri } from '$lib/rooms/uri'; 6 + 7 + export const load: PageServerLoad = async ({ locals, params, platform }) => { 8 + if (!locals.did || !locals.client) error(401, 'Not authenticated'); 9 + const communityDid = decodeURIComponent(params.communityDid); 10 + const membersUri = buildMembersUri(communityDid); 11 + 12 + let invites: Array<{ 13 + id: string; 14 + tokenHash: string; 15 + accessLevel: string; 16 + createdBy: string; 17 + createdAt: number; 18 + expiresAt: number | null; 19 + maxUses: number | null; 20 + usedCount: number; 21 + revoked: boolean; 22 + note: string | null; 23 + }> = []; 24 + try { 25 + const res = await listCommunityInvites( 26 + { 27 + env: platform!.env, 28 + client: locals.client as Client, 29 + did: locals.did as string 30 + }, 31 + { spaceUri: membersUri, includeRevoked: true } 32 + ); 33 + invites = res.invites.map((r) => ({ 34 + id: r.tokenHash.slice(0, 12), 35 + tokenHash: r.tokenHash, 36 + accessLevel: r.accessLevel, 37 + createdBy: r.createdBy, 38 + createdAt: r.createdAt, 39 + expiresAt: r.expiresAt, 40 + maxUses: r.maxUses, 41 + usedCount: r.usedCount, 42 + revoked: r.revokedAt != null, 43 + note: r.note 44 + })); 45 + } catch { 46 + // ignore — page renders with empty list 47 + } 48 + 49 + return { communityDid, membersUri, invites }; 50 + };
+154
examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.svelte
··· 1 + <script lang="ts"> 2 + import { invalidateAll } from '$app/navigation'; 3 + import { Button, Heading, Input } from '@foxui/core'; 4 + import { RelativeTime } from '@foxui/time'; 5 + import { createInvite, revokeInvite } from '$lib/rooms/invites.remote'; 6 + 7 + let { data } = $props(); 8 + 9 + let expiresMinutes = $state<number | ''>(60 * 24); // 1 day 10 + let maxUses = $state<number | ''>(1); 11 + let note = $state(''); 12 + let creating = $state(false); 13 + let lastCreated = $state<{ token: string; url: string } | null>(null); 14 + let err = $state<string | null>(null); 15 + 16 + async function create(e: SubmitEvent) { 17 + e.preventDefault(); 18 + creating = true; 19 + err = null; 20 + try { 21 + const res = await createInvite({ 22 + spaceUri: data.membersUri, 23 + expiresInMinutes: expiresMinutes === '' ? undefined : Number(expiresMinutes), 24 + maxUses: maxUses === '' ? undefined : Number(maxUses), 25 + note: note.trim() || undefined 26 + }); 27 + const url = `${window.location.origin}/join/${res.token}`; 28 + lastCreated = { token: res.token, url }; 29 + note = ''; 30 + await invalidateAll(); 31 + } catch (e) { 32 + err = e instanceof Error ? e.message : String(e); 33 + } finally { 34 + creating = false; 35 + } 36 + } 37 + 38 + async function revoke(tokenHash: string) { 39 + if (!confirm('Revoke this invite? Existing unused tokens will stop working.')) return; 40 + try { 41 + await revokeInvite({ tokenHash }); 42 + await invalidateAll(); 43 + } catch (e) { 44 + console.error(e); 45 + } 46 + } 47 + 48 + function copy(text: string) { 49 + void navigator.clipboard.writeText(text); 50 + } 51 + </script> 52 + 53 + <div class="flex-1 overflow-y-auto p-6"> 54 + <div class="mx-auto max-w-2xl"> 55 + <Heading>Invites</Heading> 56 + <p class="text-base-500 mt-1 text-sm"> 57 + Share an invite link to let new members join. They'll be added to this server's 58 + <code>members</code> role on redemption. 59 + </p> 60 + 61 + <form onsubmit={create} class="mt-6 grid grid-cols-2 gap-3"> 62 + <label class="flex flex-col gap-1 text-sm"> 63 + Expires in (minutes) 64 + <Input type="number" min="1" bind:value={expiresMinutes} /> 65 + </label> 66 + <label class="flex flex-col gap-1 text-sm"> 67 + Max uses (blank = unlimited) 68 + <Input type="number" min="1" bind:value={maxUses} /> 69 + </label> 70 + <label class="col-span-2 flex flex-col gap-1 text-sm"> 71 + Note (optional, only visible to admins) 72 + <Input bind:value={note} maxlength={500} /> 73 + </label> 74 + {#if err} 75 + <div class="col-span-2 rounded-xl border border-red-500 p-2 text-xs text-red-500"> 76 + {err} 77 + </div> 78 + {/if} 79 + <div class="col-span-2"> 80 + <Button type="submit" disabled={creating}> 81 + {creating ? 'Creating…' : 'Create invite link'} 82 + </Button> 83 + </div> 84 + </form> 85 + 86 + {#if lastCreated} 87 + <div 88 + class="bg-accent-50 dark:bg-accent-950/40 border-accent-500/30 mt-4 flex items-center gap-3 rounded-2xl border p-3" 89 + > 90 + <div class="min-w-0 flex-1"> 91 + <div class="text-xs font-semibold uppercase">New invite</div> 92 + <div class="mt-1 truncate font-mono text-xs">{lastCreated.url}</div> 93 + </div> 94 + <Button size="sm" onclick={() => copy(lastCreated!.url)}>Copy</Button> 95 + </div> 96 + {/if} 97 + 98 + <div class="mt-8"> 99 + <h2 class="mb-3 text-sm font-semibold uppercase">Active invites</h2> 100 + {#if data.invites.length === 0} 101 + <p class="text-base-500 text-sm">No invites yet.</p> 102 + {:else} 103 + <ul class="flex flex-col gap-2"> 104 + {#each data.invites as inv (inv.id)} 105 + <li 106 + class="bg-base-100 dark:bg-base-900 flex items-center justify-between rounded-2xl border p-3" 107 + > 108 + <div class="min-w-0"> 109 + <div class="flex items-center gap-2"> 110 + <span class="font-mono text-xs">{inv.id}</span> 111 + {#if inv.revoked} 112 + <span 113 + class="rounded-full bg-red-500/10 px-2 py-0.5 text-xs text-red-500" 114 + > 115 + revoked 116 + </span> 117 + {:else if inv.expiresAt && inv.expiresAt < Date.now()} 118 + <span 119 + class="bg-base-300 dark:bg-base-700 rounded-full px-2 py-0.5 text-xs" 120 + > 121 + expired 122 + </span> 123 + {:else if inv.maxUses != null && inv.usedCount >= inv.maxUses} 124 + <span 125 + class="bg-base-300 dark:bg-base-700 rounded-full px-2 py-0.5 text-xs" 126 + > 127 + used up 128 + </span> 129 + {/if} 130 + </div> 131 + <div class="text-base-500 mt-1 text-xs"> 132 + {inv.usedCount} 133 + {inv.maxUses != null ? `/ ${inv.maxUses}` : ''} used 134 + {#if inv.expiresAt} 135 + · expires <RelativeTime 136 + date={new Date(inv.expiresAt)} 137 + locale="en-US" 138 + /> 139 + {/if} 140 + {#if inv.note}· {inv.note}{/if} 141 + </div> 142 + </div> 143 + {#if !inv.revoked} 144 + <Button variant="ghost" size="sm" onclick={() => revoke(inv.tokenHash)}> 145 + Revoke 146 + </Button> 147 + {/if} 148 + </li> 149 + {/each} 150 + </ul> 151 + {/if} 152 + </div> 153 + </div> 154 + </div>
+49
examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/members/+page.server.ts
··· 1 + import type { PageServerLoad } from './$types'; 2 + import { error } from '@sveltejs/kit'; 3 + import type { Client } from '@atcute/client'; 4 + import { authedFetch } from '$lib/rooms/server'; 5 + import { buildMembersUri } from '$lib/rooms/uri'; 6 + 7 + interface MemberRow { 8 + did?: string; 9 + spaceUri?: string; 10 + accessLevel: string; 11 + grantedBy: string; 12 + grantedAt: number; 13 + } 14 + 15 + export const load: PageServerLoad = async ({ locals, params, platform }) => { 16 + if (!locals.did || !locals.client) error(401, 'Not authenticated'); 17 + const communityDid = decodeURIComponent(params.communityDid); 18 + const membersUri = buildMembersUri(communityDid); 19 + const ctx = { 20 + env: platform!.env, 21 + client: locals.client as Client, 22 + did: locals.did as string 23 + }; 24 + 25 + let members: MemberRow[] = []; 26 + try { 27 + const data = await authedFetch<{ 28 + rows?: Array<{ 29 + subject: { did?: string; spaceUri?: string }; 30 + accessLevel: string; 31 + grantedBy: string; 32 + grantedAt: number; 33 + }>; 34 + }>(ctx, 'tools.atmo.chat.community.space.listMembers', { 35 + query: { spaceUri: membersUri } 36 + }); 37 + members = (data.rows ?? []).map((r) => ({ 38 + did: r.subject.did, 39 + spaceUri: r.subject.spaceUri, 40 + accessLevel: r.accessLevel, 41 + grantedBy: r.grantedBy, 42 + grantedAt: r.grantedAt 43 + })); 44 + } catch { 45 + // empty list 46 + } 47 + 48 + return { communityDid, membersUri, members }; 49 + };
+142
examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/members/+page.svelte
··· 1 + <script lang="ts"> 2 + import { invalidateAll } from '$app/navigation'; 3 + import { Button, Input, Heading } from '@foxui/core'; 4 + import { RelativeTime } from '@foxui/time'; 5 + import { actorToDid } from '$lib/atproto/methods'; 6 + import type { Handle } from '@atcute/lexicons'; 7 + import { grantMember, revokeMember, setMemberLevel } from '$lib/rooms/rooms.remote'; 8 + import { displayName, handleOf } from '$lib/rooms/profiles.svelte'; 9 + 10 + let { data } = $props(); 11 + 12 + let handleInput = $state(''); 13 + let addingErr = $state<string | null>(null); 14 + let adding = $state(false); 15 + 16 + const LEVELS = ['member', 'manager', 'admin', 'owner'] as const; 17 + 18 + async function addMember(e: SubmitEvent) { 19 + e.preventDefault(); 20 + const h = handleInput.trim(); 21 + if (!h) return; 22 + adding = true; 23 + addingErr = null; 24 + try { 25 + let did: string; 26 + if (h.startsWith('did:')) { 27 + did = h; 28 + } else { 29 + did = await actorToDid(h as unknown as Handle); 30 + } 31 + await grantMember({ 32 + spaceUri: data.membersUri, 33 + did, 34 + accessLevel: 'member' 35 + }); 36 + handleInput = ''; 37 + await invalidateAll(); 38 + } catch (err) { 39 + addingErr = err instanceof Error ? err.message : String(err); 40 + } finally { 41 + adding = false; 42 + } 43 + } 44 + 45 + async function changeLevel(did: string, level: (typeof LEVELS)[number]) { 46 + try { 47 + await setMemberLevel({ 48 + spaceUri: data.membersUri, 49 + did, 50 + accessLevel: level 51 + }); 52 + await invalidateAll(); 53 + } catch (err) { 54 + console.error(err); 55 + } 56 + } 57 + 58 + async function remove(did: string) { 59 + if (!confirm(`Remove ${did.slice(0, 20)}… from this server?`)) return; 60 + try { 61 + await revokeMember({ spaceUri: data.membersUri, did }); 62 + await invalidateAll(); 63 + } catch (err) { 64 + console.error(err); 65 + } 66 + } 67 + </script> 68 + 69 + <div class="flex-1 overflow-y-auto p-6"> 70 + <div class="mx-auto max-w-2xl"> 71 + <Heading>Members</Heading> 72 + <p class="text-base-500 mt-1 text-sm"> 73 + Add members by handle (<code>alice.bsky.social</code>) or DID. Membership here gives access 74 + to all public channels. 75 + </p> 76 + 77 + <form onsubmit={addMember} class="mt-6 flex gap-2"> 78 + <Input 79 + bind:value={handleInput} 80 + placeholder="alice.bsky.social or did:plc:..." 81 + class="flex-1" 82 + /> 83 + <Button type="submit" disabled={adding || !handleInput.trim()}> 84 + {adding ? '…' : 'Add'} 85 + </Button> 86 + </form> 87 + {#if addingErr} 88 + <div class="mt-2 text-xs text-red-500">{addingErr}</div> 89 + {/if} 90 + 91 + <div class="mt-8"> 92 + {#if data.members.length === 0} 93 + <p class="text-base-500 text-sm">No members yet.</p> 94 + {:else} 95 + <ul class="flex flex-col gap-2"> 96 + {#each data.members as m (m.did ?? m.spaceUri)} 97 + <li 98 + class="bg-base-100 dark:bg-base-900 flex items-center justify-between rounded-2xl border p-3" 99 + > 100 + <div class="min-w-0"> 101 + {#if m.did} 102 + <div class="truncate text-sm font-medium">{displayName(m.did)}</div> 103 + {#if handleOf(m.did) && handleOf(m.did) !== displayName(m.did)} 104 + <div class="text-base-500 truncate text-xs">@{handleOf(m.did)}</div> 105 + {:else} 106 + <div class="text-base-400 truncate font-mono text-xs">{m.did}</div> 107 + {/if} 108 + {:else if m.spaceUri} 109 + <div class="truncate text-sm"> 110 + Role: <span class="font-mono">{m.spaceUri}</span> 111 + </div> 112 + {/if} 113 + <div class="text-base-500 mt-0.5 text-xs"> 114 + granted <RelativeTime date={new Date(m.grantedAt)} locale="en-US" /> 115 + </div> 116 + </div> 117 + <div class="flex items-center gap-2"> 118 + {#if m.did} 119 + <select 120 + class="bg-base-100 dark:bg-base-900 rounded-xl border px-2 py-1 text-sm" 121 + value={m.accessLevel} 122 + onchange={(e) => 123 + changeLevel(m.did!, (e.currentTarget as HTMLSelectElement).value as (typeof LEVELS)[number])} 124 + > 125 + {#each LEVELS as lv (lv)} 126 + <option value={lv}>{lv}</option> 127 + {/each} 128 + </select> 129 + <Button variant="ghost" size="sm" onclick={() => remove(m.did!)}> 130 + Remove 131 + </Button> 132 + {:else} 133 + <span class="text-base-500 text-sm">{m.accessLevel}</span> 134 + {/if} 135 + </div> 136 + </li> 137 + {/each} 138 + </ul> 139 + {/if} 140 + </div> 141 + </div> 142 + </div>