Monorepo for Wisp.place. A static site hosting service built on top of the AT Protocol.

Compare changes

Choose any two refs to compare.

+8
.dockerignore
··· 9 9 *.log 10 10 .vscode 11 11 .idea 12 + server 13 + .prettierrc 14 + testDeploy 15 + .tangled 16 + .crush 17 + .claude 18 + server 19 + hosting-service
+1
.gitignore
··· 16 16 17 17 # production 18 18 /build 19 + /result 19 20 20 21 # misc 21 22 .DS_Store
+10 -6
Dockerfile
··· 15 15 COPY public ./public 16 16 17 17 # Build the application (if needed) 18 - # RUN bun run build 18 + RUN bun build \ 19 + --compile \ 20 + --minify \ 21 + --outfile server \ 22 + src/index.ts 23 + 24 + FROM scratch AS runtime 25 + WORKDIR /app 26 + COPY --from=base /app/server /app/server 19 27 20 28 # Set environment variables (can be overridden at runtime) 21 29 ENV PORT=3000 ··· 24 32 # Expose the application port 25 33 EXPOSE 3000 26 34 27 - # Health check 28 - HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ 29 - CMD bun -e "fetch('http://localhost:3000/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))" 30 - 31 35 # Start the application 32 - CMD ["bun", "src/index.ts"] 36 + CMD ["./server"]
-41
api.md
··· 1 - /** 2 - * AUTHENTICATION ROUTES 3 - * 4 - * Handles OAuth authentication flow for Bluesky/ATProto accounts 5 - * All routes are on the editor.wisp.place subdomain 6 - * 7 - * Routes: 8 - * POST /api/auth/signin - Initiate OAuth sign-in flow 9 - * GET /api/auth/callback - OAuth callback handler (redirect from PDS) 10 - * GET /api/auth/status - Check current authentication status 11 - * POST /api/auth/logout - Sign out and clear session 12 - */ 13 - 14 - /** 15 - * CUSTOM DOMAIN ROUTES 16 - * 17 - * Handles custom domain (BYOD - Bring Your Own Domain) management 18 - * Users can claim custom domains with DNS verification (TXT + CNAME) 19 - * and map them to their sites 20 - * 21 - * Routes: 22 - * GET /api/check-domain - Fast verification check for routing (public) 23 - * GET /api/custom-domains - List user's custom domains 24 - * POST /api/custom-domains/check - Check domain availability and DNS config 25 - * POST /api/custom-domains/claim - Claim a custom domain 26 - * PUT /api/custom-domains/:id/site - Update site mapping 27 - * DELETE /api/custom-domains/:id - Remove a custom domain 28 - * POST /api/custom-domains/:id/verify - Manually trigger verification 29 - */ 30 - 31 - /** 32 - * WISP SITE MANAGEMENT ROUTES 33 - * 34 - * API endpoints for managing user's Wisp sites stored in ATProto repos 35 - * Handles reading site metadata, fetching content, updating sites, and uploads 36 - * All routes are on the editor.wisp.place subdomain 37 - * 38 - * Routes: 39 - * GET /wisp/sites - List all sites for authenticated user 40 - * POST /wisp/upload-files - Upload and deploy files as a site 41 - */
+179
bun.lock
··· 26 26 "lucide-react": "^0.546.0", 27 27 "react": "^19.2.0", 28 28 "react-dom": "^19.2.0", 29 + "react-shiki": "^0.9.0", 29 30 "tailwind-merge": "^3.3.1", 30 31 "tailwindcss": "4", 31 32 "tw-animate-css": "^1.4.0", ··· 279 280 280 281 "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], 281 282 283 + "@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="], 284 + 285 + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg=="], 286 + 287 + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA=="], 288 + 289 + "@shikijs/langs": ["@shikijs/langs@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A=="], 290 + 291 + "@shikijs/themes": ["@shikijs/themes@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ=="], 292 + 293 + "@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="], 294 + 295 + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], 296 + 282 297 "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="], 283 298 284 299 "@tanstack/query-core": ["@tanstack/query-core@5.90.2", "", {}, "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ=="], ··· 291 306 292 307 "@ts-morph/common": ["@ts-morph/common@0.25.0", "", { "dependencies": { "minimatch": "^9.0.4", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.9" } }, "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg=="], 293 308 309 + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], 310 + 311 + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 312 + 313 + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], 314 + 315 + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], 316 + 317 + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], 318 + 319 + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], 320 + 294 321 "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], 295 322 296 323 "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], ··· 298 325 "@types/react-dom": ["@types/react-dom@19.2.1", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A=="], 299 326 300 327 "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="], 328 + 329 + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], 330 + 331 + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], 301 332 302 333 "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], 303 334 ··· 347 378 348 379 "cborg": ["cborg@1.10.2", "", { "bin": { "cborg": "cli.js" } }, "sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug=="], 349 380 381 + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], 382 + 350 383 "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 351 384 385 + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], 386 + 387 + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], 388 + 389 + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], 390 + 391 + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], 392 + 352 393 "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], 353 394 354 395 "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], ··· 362 403 "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 363 404 364 405 "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 406 + 407 + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], 365 408 366 409 "commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], 367 410 ··· 378 421 "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 379 422 380 423 "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 424 + 425 + "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="], 381 426 382 427 "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], 383 428 429 + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], 430 + 384 431 "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], 385 432 386 433 "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 387 434 388 435 "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], 436 + 437 + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], 389 438 390 439 "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], 391 440 ··· 406 455 "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 407 456 408 457 "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], 458 + 459 + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], 409 460 410 461 "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], 411 462 ··· 453 504 454 505 "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 455 506 507 + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], 508 + 509 + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], 510 + 511 + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], 512 + 513 + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], 514 + 456 515 "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], 457 516 458 517 "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], ··· 463 522 464 523 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 465 524 525 + "inline-style-parser": ["inline-style-parser@0.2.6", "", {}, "sha512-gtGXVaBdl5mAes3rPcMedEBm12ibjt1kDMFfheul1wUAOVEJW60voNdMVzVkfLN06O7ZaD/rxhfKgtlgtTbMjg=="], 526 + 466 527 "ipaddr.js": ["ipaddr.js@2.2.0", "", {}, "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="], 467 528 468 529 "iron-session": ["iron-session@8.0.4", "", { "dependencies": { "cookie": "^0.7.2", "iron-webcrypto": "^1.2.1", "uncrypto": "^0.1.3" } }, "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA=="], 469 530 470 531 "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], 471 532 533 + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], 534 + 535 + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], 536 + 472 537 "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], 473 538 539 + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], 540 + 474 541 "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], 475 542 543 + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], 544 + 476 545 "iso-datestring-validator": ["iso-datestring-validator@2.2.2", "", {}, "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="], 477 546 478 547 "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], ··· 480 549 "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], 481 550 482 551 "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], 552 + 553 + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], 483 554 484 555 "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], 485 556 ··· 487 558 488 559 "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], 489 560 561 + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], 562 + 563 + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], 564 + 565 + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], 566 + 567 + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], 568 + 569 + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], 570 + 571 + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="], 572 + 573 + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], 574 + 575 + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], 576 + 490 577 "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], 491 578 492 579 "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], 493 580 494 581 "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], 495 582 583 + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], 584 + 585 + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], 586 + 587 + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], 588 + 589 + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], 590 + 591 + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], 592 + 593 + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], 594 + 595 + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], 596 + 597 + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], 598 + 599 + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], 600 + 601 + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], 602 + 603 + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], 604 + 605 + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], 606 + 607 + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], 608 + 609 + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], 610 + 611 + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], 612 + 613 + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], 614 + 615 + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], 616 + 617 + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], 618 + 619 + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], 620 + 621 + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], 622 + 623 + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], 624 + 496 625 "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], 497 626 498 627 "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], ··· 517 646 518 647 "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], 519 648 649 + "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], 650 + 651 + "oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="], 652 + 520 653 "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], 654 + 655 + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], 521 656 522 657 "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], 523 658 ··· 541 676 542 677 "process-warning": ["process-warning@3.0.0", "", {}, "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ=="], 543 678 679 + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], 680 + 544 681 "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], 545 682 546 683 "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], ··· 563 700 564 701 "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], 565 702 703 + "react-shiki": ["react-shiki@0.9.0", "", { "dependencies": { "clsx": "^2.1.1", "dequal": "^2.0.3", "hast-util-to-jsx-runtime": "^2.3.6", "shiki": "^3.11.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@types/react": ">=16.8.0", "@types/react-dom": ">=16.8.0", "react": ">= 16.8.0", "react-dom": ">= 16.8.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5t+vHGglJioG3LU6uTKFaiOC+KNW7haL8e22ZHSP7m174ZD/X2KgCVJcxvcUOM3FiqjPQD09AyS9/+RcOh3PmA=="], 704 + 566 705 "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], 567 706 568 707 "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], 569 708 570 709 "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], 571 710 711 + "regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="], 712 + 713 + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], 714 + 715 + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], 716 + 572 717 "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], 573 718 574 719 "require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="], ··· 589 734 590 735 "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], 591 736 737 + "shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="], 738 + 592 739 "shimmer": ["shimmer@1.2.1", "", {}, "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="], 593 740 594 741 "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], ··· 600 747 "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], 601 748 602 749 "sonic-boom": ["sonic-boom@3.8.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg=="], 750 + 751 + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], 603 752 604 753 "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], 605 754 ··· 609 758 610 759 "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], 611 760 761 + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], 762 + 612 763 "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 613 764 614 765 "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], 615 766 767 + "style-to-js": ["style-to-js@1.1.19", "", { "dependencies": { "style-to-object": "1.0.12" } }, "sha512-Ev+SgeqiNGT1ufsXyVC5RrJRXdrkRJ1Gol9Qw7Pb72YCKJXrBvP0ckZhBeVSrw2m06DJpei2528uIpjMb4TsoQ=="], 768 + 769 + "style-to-object": ["style-to-object@1.0.12", "", { "dependencies": { "inline-style-parser": "0.2.6" } }, "sha512-ddJqYnoT4t97QvN2C95bCgt+m7AAgXjVnkk/jxAfmp7EAB8nnqqZYEbMd3em7/vEomDb2LAQKAy1RFfv41mdNw=="], 770 + 616 771 "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], 617 772 618 773 "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], ··· 630 785 "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], 631 786 632 787 "token-types": ["token-types@6.1.1", "", { "dependencies": { "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ=="], 788 + 789 + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], 633 790 634 791 "ts-morph": ["ts-morph@24.0.0", "", { "dependencies": { "@ts-morph/common": "~0.25.0", "code-block-writer": "^13.0.3" } }, "sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw=="], 635 792 ··· 651 808 652 809 "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], 653 810 811 + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], 812 + 813 + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], 814 + 815 + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], 816 + 817 + "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], 818 + 819 + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], 820 + 654 821 "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], 655 822 656 823 "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], ··· 661 828 662 829 "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], 663 830 831 + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], 832 + 833 + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], 834 + 664 835 "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], 665 836 666 837 "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], ··· 677 848 678 849 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 679 850 851 + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], 852 + 680 853 "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 681 854 682 855 "express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], 683 856 684 857 "iron-session/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], 685 858 859 + "micromark/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 860 + 861 + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], 862 + 686 863 "proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], 687 864 688 865 "require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], ··· 692 869 "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 693 870 694 871 "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 872 + 873 + "micromark/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 695 874 696 875 "require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 697 876 }
+24
cli/.gitignore
··· 1 + .DS_STORE 2 + jacquard/ 3 + binaries/ 4 + # Generated by Cargo 5 + # will have compiled files and executables 6 + debug 7 + target 8 + 9 + # These are backup files generated by rustfmt 10 + **/*.rs.bk 11 + 12 + # MSVC Windows builds of rustc generate these, which store debugging information 13 + *.pdb 14 + 15 + # Generated by cargo mutants 16 + # Contains mutation testing data 17 + **/mutants.out*/ 18 + 19 + # RustRover 20 + # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 21 + # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 22 + # and can be added to the global gitignore or merged into this file. For a more nuclear 23 + # option (not recommended) you can uncomment the following to ignore the entire idea folder. 24 + #.idea/
+9 -151
cli/Cargo.lock
··· 922 922 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 923 923 924 924 [[package]] 925 - name = "foreign-types" 926 - version = "0.3.2" 927 - source = "registry+https://github.com/rust-lang/crates.io-index" 928 - checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 929 - dependencies = [ 930 - "foreign-types-shared", 931 - ] 932 - 933 - [[package]] 934 - name = "foreign-types-shared" 935 - version = "0.1.1" 936 - source = "registry+https://github.com/rust-lang/crates.io-index" 937 - checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 938 - 939 - [[package]] 940 925 name = "form_urlencoded" 941 926 version = "1.2.2" 942 927 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1340 1325 ] 1341 1326 1342 1327 [[package]] 1343 - name = "hyper-tls" 1344 - version = "0.6.0" 1345 - source = "registry+https://github.com/rust-lang/crates.io-index" 1346 - checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 1347 - dependencies = [ 1348 - "bytes", 1349 - "http-body-util", 1350 - "hyper", 1351 - "hyper-util", 1352 - "native-tls", 1353 - "tokio", 1354 - "tokio-native-tls", 1355 - "tower-service", 1356 - ] 1357 - 1358 - [[package]] 1359 1328 name = "hyper-util" 1360 1329 version = "0.1.17" 1361 1330 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1614 1583 [[package]] 1615 1584 name = "jacquard" 1616 1585 version = "0.9.0" 1586 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#b5cc9b35e38e24e1890ae55e700dcfad0d6d433a" 1617 1587 dependencies = [ 1618 1588 "bytes", 1619 1589 "getrandom 0.2.16", ··· 1641 1611 [[package]] 1642 1612 name = "jacquard-api" 1643 1613 version = "0.9.0" 1614 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#b5cc9b35e38e24e1890ae55e700dcfad0d6d433a" 1644 1615 dependencies = [ 1645 1616 "bon", 1646 1617 "bytes", ··· 1658 1629 [[package]] 1659 1630 name = "jacquard-common" 1660 1631 version = "0.9.0" 1632 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#b5cc9b35e38e24e1890ae55e700dcfad0d6d433a" 1661 1633 dependencies = [ 1662 1634 "base64 0.22.1", 1663 1635 "bon", ··· 1694 1666 [[package]] 1695 1667 name = "jacquard-derive" 1696 1668 version = "0.9.0" 1669 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#b5cc9b35e38e24e1890ae55e700dcfad0d6d433a" 1697 1670 dependencies = [ 1698 1671 "heck 0.5.0", 1699 1672 "jacquard-lexicon", ··· 1704 1677 1705 1678 [[package]] 1706 1679 name = "jacquard-identity" 1707 - version = "0.9.0" 1680 + version = "0.9.1" 1681 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#b5cc9b35e38e24e1890ae55e700dcfad0d6d433a" 1708 1682 dependencies = [ 1709 1683 "bon", 1710 1684 "bytes", ··· 1729 1703 1730 1704 [[package]] 1731 1705 name = "jacquard-lexicon" 1732 - version = "0.9.0" 1706 + version = "0.9.1" 1707 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#b5cc9b35e38e24e1890ae55e700dcfad0d6d433a" 1733 1708 dependencies = [ 1734 1709 "cid", 1735 1710 "dashmap", ··· 1755 1730 [[package]] 1756 1731 name = "jacquard-oauth" 1757 1732 version = "0.9.0" 1733 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#b5cc9b35e38e24e1890ae55e700dcfad0d6d433a" 1758 1734 dependencies = [ 1759 1735 "base64 0.22.1", 1760 1736 "bytes", ··· 2132 2108 ] 2133 2109 2134 2110 [[package]] 2135 - name = "native-tls" 2136 - version = "0.2.14" 2137 - source = "registry+https://github.com/rust-lang/crates.io-index" 2138 - checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 2139 - dependencies = [ 2140 - "libc", 2141 - "log", 2142 - "openssl", 2143 - "openssl-probe", 2144 - "openssl-sys", 2145 - "schannel", 2146 - "security-framework", 2147 - "security-framework-sys", 2148 - "tempfile", 2149 - ] 2150 - 2151 - [[package]] 2152 2111 name = "ndk-context" 2153 2112 version = "0.1.1" 2154 2113 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2288 2247 checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 2289 2248 2290 2249 [[package]] 2291 - name = "openssl" 2292 - version = "0.10.74" 2293 - source = "registry+https://github.com/rust-lang/crates.io-index" 2294 - checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" 2295 - dependencies = [ 2296 - "bitflags", 2297 - "cfg-if", 2298 - "foreign-types", 2299 - "libc", 2300 - "once_cell", 2301 - "openssl-macros", 2302 - "openssl-sys", 2303 - ] 2304 - 2305 - [[package]] 2306 - name = "openssl-macros" 2307 - version = "0.1.1" 2308 - source = "registry+https://github.com/rust-lang/crates.io-index" 2309 - checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 2310 - dependencies = [ 2311 - "proc-macro2", 2312 - "quote", 2313 - "syn 2.0.108", 2314 - ] 2315 - 2316 - [[package]] 2317 - name = "openssl-probe" 2318 - version = "0.1.6" 2319 - source = "registry+https://github.com/rust-lang/crates.io-index" 2320 - checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 2321 - 2322 - [[package]] 2323 - name = "openssl-sys" 2324 - version = "0.9.110" 2325 - source = "registry+https://github.com/rust-lang/crates.io-index" 2326 - checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" 2327 - dependencies = [ 2328 - "cc", 2329 - "libc", 2330 - "pkg-config", 2331 - "vcpkg", 2332 - ] 2333 - 2334 - [[package]] 2335 2250 name = "option-ext" 2336 2251 version = "0.2.0" 2337 2252 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2497 2412 "der", 2498 2413 "spki", 2499 2414 ] 2500 - 2501 - [[package]] 2502 - name = "pkg-config" 2503 - version = "0.3.32" 2504 - source = "registry+https://github.com/rust-lang/crates.io-index" 2505 - checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 2506 2415 2507 2416 [[package]] 2508 2417 name = "potential_utf" ··· 2827 2736 "http-body-util", 2828 2737 "hyper", 2829 2738 "hyper-rustls", 2830 - "hyper-tls", 2831 2739 "hyper-util", 2832 2740 "js-sys", 2833 2741 "log", 2834 2742 "mime", 2835 - "native-tls", 2836 2743 "percent-encoding", 2837 2744 "pin-project-lite", 2838 2745 "quinn", ··· 2843 2750 "serde_urlencoded", 2844 2751 "sync_wrapper", 2845 2752 "tokio", 2846 - "tokio-native-tls", 2847 2753 "tokio-rustls", 2848 2754 "tokio-util", 2849 2755 "tower", ··· 3019 2925 ] 3020 2926 3021 2927 [[package]] 3022 - name = "schannel" 3023 - version = "0.1.28" 3024 - source = "registry+https://github.com/rust-lang/crates.io-index" 3025 - checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" 3026 - dependencies = [ 3027 - "windows-sys 0.61.2", 3028 - ] 3029 - 3030 - [[package]] 3031 2928 name = "schemars" 3032 2929 version = "0.9.0" 3033 2930 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3069 2966 "pkcs8", 3070 2967 "subtle", 3071 2968 "zeroize", 3072 - ] 3073 - 3074 - [[package]] 3075 - name = "security-framework" 3076 - version = "2.11.1" 3077 - source = "registry+https://github.com/rust-lang/crates.io-index" 3078 - checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 3079 - dependencies = [ 3080 - "bitflags", 3081 - "core-foundation 0.9.4", 3082 - "core-foundation-sys", 3083 - "libc", 3084 - "security-framework-sys", 3085 - ] 3086 - 3087 - [[package]] 3088 - name = "security-framework-sys" 3089 - version = "2.15.0" 3090 - source = "registry+https://github.com/rust-lang/crates.io-index" 3091 - checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" 3092 - dependencies = [ 3093 - "core-foundation-sys", 3094 - "libc", 3095 2969 ] 3096 2970 3097 2971 [[package]] ··· 3698 3572 ] 3699 3573 3700 3574 [[package]] 3701 - name = "tokio-native-tls" 3702 - version = "0.3.1" 3703 - source = "registry+https://github.com/rust-lang/crates.io-index" 3704 - checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 3705 - dependencies = [ 3706 - "native-tls", 3707 - "tokio", 3708 - ] 3709 - 3710 - [[package]] 3711 3575 name = "tokio-rustls" 3712 3576 version = "0.26.4" 3713 3577 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3927 3791 version = "0.2.2" 3928 3792 source = "registry+https://github.com/rust-lang/crates.io-index" 3929 3793 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 3930 - 3931 - [[package]] 3932 - name = "vcpkg" 3933 - version = "0.2.15" 3934 - source = "registry+https://github.com/rust-lang/crates.io-index" 3935 - checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 3936 3794 3937 3795 [[package]] 3938 3796 name = "version_check"
+9 -8
cli/Cargo.toml
··· 8 8 place_wisp = [] 9 9 10 10 [dependencies] 11 - jacquard = { path = "jacquard/crates/jacquard", features = ["loopback"] } 12 - jacquard-oauth = { path = "jacquard/crates/jacquard-oauth" } 13 - jacquard-api = { path = "jacquard/crates/jacquard-api" } 14 - jacquard-common = { path = "jacquard/crates/jacquard-common" } 15 - jacquard-identity = { path = "jacquard/crates/jacquard-identity", features = ["dns"] } 16 - jacquard-derive = { path = "jacquard/crates/jacquard-derive" } 17 - jacquard-lexicon = { path = "jacquard/crates/jacquard-lexicon" } 11 + jacquard = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["loopback"] } 12 + jacquard-oauth = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 13 + jacquard-api = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 14 + jacquard-common = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 15 + jacquard-identity = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["dns"] } 16 + jacquard-derive = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 17 + jacquard-lexicon = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 18 18 clap = { version = "4.5.51", features = ["derive"] } 19 19 tokio = { version = "1.48", features = ["full"] } 20 20 miette = { version = "7.6.0", features = ["fancy"] } 21 21 serde_json = "1.0.145" 22 22 serde = { version = "1.0", features = ["derive"] } 23 23 shellexpand = "3.1.1" 24 - reqwest = "0.12" 24 + #reqwest = "0.12" 25 + reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } 25 26 rustversion = "1.0" 26 27 flate2 = "1.0" 27 28 base64 = "0.22"
+23
cli/build-linux.sh
··· 1 + #!/usr/bin/env bash 2 + # Build Linux binaries (statically linked) 3 + set -e 4 + mkdir -p binaries 5 + 6 + # Build Linux binaries 7 + echo "Building Linux binaries..." 8 + 9 + echo "Building Linux ARM64 (static)..." 10 + nix-shell -p rustup --run ' 11 + rustup target add aarch64-unknown-linux-musl 12 + RUSTFLAGS="-C target-feature=+crt-static" cargo zigbuild --release --target aarch64-unknown-linux-musl 13 + ' 14 + cp target/aarch64-unknown-linux-musl/release/wisp-cli binaries/wisp-cli-aarch64-linux 15 + 16 + echo "Building Linux x86_64 (static)..." 17 + nix-shell -p rustup --run ' 18 + rustup target add x86_64-unknown-linux-musl 19 + RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-unknown-linux-musl 20 + ' 21 + cp target/x86_64-unknown-linux-musl/release/wisp-cli binaries/wisp-cli-x86_64-linux 22 + 23 + echo "Done! Binaries in ./binaries/"
+15
cli/build-macos.sh
··· 1 + #!/bin/bash 2 + # Build Linux and macOS binaries 3 + 4 + set -e 5 + 6 + mkdir -p binaries 7 + rm -rf target 8 + 9 + # Build macOS binaries natively 10 + echo "Building macOS binaries..." 11 + rustup target add aarch64-apple-darwin 12 + 13 + echo "Building macOS arm64 binary." 14 + RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target aarch64-apple-darwin 15 + cp target/aarch64-apple-darwin/release/wisp-cli binaries/wisp-cli-macos-arm64
-14
cli/test_headers.rs
··· 1 - use http::Request; 2 - 3 - fn main() { 4 - let builder = Request::builder() 5 - .header(http::header::CONTENT_TYPE, "*/*") 6 - .header(http::header::CONTENT_TYPE, "application/octet-stream"); 7 - 8 - let req = builder.body(()).unwrap(); 9 - 10 - println!("Content-Type headers:"); 11 - for value in req.headers().get_all(http::header::CONTENT_TYPE) { 12 - println!(" {:?}", value); 13 - } 14 - }
+63
crates.nix
··· 1 + {...}: { 2 + perSystem = { 3 + pkgs, 4 + config, 5 + lib, 6 + inputs', 7 + ... 8 + }: { 9 + # declare projects 10 + nci.projects."wisp-place-cli" = { 11 + path = ./cli; 12 + export = false; 13 + }; 14 + nci.toolchains.mkBuild = _: 15 + with inputs'.fenix.packages; 16 + combine [ 17 + minimal.rustc 18 + minimal.cargo 19 + targets.x86_64-pc-windows-gnu.latest.rust-std 20 + targets.x86_64-unknown-linux-gnu.latest.rust-std 21 + targets.aarch64-apple-darwin.latest.rust-std 22 + ]; 23 + # configure crates 24 + nci.crates."wisp-cli" = { 25 + profiles = { 26 + dev.runTests = false; 27 + release.runTests = false; 28 + }; 29 + targets."x86_64-unknown-linux-gnu" = { 30 + default = true; 31 + }; 32 + targets."x86_64-pc-windows-gnu" = let 33 + targetPkgs = pkgs.pkgsCross.mingwW64; 34 + targetCC = targetPkgs.stdenv.cc; 35 + targetCargoEnvVarTarget = targetPkgs.stdenv.hostPlatform.rust.cargoEnvVarTarget; 36 + in rec { 37 + depsDrvConfig.mkDerivation = { 38 + nativeBuildInputs = [targetCC]; 39 + buildInputs = with targetPkgs; [windows.pthreads]; 40 + }; 41 + depsDrvConfig.env = rec { 42 + TARGET_CC = "${targetCC.targetPrefix}cc"; 43 + "CARGO_TARGET_${targetCargoEnvVarTarget}_LINKER" = TARGET_CC; 44 + }; 45 + drvConfig = depsDrvConfig; 46 + }; 47 + targets."aarch64-apple-darwin" = let 48 + targetPkgs = pkgs.pkgsCross.aarch64-darwin; 49 + targetCC = targetPkgs.stdenv.cc; 50 + targetCargoEnvVarTarget = targetPkgs.stdenv.hostPlatform.rust.cargoEnvVarTarget; 51 + in rec { 52 + depsDrvConfig.mkDerivation = { 53 + nativeBuildInputs = [targetCC]; 54 + }; 55 + depsDrvConfig.env = rec { 56 + TARGET_CC = "${targetCC.targetPrefix}cc"; 57 + "CARGO_TARGET_${targetCargoEnvVarTarget}_LINKER" = TARGET_CC; 58 + }; 59 + drvConfig = depsDrvConfig; 60 + }; 61 + }; 62 + }; 63 + }
+318
flake.lock
··· 1 + { 2 + "nodes": { 3 + "crane": { 4 + "flake": false, 5 + "locked": { 6 + "lastModified": 1758758545, 7 + "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", 8 + "owner": "ipetkov", 9 + "repo": "crane", 10 + "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", 11 + "type": "github" 12 + }, 13 + "original": { 14 + "owner": "ipetkov", 15 + "ref": "v0.21.1", 16 + "repo": "crane", 17 + "type": "github" 18 + } 19 + }, 20 + "dream2nix": { 21 + "inputs": { 22 + "nixpkgs": [ 23 + "nci", 24 + "nixpkgs" 25 + ], 26 + "purescript-overlay": "purescript-overlay", 27 + "pyproject-nix": "pyproject-nix" 28 + }, 29 + "locked": { 30 + "lastModified": 1754978539, 31 + "narHash": "sha256-nrDovydywSKRbWim9Ynmgj8SBm8LK3DI2WuhIqzOHYI=", 32 + "owner": "nix-community", 33 + "repo": "dream2nix", 34 + "rev": "fbec3263cb4895ac86ee9506cdc4e6919a1a2214", 35 + "type": "github" 36 + }, 37 + "original": { 38 + "owner": "nix-community", 39 + "repo": "dream2nix", 40 + "type": "github" 41 + } 42 + }, 43 + "fenix": { 44 + "inputs": { 45 + "nixpkgs": [ 46 + "nixpkgs" 47 + ], 48 + "rust-analyzer-src": "rust-analyzer-src" 49 + }, 50 + "locked": { 51 + "lastModified": 1762584108, 52 + "narHash": "sha256-wZUW7dlXMXaRdvNbaADqhF8gg9bAfFiMV+iyFQiDv+Y=", 53 + "owner": "nix-community", 54 + "repo": "fenix", 55 + "rev": "32f3ad3b6c690061173e1ac16708874975ec6056", 56 + "type": "github" 57 + }, 58 + "original": { 59 + "owner": "nix-community", 60 + "repo": "fenix", 61 + "type": "github" 62 + } 63 + }, 64 + "flake-compat": { 65 + "flake": false, 66 + "locked": { 67 + "lastModified": 1696426674, 68 + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 69 + "owner": "edolstra", 70 + "repo": "flake-compat", 71 + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 72 + "type": "github" 73 + }, 74 + "original": { 75 + "owner": "edolstra", 76 + "repo": "flake-compat", 77 + "type": "github" 78 + } 79 + }, 80 + "mk-naked-shell": { 81 + "flake": false, 82 + "locked": { 83 + "lastModified": 1681286841, 84 + "narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=", 85 + "owner": "90-008", 86 + "repo": "mk-naked-shell", 87 + "rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd", 88 + "type": "github" 89 + }, 90 + "original": { 91 + "owner": "90-008", 92 + "repo": "mk-naked-shell", 93 + "type": "github" 94 + } 95 + }, 96 + "nci": { 97 + "inputs": { 98 + "crane": "crane", 99 + "dream2nix": "dream2nix", 100 + "mk-naked-shell": "mk-naked-shell", 101 + "nixpkgs": [ 102 + "nixpkgs" 103 + ], 104 + "parts": "parts", 105 + "rust-overlay": "rust-overlay", 106 + "treefmt": "treefmt" 107 + }, 108 + "locked": { 109 + "lastModified": 1762582646, 110 + "narHash": "sha256-MMzE4xccG+8qbLhdaZoeFDUKWUOn3B4lhp5dZmgukmM=", 111 + "owner": "90-008", 112 + "repo": "nix-cargo-integration", 113 + "rev": "0993c449377049fa8868a664e8290ac6658e0b9a", 114 + "type": "github" 115 + }, 116 + "original": { 117 + "owner": "90-008", 118 + "repo": "nix-cargo-integration", 119 + "type": "github" 120 + } 121 + }, 122 + "nixpkgs": { 123 + "locked": { 124 + "lastModified": 1762361079, 125 + "narHash": "sha256-lz718rr1BDpZBYk7+G8cE6wee3PiBUpn8aomG/vLLiY=", 126 + "owner": "nixos", 127 + "repo": "nixpkgs", 128 + "rev": "ffcdcf99d65c61956d882df249a9be53e5902ea5", 129 + "type": "github" 130 + }, 131 + "original": { 132 + "owner": "nixos", 133 + "ref": "nixpkgs-unstable", 134 + "repo": "nixpkgs", 135 + "type": "github" 136 + } 137 + }, 138 + "parts": { 139 + "inputs": { 140 + "nixpkgs-lib": [ 141 + "nci", 142 + "nixpkgs" 143 + ] 144 + }, 145 + "locked": { 146 + "lastModified": 1762440070, 147 + "narHash": "sha256-xxdepIcb39UJ94+YydGP221rjnpkDZUlykKuF54PsqI=", 148 + "owner": "hercules-ci", 149 + "repo": "flake-parts", 150 + "rev": "26d05891e14c88eb4a5d5bee659c0db5afb609d8", 151 + "type": "github" 152 + }, 153 + "original": { 154 + "owner": "hercules-ci", 155 + "repo": "flake-parts", 156 + "type": "github" 157 + } 158 + }, 159 + "parts_2": { 160 + "inputs": { 161 + "nixpkgs-lib": [ 162 + "nixpkgs" 163 + ] 164 + }, 165 + "locked": { 166 + "lastModified": 1762440070, 167 + "narHash": "sha256-xxdepIcb39UJ94+YydGP221rjnpkDZUlykKuF54PsqI=", 168 + "owner": "hercules-ci", 169 + "repo": "flake-parts", 170 + "rev": "26d05891e14c88eb4a5d5bee659c0db5afb609d8", 171 + "type": "github" 172 + }, 173 + "original": { 174 + "owner": "hercules-ci", 175 + "repo": "flake-parts", 176 + "type": "github" 177 + } 178 + }, 179 + "purescript-overlay": { 180 + "inputs": { 181 + "flake-compat": "flake-compat", 182 + "nixpkgs": [ 183 + "nci", 184 + "dream2nix", 185 + "nixpkgs" 186 + ], 187 + "slimlock": "slimlock" 188 + }, 189 + "locked": { 190 + "lastModified": 1728546539, 191 + "narHash": "sha256-Sws7w0tlnjD+Bjck1nv29NjC5DbL6nH5auL9Ex9Iz2A=", 192 + "owner": "thomashoneyman", 193 + "repo": "purescript-overlay", 194 + "rev": "4ad4c15d07bd899d7346b331f377606631eb0ee4", 195 + "type": "github" 196 + }, 197 + "original": { 198 + "owner": "thomashoneyman", 199 + "repo": "purescript-overlay", 200 + "type": "github" 201 + } 202 + }, 203 + "pyproject-nix": { 204 + "inputs": { 205 + "nixpkgs": [ 206 + "nci", 207 + "dream2nix", 208 + "nixpkgs" 209 + ] 210 + }, 211 + "locked": { 212 + "lastModified": 1752481895, 213 + "narHash": "sha256-luVj97hIMpCbwhx3hWiRwjP2YvljWy8FM+4W9njDhLA=", 214 + "owner": "pyproject-nix", 215 + "repo": "pyproject.nix", 216 + "rev": "16ee295c25107a94e59a7fc7f2e5322851781162", 217 + "type": "github" 218 + }, 219 + "original": { 220 + "owner": "pyproject-nix", 221 + "repo": "pyproject.nix", 222 + "type": "github" 223 + } 224 + }, 225 + "root": { 226 + "inputs": { 227 + "fenix": "fenix", 228 + "nci": "nci", 229 + "nixpkgs": "nixpkgs", 230 + "parts": "parts_2" 231 + } 232 + }, 233 + "rust-analyzer-src": { 234 + "flake": false, 235 + "locked": { 236 + "lastModified": 1762438844, 237 + "narHash": "sha256-ApIKJf6CcMsV2nYBXhGF95BmZMO/QXPhgfSnkA/rVUo=", 238 + "owner": "rust-lang", 239 + "repo": "rust-analyzer", 240 + "rev": "4bf516ee5a960c1e2eee9fedd9b1c9e976a19c86", 241 + "type": "github" 242 + }, 243 + "original": { 244 + "owner": "rust-lang", 245 + "ref": "nightly", 246 + "repo": "rust-analyzer", 247 + "type": "github" 248 + } 249 + }, 250 + "rust-overlay": { 251 + "inputs": { 252 + "nixpkgs": [ 253 + "nci", 254 + "nixpkgs" 255 + ] 256 + }, 257 + "locked": { 258 + "lastModified": 1762569282, 259 + "narHash": "sha256-vINZAJpXQTZd5cfh06Rcw7hesH7sGSvi+Tn+HUieJn8=", 260 + "owner": "oxalica", 261 + "repo": "rust-overlay", 262 + "rev": "a35a6144b976f70827c2fe2f5c89d16d8f9179d8", 263 + "type": "github" 264 + }, 265 + "original": { 266 + "owner": "oxalica", 267 + "repo": "rust-overlay", 268 + "type": "github" 269 + } 270 + }, 271 + "slimlock": { 272 + "inputs": { 273 + "nixpkgs": [ 274 + "nci", 275 + "dream2nix", 276 + "purescript-overlay", 277 + "nixpkgs" 278 + ] 279 + }, 280 + "locked": { 281 + "lastModified": 1688756706, 282 + "narHash": "sha256-xzkkMv3neJJJ89zo3o2ojp7nFeaZc2G0fYwNXNJRFlo=", 283 + "owner": "thomashoneyman", 284 + "repo": "slimlock", 285 + "rev": "cf72723f59e2340d24881fd7bf61cb113b4c407c", 286 + "type": "github" 287 + }, 288 + "original": { 289 + "owner": "thomashoneyman", 290 + "repo": "slimlock", 291 + "type": "github" 292 + } 293 + }, 294 + "treefmt": { 295 + "inputs": { 296 + "nixpkgs": [ 297 + "nci", 298 + "nixpkgs" 299 + ] 300 + }, 301 + "locked": { 302 + "lastModified": 1762410071, 303 + "narHash": "sha256-aF5fvoZeoXNPxT0bejFUBXeUjXfHLSL7g+mjR/p5TEg=", 304 + "owner": "numtide", 305 + "repo": "treefmt-nix", 306 + "rev": "97a30861b13c3731a84e09405414398fbf3e109f", 307 + "type": "github" 308 + }, 309 + "original": { 310 + "owner": "numtide", 311 + "repo": "treefmt-nix", 312 + "type": "github" 313 + } 314 + } 315 + }, 316 + "root": "root", 317 + "version": 7 318 + }
+36
flake.nix
··· 1 + { 2 + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 3 + inputs.nci.url = "github:90-008/nix-cargo-integration"; 4 + inputs.nci.inputs.nixpkgs.follows = "nixpkgs"; 5 + inputs.parts.url = "github:hercules-ci/flake-parts"; 6 + inputs.parts.inputs.nixpkgs-lib.follows = "nixpkgs"; 7 + inputs.fenix = { 8 + url = "github:nix-community/fenix"; 9 + inputs.nixpkgs.follows = "nixpkgs"; 10 + }; 11 + 12 + outputs = inputs @ { 13 + parts, 14 + nci, 15 + ... 16 + }: 17 + parts.lib.mkFlake {inherit inputs;} { 18 + systems = ["x86_64-linux" "aarch64-darwin"]; 19 + imports = [ 20 + nci.flakeModule 21 + ./crates.nix 22 + ]; 23 + perSystem = { 24 + pkgs, 25 + config, 26 + ... 27 + }: let 28 + crateOutputs = config.nci.outputs."wisp-cli"; 29 + in { 30 + devShells.default = crateOutputs.devShell; 31 + packages.default = crateOutputs.packages.release; 32 + packages.wisp-cli-windows = crateOutputs.allTargets."x86_64-pc-windows-gnu".packages.release; 33 + packages.wisp-cli-darwin = crateOutputs.allTargets."aarch64-apple-darwin".packages.release; 34 + }; 35 + }; 36 + }
+3 -2
hosting-service/package.json
··· 3 3 "version": "1.0.0", 4 4 "type": "module", 5 5 "scripts": { 6 - "dev": "tsx watch src/index.ts", 6 + "dev": "tsx --env-file=.env watch src/index.ts", 7 7 "build": "tsc", 8 - "start": "tsx src/index.ts" 8 + "start": "tsx --env-file=.env src/index.ts", 9 + "backfill": "tsx --env-file=.env src/index.ts --backfill" 9 10 }, 10 11 "dependencies": { 11 12 "@atproto/api": "^0.17.4",
+20 -1
hosting-service/src/index.ts
··· 3 3 import { FirehoseWorker } from './lib/firehose'; 4 4 import { logger } from './lib/observability'; 5 5 import { mkdirSync, existsSync } from 'fs'; 6 + import { backfillCache } from './lib/backfill'; 6 7 7 8 const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3001; 8 - const CACHE_DIR = './cache/sites'; 9 + const CACHE_DIR = process.env.CACHE_DIR || './cache/sites'; 10 + 11 + // Parse CLI arguments 12 + const args = process.argv.slice(2); 13 + const hasBackfillFlag = args.includes('--backfill'); 14 + const backfillOnStartup = hasBackfillFlag || process.env.BACKFILL_ON_STARTUP === 'true'; 9 15 10 16 // Ensure cache directory exists 11 17 if (!existsSync(CACHE_DIR)) { ··· 19 25 }); 20 26 21 27 firehose.start(); 28 + 29 + // Run backfill if requested 30 + if (backfillOnStartup) { 31 + console.log('๐Ÿ”„ Backfill requested, starting cache backfill...'); 32 + backfillCache({ 33 + skipExisting: true, 34 + concurrency: 3, 35 + }).then((stats) => { 36 + console.log('โœ… Cache backfill completed'); 37 + }).catch((err) => { 38 + console.error('โŒ Cache backfill error:', err); 39 + }); 40 + } 22 41 23 42 // Add health check endpoint 24 43 app.get('/health', (c) => {
+136
hosting-service/src/lib/backfill.ts
··· 1 + import { getAllSites } from './db'; 2 + import { fetchSiteRecord, getPdsForDid, downloadAndCacheSite, isCached } from './utils'; 3 + import { logger } from './observability'; 4 + 5 + export interface BackfillOptions { 6 + skipExisting?: boolean; // Skip sites already in cache 7 + concurrency?: number; // Number of sites to cache concurrently 8 + maxSites?: number; // Maximum number of sites to backfill (for testing) 9 + } 10 + 11 + export interface BackfillStats { 12 + total: number; 13 + cached: number; 14 + skipped: number; 15 + failed: number; 16 + duration: number; 17 + } 18 + 19 + /** 20 + * Backfill all sites from the database into the local cache 21 + */ 22 + export async function backfillCache(options: BackfillOptions = {}): Promise<BackfillStats> { 23 + const { 24 + skipExisting = true, 25 + concurrency = 3, 26 + maxSites, 27 + } = options; 28 + 29 + const startTime = Date.now(); 30 + const stats: BackfillStats = { 31 + total: 0, 32 + cached: 0, 33 + skipped: 0, 34 + failed: 0, 35 + duration: 0, 36 + }; 37 + 38 + logger.info('Starting cache backfill', { skipExisting, concurrency, maxSites }); 39 + console.log(` 40 + โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 41 + โ•‘ CACHE BACKFILL STARTING โ•‘ 42 + โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 43 + `); 44 + 45 + try { 46 + // Get all sites from database 47 + let sites = await getAllSites(); 48 + stats.total = sites.length; 49 + 50 + logger.info(`Found ${sites.length} sites in database`); 51 + console.log(`๐Ÿ“Š Found ${sites.length} sites in database`); 52 + 53 + // Limit if specified 54 + if (maxSites && maxSites > 0) { 55 + sites = sites.slice(0, maxSites); 56 + console.log(`โš™๏ธ Limited to ${maxSites} sites for backfill`); 57 + } 58 + 59 + // Process sites in batches 60 + const batches: typeof sites[] = []; 61 + for (let i = 0; i < sites.length; i += concurrency) { 62 + batches.push(sites.slice(i, i + concurrency)); 63 + } 64 + 65 + let processed = 0; 66 + for (const batch of batches) { 67 + await Promise.all( 68 + batch.map(async (site) => { 69 + try { 70 + // Check if already cached 71 + if (skipExisting && isCached(site.did, site.rkey)) { 72 + stats.skipped++; 73 + processed++; 74 + logger.debug(`Skipping already cached site`, { did: site.did, rkey: site.rkey }); 75 + console.log(`โญ๏ธ [${processed}/${sites.length}] Skipped (cached): ${site.display_name || site.rkey}`); 76 + return; 77 + } 78 + 79 + // Fetch site record 80 + const siteData = await fetchSiteRecord(site.did, site.rkey); 81 + if (!siteData) { 82 + stats.failed++; 83 + processed++; 84 + logger.error('Site record not found during backfill', null, { did: site.did, rkey: site.rkey }); 85 + console.log(`โŒ [${processed}/${sites.length}] Failed (not found): ${site.display_name || site.rkey}`); 86 + return; 87 + } 88 + 89 + // Get PDS endpoint 90 + const pdsEndpoint = await getPdsForDid(site.did); 91 + if (!pdsEndpoint) { 92 + stats.failed++; 93 + processed++; 94 + logger.error('PDS not found during backfill', null, { did: site.did }); 95 + console.log(`โŒ [${processed}/${sites.length}] Failed (no PDS): ${site.display_name || site.rkey}`); 96 + return; 97 + } 98 + 99 + // Download and cache site 100 + await downloadAndCacheSite(site.did, site.rkey, siteData.record, pdsEndpoint, siteData.cid); 101 + stats.cached++; 102 + processed++; 103 + logger.info('Successfully cached site during backfill', { did: site.did, rkey: site.rkey }); 104 + console.log(`โœ… [${processed}/${sites.length}] Cached: ${site.display_name || site.rkey}`); 105 + } catch (err) { 106 + stats.failed++; 107 + processed++; 108 + logger.error('Failed to cache site during backfill', err, { did: site.did, rkey: site.rkey }); 109 + console.log(`โŒ [${processed}/${sites.length}] Failed: ${site.display_name || site.rkey}`); 110 + } 111 + }) 112 + ); 113 + } 114 + 115 + stats.duration = Date.now() - startTime; 116 + 117 + console.log(` 118 + โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— 119 + โ•‘ CACHE BACKFILL COMPLETED โ•‘ 120 + โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• 121 + 122 + ๐Ÿ“Š Total Sites: ${stats.total} 123 + โœ… Cached: ${stats.cached} 124 + โญ๏ธ Skipped: ${stats.skipped} 125 + โŒ Failed: ${stats.failed} 126 + โฑ๏ธ Duration: ${(stats.duration / 1000).toFixed(2)}s 127 + `); 128 + 129 + logger.info('Cache backfill completed', stats); 130 + } catch (err) { 131 + logger.error('Cache backfill failed', err); 132 + console.error('โŒ Cache backfill failed:', err); 133 + } 134 + 135 + return stats; 136 + }
+19
hosting-service/src/lib/db.ts
··· 81 81 } 82 82 } 83 83 84 + export interface SiteRecord { 85 + did: string; 86 + rkey: string; 87 + display_name?: string; 88 + } 89 + 90 + export async function getAllSites(): Promise<SiteRecord[]> { 91 + try { 92 + const result = await sql<SiteRecord[]>` 93 + SELECT did, rkey, display_name FROM sites 94 + ORDER BY created_at DESC 95 + `; 96 + return result; 97 + } catch (err) { 98 + console.error('Failed to get all sites', err); 99 + return []; 100 + } 101 + } 102 + 84 103 /** 85 104 * Generate a numeric lock ID from a string key 86 105 * PostgreSQL advisory locks use bigint (64-bit signed integer)
+259 -219
hosting-service/src/lib/firehose.ts
··· 1 - import { existsSync, rmSync } from 'fs'; 2 - import { getPdsForDid, downloadAndCacheSite, extractBlobCid, fetchSiteRecord } from './utils'; 3 - import { upsertSite, tryAcquireLock, releaseLock } from './db'; 4 - import { safeFetch } from './safe-fetch'; 5 - import { isRecord, validateRecord } from '../lexicon/types/place/wisp/fs'; 6 - import { Firehose } from '@atproto/sync'; 7 - import { IdResolver } from '@atproto/identity'; 1 + import { existsSync, rmSync } from 'fs' 2 + import { 3 + getPdsForDid, 4 + downloadAndCacheSite, 5 + extractBlobCid, 6 + fetchSiteRecord 7 + } from './utils' 8 + import { upsertSite, tryAcquireLock, releaseLock } from './db' 9 + import { safeFetch } from './safe-fetch' 10 + import { isRecord, validateRecord } from '../lexicon/types/place/wisp/fs' 11 + import { Firehose } from '@atproto/sync' 12 + import { IdResolver } from '@atproto/identity' 8 13 9 - const CACHE_DIR = './cache/sites'; 14 + const CACHE_DIR = './cache/sites' 10 15 11 16 export class FirehoseWorker { 12 - private firehose: Firehose | null = null; 13 - private idResolver: IdResolver; 14 - private isShuttingDown = false; 15 - private lastEventTime = Date.now(); 17 + private firehose: Firehose | null = null 18 + private idResolver: IdResolver 19 + private isShuttingDown = false 20 + private lastEventTime = Date.now() 16 21 17 - constructor( 18 - private logger?: (msg: string, data?: Record<string, unknown>) => void, 19 - ) { 20 - this.idResolver = new IdResolver(); 21 - } 22 + constructor( 23 + private logger?: (msg: string, data?: Record<string, unknown>) => void 24 + ) { 25 + this.idResolver = new IdResolver() 26 + } 22 27 23 - private log(msg: string, data?: Record<string, unknown>) { 24 - const log = this.logger || console.log; 25 - log(`[FirehoseWorker] ${msg}`, data || {}); 26 - } 28 + private log(msg: string, data?: Record<string, unknown>) { 29 + const log = this.logger || console.log 30 + log(`[FirehoseWorker] ${msg}`, data || {}) 31 + } 27 32 28 - start() { 29 - this.log('Starting firehose worker'); 30 - this.connect(); 31 - } 33 + start() { 34 + this.log('Starting firehose worker') 35 + this.connect() 36 + } 32 37 33 - stop() { 34 - this.log('Stopping firehose worker'); 35 - this.isShuttingDown = true; 38 + stop() { 39 + this.log('Stopping firehose worker') 40 + this.isShuttingDown = true 36 41 37 - if (this.firehose) { 38 - this.firehose.destroy(); 39 - this.firehose = null; 40 - } 41 - } 42 + if (this.firehose) { 43 + this.firehose.destroy() 44 + this.firehose = null 45 + } 46 + } 42 47 43 - private connect() { 44 - if (this.isShuttingDown) return; 48 + private connect() { 49 + if (this.isShuttingDown) return 45 50 46 - this.log('Connecting to AT Protocol firehose'); 51 + this.log('Connecting to AT Protocol firehose') 47 52 48 - this.firehose = new Firehose({ 49 - idResolver: this.idResolver, 50 - service: 'wss://bsky.network', 51 - filterCollections: ['place.wisp.fs'], 52 - handleEvent: async (evt: any) => { 53 - this.lastEventTime = Date.now(); 53 + this.firehose = new Firehose({ 54 + idResolver: this.idResolver, 55 + service: 'wss://bsky.network', 56 + filterCollections: ['place.wisp.fs'], 57 + handleEvent: async (evt: any) => { 58 + this.lastEventTime = Date.now() 54 59 55 - // Watch for write events 56 - if (evt.event === 'create' || evt.event === 'update') { 57 - const record = evt.record; 60 + // Watch for write events 61 + if (evt.event === 'create' || evt.event === 'update') { 62 + const record = evt.record 58 63 59 - // If the write is a valid place.wisp.fs record 60 - if ( 61 - evt.collection === 'place.wisp.fs' && 62 - isRecord(record) && 63 - validateRecord(record).success 64 - ) { 65 - this.log('Received place.wisp.fs event', { 66 - did: evt.did, 67 - event: evt.event, 68 - rkey: evt.rkey, 69 - }); 64 + // If the write is a valid place.wisp.fs record 65 + if ( 66 + evt.collection === 'place.wisp.fs' && 67 + isRecord(record) && 68 + validateRecord(record).success 69 + ) { 70 + this.log('Received place.wisp.fs event', { 71 + did: evt.did, 72 + event: evt.event, 73 + rkey: evt.rkey 74 + }) 70 75 71 - try { 72 - await this.handleCreateOrUpdate(evt.did, evt.rkey, record, evt.cid?.toString()); 73 - } catch (err) { 74 - this.log('Error handling event', { 75 - did: evt.did, 76 - event: evt.event, 77 - rkey: evt.rkey, 78 - error: err instanceof Error ? err.message : String(err), 79 - }); 80 - } 81 - } 82 - } else if (evt.event === 'delete' && evt.collection === 'place.wisp.fs') { 83 - this.log('Received delete event', { 84 - did: evt.did, 85 - rkey: evt.rkey, 86 - }); 76 + try { 77 + await this.handleCreateOrUpdate( 78 + evt.did, 79 + evt.rkey, 80 + record, 81 + evt.cid?.toString() 82 + ) 83 + } catch (err) { 84 + this.log('Error handling event', { 85 + did: evt.did, 86 + event: evt.event, 87 + rkey: evt.rkey, 88 + error: 89 + err instanceof Error 90 + ? err.message 91 + : String(err) 92 + }) 93 + } 94 + } 95 + } else if ( 96 + evt.event === 'delete' && 97 + evt.collection === 'place.wisp.fs' 98 + ) { 99 + this.log('Received delete event', { 100 + did: evt.did, 101 + rkey: evt.rkey 102 + }) 87 103 88 - try { 89 - await this.handleDelete(evt.did, evt.rkey); 90 - } catch (err) { 91 - this.log('Error handling delete', { 92 - did: evt.did, 93 - rkey: evt.rkey, 94 - error: err instanceof Error ? err.message : String(err), 95 - }); 96 - } 97 - } 98 - }, 99 - onError: (err: any) => { 100 - this.log('Firehose error', { 101 - error: err instanceof Error ? err.message : String(err), 102 - stack: err instanceof Error ? err.stack : undefined, 103 - fullError: err, 104 - }); 105 - console.error('Full firehose error:', err); 106 - }, 107 - }); 104 + try { 105 + await this.handleDelete(evt.did, evt.rkey) 106 + } catch (err) { 107 + this.log('Error handling delete', { 108 + did: evt.did, 109 + rkey: evt.rkey, 110 + error: 111 + err instanceof Error ? err.message : String(err) 112 + }) 113 + } 114 + } 115 + }, 116 + onError: (err: any) => { 117 + this.log('Firehose error', { 118 + error: err instanceof Error ? err.message : String(err), 119 + stack: err instanceof Error ? err.stack : undefined, 120 + fullError: err 121 + }) 122 + console.error('Full firehose error:', err) 123 + } 124 + }) 108 125 109 - this.firehose.start(); 110 - this.log('Firehose started'); 111 - } 126 + this.firehose.start() 127 + this.log('Firehose started') 128 + } 112 129 113 - private async handleCreateOrUpdate(did: string, site: string, record: any, eventCid?: string) { 114 - this.log('Processing create/update', { did, site }); 130 + private async handleCreateOrUpdate( 131 + did: string, 132 + site: string, 133 + record: any, 134 + eventCid?: string 135 + ) { 136 + this.log('Processing create/update', { did, site }) 115 137 116 - // Record is already validated in handleEvent 117 - const fsRecord = record; 138 + // Record is already validated in handleEvent 139 + const fsRecord = record 118 140 119 - const pdsEndpoint = await getPdsForDid(did); 120 - if (!pdsEndpoint) { 121 - this.log('Could not resolve PDS for DID', { did }); 122 - return; 123 - } 141 + const pdsEndpoint = await getPdsForDid(did) 142 + if (!pdsEndpoint) { 143 + this.log('Could not resolve PDS for DID', { did }) 144 + return 145 + } 124 146 125 - this.log('Resolved PDS', { did, pdsEndpoint }); 147 + this.log('Resolved PDS', { did, pdsEndpoint }) 126 148 127 - // Verify record exists on PDS and fetch its CID 128 - let verifiedCid: string; 129 - try { 130 - const result = await fetchSiteRecord(did, site); 149 + // Verify record exists on PDS and fetch its CID 150 + let verifiedCid: string 151 + try { 152 + const result = await fetchSiteRecord(did, site) 131 153 132 - if (!result) { 133 - this.log('Record not found on PDS, skipping cache', { did, site }); 134 - return; 135 - } 154 + if (!result) { 155 + this.log('Record not found on PDS, skipping cache', { 156 + did, 157 + site 158 + }) 159 + return 160 + } 136 161 137 - verifiedCid = result.cid; 162 + verifiedCid = result.cid 138 163 139 - // Verify event CID matches PDS CID (prevent cache poisoning) 140 - if (eventCid && eventCid !== verifiedCid) { 141 - this.log('CID mismatch detected - potential spoofed event', { 142 - did, 143 - site, 144 - eventCid, 145 - verifiedCid 146 - }); 147 - return; 148 - } 164 + // Verify event CID matches PDS CID (prevent cache poisoning) 165 + if (eventCid && eventCid !== verifiedCid) { 166 + this.log('CID mismatch detected - potential spoofed event', { 167 + did, 168 + site, 169 + eventCid, 170 + verifiedCid 171 + }) 172 + return 173 + } 149 174 150 - this.log('Record verified on PDS', { did, site, cid: verifiedCid }); 151 - } catch (err) { 152 - this.log('Failed to verify record on PDS', { 153 - did, 154 - site, 155 - error: err instanceof Error ? err.message : String(err), 156 - }); 157 - return; 158 - } 175 + this.log('Record verified on PDS', { did, site, cid: verifiedCid }) 176 + } catch (err) { 177 + this.log('Failed to verify record on PDS', { 178 + did, 179 + site, 180 + error: err instanceof Error ? err.message : String(err) 181 + }) 182 + return 183 + } 159 184 160 - // Cache the record with verified CID (uses atomic swap internally) 161 - // All instances cache locally for edge serving 162 - await downloadAndCacheSite(did, site, fsRecord, pdsEndpoint, verifiedCid); 185 + // Cache the record with verified CID (uses atomic swap internally) 186 + // All instances cache locally for edge serving 187 + await downloadAndCacheSite( 188 + did, 189 + site, 190 + fsRecord, 191 + pdsEndpoint, 192 + verifiedCid 193 + ) 163 194 164 - // Acquire distributed lock only for database write to prevent duplicate writes 165 - const lockKey = `db:upsert:${did}:${site}`; 166 - const lockAcquired = await tryAcquireLock(lockKey); 195 + // Acquire distributed lock only for database write to prevent duplicate writes 196 + const lockKey = `db:upsert:${did}:${site}` 197 + const lockAcquired = await tryAcquireLock(lockKey) 167 198 168 - if (!lockAcquired) { 169 - this.log('Another instance is writing to DB, skipping upsert', { did, site }); 170 - this.log('Successfully processed create/update (cached locally)', { did, site }); 171 - return; 172 - } 199 + if (!lockAcquired) { 200 + this.log('Another instance is writing to DB, skipping upsert', { 201 + did, 202 + site 203 + }) 204 + this.log('Successfully processed create/update (cached locally)', { 205 + did, 206 + site 207 + }) 208 + return 209 + } 173 210 174 - try { 175 - // Upsert site to database (only one instance does this) 176 - await upsertSite(did, site, fsRecord.site); 177 - this.log('Successfully processed create/update (cached + DB updated)', { did, site }); 178 - } finally { 179 - // Always release lock, even if DB write fails 180 - await releaseLock(lockKey); 181 - } 182 - } 211 + try { 212 + // Upsert site to database (only one instance does this) 213 + await upsertSite(did, site, fsRecord.site) 214 + this.log( 215 + 'Successfully processed create/update (cached + DB updated)', 216 + { did, site } 217 + ) 218 + } finally { 219 + // Always release lock, even if DB write fails 220 + await releaseLock(lockKey) 221 + } 222 + } 183 223 184 - private async handleDelete(did: string, site: string) { 185 - this.log('Processing delete', { did, site }); 224 + private async handleDelete(did: string, site: string) { 225 + this.log('Processing delete', { did, site }) 186 226 187 - // All instances should delete their local cache (no lock needed) 188 - const pdsEndpoint = await getPdsForDid(did); 189 - if (!pdsEndpoint) { 190 - this.log('Could not resolve PDS for DID', { did }); 191 - return; 192 - } 227 + // All instances should delete their local cache (no lock needed) 228 + const pdsEndpoint = await getPdsForDid(did) 229 + if (!pdsEndpoint) { 230 + this.log('Could not resolve PDS for DID', { did }) 231 + return 232 + } 193 233 194 - // Verify record is actually deleted from PDS 195 - try { 196 - const recordUrl = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=place.wisp.fs&rkey=${encodeURIComponent(site)}`; 197 - const recordRes = await safeFetch(recordUrl); 234 + // Verify record is actually deleted from PDS 235 + try { 236 + const recordUrl = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=place.wisp.fs&rkey=${encodeURIComponent(site)}` 237 + const recordRes = await safeFetch(recordUrl) 198 238 199 - if (recordRes.ok) { 200 - this.log('Record still exists on PDS, not deleting cache', { 201 - did, 202 - site, 203 - }); 204 - return; 205 - } 239 + if (recordRes.ok) { 240 + this.log('Record still exists on PDS, not deleting cache', { 241 + did, 242 + site 243 + }) 244 + return 245 + } 206 246 207 - this.log('Verified record is deleted from PDS', { 208 - did, 209 - site, 210 - status: recordRes.status, 211 - }); 212 - } catch (err) { 213 - this.log('Error verifying deletion on PDS', { 214 - did, 215 - site, 216 - error: err instanceof Error ? err.message : String(err), 217 - }); 218 - } 247 + this.log('Verified record is deleted from PDS', { 248 + did, 249 + site, 250 + status: recordRes.status 251 + }) 252 + } catch (err) { 253 + this.log('Error verifying deletion on PDS', { 254 + did, 255 + site, 256 + error: err instanceof Error ? err.message : String(err) 257 + }) 258 + } 219 259 220 - // Delete cache 221 - this.deleteCache(did, site); 260 + // Delete cache 261 + this.deleteCache(did, site) 222 262 223 - this.log('Successfully processed delete', { did, site }); 224 - } 263 + this.log('Successfully processed delete', { did, site }) 264 + } 225 265 226 - private deleteCache(did: string, site: string) { 227 - const cacheDir = `${CACHE_DIR}/${did}/${site}`; 266 + private deleteCache(did: string, site: string) { 267 + const cacheDir = `${CACHE_DIR}/${did}/${site}` 228 268 229 - if (!existsSync(cacheDir)) { 230 - this.log('Cache directory does not exist, nothing to delete', { 231 - did, 232 - site, 233 - }); 234 - return; 235 - } 269 + if (!existsSync(cacheDir)) { 270 + this.log('Cache directory does not exist, nothing to delete', { 271 + did, 272 + site 273 + }) 274 + return 275 + } 236 276 237 - try { 238 - rmSync(cacheDir, { recursive: true, force: true }); 239 - this.log('Cache deleted', { did, site, path: cacheDir }); 240 - } catch (err) { 241 - this.log('Failed to delete cache', { 242 - did, 243 - site, 244 - path: cacheDir, 245 - error: err instanceof Error ? err.message : String(err), 246 - }); 247 - } 248 - } 277 + try { 278 + rmSync(cacheDir, { recursive: true, force: true }) 279 + this.log('Cache deleted', { did, site, path: cacheDir }) 280 + } catch (err) { 281 + this.log('Failed to delete cache', { 282 + did, 283 + site, 284 + path: cacheDir, 285 + error: err instanceof Error ? err.message : String(err) 286 + }) 287 + } 288 + } 249 289 250 - getHealth() { 251 - const isConnected = this.firehose !== null; 252 - const timeSinceLastEvent = Date.now() - this.lastEventTime; 290 + getHealth() { 291 + const isConnected = this.firehose !== null 292 + const timeSinceLastEvent = Date.now() - this.lastEventTime 253 293 254 - return { 255 - connected: isConnected, 256 - lastEventTime: this.lastEventTime, 257 - timeSinceLastEvent, 258 - healthy: isConnected && timeSinceLastEvent < 300000, // 5 minutes 259 - }; 260 - } 294 + return { 295 + connected: isConnected, 296 + lastEventTime: this.lastEventTime, 297 + timeSinceLastEvent, 298 + healthy: isConnected && timeSinceLastEvent < 300000 // 5 minutes 299 + } 300 + } 261 301 }
+457
hosting-service/src/lib/html-rewriter.test.ts
··· 1 + import { describe, test, expect } from 'bun:test' 2 + import { rewriteHtmlPaths, isHtmlContent } from './html-rewriter' 3 + 4 + describe('rewriteHtmlPaths', () => { 5 + const basePath = '/identifier/site/' 6 + 7 + describe('absolute paths', () => { 8 + test('rewrites absolute paths with leading slash', () => { 9 + const html = '<img src="/image.png">' 10 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 11 + expect(result).toBe('<img src="/identifier/site/image.png">') 12 + }) 13 + 14 + test('rewrites nested absolute paths', () => { 15 + const html = '<link href="/css/style.css">' 16 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 17 + expect(result).toBe('<link href="/identifier/site/css/style.css">') 18 + }) 19 + }) 20 + 21 + describe('relative paths from root document', () => { 22 + test('rewrites relative paths with ./ prefix', () => { 23 + const html = '<img src="./image.png">' 24 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 25 + expect(result).toBe('<img src="/identifier/site/image.png">') 26 + }) 27 + 28 + test('rewrites relative paths without prefix', () => { 29 + const html = '<img src="image.png">' 30 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 31 + expect(result).toBe('<img src="/identifier/site/image.png">') 32 + }) 33 + 34 + test('rewrites relative paths with ../ (should stay at root)', () => { 35 + const html = '<img src="../image.png">' 36 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 37 + expect(result).toBe('<img src="/identifier/site/image.png">') 38 + }) 39 + }) 40 + 41 + describe('relative paths from nested documents', () => { 42 + test('rewrites relative path from nested document', () => { 43 + const html = '<img src="./photo.jpg">' 44 + const result = rewriteHtmlPaths( 45 + html, 46 + basePath, 47 + 'folder1/folder2/index.html' 48 + ) 49 + expect(result).toBe( 50 + '<img src="/identifier/site/folder1/folder2/photo.jpg">' 51 + ) 52 + }) 53 + 54 + test('rewrites plain filename from nested document', () => { 55 + const html = '<script src="app.js"></script>' 56 + const result = rewriteHtmlPaths( 57 + html, 58 + basePath, 59 + 'folder1/folder2/index.html' 60 + ) 61 + expect(result).toBe( 62 + '<script src="/identifier/site/folder1/folder2/app.js"></script>' 63 + ) 64 + }) 65 + 66 + test('rewrites ../ to go up one level', () => { 67 + const html = '<img src="../image.png">' 68 + const result = rewriteHtmlPaths( 69 + html, 70 + basePath, 71 + 'folder1/folder2/folder3/index.html' 72 + ) 73 + expect(result).toBe( 74 + '<img src="/identifier/site/folder1/folder2/image.png">' 75 + ) 76 + }) 77 + 78 + test('rewrites multiple ../ to go up multiple levels', () => { 79 + const html = '<link href="../../css/style.css">' 80 + const result = rewriteHtmlPaths( 81 + html, 82 + basePath, 83 + 'folder1/folder2/folder3/index.html' 84 + ) 85 + expect(result).toBe( 86 + '<link href="/identifier/site/folder1/css/style.css">' 87 + ) 88 + }) 89 + 90 + test('rewrites ../ with additional path segments', () => { 91 + const html = '<img src="../assets/logo.png">' 92 + const result = rewriteHtmlPaths( 93 + html, 94 + basePath, 95 + 'pages/about/index.html' 96 + ) 97 + expect(result).toBe( 98 + '<img src="/identifier/site/pages/assets/logo.png">' 99 + ) 100 + }) 101 + 102 + test('handles complex nested relative paths', () => { 103 + const html = '<script src="../../lib/vendor/jquery.js"></script>' 104 + const result = rewriteHtmlPaths( 105 + html, 106 + basePath, 107 + 'pages/blog/post/index.html' 108 + ) 109 + expect(result).toBe( 110 + '<script src="/identifier/site/pages/lib/vendor/jquery.js"></script>' 111 + ) 112 + }) 113 + 114 + test('handles ../ going past root (stays at root)', () => { 115 + const html = '<img src="../../../image.png">' 116 + const result = rewriteHtmlPaths(html, basePath, 'folder1/index.html') 117 + expect(result).toBe('<img src="/identifier/site/image.png">') 118 + }) 119 + }) 120 + 121 + describe('external URLs and special schemes', () => { 122 + test('does not rewrite http URLs', () => { 123 + const html = '<img src="http://example.com/image.png">' 124 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 125 + expect(result).toBe('<img src="http://example.com/image.png">') 126 + }) 127 + 128 + test('does not rewrite https URLs', () => { 129 + const html = '<link href="https://cdn.example.com/style.css">' 130 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 131 + expect(result).toBe( 132 + '<link href="https://cdn.example.com/style.css">' 133 + ) 134 + }) 135 + 136 + test('does not rewrite protocol-relative URLs', () => { 137 + const html = '<script src="//cdn.example.com/script.js"></script>' 138 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 139 + expect(result).toBe( 140 + '<script src="//cdn.example.com/script.js"></script>' 141 + ) 142 + }) 143 + 144 + test('does not rewrite data URIs', () => { 145 + const html = 146 + '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA">' 147 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 148 + expect(result).toBe( 149 + '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA">' 150 + ) 151 + }) 152 + 153 + test('does not rewrite mailto links', () => { 154 + const html = '<a href="mailto:test@example.com">Email</a>' 155 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 156 + expect(result).toBe('<a href="mailto:test@example.com">Email</a>') 157 + }) 158 + 159 + test('does not rewrite tel links', () => { 160 + const html = '<a href="tel:+1234567890">Call</a>' 161 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 162 + expect(result).toBe('<a href="tel:+1234567890">Call</a>') 163 + }) 164 + }) 165 + 166 + describe('different HTML attributes', () => { 167 + test('rewrites src attribute', () => { 168 + const html = '<img src="/image.png">' 169 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 170 + expect(result).toBe('<img src="/identifier/site/image.png">') 171 + }) 172 + 173 + test('rewrites href attribute', () => { 174 + const html = '<a href="/page.html">Link</a>' 175 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 176 + expect(result).toBe('<a href="/identifier/site/page.html">Link</a>') 177 + }) 178 + 179 + test('rewrites action attribute', () => { 180 + const html = '<form action="/submit"></form>' 181 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 182 + expect(result).toBe('<form action="/identifier/site/submit"></form>') 183 + }) 184 + 185 + test('rewrites data attribute', () => { 186 + const html = '<object data="/document.pdf"></object>' 187 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 188 + expect(result).toBe( 189 + '<object data="/identifier/site/document.pdf"></object>' 190 + ) 191 + }) 192 + 193 + test('rewrites poster attribute', () => { 194 + const html = '<video poster="/thumbnail.jpg"></video>' 195 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 196 + expect(result).toBe( 197 + '<video poster="/identifier/site/thumbnail.jpg"></video>' 198 + ) 199 + }) 200 + 201 + test('rewrites srcset attribute with single URL', () => { 202 + const html = '<img srcset="/image.png 1x">' 203 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 204 + expect(result).toBe( 205 + '<img srcset="/identifier/site/image.png 1x">' 206 + ) 207 + }) 208 + 209 + test('rewrites srcset attribute with multiple URLs', () => { 210 + const html = '<img srcset="/image-1x.png 1x, /image-2x.png 2x">' 211 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 212 + expect(result).toBe( 213 + '<img srcset="/identifier/site/image-1x.png 1x, /identifier/site/image-2x.png 2x">' 214 + ) 215 + }) 216 + 217 + test('rewrites srcset with width descriptors', () => { 218 + const html = '<img srcset="/small.jpg 320w, /large.jpg 1024w">' 219 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 220 + expect(result).toBe( 221 + '<img srcset="/identifier/site/small.jpg 320w, /identifier/site/large.jpg 1024w">' 222 + ) 223 + }) 224 + 225 + test('rewrites srcset with relative paths from nested document', () => { 226 + const html = '<img srcset="../img1.png 1x, ../img2.png 2x">' 227 + const result = rewriteHtmlPaths( 228 + html, 229 + basePath, 230 + 'folder1/folder2/index.html' 231 + ) 232 + expect(result).toBe( 233 + '<img srcset="/identifier/site/folder1/img1.png 1x, /identifier/site/folder1/img2.png 2x">' 234 + ) 235 + }) 236 + }) 237 + 238 + describe('quote handling', () => { 239 + test('handles double quotes', () => { 240 + const html = '<img src="/image.png">' 241 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 242 + expect(result).toBe('<img src="/identifier/site/image.png">') 243 + }) 244 + 245 + test('handles single quotes', () => { 246 + const html = "<img src='/image.png'>" 247 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 248 + expect(result).toBe("<img src='/identifier/site/image.png'>") 249 + }) 250 + 251 + test('handles mixed quotes in same document', () => { 252 + const html = '<img src="/img1.png"><link href=\'/style.css\'>' 253 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 254 + expect(result).toBe( 255 + '<img src="/identifier/site/img1.png"><link href=\'/identifier/site/style.css\'>' 256 + ) 257 + }) 258 + }) 259 + 260 + describe('multiple rewrites in same document', () => { 261 + test('rewrites multiple attributes in complex HTML', () => { 262 + const html = ` 263 + <!DOCTYPE html> 264 + <html> 265 + <head> 266 + <link href="/css/style.css" rel="stylesheet"> 267 + <script src="/js/app.js"></script> 268 + </head> 269 + <body> 270 + <img src="/images/logo.png" alt="Logo"> 271 + <a href="/about.html">About</a> 272 + <form action="/submit"> 273 + <button type="submit">Submit</button> 274 + </form> 275 + </body> 276 + </html> 277 + ` 278 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 279 + expect(result).toContain('href="/identifier/site/css/style.css"') 280 + expect(result).toContain('src="/identifier/site/js/app.js"') 281 + expect(result).toContain('src="/identifier/site/images/logo.png"') 282 + expect(result).toContain('href="/identifier/site/about.html"') 283 + expect(result).toContain('action="/identifier/site/submit"') 284 + }) 285 + 286 + test('handles mix of relative and absolute paths', () => { 287 + const html = ` 288 + <img src="/abs/image.png"> 289 + <img src="./rel/image.png"> 290 + <img src="../parent/image.png"> 291 + <img src="https://external.com/image.png"> 292 + ` 293 + const result = rewriteHtmlPaths( 294 + html, 295 + basePath, 296 + 'folder1/folder2/page.html' 297 + ) 298 + expect(result).toContain('src="/identifier/site/abs/image.png"') 299 + expect(result).toContain( 300 + 'src="/identifier/site/folder1/folder2/rel/image.png"' 301 + ) 302 + expect(result).toContain( 303 + 'src="/identifier/site/folder1/parent/image.png"' 304 + ) 305 + expect(result).toContain('src="https://external.com/image.png"') 306 + }) 307 + }) 308 + 309 + describe('edge cases', () => { 310 + test('handles empty src attribute', () => { 311 + const html = '<img src="">' 312 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 313 + expect(result).toBe('<img src="">') 314 + }) 315 + 316 + test('handles basePath without trailing slash', () => { 317 + const html = '<img src="/image.png">' 318 + const result = rewriteHtmlPaths(html, '/identifier/site', 'index.html') 319 + expect(result).toBe('<img src="/identifier/site/image.png">') 320 + }) 321 + 322 + test('handles basePath with trailing slash', () => { 323 + const html = '<img src="/image.png">' 324 + const result = rewriteHtmlPaths( 325 + html, 326 + '/identifier/site/', 327 + 'index.html' 328 + ) 329 + expect(result).toBe('<img src="/identifier/site/image.png">') 330 + }) 331 + 332 + test('handles whitespace around equals sign', () => { 333 + const html = '<img src = "/image.png">' 334 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 335 + expect(result).toBe('<img src="/identifier/site/image.png">') 336 + }) 337 + 338 + test('preserves query strings in URLs', () => { 339 + const html = '<img src="/image.png?v=123">' 340 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 341 + expect(result).toBe('<img src="/identifier/site/image.png?v=123">') 342 + }) 343 + 344 + test('preserves hash fragments in URLs', () => { 345 + const html = '<a href="/page.html#section">Link</a>' 346 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 347 + expect(result).toBe( 348 + '<a href="/identifier/site/page.html#section">Link</a>' 349 + ) 350 + }) 351 + 352 + test('handles paths with special characters', () => { 353 + const html = '<img src="/folder-name/file_name.png">' 354 + const result = rewriteHtmlPaths(html, basePath, 'index.html') 355 + expect(result).toBe( 356 + '<img src="/identifier/site/folder-name/file_name.png">' 357 + ) 358 + }) 359 + }) 360 + 361 + describe('real-world scenario', () => { 362 + test('handles the example from the bug report', () => { 363 + // HTML file at: /folder1/folder2/folder3/index.html 364 + // Image at: /folder1/folder2/img.png 365 + // Reference: src="../img.png" 366 + const html = '<img src="../img.png">' 367 + const result = rewriteHtmlPaths( 368 + html, 369 + basePath, 370 + 'folder1/folder2/folder3/index.html' 371 + ) 372 + expect(result).toBe( 373 + '<img src="/identifier/site/folder1/folder2/img.png">' 374 + ) 375 + }) 376 + 377 + test('handles deeply nested static site structure', () => { 378 + // A typical static site with nested pages and shared assets 379 + const html = ` 380 + <!DOCTYPE html> 381 + <html> 382 + <head> 383 + <link href="../../css/style.css" rel="stylesheet"> 384 + <link href="../../css/theme.css" rel="stylesheet"> 385 + <script src="../../js/main.js"></script> 386 + </head> 387 + <body> 388 + <img src="../../images/logo.png" alt="Logo"> 389 + <img src="./post-image.jpg" alt="Post"> 390 + <a href="../index.html">Back to Blog</a> 391 + <a href="../../index.html">Home</a> 392 + </body> 393 + </html> 394 + ` 395 + const result = rewriteHtmlPaths( 396 + html, 397 + basePath, 398 + 'blog/posts/my-post.html' 399 + ) 400 + 401 + // Assets two levels up 402 + expect(result).toContain('href="/identifier/site/css/style.css"') 403 + expect(result).toContain('href="/identifier/site/css/theme.css"') 404 + expect(result).toContain('src="/identifier/site/js/main.js"') 405 + expect(result).toContain('src="/identifier/site/images/logo.png"') 406 + 407 + // Same directory 408 + expect(result).toContain( 409 + 'src="/identifier/site/blog/posts/post-image.jpg"' 410 + ) 411 + 412 + // One level up 413 + expect(result).toContain('href="/identifier/site/blog/index.html"') 414 + 415 + // Two levels up 416 + expect(result).toContain('href="/identifier/site/index.html"') 417 + }) 418 + }) 419 + }) 420 + 421 + describe('isHtmlContent', () => { 422 + test('identifies HTML by content type', () => { 423 + expect(isHtmlContent('file.txt', 'text/html')).toBe(true) 424 + expect(isHtmlContent('file.txt', 'text/html; charset=utf-8')).toBe( 425 + true 426 + ) 427 + }) 428 + 429 + test('identifies HTML by .html extension', () => { 430 + expect(isHtmlContent('index.html')).toBe(true) 431 + expect(isHtmlContent('page.html', undefined)).toBe(true) 432 + expect(isHtmlContent('/path/to/file.html')).toBe(true) 433 + }) 434 + 435 + test('identifies HTML by .htm extension', () => { 436 + expect(isHtmlContent('index.htm')).toBe(true) 437 + expect(isHtmlContent('page.htm', undefined)).toBe(true) 438 + }) 439 + 440 + test('handles case-insensitive extensions', () => { 441 + expect(isHtmlContent('INDEX.HTML')).toBe(true) 442 + expect(isHtmlContent('page.HTM')).toBe(true) 443 + expect(isHtmlContent('File.HtMl')).toBe(true) 444 + }) 445 + 446 + test('returns false for non-HTML files', () => { 447 + expect(isHtmlContent('script.js')).toBe(false) 448 + expect(isHtmlContent('style.css')).toBe(false) 449 + expect(isHtmlContent('image.png')).toBe(false) 450 + expect(isHtmlContent('data.json')).toBe(false) 451 + }) 452 + 453 + test('returns false for files with no extension', () => { 454 + expect(isHtmlContent('README')).toBe(false) 455 + expect(isHtmlContent('Makefile')).toBe(false) 456 + }) 457 + })
+178 -99
hosting-service/src/lib/html-rewriter.ts
··· 4 4 */ 5 5 6 6 const REWRITABLE_ATTRIBUTES = [ 7 - 'src', 8 - 'href', 9 - 'action', 10 - 'data', 11 - 'poster', 12 - 'srcset', 13 - ] as const; 7 + 'src', 8 + 'href', 9 + 'action', 10 + 'data', 11 + 'poster', 12 + 'srcset' 13 + ] as const 14 14 15 15 /** 16 16 * Check if a path should be rewritten 17 17 */ 18 18 function shouldRewritePath(path: string): boolean { 19 - // Don't rewrite empty paths 20 - if (!path) return false; 19 + // Don't rewrite empty paths 20 + if (!path) return false 21 21 22 - // Don't rewrite external URLs (http://, https://, //) 23 - if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('//')) { 24 - return false; 25 - } 22 + // Don't rewrite external URLs (http://, https://, //) 23 + if ( 24 + path.startsWith('http://') || 25 + path.startsWith('https://') || 26 + path.startsWith('//') 27 + ) { 28 + return false 29 + } 26 30 27 - // Don't rewrite data URIs or other schemes (except file paths) 28 - if (path.includes(':') && !path.startsWith('./') && !path.startsWith('../')) { 29 - return false; 30 - } 31 + // Don't rewrite data URIs or other schemes (except file paths) 32 + if ( 33 + path.includes(':') && 34 + !path.startsWith('./') && 35 + !path.startsWith('../') 36 + ) { 37 + return false 38 + } 31 39 32 - // Don't rewrite pure anchors or paths that start with /# 33 - if (path.startsWith('#') || path.startsWith('/#')) return false; 40 + // Rewrite absolute paths (/) and relative paths (./ or ../ or plain filenames) 41 + return true 42 + } 43 + 44 + /** 45 + * Normalize a path by resolving . and .. segments 46 + */ 47 + function normalizePath(path: string): string { 48 + const parts = path.split('/') 49 + const result: string[] = [] 50 + 51 + for (const part of parts) { 52 + if (part === '.' || part === '') { 53 + // Skip current directory and empty parts (but keep leading empty for absolute paths) 54 + if (part === '' && result.length === 0) { 55 + result.push(part) 56 + } 57 + continue 58 + } 59 + if (part === '..') { 60 + // Go up one directory (but not past root) 61 + if (result.length > 0 && result[result.length - 1] !== '..') { 62 + result.pop() 63 + } 64 + continue 65 + } 66 + result.push(part) 67 + } 34 68 35 - // Don't rewrite relative paths (./ or ../) 36 - if (path.startsWith('./') || path.startsWith('../')) return false; 69 + return result.join('/') 70 + } 37 71 38 - // Rewrite absolute paths (/) 39 - return true; 72 + /** 73 + * Get the directory path from a file path 74 + * e.g., "folder1/folder2/file.html" -> "folder1/folder2/" 75 + */ 76 + function getDirectory(filepath: string): string { 77 + const lastSlash = filepath.lastIndexOf('/') 78 + if (lastSlash === -1) { 79 + return '' 80 + } 81 + return filepath.substring(0, lastSlash + 1) 40 82 } 41 83 42 84 /** 43 85 * Rewrite a single path 44 86 */ 45 - function rewritePath(path: string, basePath: string): string { 46 - if (!shouldRewritePath(path)) { 47 - return path; 48 - } 87 + function rewritePath( 88 + path: string, 89 + basePath: string, 90 + documentPath: string 91 + ): string { 92 + if (!shouldRewritePath(path)) { 93 + return path 94 + } 95 + 96 + // Handle absolute paths: /file.js -> /base/file.js 97 + if (path.startsWith('/')) { 98 + return basePath + path.slice(1) 99 + } 100 + 101 + // Handle relative paths by resolving against document directory 102 + const documentDir = getDirectory(documentPath) 103 + let resolvedPath: string 49 104 50 - // Handle absolute paths: /file.js -> /base/file.js 51 - if (path.startsWith('/')) { 52 - return basePath + path.slice(1); 53 - } 105 + if (path.startsWith('./')) { 106 + // ./file.js relative to current directory 107 + resolvedPath = documentDir + path.slice(2) 108 + } else if (path.startsWith('../')) { 109 + // ../file.js relative to parent directory 110 + resolvedPath = documentDir + path 111 + } else { 112 + // file.js (no prefix) - treat as relative to current directory 113 + resolvedPath = documentDir + path 114 + } 54 115 55 - // At this point, only plain filenames without ./ or ../ prefix should reach here 56 - // But since we're filtering those in shouldRewritePath, this shouldn't happen 57 - return path; 116 + // Normalize the path to resolve .. and . 117 + resolvedPath = normalizePath(resolvedPath) 118 + 119 + return basePath + resolvedPath 58 120 } 59 121 60 122 /** 61 123 * Rewrite srcset attribute (can contain multiple URLs) 62 124 * Format: "url1 1x, url2 2x" or "url1 100w, url2 200w" 63 125 */ 64 - function rewriteSrcset(srcset: string, basePath: string): string { 65 - return srcset 66 - .split(',') 67 - .map(part => { 68 - const trimmed = part.trim(); 69 - const spaceIndex = trimmed.indexOf(' '); 126 + function rewriteSrcset( 127 + srcset: string, 128 + basePath: string, 129 + documentPath: string 130 + ): string { 131 + return srcset 132 + .split(',') 133 + .map((part) => { 134 + const trimmed = part.trim() 135 + const spaceIndex = trimmed.indexOf(' ') 70 136 71 - if (spaceIndex === -1) { 72 - // No descriptor, just URL 73 - return rewritePath(trimmed, basePath); 74 - } 137 + if (spaceIndex === -1) { 138 + // No descriptor, just URL 139 + return rewritePath(trimmed, basePath, documentPath) 140 + } 75 141 76 - const url = trimmed.substring(0, spaceIndex); 77 - const descriptor = trimmed.substring(spaceIndex); 78 - return rewritePath(url, basePath) + descriptor; 79 - }) 80 - .join(', '); 142 + const url = trimmed.substring(0, spaceIndex) 143 + const descriptor = trimmed.substring(spaceIndex) 144 + return rewritePath(url, basePath, documentPath) + descriptor 145 + }) 146 + .join(', ') 81 147 } 82 148 83 149 /** 84 - * Rewrite absolute paths in HTML content 150 + * Rewrite absolute and relative paths in HTML content 85 151 * Uses simple regex matching for safety (no full HTML parsing) 86 152 */ 87 - export function rewriteHtmlPaths(html: string, basePath: string): string { 88 - // Ensure base path ends with / 89 - const normalizedBase = basePath.endsWith('/') ? basePath : basePath + '/'; 153 + export function rewriteHtmlPaths( 154 + html: string, 155 + basePath: string, 156 + documentPath: string 157 + ): string { 158 + // Ensure base path ends with / 159 + const normalizedBase = basePath.endsWith('/') ? basePath : basePath + '/' 90 160 91 - let rewritten = html; 161 + let rewritten = html 92 162 93 - // Rewrite each attribute type 94 - // Use more specific patterns to prevent ReDoS attacks 95 - for (const attr of REWRITABLE_ATTRIBUTES) { 96 - if (attr === 'srcset') { 97 - // Special handling for srcset - use possessive quantifiers via atomic grouping simulation 98 - // Limit whitespace to reasonable amount (max 5 spaces) to prevent ReDoS 99 - const srcsetRegex = new RegExp( 100 - `\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}"([^"]*)"`, 101 - 'gi' 102 - ); 103 - rewritten = rewritten.replace(srcsetRegex, (match, value) => { 104 - const rewrittenValue = rewriteSrcset(value, normalizedBase); 105 - return `${attr}="${rewrittenValue}"`; 106 - }); 107 - } else { 108 - // Regular attributes with quoted values 109 - // Limit whitespace to prevent catastrophic backtracking 110 - const doubleQuoteRegex = new RegExp( 111 - `\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}"([^"]*)"`, 112 - 'gi' 113 - ); 114 - const singleQuoteRegex = new RegExp( 115 - `\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}'([^']*)'`, 116 - 'gi' 117 - ); 163 + // Rewrite each attribute type 164 + // Use more specific patterns to prevent ReDoS attacks 165 + for (const attr of REWRITABLE_ATTRIBUTES) { 166 + if (attr === 'srcset') { 167 + // Special handling for srcset - use possessive quantifiers via atomic grouping simulation 168 + // Limit whitespace to reasonable amount (max 5 spaces) to prevent ReDoS 169 + const srcsetRegex = new RegExp( 170 + `\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}"([^"]*)"`, 171 + 'gi' 172 + ) 173 + rewritten = rewritten.replace(srcsetRegex, (match, value) => { 174 + const rewrittenValue = rewriteSrcset( 175 + value, 176 + normalizedBase, 177 + documentPath 178 + ) 179 + return `${attr}="${rewrittenValue}"` 180 + }) 181 + } else { 182 + // Regular attributes with quoted values 183 + // Limit whitespace to prevent catastrophic backtracking 184 + const doubleQuoteRegex = new RegExp( 185 + `\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}"([^"]*)"`, 186 + 'gi' 187 + ) 188 + const singleQuoteRegex = new RegExp( 189 + `\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}'([^']*)'`, 190 + 'gi' 191 + ) 118 192 119 - rewritten = rewritten.replace(doubleQuoteRegex, (match, value) => { 120 - const rewrittenValue = rewritePath(value, normalizedBase); 121 - return `${attr}="${rewrittenValue}"`; 122 - }); 193 + rewritten = rewritten.replace(doubleQuoteRegex, (match, value) => { 194 + const rewrittenValue = rewritePath( 195 + value, 196 + normalizedBase, 197 + documentPath 198 + ) 199 + return `${attr}="${rewrittenValue}"` 200 + }) 123 201 124 - rewritten = rewritten.replace(singleQuoteRegex, (match, value) => { 125 - const rewrittenValue = rewritePath(value, normalizedBase); 126 - return `${attr}='${rewrittenValue}'`; 127 - }); 128 - } 129 - } 202 + rewritten = rewritten.replace(singleQuoteRegex, (match, value) => { 203 + const rewrittenValue = rewritePath( 204 + value, 205 + normalizedBase, 206 + documentPath 207 + ) 208 + return `${attr}='${rewrittenValue}'` 209 + }) 210 + } 211 + } 130 212 131 - return rewritten; 213 + return rewritten 132 214 } 133 215 134 216 /** 135 217 * Check if content is HTML based on content or filename 136 218 */ 137 - export function isHtmlContent( 138 - filepath: string, 139 - contentType?: string 140 - ): boolean { 141 - if (contentType && contentType.includes('text/html')) { 142 - return true; 143 - } 219 + export function isHtmlContent(filepath: string, contentType?: string): boolean { 220 + if (contentType && contentType.includes('text/html')) { 221 + return true 222 + } 144 223 145 - const ext = filepath.toLowerCase().split('.').pop(); 146 - return ext === 'html' || ext === 'htm'; 224 + const ext = filepath.toLowerCase().split('.').pop() 225 + return ext === 'html' || ext === 'htm' 147 226 }
+76 -12
hosting-service/src/lib/utils.ts
··· 5 5 import { safeFetchJson, safeFetchBlob } from './safe-fetch'; 6 6 import { CID } from 'multiformats'; 7 7 8 - const CACHE_DIR = './cache/sites'; 8 + const CACHE_DIR = process.env.CACHE_DIR || './cache/sites'; 9 9 const CACHE_TTL = 14 * 24 * 60 * 60 * 1000; // 14 days cache TTL 10 10 11 11 interface CacheMetadata { ··· 13 13 cachedAt: number; 14 14 did: string; 15 15 rkey: string; 16 + } 17 + 18 + /** 19 + * Determines if a MIME type should benefit from gzip compression. 20 + * Returns true for text-based web assets (HTML, CSS, JS, JSON, XML, SVG). 21 + * Returns false for already-compressed formats (images, video, audio, PDFs). 22 + * 23 + */ 24 + export function shouldCompressMimeType(mimeType: string | undefined): boolean { 25 + if (!mimeType) return false; 26 + 27 + const mime = mimeType.toLowerCase(); 28 + 29 + // Text-based web assets that benefit from compression 30 + const compressibleTypes = [ 31 + 'text/html', 32 + 'text/css', 33 + 'text/javascript', 34 + 'application/javascript', 35 + 'application/x-javascript', 36 + 'text/xml', 37 + 'application/xml', 38 + 'application/json', 39 + 'text/plain', 40 + 'image/svg+xml', 41 + ]; 42 + 43 + if (compressibleTypes.some(type => mime === type || mime.startsWith(type))) { 44 + return true; 45 + } 46 + 47 + // Already-compressed formats that should NOT be double-compressed 48 + const alreadyCompressedPrefixes = [ 49 + 'video/', 50 + 'audio/', 51 + 'image/', 52 + 'application/pdf', 53 + 'application/zip', 54 + 'application/gzip', 55 + ]; 56 + 57 + if (alreadyCompressedPrefixes.some(prefix => mime.startsWith(prefix))) { 58 + return false; 59 + } 60 + 61 + // Default to not compressing for unknown types 62 + return false; 16 63 } 17 64 18 65 interface IpldLink { ··· 270 317 271 318 console.log(`[DEBUG] ${filePath}: fetched ${content.length} bytes, base64=${base64}, encoding=${encoding}, mimeType=${mimeType}`); 272 319 273 - // If content is base64-encoded, decode it back to binary (gzipped or not) 320 + // If content is base64-encoded, decode it back to raw binary (gzipped or not) 274 321 if (base64) { 275 322 const originalSize = content.length; 276 - // The content from the blob is base64 text, decode it directly to binary 277 - const buffer = Buffer.from(content); 278 - const base64String = buffer.toString('ascii'); // Use ascii for base64 text, not utf-8 279 - console.log(`[DEBUG] ${filePath}: base64 string first 100 chars: ${base64String.substring(0, 100)}`); 323 + // Decode base64 directly from raw bytes - no string conversion 324 + // The blob contains base64-encoded text as raw bytes, decode it in-place 325 + const textDecoder = new TextDecoder(); 326 + const base64String = textDecoder.decode(content); 280 327 content = Buffer.from(base64String, 'base64'); 281 - console.log(`[DEBUG] ${filePath}: decoded from ${originalSize} bytes to ${content.length} bytes`); 328 + console.log(`[DEBUG] ${filePath}: decoded base64 from ${originalSize} bytes to ${content.length} bytes`); 282 329 283 330 // Check if it's actually gzipped by looking at magic bytes 284 331 if (content.length >= 2) { 285 - const magic = content[0] === 0x1f && content[1] === 0x8b; 286 - const byte0 = content[0]; 287 - const byte1 = content[1]; 288 - console.log(`[DEBUG] ${filePath}: has gzip magic bytes: ${magic} (0x${byte0?.toString(16)}, 0x${byte1?.toString(16)})`); 332 + const hasGzipMagic = content[0] === 0x1f && content[1] === 0x8b; 333 + console.log(`[DEBUG] ${filePath}: has gzip magic bytes: ${hasGzipMagic}`); 289 334 } 290 335 } 291 336 ··· 296 341 mkdirSync(fileDir, { recursive: true }); 297 342 } 298 343 344 + // Use the shared function to determine if this should remain compressed 345 + const shouldStayCompressed = shouldCompressMimeType(mimeType); 346 + 347 + // Decompress files that shouldn't be stored compressed 348 + if (encoding === 'gzip' && !shouldStayCompressed && content.length >= 2 && 349 + content[0] === 0x1f && content[1] === 0x8b) { 350 + console.log(`[DEBUG] ${filePath}: decompressing non-compressible type (${mimeType}) before caching`); 351 + try { 352 + const { gunzipSync } = await import('zlib'); 353 + const decompressed = gunzipSync(content); 354 + console.log(`[DEBUG] ${filePath}: decompressed from ${content.length} to ${decompressed.length} bytes`); 355 + content = decompressed; 356 + // Clear the encoding flag since we're storing decompressed 357 + encoding = undefined; 358 + } catch (error) { 359 + console.log(`[DEBUG] ${filePath}: failed to decompress, storing original gzipped content. Error:`, error); 360 + } 361 + } 362 + 299 363 await writeFile(cacheFile, content); 300 364 301 - // Store metadata if file is compressed 365 + // Store metadata only if file is still compressed 302 366 if (encoding === 'gzip' && mimeType) { 303 367 const metaFile = `${cacheFile}.meta`; 304 368 await writeFile(metaFile, JSON.stringify({ encoding, mimeType }));
+30 -36
hosting-service/src/server.ts
··· 1 1 import { Hono } from 'hono'; 2 2 import { getWispDomain, getCustomDomain, getCustomDomainByHash } from './lib/db'; 3 - import { resolveDid, getPdsForDid, fetchSiteRecord, downloadAndCacheSite, getCachedFilePath, isCached, sanitizePath } from './lib/utils'; 3 + import { resolveDid, getPdsForDid, fetchSiteRecord, downloadAndCacheSite, getCachedFilePath, isCached, sanitizePath, shouldCompressMimeType } from './lib/utils'; 4 4 import { rewriteHtmlPaths, isHtmlContent } from './lib/html-rewriter'; 5 5 import { existsSync, readFileSync } from 'fs'; 6 6 import { lookup } from 'mime-types'; ··· 45 45 // Check actual content for gzip magic bytes 46 46 if (content.length >= 2) { 47 47 const hasGzipMagic = content[0] === 0x1f && content[1] === 0x8b; 48 - const byte0 = content[0]; 49 - const byte1 = content[1]; 50 - console.log(`[DEBUG SERVE] ${requestPath}: has gzip magic bytes=${hasGzipMagic} (0x${byte0?.toString(16)}, 0x${byte1?.toString(16)})`); 48 + console.log(`[DEBUG SERVE] ${requestPath}: has gzip magic bytes=${hasGzipMagic}`); 51 49 } 52 50 53 51 if (meta.encoding === 'gzip' && meta.mimeType) { 54 - // Don't serve already-compressed media formats with Content-Encoding: gzip 55 - // These formats (video, audio, images) are already compressed and the browser 56 - // can't decode them if we add another layer of compression 57 - const alreadyCompressedTypes = [ 58 - 'video/', 'audio/', 'image/jpeg', 'image/jpg', 'image/png', 59 - 'image/gif', 'image/webp', 'application/pdf' 60 - ]; 52 + // Use shared function to determine if this should be served compressed 53 + const shouldServeCompressed = shouldCompressMimeType(meta.mimeType); 61 54 62 - const isAlreadyCompressed = alreadyCompressedTypes.some(type => 63 - meta.mimeType.toLowerCase().startsWith(type) 64 - ); 65 - 66 - if (isAlreadyCompressed) { 67 - // Decompress the file before serving 68 - console.log(`[DEBUG SERVE] ${requestPath}: decompressing already-compressed media type`); 55 + if (!shouldServeCompressed) { 56 + // This shouldn't happen if caching is working correctly, but handle it gracefully 57 + console.log(`[DEBUG SERVE] ${requestPath}: decompressing file that shouldn't be compressed (${meta.mimeType})`); 69 58 const { gunzipSync } = await import('zlib'); 70 59 const decompressed = gunzipSync(content); 71 60 console.log(`[DEBUG SERVE] ${requestPath}: decompressed from ${content.length} to ${decompressed.length} bytes`); ··· 157 146 } 158 147 159 148 // Check if this is HTML content that needs rewriting 160 - // Note: For gzipped HTML with path rewriting, we need to decompress, rewrite, and serve uncompressed 161 - // This is a trade-off for the sites.wisp.place domain which needs path rewriting 149 + // We decompress, rewrite paths, then recompress for efficient delivery 162 150 if (isHtmlContent(requestPath, mimeType)) { 163 151 let content: string; 164 152 if (isGzipped) { ··· 168 156 } else { 169 157 content = readFileSync(cachedFile, 'utf-8'); 170 158 } 171 - const rewritten = rewriteHtmlPaths(content, basePath); 172 - return new Response(rewritten, { 159 + const rewritten = rewriteHtmlPaths(content, basePath, requestPath); 160 + 161 + // Recompress the HTML for efficient delivery 162 + const { gzipSync } = await import('zlib'); 163 + const recompressed = gzipSync(Buffer.from(rewritten, 'utf-8')); 164 + 165 + return new Response(recompressed, { 173 166 headers: { 174 167 'Content-Type': 'text/html; charset=utf-8', 168 + 'Content-Encoding': 'gzip', 175 169 }, 176 170 }); 177 171 } ··· 179 173 // Non-HTML files: serve gzipped content as-is with proper headers 180 174 const content = readFileSync(cachedFile); 181 175 if (isGzipped) { 182 - // Don't serve already-compressed media formats with Content-Encoding: gzip 183 - const alreadyCompressedTypes = [ 184 - 'video/', 'audio/', 'image/jpeg', 'image/jpg', 'image/png', 185 - 'image/gif', 'image/webp', 'application/pdf' 186 - ]; 176 + // Use shared function to determine if this should be served compressed 177 + const shouldServeCompressed = shouldCompressMimeType(mimeType); 187 178 188 - const isAlreadyCompressed = alreadyCompressedTypes.some(type => 189 - mimeType.toLowerCase().startsWith(type) 190 - ); 191 - 192 - if (isAlreadyCompressed) { 193 - // Decompress the file before serving 179 + if (!shouldServeCompressed) { 180 + // This shouldn't happen if caching is working correctly, but handle it gracefully 194 181 const { gunzipSync } = await import('zlib'); 195 182 const decompressed = gunzipSync(content); 196 183 return new Response(decompressed, { ··· 228 215 } 229 216 } 230 217 231 - // HTML needs path rewriting, so decompress if needed 218 + // HTML needs path rewriting, decompress, rewrite, then recompress 232 219 let content: string; 233 220 if (isGzipped) { 234 221 const { gunzipSync } = await import('zlib'); ··· 237 224 } else { 238 225 content = readFileSync(indexFile, 'utf-8'); 239 226 } 240 - const rewritten = rewriteHtmlPaths(content, basePath); 241 - return new Response(rewritten, { 227 + const indexPath = `${requestPath}/index.html`; 228 + const rewritten = rewriteHtmlPaths(content, basePath, indexPath); 229 + 230 + // Recompress the HTML for efficient delivery 231 + const { gzipSync } = await import('zlib'); 232 + const recompressed = gzipSync(Buffer.from(rewritten, 'utf-8')); 233 + 234 + return new Response(recompressed, { 242 235 headers: { 243 236 'Content-Type': 'text/html; charset=utf-8', 237 + 'Content-Encoding': 'gzip', 244 238 }, 245 239 }); 246 240 }
+1
package.json
··· 30 30 "lucide-react": "^0.546.0", 31 31 "react": "^19.2.0", 32 32 "react-dom": "^19.2.0", 33 + "react-shiki": "^0.9.0", 33 34 "tailwind-merge": "^3.3.1", 34 35 "tailwindcss": "4", 35 36 "tw-animate-css": "^1.4.0",
+23
public/components/ui/code-block.tsx
··· 1 + import ShikiHighlighter from 'react-shiki' 2 + 3 + interface CodeBlockProps { 4 + code: string 5 + language?: string 6 + className?: string 7 + } 8 + 9 + export function CodeBlock({ code, language = 'bash', className = '' }: CodeBlockProps) { 10 + return ( 11 + <ShikiHighlighter 12 + language={language} 13 + theme={{ 14 + light: 'catppuccin-latte', 15 + dark: 'catppuccin-mocha', 16 + }} 17 + defaultColor="light-dark()" 18 + className={className} 19 + > 20 + {code.trim()} 21 + </ShikiHighlighter> 22 + ) 23 + }
+1 -1
public/components/ui/radio-group.tsx
··· 27 27 <RadioGroupPrimitive.Item 28 28 data-slot="radio-group-item" 29 29 className={cn( 30 - "border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", 30 + "border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/50 aspect-square size-4 shrink-0 rounded-full border border-black/30 dark:border-white/30 shadow-inner transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", 31 31 className 32 32 )} 33 33 {...props}
+2 -2
public/components/ui/tabs.tsx
··· 24 24 <TabsPrimitive.List 25 25 data-slot="tabs-list" 26 26 className={cn( 27 - "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]", 27 + "bg-muted dark:bg-muted/80 text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]", 28 28 className 29 29 )} 30 30 {...props} ··· 40 40 <TabsPrimitive.Trigger 41 41 data-slot="tabs-trigger" 42 42 className={cn( 43 - "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 43 + "data-[state=active]:bg-background dark:data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-border focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-border dark:data-[state=active]:shadow-sm text-foreground dark:text-muted-foreground/70 inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 44 44 className 45 45 )} 46 46 {...props}
+268 -19
public/editor/editor.tsx
··· 38 38 Settings 39 39 } from 'lucide-react' 40 40 import { RadioGroup, RadioGroupItem } from '@public/components/ui/radio-group' 41 + import { CodeBlock } from '@public/components/ui/code-block' 41 42 42 43 import Layout from '@public/layouts' 43 44 ··· 579 580 </div> 580 581 581 582 <Tabs defaultValue="sites" className="space-y-6 w-full"> 582 - <TabsList className="grid w-full grid-cols-3 max-w-md"> 583 + <TabsList className="grid w-full grid-cols-4"> 583 584 <TabsTrigger value="sites">Sites</TabsTrigger> 584 585 <TabsTrigger value="domains">Domains</TabsTrigger> 585 586 <TabsTrigger value="upload">Upload</TabsTrigger> 587 + <TabsTrigger value="cli">CLI</TabsTrigger> 586 588 </TabsList> 587 589 588 590 {/* Sites Tab */} ··· 884 886 </CardHeader> 885 887 <CardContent className="space-y-6"> 886 888 <div className="space-y-4"> 887 - <RadioGroup 888 - value={siteMode} 889 - onValueChange={(value) => setSiteMode(value as 'existing' | 'new')} 890 - disabled={isUploading} 891 - > 892 - <div className="flex items-center space-x-2"> 893 - <RadioGroupItem value="existing" id="existing" /> 894 - <Label htmlFor="existing" className="cursor-pointer"> 895 - Update existing site 896 - </Label> 897 - </div> 898 - <div className="flex items-center space-x-2"> 899 - <RadioGroupItem value="new" id="new" /> 900 - <Label htmlFor="new" className="cursor-pointer"> 901 - Create new site 902 - </Label> 903 - </div> 904 - </RadioGroup> 889 + <div className="p-4 bg-muted/50 rounded-lg"> 890 + <RadioGroup 891 + value={siteMode} 892 + onValueChange={(value) => setSiteMode(value as 'existing' | 'new')} 893 + disabled={isUploading} 894 + > 895 + <div className="flex items-center space-x-2"> 896 + <RadioGroupItem value="existing" id="existing" /> 897 + <Label htmlFor="existing" className="cursor-pointer"> 898 + Update existing site 899 + </Label> 900 + </div> 901 + <div className="flex items-center space-x-2"> 902 + <RadioGroupItem value="new" id="new" /> 903 + <Label htmlFor="new" className="cursor-pointer"> 904 + Create new site 905 + </Label> 906 + </div> 907 + </RadioGroup> 908 + </div> 905 909 906 910 {siteMode === 'existing' ? ( 907 911 <div className="space-y-2"> ··· 1074 1078 </> 1075 1079 )} 1076 1080 </Button> 1081 + </CardContent> 1082 + </Card> 1083 + </TabsContent> 1084 + 1085 + {/* CLI Tab */} 1086 + <TabsContent value="cli" className="space-y-4 min-h-[400px]"> 1087 + <Card> 1088 + <CardHeader> 1089 + <div className="flex items-center gap-2 mb-2"> 1090 + <CardTitle>Wisp CLI Tool</CardTitle> 1091 + <Badge variant="secondary" className="text-xs">v0.1.0</Badge> 1092 + <Badge variant="outline" className="text-xs">Alpha</Badge> 1093 + </div> 1094 + <CardDescription> 1095 + Deploy static sites directly from your terminal 1096 + </CardDescription> 1097 + </CardHeader> 1098 + <CardContent className="space-y-6"> 1099 + <div className="prose prose-sm max-w-none dark:prose-invert"> 1100 + <p className="text-sm text-muted-foreground"> 1101 + The Wisp CLI is a command-line tool for deploying static websites directly to your AT Protocol account. 1102 + Authenticate with app password or OAuth and deploy from CI/CD pipelines. 1103 + </p> 1104 + </div> 1105 + 1106 + <div className="space-y-3"> 1107 + <h3 className="text-sm font-semibold">Download CLI</h3> 1108 + <div className="grid gap-2"> 1109 + <div className="p-3 bg-muted/50 hover:bg-muted rounded-lg transition-colors border border-border"> 1110 + <a 1111 + href="https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-macos-arm64" 1112 + target="_blank" 1113 + rel="noopener noreferrer" 1114 + className="flex items-center justify-between mb-2" 1115 + > 1116 + <span className="font-mono text-sm">macOS (Apple Silicon)</span> 1117 + <ExternalLink className="w-4 h-4 text-muted-foreground" /> 1118 + </a> 1119 + <div className="text-xs text-muted-foreground"> 1120 + <span className="font-mono">SHA256: 637e325d9668ca745e01493d80dfc72447ef0a889b313e28913ca65c94c7aaae</span> 1121 + </div> 1122 + </div> 1123 + <div className="p-3 bg-muted/50 hover:bg-muted rounded-lg transition-colors border border-border"> 1124 + <a 1125 + href="https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-aarch64-linux" 1126 + target="_blank" 1127 + rel="noopener noreferrer" 1128 + className="flex items-center justify-between mb-2" 1129 + > 1130 + <span className="font-mono text-sm">Linux (ARM64)</span> 1131 + <ExternalLink className="w-4 h-4 text-muted-foreground" /> 1132 + </a> 1133 + <div className="text-xs text-muted-foreground"> 1134 + <span className="font-mono">SHA256: 01561656b64826f95b39f13c65c97da8bcc63ecd9f4d7e4e369c8ba8c903c22a</span> 1135 + </div> 1136 + </div> 1137 + <div className="p-3 bg-muted/50 hover:bg-muted rounded-lg transition-colors border border-border"> 1138 + <a 1139 + href="https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux" 1140 + target="_blank" 1141 + rel="noopener noreferrer" 1142 + className="flex items-center justify-between mb-2" 1143 + > 1144 + <span className="font-mono text-sm">Linux (x86_64)</span> 1145 + <ExternalLink className="w-4 h-4 text-muted-foreground" /> 1146 + </a> 1147 + <div className="text-xs text-muted-foreground"> 1148 + <span className="font-mono">SHA256: 1ff485b9bcf89bc5721a862863c4843cf4530cbcd2489cf200cb24a44f7865a2</span> 1149 + </div> 1150 + </div> 1151 + </div> 1152 + </div> 1153 + 1154 + <div className="space-y-3"> 1155 + <h3 className="text-sm font-semibold">Basic Usage</h3> 1156 + <CodeBlock 1157 + code={`# Download and make executable 1158 + curl -O https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-macos-arm64 1159 + chmod +x wisp-cli-macos-arm64 1160 + 1161 + # Deploy your site (will use OAuth) 1162 + ./wisp-cli-macos-arm64 your-handle.bsky.social \\ 1163 + --path ./dist \\ 1164 + --site my-site 1165 + 1166 + # Your site will be available at: 1167 + # https://sites.wisp.place/your-handle/my-site`} 1168 + language="bash" 1169 + /> 1170 + </div> 1171 + 1172 + <div className="space-y-3"> 1173 + <h3 className="text-sm font-semibold">CI/CD with Tangled Spindle</h3> 1174 + <p className="text-xs text-muted-foreground"> 1175 + Deploy automatically on every push using{' '} 1176 + <a 1177 + href="https://blog.tangled.org/ci" 1178 + target="_blank" 1179 + rel="noopener noreferrer" 1180 + className="text-accent hover:underline" 1181 + > 1182 + Tangled Spindle 1183 + </a> 1184 + </p> 1185 + 1186 + <div className="space-y-4"> 1187 + <div> 1188 + <h4 className="text-xs font-semibold mb-2 flex items-center gap-2"> 1189 + <span>Example 1: Simple Asset Publishing</span> 1190 + <Badge variant="secondary" className="text-xs">Copy Files</Badge> 1191 + </h4> 1192 + <CodeBlock 1193 + code={`when: 1194 + - event: ['push'] 1195 + branch: ['main'] 1196 + - event: ['manual'] 1197 + 1198 + engine: 'nixery' 1199 + 1200 + clone: 1201 + skip: false 1202 + depth: 1 1203 + 1204 + dependencies: 1205 + nixpkgs: 1206 + - coreutils 1207 + - curl 1208 + 1209 + environment: 1210 + SITE_PATH: '.' # Copy entire repo 1211 + SITE_NAME: 'myWebbedSite' 1212 + WISP_HANDLE: 'your-handle.bsky.social' 1213 + 1214 + steps: 1215 + - name: deploy assets to wisp 1216 + command: | 1217 + # Download Wisp CLI 1218 + curl https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli 1219 + chmod +x wisp-cli 1220 + 1221 + # Deploy to Wisp 1222 + ./wisp-cli \\ 1223 + "$WISP_HANDLE" \\ 1224 + --path "$SITE_PATH" \\ 1225 + --site "$SITE_NAME" \\ 1226 + --password "$WISP_APP_PASSWORD" 1227 + 1228 + # Output 1229 + #Deployed site 'myWebbedSite': at://did:plc:ttdrpj45ibqunmfhdsb4zdwq/place.wisp.fs/myWebbedSite 1230 + #Available at: https://sites.wisp.place/did:plc:ttdrpj45ibqunmfhdsb4zdwq/myWebbedSite 1231 + `} 1232 + language="yaml" 1233 + /> 1234 + </div> 1235 + 1236 + <div> 1237 + <h4 className="text-xs font-semibold mb-2 flex items-center gap-2"> 1238 + <span>Example 2: React/Vite Build & Deploy</span> 1239 + <Badge variant="secondary" className="text-xs">Full Build</Badge> 1240 + </h4> 1241 + <CodeBlock 1242 + code={`when: 1243 + - event: ['push'] 1244 + branch: ['main'] 1245 + - event: ['manual'] 1246 + 1247 + engine: 'nixery' 1248 + 1249 + clone: 1250 + skip: false 1251 + depth: 1 1252 + submodules: false 1253 + 1254 + dependencies: 1255 + nixpkgs: 1256 + - nodejs 1257 + - coreutils 1258 + - curl 1259 + github:NixOS/nixpkgs/nixpkgs-unstable: 1260 + - bun 1261 + 1262 + environment: 1263 + SITE_PATH: 'dist' 1264 + SITE_NAME: 'my-react-site' 1265 + WISP_HANDLE: 'your-handle.bsky.social' 1266 + 1267 + steps: 1268 + - name: build site 1269 + command: | 1270 + # necessary to ensure bun is in PATH 1271 + export PATH="$HOME/.nix-profile/bin:$PATH" 1272 + 1273 + bun install --frozen-lockfile 1274 + 1275 + # build with vite, run directly to get around env issues 1276 + bun node_modules/.bin/vite build 1277 + 1278 + - name: deploy to wisp 1279 + command: | 1280 + # Download Wisp CLI 1281 + curl https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli 1282 + chmod +x wisp-cli 1283 + 1284 + # Deploy to Wisp 1285 + ./wisp-cli \\ 1286 + "$WISP_HANDLE" \\ 1287 + --path "$SITE_PATH" \\ 1288 + --site "$SITE_NAME" \\ 1289 + --password "$WISP_APP_PASSWORD"`} 1290 + language="yaml" 1291 + /> 1292 + </div> 1293 + </div> 1294 + 1295 + <div className="p-3 bg-muted/30 rounded-lg border-l-4 border-accent"> 1296 + <p className="text-xs text-muted-foreground"> 1297 + <strong className="text-foreground">Note:</strong> Set <code className="px-1.5 py-0.5 bg-background rounded text-xs">WISP_APP_PASSWORD</code> as a secret in your Tangled Spindle repository settings. 1298 + Generate an app password from your AT Protocol account settings. 1299 + </p> 1300 + </div> 1301 + </div> 1302 + 1303 + <div className="space-y-3"> 1304 + <h3 className="text-sm font-semibold">Learn More</h3> 1305 + <div className="grid gap-2"> 1306 + <a 1307 + href="https://tangled.org/@nekomimi.pet/wisp.place-monorepo/tree/main/cli" 1308 + target="_blank" 1309 + rel="noopener noreferrer" 1310 + className="flex items-center justify-between p-3 bg-muted/50 hover:bg-muted rounded-lg transition-colors border border-border" 1311 + > 1312 + <span className="text-sm">Source Code</span> 1313 + <ExternalLink className="w-4 h-4 text-muted-foreground" /> 1314 + </a> 1315 + <a 1316 + href="https://blog.tangled.org/ci" 1317 + target="_blank" 1318 + rel="noopener noreferrer" 1319 + className="flex items-center justify-between p-3 bg-muted/50 hover:bg-muted rounded-lg transition-colors border border-border" 1320 + > 1321 + <span className="text-sm">Tangled Spindle CI/CD</span> 1322 + <ExternalLink className="w-4 h-4 text-muted-foreground" /> 1323 + </a> 1324 + </div> 1325 + </div> 1077 1326 </CardContent> 1078 1327 </Card> 1079 1328 </TabsContent>
+24
public/layouts/index.tsx
··· 1 1 import type { PropsWithChildren } from 'react' 2 + import { useEffect } from 'react' 2 3 3 4 import { QueryClientProvider, QueryClient } from '@tanstack/react-query' 4 5 import clsx from 'clsx' ··· 12 13 } 13 14 14 15 export default function Layout({ children, className }: LayoutProps) { 16 + useEffect(() => { 17 + // Function to update dark mode based on system preference 18 + const updateDarkMode = (e: MediaQueryList | MediaQueryListEvent) => { 19 + if (e.matches) { 20 + document.documentElement.classList.add('dark') 21 + } else { 22 + document.documentElement.classList.remove('dark') 23 + } 24 + } 25 + 26 + // Create media query 27 + const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)') 28 + 29 + // Set initial value 30 + updateDarkMode(darkModeQuery) 31 + 32 + // Listen for changes 33 + darkModeQuery.addEventListener('change', updateDarkMode) 34 + 35 + // Cleanup 36 + return () => darkModeQuery.removeEventListener('change', updateDarkMode) 37 + }, []) 38 + 15 39 return ( 16 40 <QueryClientProvider client={client}> 17 41 <div
+62 -36
public/styles/global.css
··· 1 1 @import "tailwindcss"; 2 2 @import "tw-animate-css"; 3 3 4 - @custom-variant dark (&:is(.dark *)); 4 + @custom-variant dark (@media (prefers-color-scheme: dark)); 5 5 6 6 :root { 7 + color-scheme: light; 8 + 7 9 /* Warm beige background inspired by Sunset design #E9DDD8 */ 8 10 --background: oklch(0.90 0.012 35); 9 11 /* Very dark brown text for strong contrast #2A2420 */ ··· 57 59 } 58 60 59 61 .dark { 60 - /* #413C58 - violet background for dark mode */ 61 - --background: oklch(0.28 0.04 285); 62 - /* #F2E7C9 - parchment text */ 63 - --foreground: oklch(0.93 0.03 85); 62 + color-scheme: dark; 64 63 65 - --card: oklch(0.32 0.04 285); 66 - --card-foreground: oklch(0.93 0.03 85); 64 + /* Slate violet background - #2C2C2C with violet tint */ 65 + --background: oklch(0.23 0.015 285); 66 + /* Light gray text - #E4E4E4 */ 67 + --foreground: oklch(0.90 0.005 285); 67 68 68 - --popover: oklch(0.32 0.04 285); 69 - --popover-foreground: oklch(0.93 0.03 85); 69 + /* Slightly lighter slate for cards */ 70 + --card: oklch(0.28 0.015 285); 71 + --card-foreground: oklch(0.90 0.005 285); 70 72 71 - /* #FFAAD2 - pink primary in dark mode */ 72 - --primary: oklch(0.78 0.15 345); 73 - --primary-foreground: oklch(0.32 0.04 285); 73 + --popover: oklch(0.28 0.015 285); 74 + --popover-foreground: oklch(0.90 0.005 285); 74 75 75 - --accent: oklch(0.78 0.15 345); 76 - --accent-foreground: oklch(0.32 0.04 285); 76 + /* Lavender buttons - #B39CD0 */ 77 + --primary: oklch(0.70 0.10 295); 78 + --primary-foreground: oklch(0.23 0.015 285); 77 79 78 - --secondary: oklch(0.56 0.08 220); 79 - --secondary-foreground: oklch(0.93 0.03 85); 80 + /* Soft pink accent - #FFC1CC */ 81 + --accent: oklch(0.85 0.08 5); 82 + --accent-foreground: oklch(0.23 0.015 285); 80 83 81 - --muted: oklch(0.38 0.03 285); 82 - --muted-foreground: oklch(0.75 0.02 85); 84 + /* Light cyan secondary - #A8DADC */ 85 + --secondary: oklch(0.82 0.05 200); 86 + --secondary-foreground: oklch(0.23 0.015 285); 83 87 84 - --border: oklch(0.42 0.03 285); 85 - --input: oklch(0.42 0.03 285); 86 - --ring: oklch(0.78 0.15 345); 88 + /* Muted slate areas */ 89 + --muted: oklch(0.33 0.015 285); 90 + --muted-foreground: oklch(0.72 0.01 285); 87 91 88 - --destructive: oklch(0.577 0.245 27.325); 89 - --destructive-foreground: oklch(0.985 0 0); 92 + /* Subtle borders */ 93 + --border: oklch(0.38 0.02 285); 94 + --input: oklch(0.30 0.015 285); 95 + --ring: oklch(0.70 0.10 295); 90 96 91 - --chart-1: oklch(0.78 0.15 345); 92 - --chart-2: oklch(0.93 0.03 85); 93 - --chart-3: oklch(0.56 0.08 220); 94 - --chart-4: oklch(0.85 0.02 130); 95 - --chart-5: oklch(0.32 0.04 285); 96 - --sidebar: oklch(0.205 0 0); 97 - --sidebar-foreground: oklch(0.985 0 0); 98 - --sidebar-primary: oklch(0.488 0.243 264.376); 99 - --sidebar-primary-foreground: oklch(0.985 0 0); 100 - --sidebar-accent: oklch(0.269 0 0); 101 - --sidebar-accent-foreground: oklch(0.985 0 0); 102 - --sidebar-border: oklch(0.269 0 0); 103 - --sidebar-ring: oklch(0.439 0 0); 97 + /* Warm destructive color */ 98 + --destructive: oklch(0.60 0.22 27); 99 + --destructive-foreground: oklch(0.98 0.01 85); 100 + 101 + /* Chart colors using the accent palette */ 102 + --chart-1: oklch(0.85 0.08 5); 103 + --chart-2: oklch(0.82 0.05 200); 104 + --chart-3: oklch(0.70 0.10 295); 105 + --chart-4: oklch(0.75 0.08 340); 106 + --chart-5: oklch(0.65 0.08 180); 107 + 108 + /* Sidebar slate */ 109 + --sidebar: oklch(0.20 0.015 285); 110 + --sidebar-foreground: oklch(0.90 0.005 285); 111 + --sidebar-primary: oklch(0.70 0.10 295); 112 + --sidebar-primary-foreground: oklch(0.20 0.015 285); 113 + --sidebar-accent: oklch(0.28 0.015 285); 114 + --sidebar-accent-foreground: oklch(0.90 0.005 285); 115 + --sidebar-border: oklch(0.32 0.02 285); 116 + --sidebar-ring: oklch(0.70 0.10 295); 104 117 } 105 118 106 119 @theme inline { ··· 164 177 .arrow-animate { 165 178 animation: arrow-bounce 1.5s ease-in-out infinite; 166 179 } 180 + 181 + /* Shiki syntax highlighting styles */ 182 + .shiki-wrapper { 183 + border-radius: 0.5rem; 184 + padding: 1rem; 185 + overflow-x: auto; 186 + border: 1px solid hsl(var(--border)); 187 + } 188 + 189 + .shiki-wrapper pre { 190 + margin: 0 !important; 191 + padding: 0 !important; 192 + }
+16 -8
src/index.ts
··· 1 1 import { Elysia } from 'elysia' 2 + import type { Context } from 'elysia' 2 3 import { cors } from '@elysiajs/cors' 3 - import { openapi, fromTypes } from '@elysiajs/openapi' 4 4 import { staticPlugin } from '@elysiajs/static' 5 5 6 6 import type { Config } from './lib/types' ··· 58 58 dnsVerifier.start() 59 59 logger.info('DNS Verifier Started - checking custom domains every 10 minutes') 60 60 61 - export const app = new Elysia() 62 - .use(openapi({ 63 - references: fromTypes() 64 - })) 61 + export const app = new Elysia({ 62 + serve: { 63 + maxPayloadLength: 1024 * 1024 * 128 * 3, 64 + development: Bun.env.NODE_ENV !== 'production' ? true : false, 65 + id: Bun.env.NODE_ENV !== 'production' ? undefined : null, 66 + } 67 + }) 65 68 // Observability middleware 66 69 .onBeforeHandle(observabilityMiddleware('main-app').beforeHandle) 67 - .onAfterHandle((ctx) => { 70 + .onAfterHandle((ctx: Context) => { 68 71 observabilityMiddleware('main-app').afterHandle(ctx) 69 72 // Security headers middleware 70 73 const { set } = ctx ··· 104 107 prefix: '/' 105 108 }) 106 109 ) 107 - .get('/client-metadata.json', (c) => { 110 + .get('/client-metadata.json', () => { 108 111 return createClientMetadata(config) 109 112 }) 110 - .get('/jwks.json', async (c) => { 113 + .get('/jwks.json', async () => { 111 114 const keys = await getCurrentKeys() 112 115 if (!keys.length) return { keys: [] } 113 116 ··· 143 146 error: error instanceof Error ? error.message : String(error) 144 147 } 145 148 } 149 + }) 150 + .get('/.well-known/atproto-did', ({ set }) => { 151 + // Return plain text DID for AT Protocol domain verification 152 + set.headers['Content-Type'] = 'text/plain' 153 + return 'did:plc:7puq73yz2hkvbcpdhnsze2qw' 146 154 }) 147 155 .use(cors({ 148 156 origin: config.domain,
+10 -6
src/lib/observability.ts
··· 312 312 service 313 313 ) 314 314 315 - logCollector.error( 316 - `Request failed: ${request.method} ${url.pathname}`, 317 - service, 318 - error, 319 - { statusCode: set.status || 500 } 320 - ) 315 + // Don't log 404 errors 316 + const statusCode = set.status || 500 317 + if (statusCode !== 404) { 318 + logCollector.error( 319 + `Request failed: ${request.method} ${url.pathname}`, 320 + service, 321 + error, 322 + { statusCode } 323 + ) 324 + } 321 325 } 322 326 } 323 327 }