+64
-35
bun.lock
+64
-35
bun.lock
···
25
25
"elysia": "latest",
26
26
"iron-session": "^8.0.4",
27
27
"lucide-react": "^0.546.0",
28
+
"multiformats": "^13.4.1",
28
29
"react": "^19.2.0",
29
30
"react-dom": "^19.2.0",
30
31
"react-shiki": "^0.9.0",
···
44
45
},
45
46
"trustedDependencies": [
46
47
"core-js",
48
+
"cbor-extract",
49
+
"bun",
47
50
"protobufjs",
48
51
],
49
52
"packages": {
···
51
54
52
55
"@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.3", "", { "dependencies": { "@atproto-labs/pipe": "0.1.1" } }, "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="],
53
56
54
-
"@atproto-labs/fetch-node": ["@atproto-labs/fetch-node@0.1.10", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "ipaddr.js": "^2.1.0", "undici": "^6.14.1" } }, "sha512-o7hGaonA71A6p7O107VhM6UBUN/g9tTyYohMp1q0Kf6xQ4npnuZYRSHSf2g6reSfGQJ1GoFNjBObETTT1ge/jQ=="],
57
+
"@atproto-labs/fetch-node": ["@atproto-labs/fetch-node@0.2.0", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "ipaddr.js": "^2.1.0", "undici": "^6.14.1" } }, "sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q=="],
55
58
56
59
"@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.2", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.1", "zod": "^3.23.8" } }, "sha512-KIerCzh3qb+zZoqWbIvTlvBY0XPq0r56kwViaJY/LTe/3oPO2JaqlYKS/F4dByWBhHK6YoUOJ0sWrh6PMJl40A=="],
57
60
58
-
"@atproto-labs/handle-resolver-node": ["@atproto-labs/handle-resolver-node@0.1.20", "", { "dependencies": { "@atproto-labs/fetch-node": "0.1.10", "@atproto-labs/handle-resolver": "0.3.2", "@atproto/did": "0.2.1" } }, "sha512-094EL61XN9M7vm22cloSOxk/gcTRaCK52vN7BYgXgdoEI8uJJMTFXenQqu+LRGwiCcjvyclcBqbaz0DzJep50Q=="],
61
+
"@atproto-labs/handle-resolver-node": ["@atproto-labs/handle-resolver-node@0.1.21", "", { "dependencies": { "@atproto-labs/fetch-node": "0.2.0", "@atproto-labs/handle-resolver": "0.3.2", "@atproto/did": "0.2.1" } }, "sha512-fuJy5Px5pGF3lJX/ATdurbT8tbmaFWtf+PPxAQDFy7ot2no3t+iaAgymhyxYymrssOuWs6BwOP8tyF3VrfdwtQ=="],
59
62
60
63
"@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.3.2", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.2", "@atproto-labs/handle-resolver": "0.3.2" } }, "sha512-MYxO9pe0WsFyi5HFdKAwqIqHfiF2kBPoVhAIuH/4PYHzGr799ED47xLhNMxR3ZUYrJm5+TQzWXypGZ0Btw1Ffw=="],
61
64
···
65
68
66
69
"@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="],
67
70
68
-
"@atproto/api": ["@atproto/api@0.17.3", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "@atproto/xrpc": "^0.7.5", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-pdQXhUAapNPdmN00W0vX5ta/aMkHqfgBHATt20X02XwxQpY2AnrPm2Iog4FyjsZqoHooAtCNV/NWJ4xfddJzsg=="],
71
+
"@atproto/api": ["@atproto/api@0.17.7", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "@atproto/xrpc": "^0.7.5", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-V+OJBZq9chcrD21xk1bUa6oc5DSKfQj5DmUPf5rmZncqL1w9ZEbS38H5cMyqqdhfgo2LWeDRdZHD0rvNyJsIaw=="],
69
72
70
73
"@atproto/common": ["@atproto/common@0.4.12", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@ipld/dag-cbor": "^7.0.3", "cbor-x": "^1.5.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-NC+TULLQiqs6MvNymhQS5WDms3SlbIKGLf4n33tpftRJcalh507rI+snbcUb7TLIkKw7VO17qMqxEXtIdd5auQ=="],
71
74
···
81
84
82
85
"@atproto/jwk-webcrypto": ["@atproto/jwk-webcrypto@0.2.0", "", { "dependencies": { "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "zod": "^3.23.8" } }, "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg=="],
83
86
84
-
"@atproto/lex-cli": ["@atproto/lex-cli@0.9.5", "", { "dependencies": { "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "chalk": "^4.1.2", "commander": "^9.4.0", "prettier": "^3.2.5", "ts-morph": "^24.0.0", "yesno": "^0.4.0", "zod": "^3.23.8" }, "bin": { "lex": "dist/index.js" } }, "sha512-zun4jhD1dbjD7IHtLIjh/1UsMx+6E8+OyOT2GXYAKIj9N6wmLKM/v2OeQBKxcyqUmtRi57lxWnGikWjjU7pplQ=="],
87
+
"@atproto/lex-cli": ["@atproto/lex-cli@0.9.6", "", { "dependencies": { "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "chalk": "^4.1.2", "commander": "^9.4.0", "prettier": "^3.2.5", "ts-morph": "^24.0.0", "yesno": "^0.4.0", "zod": "^3.23.8" }, "bin": { "lex": "dist/index.js" } }, "sha512-EedEKmURoSP735YwSDHsFrLOhZ4P2it8goCHv5ApWi/R9DFpOKOpmYfIXJ9MAprK8cw+yBnjDJbzpLJy7UXlTg=="],
85
88
86
89
"@atproto/lexicon": ["@atproto/lexicon@0.5.1", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A=="],
87
90
88
-
"@atproto/oauth-client": ["@atproto/oauth-client@0.5.7", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.2", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.2", "@atproto-labs/identity-resolver": "0.3.2", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.1", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.4.2", "@atproto/xrpc": "0.7.5", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-pDvbvy9DCxrAJv7bAbBUzWrHZKhFy091HvEMZhr+EyZA6gSCGYmmQJG/coDj0oICSVQeafAZd+IxR0YUCWwmEg=="],
91
+
"@atproto/oauth-client": ["@atproto/oauth-client@0.5.8", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.2", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.2", "@atproto-labs/identity-resolver": "0.3.2", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.1", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.5.0", "@atproto/xrpc": "0.7.5", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-7YEym6d97+Dd73qGdkQTXi5La8xvCQxwRUDzzlR/NVAARa9a4YP7MCmqBJVeP2anT0By+DSAPyPDLTsxcjIcCg=="],
89
92
90
-
"@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.3.9", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.2", "@atproto-labs/handle-resolver-node": "0.1.20", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.2.1", "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.7", "@atproto/oauth-types": "0.4.2" } }, "sha512-JdzwDQ8Gczl0lgfJNm7lG7omkJ4yu99IuGkkRWixpEvKY/jY/mDZaho+3pfd29SrUvwQOOx4Bm4l7DGeYwxxyA=="],
93
+
"@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.3.10", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.2", "@atproto-labs/handle-resolver-node": "0.1.21", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.2.1", "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.8", "@atproto/oauth-types": "0.5.0" } }, "sha512-6khKlJqu1Ed5rt3rzcTD5hymB6JUjKdOHWYXwiphw4inkAIo6GxLCighI4eGOqZorYk2j8ueeTNB6KsgH0kcRw=="],
91
94
92
-
"@atproto/oauth-types": ["@atproto/oauth-types@0.4.2", "", { "dependencies": { "@atproto/did": "0.2.1", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-gcfNTyFsPJcYDf79M0iKHykWqzxloscioKoerdIN3MTS3htiNOSgZjm2p8ho7pdrElLzea3qktuhTQI39j1XFQ=="],
95
+
"@atproto/oauth-types": ["@atproto/oauth-types@0.5.0", "", { "dependencies": { "@atproto/did": "0.2.1", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-33xz7HcXhbl+XRqbIMVu3GE02iK1nKe2oMWENASsfZEYbCz2b9ZOarOFuwi7g4LKqpGowGp0iRKsQHFcq4SDaQ=="],
93
96
94
97
"@atproto/syntax": ["@atproto/syntax@0.4.1", "", {}, "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw=="],
95
98
···
113
116
114
117
"@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="],
115
118
116
-
"@elysiajs/eden": ["@elysiajs/eden@1.4.3", "", { "peerDependencies": { "elysia": ">= 1.4.0-exp.0" } }, "sha512-mX0v5cTvTJiDsDWNEEyuoqudOvW5J+tXsvp/ZOJXJF3iCIEJI0Brvm78ymPrvwiOG4nUr3lS8BxUfbNf32DSXA=="],
119
+
"@elysiajs/eden": ["@elysiajs/eden@1.4.4", "", { "peerDependencies": { "elysia": ">= 1.4.0-exp.0" } }, "sha512-/LVqflmgUcCiXb8rz1iRq9Rx3SWfIV/EkoNqDFGMx+TvOyo8QHAygFXAVQz7RHs+jk6n6mEgpI6KlKBANoErsQ=="],
117
120
118
121
"@elysiajs/openapi": ["@elysiajs/openapi@1.4.11", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-d75bMxYJpN6qSDi/z9L1S7SLk1S/8Px+cTb3W2lrYzU8uQ5E0kXdy1oOMJEfTyVsz3OA19NP9KNxE7ztSbLBLg=="],
119
122
120
123
"@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=="],
121
124
122
-
"@elysiajs/static": ["@elysiajs/static@1.4.2", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lAEvdxeBhU/jX/hTzfoP+1AtqhsKNCwW4Q+tfNwAShWU6s4ZPQxR1hLoHBveeApofJt4HWEq/tBGvfFz3ODuKg=="],
125
+
"@elysiajs/static": ["@elysiajs/static@1.4.6", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-cd61aY/DHOVhlnBjzTBX8E1XANIrsCH8MwEGHeLMaZzNrz0gD4Q8Qsde2dFMzu81I7ZDaaZ2Rim9blSLtUrYBg=="],
123
126
124
-
"@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=="],
127
+
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.1", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ=="],
125
128
126
129
"@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=="],
127
130
···
187
190
188
191
"@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=="],
189
192
190
-
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.37.0", "", {}, "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA=="],
193
+
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.38.0", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="],
191
194
192
-
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WeXSaL29ylJEZMYHHW28QZ6rgAbxQ1KuNSZD9gvd3fPlo0s6s2PglvPArjjP07nmvIK9m4OffN0k4M98O7WmAg=="],
195
+
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-licBDIbbLP5L5/S0+bwtJynso94XD3KyqSP48K59Sq7Mude6C7dR5ZujZm4Ut4BwZqUFfNOfYNMWBU5nlL7t1A=="],
193
196
194
-
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CFKjoUWQH0Oz3UHYfKbdKLq0wGryrFsTJEYq839qAwHQSECvVZYAnxVVDYUDa0yQFonhO2qSHY41f6HK+b7xtw=="],
197
+
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-hn8lLzsYyyh6ULo2E8v2SqtrWOkdQKJwapeVy1rDw7juTTeHY3KDudGWf4mVYteC9riZU6HD88Fn3nGwyX0eIg=="],
195
198
196
-
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-+FSr/ub5vA/EkD3fMhHJUzYioSf/sXd50OGxNDAntVxcDu4tXL/81Ka3R/gkZmjznpLFIzovU/1Ts+b7dlkrfw=="],
199
+
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-UHxdtbyxdtNJUNcXtIrjx3Lmq8ji3KywlXtIHV/0vn9A8W5mulqOcryqUWMFVH9JTIIzmNn6Q/qVmXHTME63Ww=="],
197
200
198
-
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WHthS/eLkCNcp9pk4W8aubRl9fIUgt2XhHyLrP0GClB1FVvmodu/zIOtG0NXNpzlzB8+gglOkGo4dPjfVf4Z+g=="],
201
+
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5uZzxzvHU/z+3cZwN/A0H8G+enQ+9FkeJVZkE2fwK2XhiJZFUGAuWajCpy7GepvOWlqV7VjPaKi2+Qmr4IX7nQ=="],
199
202
200
-
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-HT5sr7N8NDYbQRjAnT7ISpx64y+ewZZRQozOJb0+KQObKvg4UUNXGm4Pn1xA4/WPMZDDazjO8E2vtOQw1nJlAQ=="],
203
+
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-OD9DYkjes7WXieBn4zQZGXWhRVZhIEWMDGCetZ3H4vxIuweZ++iul/CNX5jdpNXaJ17myb1ROMvmRbrqW44j3w=="],
201
204
202
-
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-sGEWoJQXO4GDr0x4t/yJQ/Bq1yNkOdX9tHbZZ+DBGJt3z3r7jeb4Digv8xQUk6gdTFC9vnGHuin+KW3/yD1Aww=="],
205
+
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EoEuRP9bxAxVKuvi6tZ0ZENjueP4lvjz0mKsMzdG0kwg/2apGKiirH1l0RIcdmvfDGGuDmNiv/XBpkoXq1x8ug=="],
203
206
204
-
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-OmlEH3nlxQyv7HOvTH21vyNAZGv9DIPnrTznzvKiOQxkOphhCyKvPTlF13ydw4s/i18iwaUrhHy+YG9HSSxa4Q=="],
207
+
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m9Ov9YH8KjRLui87eNtQQFKVnjGsNk3xgbrR9c8d2FS3NfZSxmVjSeBvEsDjzNf1TXLDriHb/NYOlpiMf/QzDg=="],
205
208
206
-
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rtzUEzCynl3Rhgn/iR9DQezSFiZMcAXAbU+xfROqsweMGKwvwIA2ckyyckO08psEP8XcUZTs3LT9CH7PnaMiEA=="],
209
+
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-3TuOsRVoG8K+soQWRo+Cp5ACpRs6rTFSu5tAqc/6WrqwbNWmqjov/eWJPTgz3gPXnC7uNKVG7RxxAmV8r2EYTQ=="],
207
210
208
-
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-hrr7mDvUjMX1tuJaXz448tMsgKIqGJBY8+rJqztKOw1U5+a/v2w5HuIIW1ce7ut0ZwEn+KIDvAujlPvpH33vpQ=="],
211
+
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-q8Hto8hcpofPJjvuvjuwyYvhOaAzPw1F5vRUUeOJDmDwZ4lZhANFM0rUwchMzfWUJCD6jg8/EVQ8MiixnZWU0A=="],
209
212
210
-
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xXwtpZVVP7T+vkxcF/TUVVOGRjEfkByO4mKveKYb4xnHWV4u4NnV0oNmzyMKkvmj10to5j2h0oZxA4ZVVv4gfA=="],
213
+
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-nZJUa5NprPYQ4Ii4cMwtP9PzlJJTp1XhxJ+A9eSn1Jfr6YygVWyN2KLjenyI93IcuBouBAaepDAVZZjH2lFBhg=="],
211
214
212
-
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/jVZ8eYjpYHLDFNoT86cP+AjuWvpkzFY+0R0a1bdeu0sQ6ILuy1FV6hz1hUAP390E09VCo5oP76fnx29giHTtA=="],
215
+
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-s00T99MjB+xLOWq+t+wVaVBrry+oBOZNiTJijt+bmkp/MJptYS3FGvs7a+nkjLNzoNDoWQcXgKew6AaHES37Bg=="],
213
216
214
217
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
215
218
···
253
256
254
257
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
255
258
256
-
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
259
+
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
257
260
258
261
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
259
262
···
265
268
266
269
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
267
270
268
-
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
271
+
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
269
272
270
273
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
271
274
···
299
302
300
303
"@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="],
301
304
302
-
"@tanstack/query-core": ["@tanstack/query-core@5.90.2", "", {}, "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ=="],
305
+
"@tanstack/query-core": ["@tanstack/query-core@5.90.7", "", {}, "sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ=="],
303
306
304
-
"@tanstack/react-query": ["@tanstack/react-query@5.90.2", "", { "dependencies": { "@tanstack/query-core": "5.90.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw=="],
307
+
"@tanstack/react-query": ["@tanstack/react-query@5.90.7", "", { "dependencies": { "@tanstack/query-core": "5.90.7" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ=="],
305
308
306
309
"@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=="],
307
310
···
321
324
322
325
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
323
326
324
-
"@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="],
327
+
"@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
325
328
326
329
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
327
330
328
-
"@types/react-dom": ["@types/react-dom@19.2.1", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A=="],
331
+
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
329
332
330
333
"@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="],
331
334
···
363
366
364
367
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
365
368
366
-
"bun": ["bun@1.3.0", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.0", "@oven/bun-darwin-x64": "1.3.0", "@oven/bun-darwin-x64-baseline": "1.3.0", "@oven/bun-linux-aarch64": "1.3.0", "@oven/bun-linux-aarch64-musl": "1.3.0", "@oven/bun-linux-x64": "1.3.0", "@oven/bun-linux-x64-baseline": "1.3.0", "@oven/bun-linux-x64-musl": "1.3.0", "@oven/bun-linux-x64-musl-baseline": "1.3.0", "@oven/bun-windows-x64": "1.3.0", "@oven/bun-windows-x64-baseline": "1.3.0" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-YI7mFs7iWc/VsGsh2aw6eAPD2cjzn1j+LKdYVk09x1CrdTWKYIHyd+dG5iQoN9//3hCDoZj8U6vKpZzEf5UARA=="],
369
+
"bun": ["bun@1.3.2", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.2", "@oven/bun-darwin-x64": "1.3.2", "@oven/bun-darwin-x64-baseline": "1.3.2", "@oven/bun-linux-aarch64": "1.3.2", "@oven/bun-linux-aarch64-musl": "1.3.2", "@oven/bun-linux-x64": "1.3.2", "@oven/bun-linux-x64-baseline": "1.3.2", "@oven/bun-linux-x64-musl": "1.3.2", "@oven/bun-linux-x64-musl-baseline": "1.3.2", "@oven/bun-windows-x64": "1.3.2", "@oven/bun-windows-x64-baseline": "1.3.2" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-x75mPJiEfhO1j4Tfc65+PtW6ZyrAB6yTZInydnjDZXF9u9PRAnr6OK3v0Q9dpDl0dxRHkXlYvJ8tteJxc8t4Sw=="],
367
370
368
371
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
369
372
370
-
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
373
+
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
371
374
372
375
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
373
376
···
443
446
444
447
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
445
448
446
-
"elysia": ["elysia@1.4.11", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-cphuzQj0fRw1ICRvwHy2H3xQio9bycaZUVHnDHJQnKqBfMNlZ+Hzj6TMmt9lc0Az0mvbCnPXWVF7y1MCRhUuOA=="],
449
+
"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=="],
447
450
448
451
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
449
452
···
579
582
580
583
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
581
584
585
+
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
586
+
582
587
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
583
588
584
589
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
···
637
642
638
643
"ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
639
644
640
-
"multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
645
+
"multiformats": ["multiformats@13.4.1", "", {}, "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q=="],
641
646
642
647
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
643
648
···
777
782
778
783
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
779
784
780
-
"tailwindcss": ["tailwindcss@4.1.14", "", {}, "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA=="],
785
+
"tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
781
786
782
787
"thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="],
783
788
784
789
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
785
790
786
-
"tlds": ["tlds@1.260.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ=="],
791
+
"tlds": ["tlds@1.261.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA=="],
787
792
788
793
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
789
794
···
809
814
810
815
"undici": ["undici@6.22.0", "", {}, "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw=="],
811
816
812
-
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
817
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
813
818
814
819
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
815
820
···
853
858
854
859
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
855
860
861
+
"@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
862
+
863
+
"@atproto/common/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
864
+
865
+
"@atproto/common-web/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
866
+
867
+
"@atproto/jwk/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
868
+
869
+
"@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
870
+
871
+
"@atproto/oauth-client/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
872
+
873
+
"@ipld/dag-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
874
+
875
+
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
876
+
877
+
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
878
+
879
+
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
880
+
881
+
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
882
+
856
883
"@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
857
884
858
885
"express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
···
870
897
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
871
898
872
899
"send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
900
+
901
+
"uint8arrays/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
873
902
874
903
"@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
875
904
+120
-23
hosting-service/src/lib/utils.ts
+120
-23
hosting-service/src/lib/utils.ts
···
13
13
cachedAt: number;
14
14
did: string;
15
15
rkey: string;
16
+
// Map of file path to blob CID for incremental updates
17
+
fileCids?: Record<string, string>;
16
18
}
17
19
18
20
/**
···
200
202
throw new Error('Invalid record structure: root missing entries array');
201
203
}
202
204
205
+
// Get existing cache metadata to check for incremental updates
206
+
const existingMetadata = await getCacheMetadata(did, rkey);
207
+
const existingFileCids = existingMetadata?.fileCids || {};
208
+
203
209
// Use a temporary directory with timestamp to avoid collisions
204
210
const tempSuffix = `.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
205
211
const tempDir = `${CACHE_DIR}/${did}/${rkey}${tempSuffix}`;
206
212
const finalDir = `${CACHE_DIR}/${did}/${rkey}`;
207
213
208
214
try {
209
-
// Download to temporary directory
210
-
await cacheFiles(did, rkey, record.root.entries, pdsEndpoint, '', tempSuffix);
211
-
await saveCacheMetadata(did, rkey, recordCid, tempSuffix);
215
+
// Collect file CIDs from the new record
216
+
const newFileCids: Record<string, string> = {};
217
+
collectFileCidsFromEntries(record.root.entries, '', newFileCids);
218
+
219
+
// Download/copy files to temporary directory (with incremental logic)
220
+
await cacheFiles(did, rkey, record.root.entries, pdsEndpoint, '', tempSuffix, existingFileCids, finalDir);
221
+
await saveCacheMetadata(did, rkey, recordCid, tempSuffix, newFileCids);
212
222
213
223
// Atomically replace old cache with new cache
214
224
// On POSIX systems (Linux/macOS), rename is atomic
···
245
255
}
246
256
}
247
257
258
+
/**
259
+
* Recursively collect file CIDs from entries for incremental update tracking
260
+
*/
261
+
function collectFileCidsFromEntries(entries: Entry[], pathPrefix: string, fileCids: Record<string, string>): void {
262
+
for (const entry of entries) {
263
+
const currentPath = pathPrefix ? `${pathPrefix}/${entry.name}` : entry.name;
264
+
const node = entry.node;
265
+
266
+
if ('type' in node && node.type === 'directory' && 'entries' in node) {
267
+
collectFileCidsFromEntries(node.entries, currentPath, fileCids);
268
+
} else if ('type' in node && node.type === 'file' && 'blob' in node) {
269
+
const fileNode = node as File;
270
+
const cid = extractBlobCid(fileNode.blob);
271
+
if (cid) {
272
+
fileCids[currentPath] = cid;
273
+
}
274
+
}
275
+
}
276
+
}
277
+
248
278
async function cacheFiles(
249
279
did: string,
250
280
site: string,
251
281
entries: Entry[],
252
282
pdsEndpoint: string,
253
283
pathPrefix: string,
254
-
dirSuffix: string = ''
284
+
dirSuffix: string = '',
285
+
existingFileCids: Record<string, string> = {},
286
+
existingCacheDir?: string
255
287
): Promise<void> {
256
-
// Collect all file blob download tasks first
288
+
// Collect file tasks, separating unchanged files from new/changed files
257
289
const downloadTasks: Array<() => Promise<void>> = [];
258
-
290
+
const copyTasks: Array<() => Promise<void>> = [];
291
+
259
292
function collectFileTasks(
260
293
entries: Entry[],
261
294
currentPathPrefix: string
···
268
301
collectFileTasks(node.entries, currentPath);
269
302
} else if ('type' in node && node.type === 'file' && 'blob' in node) {
270
303
const fileNode = node as File;
271
-
downloadTasks.push(() => cacheFileBlob(
272
-
did,
273
-
site,
274
-
currentPath,
275
-
fileNode.blob,
276
-
pdsEndpoint,
277
-
fileNode.encoding,
278
-
fileNode.mimeType,
279
-
fileNode.base64,
280
-
dirSuffix
281
-
));
304
+
const cid = extractBlobCid(fileNode.blob);
305
+
306
+
// Check if file is unchanged (same CID as existing cache)
307
+
if (cid && existingFileCids[currentPath] === cid && existingCacheDir) {
308
+
// File unchanged - copy from existing cache instead of downloading
309
+
copyTasks.push(() => copyExistingFile(
310
+
did,
311
+
site,
312
+
currentPath,
313
+
dirSuffix,
314
+
existingCacheDir
315
+
));
316
+
} else {
317
+
// File new or changed - download it
318
+
downloadTasks.push(() => cacheFileBlob(
319
+
did,
320
+
site,
321
+
currentPath,
322
+
fileNode.blob,
323
+
pdsEndpoint,
324
+
fileNode.encoding,
325
+
fileNode.mimeType,
326
+
fileNode.base64,
327
+
dirSuffix
328
+
));
329
+
}
282
330
}
283
331
}
284
332
}
285
333
286
334
collectFileTasks(entries, pathPrefix);
287
335
288
-
// Execute downloads concurrently with a limit of 3 at a time
289
-
const concurrencyLimit = 3;
290
-
for (let i = 0; i < downloadTasks.length; i += concurrencyLimit) {
291
-
const batch = downloadTasks.slice(i, i + concurrencyLimit);
336
+
console.log(`[Incremental Update] Files to copy: ${copyTasks.length}, Files to download: ${downloadTasks.length}`);
337
+
338
+
// Copy unchanged files in parallel (fast local operations)
339
+
const copyLimit = 10;
340
+
for (let i = 0; i < copyTasks.length; i += copyLimit) {
341
+
const batch = copyTasks.slice(i, i + copyLimit);
342
+
await Promise.all(batch.map(task => task()));
343
+
}
344
+
345
+
// Download new/changed files concurrently with a limit of 3 at a time
346
+
const downloadLimit = 3;
347
+
for (let i = 0; i < downloadTasks.length; i += downloadLimit) {
348
+
const batch = downloadTasks.slice(i, i + downloadLimit);
292
349
await Promise.all(batch.map(task => task()));
350
+
}
351
+
}
352
+
353
+
/**
354
+
* Copy an unchanged file from existing cache to new cache location
355
+
*/
356
+
async function copyExistingFile(
357
+
did: string,
358
+
site: string,
359
+
filePath: string,
360
+
dirSuffix: string,
361
+
existingCacheDir: string
362
+
): Promise<void> {
363
+
const { copyFile } = await import('fs/promises');
364
+
365
+
const sourceFile = `${existingCacheDir}/${filePath}`;
366
+
const destFile = `${CACHE_DIR}/${did}/${site}${dirSuffix}/${filePath}`;
367
+
const destDir = destFile.substring(0, destFile.lastIndexOf('/'));
368
+
369
+
// Create destination directory if needed
370
+
if (destDir && !existsSync(destDir)) {
371
+
mkdirSync(destDir, { recursive: true });
372
+
}
373
+
374
+
try {
375
+
// Copy the file
376
+
await copyFile(sourceFile, destFile);
377
+
378
+
// Copy metadata file if it exists
379
+
const sourceMetaFile = `${sourceFile}.meta`;
380
+
const destMetaFile = `${destFile}.meta`;
381
+
if (existsSync(sourceMetaFile)) {
382
+
await copyFile(sourceMetaFile, destMetaFile);
383
+
}
384
+
385
+
console.log(`[Incremental] Copied unchanged file: ${filePath}`);
386
+
} catch (err) {
387
+
console.error(`[Incremental] Failed to copy file ${filePath}, will attempt download:`, err);
388
+
throw err;
293
389
}
294
390
}
295
391
···
404
500
return existsSync(`${CACHE_DIR}/${did}/${site}`);
405
501
}
406
502
407
-
async function saveCacheMetadata(did: string, rkey: string, recordCid: string, dirSuffix: string = ''): Promise<void> {
503
+
async function saveCacheMetadata(did: string, rkey: string, recordCid: string, dirSuffix: string = '', fileCids?: Record<string, string>): Promise<void> {
408
504
const metadata: CacheMetadata = {
409
505
recordCid,
410
506
cachedAt: Date.now(),
411
507
did,
412
-
rkey
508
+
rkey,
509
+
fileCids
413
510
};
414
511
415
512
const metadataPath = `${CACHE_DIR}/${did}/${rkey}${dirSuffix}/.metadata.json`;
+3
-1
hosting-service/tsconfig.json
+3
-1
hosting-service/tsconfig.json
+3
package.json
+3
package.json
···
29
29
"elysia": "latest",
30
30
"iron-session": "^8.0.4",
31
31
"lucide-react": "^0.546.0",
32
+
"multiformats": "^13.4.1",
32
33
"react": "^19.2.0",
33
34
"react-dom": "^19.2.0",
34
35
"react-shiki": "^0.9.0",
···
46
47
},
47
48
"module": "src/index.js",
48
49
"trustedDependencies": [
50
+
"bun",
51
+
"cbor-extract",
49
52
"core-js",
50
53
"protobufjs"
51
54
]
+2
-2
public/editor/editor.tsx
+2
-2
public/editor/editor.tsx
···
748
748
749
749
<div className="p-4 bg-muted/30 rounded-lg border-l-4 border-yellow-500/50">
750
750
<div className="flex items-start gap-2">
751
-
<AlertCircle className="w-4 h-4 text-yellow-600 dark:text-yellow-400 mt-0.5 flex-shrink-0" />
751
+
<AlertCircle className="w-4 h-4 text-yellow-600 dark:text-yellow-400 mt-0.5 shrink-0" />
752
752
<div className="flex-1 space-y-1">
753
753
<p className="text-xs font-semibold text-yellow-600 dark:text-yellow-400">
754
754
Note about sites.wisp.place URLs
···
1120
1120
{skippedFiles.length > 0 && (
1121
1121
<div className="p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
1122
1122
<div className="flex items-start gap-2 text-yellow-600 dark:text-yellow-400 mb-2">
1123
-
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
1123
+
<AlertCircle className="w-4 h-4 mt-0.5 shrink-0" />
1124
1124
<div className="flex-1">
1125
1125
<span className="font-medium">
1126
1126
{skippedFiles.length} file{skippedFiles.length > 1 ? 's' : ''} skipped
+59
-7
src/lib/db.ts
+59
-7
src/lib/db.ts
···
108
108
)
109
109
`;
110
110
111
+
// Create indexes for common query patterns
112
+
await Promise.all([
113
+
// oauth_states cleanup queries
114
+
db`CREATE INDEX IF NOT EXISTS idx_oauth_states_expires_at ON oauth_states(expires_at)`.catch(err => {
115
+
if (!err.message?.includes('already exists')) {
116
+
console.error('Failed to create idx_oauth_states_expires_at:', err);
117
+
}
118
+
}),
119
+
120
+
// oauth_sessions cleanup queries
121
+
db`CREATE INDEX IF NOT EXISTS idx_oauth_sessions_expires_at ON oauth_sessions(expires_at)`.catch(err => {
122
+
if (!err.message?.includes('already exists')) {
123
+
console.error('Failed to create idx_oauth_sessions_expires_at:', err);
124
+
}
125
+
}),
126
+
127
+
// oauth_keys key rotation queries
128
+
db`CREATE INDEX IF NOT EXISTS idx_oauth_keys_created_at ON oauth_keys(created_at)`.catch(err => {
129
+
if (!err.message?.includes('already exists')) {
130
+
console.error('Failed to create idx_oauth_keys_created_at:', err);
131
+
}
132
+
}),
133
+
134
+
// domains queries by (did, rkey)
135
+
db`CREATE INDEX IF NOT EXISTS idx_domains_did_rkey ON domains(did, rkey)`.catch(err => {
136
+
if (!err.message?.includes('already exists')) {
137
+
console.error('Failed to create idx_domains_did_rkey:', err);
138
+
}
139
+
}),
140
+
141
+
// custom_domains queries by did
142
+
db`CREATE INDEX IF NOT EXISTS idx_custom_domains_did ON custom_domains(did)`.catch(err => {
143
+
if (!err.message?.includes('already exists')) {
144
+
console.error('Failed to create idx_custom_domains_did:', err);
145
+
}
146
+
}),
147
+
148
+
// custom_domains queries by (did, rkey)
149
+
db`CREATE INDEX IF NOT EXISTS idx_custom_domains_did_rkey ON custom_domains(did, rkey)`.catch(err => {
150
+
if (!err.message?.includes('already exists')) {
151
+
console.error('Failed to create idx_custom_domains_did_rkey:', err);
152
+
}
153
+
}),
154
+
155
+
// custom_domains DNS verification worker queries
156
+
db`CREATE INDEX IF NOT EXISTS idx_custom_domains_verified ON custom_domains(verified)`.catch(err => {
157
+
if (!err.message?.includes('already exists')) {
158
+
console.error('Failed to create idx_custom_domains_verified:', err);
159
+
}
160
+
}),
161
+
162
+
// sites queries by did
163
+
db`CREATE INDEX IF NOT EXISTS idx_sites_did ON sites(did)`.catch(err => {
164
+
if (!err.message?.includes('already exists')) {
165
+
console.error('Failed to create idx_sites_did:', err);
166
+
}
167
+
})
168
+
]);
169
+
111
170
const RESERVED_HANDLES = new Set([
112
171
"www",
113
172
"api",
···
244
303
245
304
const stateStore = {
246
305
async set(key: string, data: any) {
247
-
console.debug('[stateStore] set', key)
248
306
const expiresAt = Math.floor(Date.now() / 1000) + STATE_TIMEOUT;
249
307
await db`
250
308
INSERT INTO oauth_states (key, data, created_at, expires_at)
···
253
311
`;
254
312
},
255
313
async get(key: string) {
256
-
console.debug('[stateStore] get', key)
257
314
const now = Math.floor(Date.now() / 1000);
258
315
const result = await db`
259
316
SELECT data, expires_at
···
265
322
// Check if expired
266
323
const expiresAt = Number(result[0].expires_at);
267
324
if (expiresAt && now > expiresAt) {
268
-
console.debug('[stateStore] State expired, deleting', key);
269
325
await db`DELETE FROM oauth_states WHERE key = ${key}`;
270
326
return undefined;
271
327
}
···
273
329
return JSON.parse(result[0].data);
274
330
},
275
331
async del(key: string) {
276
-
console.debug('[stateStore] del', key)
277
332
await db`DELETE FROM oauth_states WHERE key = ${key}`;
278
333
}
279
334
};
280
335
281
336
const sessionStore = {
282
337
async set(sub: string, data: any) {
283
-
console.debug('[sessionStore] set', sub)
284
338
const expiresAt = Math.floor(Date.now() / 1000) + SESSION_TIMEOUT;
285
339
await db`
286
340
INSERT INTO oauth_sessions (sub, data, updated_at, expires_at)
···
292
346
`;
293
347
},
294
348
async get(sub: string) {
295
-
console.debug('[sessionStore] get', sub)
296
349
const now = Math.floor(Date.now() / 1000);
297
350
const result = await db`
298
351
SELECT data, expires_at
···
312
365
return JSON.parse(result[0].data);
313
366
},
314
367
async del(sub: string) {
315
-
console.debug('[sessionStore] del', sub)
316
368
await db`DELETE FROM oauth_sessions WHERE sub = ${sub}`;
317
369
}
318
370
};
-1
src/lib/oauth-client.ts
-1
src/lib/oauth-client.ts
+360
src/lib/wisp-utils.test.ts
+360
src/lib/wisp-utils.test.ts
···
5
5
processUploadedFiles,
6
6
createManifest,
7
7
updateFileBlobs,
8
+
computeCID,
9
+
extractBlobMap,
8
10
type UploadedFile,
9
11
type FileUploadResult,
10
12
} from './wisp-utils'
···
637
639
}
638
640
})
639
641
})
642
+
643
+
describe('computeCID', () => {
644
+
test('should compute CID for gzipped+base64 encoded content', () => {
645
+
// This simulates the actual flow: gzip -> base64 -> compute CID
646
+
const originalContent = Buffer.from('Hello, World!')
647
+
const gzipped = compressFile(originalContent)
648
+
const base64Content = Buffer.from(gzipped.toString('base64'), 'binary')
649
+
650
+
const cid = computeCID(base64Content)
651
+
652
+
// CID should be a valid CIDv1 string starting with 'bafkrei'
653
+
expect(cid).toMatch(/^bafkrei[a-z0-9]+$/)
654
+
expect(cid.length).toBeGreaterThan(10)
655
+
})
656
+
657
+
test('should compute deterministic CIDs for identical content', () => {
658
+
const content = Buffer.from('Test content for CID calculation')
659
+
const gzipped = compressFile(content)
660
+
const base64Content = Buffer.from(gzipped.toString('base64'), 'binary')
661
+
662
+
const cid1 = computeCID(base64Content)
663
+
const cid2 = computeCID(base64Content)
664
+
665
+
expect(cid1).toBe(cid2)
666
+
})
667
+
668
+
test('should compute different CIDs for different content', () => {
669
+
const content1 = Buffer.from('Content A')
670
+
const content2 = Buffer.from('Content B')
671
+
672
+
const gzipped1 = compressFile(content1)
673
+
const gzipped2 = compressFile(content2)
674
+
675
+
const base64Content1 = Buffer.from(gzipped1.toString('base64'), 'binary')
676
+
const base64Content2 = Buffer.from(gzipped2.toString('base64'), 'binary')
677
+
678
+
const cid1 = computeCID(base64Content1)
679
+
const cid2 = computeCID(base64Content2)
680
+
681
+
expect(cid1).not.toBe(cid2)
682
+
})
683
+
684
+
test('should handle empty content', () => {
685
+
const emptyContent = Buffer.from('')
686
+
const gzipped = compressFile(emptyContent)
687
+
const base64Content = Buffer.from(gzipped.toString('base64'), 'binary')
688
+
689
+
const cid = computeCID(base64Content)
690
+
691
+
expect(cid).toMatch(/^bafkrei[a-z0-9]+$/)
692
+
})
693
+
694
+
test('should compute same CID as PDS for base64-encoded content', () => {
695
+
// Test that binary encoding produces correct bytes for CID calculation
696
+
const testContent = Buffer.from('<!DOCTYPE html><html><body>Hello</body></html>')
697
+
const gzipped = compressFile(testContent)
698
+
const base64Content = Buffer.from(gzipped.toString('base64'), 'binary')
699
+
700
+
// Compute CID twice to ensure consistency
701
+
const cid1 = computeCID(base64Content)
702
+
const cid2 = computeCID(base64Content)
703
+
704
+
expect(cid1).toBe(cid2)
705
+
expect(cid1).toMatch(/^bafkrei/)
706
+
})
707
+
708
+
test('should use binary encoding for base64 strings', () => {
709
+
// This test verifies we're using the correct encoding method
710
+
// For base64 strings, 'binary' encoding ensures each character becomes exactly one byte
711
+
const content = Buffer.from('Test content')
712
+
const gzipped = compressFile(content)
713
+
const base64String = gzipped.toString('base64')
714
+
715
+
// Using binary encoding (what we use in production)
716
+
const base64Content = Buffer.from(base64String, 'binary')
717
+
718
+
// Verify the length matches the base64 string length
719
+
expect(base64Content.length).toBe(base64String.length)
720
+
721
+
// Verify CID is computed correctly
722
+
const cid = computeCID(base64Content)
723
+
expect(cid).toMatch(/^bafkrei/)
724
+
})
725
+
})
726
+
727
+
describe('extractBlobMap', () => {
728
+
test('should extract blob map from flat directory structure', () => {
729
+
const mockCid = CID.parse(TEST_CID_STRING)
730
+
const mockBlob = new BlobRef(mockCid, 'text/html', 100)
731
+
732
+
const directory: Directory = {
733
+
$type: 'place.wisp.fs#directory',
734
+
type: 'directory',
735
+
entries: [
736
+
{
737
+
name: 'index.html',
738
+
node: {
739
+
$type: 'place.wisp.fs#file',
740
+
type: 'file',
741
+
blob: mockBlob,
742
+
},
743
+
},
744
+
],
745
+
}
746
+
747
+
const blobMap = extractBlobMap(directory)
748
+
749
+
expect(blobMap.size).toBe(1)
750
+
expect(blobMap.has('index.html')).toBe(true)
751
+
752
+
const entry = blobMap.get('index.html')
753
+
expect(entry?.cid).toBe(TEST_CID_STRING)
754
+
expect(entry?.blobRef).toBe(mockBlob)
755
+
})
756
+
757
+
test('should extract blob map from nested directory structure', () => {
758
+
const mockCid1 = CID.parse(TEST_CID_STRING)
759
+
const mockCid2 = CID.parse('bafkreiabaduc3573q6snt2xgxzpglwuaojkzflocncrh2vj5j3jykdpqhi')
760
+
761
+
const mockBlob1 = new BlobRef(mockCid1, 'text/html', 100)
762
+
const mockBlob2 = new BlobRef(mockCid2, 'text/css', 50)
763
+
764
+
const directory: Directory = {
765
+
$type: 'place.wisp.fs#directory',
766
+
type: 'directory',
767
+
entries: [
768
+
{
769
+
name: 'index.html',
770
+
node: {
771
+
$type: 'place.wisp.fs#file',
772
+
type: 'file',
773
+
blob: mockBlob1,
774
+
},
775
+
},
776
+
{
777
+
name: 'assets',
778
+
node: {
779
+
$type: 'place.wisp.fs#directory',
780
+
type: 'directory',
781
+
entries: [
782
+
{
783
+
name: 'styles.css',
784
+
node: {
785
+
$type: 'place.wisp.fs#file',
786
+
type: 'file',
787
+
blob: mockBlob2,
788
+
},
789
+
},
790
+
],
791
+
},
792
+
},
793
+
],
794
+
}
795
+
796
+
const blobMap = extractBlobMap(directory)
797
+
798
+
expect(blobMap.size).toBe(2)
799
+
expect(blobMap.has('index.html')).toBe(true)
800
+
expect(blobMap.has('assets/styles.css')).toBe(true)
801
+
802
+
expect(blobMap.get('index.html')?.cid).toBe(TEST_CID_STRING)
803
+
expect(blobMap.get('assets/styles.css')?.cid).toBe('bafkreiabaduc3573q6snt2xgxzpglwuaojkzflocncrh2vj5j3jykdpqhi')
804
+
})
805
+
806
+
test('should handle deeply nested directory structures', () => {
807
+
const mockCid = CID.parse(TEST_CID_STRING)
808
+
const mockBlob = new BlobRef(mockCid, 'text/javascript', 200)
809
+
810
+
const directory: Directory = {
811
+
$type: 'place.wisp.fs#directory',
812
+
type: 'directory',
813
+
entries: [
814
+
{
815
+
name: 'src',
816
+
node: {
817
+
$type: 'place.wisp.fs#directory',
818
+
type: 'directory',
819
+
entries: [
820
+
{
821
+
name: 'lib',
822
+
node: {
823
+
$type: 'place.wisp.fs#directory',
824
+
type: 'directory',
825
+
entries: [
826
+
{
827
+
name: 'utils.js',
828
+
node: {
829
+
$type: 'place.wisp.fs#file',
830
+
type: 'file',
831
+
blob: mockBlob,
832
+
},
833
+
},
834
+
],
835
+
},
836
+
},
837
+
],
838
+
},
839
+
},
840
+
],
841
+
}
842
+
843
+
const blobMap = extractBlobMap(directory)
844
+
845
+
expect(blobMap.size).toBe(1)
846
+
expect(blobMap.has('src/lib/utils.js')).toBe(true)
847
+
expect(blobMap.get('src/lib/utils.js')?.cid).toBe(TEST_CID_STRING)
848
+
})
849
+
850
+
test('should handle empty directory', () => {
851
+
const directory: Directory = {
852
+
$type: 'place.wisp.fs#directory',
853
+
type: 'directory',
854
+
entries: [],
855
+
}
856
+
857
+
const blobMap = extractBlobMap(directory)
858
+
859
+
expect(blobMap.size).toBe(0)
860
+
})
861
+
862
+
test('should correctly extract CID from BlobRef instances (not plain objects)', () => {
863
+
// This test verifies the fix: AT Protocol SDK returns BlobRef instances,
864
+
// not plain objects with $type and $link properties
865
+
const mockCid = CID.parse(TEST_CID_STRING)
866
+
const mockBlob = new BlobRef(mockCid, 'application/octet-stream', 500)
867
+
868
+
const directory: Directory = {
869
+
$type: 'place.wisp.fs#directory',
870
+
type: 'directory',
871
+
entries: [
872
+
{
873
+
name: 'test.bin',
874
+
node: {
875
+
$type: 'place.wisp.fs#file',
876
+
type: 'file',
877
+
blob: mockBlob,
878
+
},
879
+
},
880
+
],
881
+
}
882
+
883
+
const blobMap = extractBlobMap(directory)
884
+
885
+
// The fix: we call .toString() on the CID instance instead of accessing $link
886
+
expect(blobMap.get('test.bin')?.cid).toBe(TEST_CID_STRING)
887
+
expect(blobMap.get('test.bin')?.blobRef.ref.toString()).toBe(TEST_CID_STRING)
888
+
})
889
+
890
+
test('should handle multiple files in same directory', () => {
891
+
const mockCid1 = CID.parse(TEST_CID_STRING)
892
+
const mockCid2 = CID.parse('bafkreiabaduc3573q6snt2xgxzpglwuaojkzflocncrh2vj5j3jykdpqhi')
893
+
const mockCid3 = CID.parse('bafkreieb3ixgchss44kw7xiavnkns47emdfsqbhcdfluo3p6n3o53fl3vq')
894
+
895
+
const mockBlob1 = new BlobRef(mockCid1, 'image/png', 1000)
896
+
const mockBlob2 = new BlobRef(mockCid2, 'image/png', 2000)
897
+
const mockBlob3 = new BlobRef(mockCid3, 'image/png', 3000)
898
+
899
+
const directory: Directory = {
900
+
$type: 'place.wisp.fs#directory',
901
+
type: 'directory',
902
+
entries: [
903
+
{
904
+
name: 'images',
905
+
node: {
906
+
$type: 'place.wisp.fs#directory',
907
+
type: 'directory',
908
+
entries: [
909
+
{
910
+
name: 'logo.png',
911
+
node: {
912
+
$type: 'place.wisp.fs#file',
913
+
type: 'file',
914
+
blob: mockBlob1,
915
+
},
916
+
},
917
+
{
918
+
name: 'banner.png',
919
+
node: {
920
+
$type: 'place.wisp.fs#file',
921
+
type: 'file',
922
+
blob: mockBlob2,
923
+
},
924
+
},
925
+
{
926
+
name: 'icon.png',
927
+
node: {
928
+
$type: 'place.wisp.fs#file',
929
+
type: 'file',
930
+
blob: mockBlob3,
931
+
},
932
+
},
933
+
],
934
+
},
935
+
},
936
+
],
937
+
}
938
+
939
+
const blobMap = extractBlobMap(directory)
940
+
941
+
expect(blobMap.size).toBe(3)
942
+
expect(blobMap.has('images/logo.png')).toBe(true)
943
+
expect(blobMap.has('images/banner.png')).toBe(true)
944
+
expect(blobMap.has('images/icon.png')).toBe(true)
945
+
})
946
+
947
+
test('should handle mixed directory and file structure', () => {
948
+
const mockCid1 = CID.parse(TEST_CID_STRING)
949
+
const mockCid2 = CID.parse('bafkreiabaduc3573q6snt2xgxzpglwuaojkzflocncrh2vj5j3jykdpqhi')
950
+
const mockCid3 = CID.parse('bafkreieb3ixgchss44kw7xiavnkns47emdfsqbhcdfluo3p6n3o53fl3vq')
951
+
952
+
const directory: Directory = {
953
+
$type: 'place.wisp.fs#directory',
954
+
type: 'directory',
955
+
entries: [
956
+
{
957
+
name: 'index.html',
958
+
node: {
959
+
$type: 'place.wisp.fs#file',
960
+
type: 'file',
961
+
blob: new BlobRef(mockCid1, 'text/html', 100),
962
+
},
963
+
},
964
+
{
965
+
name: 'assets',
966
+
node: {
967
+
$type: 'place.wisp.fs#directory',
968
+
type: 'directory',
969
+
entries: [
970
+
{
971
+
name: 'styles.css',
972
+
node: {
973
+
$type: 'place.wisp.fs#file',
974
+
type: 'file',
975
+
blob: new BlobRef(mockCid2, 'text/css', 50),
976
+
},
977
+
},
978
+
],
979
+
},
980
+
},
981
+
{
982
+
name: 'README.md',
983
+
node: {
984
+
$type: 'place.wisp.fs#file',
985
+
type: 'file',
986
+
blob: new BlobRef(mockCid3, 'text/markdown', 200),
987
+
},
988
+
},
989
+
],
990
+
}
991
+
992
+
const blobMap = extractBlobMap(directory)
993
+
994
+
expect(blobMap.size).toBe(3)
995
+
expect(blobMap.has('index.html')).toBe(true)
996
+
expect(blobMap.has('assets/styles.css')).toBe(true)
997
+
expect(blobMap.has('README.md')).toBe(true)
998
+
})
999
+
})
+65
-2
src/lib/wisp-utils.ts
+65
-2
src/lib/wisp-utils.ts
···
2
2
import type { Record, Directory, File, Entry } from "../lexicons/types/place/wisp/fs";
3
3
import { validateRecord } from "../lexicons/types/place/wisp/fs";
4
4
import { gzipSync } from 'zlib';
5
+
import { CID } from 'multiformats/cid';
6
+
import { sha256 } from 'multiformats/hashes/sha2';
7
+
import * as raw from 'multiformats/codecs/raw';
8
+
import { createHash } from 'crypto';
9
+
import * as mf from 'multiformats';
5
10
6
11
export interface UploadedFile {
7
12
name: string;
···
48
53
}
49
54
50
55
/**
51
-
* Compress a file using gzip
56
+
* Compress a file using gzip with deterministic output
57
+
* Sets mtime to 0 to ensure identical content produces identical compressed output
52
58
*/
53
59
export function compressFile(content: Buffer): Buffer {
54
-
return gzipSync(content, { level: 9 });
60
+
return gzipSync(content, {
61
+
level: 9,
62
+
mtime: 0 // Fixed timestamp for deterministic compression
63
+
});
55
64
}
56
65
57
66
/**
···
65
74
const directoryMap = new Map<string, UploadedFile[]>();
66
75
67
76
for (const file of files) {
77
+
// Skip undefined/null files (defensive)
78
+
if (!file || !file.name) {
79
+
console.error('Skipping undefined or invalid file in processUploadedFiles');
80
+
continue;
81
+
}
82
+
68
83
// Remove any base folder name from the path
69
84
const normalizedPath = file.name.replace(/^[^\/]*\//, '');
70
85
const parts = normalizedPath.split('/');
···
239
254
240
255
return result;
241
256
}
257
+
258
+
/**
259
+
* Compute CID (Content Identifier) for blob content
260
+
* Uses the same algorithm as AT Protocol: CIDv1 with raw codec and SHA-256
261
+
* Based on @atproto/common/src/ipld.ts sha256RawToCid implementation
262
+
*/
263
+
export function computeCID(content: Buffer): string {
264
+
// Use node crypto to compute sha256 hash (same as AT Protocol)
265
+
const hash = createHash('sha256').update(content).digest();
266
+
// Create digest object from hash bytes
267
+
const digest = mf.digest.create(sha256.code, hash);
268
+
// Create CIDv1 with raw codec
269
+
const cid = CID.createV1(raw.code, digest);
270
+
return cid.toString();
271
+
}
272
+
273
+
/**
274
+
* Extract blob information from a directory tree
275
+
* Returns a map of file paths to their blob refs and CIDs
276
+
*/
277
+
export function extractBlobMap(
278
+
directory: Directory,
279
+
currentPath: string = ''
280
+
): Map<string, { blobRef: BlobRef; cid: string }> {
281
+
const blobMap = new Map<string, { blobRef: BlobRef; cid: string }>();
282
+
283
+
for (const entry of directory.entries) {
284
+
const fullPath = currentPath ? `${currentPath}/${entry.name}` : entry.name;
285
+
286
+
if ('type' in entry.node && entry.node.type === 'file') {
287
+
const fileNode = entry.node as File;
288
+
// AT Protocol SDK returns BlobRef class instances, not plain objects
289
+
// The ref is a CID instance that can be converted to string
290
+
if (fileNode.blob && fileNode.blob.ref) {
291
+
const cidString = fileNode.blob.ref.toString();
292
+
blobMap.set(fullPath, {
293
+
blobRef: fileNode.blob,
294
+
cid: cidString
295
+
});
296
+
}
297
+
} else if ('type' in entry.node && entry.node.type === 'directory') {
298
+
const subMap = extractBlobMap(entry.node as Directory, fullPath);
299
+
subMap.forEach((value, key) => blobMap.set(key, value));
300
+
}
301
+
}
302
+
303
+
return blobMap;
304
+
}
+130
-10
src/routes/wisp.ts
+130
-10
src/routes/wisp.ts
···
9
9
createManifest,
10
10
updateFileBlobs,
11
11
shouldCompressFile,
12
-
compressFile
12
+
compressFile,
13
+
computeCID,
14
+
extractBlobMap
13
15
} from '../lib/wisp-utils'
14
16
import { upsertSite } from '../lib/db'
15
17
import { logger } from '../lib/observability'
···
48
50
siteName: string;
49
51
files: File | File[]
50
52
};
53
+
54
+
console.log('=== UPLOAD FILES START ===');
55
+
console.log('Site name:', siteName);
56
+
console.log('Files received:', Array.isArray(files) ? files.length : 'single file');
51
57
52
58
try {
53
59
if (!siteName) {
···
106
112
107
113
// Create agent with OAuth session
108
114
const agent = new Agent((url, init) => auth.session.fetchHandler(url, init))
115
+
console.log('Agent created for DID:', auth.did);
116
+
117
+
// Try to fetch existing record to enable incremental updates
118
+
let existingBlobMap = new Map<string, { blobRef: any; cid: string }>();
119
+
console.log('Attempting to fetch existing record...');
120
+
try {
121
+
const rkey = siteName;
122
+
const existingRecord = await agent.com.atproto.repo.getRecord({
123
+
repo: auth.did,
124
+
collection: 'place.wisp.fs',
125
+
rkey: rkey
126
+
});
127
+
console.log('Existing record found!');
128
+
129
+
if (existingRecord.data.value && typeof existingRecord.data.value === 'object' && 'root' in existingRecord.data.value) {
130
+
const manifest = existingRecord.data.value as any;
131
+
existingBlobMap = extractBlobMap(manifest.root);
132
+
console.log(`Found existing manifest with ${existingBlobMap.size} files for incremental update`);
133
+
logger.info(`Found existing manifest with ${existingBlobMap.size} files for incremental update`);
134
+
}
135
+
} catch (error: any) {
136
+
console.log('No existing record found or error:', error?.message || error);
137
+
// Record doesn't exist yet, this is a new site
138
+
if (error?.status !== 400 && error?.error !== 'RecordNotFound') {
139
+
logger.warn('Failed to fetch existing record, proceeding with full upload', error);
140
+
}
141
+
}
109
142
110
143
// Convert File objects to UploadedFile format
111
144
// Elysia gives us File objects directly, handle both single file and array
···
113
146
const uploadedFiles: UploadedFile[] = [];
114
147
const skippedFiles: Array<{ name: string; reason: string }> = [];
115
148
116
-
149
+
console.log('Processing files, count:', fileArray.length);
117
150
118
151
for (let i = 0; i < fileArray.length; i++) {
119
152
const file = fileArray[i];
153
+
console.log(`Processing file ${i + 1}/${fileArray.length}:`, file.name, file.size, 'bytes');
120
154
121
155
// Skip files that are too large (limit to 100MB per file)
122
156
const maxSize = MAX_FILE_SIZE; // 100MB
···
135
169
// Compress and base64 encode ALL files
136
170
const compressedContent = compressFile(originalContent);
137
171
// Base64 encode the gzipped content to prevent PDS content sniffing
138
-
const base64Content = Buffer.from(compressedContent.toString('base64'), 'utf-8');
172
+
// Convert base64 string to bytes using binary encoding (each char becomes exactly one byte)
173
+
// This is what PDS receives and computes CID on
174
+
const base64Content = Buffer.from(compressedContent.toString('base64'), 'binary');
139
175
const compressionRatio = (compressedContent.length / originalContent.length * 100).toFixed(1);
176
+
console.log(`Compressing ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%), base64: ${base64Content.length} bytes`);
140
177
logger.info(`Compressing ${file.name}: ${originalContent.length} -> ${compressedContent.length} bytes (${compressionRatio}%), base64: ${base64Content.length} bytes`);
141
178
142
179
uploadedFiles.push({
143
180
name: file.name,
144
-
content: base64Content,
181
+
content: base64Content, // This is the gzipped+base64 content that will be uploaded and CID-computed
145
182
mimeType: originalMimeType,
146
183
size: base64Content.length,
147
184
compressed: true,
···
206
243
}
207
244
208
245
// Process files into directory structure
209
-
const { directory, fileCount } = processUploadedFiles(uploadedFiles);
246
+
console.log('Processing uploaded files into directory structure...');
247
+
console.log('uploadedFiles array length:', uploadedFiles.length);
248
+
console.log('uploadedFiles contents:', uploadedFiles.map((f, i) => `${i}: ${f?.name || 'UNDEFINED'}`));
210
249
211
-
// Upload files as blobs in parallel
250
+
// Filter out any undefined/null/invalid entries (defensive)
251
+
const validUploadedFiles = uploadedFiles.filter((f, i) => {
252
+
if (!f) {
253
+
console.error(`Filtering out undefined/null file at index ${i}`);
254
+
return false;
255
+
}
256
+
if (!f.name) {
257
+
console.error(`Filtering out file with no name at index ${i}:`, f);
258
+
return false;
259
+
}
260
+
if (!f.content) {
261
+
console.error(`Filtering out file with no content at index ${i}:`, f.name);
262
+
return false;
263
+
}
264
+
return true;
265
+
});
266
+
if (validUploadedFiles.length !== uploadedFiles.length) {
267
+
console.warn(`Filtered out ${uploadedFiles.length - validUploadedFiles.length} invalid files`);
268
+
}
269
+
console.log('validUploadedFiles length:', validUploadedFiles.length);
270
+
271
+
const { directory, fileCount } = processUploadedFiles(validUploadedFiles);
272
+
console.log('Directory structure created, file count:', fileCount);
273
+
274
+
// Upload files as blobs in parallel (or reuse existing blobs with matching CIDs)
275
+
console.log('Starting blob upload/reuse phase...');
212
276
// For compressed files, we upload as octet-stream and store the original MIME type in metadata
213
277
// For text/html files, we also use octet-stream as a workaround for PDS image pipeline issues
214
-
const uploadPromises = uploadedFiles.map(async (file, i) => {
278
+
const uploadPromises = validUploadedFiles.map(async (file, i) => {
215
279
try {
280
+
// Skip undefined files (shouldn't happen after filter, but defensive)
281
+
if (!file || !file.name) {
282
+
console.error(`ERROR: Undefined file at index ${i} in validUploadedFiles!`);
283
+
throw new Error(`Undefined file at index ${i}`);
284
+
}
285
+
286
+
// Compute CID for this file to check if it already exists
287
+
// Note: file.content is already gzipped+base64 encoded
288
+
const fileCID = computeCID(file.content);
289
+
290
+
// Normalize the file path for comparison (remove base folder prefix like "cobblemon/")
291
+
const normalizedPath = file.name.replace(/^[^\/]*\//, '');
292
+
293
+
// Check if we have an existing blob with the same CID
294
+
// Try both the normalized path and the full path
295
+
const existingBlob = existingBlobMap.get(normalizedPath) || existingBlobMap.get(file.name);
296
+
297
+
if (existingBlob && existingBlob.cid === fileCID) {
298
+
// Reuse existing blob - no need to upload
299
+
logger.info(`[File Upload] Reusing existing blob for: ${file.name} (CID: ${fileCID})`);
300
+
301
+
return {
302
+
result: {
303
+
hash: existingBlob.cid,
304
+
blobRef: existingBlob.blobRef,
305
+
...(file.compressed && {
306
+
encoding: 'gzip' as const,
307
+
mimeType: file.originalMimeType || file.mimeType,
308
+
base64: true
309
+
})
310
+
},
311
+
filePath: file.name,
312
+
sentMimeType: file.mimeType,
313
+
returnedMimeType: existingBlob.blobRef.mimeType,
314
+
reused: true
315
+
};
316
+
}
317
+
318
+
// File is new or changed - upload it
216
319
// If compressed, always upload as octet-stream
217
320
// Otherwise, workaround: PDS incorrectly processes text/html through image pipeline
218
321
const uploadMimeType = file.compressed || file.mimeType.startsWith('text/html')
···
220
323
: file.mimeType;
221
324
222
325
const compressionInfo = file.compressed ? ' (gzipped)' : '';
223
-
logger.info(`[File Upload] Uploading file: ${file.name} (original: ${file.mimeType}, sending as: ${uploadMimeType}, ${file.size} bytes${compressionInfo})`);
326
+
logger.info(`[File Upload] Uploading new/changed file: ${file.name} (original: ${file.mimeType}, sending as: ${uploadMimeType}, ${file.size} bytes${compressionInfo}, CID: ${fileCID})`);
224
327
225
328
const uploadResult = await agent.com.atproto.repo.uploadBlob(
226
329
file.content,
···
244
347
},
245
348
filePath: file.name,
246
349
sentMimeType: file.mimeType,
247
-
returnedMimeType: returnedBlobRef.mimeType
350
+
returnedMimeType: returnedBlobRef.mimeType,
351
+
reused: false
248
352
};
249
353
} catch (uploadError) {
250
354
logger.error('Upload failed for file', uploadError);
···
255
359
// Wait for all uploads to complete
256
360
const uploadedBlobs = await Promise.all(uploadPromises);
257
361
362
+
// Count reused vs uploaded blobs
363
+
const reusedCount = uploadedBlobs.filter(b => (b as any).reused).length;
364
+
const uploadedCount = uploadedBlobs.filter(b => !(b as any).reused).length;
365
+
console.log(`Blob statistics: ${reusedCount} reused, ${uploadedCount} uploaded, ${uploadedBlobs.length} total`);
366
+
logger.info(`Blob statistics: ${reusedCount} reused, ${uploadedCount} uploaded, ${uploadedBlobs.length} total`);
367
+
258
368
// Extract results and file paths in correct order
259
369
const uploadResults: FileUploadResult[] = uploadedBlobs.map(blob => blob.result);
260
370
const filePaths: string[] = uploadedBlobs.map(blob => blob.filePath);
261
371
262
372
// Update directory with file blobs
373
+
console.log('Updating directory with blob references...');
263
374
const updatedDirectory = updateFileBlobs(directory, uploadResults, filePaths);
264
375
265
376
// Create manifest
377
+
console.log('Creating manifest...');
266
378
const manifest = createManifest(siteName, updatedDirectory, fileCount);
379
+
console.log('Manifest created successfully');
267
380
268
381
// Use site name as rkey
269
382
const rkey = siteName;
270
383
271
384
let record;
272
385
try {
386
+
console.log('Putting record to PDS with rkey:', rkey);
273
387
record = await agent.com.atproto.repo.putRecord({
274
388
repo: auth.did,
275
389
collection: 'place.wisp.fs',
276
390
rkey: rkey,
277
391
record: manifest
278
392
});
393
+
console.log('Record successfully created on PDS:', record.data.uri);
279
394
} catch (putRecordError: any) {
395
+
console.error('FAILED to create record on PDS:', putRecordError);
280
396
logger.error('Failed to create record on PDS', putRecordError);
281
397
282
398
throw putRecordError;
···
292
408
fileCount,
293
409
siteName,
294
410
skippedFiles,
295
-
uploadedCount: uploadedFiles.length
411
+
uploadedCount: validUploadedFiles.length
296
412
};
297
413
414
+
console.log('=== UPLOAD FILES COMPLETE ===');
298
415
return result;
299
416
} catch (error) {
417
+
console.error('=== UPLOAD ERROR ===');
418
+
console.error('Error details:', error);
419
+
console.error('Stack trace:', error instanceof Error ? error.stack : 'N/A');
300
420
logger.error('Upload error', error, {
301
421
message: error instanceof Error ? error.message : 'Unknown error',
302
422
name: error instanceof Error ? error.name : undefined
History
2 rounds
0 comments
nekomimi.pet
submitted
#1
4 commits
expand
collapse
save CIDs to local metadata file, incrementally download new files, copy old
update package.json to trust postinstalls
check manifest and calculate CIDs then compare if we need to reupload blobs
index db
1/1 failed
expand
collapse
expand 0 comments
pull request successfully merged
nekomimi.pet
submitted
#0
3 commits
expand
collapse
save CIDs to local metadata file, incrementally download new files, copy old
update package.json to trust postinstalls
check manifest and calculate CIDs then compare if we need to reupload blobs