+22
.tangled/workflows/test.yml
+22
.tangled/workflows/test.yml
···
1
+
when:
2
+
- event: ["push", "pull_request"]
3
+
branch: main
4
+
5
+
engine: nixery
6
+
7
+
dependencies:
8
+
nixpkgs:
9
+
- git
10
+
github:NixOS/nixpkgs/nixpkgs-unstable:
11
+
- bun
12
+
13
+
steps:
14
+
- name: install dependencies
15
+
command: |
16
+
export PATH="$HOME/.nix-profile/bin:$PATH"
17
+
bun install
18
+
19
+
- name: run all tests
20
+
command: |
21
+
export PATH="$HOME/.nix-profile/bin:$PATH"
22
+
bun test
+12
-190
hosting-service/bun.lock
+12
-190
hosting-service/bun.lock
···
9
9
"@atproto/lexicon": "^0.5.1",
10
10
"@atproto/sync": "^0.1.36",
11
11
"@atproto/xrpc": "^0.7.5",
12
-
"@elysiajs/node": "^1.4.2",
13
-
"@elysiajs/opentelemetry": "latest",
14
-
"elysia": "^1.4.15",
12
+
"@hono/node-server": "^1.19.6",
13
+
"hono": "^4.10.4",
15
14
"mime-types": "^2.1.35",
16
15
"multiformats": "^13.4.1",
17
16
"postgres": "^3.4.5",
18
17
},
19
18
"devDependencies": {
19
+
"@types/bun": "^1.3.1",
20
20
"@types/mime-types": "^2.1.4",
21
21
"@types/node": "^22.10.5",
22
22
"tsx": "^4.19.2",
···
46
46
47
47
"@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" } }, ""],
48
48
49
-
"@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
50
-
51
49
"@cbor-extract/cbor-extract-darwin-arm64": ["@cbor-extract/cbor-extract-darwin-arm64@2.2.0", "", { "os": "darwin", "cpu": "arm64" }, ""],
52
-
53
-
"@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=="],
54
-
55
-
"@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=="],
56
50
57
51
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="],
58
52
···
106
100
107
101
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="],
108
102
109
-
"@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=="],
110
-
111
-
"@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=="],
103
+
"@hono/node-server": ["@hono/node-server@1.19.6", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw=="],
112
104
113
105
"@ipld/dag-cbor": ["@ipld/dag-cbor@7.0.3", "", { "dependencies": { "cborg": "^1.6.0", "multiformats": "^9.5.4" } }, ""],
114
106
115
-
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
116
-
117
107
"@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, ""],
118
108
119
109
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, ""],
120
110
121
-
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
122
-
123
-
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q=="],
124
-
125
-
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA=="],
126
-
127
-
"@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=="],
128
-
129
-
"@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=="],
130
-
131
-
"@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=="],
132
-
133
-
"@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=="],
134
-
135
-
"@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=="],
136
-
137
-
"@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=="],
138
-
139
-
"@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=="],
140
-
141
-
"@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=="],
142
-
143
-
"@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=="],
144
-
145
-
"@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=="],
146
-
147
-
"@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=="],
148
-
149
-
"@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=="],
150
-
151
-
"@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=="],
152
-
153
-
"@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=="],
154
-
155
-
"@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=="],
156
-
157
-
"@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=="],
158
-
159
-
"@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=="],
160
-
161
-
"@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=="],
162
-
163
-
"@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=="],
164
-
165
-
"@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=="],
166
-
167
-
"@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=="],
168
-
169
-
"@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=="],
170
-
171
-
"@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=="],
172
-
173
-
"@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=="],
174
-
175
-
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.37.0", "", {}, "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA=="],
176
-
177
-
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
178
-
179
-
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
180
-
181
-
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
182
-
183
-
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
184
-
185
-
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
186
-
187
-
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
188
-
189
-
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
190
-
191
-
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
192
-
193
-
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
194
-
195
-
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
196
-
197
-
"@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="],
198
-
199
-
"@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=="],
200
-
201
-
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
111
+
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
202
112
203
113
"@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="],
204
114
205
115
"@types/node": ["@types/node@22.18.12", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog=="],
206
116
207
-
"@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="],
117
+
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
208
118
209
119
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, ""],
210
120
211
121
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, ""],
212
122
213
-
"acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
214
-
215
-
"acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
216
-
217
-
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
218
-
219
-
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
220
-
221
123
"array-flatten": ["array-flatten@1.1.1", "", {}, ""],
222
124
223
125
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, ""],
···
230
132
231
133
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, ""],
232
134
135
+
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
136
+
233
137
"bytes": ["bytes@3.1.2", "", {}, ""],
234
138
235
139
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, ""],
···
242
146
243
147
"cborg": ["cborg@1.10.2", "", { "bin": "cli.js" }, ""],
244
148
245
-
"cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="],
246
-
247
-
"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=="],
248
-
249
-
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
250
-
251
-
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
252
-
253
149
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, ""],
254
150
255
151
"content-type": ["content-type@1.0.5", "", {}, ""],
256
152
257
-
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
153
+
"cookie": ["cookie@0.7.1", "", {}, ""],
258
154
259
155
"cookie-signature": ["cookie-signature@1.0.6", "", {}, ""],
260
156
261
-
"crossws": ["crossws@0.4.1", "", { "peerDependencies": { "srvx": ">=0.7.1" }, "optionalPeers": ["srvx"] }, "sha512-E7WKBcHVhAVrY6JYD5kteNqVq1GSZxqGrdSiwXR9at+XHi43HJoCQKXcCczR5LBnBquFZPsB3o7HklulKoBU5w=="],
157
+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
262
158
263
159
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, ""],
264
160
···
272
168
273
169
"ee-first": ["ee-first@1.1.1", "", {}, ""],
274
170
275
-
"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=="],
276
-
277
-
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
278
-
279
171
"encodeurl": ["encodeurl@2.0.0", "", {}, ""],
280
172
281
173
"es-define-property": ["es-define-property@1.0.1", "", {}, ""],
···
286
178
287
179
"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=="],
288
180
289
-
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
290
-
291
181
"escape-html": ["escape-html@1.0.3", "", {}, ""],
292
182
293
183
"etag": ["etag@1.8.1", "", {}, ""],
···
298
188
299
189
"events": ["events@3.3.0", "", {}, ""],
300
190
301
-
"exact-mirror": ["exact-mirror@0.2.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" } }, "sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA=="],
302
-
303
191
"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" } }, ""],
304
-
305
-
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
306
192
307
193
"fast-redact": ["fast-redact@3.5.0", "", {}, ""],
308
194
309
-
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
310
-
311
-
"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=="],
312
-
313
195
"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" } }, ""],
314
196
315
197
"forwarded": ["forwarded@0.2.0", "", {}, ""],
···
319
201
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
320
202
321
203
"function-bind": ["function-bind@1.1.2", "", {}, ""],
322
-
323
-
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
324
204
325
205
"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" } }, ""],
326
206
···
336
216
337
217
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, ""],
338
218
219
+
"hono": ["hono@4.10.4", "", {}, "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg=="],
220
+
339
221
"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" } }, ""],
340
222
341
223
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, ""],
342
224
343
225
"ieee754": ["ieee754@1.2.1", "", {}, ""],
344
226
345
-
"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=="],
346
-
347
227
"inherits": ["inherits@2.0.4", "", {}, ""],
348
228
349
229
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, ""],
350
230
351
-
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
352
-
353
-
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
354
-
355
231
"iso-datestring-validator": ["iso-datestring-validator@2.2.2", "", {}, ""],
356
-
357
-
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
358
-
359
-
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
360
232
361
233
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, ""],
362
234
363
235
"media-typer": ["media-typer@0.3.0", "", {}, ""],
364
236
365
-
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
366
-
367
237
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, ""],
368
238
369
239
"methods": ["methods@1.1.2", "", {}, ""],
···
373
243
"mime-db": ["mime-db@1.52.0", "", {}, ""],
374
244
375
245
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, ""],
376
-
377
-
"module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="],
378
246
379
247
"ms": ["ms@2.0.0", "", {}, ""],
380
248
···
390
258
391
259
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, ""],
392
260
393
-
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
394
-
395
261
"p-finally": ["p-finally@1.0.0", "", {}, ""],
396
262
397
263
"p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, ""],
···
400
266
401
267
"parseurl": ["parseurl@1.3.3", "", {}, ""],
402
268
403
-
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
404
-
405
269
"path-to-regexp": ["path-to-regexp@0.1.12", "", {}, ""],
406
270
407
271
"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" }, ""],
···
415
279
"process": ["process@0.11.10", "", {}, ""],
416
280
417
281
"process-warning": ["process-warning@3.0.0", "", {}, ""],
418
-
419
-
"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=="],
420
282
421
283
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, ""],
422
284
···
434
296
435
297
"real-require": ["real-require@0.2.0", "", {}, ""],
436
298
437
-
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
438
-
439
-
"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=="],
440
-
441
-
"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=="],
442
-
443
299
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
444
300
445
301
"safe-buffer": ["safe-buffer@5.2.1", "", {}, ""],
···
454
310
455
311
"setprototypeof": ["setprototypeof@1.2.0", "", {}, ""],
456
312
457
-
"shimmer": ["shimmer@1.2.1", "", {}, "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="],
458
-
459
313
"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" } }, ""],
460
314
461
315
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, ""],
···
468
322
469
323
"split2": ["split2@4.2.0", "", {}, ""],
470
324
471
-
"srvx": ["srvx@0.9.5", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-nQsA2c8q3XwbSn6kTxVQjz0zS096rV+Be2pzJwrYEAdtnYszLw4MTy8JWJjz1XEGBZwP0qW51SUIX3WdjdRemQ=="],
472
-
473
325
"statuses": ["statuses@2.0.1", "", {}, ""],
474
-
475
-
"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=="],
476
326
477
327
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, ""],
478
328
479
-
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
480
-
481
-
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
482
-
483
-
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
484
-
485
329
"thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, ""],
486
330
487
331
"tlds": ["tlds@1.261.0", "", { "bin": "bin.js" }, ""],
488
332
489
333
"toidentifier": ["toidentifier@1.0.1", "", {}, ""],
490
334
491
-
"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=="],
492
-
493
335
"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=="],
494
336
495
337
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, ""],
496
-
497
-
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
498
338
499
339
"uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, ""],
500
340
···
508
348
509
349
"vary": ["vary@1.1.2", "", {}, ""],
510
350
511
-
"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=="],
512
-
513
351
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, ""],
514
352
515
-
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
516
-
517
-
"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=="],
518
-
519
-
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
520
-
521
353
"zod": ["zod@3.25.76", "", {}, ""],
522
354
523
355
"@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, ""],
···
534
366
535
367
"@ipld/dag-cbor/multiformats": ["multiformats@9.9.0", "", {}, ""],
536
368
537
-
"@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
538
-
539
-
"express/cookie": ["cookie@0.7.1", "", {}, ""],
540
-
541
-
"require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
542
-
543
369
"send/encodeurl": ["encodeurl@1.0.2", "", {}, ""],
544
370
545
371
"send/ms": ["ms@2.1.3", "", {}, ""],
546
372
547
373
"uint8arrays/multiformats": ["multiformats@9.9.0", "", {}, ""],
548
-
549
-
"@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, ""],
550
-
551
-
"require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, ""],
552
374
}
553
375
}
+1
hosting-service/package.json
+1
hosting-service/package.json
+9
-14
hosting-service/src/lib/html-rewriter.ts
+9
-14
hosting-service/src/lib/html-rewriter.ts
···
29
29
return false;
30
30
}
31
31
32
-
// Don't rewrite pure anchors
33
-
if (path.startsWith('#')) return false;
32
+
// Don't rewrite pure anchors or paths that start with /#
33
+
if (path.startsWith('#') || path.startsWith('/#')) return false;
34
+
35
+
// Don't rewrite relative paths (./ or ../)
36
+
if (path.startsWith('./') || path.startsWith('../')) return false;
34
37
35
-
// Rewrite absolute paths (/) and relative paths (./ or ../ or plain filenames)
38
+
// Rewrite absolute paths (/)
36
39
return true;
37
40
}
38
41
···
49
52
return basePath + path.slice(1);
50
53
}
51
54
52
-
// Handle relative paths: ./file.js or ../file.js or file.js -> /base/file.js
53
-
// Strip leading ./ or ../ and just use the base path
54
-
let cleanPath = path;
55
-
if (cleanPath.startsWith('./')) {
56
-
cleanPath = cleanPath.slice(2);
57
-
} else if (cleanPath.startsWith('../')) {
58
-
// For sites.wisp.place, we can't go up from the site root, so just use base path
59
-
cleanPath = cleanPath.replace(/^(\.\.\/)+/, '');
60
-
}
61
-
62
-
return basePath + cleanPath;
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;
63
58
}
64
59
65
60
/**
+8
-3
hosting-service/src/lib/safe-fetch.ts
+8
-3
hosting-service/src/lib/safe-fetch.ts
···
24
24
const FETCH_TIMEOUT = 120000; // 120 seconds
25
25
const FETCH_TIMEOUT_BLOB = 120000; // 2 minutes for blob downloads
26
26
const MAX_RESPONSE_SIZE = 10 * 1024 * 1024; // 10MB
27
+
const MAX_JSON_SIZE = 1024 * 1024; // 1MB
28
+
const MAX_BLOB_SIZE = 100 * 1024 * 1024; // 100MB
29
+
const MAX_REDIRECTS = 10;
27
30
28
31
function isBlockedHost(hostname: string): boolean {
29
32
const lowerHost = hostname.toLowerCase();
···
72
75
const response = await fetch(url, {
73
76
...options,
74
77
signal: controller.signal,
78
+
redirect: 'follow',
75
79
});
76
80
77
81
const contentLength = response.headers.get('content-length');
···
94
98
url: string,
95
99
options?: RequestInit & { maxSize?: number; timeout?: number }
96
100
): Promise<T> {
97
-
const maxJsonSize = options?.maxSize ?? 1024 * 1024; // 1MB default for JSON
101
+
const maxJsonSize = options?.maxSize ?? MAX_JSON_SIZE;
98
102
const response = await safeFetch(url, { ...options, maxSize: maxJsonSize });
99
103
100
104
if (!response.ok) {
···
140
144
url: string,
141
145
options?: RequestInit & { maxSize?: number; timeout?: number }
142
146
): Promise<Uint8Array> {
143
-
const maxBlobSize = options?.maxSize ?? MAX_RESPONSE_SIZE;
144
-
const response = await safeFetch(url, { ...options, maxSize: maxBlobSize });
147
+
const maxBlobSize = options?.maxSize ?? MAX_BLOB_SIZE;
148
+
const timeoutMs = options?.timeout ?? FETCH_TIMEOUT_BLOB;
149
+
const response = await safeFetch(url, { ...options, maxSize: maxBlobSize, timeout: timeoutMs });
145
150
146
151
if (!response.ok) {
147
152
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+169
hosting-service/src/lib/utils.test.ts
+169
hosting-service/src/lib/utils.test.ts
···
1
+
import { describe, test, expect } from 'bun:test'
2
+
import { sanitizePath, extractBlobCid } from './utils'
3
+
import { CID } from 'multiformats'
4
+
5
+
describe('sanitizePath', () => {
6
+
test('allows normal file paths', () => {
7
+
expect(sanitizePath('index.html')).toBe('index.html')
8
+
expect(sanitizePath('css/styles.css')).toBe('css/styles.css')
9
+
expect(sanitizePath('images/logo.png')).toBe('images/logo.png')
10
+
expect(sanitizePath('js/app.js')).toBe('js/app.js')
11
+
})
12
+
13
+
test('allows deeply nested paths', () => {
14
+
expect(sanitizePath('assets/images/icons/favicon.ico')).toBe('assets/images/icons/favicon.ico')
15
+
expect(sanitizePath('a/b/c/d/e/f.txt')).toBe('a/b/c/d/e/f.txt')
16
+
})
17
+
18
+
test('removes leading slashes', () => {
19
+
expect(sanitizePath('/index.html')).toBe('index.html')
20
+
expect(sanitizePath('//index.html')).toBe('index.html')
21
+
expect(sanitizePath('///index.html')).toBe('index.html')
22
+
expect(sanitizePath('/css/styles.css')).toBe('css/styles.css')
23
+
})
24
+
25
+
test('blocks parent directory traversal', () => {
26
+
expect(sanitizePath('../etc/passwd')).toBe('etc/passwd')
27
+
expect(sanitizePath('../../etc/passwd')).toBe('etc/passwd')
28
+
expect(sanitizePath('../../../etc/passwd')).toBe('etc/passwd')
29
+
expect(sanitizePath('css/../../../etc/passwd')).toBe('css/etc/passwd')
30
+
})
31
+
32
+
test('blocks directory traversal in middle of path', () => {
33
+
expect(sanitizePath('images/../../../etc/passwd')).toBe('images/etc/passwd')
34
+
// Note: sanitizePath only filters out ".." segments, doesn't resolve paths
35
+
expect(sanitizePath('a/b/../c')).toBe('a/b/c')
36
+
expect(sanitizePath('a/../b/../c')).toBe('a/b/c')
37
+
})
38
+
39
+
test('removes current directory references', () => {
40
+
expect(sanitizePath('./index.html')).toBe('index.html')
41
+
expect(sanitizePath('././index.html')).toBe('index.html')
42
+
expect(sanitizePath('css/./styles.css')).toBe('css/styles.css')
43
+
expect(sanitizePath('./css/./styles.css')).toBe('css/styles.css')
44
+
})
45
+
46
+
test('removes empty path segments', () => {
47
+
expect(sanitizePath('css//styles.css')).toBe('css/styles.css')
48
+
expect(sanitizePath('css///styles.css')).toBe('css/styles.css')
49
+
expect(sanitizePath('a//b//c')).toBe('a/b/c')
50
+
})
51
+
52
+
test('blocks null bytes', () => {
53
+
// Null bytes cause the entire segment to be filtered out
54
+
expect(sanitizePath('index.html\0.txt')).toBe('')
55
+
expect(sanitizePath('test\0')).toBe('')
56
+
// Null byte in middle segment
57
+
expect(sanitizePath('css/bad\0name/styles.css')).toBe('css/styles.css')
58
+
})
59
+
60
+
test('handles mixed attacks', () => {
61
+
expect(sanitizePath('/../../../etc/passwd')).toBe('etc/passwd')
62
+
expect(sanitizePath('/./././../etc/passwd')).toBe('etc/passwd')
63
+
expect(sanitizePath('//../../.\0./etc/passwd')).toBe('etc/passwd')
64
+
})
65
+
66
+
test('handles edge cases', () => {
67
+
expect(sanitizePath('')).toBe('')
68
+
expect(sanitizePath('/')).toBe('')
69
+
expect(sanitizePath('//')).toBe('')
70
+
expect(sanitizePath('.')).toBe('')
71
+
expect(sanitizePath('..')).toBe('')
72
+
expect(sanitizePath('../..')).toBe('')
73
+
})
74
+
75
+
test('preserves valid special characters in filenames', () => {
76
+
expect(sanitizePath('file-name.html')).toBe('file-name.html')
77
+
expect(sanitizePath('file_name.html')).toBe('file_name.html')
78
+
expect(sanitizePath('file.name.html')).toBe('file.name.html')
79
+
expect(sanitizePath('file (1).html')).toBe('file (1).html')
80
+
expect(sanitizePath('file@2x.png')).toBe('file@2x.png')
81
+
})
82
+
83
+
test('handles Unicode characters', () => {
84
+
expect(sanitizePath('文件.html')).toBe('文件.html')
85
+
expect(sanitizePath('файл.html')).toBe('файл.html')
86
+
expect(sanitizePath('ファイル.html')).toBe('ファイル.html')
87
+
})
88
+
})
89
+
90
+
describe('extractBlobCid', () => {
91
+
const TEST_CID = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y'
92
+
93
+
test('extracts CID from IPLD link', () => {
94
+
const blobRef = { $link: TEST_CID }
95
+
expect(extractBlobCid(blobRef)).toBe(TEST_CID)
96
+
})
97
+
98
+
test('extracts CID from typed BlobRef with CID object', () => {
99
+
const cid = CID.parse(TEST_CID)
100
+
const blobRef = { ref: cid }
101
+
const result = extractBlobCid(blobRef)
102
+
expect(result).toBe(TEST_CID)
103
+
})
104
+
105
+
test('extracts CID from typed BlobRef with IPLD link', () => {
106
+
const blobRef = {
107
+
ref: { $link: TEST_CID }
108
+
}
109
+
expect(extractBlobCid(blobRef)).toBe(TEST_CID)
110
+
})
111
+
112
+
test('extracts CID from untyped BlobRef', () => {
113
+
const blobRef = { cid: TEST_CID }
114
+
expect(extractBlobCid(blobRef)).toBe(TEST_CID)
115
+
})
116
+
117
+
test('returns null for invalid blob ref', () => {
118
+
expect(extractBlobCid(null)).toBe(null)
119
+
expect(extractBlobCid(undefined)).toBe(null)
120
+
expect(extractBlobCid({})).toBe(null)
121
+
expect(extractBlobCid('not-an-object')).toBe(null)
122
+
expect(extractBlobCid(123)).toBe(null)
123
+
})
124
+
125
+
test('returns null for malformed objects', () => {
126
+
expect(extractBlobCid({ wrongKey: 'value' })).toBe(null)
127
+
expect(extractBlobCid({ ref: 'not-a-cid' })).toBe(null)
128
+
expect(extractBlobCid({ ref: {} })).toBe(null)
129
+
})
130
+
131
+
test('handles nested structures from AT Proto API', () => {
132
+
// Real structure from AT Proto
133
+
const blobRef = {
134
+
$type: 'blob',
135
+
ref: CID.parse(TEST_CID),
136
+
mimeType: 'text/html',
137
+
size: 1234
138
+
}
139
+
expect(extractBlobCid(blobRef)).toBe(TEST_CID)
140
+
})
141
+
142
+
test('handles BlobRef with additional properties', () => {
143
+
const blobRef = {
144
+
ref: { $link: TEST_CID },
145
+
mimeType: 'image/png',
146
+
size: 5678,
147
+
someOtherField: 'value'
148
+
}
149
+
expect(extractBlobCid(blobRef)).toBe(TEST_CID)
150
+
})
151
+
152
+
test('prioritizes checking IPLD link first', () => {
153
+
// Direct $link takes precedence
154
+
const directLink = { $link: TEST_CID }
155
+
expect(extractBlobCid(directLink)).toBe(TEST_CID)
156
+
})
157
+
158
+
test('handles CID v0 format', () => {
159
+
const cidV0 = 'QmZ4tDuvesekSs4qM5ZBKpXiZGun7S2CYtEZRB3DYXkjGx'
160
+
const blobRef = { $link: cidV0 }
161
+
expect(extractBlobCid(blobRef)).toBe(cidV0)
162
+
})
163
+
164
+
test('handles CID v1 format', () => {
165
+
const cidV1 = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi'
166
+
const blobRef = { $link: cidV1 }
167
+
expect(extractBlobCid(blobRef)).toBe(cidV1)
168
+
})
169
+
})
+1
-1
hosting-service/tsconfig.json
+1
-1
hosting-service/tsconfig.json
+1
-1
package.json
+1
-1
package.json
···
2
2
"name": "elysia-static",
3
3
"version": "1.0.50",
4
4
"scripts": {
5
-
"test": "echo \"Error: no test specified\" && exit 1",
5
+
"test": "bun test",
6
6
"dev": "bun run --watch src/index.ts",
7
7
"start": "bun run src/index.ts",
8
8
"build": "bun build --compile --target bun --outfile server src/index.ts"
+81
src/lib/csrf.test.ts
+81
src/lib/csrf.test.ts
···
1
+
import { describe, test, expect } from 'bun:test'
2
+
import { verifyRequestOrigin } from './csrf'
3
+
4
+
describe('verifyRequestOrigin', () => {
5
+
test('should accept matching origin and host', () => {
6
+
expect(verifyRequestOrigin('https://example.com', ['example.com'])).toBe(true)
7
+
expect(verifyRequestOrigin('http://localhost:8000', ['localhost:8000'])).toBe(true)
8
+
expect(verifyRequestOrigin('https://app.example.com', ['app.example.com'])).toBe(true)
9
+
})
10
+
11
+
test('should accept origin matching one of multiple allowed hosts', () => {
12
+
const allowedHosts = ['example.com', 'app.example.com', 'localhost:8000']
13
+
expect(verifyRequestOrigin('https://example.com', allowedHosts)).toBe(true)
14
+
expect(verifyRequestOrigin('https://app.example.com', allowedHosts)).toBe(true)
15
+
expect(verifyRequestOrigin('http://localhost:8000', allowedHosts)).toBe(true)
16
+
})
17
+
18
+
test('should reject non-matching origin', () => {
19
+
expect(verifyRequestOrigin('https://evil.com', ['example.com'])).toBe(false)
20
+
expect(verifyRequestOrigin('https://fake-example.com', ['example.com'])).toBe(false)
21
+
expect(verifyRequestOrigin('https://example.com.evil.com', ['example.com'])).toBe(false)
22
+
})
23
+
24
+
test('should reject empty origin', () => {
25
+
expect(verifyRequestOrigin('', ['example.com'])).toBe(false)
26
+
})
27
+
28
+
test('should reject invalid URL format', () => {
29
+
expect(verifyRequestOrigin('not-a-url', ['example.com'])).toBe(false)
30
+
expect(verifyRequestOrigin('javascript:alert(1)', ['example.com'])).toBe(false)
31
+
expect(verifyRequestOrigin('file:///etc/passwd', ['example.com'])).toBe(false)
32
+
})
33
+
34
+
test('should handle different protocols correctly', () => {
35
+
// Same host, different protocols should match (we only check host)
36
+
expect(verifyRequestOrigin('http://example.com', ['example.com'])).toBe(true)
37
+
expect(verifyRequestOrigin('https://example.com', ['example.com'])).toBe(true)
38
+
})
39
+
40
+
test('should handle port numbers correctly', () => {
41
+
expect(verifyRequestOrigin('http://localhost:3000', ['localhost:3000'])).toBe(true)
42
+
expect(verifyRequestOrigin('http://localhost:3000', ['localhost:8000'])).toBe(false)
43
+
expect(verifyRequestOrigin('http://localhost', ['localhost'])).toBe(true)
44
+
})
45
+
46
+
test('should handle subdomains correctly', () => {
47
+
expect(verifyRequestOrigin('https://sub.example.com', ['sub.example.com'])).toBe(true)
48
+
expect(verifyRequestOrigin('https://sub.example.com', ['example.com'])).toBe(false)
49
+
})
50
+
51
+
test('should handle case sensitivity (exact match required)', () => {
52
+
// URL host is automatically lowercased by URL parser
53
+
expect(verifyRequestOrigin('https://EXAMPLE.COM', ['example.com'])).toBe(true)
54
+
expect(verifyRequestOrigin('https://example.com', ['example.com'])).toBe(true)
55
+
// But allowed hosts are case-sensitive
56
+
expect(verifyRequestOrigin('https://example.com', ['EXAMPLE.COM'])).toBe(false)
57
+
})
58
+
59
+
test('should handle trailing slashes in origin', () => {
60
+
expect(verifyRequestOrigin('https://example.com/', ['example.com'])).toBe(true)
61
+
})
62
+
63
+
test('should handle paths in origin (host extraction)', () => {
64
+
expect(verifyRequestOrigin('https://example.com/path/to/page', ['example.com'])).toBe(true)
65
+
expect(verifyRequestOrigin('https://evil.com/example.com', ['example.com'])).toBe(false)
66
+
})
67
+
68
+
test('should reject when allowed hosts is empty', () => {
69
+
expect(verifyRequestOrigin('https://example.com', [])).toBe(false)
70
+
})
71
+
72
+
test('should handle IPv4 addresses', () => {
73
+
expect(verifyRequestOrigin('http://127.0.0.1:8000', ['127.0.0.1:8000'])).toBe(true)
74
+
expect(verifyRequestOrigin('http://192.168.1.1', ['192.168.1.1'])).toBe(true)
75
+
})
76
+
77
+
test('should handle IPv6 addresses', () => {
78
+
expect(verifyRequestOrigin('http://[::1]:8000', ['[::1]:8000'])).toBe(true)
79
+
expect(verifyRequestOrigin('http://[2001:db8::1]', ['[2001:db8::1]'])).toBe(true)
80
+
})
81
+
})
+639
src/lib/wisp-utils.test.ts
+639
src/lib/wisp-utils.test.ts
···
1
+
import { describe, test, expect } from 'bun:test'
2
+
import {
3
+
shouldCompressFile,
4
+
compressFile,
5
+
processUploadedFiles,
6
+
createManifest,
7
+
updateFileBlobs,
8
+
type UploadedFile,
9
+
type FileUploadResult,
10
+
} from './wisp-utils'
11
+
import type { Directory } from '../lexicons/types/place/wisp/fs'
12
+
import { gunzipSync } from 'zlib'
13
+
import { BlobRef } from '@atproto/api'
14
+
import { CID } from 'multiformats/cid'
15
+
16
+
// Helper function to create a valid CID for testing
17
+
// Using a real valid CID from actual AT Protocol usage
18
+
const TEST_CID_STRING = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y'
19
+
20
+
function createMockBlobRef(mimeType: string, size: number): BlobRef {
21
+
// Create a properly formatted CID
22
+
const cid = CID.parse(TEST_CID_STRING)
23
+
return new BlobRef(cid, mimeType, size)
24
+
}
25
+
26
+
describe('shouldCompressFile', () => {
27
+
test('should compress HTML files', () => {
28
+
expect(shouldCompressFile('text/html')).toBe(true)
29
+
expect(shouldCompressFile('text/html; charset=utf-8')).toBe(true)
30
+
})
31
+
32
+
test('should compress CSS files', () => {
33
+
expect(shouldCompressFile('text/css')).toBe(true)
34
+
})
35
+
36
+
test('should compress JavaScript files', () => {
37
+
expect(shouldCompressFile('text/javascript')).toBe(true)
38
+
expect(shouldCompressFile('application/javascript')).toBe(true)
39
+
expect(shouldCompressFile('application/x-javascript')).toBe(true)
40
+
})
41
+
42
+
test('should compress JSON files', () => {
43
+
expect(shouldCompressFile('application/json')).toBe(true)
44
+
})
45
+
46
+
test('should compress SVG files', () => {
47
+
expect(shouldCompressFile('image/svg+xml')).toBe(true)
48
+
})
49
+
50
+
test('should compress XML files', () => {
51
+
expect(shouldCompressFile('text/xml')).toBe(true)
52
+
expect(shouldCompressFile('application/xml')).toBe(true)
53
+
})
54
+
55
+
test('should compress plain text files', () => {
56
+
expect(shouldCompressFile('text/plain')).toBe(true)
57
+
})
58
+
59
+
test('should NOT compress images', () => {
60
+
expect(shouldCompressFile('image/png')).toBe(false)
61
+
expect(shouldCompressFile('image/jpeg')).toBe(false)
62
+
expect(shouldCompressFile('image/jpg')).toBe(false)
63
+
expect(shouldCompressFile('image/gif')).toBe(false)
64
+
expect(shouldCompressFile('image/webp')).toBe(false)
65
+
})
66
+
67
+
test('should NOT compress videos', () => {
68
+
expect(shouldCompressFile('video/mp4')).toBe(false)
69
+
expect(shouldCompressFile('video/webm')).toBe(false)
70
+
})
71
+
72
+
test('should NOT compress already compressed formats', () => {
73
+
expect(shouldCompressFile('application/zip')).toBe(false)
74
+
expect(shouldCompressFile('application/gzip')).toBe(false)
75
+
expect(shouldCompressFile('application/pdf')).toBe(false)
76
+
})
77
+
78
+
test('should NOT compress fonts', () => {
79
+
expect(shouldCompressFile('font/woff')).toBe(false)
80
+
expect(shouldCompressFile('font/woff2')).toBe(false)
81
+
expect(shouldCompressFile('font/ttf')).toBe(false)
82
+
})
83
+
})
84
+
85
+
describe('compressFile', () => {
86
+
test('should compress text content', () => {
87
+
const content = Buffer.from('Hello, World! '.repeat(100))
88
+
const compressed = compressFile(content)
89
+
90
+
expect(compressed.length).toBeLessThan(content.length)
91
+
92
+
// Verify we can decompress it back
93
+
const decompressed = gunzipSync(compressed)
94
+
expect(decompressed.toString()).toBe(content.toString())
95
+
})
96
+
97
+
test('should compress HTML content significantly', () => {
98
+
const html = `
99
+
<!DOCTYPE html>
100
+
<html>
101
+
<head><title>Test</title></head>
102
+
<body>
103
+
${'<p>Hello World!</p>\n'.repeat(50)}
104
+
</body>
105
+
</html>
106
+
`
107
+
const content = Buffer.from(html)
108
+
const compressed = compressFile(content)
109
+
110
+
expect(compressed.length).toBeLessThan(content.length)
111
+
112
+
// Verify decompression
113
+
const decompressed = gunzipSync(compressed)
114
+
expect(decompressed.toString()).toBe(html)
115
+
})
116
+
117
+
test('should handle empty content', () => {
118
+
const content = Buffer.from('')
119
+
const compressed = compressFile(content)
120
+
const decompressed = gunzipSync(compressed)
121
+
expect(decompressed.toString()).toBe('')
122
+
})
123
+
124
+
test('should produce deterministic compression', () => {
125
+
const content = Buffer.from('Test content')
126
+
const compressed1 = compressFile(content)
127
+
const compressed2 = compressFile(content)
128
+
129
+
expect(compressed1.toString('base64')).toBe(compressed2.toString('base64'))
130
+
})
131
+
})
132
+
133
+
describe('processUploadedFiles', () => {
134
+
test('should process single root-level file', () => {
135
+
const files: UploadedFile[] = [
136
+
{
137
+
name: 'index.html',
138
+
content: Buffer.from('<html></html>'),
139
+
mimeType: 'text/html',
140
+
size: 13,
141
+
},
142
+
]
143
+
144
+
const result = processUploadedFiles(files)
145
+
146
+
expect(result.fileCount).toBe(1)
147
+
expect(result.directory.type).toBe('directory')
148
+
expect(result.directory.entries).toHaveLength(1)
149
+
expect(result.directory.entries[0].name).toBe('index.html')
150
+
151
+
const node = result.directory.entries[0].node
152
+
expect('blob' in node).toBe(true) // It's a file node
153
+
})
154
+
155
+
test('should process multiple root-level files', () => {
156
+
const files: UploadedFile[] = [
157
+
{
158
+
name: 'index.html',
159
+
content: Buffer.from('<html></html>'),
160
+
mimeType: 'text/html',
161
+
size: 13,
162
+
},
163
+
{
164
+
name: 'styles.css',
165
+
content: Buffer.from('body {}'),
166
+
mimeType: 'text/css',
167
+
size: 7,
168
+
},
169
+
{
170
+
name: 'script.js',
171
+
content: Buffer.from('console.log("hi")'),
172
+
mimeType: 'application/javascript',
173
+
size: 17,
174
+
},
175
+
]
176
+
177
+
const result = processUploadedFiles(files)
178
+
179
+
expect(result.fileCount).toBe(3)
180
+
expect(result.directory.entries).toHaveLength(3)
181
+
182
+
const names = result.directory.entries.map(e => e.name)
183
+
expect(names).toContain('index.html')
184
+
expect(names).toContain('styles.css')
185
+
expect(names).toContain('script.js')
186
+
})
187
+
188
+
test('should process files with subdirectories', () => {
189
+
const files: UploadedFile[] = [
190
+
{
191
+
name: 'dist/index.html',
192
+
content: Buffer.from('<html></html>'),
193
+
mimeType: 'text/html',
194
+
size: 13,
195
+
},
196
+
{
197
+
name: 'dist/css/styles.css',
198
+
content: Buffer.from('body {}'),
199
+
mimeType: 'text/css',
200
+
size: 7,
201
+
},
202
+
{
203
+
name: 'dist/js/app.js',
204
+
content: Buffer.from('console.log()'),
205
+
mimeType: 'application/javascript',
206
+
size: 13,
207
+
},
208
+
]
209
+
210
+
const result = processUploadedFiles(files)
211
+
212
+
expect(result.fileCount).toBe(3)
213
+
expect(result.directory.entries).toHaveLength(3) // index.html, css/, js/
214
+
215
+
// Check root has index.html (after base folder removal)
216
+
const indexEntry = result.directory.entries.find(e => e.name === 'index.html')
217
+
expect(indexEntry).toBeDefined()
218
+
219
+
// Check css directory exists
220
+
const cssDir = result.directory.entries.find(e => e.name === 'css')
221
+
expect(cssDir).toBeDefined()
222
+
expect('entries' in cssDir!.node).toBe(true)
223
+
224
+
if ('entries' in cssDir!.node) {
225
+
expect(cssDir!.node.entries).toHaveLength(1)
226
+
expect(cssDir!.node.entries[0].name).toBe('styles.css')
227
+
}
228
+
229
+
// Check js directory exists
230
+
const jsDir = result.directory.entries.find(e => e.name === 'js')
231
+
expect(jsDir).toBeDefined()
232
+
expect('entries' in jsDir!.node).toBe(true)
233
+
})
234
+
235
+
test('should handle deeply nested subdirectories', () => {
236
+
const files: UploadedFile[] = [
237
+
{
238
+
name: 'dist/deep/nested/folder/file.txt',
239
+
content: Buffer.from('content'),
240
+
mimeType: 'text/plain',
241
+
size: 7,
242
+
},
243
+
]
244
+
245
+
const result = processUploadedFiles(files)
246
+
247
+
expect(result.fileCount).toBe(1)
248
+
249
+
// Navigate through the directory structure (base folder removed)
250
+
const deepDir = result.directory.entries.find(e => e.name === 'deep')
251
+
expect(deepDir).toBeDefined()
252
+
expect('entries' in deepDir!.node).toBe(true)
253
+
254
+
if ('entries' in deepDir!.node) {
255
+
const nestedDir = deepDir!.node.entries.find(e => e.name === 'nested')
256
+
expect(nestedDir).toBeDefined()
257
+
258
+
if (nestedDir && 'entries' in nestedDir.node) {
259
+
const folderDir = nestedDir.node.entries.find(e => e.name === 'folder')
260
+
expect(folderDir).toBeDefined()
261
+
262
+
if (folderDir && 'entries' in folderDir.node) {
263
+
expect(folderDir.node.entries).toHaveLength(1)
264
+
expect(folderDir.node.entries[0].name).toBe('file.txt')
265
+
}
266
+
}
267
+
}
268
+
})
269
+
270
+
test('should remove base folder name from paths', () => {
271
+
const files: UploadedFile[] = [
272
+
{
273
+
name: 'dist/index.html',
274
+
content: Buffer.from('<html></html>'),
275
+
mimeType: 'text/html',
276
+
size: 13,
277
+
},
278
+
{
279
+
name: 'dist/css/styles.css',
280
+
content: Buffer.from('body {}'),
281
+
mimeType: 'text/css',
282
+
size: 7,
283
+
},
284
+
]
285
+
286
+
const result = processUploadedFiles(files)
287
+
288
+
// After removing 'dist/', we should have index.html and css/ at root
289
+
expect(result.directory.entries.find(e => e.name === 'index.html')).toBeDefined()
290
+
expect(result.directory.entries.find(e => e.name === 'css')).toBeDefined()
291
+
expect(result.directory.entries.find(e => e.name === 'dist')).toBeUndefined()
292
+
})
293
+
294
+
test('should handle empty file list', () => {
295
+
const files: UploadedFile[] = []
296
+
const result = processUploadedFiles(files)
297
+
298
+
expect(result.fileCount).toBe(0)
299
+
expect(result.directory.entries).toHaveLength(0)
300
+
})
301
+
302
+
test('should handle multiple files in same subdirectory', () => {
303
+
const files: UploadedFile[] = [
304
+
{
305
+
name: 'dist/assets/image1.png',
306
+
content: Buffer.from('png1'),
307
+
mimeType: 'image/png',
308
+
size: 4,
309
+
},
310
+
{
311
+
name: 'dist/assets/image2.png',
312
+
content: Buffer.from('png2'),
313
+
mimeType: 'image/png',
314
+
size: 4,
315
+
},
316
+
]
317
+
318
+
const result = processUploadedFiles(files)
319
+
320
+
expect(result.fileCount).toBe(2)
321
+
322
+
const assetsDir = result.directory.entries.find(e => e.name === 'assets')
323
+
expect(assetsDir).toBeDefined()
324
+
325
+
if ('entries' in assetsDir!.node) {
326
+
expect(assetsDir!.node.entries).toHaveLength(2)
327
+
const names = assetsDir!.node.entries.map(e => e.name)
328
+
expect(names).toContain('image1.png')
329
+
expect(names).toContain('image2.png')
330
+
}
331
+
})
332
+
})
333
+
334
+
describe('createManifest', () => {
335
+
test('should create valid manifest', () => {
336
+
const root: Directory = {
337
+
$type: 'place.wisp.fs#directory',
338
+
type: 'directory',
339
+
entries: [],
340
+
}
341
+
342
+
const manifest = createManifest('example.com', root, 0)
343
+
344
+
expect(manifest.$type).toBe('place.wisp.fs')
345
+
expect(manifest.site).toBe('example.com')
346
+
expect(manifest.root).toBe(root)
347
+
expect(manifest.fileCount).toBe(0)
348
+
expect(manifest.createdAt).toBeDefined()
349
+
350
+
// Verify it's a valid ISO date string
351
+
const date = new Date(manifest.createdAt)
352
+
expect(date.toISOString()).toBe(manifest.createdAt)
353
+
})
354
+
355
+
test('should create manifest with file count', () => {
356
+
const root: Directory = {
357
+
$type: 'place.wisp.fs#directory',
358
+
type: 'directory',
359
+
entries: [],
360
+
}
361
+
362
+
const manifest = createManifest('test-site', root, 42)
363
+
364
+
expect(manifest.fileCount).toBe(42)
365
+
expect(manifest.site).toBe('test-site')
366
+
})
367
+
368
+
test('should create manifest with populated directory', () => {
369
+
const mockBlob = createMockBlobRef('text/html', 100)
370
+
371
+
const root: Directory = {
372
+
$type: 'place.wisp.fs#directory',
373
+
type: 'directory',
374
+
entries: [
375
+
{
376
+
name: 'index.html',
377
+
node: {
378
+
$type: 'place.wisp.fs#file',
379
+
type: 'file',
380
+
blob: mockBlob,
381
+
},
382
+
},
383
+
],
384
+
}
385
+
386
+
const manifest = createManifest('populated-site', root, 1)
387
+
388
+
expect(manifest).toBeDefined()
389
+
expect(manifest.site).toBe('populated-site')
390
+
expect(manifest.root.entries).toHaveLength(1)
391
+
})
392
+
})
393
+
394
+
describe('updateFileBlobs', () => {
395
+
test('should update single file blob at root', () => {
396
+
const directory: Directory = {
397
+
$type: 'place.wisp.fs#directory',
398
+
type: 'directory',
399
+
entries: [
400
+
{
401
+
name: 'index.html',
402
+
node: {
403
+
$type: 'place.wisp.fs#file',
404
+
type: 'file',
405
+
blob: undefined as any,
406
+
},
407
+
},
408
+
],
409
+
}
410
+
411
+
const mockBlob = createMockBlobRef('text/html', 100)
412
+
const uploadResults: FileUploadResult[] = [
413
+
{
414
+
hash: TEST_CID_STRING,
415
+
blobRef: mockBlob,
416
+
mimeType: 'text/html',
417
+
},
418
+
]
419
+
420
+
const filePaths = ['index.html']
421
+
422
+
const updated = updateFileBlobs(directory, uploadResults, filePaths)
423
+
424
+
expect(updated.entries).toHaveLength(1)
425
+
const fileNode = updated.entries[0].node
426
+
427
+
if ('blob' in fileNode) {
428
+
expect(fileNode.blob).toBeDefined()
429
+
expect(fileNode.blob.mimeType).toBe('text/html')
430
+
expect(fileNode.blob.size).toBe(100)
431
+
} else {
432
+
throw new Error('Expected file node')
433
+
}
434
+
})
435
+
436
+
test('should update files in nested directories', () => {
437
+
const directory: Directory = {
438
+
$type: 'place.wisp.fs#directory',
439
+
type: 'directory',
440
+
entries: [
441
+
{
442
+
name: 'css',
443
+
node: {
444
+
$type: 'place.wisp.fs#directory',
445
+
type: 'directory',
446
+
entries: [
447
+
{
448
+
name: 'styles.css',
449
+
node: {
450
+
$type: 'place.wisp.fs#file',
451
+
type: 'file',
452
+
blob: undefined as any,
453
+
},
454
+
},
455
+
],
456
+
},
457
+
},
458
+
],
459
+
}
460
+
461
+
const mockBlob = createMockBlobRef('text/css', 50)
462
+
const uploadResults: FileUploadResult[] = [
463
+
{
464
+
hash: TEST_CID_STRING,
465
+
blobRef: mockBlob,
466
+
mimeType: 'text/css',
467
+
encoding: 'gzip',
468
+
},
469
+
]
470
+
471
+
const filePaths = ['css/styles.css']
472
+
473
+
const updated = updateFileBlobs(directory, uploadResults, filePaths)
474
+
475
+
const cssDir = updated.entries[0]
476
+
expect(cssDir.name).toBe('css')
477
+
478
+
if ('entries' in cssDir.node) {
479
+
const cssFile = cssDir.node.entries[0]
480
+
expect(cssFile.name).toBe('styles.css')
481
+
482
+
if ('blob' in cssFile.node) {
483
+
expect(cssFile.node.blob.mimeType).toBe('text/css')
484
+
if ('encoding' in cssFile.node) {
485
+
expect(cssFile.node.encoding).toBe('gzip')
486
+
}
487
+
} else {
488
+
throw new Error('Expected file node')
489
+
}
490
+
} else {
491
+
throw new Error('Expected directory node')
492
+
}
493
+
})
494
+
495
+
test('should handle normalized paths with base folder removed', () => {
496
+
const directory: Directory = {
497
+
$type: 'place.wisp.fs#directory',
498
+
type: 'directory',
499
+
entries: [
500
+
{
501
+
name: 'index.html',
502
+
node: {
503
+
$type: 'place.wisp.fs#file',
504
+
type: 'file',
505
+
blob: undefined as any,
506
+
},
507
+
},
508
+
],
509
+
}
510
+
511
+
const mockBlob = createMockBlobRef('text/html', 100)
512
+
const uploadResults: FileUploadResult[] = [
513
+
{
514
+
hash: TEST_CID_STRING,
515
+
blobRef: mockBlob,
516
+
},
517
+
]
518
+
519
+
// Path includes base folder that should be normalized
520
+
const filePaths = ['dist/index.html']
521
+
522
+
const updated = updateFileBlobs(directory, uploadResults, filePaths)
523
+
524
+
const fileNode = updated.entries[0].node
525
+
if ('blob' in fileNode) {
526
+
expect(fileNode.blob).toBeDefined()
527
+
} else {
528
+
throw new Error('Expected file node')
529
+
}
530
+
})
531
+
532
+
test('should preserve file metadata (encoding, mimeType, base64)', () => {
533
+
const directory: Directory = {
534
+
$type: 'place.wisp.fs#directory',
535
+
type: 'directory',
536
+
entries: [
537
+
{
538
+
name: 'data.json',
539
+
node: {
540
+
$type: 'place.wisp.fs#file',
541
+
type: 'file',
542
+
blob: undefined as any,
543
+
},
544
+
},
545
+
],
546
+
}
547
+
548
+
const mockBlob = createMockBlobRef('application/json', 200)
549
+
const uploadResults: FileUploadResult[] = [
550
+
{
551
+
hash: TEST_CID_STRING,
552
+
blobRef: mockBlob,
553
+
mimeType: 'application/json',
554
+
encoding: 'gzip',
555
+
base64: true,
556
+
},
557
+
]
558
+
559
+
const filePaths = ['data.json']
560
+
561
+
const updated = updateFileBlobs(directory, uploadResults, filePaths)
562
+
563
+
const fileNode = updated.entries[0].node
564
+
if ('blob' in fileNode && 'mimeType' in fileNode && 'encoding' in fileNode && 'base64' in fileNode) {
565
+
expect(fileNode.mimeType).toBe('application/json')
566
+
expect(fileNode.encoding).toBe('gzip')
567
+
expect(fileNode.base64).toBe(true)
568
+
} else {
569
+
throw new Error('Expected file node with metadata')
570
+
}
571
+
})
572
+
573
+
test('should handle multiple files at different directory levels', () => {
574
+
const directory: Directory = {
575
+
$type: 'place.wisp.fs#directory',
576
+
type: 'directory',
577
+
entries: [
578
+
{
579
+
name: 'index.html',
580
+
node: {
581
+
$type: 'place.wisp.fs#file',
582
+
type: 'file',
583
+
blob: undefined as any,
584
+
},
585
+
},
586
+
{
587
+
name: 'assets',
588
+
node: {
589
+
$type: 'place.wisp.fs#directory',
590
+
type: 'directory',
591
+
entries: [
592
+
{
593
+
name: 'logo.svg',
594
+
node: {
595
+
$type: 'place.wisp.fs#file',
596
+
type: 'file',
597
+
blob: undefined as any,
598
+
},
599
+
},
600
+
],
601
+
},
602
+
},
603
+
],
604
+
}
605
+
606
+
const htmlBlob = createMockBlobRef('text/html', 100)
607
+
const svgBlob = createMockBlobRef('image/svg+xml', 500)
608
+
609
+
const uploadResults: FileUploadResult[] = [
610
+
{
611
+
hash: TEST_CID_STRING,
612
+
blobRef: htmlBlob,
613
+
},
614
+
{
615
+
hash: TEST_CID_STRING,
616
+
blobRef: svgBlob,
617
+
},
618
+
]
619
+
620
+
const filePaths = ['index.html', 'assets/logo.svg']
621
+
622
+
const updated = updateFileBlobs(directory, uploadResults, filePaths)
623
+
624
+
// Check root file
625
+
const indexNode = updated.entries[0].node
626
+
if ('blob' in indexNode) {
627
+
expect(indexNode.blob.mimeType).toBe('text/html')
628
+
}
629
+
630
+
// Check nested file
631
+
const assetsDir = updated.entries[1]
632
+
if ('entries' in assetsDir.node) {
633
+
const logoNode = assetsDir.node.entries[0].node
634
+
if ('blob' in logoNode) {
635
+
expect(logoNode.blob.mimeType).toBe('image/svg+xml')
636
+
}
637
+
}
638
+
})
639
+
})