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

refactor(hosting-service): migrate from Elysia to Hono and improve fetch safety

- Switch from Elysia to Hono framework
- Remove OpenTelemetry dependencies
- Fix HTML path rewriting to skip relative and hash paths
- Add size limits and redirect handling to safe-fetch
- Add tests

+22
.tangled/workflows/test.yml
···
+
when:
+
- event: ["push", "pull_request"]
+
branch: main
+
+
engine: nixery
+
+
dependencies:
+
nixpkgs:
+
- git
+
github:NixOS/nixpkgs/nixpkgs-unstable:
+
- bun
+
+
steps:
+
- name: install dependencies
+
command: |
+
export PATH="$HOME/.nix-profile/bin:$PATH"
+
bun install
+
+
- name: run all tests
+
command: |
+
export PATH="$HOME/.nix-profile/bin:$PATH"
+
bun test
+12 -190
hosting-service/bun.lock
···
"@atproto/lexicon": "^0.5.1",
"@atproto/sync": "^0.1.36",
"@atproto/xrpc": "^0.7.5",
-
"@elysiajs/node": "^1.4.2",
-
"@elysiajs/opentelemetry": "latest",
-
"elysia": "^1.4.15",
+
"@hono/node-server": "^1.19.6",
+
"hono": "^4.10.4",
"mime-types": "^2.1.35",
"multiformats": "^13.4.1",
"postgres": "^3.4.5",
},
"devDependencies": {
+
"@types/bun": "^1.3.1",
"@types/mime-types": "^2.1.4",
"@types/node": "^22.10.5",
"tsx": "^4.19.2",
···
"@atproto/xrpc-server": ["@atproto/xrpc-server@0.9.5", "", { "dependencies": { "@atproto/common": "^0.4.12", "@atproto/crypto": "^0.4.4", "@atproto/lexicon": "^0.5.1", "@atproto/xrpc": "^0.7.5", "cbor-x": "^1.5.1", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "uint8arrays": "3.0.0", "ws": "^8.12.0", "zod": "^3.23.8" } }, ""],
-
"@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
-
"@cbor-extract/cbor-extract-darwin-arm64": ["@cbor-extract/cbor-extract-darwin-arm64@2.2.0", "", { "os": "darwin", "cpu": "arm64" }, ""],
-
-
"@elysiajs/node": ["@elysiajs/node@1.4.2", "", { "dependencies": { "crossws": "^0.4.1", "srvx": "^0.9.4" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-zqeBAV4/faCcmIEjCp3g6jRwsbaWsd5HqmlEf3CirD9HkTWQNo4T+GN/qGZi7zgd84D3Kzxsny7ZTMXEfrDSXQ=="],
-
-
"@elysiajs/opentelemetry": ["@elysiajs/opentelemetry@1.4.6", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-jR7t4M6ZvMnBqzzHsNTL6y3sNq9jbGi2vKxbkizi/OO5tlvlKl/rnBGyFjZUjQ1Hte7rCz+2kfmgOQMhkjk+Og=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="],
···
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="],
-
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.0", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg=="],
-
-
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],
+
"@hono/node-server": ["@hono/node-server@1.19.6", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw=="],
"@ipld/dag-cbor": ["@ipld/dag-cbor@7.0.3", "", { "dependencies": { "cborg": "^1.6.0", "multiformats": "^9.5.4" } }, ""],
-
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
-
"@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, ""],
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, ""],
-
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
-
-
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q=="],
-
-
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA=="],
-
-
"@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
-
-
"@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/sdk-logs": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+3MDfa5YQPGM3WXxW9kqGD85Q7s9wlEMVNhXXG7tYFLnIeaseUt9YtCeFhEDFzfEktacdFpOtXmJuNW8cHbU5A=="],
-
-
"@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/sdk-logs": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-KfWw49htbGGp9s8N4KI8EQ9XuqKJ0VG+yVYVYFiCYSjEV32qpQ5qZ9UZBzOZ6xRb+E16SXOSCT3RkqBVSABZ+g=="],
-
-
"@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-GmahpUU/55hxfH4TP77ChOfftADsCq/nuri73I/AVLe2s4NIglvTsaACkFVZAVmnXXyPS00Fk3x27WS3yO07zA=="],
-
-
"@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew=="],
-
-
"@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="],
-
-
"@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-E+uPj0yyvz81U9pvLZp3oHtFrEzNSqKGVkIViTQY1rH3TOobeJPSpLnTVXACnCwkPR5XeTvPnK3pZ2Kni8AFMg=="],
-
-
"@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ZYdlU9r0USuuYppiDyU2VFRA0kFl855ylnb3N/2aOlXrbA4PMCznen7gmPbetGQu7pz8Jbaf4fwvrDnVdQQXSw=="],
-
-
"@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-hmeZrUkFl1YMsgukSuHCFPYeF9df0hHoKeHUthRKFCxiURs+GwF1VuabuHmBMZnjTbsuvNjOB+JSs37Csem/5Q=="],
-
-
"@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Goi//m/7ZHeUedxTGVmEzH19NgqJY+Bzr6zXo1Rni1+hwqaksEyJ44gdlEMREu6dzX1DlAaH/qSykSVzdrdafA=="],
-
-
"@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-V9TDSD3PjK1OREw2iT9TUTzNYEVWJk4Nhodzhp9eiz4onDMYmPy3LaGbPv81yIR6dUb/hNp/SIhpiCHwFUq2Vg=="],
-
-
"@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-icxaKZ+jZL/NHXX8Aru4HGsrdhK0MLcuRXkX5G5IRmCgoRLw+Br6I/nMVozX2xjGGwV7hw2g+4Slj8K7s4HbVg=="],
-
-
"@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg=="],
-
-
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
-
-
"@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="],
-
-
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
-
-
"@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA=="],
-
-
"@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA=="],
-
-
"@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
-
-
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA=="],
-
-
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
-
-
"@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", "@opentelemetry/exporter-logs-otlp-http": "0.200.0", "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", "@opentelemetry/exporter-prometheus": "0.200.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", "@opentelemetry/exporter-trace-otlp-http": "0.200.0", "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", "@opentelemetry/exporter-zipkin": "2.0.0", "@opentelemetry/instrumentation": "0.200.0", "@opentelemetry/propagator-b3": "2.0.0", "@opentelemetry/propagator-jaeger": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/sdk-trace-node": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A=="],
-
-
"@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw=="],
-
-
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.0.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.0.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-omdilCZozUjQwY3uZRBwbaRMJ3p09l4t187Lsdf0dGMye9WKD4NGcpgZRvqhI1dwcH6og+YXQEtoO9Wx3ykilg=="],
-
-
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.37.0", "", {}, "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA=="],
-
-
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
-
-
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
-
-
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
-
-
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
-
-
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
-
-
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
-
-
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
-
-
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
-
-
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
-
-
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
-
-
"@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="],
-
-
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
-
-
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
+
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
"@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="],
"@types/node": ["@types/node@22.18.12", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog=="],
-
"@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="],
+
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, ""],
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, ""],
-
"acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
-
-
"acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
-
-
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
-
-
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
-
"array-flatten": ["array-flatten@1.1.1", "", {}, ""],
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, ""],
···
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, ""],
+
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
+
"bytes": ["bytes@3.1.2", "", {}, ""],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, ""],
···
"cborg": ["cborg@1.10.2", "", { "bin": "cli.js" }, ""],
-
"cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="],
-
-
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
-
-
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
-
-
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, ""],
"content-type": ["content-type@1.0.5", "", {}, ""],
-
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
+
"cookie": ["cookie@0.7.1", "", {}, ""],
"cookie-signature": ["cookie-signature@1.0.6", "", {}, ""],
-
"crossws": ["crossws@0.4.1", "", { "peerDependencies": { "srvx": ">=0.7.1" }, "optionalPeers": ["srvx"] }, "sha512-E7WKBcHVhAVrY6JYD5kteNqVq1GSZxqGrdSiwXR9at+XHi43HJoCQKXcCczR5LBnBquFZPsB3o7HklulKoBU5w=="],
+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, ""],
···
"ee-first": ["ee-first@1.1.1", "", {}, ""],
-
"elysia": ["elysia@1.4.15", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-RaDqqZdLuC4UJetfVRQ4Z5aVpGgEtQ+pZnsbI4ZzEaf3l/MzuHcqSVoL/Fue3d6qE4RV9HMB2rAZaHyPIxkyzg=="],
-
-
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
-
"encodeurl": ["encodeurl@2.0.0", "", {}, ""],
"es-define-property": ["es-define-property@1.0.1", "", {}, ""],
···
"esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": "bin/esbuild" }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="],
-
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
-
"escape-html": ["escape-html@1.0.3", "", {}, ""],
"etag": ["etag@1.8.1", "", {}, ""],
···
"events": ["events@3.3.0", "", {}, ""],
-
"exact-mirror": ["exact-mirror@0.2.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" } }, "sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA=="],
-
"express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, ""],
-
-
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
"fast-redact": ["fast-redact@3.5.0", "", {}, ""],
-
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
-
-
"file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="],
-
"finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, ""],
"forwarded": ["forwarded@0.2.0", "", {}, ""],
···
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, ""],
-
-
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, ""],
···
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, ""],
+
"hono": ["hono@4.10.4", "", {}, "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg=="],
+
"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" } }, ""],
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, ""],
"ieee754": ["ieee754@1.2.1", "", {}, ""],
-
"import-in-the-middle": ["import-in-the-middle@1.15.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA=="],
-
"inherits": ["inherits@2.0.4", "", {}, ""],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, ""],
-
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
-
-
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
-
"iso-datestring-validator": ["iso-datestring-validator@2.2.2", "", {}, ""],
-
-
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
-
-
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, ""],
"media-typer": ["media-typer@0.3.0", "", {}, ""],
-
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
-
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, ""],
"methods": ["methods@1.1.2", "", {}, ""],
···
"mime-db": ["mime-db@1.52.0", "", {}, ""],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, ""],
-
-
"module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="],
"ms": ["ms@2.0.0", "", {}, ""],
···
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, ""],
-
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
-
"p-finally": ["p-finally@1.0.0", "", {}, ""],
"p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, ""],
···
"parseurl": ["parseurl@1.3.3", "", {}, ""],
-
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
-
"path-to-regexp": ["path-to-regexp@0.1.12", "", {}, ""],
"pino": ["pino@8.21.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^1.2.0", "pino-std-serializers": "^6.0.0", "process-warning": "^3.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^3.7.0", "thread-stream": "^2.6.0" }, "bin": "bin.js" }, ""],
···
"process": ["process@0.11.10", "", {}, ""],
"process-warning": ["process-warning@3.0.0", "", {}, ""],
-
-
"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=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, ""],
···
"real-require": ["real-require@0.2.0", "", {}, ""],
-
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
-
-
"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=="],
-
-
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
-
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, ""],
···
"setprototypeof": ["setprototypeof@1.2.0", "", {}, ""],
-
"shimmer": ["shimmer@1.2.1", "", {}, "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="],
-
"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" } }, ""],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, ""],
···
"split2": ["split2@4.2.0", "", {}, ""],
-
"srvx": ["srvx@0.9.5", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-nQsA2c8q3XwbSn6kTxVQjz0zS096rV+Be2pzJwrYEAdtnYszLw4MTy8JWJjz1XEGBZwP0qW51SUIX3WdjdRemQ=="],
-
"statuses": ["statuses@2.0.1", "", {}, ""],
-
-
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, ""],
-
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
-
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
-
-
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
-
"thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, ""],
"tlds": ["tlds@1.261.0", "", { "bin": "bin.js" }, ""],
"toidentifier": ["toidentifier@1.0.1", "", {}, ""],
-
"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=="],
-
"tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": "dist/cli.mjs" }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="],
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, ""],
-
-
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
"uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, ""],
···
"vary": ["vary@1.1.2", "", {}, ""],
-
"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=="],
-
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, ""],
-
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
-
-
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
-
-
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
-
"zod": ["zod@3.25.76", "", {}, ""],
"@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, ""],
···
"@ipld/dag-cbor/multiformats": ["multiformats@9.9.0", "", {}, ""],
-
"@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
-
-
"express/cookie": ["cookie@0.7.1", "", {}, ""],
-
-
"require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
-
"send/encodeurl": ["encodeurl@1.0.2", "", {}, ""],
"send/ms": ["ms@2.1.3", "", {}, ""],
"uint8arrays/multiformats": ["multiformats@9.9.0", "", {}, ""],
-
-
"@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, ""],
-
-
"require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, ""],
}
}
+1
hosting-service/package.json
···
"postgres": "^3.4.5"
},
"devDependencies": {
+
"@types/bun": "^1.3.1",
"@types/mime-types": "^2.1.4",
"@types/node": "^22.10.5",
"tsx": "^4.19.2"
+9 -14
hosting-service/src/lib/html-rewriter.ts
···
return false;
}
-
// Don't rewrite pure anchors
-
if (path.startsWith('#')) return false;
+
// Don't rewrite pure anchors or paths that start with /#
+
if (path.startsWith('#') || path.startsWith('/#')) return false;
+
+
// Don't rewrite relative paths (./ or ../)
+
if (path.startsWith('./') || path.startsWith('../')) return false;
-
// Rewrite absolute paths (/) and relative paths (./ or ../ or plain filenames)
+
// Rewrite absolute paths (/)
return true;
}
···
return basePath + path.slice(1);
}
-
// Handle relative paths: ./file.js or ../file.js or file.js -> /base/file.js
-
// Strip leading ./ or ../ and just use the base path
-
let cleanPath = path;
-
if (cleanPath.startsWith('./')) {
-
cleanPath = cleanPath.slice(2);
-
} else if (cleanPath.startsWith('../')) {
-
// For sites.wisp.place, we can't go up from the site root, so just use base path
-
cleanPath = cleanPath.replace(/^(\.\.\/)+/, '');
-
}
-
-
return basePath + cleanPath;
+
// At this point, only plain filenames without ./ or ../ prefix should reach here
+
// But since we're filtering those in shouldRewritePath, this shouldn't happen
+
return path;
}
/**
+8 -3
hosting-service/src/lib/safe-fetch.ts
···
const FETCH_TIMEOUT = 120000; // 120 seconds
const FETCH_TIMEOUT_BLOB = 120000; // 2 minutes for blob downloads
const MAX_RESPONSE_SIZE = 10 * 1024 * 1024; // 10MB
+
const MAX_JSON_SIZE = 1024 * 1024; // 1MB
+
const MAX_BLOB_SIZE = 100 * 1024 * 1024; // 100MB
+
const MAX_REDIRECTS = 10;
function isBlockedHost(hostname: string): boolean {
const lowerHost = hostname.toLowerCase();
···
const response = await fetch(url, {
...options,
signal: controller.signal,
+
redirect: 'follow',
});
const contentLength = response.headers.get('content-length');
···
url: string,
options?: RequestInit & { maxSize?: number; timeout?: number }
): Promise<T> {
-
const maxJsonSize = options?.maxSize ?? 1024 * 1024; // 1MB default for JSON
+
const maxJsonSize = options?.maxSize ?? MAX_JSON_SIZE;
const response = await safeFetch(url, { ...options, maxSize: maxJsonSize });
if (!response.ok) {
···
url: string,
options?: RequestInit & { maxSize?: number; timeout?: number }
): Promise<Uint8Array> {
-
const maxBlobSize = options?.maxSize ?? MAX_RESPONSE_SIZE;
-
const response = await safeFetch(url, { ...options, maxSize: maxBlobSize });
+
const maxBlobSize = options?.maxSize ?? MAX_BLOB_SIZE;
+
const timeoutMs = options?.timeout ?? FETCH_TIMEOUT_BLOB;
+
const response = await safeFetch(url, { ...options, maxSize: maxBlobSize, timeout: timeoutMs });
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+169
hosting-service/src/lib/utils.test.ts
···
+
import { describe, test, expect } from 'bun:test'
+
import { sanitizePath, extractBlobCid } from './utils'
+
import { CID } from 'multiformats'
+
+
describe('sanitizePath', () => {
+
test('allows normal file paths', () => {
+
expect(sanitizePath('index.html')).toBe('index.html')
+
expect(sanitizePath('css/styles.css')).toBe('css/styles.css')
+
expect(sanitizePath('images/logo.png')).toBe('images/logo.png')
+
expect(sanitizePath('js/app.js')).toBe('js/app.js')
+
})
+
+
test('allows deeply nested paths', () => {
+
expect(sanitizePath('assets/images/icons/favicon.ico')).toBe('assets/images/icons/favicon.ico')
+
expect(sanitizePath('a/b/c/d/e/f.txt')).toBe('a/b/c/d/e/f.txt')
+
})
+
+
test('removes leading slashes', () => {
+
expect(sanitizePath('/index.html')).toBe('index.html')
+
expect(sanitizePath('//index.html')).toBe('index.html')
+
expect(sanitizePath('///index.html')).toBe('index.html')
+
expect(sanitizePath('/css/styles.css')).toBe('css/styles.css')
+
})
+
+
test('blocks parent directory traversal', () => {
+
expect(sanitizePath('../etc/passwd')).toBe('etc/passwd')
+
expect(sanitizePath('../../etc/passwd')).toBe('etc/passwd')
+
expect(sanitizePath('../../../etc/passwd')).toBe('etc/passwd')
+
expect(sanitizePath('css/../../../etc/passwd')).toBe('css/etc/passwd')
+
})
+
+
test('blocks directory traversal in middle of path', () => {
+
expect(sanitizePath('images/../../../etc/passwd')).toBe('images/etc/passwd')
+
// Note: sanitizePath only filters out ".." segments, doesn't resolve paths
+
expect(sanitizePath('a/b/../c')).toBe('a/b/c')
+
expect(sanitizePath('a/../b/../c')).toBe('a/b/c')
+
})
+
+
test('removes current directory references', () => {
+
expect(sanitizePath('./index.html')).toBe('index.html')
+
expect(sanitizePath('././index.html')).toBe('index.html')
+
expect(sanitizePath('css/./styles.css')).toBe('css/styles.css')
+
expect(sanitizePath('./css/./styles.css')).toBe('css/styles.css')
+
})
+
+
test('removes empty path segments', () => {
+
expect(sanitizePath('css//styles.css')).toBe('css/styles.css')
+
expect(sanitizePath('css///styles.css')).toBe('css/styles.css')
+
expect(sanitizePath('a//b//c')).toBe('a/b/c')
+
})
+
+
test('blocks null bytes', () => {
+
// Null bytes cause the entire segment to be filtered out
+
expect(sanitizePath('index.html\0.txt')).toBe('')
+
expect(sanitizePath('test\0')).toBe('')
+
// Null byte in middle segment
+
expect(sanitizePath('css/bad\0name/styles.css')).toBe('css/styles.css')
+
})
+
+
test('handles mixed attacks', () => {
+
expect(sanitizePath('/../../../etc/passwd')).toBe('etc/passwd')
+
expect(sanitizePath('/./././../etc/passwd')).toBe('etc/passwd')
+
expect(sanitizePath('//../../.\0./etc/passwd')).toBe('etc/passwd')
+
})
+
+
test('handles edge cases', () => {
+
expect(sanitizePath('')).toBe('')
+
expect(sanitizePath('/')).toBe('')
+
expect(sanitizePath('//')).toBe('')
+
expect(sanitizePath('.')).toBe('')
+
expect(sanitizePath('..')).toBe('')
+
expect(sanitizePath('../..')).toBe('')
+
})
+
+
test('preserves valid special characters in filenames', () => {
+
expect(sanitizePath('file-name.html')).toBe('file-name.html')
+
expect(sanitizePath('file_name.html')).toBe('file_name.html')
+
expect(sanitizePath('file.name.html')).toBe('file.name.html')
+
expect(sanitizePath('file (1).html')).toBe('file (1).html')
+
expect(sanitizePath('file@2x.png')).toBe('file@2x.png')
+
})
+
+
test('handles Unicode characters', () => {
+
expect(sanitizePath('文件.html')).toBe('文件.html')
+
expect(sanitizePath('файл.html')).toBe('файл.html')
+
expect(sanitizePath('ファイル.html')).toBe('ファイル.html')
+
})
+
})
+
+
describe('extractBlobCid', () => {
+
const TEST_CID = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y'
+
+
test('extracts CID from IPLD link', () => {
+
const blobRef = { $link: TEST_CID }
+
expect(extractBlobCid(blobRef)).toBe(TEST_CID)
+
})
+
+
test('extracts CID from typed BlobRef with CID object', () => {
+
const cid = CID.parse(TEST_CID)
+
const blobRef = { ref: cid }
+
const result = extractBlobCid(blobRef)
+
expect(result).toBe(TEST_CID)
+
})
+
+
test('extracts CID from typed BlobRef with IPLD link', () => {
+
const blobRef = {
+
ref: { $link: TEST_CID }
+
}
+
expect(extractBlobCid(blobRef)).toBe(TEST_CID)
+
})
+
+
test('extracts CID from untyped BlobRef', () => {
+
const blobRef = { cid: TEST_CID }
+
expect(extractBlobCid(blobRef)).toBe(TEST_CID)
+
})
+
+
test('returns null for invalid blob ref', () => {
+
expect(extractBlobCid(null)).toBe(null)
+
expect(extractBlobCid(undefined)).toBe(null)
+
expect(extractBlobCid({})).toBe(null)
+
expect(extractBlobCid('not-an-object')).toBe(null)
+
expect(extractBlobCid(123)).toBe(null)
+
})
+
+
test('returns null for malformed objects', () => {
+
expect(extractBlobCid({ wrongKey: 'value' })).toBe(null)
+
expect(extractBlobCid({ ref: 'not-a-cid' })).toBe(null)
+
expect(extractBlobCid({ ref: {} })).toBe(null)
+
})
+
+
test('handles nested structures from AT Proto API', () => {
+
// Real structure from AT Proto
+
const blobRef = {
+
$type: 'blob',
+
ref: CID.parse(TEST_CID),
+
mimeType: 'text/html',
+
size: 1234
+
}
+
expect(extractBlobCid(blobRef)).toBe(TEST_CID)
+
})
+
+
test('handles BlobRef with additional properties', () => {
+
const blobRef = {
+
ref: { $link: TEST_CID },
+
mimeType: 'image/png',
+
size: 5678,
+
someOtherField: 'value'
+
}
+
expect(extractBlobCid(blobRef)).toBe(TEST_CID)
+
})
+
+
test('prioritizes checking IPLD link first', () => {
+
// Direct $link takes precedence
+
const directLink = { $link: TEST_CID }
+
expect(extractBlobCid(directLink)).toBe(TEST_CID)
+
})
+
+
test('handles CID v0 format', () => {
+
const cidV0 = 'QmZ4tDuvesekSs4qM5ZBKpXiZGun7S2CYtEZRB3DYXkjGx'
+
const blobRef = { $link: cidV0 }
+
expect(extractBlobCid(blobRef)).toBe(cidV0)
+
})
+
+
test('handles CID v1 format', () => {
+
const cidV1 = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi'
+
const blobRef = { $link: cidV1 }
+
expect(extractBlobCid(blobRef)).toBe(cidV1)
+
})
+
})
+1 -1
hosting-service/tsconfig.json
···
"sourceMap": true,
/* Code doesn't run in DOM */
-
"lib": ["es2022"]
+
"lib": ["es2022"],
}
}
+1 -1
package.json
···
"name": "elysia-static",
"version": "1.0.50",
"scripts": {
-
"test": "echo \"Error: no test specified\" && exit 1",
+
"test": "bun test",
"dev": "bun run --watch src/index.ts",
"start": "bun run src/index.ts",
"build": "bun build --compile --target bun --outfile server src/index.ts"
+81
src/lib/csrf.test.ts
···
+
import { describe, test, expect } from 'bun:test'
+
import { verifyRequestOrigin } from './csrf'
+
+
describe('verifyRequestOrigin', () => {
+
test('should accept matching origin and host', () => {
+
expect(verifyRequestOrigin('https://example.com', ['example.com'])).toBe(true)
+
expect(verifyRequestOrigin('http://localhost:8000', ['localhost:8000'])).toBe(true)
+
expect(verifyRequestOrigin('https://app.example.com', ['app.example.com'])).toBe(true)
+
})
+
+
test('should accept origin matching one of multiple allowed hosts', () => {
+
const allowedHosts = ['example.com', 'app.example.com', 'localhost:8000']
+
expect(verifyRequestOrigin('https://example.com', allowedHosts)).toBe(true)
+
expect(verifyRequestOrigin('https://app.example.com', allowedHosts)).toBe(true)
+
expect(verifyRequestOrigin('http://localhost:8000', allowedHosts)).toBe(true)
+
})
+
+
test('should reject non-matching origin', () => {
+
expect(verifyRequestOrigin('https://evil.com', ['example.com'])).toBe(false)
+
expect(verifyRequestOrigin('https://fake-example.com', ['example.com'])).toBe(false)
+
expect(verifyRequestOrigin('https://example.com.evil.com', ['example.com'])).toBe(false)
+
})
+
+
test('should reject empty origin', () => {
+
expect(verifyRequestOrigin('', ['example.com'])).toBe(false)
+
})
+
+
test('should reject invalid URL format', () => {
+
expect(verifyRequestOrigin('not-a-url', ['example.com'])).toBe(false)
+
expect(verifyRequestOrigin('javascript:alert(1)', ['example.com'])).toBe(false)
+
expect(verifyRequestOrigin('file:///etc/passwd', ['example.com'])).toBe(false)
+
})
+
+
test('should handle different protocols correctly', () => {
+
// Same host, different protocols should match (we only check host)
+
expect(verifyRequestOrigin('http://example.com', ['example.com'])).toBe(true)
+
expect(verifyRequestOrigin('https://example.com', ['example.com'])).toBe(true)
+
})
+
+
test('should handle port numbers correctly', () => {
+
expect(verifyRequestOrigin('http://localhost:3000', ['localhost:3000'])).toBe(true)
+
expect(verifyRequestOrigin('http://localhost:3000', ['localhost:8000'])).toBe(false)
+
expect(verifyRequestOrigin('http://localhost', ['localhost'])).toBe(true)
+
})
+
+
test('should handle subdomains correctly', () => {
+
expect(verifyRequestOrigin('https://sub.example.com', ['sub.example.com'])).toBe(true)
+
expect(verifyRequestOrigin('https://sub.example.com', ['example.com'])).toBe(false)
+
})
+
+
test('should handle case sensitivity (exact match required)', () => {
+
// URL host is automatically lowercased by URL parser
+
expect(verifyRequestOrigin('https://EXAMPLE.COM', ['example.com'])).toBe(true)
+
expect(verifyRequestOrigin('https://example.com', ['example.com'])).toBe(true)
+
// But allowed hosts are case-sensitive
+
expect(verifyRequestOrigin('https://example.com', ['EXAMPLE.COM'])).toBe(false)
+
})
+
+
test('should handle trailing slashes in origin', () => {
+
expect(verifyRequestOrigin('https://example.com/', ['example.com'])).toBe(true)
+
})
+
+
test('should handle paths in origin (host extraction)', () => {
+
expect(verifyRequestOrigin('https://example.com/path/to/page', ['example.com'])).toBe(true)
+
expect(verifyRequestOrigin('https://evil.com/example.com', ['example.com'])).toBe(false)
+
})
+
+
test('should reject when allowed hosts is empty', () => {
+
expect(verifyRequestOrigin('https://example.com', [])).toBe(false)
+
})
+
+
test('should handle IPv4 addresses', () => {
+
expect(verifyRequestOrigin('http://127.0.0.1:8000', ['127.0.0.1:8000'])).toBe(true)
+
expect(verifyRequestOrigin('http://192.168.1.1', ['192.168.1.1'])).toBe(true)
+
})
+
+
test('should handle IPv6 addresses', () => {
+
expect(verifyRequestOrigin('http://[::1]:8000', ['[::1]:8000'])).toBe(true)
+
expect(verifyRequestOrigin('http://[2001:db8::1]', ['[2001:db8::1]'])).toBe(true)
+
})
+
})
+639
src/lib/wisp-utils.test.ts
···
+
import { describe, test, expect } from 'bun:test'
+
import {
+
shouldCompressFile,
+
compressFile,
+
processUploadedFiles,
+
createManifest,
+
updateFileBlobs,
+
type UploadedFile,
+
type FileUploadResult,
+
} from './wisp-utils'
+
import type { Directory } from '../lexicons/types/place/wisp/fs'
+
import { gunzipSync } from 'zlib'
+
import { BlobRef } from '@atproto/api'
+
import { CID } from 'multiformats/cid'
+
+
// Helper function to create a valid CID for testing
+
// Using a real valid CID from actual AT Protocol usage
+
const TEST_CID_STRING = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y'
+
+
function createMockBlobRef(mimeType: string, size: number): BlobRef {
+
// Create a properly formatted CID
+
const cid = CID.parse(TEST_CID_STRING)
+
return new BlobRef(cid, mimeType, size)
+
}
+
+
describe('shouldCompressFile', () => {
+
test('should compress HTML files', () => {
+
expect(shouldCompressFile('text/html')).toBe(true)
+
expect(shouldCompressFile('text/html; charset=utf-8')).toBe(true)
+
})
+
+
test('should compress CSS files', () => {
+
expect(shouldCompressFile('text/css')).toBe(true)
+
})
+
+
test('should compress JavaScript files', () => {
+
expect(shouldCompressFile('text/javascript')).toBe(true)
+
expect(shouldCompressFile('application/javascript')).toBe(true)
+
expect(shouldCompressFile('application/x-javascript')).toBe(true)
+
})
+
+
test('should compress JSON files', () => {
+
expect(shouldCompressFile('application/json')).toBe(true)
+
})
+
+
test('should compress SVG files', () => {
+
expect(shouldCompressFile('image/svg+xml')).toBe(true)
+
})
+
+
test('should compress XML files', () => {
+
expect(shouldCompressFile('text/xml')).toBe(true)
+
expect(shouldCompressFile('application/xml')).toBe(true)
+
})
+
+
test('should compress plain text files', () => {
+
expect(shouldCompressFile('text/plain')).toBe(true)
+
})
+
+
test('should NOT compress images', () => {
+
expect(shouldCompressFile('image/png')).toBe(false)
+
expect(shouldCompressFile('image/jpeg')).toBe(false)
+
expect(shouldCompressFile('image/jpg')).toBe(false)
+
expect(shouldCompressFile('image/gif')).toBe(false)
+
expect(shouldCompressFile('image/webp')).toBe(false)
+
})
+
+
test('should NOT compress videos', () => {
+
expect(shouldCompressFile('video/mp4')).toBe(false)
+
expect(shouldCompressFile('video/webm')).toBe(false)
+
})
+
+
test('should NOT compress already compressed formats', () => {
+
expect(shouldCompressFile('application/zip')).toBe(false)
+
expect(shouldCompressFile('application/gzip')).toBe(false)
+
expect(shouldCompressFile('application/pdf')).toBe(false)
+
})
+
+
test('should NOT compress fonts', () => {
+
expect(shouldCompressFile('font/woff')).toBe(false)
+
expect(shouldCompressFile('font/woff2')).toBe(false)
+
expect(shouldCompressFile('font/ttf')).toBe(false)
+
})
+
})
+
+
describe('compressFile', () => {
+
test('should compress text content', () => {
+
const content = Buffer.from('Hello, World! '.repeat(100))
+
const compressed = compressFile(content)
+
+
expect(compressed.length).toBeLessThan(content.length)
+
+
// Verify we can decompress it back
+
const decompressed = gunzipSync(compressed)
+
expect(decompressed.toString()).toBe(content.toString())
+
})
+
+
test('should compress HTML content significantly', () => {
+
const html = `
+
<!DOCTYPE html>
+
<html>
+
<head><title>Test</title></head>
+
<body>
+
${'<p>Hello World!</p>\n'.repeat(50)}
+
</body>
+
</html>
+
`
+
const content = Buffer.from(html)
+
const compressed = compressFile(content)
+
+
expect(compressed.length).toBeLessThan(content.length)
+
+
// Verify decompression
+
const decompressed = gunzipSync(compressed)
+
expect(decompressed.toString()).toBe(html)
+
})
+
+
test('should handle empty content', () => {
+
const content = Buffer.from('')
+
const compressed = compressFile(content)
+
const decompressed = gunzipSync(compressed)
+
expect(decompressed.toString()).toBe('')
+
})
+
+
test('should produce deterministic compression', () => {
+
const content = Buffer.from('Test content')
+
const compressed1 = compressFile(content)
+
const compressed2 = compressFile(content)
+
+
expect(compressed1.toString('base64')).toBe(compressed2.toString('base64'))
+
})
+
})
+
+
describe('processUploadedFiles', () => {
+
test('should process single root-level file', () => {
+
const files: UploadedFile[] = [
+
{
+
name: 'index.html',
+
content: Buffer.from('<html></html>'),
+
mimeType: 'text/html',
+
size: 13,
+
},
+
]
+
+
const result = processUploadedFiles(files)
+
+
expect(result.fileCount).toBe(1)
+
expect(result.directory.type).toBe('directory')
+
expect(result.directory.entries).toHaveLength(1)
+
expect(result.directory.entries[0].name).toBe('index.html')
+
+
const node = result.directory.entries[0].node
+
expect('blob' in node).toBe(true) // It's a file node
+
})
+
+
test('should process multiple root-level files', () => {
+
const files: UploadedFile[] = [
+
{
+
name: 'index.html',
+
content: Buffer.from('<html></html>'),
+
mimeType: 'text/html',
+
size: 13,
+
},
+
{
+
name: 'styles.css',
+
content: Buffer.from('body {}'),
+
mimeType: 'text/css',
+
size: 7,
+
},
+
{
+
name: 'script.js',
+
content: Buffer.from('console.log("hi")'),
+
mimeType: 'application/javascript',
+
size: 17,
+
},
+
]
+
+
const result = processUploadedFiles(files)
+
+
expect(result.fileCount).toBe(3)
+
expect(result.directory.entries).toHaveLength(3)
+
+
const names = result.directory.entries.map(e => e.name)
+
expect(names).toContain('index.html')
+
expect(names).toContain('styles.css')
+
expect(names).toContain('script.js')
+
})
+
+
test('should process files with subdirectories', () => {
+
const files: UploadedFile[] = [
+
{
+
name: 'dist/index.html',
+
content: Buffer.from('<html></html>'),
+
mimeType: 'text/html',
+
size: 13,
+
},
+
{
+
name: 'dist/css/styles.css',
+
content: Buffer.from('body {}'),
+
mimeType: 'text/css',
+
size: 7,
+
},
+
{
+
name: 'dist/js/app.js',
+
content: Buffer.from('console.log()'),
+
mimeType: 'application/javascript',
+
size: 13,
+
},
+
]
+
+
const result = processUploadedFiles(files)
+
+
expect(result.fileCount).toBe(3)
+
expect(result.directory.entries).toHaveLength(3) // index.html, css/, js/
+
+
// Check root has index.html (after base folder removal)
+
const indexEntry = result.directory.entries.find(e => e.name === 'index.html')
+
expect(indexEntry).toBeDefined()
+
+
// Check css directory exists
+
const cssDir = result.directory.entries.find(e => e.name === 'css')
+
expect(cssDir).toBeDefined()
+
expect('entries' in cssDir!.node).toBe(true)
+
+
if ('entries' in cssDir!.node) {
+
expect(cssDir!.node.entries).toHaveLength(1)
+
expect(cssDir!.node.entries[0].name).toBe('styles.css')
+
}
+
+
// Check js directory exists
+
const jsDir = result.directory.entries.find(e => e.name === 'js')
+
expect(jsDir).toBeDefined()
+
expect('entries' in jsDir!.node).toBe(true)
+
})
+
+
test('should handle deeply nested subdirectories', () => {
+
const files: UploadedFile[] = [
+
{
+
name: 'dist/deep/nested/folder/file.txt',
+
content: Buffer.from('content'),
+
mimeType: 'text/plain',
+
size: 7,
+
},
+
]
+
+
const result = processUploadedFiles(files)
+
+
expect(result.fileCount).toBe(1)
+
+
// Navigate through the directory structure (base folder removed)
+
const deepDir = result.directory.entries.find(e => e.name === 'deep')
+
expect(deepDir).toBeDefined()
+
expect('entries' in deepDir!.node).toBe(true)
+
+
if ('entries' in deepDir!.node) {
+
const nestedDir = deepDir!.node.entries.find(e => e.name === 'nested')
+
expect(nestedDir).toBeDefined()
+
+
if (nestedDir && 'entries' in nestedDir.node) {
+
const folderDir = nestedDir.node.entries.find(e => e.name === 'folder')
+
expect(folderDir).toBeDefined()
+
+
if (folderDir && 'entries' in folderDir.node) {
+
expect(folderDir.node.entries).toHaveLength(1)
+
expect(folderDir.node.entries[0].name).toBe('file.txt')
+
}
+
}
+
}
+
})
+
+
test('should remove base folder name from paths', () => {
+
const files: UploadedFile[] = [
+
{
+
name: 'dist/index.html',
+
content: Buffer.from('<html></html>'),
+
mimeType: 'text/html',
+
size: 13,
+
},
+
{
+
name: 'dist/css/styles.css',
+
content: Buffer.from('body {}'),
+
mimeType: 'text/css',
+
size: 7,
+
},
+
]
+
+
const result = processUploadedFiles(files)
+
+
// After removing 'dist/', we should have index.html and css/ at root
+
expect(result.directory.entries.find(e => e.name === 'index.html')).toBeDefined()
+
expect(result.directory.entries.find(e => e.name === 'css')).toBeDefined()
+
expect(result.directory.entries.find(e => e.name === 'dist')).toBeUndefined()
+
})
+
+
test('should handle empty file list', () => {
+
const files: UploadedFile[] = []
+
const result = processUploadedFiles(files)
+
+
expect(result.fileCount).toBe(0)
+
expect(result.directory.entries).toHaveLength(0)
+
})
+
+
test('should handle multiple files in same subdirectory', () => {
+
const files: UploadedFile[] = [
+
{
+
name: 'dist/assets/image1.png',
+
content: Buffer.from('png1'),
+
mimeType: 'image/png',
+
size: 4,
+
},
+
{
+
name: 'dist/assets/image2.png',
+
content: Buffer.from('png2'),
+
mimeType: 'image/png',
+
size: 4,
+
},
+
]
+
+
const result = processUploadedFiles(files)
+
+
expect(result.fileCount).toBe(2)
+
+
const assetsDir = result.directory.entries.find(e => e.name === 'assets')
+
expect(assetsDir).toBeDefined()
+
+
if ('entries' in assetsDir!.node) {
+
expect(assetsDir!.node.entries).toHaveLength(2)
+
const names = assetsDir!.node.entries.map(e => e.name)
+
expect(names).toContain('image1.png')
+
expect(names).toContain('image2.png')
+
}
+
})
+
})
+
+
describe('createManifest', () => {
+
test('should create valid manifest', () => {
+
const root: Directory = {
+
$type: 'place.wisp.fs#directory',
+
type: 'directory',
+
entries: [],
+
}
+
+
const manifest = createManifest('example.com', root, 0)
+
+
expect(manifest.$type).toBe('place.wisp.fs')
+
expect(manifest.site).toBe('example.com')
+
expect(manifest.root).toBe(root)
+
expect(manifest.fileCount).toBe(0)
+
expect(manifest.createdAt).toBeDefined()
+
+
// Verify it's a valid ISO date string
+
const date = new Date(manifest.createdAt)
+
expect(date.toISOString()).toBe(manifest.createdAt)
+
})
+
+
test('should create manifest with file count', () => {
+
const root: Directory = {
+
$type: 'place.wisp.fs#directory',
+
type: 'directory',
+
entries: [],
+
}
+
+
const manifest = createManifest('test-site', root, 42)
+
+
expect(manifest.fileCount).toBe(42)
+
expect(manifest.site).toBe('test-site')
+
})
+
+
test('should create manifest with populated directory', () => {
+
const mockBlob = createMockBlobRef('text/html', 100)
+
+
const root: Directory = {
+
$type: 'place.wisp.fs#directory',
+
type: 'directory',
+
entries: [
+
{
+
name: 'index.html',
+
node: {
+
$type: 'place.wisp.fs#file',
+
type: 'file',
+
blob: mockBlob,
+
},
+
},
+
],
+
}
+
+
const manifest = createManifest('populated-site', root, 1)
+
+
expect(manifest).toBeDefined()
+
expect(manifest.site).toBe('populated-site')
+
expect(manifest.root.entries).toHaveLength(1)
+
})
+
})
+
+
describe('updateFileBlobs', () => {
+
test('should update single file blob at root', () => {
+
const directory: Directory = {
+
$type: 'place.wisp.fs#directory',
+
type: 'directory',
+
entries: [
+
{
+
name: 'index.html',
+
node: {
+
$type: 'place.wisp.fs#file',
+
type: 'file',
+
blob: undefined as any,
+
},
+
},
+
],
+
}
+
+
const mockBlob = createMockBlobRef('text/html', 100)
+
const uploadResults: FileUploadResult[] = [
+
{
+
hash: TEST_CID_STRING,
+
blobRef: mockBlob,
+
mimeType: 'text/html',
+
},
+
]
+
+
const filePaths = ['index.html']
+
+
const updated = updateFileBlobs(directory, uploadResults, filePaths)
+
+
expect(updated.entries).toHaveLength(1)
+
const fileNode = updated.entries[0].node
+
+
if ('blob' in fileNode) {
+
expect(fileNode.blob).toBeDefined()
+
expect(fileNode.blob.mimeType).toBe('text/html')
+
expect(fileNode.blob.size).toBe(100)
+
} else {
+
throw new Error('Expected file node')
+
}
+
})
+
+
test('should update files in nested directories', () => {
+
const directory: Directory = {
+
$type: 'place.wisp.fs#directory',
+
type: 'directory',
+
entries: [
+
{
+
name: 'css',
+
node: {
+
$type: 'place.wisp.fs#directory',
+
type: 'directory',
+
entries: [
+
{
+
name: 'styles.css',
+
node: {
+
$type: 'place.wisp.fs#file',
+
type: 'file',
+
blob: undefined as any,
+
},
+
},
+
],
+
},
+
},
+
],
+
}
+
+
const mockBlob = createMockBlobRef('text/css', 50)
+
const uploadResults: FileUploadResult[] = [
+
{
+
hash: TEST_CID_STRING,
+
blobRef: mockBlob,
+
mimeType: 'text/css',
+
encoding: 'gzip',
+
},
+
]
+
+
const filePaths = ['css/styles.css']
+
+
const updated = updateFileBlobs(directory, uploadResults, filePaths)
+
+
const cssDir = updated.entries[0]
+
expect(cssDir.name).toBe('css')
+
+
if ('entries' in cssDir.node) {
+
const cssFile = cssDir.node.entries[0]
+
expect(cssFile.name).toBe('styles.css')
+
+
if ('blob' in cssFile.node) {
+
expect(cssFile.node.blob.mimeType).toBe('text/css')
+
if ('encoding' in cssFile.node) {
+
expect(cssFile.node.encoding).toBe('gzip')
+
}
+
} else {
+
throw new Error('Expected file node')
+
}
+
} else {
+
throw new Error('Expected directory node')
+
}
+
})
+
+
test('should handle normalized paths with base folder removed', () => {
+
const directory: Directory = {
+
$type: 'place.wisp.fs#directory',
+
type: 'directory',
+
entries: [
+
{
+
name: 'index.html',
+
node: {
+
$type: 'place.wisp.fs#file',
+
type: 'file',
+
blob: undefined as any,
+
},
+
},
+
],
+
}
+
+
const mockBlob = createMockBlobRef('text/html', 100)
+
const uploadResults: FileUploadResult[] = [
+
{
+
hash: TEST_CID_STRING,
+
blobRef: mockBlob,
+
},
+
]
+
+
// Path includes base folder that should be normalized
+
const filePaths = ['dist/index.html']
+
+
const updated = updateFileBlobs(directory, uploadResults, filePaths)
+
+
const fileNode = updated.entries[0].node
+
if ('blob' in fileNode) {
+
expect(fileNode.blob).toBeDefined()
+
} else {
+
throw new Error('Expected file node')
+
}
+
})
+
+
test('should preserve file metadata (encoding, mimeType, base64)', () => {
+
const directory: Directory = {
+
$type: 'place.wisp.fs#directory',
+
type: 'directory',
+
entries: [
+
{
+
name: 'data.json',
+
node: {
+
$type: 'place.wisp.fs#file',
+
type: 'file',
+
blob: undefined as any,
+
},
+
},
+
],
+
}
+
+
const mockBlob = createMockBlobRef('application/json', 200)
+
const uploadResults: FileUploadResult[] = [
+
{
+
hash: TEST_CID_STRING,
+
blobRef: mockBlob,
+
mimeType: 'application/json',
+
encoding: 'gzip',
+
base64: true,
+
},
+
]
+
+
const filePaths = ['data.json']
+
+
const updated = updateFileBlobs(directory, uploadResults, filePaths)
+
+
const fileNode = updated.entries[0].node
+
if ('blob' in fileNode && 'mimeType' in fileNode && 'encoding' in fileNode && 'base64' in fileNode) {
+
expect(fileNode.mimeType).toBe('application/json')
+
expect(fileNode.encoding).toBe('gzip')
+
expect(fileNode.base64).toBe(true)
+
} else {
+
throw new Error('Expected file node with metadata')
+
}
+
})
+
+
test('should handle multiple files at different directory levels', () => {
+
const directory: Directory = {
+
$type: 'place.wisp.fs#directory',
+
type: 'directory',
+
entries: [
+
{
+
name: 'index.html',
+
node: {
+
$type: 'place.wisp.fs#file',
+
type: 'file',
+
blob: undefined as any,
+
},
+
},
+
{
+
name: 'assets',
+
node: {
+
$type: 'place.wisp.fs#directory',
+
type: 'directory',
+
entries: [
+
{
+
name: 'logo.svg',
+
node: {
+
$type: 'place.wisp.fs#file',
+
type: 'file',
+
blob: undefined as any,
+
},
+
},
+
],
+
},
+
},
+
],
+
}
+
+
const htmlBlob = createMockBlobRef('text/html', 100)
+
const svgBlob = createMockBlobRef('image/svg+xml', 500)
+
+
const uploadResults: FileUploadResult[] = [
+
{
+
hash: TEST_CID_STRING,
+
blobRef: htmlBlob,
+
},
+
{
+
hash: TEST_CID_STRING,
+
blobRef: svgBlob,
+
},
+
]
+
+
const filePaths = ['index.html', 'assets/logo.svg']
+
+
const updated = updateFileBlobs(directory, uploadResults, filePaths)
+
+
// Check root file
+
const indexNode = updated.entries[0].node
+
if ('blob' in indexNode) {
+
expect(indexNode.blob.mimeType).toBe('text/html')
+
}
+
+
// Check nested file
+
const assetsDir = updated.entries[1]
+
if ('entries' in assetsDir.node) {
+
const logoNode = assetsDir.node.entries[0].node
+
if ('blob' in logoNode) {
+
expect(logoNode.blob.mimeType).toBe('image/svg+xml')
+
}
+
}
+
})
+
})