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

save CIDs to local metadata file, incrementally download new files, copy old #1

merged opened by nekomimi.pet targeting main from incremental-updates
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:ttdrpj45ibqunmfhdsb4zdwq/sh.tangled.repo.pull/3m56nk25gfq22
+806 -81
Diff #1
+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
··· 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
··· 24 24 25 25 /* Code doesn't run in DOM */ 26 26 "lib": ["es2022"], 27 - } 27 + }, 28 + "include": ["src/**/*"], 29 + "exclude": ["node_modules", "cache", "dist"] 28 30 }
+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
··· 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
··· 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
··· 58 58 `; 59 59 }, 60 60 async get(sub: string) { 61 - console.debug('[sessionStore] get', sub) 62 61 const now = Math.floor(Date.now() / 1000); 63 62 const result = await db` 64 63 SELECT data, expires_at
+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
··· 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
··· 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
sign up or login to add to the discussion
4 commits
expand
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
expand 0 comments
pull request successfully merged
3 commits
expand
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
1/1 failed
expand
expand 0 comments