at://nekomimi.pet/sh.tangled.repo.pull/3m56nk25gfq22
Back to Collection
Record JSON
{
"$type": "sh.tangled.repo.pull",
"createdAt": "2025-11-09T08:33:27Z",
"patch": "From fccf65cf0cbd4fc94f8a418ebeeacb74945020e4 Mon Sep 17 00:00:00 2001\nFrom: \"@nekomimi.pet\" \u003cana@nekoimimi.pet\u003e\nDate: Sat, 8 Nov 2025 23:44:57 -0500\nSubject: [PATCH] save CIDs to local metadata file, incrementally download new\n files, copy old\n\n---\n hosting-service/src/lib/utils.ts | 143 ++++++++++++++++++++++++++-----\n hosting-service/tsconfig.json | 4 +-\n 2 files changed, 123 insertions(+), 24 deletions(-)\n\ndiff --git a/hosting-service/src/lib/utils.ts b/hosting-service/src/lib/utils.ts\nindex f4fec2f..856b51c 100644\n--- a/hosting-service/src/lib/utils.ts\n+++ b/hosting-service/src/lib/utils.ts\n@@ -13,6 +13,8 @@ interface CacheMetadata {\n cachedAt: number;\n did: string;\n rkey: string;\n+ // Map of file path to blob CID for incremental updates\n+ fileCids?: Record\u003cstring, string\u003e;\n }\n \n /**\n@@ -200,15 +202,23 @@ export async function downloadAndCacheSite(did: string, rkey: string, record: Wi\n throw new Error('Invalid record structure: root missing entries array');\n }\n \n+ // Get existing cache metadata to check for incremental updates\n+ const existingMetadata = await getCacheMetadata(did, rkey);\n+ const existingFileCids = existingMetadata?.fileCids || {};\n+\n // Use a temporary directory with timestamp to avoid collisions\n const tempSuffix = `.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n const tempDir = `${CACHE_DIR}/${did}/${rkey}${tempSuffix}`;\n const finalDir = `${CACHE_DIR}/${did}/${rkey}`;\n \n try {\n- // Download to temporary directory\n- await cacheFiles(did, rkey, record.root.entries, pdsEndpoint, '', tempSuffix);\n- await saveCacheMetadata(did, rkey, recordCid, tempSuffix);\n+ // Collect file CIDs from the new record\n+ const newFileCids: Record\u003cstring, string\u003e = {};\n+ collectFileCidsFromEntries(record.root.entries, '', newFileCids);\n+\n+ // Download/copy files to temporary directory (with incremental logic)\n+ await cacheFiles(did, rkey, record.root.entries, pdsEndpoint, '', tempSuffix, existingFileCids, finalDir);\n+ await saveCacheMetadata(did, rkey, recordCid, tempSuffix, newFileCids);\n \n // Atomically replace old cache with new cache\n // On POSIX systems (Linux/macOS), rename is atomic\n@@ -245,17 +255,40 @@ export async function downloadAndCacheSite(did: string, rkey: string, record: Wi\n }\n }\n \n+/**\n+ * Recursively collect file CIDs from entries for incremental update tracking\n+ */\n+function collectFileCidsFromEntries(entries: Entry[], pathPrefix: string, fileCids: Record\u003cstring, string\u003e): void {\n+ for (const entry of entries) {\n+ const currentPath = pathPrefix ? `${pathPrefix}/${entry.name}` : entry.name;\n+ const node = entry.node;\n+\n+ if ('type' in node \u0026\u0026 node.type === 'directory' \u0026\u0026 'entries' in node) {\n+ collectFileCidsFromEntries(node.entries, currentPath, fileCids);\n+ } else if ('type' in node \u0026\u0026 node.type === 'file' \u0026\u0026 'blob' in node) {\n+ const fileNode = node as File;\n+ const cid = extractBlobCid(fileNode.blob);\n+ if (cid) {\n+ fileCids[currentPath] = cid;\n+ }\n+ }\n+ }\n+}\n+\n async function cacheFiles(\n did: string,\n site: string,\n entries: Entry[],\n pdsEndpoint: string,\n pathPrefix: string,\n- dirSuffix: string = ''\n+ dirSuffix: string = '',\n+ existingFileCids: Record\u003cstring, string\u003e = {},\n+ existingCacheDir?: string\n ): Promise\u003cvoid\u003e {\n- // Collect all file blob download tasks first\n+ // Collect file tasks, separating unchanged files from new/changed files\n const downloadTasks: Array\u003c() =\u003e Promise\u003cvoid\u003e\u003e = [];\n- \n+ const copyTasks: Array\u003c() =\u003e Promise\u003cvoid\u003e\u003e = [];\n+\n function collectFileTasks(\n entries: Entry[],\n currentPathPrefix: string\n@@ -268,29 +301,92 @@ async function cacheFiles(\n collectFileTasks(node.entries, currentPath);\n } else if ('type' in node \u0026\u0026 node.type === 'file' \u0026\u0026 'blob' in node) {\n const fileNode = node as File;\n- downloadTasks.push(() =\u003e cacheFileBlob(\n- did,\n- site,\n- currentPath,\n- fileNode.blob,\n- pdsEndpoint,\n- fileNode.encoding,\n- fileNode.mimeType,\n- fileNode.base64,\n- dirSuffix\n- ));\n+ const cid = extractBlobCid(fileNode.blob);\n+\n+ // Check if file is unchanged (same CID as existing cache)\n+ if (cid \u0026\u0026 existingFileCids[currentPath] === cid \u0026\u0026 existingCacheDir) {\n+ // File unchanged - copy from existing cache instead of downloading\n+ copyTasks.push(() =\u003e copyExistingFile(\n+ did,\n+ site,\n+ currentPath,\n+ dirSuffix,\n+ existingCacheDir\n+ ));\n+ } else {\n+ // File new or changed - download it\n+ downloadTasks.push(() =\u003e cacheFileBlob(\n+ did,\n+ site,\n+ currentPath,\n+ fileNode.blob,\n+ pdsEndpoint,\n+ fileNode.encoding,\n+ fileNode.mimeType,\n+ fileNode.base64,\n+ dirSuffix\n+ ));\n+ }\n }\n }\n }\n \n collectFileTasks(entries, pathPrefix);\n \n- // Execute downloads concurrently with a limit of 3 at a time\n- const concurrencyLimit = 3;\n- for (let i = 0; i \u003c downloadTasks.length; i += concurrencyLimit) {\n- const batch = downloadTasks.slice(i, i + concurrencyLimit);\n+ console.log(`[Incremental Update] Files to copy: ${copyTasks.length}, Files to download: ${downloadTasks.length}`);\n+\n+ // Copy unchanged files in parallel (fast local operations)\n+ const copyLimit = 10;\n+ for (let i = 0; i \u003c copyTasks.length; i += copyLimit) {\n+ const batch = copyTasks.slice(i, i + copyLimit);\n await Promise.all(batch.map(task =\u003e task()));\n }\n+\n+ // Download new/changed files concurrently with a limit of 3 at a time\n+ const downloadLimit = 3;\n+ for (let i = 0; i \u003c downloadTasks.length; i += downloadLimit) {\n+ const batch = downloadTasks.slice(i, i + downloadLimit);\n+ await Promise.all(batch.map(task =\u003e task()));\n+ }\n+}\n+\n+/**\n+ * Copy an unchanged file from existing cache to new cache location\n+ */\n+async function copyExistingFile(\n+ did: string,\n+ site: string,\n+ filePath: string,\n+ dirSuffix: string,\n+ existingCacheDir: string\n+): Promise\u003cvoid\u003e {\n+ const { copyFile } = await import('fs/promises');\n+\n+ const sourceFile = `${existingCacheDir}/${filePath}`;\n+ const destFile = `${CACHE_DIR}/${did}/${site}${dirSuffix}/${filePath}`;\n+ const destDir = destFile.substring(0, destFile.lastIndexOf('/'));\n+\n+ // Create destination directory if needed\n+ if (destDir \u0026\u0026 !existsSync(destDir)) {\n+ mkdirSync(destDir, { recursive: true });\n+ }\n+\n+ try {\n+ // Copy the file\n+ await copyFile(sourceFile, destFile);\n+\n+ // Copy metadata file if it exists\n+ const sourceMetaFile = `${sourceFile}.meta`;\n+ const destMetaFile = `${destFile}.meta`;\n+ if (existsSync(sourceMetaFile)) {\n+ await copyFile(sourceMetaFile, destMetaFile);\n+ }\n+\n+ console.log(`[Incremental] Copied unchanged file: ${filePath}`);\n+ } catch (err) {\n+ console.error(`[Incremental] Failed to copy file ${filePath}, will attempt download:`, err);\n+ throw err;\n+ }\n }\n \n async function cacheFileBlob(\n@@ -404,12 +500,13 @@ export function isCached(did: string, site: string): boolean {\n return existsSync(`${CACHE_DIR}/${did}/${site}`);\n }\n \n-async function saveCacheMetadata(did: string, rkey: string, recordCid: string, dirSuffix: string = ''): Promise\u003cvoid\u003e {\n+async function saveCacheMetadata(did: string, rkey: string, recordCid: string, dirSuffix: string = '', fileCids?: Record\u003cstring, string\u003e): Promise\u003cvoid\u003e {\n const metadata: CacheMetadata = {\n recordCid,\n cachedAt: Date.now(),\n did,\n- rkey\n+ rkey,\n+ fileCids\n };\n \n const metadataPath = `${CACHE_DIR}/${did}/${rkey}${dirSuffix}/.metadata.json`;\ndiff --git a/hosting-service/tsconfig.json b/hosting-service/tsconfig.json\nindex aa23e3b..6de29f3 100644\n--- a/hosting-service/tsconfig.json\n+++ b/hosting-service/tsconfig.json\n@@ -24,5 +24,7 @@\n \n \t\t/* Code doesn't run in DOM */\n \t\t\"lib\": [\"es2022\"],\n-\t}\n+\t},\n+\t\"include\": [\"src/**/*\"],\n+\t\"exclude\": [\"node_modules\", \"cache\", \"dist\"]\n }\n-- \n2.51.0\n\n\nFrom 69953ea1f4ee3c1008770a35846d789c931db13b Mon Sep 17 00:00:00 2001\nFrom: \"@nekomimi.pet\" \u003cana@nekoimimi.pet\u003e\nDate: Sat, 8 Nov 2025 23:46:03 -0500\nSubject: [PATCH] update package.json to trust postinstalls\n\n---\n bun.lock | 80 ++++++++++++++++++++++++++++++----------------------\n package.json | 2 ++\n 2 files changed, 48 insertions(+), 34 deletions(-)\n\ndiff --git a/bun.lock b/bun.lock\nindex 87c990b..7b3f2c7 100644\n--- a/bun.lock\n+++ b/bun.lock\n@@ -44,6 +44,8 @@\n },\n \"trustedDependencies\": [\n \"core-js\",\n+ \"cbor-extract\",\n+ \"bun\",\n \"protobufjs\",\n ],\n \"packages\": {\n@@ -51,11 +53,11 @@\n \n \"@atproto-labs/fetch\": [\"@atproto-labs/fetch@0.2.3\", \"\", { \"dependencies\": { \"@atproto-labs/pipe\": \"0.1.1\" } }, \"sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==\"],\n \n- \"@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==\"],\n+ \"@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==\"],\n \n \"@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==\"],\n \n- \"@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==\"],\n+ \"@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==\"],\n \n \"@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==\"],\n \n@@ -65,7 +67,7 @@\n \n \"@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==\"],\n \n- \"@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==\"],\n+ \"@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==\"],\n \n \"@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==\"],\n \n@@ -81,15 +83,15 @@\n \n \"@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==\"],\n \n- \"@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==\"],\n+ \"@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==\"],\n \n \"@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==\"],\n \n- \"@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==\"],\n+ \"@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==\"],\n \n- \"@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==\"],\n+ \"@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==\"],\n \n- \"@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==\"],\n+ \"@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==\"],\n \n \"@atproto/syntax\": [\"@atproto/syntax@0.4.1\", \"\", {}, \"sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==\"],\n \n@@ -113,15 +115,15 @@\n \n \"@elysiajs/cors\": [\"@elysiajs/cors@1.4.0\", \"\", { \"peerDependencies\": { \"elysia\": \"\u003e= 1.4.0\" } }, \"sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g==\"],\n \n- \"@elysiajs/eden\": [\"@elysiajs/eden@1.4.3\", \"\", { \"peerDependencies\": { \"elysia\": \"\u003e= 1.4.0-exp.0\" } }, \"sha512-mX0v5cTvTJiDsDWNEEyuoqudOvW5J+tXsvp/ZOJXJF3iCIEJI0Brvm78ymPrvwiOG4nUr3lS8BxUfbNf32DSXA==\"],\n+ \"@elysiajs/eden\": [\"@elysiajs/eden@1.4.4\", \"\", { \"peerDependencies\": { \"elysia\": \"\u003e= 1.4.0-exp.0\" } }, \"sha512-/LVqflmgUcCiXb8rz1iRq9Rx3SWfIV/EkoNqDFGMx+TvOyo8QHAygFXAVQz7RHs+jk6n6mEgpI6KlKBANoErsQ==\"],\n \n \"@elysiajs/openapi\": [\"@elysiajs/openapi@1.4.11\", \"\", { \"peerDependencies\": { \"elysia\": \"\u003e= 1.4.0\" } }, \"sha512-d75bMxYJpN6qSDi/z9L1S7SLk1S/8Px+cTb3W2lrYzU8uQ5E0kXdy1oOMJEfTyVsz3OA19NP9KNxE7ztSbLBLg==\"],\n \n \"@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\": \"\u003e= 1.4.0\" } }, \"sha512-jR7t4M6ZvMnBqzzHsNTL6y3sNq9jbGi2vKxbkizi/OO5tlvlKl/rnBGyFjZUjQ1Hte7rCz+2kfmgOQMhkjk+Og==\"],\n \n- \"@elysiajs/static\": [\"@elysiajs/static@1.4.2\", \"\", { \"peerDependencies\": { \"elysia\": \"\u003e= 1.4.0\" } }, \"sha512-lAEvdxeBhU/jX/hTzfoP+1AtqhsKNCwW4Q+tfNwAShWU6s4ZPQxR1hLoHBveeApofJt4HWEq/tBGvfFz3ODuKg==\"],\n+ \"@elysiajs/static\": [\"@elysiajs/static@1.4.6\", \"\", { \"peerDependencies\": { \"elysia\": \"\u003e= 1.4.0\" } }, \"sha512-cd61aY/DHOVhlnBjzTBX8E1XANIrsCH8MwEGHeLMaZzNrz0gD4Q8Qsde2dFMzu81I7ZDaaZ2Rim9blSLtUrYBg==\"],\n \n- \"@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==\"],\n+ \"@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==\"],\n \n \"@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==\"],\n \n@@ -187,29 +189,29 @@\n \n \"@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\": \"\u003e=1.0.0 \u003c1.10.0\" } }, \"sha512-omdilCZozUjQwY3uZRBwbaRMJ3p09l4t187Lsdf0dGMye9WKD4NGcpgZRvqhI1dwcH6og+YXQEtoO9Wx3ykilg==\"],\n \n- \"@opentelemetry/semantic-conventions\": [\"@opentelemetry/semantic-conventions@1.37.0\", \"\", {}, \"sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==\"],\n+ \"@opentelemetry/semantic-conventions\": [\"@opentelemetry/semantic-conventions@1.38.0\", \"\", {}, \"sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==\"],\n \n- \"@oven/bun-darwin-aarch64\": [\"@oven/bun-darwin-aarch64@1.3.0\", \"\", { \"os\": \"darwin\", \"cpu\": \"arm64\" }, \"sha512-WeXSaL29ylJEZMYHHW28QZ6rgAbxQ1KuNSZD9gvd3fPlo0s6s2PglvPArjjP07nmvIK9m4OffN0k4M98O7WmAg==\"],\n+ \"@oven/bun-darwin-aarch64\": [\"@oven/bun-darwin-aarch64@1.3.2\", \"\", { \"os\": \"darwin\", \"cpu\": \"arm64\" }, \"sha512-licBDIbbLP5L5/S0+bwtJynso94XD3KyqSP48K59Sq7Mude6C7dR5ZujZm4Ut4BwZqUFfNOfYNMWBU5nlL7t1A==\"],\n \n- \"@oven/bun-darwin-x64\": [\"@oven/bun-darwin-x64@1.3.0\", \"\", { \"os\": \"darwin\", \"cpu\": \"x64\" }, \"sha512-CFKjoUWQH0Oz3UHYfKbdKLq0wGryrFsTJEYq839qAwHQSECvVZYAnxVVDYUDa0yQFonhO2qSHY41f6HK+b7xtw==\"],\n+ \"@oven/bun-darwin-x64\": [\"@oven/bun-darwin-x64@1.3.2\", \"\", { \"os\": \"darwin\", \"cpu\": \"x64\" }, \"sha512-hn8lLzsYyyh6ULo2E8v2SqtrWOkdQKJwapeVy1rDw7juTTeHY3KDudGWf4mVYteC9riZU6HD88Fn3nGwyX0eIg==\"],\n \n- \"@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==\"],\n+ \"@oven/bun-darwin-x64-baseline\": [\"@oven/bun-darwin-x64-baseline@1.3.2\", \"\", { \"os\": \"darwin\", \"cpu\": \"x64\" }, \"sha512-UHxdtbyxdtNJUNcXtIrjx3Lmq8ji3KywlXtIHV/0vn9A8W5mulqOcryqUWMFVH9JTIIzmNn6Q/qVmXHTME63Ww==\"],\n \n- \"@oven/bun-linux-aarch64\": [\"@oven/bun-linux-aarch64@1.3.0\", \"\", { \"os\": \"linux\", \"cpu\": \"arm64\" }, \"sha512-WHthS/eLkCNcp9pk4W8aubRl9fIUgt2XhHyLrP0GClB1FVvmodu/zIOtG0NXNpzlzB8+gglOkGo4dPjfVf4Z+g==\"],\n+ \"@oven/bun-linux-aarch64\": [\"@oven/bun-linux-aarch64@1.3.2\", \"\", { \"os\": \"linux\", \"cpu\": \"arm64\" }, \"sha512-5uZzxzvHU/z+3cZwN/A0H8G+enQ+9FkeJVZkE2fwK2XhiJZFUGAuWajCpy7GepvOWlqV7VjPaKi2+Qmr4IX7nQ==\"],\n \n- \"@oven/bun-linux-aarch64-musl\": [\"@oven/bun-linux-aarch64-musl@1.3.0\", \"\", { \"os\": \"linux\", \"cpu\": \"arm64\" }, \"sha512-HT5sr7N8NDYbQRjAnT7ISpx64y+ewZZRQozOJb0+KQObKvg4UUNXGm4Pn1xA4/WPMZDDazjO8E2vtOQw1nJlAQ==\"],\n+ \"@oven/bun-linux-aarch64-musl\": [\"@oven/bun-linux-aarch64-musl@1.3.2\", \"\", { \"os\": \"linux\", \"cpu\": \"arm64\" }, \"sha512-OD9DYkjes7WXieBn4zQZGXWhRVZhIEWMDGCetZ3H4vxIuweZ++iul/CNX5jdpNXaJ17myb1ROMvmRbrqW44j3w==\"],\n \n- \"@oven/bun-linux-x64\": [\"@oven/bun-linux-x64@1.3.0\", \"\", { \"os\": \"linux\", \"cpu\": \"x64\" }, \"sha512-sGEWoJQXO4GDr0x4t/yJQ/Bq1yNkOdX9tHbZZ+DBGJt3z3r7jeb4Digv8xQUk6gdTFC9vnGHuin+KW3/yD1Aww==\"],\n+ \"@oven/bun-linux-x64\": [\"@oven/bun-linux-x64@1.3.2\", \"\", { \"os\": \"linux\", \"cpu\": \"x64\" }, \"sha512-EoEuRP9bxAxVKuvi6tZ0ZENjueP4lvjz0mKsMzdG0kwg/2apGKiirH1l0RIcdmvfDGGuDmNiv/XBpkoXq1x8ug==\"],\n \n- \"@oven/bun-linux-x64-baseline\": [\"@oven/bun-linux-x64-baseline@1.3.0\", \"\", { \"os\": \"linux\", \"cpu\": \"x64\" }, \"sha512-OmlEH3nlxQyv7HOvTH21vyNAZGv9DIPnrTznzvKiOQxkOphhCyKvPTlF13ydw4s/i18iwaUrhHy+YG9HSSxa4Q==\"],\n+ \"@oven/bun-linux-x64-baseline\": [\"@oven/bun-linux-x64-baseline@1.3.2\", \"\", { \"os\": \"linux\", \"cpu\": \"x64\" }, \"sha512-m9Ov9YH8KjRLui87eNtQQFKVnjGsNk3xgbrR9c8d2FS3NfZSxmVjSeBvEsDjzNf1TXLDriHb/NYOlpiMf/QzDg==\"],\n \n- \"@oven/bun-linux-x64-musl\": [\"@oven/bun-linux-x64-musl@1.3.0\", \"\", { \"os\": \"linux\", \"cpu\": \"x64\" }, \"sha512-rtzUEzCynl3Rhgn/iR9DQezSFiZMcAXAbU+xfROqsweMGKwvwIA2ckyyckO08psEP8XcUZTs3LT9CH7PnaMiEA==\"],\n+ \"@oven/bun-linux-x64-musl\": [\"@oven/bun-linux-x64-musl@1.3.2\", \"\", { \"os\": \"linux\", \"cpu\": \"x64\" }, \"sha512-3TuOsRVoG8K+soQWRo+Cp5ACpRs6rTFSu5tAqc/6WrqwbNWmqjov/eWJPTgz3gPXnC7uNKVG7RxxAmV8r2EYTQ==\"],\n \n- \"@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==\"],\n+ \"@oven/bun-linux-x64-musl-baseline\": [\"@oven/bun-linux-x64-musl-baseline@1.3.2\", \"\", { \"os\": \"linux\", \"cpu\": \"x64\" }, \"sha512-q8Hto8hcpofPJjvuvjuwyYvhOaAzPw1F5vRUUeOJDmDwZ4lZhANFM0rUwchMzfWUJCD6jg8/EVQ8MiixnZWU0A==\"],\n \n- \"@oven/bun-windows-x64\": [\"@oven/bun-windows-x64@1.3.0\", \"\", { \"os\": \"win32\", \"cpu\": \"x64\" }, \"sha512-xXwtpZVVP7T+vkxcF/TUVVOGRjEfkByO4mKveKYb4xnHWV4u4NnV0oNmzyMKkvmj10to5j2h0oZxA4ZVVv4gfA==\"],\n+ \"@oven/bun-windows-x64\": [\"@oven/bun-windows-x64@1.3.2\", \"\", { \"os\": \"win32\", \"cpu\": \"x64\" }, \"sha512-nZJUa5NprPYQ4Ii4cMwtP9PzlJJTp1XhxJ+A9eSn1Jfr6YygVWyN2KLjenyI93IcuBouBAaepDAVZZjH2lFBhg==\"],\n \n- \"@oven/bun-windows-x64-baseline\": [\"@oven/bun-windows-x64-baseline@1.3.0\", \"\", { \"os\": \"win32\", \"cpu\": \"x64\" }, \"sha512-/jVZ8eYjpYHLDFNoT86cP+AjuWvpkzFY+0R0a1bdeu0sQ6ILuy1FV6hz1hUAP390E09VCo5oP76fnx29giHTtA==\"],\n+ \"@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==\"],\n \n \"@protobufjs/aspromise\": [\"@protobufjs/aspromise@1.1.2\", \"\", {}, \"sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==\"],\n \n@@ -253,7 +255,7 @@\n \n \"@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==\"],\n \n- \"@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==\"],\n+ \"@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==\"],\n \n \"@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==\"],\n \n@@ -265,7 +267,7 @@\n \n \"@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==\"],\n \n- \"@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==\"],\n+ \"@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==\"],\n \n \"@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==\"],\n \n@@ -299,9 +301,9 @@\n \n \"@sinclair/typebox\": [\"@sinclair/typebox@0.34.41\", \"\", {}, \"sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==\"],\n \n- \"@tanstack/query-core\": [\"@tanstack/query-core@5.90.2\", \"\", {}, \"sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==\"],\n+ \"@tanstack/query-core\": [\"@tanstack/query-core@5.90.7\", \"\", {}, \"sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ==\"],\n \n- \"@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==\"],\n+ \"@tanstack/react-query\": [\"@tanstack/react-query@5.90.7\", \"\", { \"dependencies\": { \"@tanstack/query-core\": \"5.90.7\" }, \"peerDependencies\": { \"react\": \"^18 || ^19\" } }, \"sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ==\"],\n \n \"@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==\"],\n \n@@ -321,11 +323,11 @@\n \n \"@types/ms\": [\"@types/ms@2.1.0\", \"\", {}, \"sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==\"],\n \n- \"@types/node\": [\"@types/node@24.7.2\", \"\", { \"dependencies\": { \"undici-types\": \"~7.14.0\" } }, \"sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==\"],\n+ \"@types/node\": [\"@types/node@24.10.0\", \"\", { \"dependencies\": { \"undici-types\": \"~7.16.0\" } }, \"sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==\"],\n \n \"@types/react\": [\"@types/react@19.2.2\", \"\", { \"dependencies\": { \"csstype\": \"^3.0.2\" } }, \"sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==\"],\n \n- \"@types/react-dom\": [\"@types/react-dom@19.2.1\", \"\", { \"peerDependencies\": { \"@types/react\": \"^19.2.0\" } }, \"sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==\"],\n+ \"@types/react-dom\": [\"@types/react-dom@19.2.2\", \"\", { \"peerDependencies\": { \"@types/react\": \"^19.2.0\" } }, \"sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==\"],\n \n \"@types/shimmer\": [\"@types/shimmer@1.2.0\", \"\", {}, \"sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==\"],\n \n@@ -363,11 +365,11 @@\n \n \"buffer\": [\"buffer@6.0.3\", \"\", { \"dependencies\": { \"base64-js\": \"^1.3.1\", \"ieee754\": \"^1.2.1\" } }, \"sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==\"],\n \n- \"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==\"],\n+ \"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==\"],\n \n \"bun-plugin-tailwind\": [\"bun-plugin-tailwind@0.1.2\", \"\", { \"peerDependencies\": { \"bun\": \"\u003e=1.0.0\" } }, \"sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg==\"],\n \n- \"bun-types\": [\"bun-types@1.3.0\", \"\", { \"dependencies\": { \"@types/node\": \"*\" }, \"peerDependencies\": { \"@types/react\": \"^19\" } }, \"sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ==\"],\n+ \"bun-types\": [\"bun-types@1.3.2\", \"\", { \"dependencies\": { \"@types/node\": \"*\" }, \"peerDependencies\": { \"@types/react\": \"^19\" } }, \"sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg==\"],\n \n \"bytes\": [\"bytes@3.1.2\", \"\", {}, \"sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==\"],\n \n@@ -443,7 +445,7 @@\n \n \"ee-first\": [\"ee-first@1.1.1\", \"\", {}, \"sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==\"],\n \n- \"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\": \"\u003e= 0.34.0 \u003c 1\", \"file-type\": \"\u003e= 20.0.0\", \"openapi-types\": \"\u003e= 12.0.0\", \"typescript\": \"\u003e= 5.0.0\" }, \"optionalPeers\": [\"typescript\"] }, \"sha512-cphuzQj0fRw1ICRvwHy2H3xQio9bycaZUVHnDHJQnKqBfMNlZ+Hzj6TMmt9lc0Az0mvbCnPXWVF7y1MCRhUuOA==\"],\n+ \"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\": \"\u003e= 0.34.0 \u003c 1\", \"@types/bun\": \"\u003e= 1.2.0\", \"file-type\": \"\u003e= 20.0.0\", \"openapi-types\": \"\u003e= 12.0.0\", \"typescript\": \"\u003e= 5.0.0\" }, \"optionalPeers\": [\"@types/bun\", \"typescript\"] }, \"sha512-RaDqqZdLuC4UJetfVRQ4Z5aVpGgEtQ+pZnsbI4ZzEaf3l/MzuHcqSVoL/Fue3d6qE4RV9HMB2rAZaHyPIxkyzg==\"],\n \n \"emoji-regex\": [\"emoji-regex@8.0.0\", \"\", {}, \"sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==\"],\n \n@@ -579,6 +581,8 @@\n \n \"media-typer\": [\"media-typer@0.3.0\", \"\", {}, \"sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==\"],\n \n+ \"memoirist\": [\"memoirist@0.4.0\", \"\", {}, \"sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg==\"],\n+\n \"merge-descriptors\": [\"merge-descriptors@1.0.3\", \"\", {}, \"sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==\"],\n \n \"methods\": [\"methods@1.1.2\", \"\", {}, \"sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==\"],\n@@ -777,13 +781,13 @@\n \n \"tailwind-merge\": [\"tailwind-merge@3.3.1\", \"\", {}, \"sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==\"],\n \n- \"tailwindcss\": [\"tailwindcss@4.1.14\", \"\", {}, \"sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==\"],\n+ \"tailwindcss\": [\"tailwindcss@4.1.17\", \"\", {}, \"sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==\"],\n \n \"thread-stream\": [\"thread-stream@2.7.0\", \"\", { \"dependencies\": { \"real-require\": \"^0.2.0\" } }, \"sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==\"],\n \n \"tinyglobby\": [\"tinyglobby@0.2.15\", \"\", { \"dependencies\": { \"fdir\": \"^6.5.0\", \"picomatch\": \"^4.0.3\" } }, \"sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==\"],\n \n- \"tlds\": [\"tlds@1.260.0\", \"\", { \"bin\": { \"tlds\": \"bin.js\" } }, \"sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==\"],\n+ \"tlds\": [\"tlds@1.261.0\", \"\", { \"bin\": { \"tlds\": \"bin.js\" } }, \"sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==\"],\n \n \"toidentifier\": [\"toidentifier@1.0.1\", \"\", {}, \"sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==\"],\n \n@@ -809,7 +813,7 @@\n \n \"undici\": [\"undici@6.22.0\", \"\", {}, \"sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==\"],\n \n- \"undici-types\": [\"undici-types@7.14.0\", \"\", {}, \"sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==\"],\n+ \"undici-types\": [\"undici-types@7.16.0\", \"\", {}, \"sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==\"],\n \n \"unist-util-is\": [\"unist-util-is@6.0.1\", \"\", { \"dependencies\": { \"@types/unist\": \"^3.0.0\" } }, \"sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==\"],\n \n@@ -853,6 +857,14 @@\n \n \"zwitch\": [\"zwitch@2.0.4\", \"\", {}, \"sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==\"],\n \n+ \"@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==\"],\n+\n+ \"@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==\"],\n+\n+ \"@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==\"],\n+\n+ \"@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==\"],\n+\n \"@tokenizer/inflate/debug\": [\"debug@4.4.3\", \"\", { \"dependencies\": { \"ms\": \"^2.1.3\" } }, \"sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==\"],\n \n \"express/cookie\": [\"cookie@0.7.1\", \"\", {}, \"sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==\"],\ndiff --git a/package.json b/package.json\nindex f8c07bb..9cb2334 100644\n--- a/package.json\n+++ b/package.json\n@@ -46,6 +46,8 @@\n },\n \"module\": \"src/index.js\",\n \"trustedDependencies\": [\n+ \"bun\",\n+ \"cbor-extract\",\n \"core-js\",\n \"protobufjs\"\n ]\n-- \n2.51.0\n\n\nFrom 618711a16ba13f919a8d5d438eb9b51b29a0b1eb Mon Sep 17 00:00:00 2001\nFrom: \"@nekomimi.pet\" \u003cana@nekoimimi.pet\u003e\nDate: Sun, 9 Nov 2025 03:00:28 -0500\nSubject: [PATCH] check manifest and calculate CIDs then compare if we need to\n reupload blobs\n\n---\n bun.lock | 19 +-\n package.json | 1 +\n public/editor/editor.tsx | 4 +-\n src/lib/db.ts | 7 -\n src/lib/oauth-client.ts | 1 -\n src/lib/wisp-utils.test.ts | 360 +++++++++++++++++++++++++++++++++++++\n src/lib/wisp-utils.ts | 67 ++++++-\n src/routes/wisp.ts | 140 +++++++++++++--\n 8 files changed, 576 insertions(+), 23 deletions(-)\n\ndiff --git a/bun.lock b/bun.lock\nindex 7b3f2c7..292986e 100644\n--- a/bun.lock\n+++ b/bun.lock\n@@ -25,6 +25,7 @@\n \"elysia\": \"latest\",\n \"iron-session\": \"^8.0.4\",\n \"lucide-react\": \"^0.546.0\",\n+ \"multiformats\": \"^13.4.1\",\n \"react\": \"^19.2.0\",\n \"react-dom\": \"^19.2.0\",\n \"react-shiki\": \"^0.9.0\",\n@@ -641,7 +642,7 @@\n \n \"ms\": [\"ms@2.0.0\", \"\", {}, \"sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==\"],\n \n- \"multiformats\": [\"multiformats@9.9.0\", \"\", {}, \"sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==\"],\n+ \"multiformats\": [\"multiformats@13.4.1\", \"\", {}, \"sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==\"],\n \n \"negotiator\": [\"negotiator@0.6.3\", \"\", {}, \"sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==\"],\n \n@@ -857,6 +858,20 @@\n \n \"zwitch\": [\"zwitch@2.0.4\", \"\", {}, \"sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==\"],\n \n+ \"@atproto/api/multiformats\": [\"multiformats@9.9.0\", \"\", {}, \"sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==\"],\n+\n+ \"@atproto/common/multiformats\": [\"multiformats@9.9.0\", \"\", {}, \"sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==\"],\n+\n+ \"@atproto/common-web/multiformats\": [\"multiformats@9.9.0\", \"\", {}, \"sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==\"],\n+\n+ \"@atproto/jwk/multiformats\": [\"multiformats@9.9.0\", \"\", {}, \"sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==\"],\n+\n+ \"@atproto/lexicon/multiformats\": [\"multiformats@9.9.0\", \"\", {}, \"sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==\"],\n+\n+ \"@atproto/oauth-client/multiformats\": [\"multiformats@9.9.0\", \"\", {}, \"sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==\"],\n+\n+ \"@ipld/dag-cbor/multiformats\": [\"multiformats@9.9.0\", \"\", {}, \"sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==\"],\n+\n \"@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==\"],\n \n \"@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==\"],\n@@ -883,6 +898,8 @@\n \n \"send/ms\": [\"ms@2.1.3\", \"\", {}, \"sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==\"],\n \n+ \"uint8arrays/multiformats\": [\"multiformats@9.9.0\", \"\", {}, \"sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==\"],\n+\n \"@tokenizer/inflate/debug/ms\": [\"ms@2.1.3\", \"\", {}, \"sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==\"],\n \n \"micromark/debug/ms\": [\"ms@2.1.3\", \"\", {}, \"sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==\"],\ndiff --git a/package.json b/package.json\nindex 9cb2334..0b202fc 100644\n--- a/package.json\n+++ b/package.json\n@@ -29,6 +29,7 @@\n \"elysia\": \"latest\",\n \"iron-session\": \"^8.0.4\",\n \"lucide-react\": \"^0.546.0\",\n+ \"multiformats\": \"^13.4.1\",\n \"react\": \"^19.2.0\",\n \"react-dom\": \"^19.2.0\",\n \"react-shiki\": \"^0.9.0\",\ndiff --git a/public/editor/editor.tsx b/public/editor/editor.tsx\nindex da7530f..359f1af 100644\n--- a/public/editor/editor.tsx\n+++ b/public/editor/editor.tsx\n@@ -748,7 +748,7 @@ function Dashboard() {\n \n \t\t\t\t\t\t\u003cdiv className=\"p-4 bg-muted/30 rounded-lg border-l-4 border-yellow-500/50\"\u003e\n \t\t\t\t\t\t\t\u003cdiv className=\"flex items-start gap-2\"\u003e\n-\t\t\t\t\t\t\t\t\u003cAlertCircle className=\"w-4 h-4 text-yellow-600 dark:text-yellow-400 mt-0.5 flex-shrink-0\" /\u003e\n+\t\t\t\t\t\t\t\t\u003cAlertCircle className=\"w-4 h-4 text-yellow-600 dark:text-yellow-400 mt-0.5 shrink-0\" /\u003e\n \t\t\t\t\t\t\t\t\u003cdiv className=\"flex-1 space-y-1\"\u003e\n \t\t\t\t\t\t\t\t\t\u003cp className=\"text-xs font-semibold text-yellow-600 dark:text-yellow-400\"\u003e\n \t\t\t\t\t\t\t\t\t\tNote about sites.wisp.place URLs\n@@ -1120,7 +1120,7 @@ function Dashboard() {\n \t\t\t\t\t\t\t\t\t\t{skippedFiles.length \u003e 0 \u0026\u0026 (\n \t\t\t\t\t\t\t\t\t\t\t\u003cdiv className=\"p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg\"\u003e\n \t\t\t\t\t\t\t\t\t\t\t\t\u003cdiv className=\"flex items-start gap-2 text-yellow-600 dark:text-yellow-400 mb-2\"\u003e\n-\t\t\t\t\t\t\t\t\t\t\t\t\t\u003cAlertCircle className=\"w-4 h-4 mt-0.5 flex-shrink-0\" /\u003e\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\u003cAlertCircle className=\"w-4 h-4 mt-0.5 shrink-0\" /\u003e\n \t\t\t\t\t\t\t\t\t\t\t\t\t\u003cdiv className=\"flex-1\"\u003e\n \t\t\t\t\t\t\t\t\t\t\t\t\t\t\u003cspan className=\"font-medium\"\u003e\n \t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{skippedFiles.length} file{skippedFiles.length \u003e 1 ? 's' : ''} skipped\ndiff --git a/src/lib/db.ts b/src/lib/db.ts\nindex 887f212..b5f82c4 100644\n--- a/src/lib/db.ts\n+++ b/src/lib/db.ts\n@@ -244,7 +244,6 @@ const STATE_TIMEOUT = 60 * 60; // 3600 seconds\n \n const stateStore = {\n async set(key: string, data: any) {\n- console.debug('[stateStore] set', key)\n const expiresAt = Math.floor(Date.now() / 1000) + STATE_TIMEOUT;\n await db`\n INSERT INTO oauth_states (key, data, created_at, expires_at)\n@@ -253,7 +252,6 @@ const stateStore = {\n `;\n },\n async get(key: string) {\n- console.debug('[stateStore] get', key)\n const now = Math.floor(Date.now() / 1000);\n const result = await db`\n SELECT data, expires_at\n@@ -265,7 +263,6 @@ const stateStore = {\n // Check if expired\n const expiresAt = Number(result[0].expires_at);\n if (expiresAt \u0026\u0026 now \u003e expiresAt) {\n- console.debug('[stateStore] State expired, deleting', key);\n await db`DELETE FROM oauth_states WHERE key = ${key}`;\n return undefined;\n }\n@@ -273,14 +270,12 @@ const stateStore = {\n return JSON.parse(result[0].data);\n },\n async del(key: string) {\n- console.debug('[stateStore] del', key)\n await db`DELETE FROM oauth_states WHERE key = ${key}`;\n }\n };\n \n const sessionStore = {\n async set(sub: string, data: any) {\n- console.debug('[sessionStore] set', sub)\n const expiresAt = Math.floor(Date.now() / 1000) + SESSION_TIMEOUT;\n await db`\n INSERT INTO oauth_sessions (sub, data, updated_at, expires_at)\n@@ -292,7 +287,6 @@ const sessionStore = {\n `;\n },\n async get(sub: string) {\n- console.debug('[sessionStore] get', sub)\n const now = Math.floor(Date.now() / 1000);\n const result = await db`\n SELECT data, expires_at\n@@ -312,7 +306,6 @@ const sessionStore = {\n return JSON.parse(result[0].data);\n },\n async del(sub: string) {\n- console.debug('[sessionStore] del', sub)\n await db`DELETE FROM oauth_sessions WHERE sub = ${sub}`;\n }\n };\ndiff --git a/src/lib/oauth-client.ts b/src/lib/oauth-client.ts\nindex a087c80..c73d236 100644\n--- a/src/lib/oauth-client.ts\n+++ b/src/lib/oauth-client.ts\n@@ -58,7 +58,6 @@ const sessionStore = {\n `;\n },\n async get(sub: string) {\n- console.debug('[sessionStore] get', sub)\n const now = Math.floor(Date.now() / 1000);\n const result = await db`\n SELECT data, expires_at\ndiff --git a/src/lib/wisp-utils.test.ts b/src/lib/wisp-utils.test.ts\nindex ff180f0..18c423f 100644\n--- a/src/lib/wisp-utils.test.ts\n+++ b/src/lib/wisp-utils.test.ts\n@@ -5,6 +5,8 @@ import {\n \tprocessUploadedFiles,\n \tcreateManifest,\n \tupdateFileBlobs,\n+\tcomputeCID,\n+\textractBlobMap,\n \ttype UploadedFile,\n \ttype FileUploadResult,\n } from './wisp-utils'\n@@ -637,3 +639,361 @@ describe('updateFileBlobs', () =\u003e {\n \t\t}\n \t})\n })\n+\n+describe('computeCID', () =\u003e {\n+\ttest('should compute CID for gzipped+base64 encoded content', () =\u003e {\n+\t\t// This simulates the actual flow: gzip -\u003e base64 -\u003e compute CID\n+\t\tconst originalContent = Buffer.from('Hello, World!')\n+\t\tconst gzipped = compressFile(originalContent)\n+\t\tconst base64Content = Buffer.from(gzipped.toString('base64'), 'binary')\n+\n+\t\tconst cid = computeCID(base64Content)\n+\n+\t\t// CID should be a valid CIDv1 string starting with 'bafkrei'\n+\t\texpect(cid).toMatch(/^bafkrei[a-z0-9]+$/)\n+\t\texpect(cid.length).toBeGreaterThan(10)\n+\t})\n+\n+\ttest('should compute deterministic CIDs for identical content', () =\u003e {\n+\t\tconst content = Buffer.from('Test content for CID calculation')\n+\t\tconst gzipped = compressFile(content)\n+\t\tconst base64Content = Buffer.from(gzipped.toString('base64'), 'binary')\n+\n+\t\tconst cid1 = computeCID(base64Content)\n+\t\tconst cid2 = computeCID(base64Content)\n+\n+\t\texpect(cid1).toBe(cid2)\n+\t})\n+\n+\ttest('should compute different CIDs for different content', () =\u003e {\n+\t\tconst content1 = Buffer.from('Content A')\n+\t\tconst content2 = Buffer.from('Content B')\n+\n+\t\tconst gzipped1 = compressFile(content1)\n+\t\tconst gzipped2 = compressFile(content2)\n+\n+\t\tconst base64Content1 = Buffer.from(gzipped1.toString('base64'), 'binary')\n+\t\tconst base64Content2 = Buffer.from(gzipped2.toString('base64'), 'binary')\n+\n+\t\tconst cid1 = computeCID(base64Content1)\n+\t\tconst cid2 = computeCID(base64Content2)\n+\n+\t\texpect(cid1).not.toBe(cid2)\n+\t})\n+\n+\ttest('should handle empty content', () =\u003e {\n+\t\tconst emptyContent = Buffer.from('')\n+\t\tconst gzipped = compressFile(emptyContent)\n+\t\tconst base64Content = Buffer.from(gzipped.toString('base64'), 'binary')\n+\n+\t\tconst cid = computeCID(base64Content)\n+\n+\t\texpect(cid).toMatch(/^bafkrei[a-z0-9]+$/)\n+\t})\n+\n+\ttest('should compute same CID as PDS for base64-encoded content', () =\u003e {\n+\t\t// Test that binary encoding produces correct bytes for CID calculation\n+\t\tconst testContent = Buffer.from('\u003c!DOCTYPE html\u003e\u003chtml\u003e\u003cbody\u003eHello\u003c/body\u003e\u003c/html\u003e')\n+\t\tconst gzipped = compressFile(testContent)\n+\t\tconst base64Content = Buffer.from(gzipped.toString('base64'), 'binary')\n+\n+\t\t// Compute CID twice to ensure consistency\n+\t\tconst cid1 = computeCID(base64Content)\n+\t\tconst cid2 = computeCID(base64Content)\n+\n+\t\texpect(cid1).toBe(cid2)\n+\t\texpect(cid1).toMatch(/^bafkrei/)\n+\t})\n+\n+\ttest('should use binary encoding for base64 strings', () =\u003e {\n+\t\t// This test verifies we're using the correct encoding method\n+\t\t// For base64 strings, 'binary' encoding ensures each character becomes exactly one byte\n+\t\tconst content = Buffer.from('Test content')\n+\t\tconst gzipped = compressFile(content)\n+\t\tconst base64String = gzipped.toString('base64')\n+\n+\t\t// Using binary encoding (what we use in production)\n+\t\tconst base64Content = Buffer.from(base64String, 'binary')\n+\n+\t\t// Verify the length matches the base64 string length\n+\t\texpect(base64Content.length).toBe(base64String.length)\n+\n+\t\t// Verify CID is computed correctly\n+\t\tconst cid = computeCID(base64Content)\n+\t\texpect(cid).toMatch(/^bafkrei/)\n+\t})\n+})\n+\n+describe('extractBlobMap', () =\u003e {\n+\ttest('should extract blob map from flat directory structure', () =\u003e {\n+\t\tconst mockCid = CID.parse(TEST_CID_STRING)\n+\t\tconst mockBlob = new BlobRef(mockCid, 'text/html', 100)\n+\n+\t\tconst directory: Directory = {\n+\t\t\t$type: 'place.wisp.fs#directory',\n+\t\t\ttype: 'directory',\n+\t\t\tentries: [\n+\t\t\t\t{\n+\t\t\t\t\tname: 'index.html',\n+\t\t\t\t\tnode: {\n+\t\t\t\t\t\t$type: 'place.wisp.fs#file',\n+\t\t\t\t\t\ttype: 'file',\n+\t\t\t\t\t\tblob: mockBlob,\n+\t\t\t\t\t},\n+\t\t\t\t},\n+\t\t\t],\n+\t\t}\n+\n+\t\tconst blobMap = extractBlobMap(directory)\n+\n+\t\texpect(blobMap.size).toBe(1)\n+\t\texpect(blobMap.has('index.html')).toBe(true)\n+\n+\t\tconst entry = blobMap.get('index.html')\n+\t\texpect(entry?.cid).toBe(TEST_CID_STRING)\n+\t\texpect(entry?.blobRef).toBe(mockBlob)\n+\t})\n+\n+\ttest('should extract blob map from nested directory structure', () =\u003e {\n+\t\tconst mockCid1 = CID.parse(TEST_CID_STRING)\n+\t\tconst mockCid2 = CID.parse('bafkreiabaduc3573q6snt2xgxzpglwuaojkzflocncrh2vj5j3jykdpqhi')\n+\n+\t\tconst mockBlob1 = new BlobRef(mockCid1, 'text/html', 100)\n+\t\tconst mockBlob2 = new BlobRef(mockCid2, 'text/css', 50)\n+\n+\t\tconst directory: Directory = {\n+\t\t\t$type: 'place.wisp.fs#directory',\n+\t\t\ttype: 'directory',\n+\t\t\tentries: [\n+\t\t\t\t{\n+\t\t\t\t\tname: 'index.html',\n+\t\t\t\t\tnode: {\n+\t\t\t\t\t\t$type: 'place.wisp.fs#file',\n+\t\t\t\t\t\ttype: 'file',\n+\t\t\t\t\t\tblob: mockBlob1,\n+\t\t\t\t\t},\n+\t\t\t\t},\n+\t\t\t\t{\n+\t\t\t\t\tname: 'assets',\n+\t\t\t\t\tnode: {\n+\t\t\t\t\t\t$type: 'place.wisp.fs#directory',\n+\t\t\t\t\t\ttype: 'directory',\n+\t\t\t\t\t\tentries: [\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tname: 'styles.css',\n+\t\t\t\t\t\t\t\tnode: {\n+\t\t\t\t\t\t\t\t\t$type: 'place.wisp.fs#file',\n+\t\t\t\t\t\t\t\t\ttype: 'file',\n+\t\t\t\t\t\t\t\t\tblob: mockBlob2,\n+\t\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t],\n+\t\t\t\t\t},\n+\t\t\t\t},\n+\t\t\t],\n+\t\t}\n+\n+\t\tconst blobMap = extractBlobMap(directory)\n+\n+\t\texpect(blobMap.size).toBe(2)\n+\t\texpect(blobMap.has('index.html')).toBe(true)\n+\t\texpect(blobMap.has('assets/styles.css')).toBe(true)\n+\n+\t\texpect(blobMap.get('index.html')?.cid).toBe(TEST_CID_STRING)\n+\t\texpect(blobMap.get('assets/styles.css')?.cid).toBe('bafkreiabaduc3573q6snt2xgxzpglwuaojkzflocncrh2vj5j3jykdpqhi')\n+\t})\n+\n+\ttest('should handle deeply nested directory structures', () =\u003e {\n+\t\tconst mockCid = CID.parse(TEST_CID_STRING)\n+\t\tconst mockBlob = new BlobRef(mockCid, 'text/javascript', 200)\n+\n+\t\tconst directory: Directory = {\n+\t\t\t$type: 'place.wisp.fs#directory',\n+\t\t\ttype: 'directory',\n+\t\t\tentries: [\n+\t\t\t\t{\n+\t\t\t\t\tname: 'src',\n+\t\t\t\t\tnode: {\n+\t\t\t\t\t\t$type: 'place.wisp.fs#directory',\n+\t\t\t\t\t\ttype: 'directory',\n+\t\t\t\t\t\tentries: [\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tname: 'lib',\n+\t\t\t\t\t\t\t\tnode: {\n+\t\t\t\t\t\t\t\t\t$type: 'place.wisp.fs#directory',\n+\t\t\t\t\t\t\t\t\ttype: 'directory',\n+\t\t\t\t\t\t\t\t\tentries: [\n+\t\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\t\tname: 'utils.js',\n+\t\t\t\t\t\t\t\t\t\t\tnode: {\n+\t\t\t\t\t\t\t\t\t\t\t\t$type: 'place.wisp.fs#file',\n+\t\t\t\t\t\t\t\t\t\t\t\ttype: 'file',\n+\t\t\t\t\t\t\t\t\t\t\t\tblob: mockBlob,\n+\t\t\t\t\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t\t\t\t],\n+\t\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t],\n+\t\t\t\t\t},\n+\t\t\t\t},\n+\t\t\t],\n+\t\t}\n+\n+\t\tconst blobMap = extractBlobMap(directory)\n+\n+\t\texpect(blobMap.size).toBe(1)\n+\t\texpect(blobMap.has('src/lib/utils.js')).toBe(true)\n+\t\texpect(blobMap.get('src/lib/utils.js')?.cid).toBe(TEST_CID_STRING)\n+\t})\n+\n+\ttest('should handle empty directory', () =\u003e {\n+\t\tconst directory: Directory = {\n+\t\t\t$type: 'place.wisp.fs#directory',\n+\t\t\ttype: 'directory',\n+\t\t\tentries: [],\n+\t\t}\n+\n+\t\tconst blobMap = extractBlobMap(directory)\n+\n+\t\texpect(blobMap.size).toBe(0)\n+\t})\n+\n+\ttest('should correctly extract CID from BlobRef instances (not plain objects)', () =\u003e {\n+\t\t// This test verifies the fix: AT Protocol SDK returns BlobRef instances,\n+\t\t// not plain objects with $type and $link properties\n+\t\tconst mockCid = CID.parse(TEST_CID_STRING)\n+\t\tconst mockBlob = new BlobRef(mockCid, 'application/octet-stream', 500)\n+\n+\t\tconst directory: Directory = {\n+\t\t\t$type: 'place.wisp.fs#directory',\n+\t\t\ttype: 'directory',\n+\t\t\tentries: [\n+\t\t\t\t{\n+\t\t\t\t\tname: 'test.bin',\n+\t\t\t\t\tnode: {\n+\t\t\t\t\t\t$type: 'place.wisp.fs#file',\n+\t\t\t\t\t\ttype: 'file',\n+\t\t\t\t\t\tblob: mockBlob,\n+\t\t\t\t\t},\n+\t\t\t\t},\n+\t\t\t],\n+\t\t}\n+\n+\t\tconst blobMap = extractBlobMap(directory)\n+\n+\t\t// The fix: we call .toString() on the CID instance instead of accessing $link\n+\t\texpect(blobMap.get('test.bin')?.cid).toBe(TEST_CID_STRING)\n+\t\texpect(blobMap.get('test.bin')?.blobRef.ref.toString()).toBe(TEST_CID_STRING)\n+\t})\n+\n+\ttest('should handle multiple files in same directory', () =\u003e {\n+\t\tconst mockCid1 = CID.parse(TEST_CID_STRING)\n+\t\tconst mockCid2 = CID.parse('bafkreiabaduc3573q6snt2xgxzpglwuaojkzflocncrh2vj5j3jykdpqhi')\n+\t\tconst mockCid3 = CID.parse('bafkreieb3ixgchss44kw7xiavnkns47emdfsqbhcdfluo3p6n3o53fl3vq')\n+\n+\t\tconst mockBlob1 = new BlobRef(mockCid1, 'image/png', 1000)\n+\t\tconst mockBlob2 = new BlobRef(mockCid2, 'image/png', 2000)\n+\t\tconst mockBlob3 = new BlobRef(mockCid3, 'image/png', 3000)\n+\n+\t\tconst directory: Directory = {\n+\t\t\t$type: 'place.wisp.fs#directory',\n+\t\t\ttype: 'directory',\n+\t\t\tentries: [\n+\t\t\t\t{\n+\t\t\t\t\tname: 'images',\n+\t\t\t\t\tnode: {\n+\t\t\t\t\t\t$type: 'place.wisp.fs#directory',\n+\t\t\t\t\t\ttype: 'directory',\n+\t\t\t\t\t\tentries: [\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tname: 'logo.png',\n+\t\t\t\t\t\t\t\tnode: {\n+\t\t\t\t\t\t\t\t\t$type: 'place.wisp.fs#file',\n+\t\t\t\t\t\t\t\t\ttype: 'file',\n+\t\t\t\t\t\t\t\t\tblob: mockBlob1,\n+\t\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tname: 'banner.png',\n+\t\t\t\t\t\t\t\tnode: {\n+\t\t\t\t\t\t\t\t\t$type: 'place.wisp.fs#file',\n+\t\t\t\t\t\t\t\t\ttype: 'file',\n+\t\t\t\t\t\t\t\t\tblob: mockBlob2,\n+\t\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tname: 'icon.png',\n+\t\t\t\t\t\t\t\tnode: {\n+\t\t\t\t\t\t\t\t\t$type: 'place.wisp.fs#file',\n+\t\t\t\t\t\t\t\t\ttype: 'file',\n+\t\t\t\t\t\t\t\t\tblob: mockBlob3,\n+\t\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t],\n+\t\t\t\t\t},\n+\t\t\t\t},\n+\t\t\t],\n+\t\t}\n+\n+\t\tconst blobMap = extractBlobMap(directory)\n+\n+\t\texpect(blobMap.size).toBe(3)\n+\t\texpect(blobMap.has('images/logo.png')).toBe(true)\n+\t\texpect(blobMap.has('images/banner.png')).toBe(true)\n+\t\texpect(blobMap.has('images/icon.png')).toBe(true)\n+\t})\n+\n+\ttest('should handle mixed directory and file structure', () =\u003e {\n+\t\tconst mockCid1 = CID.parse(TEST_CID_STRING)\n+\t\tconst mockCid2 = CID.parse('bafkreiabaduc3573q6snt2xgxzpglwuaojkzflocncrh2vj5j3jykdpqhi')\n+\t\tconst mockCid3 = CID.parse('bafkreieb3ixgchss44kw7xiavnkns47emdfsqbhcdfluo3p6n3o53fl3vq')\n+\n+\t\tconst directory: Directory = {\n+\t\t\t$type: 'place.wisp.fs#directory',\n+\t\t\ttype: 'directory',\n+\t\t\tentries: [\n+\t\t\t\t{\n+\t\t\t\t\tname: 'index.html',\n+\t\t\t\t\tnode: {\n+\t\t\t\t\t\t$type: 'place.wisp.fs#file',\n+\t\t\t\t\t\ttype: 'file',\n+\t\t\t\t\t\tblob: new BlobRef(mockCid1, 'text/html', 100),\n+\t\t\t\t\t},\n+\t\t\t\t},\n+\t\t\t\t{\n+\t\t\t\t\tname: 'assets',\n+\t\t\t\t\tnode: {\n+\t\t\t\t\t\t$type: 'place.wisp.fs#directory',\n+\t\t\t\t\t\ttype: 'directory',\n+\t\t\t\t\t\tentries: [\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tname: 'styles.css',\n+\t\t\t\t\t\t\t\tnode: {\n+\t\t\t\t\t\t\t\t\t$type: 'place.wisp.fs#file',\n+\t\t\t\t\t\t\t\t\ttype: 'file',\n+\t\t\t\t\t\t\t\t\tblob: new BlobRef(mockCid2, 'text/css', 50),\n+\t\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t],\n+\t\t\t\t\t},\n+\t\t\t\t},\n+\t\t\t\t{\n+\t\t\t\t\tname: 'README.md',\n+\t\t\t\t\tnode: {\n+\t\t\t\t\t\t$type: 'place.wisp.fs#file',\n+\t\t\t\t\t\ttype: 'file',\n+\t\t\t\t\t\tblob: new BlobRef(mockCid3, 'text/markdown', 200),\n+\t\t\t\t\t},\n+\t\t\t\t},\n+\t\t\t],\n+\t\t}\n+\n+\t\tconst blobMap = extractBlobMap(directory)\n+\n+\t\texpect(blobMap.size).toBe(3)\n+\t\texpect(blobMap.has('index.html')).toBe(true)\n+\t\texpect(blobMap.has('assets/styles.css')).toBe(true)\n+\t\texpect(blobMap.has('README.md')).toBe(true)\n+\t})\n+})\ndiff --git a/src/lib/wisp-utils.ts b/src/lib/wisp-utils.ts\nindex 5e06d5a..5a785ed 100644\n--- a/src/lib/wisp-utils.ts\n+++ b/src/lib/wisp-utils.ts\n@@ -2,6 +2,11 @@ import type { BlobRef } from \"@atproto/api\";\n import type { Record, Directory, File, Entry } from \"../lexicons/types/place/wisp/fs\";\n import { validateRecord } from \"../lexicons/types/place/wisp/fs\";\n import { gzipSync } from 'zlib';\n+import { CID } from 'multiformats/cid';\n+import { sha256 } from 'multiformats/hashes/sha2';\n+import * as raw from 'multiformats/codecs/raw';\n+import { createHash } from 'crypto';\n+import * as mf from 'multiformats';\n \n export interface UploadedFile {\n \tname: string;\n@@ -48,10 +53,14 @@ export function shouldCompressFile(mimeType: string): boolean {\n }\n \n /**\n- * Compress a file using gzip\n+ * Compress a file using gzip with deterministic output\n+ * Sets mtime to 0 to ensure identical content produces identical compressed output\n */\n export function compressFile(content: Buffer): Buffer {\n-\treturn gzipSync(content, { level: 9 });\n+\treturn gzipSync(content, {\n+\t\tlevel: 9,\n+\t\tmtime: 0 // Fixed timestamp for deterministic compression\n+\t});\n }\n \n /**\n@@ -65,6 +74,12 @@ export function processUploadedFiles(files: UploadedFile[]): ProcessedDirectory\n \tconst directoryMap = new Map\u003cstring, UploadedFile[]\u003e();\n \n \tfor (const file of files) {\n+\t\t// Skip undefined/null files (defensive)\n+\t\tif (!file || !file.name) {\n+\t\t\tconsole.error('Skipping undefined or invalid file in processUploadedFiles');\n+\t\t\tcontinue;\n+\t\t}\n+\n \t\t// Remove any base folder name from the path\n \t\tconst normalizedPath = file.name.replace(/^[^\\/]*\\//, '');\n \t\tconst parts = normalizedPath.split('/');\n@@ -239,3 +254,51 @@ export function updateFileBlobs(\n \n \treturn result;\n }\n+\n+/**\n+ * Compute CID (Content Identifier) for blob content\n+ * Uses the same algorithm as AT Protocol: CIDv1 with raw codec and SHA-256\n+ * Based on @atproto/common/src/ipld.ts sha256RawToCid implementation\n+ */\n+export function computeCID(content: Buffer): string {\n+\t// Use node crypto to compute sha256 hash (same as AT Protocol)\n+\tconst hash = createHash('sha256').update(content).digest();\n+\t// Create digest object from hash bytes\n+\tconst digest = mf.digest.create(sha256.code, hash);\n+\t// Create CIDv1 with raw codec\n+\tconst cid = CID.createV1(raw.code, digest);\n+\treturn cid.toString();\n+}\n+\n+/**\n+ * Extract blob information from a directory tree\n+ * Returns a map of file paths to their blob refs and CIDs\n+ */\n+export function extractBlobMap(\n+\tdirectory: Directory,\n+\tcurrentPath: string = ''\n+): Map\u003cstring, { blobRef: BlobRef; cid: string }\u003e {\n+\tconst blobMap = new Map\u003cstring, { blobRef: BlobRef; cid: string }\u003e();\n+\n+\tfor (const entry of directory.entries) {\n+\t\tconst fullPath = currentPath ? `${currentPath}/${entry.name}` : entry.name;\n+\n+\t\tif ('type' in entry.node \u0026\u0026 entry.node.type === 'file') {\n+\t\t\tconst fileNode = entry.node as File;\n+\t\t\t// AT Protocol SDK returns BlobRef class instances, not plain objects\n+\t\t\t// The ref is a CID instance that can be converted to string\n+\t\t\tif (fileNode.blob \u0026\u0026 fileNode.blob.ref) {\n+\t\t\t\tconst cidString = fileNode.blob.ref.toString();\n+\t\t\t\tblobMap.set(fullPath, {\n+\t\t\t\t\tblobRef: fileNode.blob,\n+\t\t\t\t\tcid: cidString\n+\t\t\t\t});\n+\t\t\t}\n+\t\t} else if ('type' in entry.node \u0026\u0026 entry.node.type === 'directory') {\n+\t\t\tconst subMap = extractBlobMap(entry.node as Directory, fullPath);\n+\t\t\tsubMap.forEach((value, key) =\u003e blobMap.set(key, value));\n+\t\t}\n+\t}\n+\n+\treturn blobMap;\n+}\ndiff --git a/src/routes/wisp.ts b/src/routes/wisp.ts\nindex 522717a..39b51f1 100644\n--- a/src/routes/wisp.ts\n+++ b/src/routes/wisp.ts\n@@ -9,7 +9,9 @@ import {\n \tcreateManifest,\n \tupdateFileBlobs,\n \tshouldCompressFile,\n-\tcompressFile\n+\tcompressFile,\n+\tcomputeCID,\n+\textractBlobMap\n } from '../lib/wisp-utils'\n import { upsertSite } from '../lib/db'\n import { logger } from '../lib/observability'\n@@ -49,6 +51,10 @@ export const wispRoutes = (client: NodeOAuthClient) =\u003e\n \t\t\t\t\tfiles: File | File[]\n \t\t\t\t};\n \n+\t\t\t\tconsole.log('=== UPLOAD FILES START ===');\n+\t\t\t\tconsole.log('Site name:', siteName);\n+\t\t\t\tconsole.log('Files received:', Array.isArray(files) ? files.length : 'single file');\n+\n \t\t\t\ttry {\n \t\t\t\t\tif (!siteName) {\n \t\t\t\t\t\tthrow new Error('Site name is required')\n@@ -106,6 +112,33 @@ export const wispRoutes = (client: NodeOAuthClient) =\u003e\n \n \t\t\t\t\t// Create agent with OAuth session\n \t\t\t\t\tconst agent = new Agent((url, init) =\u003e auth.session.fetchHandler(url, init))\n+\t\t\t\t\tconsole.log('Agent created for DID:', auth.did);\n+\n+\t\t\t\t\t// Try to fetch existing record to enable incremental updates\n+\t\t\t\t\tlet existingBlobMap = new Map\u003cstring, { blobRef: any; cid: string }\u003e();\n+\t\t\t\t\tconsole.log('Attempting to fetch existing record...');\n+\t\t\t\t\ttry {\n+\t\t\t\t\t\tconst rkey = siteName;\n+\t\t\t\t\t\tconst existingRecord = await agent.com.atproto.repo.getRecord({\n+\t\t\t\t\t\t\trepo: auth.did,\n+\t\t\t\t\t\t\tcollection: 'place.wisp.fs',\n+\t\t\t\t\t\t\trkey: rkey\n+\t\t\t\t\t\t});\n+\t\t\t\t\t\tconsole.log('Existing record found!');\n+\n+\t\t\t\t\t\tif (existingRecord.data.value \u0026\u0026 typeof existingRecord.data.value === 'object' \u0026\u0026 'root' in existingRecord.data.value) {\n+\t\t\t\t\t\t\tconst manifest = existingRecord.data.value as any;\n+\t\t\t\t\t\t\texistingBlobMap = extractBlobMap(manifest.root);\n+\t\t\t\t\t\t\tconsole.log(`Found existing manifest with ${existingBlobMap.size} files for incremental update`);\n+\t\t\t\t\t\t\tlogger.info(`Found existing manifest with ${existingBlobMap.size} files for incremental update`);\n+\t\t\t\t\t\t}\n+\t\t\t\t\t} catch (error: any) {\n+\t\t\t\t\t\tconsole.log('No existing record found or error:', error?.message || error);\n+\t\t\t\t\t\t// Record doesn't exist yet, this is a new site\n+\t\t\t\t\t\tif (error?.status !== 400 \u0026\u0026 error?.error !== 'RecordNotFound') {\n+\t\t\t\t\t\t\tlogger.warn('Failed to fetch existing record, proceeding with full upload', error);\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n \n \t\t\t\t\t// Convert File objects to UploadedFile format\n \t\t\t\t\t// Elysia gives us File objects directly, handle both single file and array\n@@ -113,10 +146,11 @@ export const wispRoutes = (client: NodeOAuthClient) =\u003e\n \t\t\t\t\tconst uploadedFiles: UploadedFile[] = [];\n \t\t\t\t\tconst skippedFiles: Array\u003c{ name: string; reason: string }\u003e = [];\n \n-\n+\t\t\t\t\tconsole.log('Processing files, count:', fileArray.length);\n \n \t\t\t\t\tfor (let i = 0; i \u003c fileArray.length; i++) {\n \t\t\t\t\t\tconst file = fileArray[i];\n+\t\t\t\t\t\tconsole.log(`Processing file ${i + 1}/${fileArray.length}:`, file.name, file.size, 'bytes');\n \n \t\t\t\t\t\t// Skip files that are too large (limit to 100MB per file)\n \t\t\t\t\t\tconst maxSize = MAX_FILE_SIZE; // 100MB\n@@ -135,13 +169,16 @@ export const wispRoutes = (client: NodeOAuthClient) =\u003e\n \t\t\t\t\t\t// Compress and base64 encode ALL files\n \t\t\t\t\t\tconst compressedContent = compressFile(originalContent);\n \t\t\t\t\t\t// Base64 encode the gzipped content to prevent PDS content sniffing\n-\t\t\t\t\t\tconst base64Content = Buffer.from(compressedContent.toString('base64'), 'utf-8');\n+\t\t\t\t\t\t// Convert base64 string to bytes using binary encoding (each char becomes exactly one byte)\n+\t\t\t\t\t\t// This is what PDS receives and computes CID on\n+\t\t\t\t\t\tconst base64Content = Buffer.from(compressedContent.toString('base64'), 'binary');\n \t\t\t\t\t\tconst compressionRatio = (compressedContent.length / originalContent.length * 100).toFixed(1);\n+\t\t\t\t\t\tconsole.log(`Compressing ${file.name}: ${originalContent.length} -\u003e ${compressedContent.length} bytes (${compressionRatio}%), base64: ${base64Content.length} bytes`);\n \t\t\t\t\t\tlogger.info(`Compressing ${file.name}: ${originalContent.length} -\u003e ${compressedContent.length} bytes (${compressionRatio}%), base64: ${base64Content.length} bytes`);\n \n \t\t\t\t\t\tuploadedFiles.push({\n \t\t\t\t\t\t\tname: file.name,\n-\t\t\t\t\t\t\tcontent: base64Content,\n+\t\t\t\t\t\t\tcontent: base64Content, // This is the gzipped+base64 content that will be uploaded and CID-computed\n \t\t\t\t\t\t\tmimeType: originalMimeType,\n \t\t\t\t\t\t\tsize: base64Content.length,\n \t\t\t\t\t\t\tcompressed: true,\n@@ -206,13 +243,79 @@ export const wispRoutes = (client: NodeOAuthClient) =\u003e\n \t\t\t\t\t}\n \n \t\t\t\t\t// Process files into directory structure\n-\t\t\t\t\tconst { directory, fileCount } = processUploadedFiles(uploadedFiles);\n+\t\t\t\t\tconsole.log('Processing uploaded files into directory structure...');\n+\t\t\t\t\tconsole.log('uploadedFiles array length:', uploadedFiles.length);\n+\t\t\t\t\tconsole.log('uploadedFiles contents:', uploadedFiles.map((f, i) =\u003e `${i}: ${f?.name || 'UNDEFINED'}`));\n+\n+\t\t\t\t\t// Filter out any undefined/null/invalid entries (defensive)\n+\t\t\t\t\tconst validUploadedFiles = uploadedFiles.filter((f, i) =\u003e {\n+\t\t\t\t\t\tif (!f) {\n+\t\t\t\t\t\t\tconsole.error(`Filtering out undefined/null file at index ${i}`);\n+\t\t\t\t\t\t\treturn false;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\tif (!f.name) {\n+\t\t\t\t\t\t\tconsole.error(`Filtering out file with no name at index ${i}:`, f);\n+\t\t\t\t\t\t\treturn false;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\tif (!f.content) {\n+\t\t\t\t\t\t\tconsole.error(`Filtering out file with no content at index ${i}:`, f.name);\n+\t\t\t\t\t\t\treturn false;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\treturn true;\n+\t\t\t\t\t});\n+\t\t\t\t\tif (validUploadedFiles.length !== uploadedFiles.length) {\n+\t\t\t\t\t\tconsole.warn(`Filtered out ${uploadedFiles.length - validUploadedFiles.length} invalid files`);\n+\t\t\t\t\t}\n+\t\t\t\t\tconsole.log('validUploadedFiles length:', validUploadedFiles.length);\n \n-\t\t\t\t\t// Upload files as blobs in parallel\n+\t\t\t\t\tconst { directory, fileCount } = processUploadedFiles(validUploadedFiles);\n+\t\t\t\t\tconsole.log('Directory structure created, file count:', fileCount);\n+\n+\t\t\t\t\t// Upload files as blobs in parallel (or reuse existing blobs with matching CIDs)\n+\t\t\t\t\tconsole.log('Starting blob upload/reuse phase...');\n \t\t\t\t\t// For compressed files, we upload as octet-stream and store the original MIME type in metadata\n \t\t\t\t\t// For text/html files, we also use octet-stream as a workaround for PDS image pipeline issues\n-\t\t\t\t\tconst uploadPromises = uploadedFiles.map(async (file, i) =\u003e {\n+\t\t\t\t\tconst uploadPromises = validUploadedFiles.map(async (file, i) =\u003e {\n \t\t\t\t\t\ttry {\n+\t\t\t\t\t\t\t// Skip undefined files (shouldn't happen after filter, but defensive)\n+\t\t\t\t\t\t\tif (!file || !file.name) {\n+\t\t\t\t\t\t\t\tconsole.error(`ERROR: Undefined file at index ${i} in validUploadedFiles!`);\n+\t\t\t\t\t\t\t\tthrow new Error(`Undefined file at index ${i}`);\n+\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\t// Compute CID for this file to check if it already exists\n+\t\t\t\t\t\t\t// Note: file.content is already gzipped+base64 encoded\n+\t\t\t\t\t\t\tconst fileCID = computeCID(file.content);\n+\n+\t\t\t\t\t\t\t// Normalize the file path for comparison (remove base folder prefix like \"cobblemon/\")\n+\t\t\t\t\t\t\tconst normalizedPath = file.name.replace(/^[^\\/]*\\//, '');\n+\n+\t\t\t\t\t\t\t// Check if we have an existing blob with the same CID\n+\t\t\t\t\t\t\t// Try both the normalized path and the full path\n+\t\t\t\t\t\t\tconst existingBlob = existingBlobMap.get(normalizedPath) || existingBlobMap.get(file.name);\n+\n+\t\t\t\t\t\t\tif (existingBlob \u0026\u0026 existingBlob.cid === fileCID) {\n+\t\t\t\t\t\t\t\t// Reuse existing blob - no need to upload\n+\t\t\t\t\t\t\t\tlogger.info(`[File Upload] Reusing existing blob for: ${file.name} (CID: ${fileCID})`);\n+\n+\t\t\t\t\t\t\t\treturn {\n+\t\t\t\t\t\t\t\t\tresult: {\n+\t\t\t\t\t\t\t\t\t\thash: existingBlob.cid,\n+\t\t\t\t\t\t\t\t\t\tblobRef: existingBlob.blobRef,\n+\t\t\t\t\t\t\t\t\t\t...(file.compressed \u0026\u0026 {\n+\t\t\t\t\t\t\t\t\t\t\tencoding: 'gzip' as const,\n+\t\t\t\t\t\t\t\t\t\t\tmimeType: file.originalMimeType || file.mimeType,\n+\t\t\t\t\t\t\t\t\t\t\tbase64: true\n+\t\t\t\t\t\t\t\t\t\t})\n+\t\t\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t\t\t\tfilePath: file.name,\n+\t\t\t\t\t\t\t\t\tsentMimeType: file.mimeType,\n+\t\t\t\t\t\t\t\t\treturnedMimeType: existingBlob.blobRef.mimeType,\n+\t\t\t\t\t\t\t\t\treused: true\n+\t\t\t\t\t\t\t\t};\n+\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\t// File is new or changed - upload it\n \t\t\t\t\t\t\t// If compressed, always upload as octet-stream\n \t\t\t\t\t\t\t// Otherwise, workaround: PDS incorrectly processes text/html through image pipeline\n \t\t\t\t\t\t\tconst uploadMimeType = file.compressed || file.mimeType.startsWith('text/html')\n@@ -220,7 +323,7 @@ export const wispRoutes = (client: NodeOAuthClient) =\u003e\n \t\t\t\t\t\t\t\t: file.mimeType;\n \n \t\t\t\t\t\t\tconst compressionInfo = file.compressed ? ' (gzipped)' : '';\n-\t\t\t\t\t\t\tlogger.info(`[File Upload] Uploading file: ${file.name} (original: ${file.mimeType}, sending as: ${uploadMimeType}, ${file.size} bytes${compressionInfo})`);\n+\t\t\t\t\t\t\tlogger.info(`[File Upload] Uploading new/changed file: ${file.name} (original: ${file.mimeType}, sending as: ${uploadMimeType}, ${file.size} bytes${compressionInfo}, CID: ${fileCID})`);\n \n \t\t\t\t\t\t\tconst uploadResult = await agent.com.atproto.repo.uploadBlob(\n \t\t\t\t\t\t\t\tfile.content,\n@@ -244,7 +347,8 @@ export const wispRoutes = (client: NodeOAuthClient) =\u003e\n \t\t\t\t\t\t\t\t},\n \t\t\t\t\t\t\t\tfilePath: file.name,\n \t\t\t\t\t\t\t\tsentMimeType: file.mimeType,\n-\t\t\t\t\t\t\t\treturnedMimeType: returnedBlobRef.mimeType\n+\t\t\t\t\t\t\t\treturnedMimeType: returnedBlobRef.mimeType,\n+\t\t\t\t\t\t\t\treused: false\n \t\t\t\t\t\t\t};\n \t\t\t\t\t\t} catch (uploadError) {\n \t\t\t\t\t\t\tlogger.error('Upload failed for file', uploadError);\n@@ -255,28 +359,40 @@ export const wispRoutes = (client: NodeOAuthClient) =\u003e\n \t\t\t\t\t// Wait for all uploads to complete\n \t\t\t\t\tconst uploadedBlobs = await Promise.all(uploadPromises);\n \n+\t\t\t\t\t// Count reused vs uploaded blobs\n+\t\t\t\t\tconst reusedCount = uploadedBlobs.filter(b =\u003e (b as any).reused).length;\n+\t\t\t\t\tconst uploadedCount = uploadedBlobs.filter(b =\u003e !(b as any).reused).length;\n+\t\t\t\t\tconsole.log(`Blob statistics: ${reusedCount} reused, ${uploadedCount} uploaded, ${uploadedBlobs.length} total`);\n+\t\t\t\t\tlogger.info(`Blob statistics: ${reusedCount} reused, ${uploadedCount} uploaded, ${uploadedBlobs.length} total`);\n+\n \t\t\t\t\t// Extract results and file paths in correct order\n \t\t\t\t\tconst uploadResults: FileUploadResult[] = uploadedBlobs.map(blob =\u003e blob.result);\n \t\t\t\t\tconst filePaths: string[] = uploadedBlobs.map(blob =\u003e blob.filePath);\n \n \t\t\t\t\t// Update directory with file blobs\n+\t\t\t\t\tconsole.log('Updating directory with blob references...');\n \t\t\t\t\tconst updatedDirectory = updateFileBlobs(directory, uploadResults, filePaths);\n \n \t\t\t\t\t// Create manifest\n+\t\t\t\t\tconsole.log('Creating manifest...');\n \t\t\t\t\tconst manifest = createManifest(siteName, updatedDirectory, fileCount);\n+\t\t\t\t\tconsole.log('Manifest created successfully');\n \n \t\t\t\t\t// Use site name as rkey\n \t\t\t\t\tconst rkey = siteName;\n \n \t\t\t\t\tlet record;\n \t\t\t\t\ttry {\n+\t\t\t\t\t\tconsole.log('Putting record to PDS with rkey:', rkey);\n \t\t\t\t\t\trecord = await agent.com.atproto.repo.putRecord({\n \t\t\t\t\t\t\trepo: auth.did,\n \t\t\t\t\t\t\tcollection: 'place.wisp.fs',\n \t\t\t\t\t\t\trkey: rkey,\n \t\t\t\t\t\t\trecord: manifest\n \t\t\t\t\t\t});\n+\t\t\t\t\t\tconsole.log('Record successfully created on PDS:', record.data.uri);\n \t\t\t\t\t} catch (putRecordError: any) {\n+\t\t\t\t\t\tconsole.error('FAILED to create record on PDS:', putRecordError);\n \t\t\t\t\t\tlogger.error('Failed to create record on PDS', putRecordError);\n \n \t\t\t\t\t\tthrow putRecordError;\n@@ -292,11 +408,15 @@ export const wispRoutes = (client: NodeOAuthClient) =\u003e\n \t\t\t\t\t\tfileCount,\n \t\t\t\t\t\tsiteName,\n \t\t\t\t\t\tskippedFiles,\n-\t\t\t\t\t\tuploadedCount: uploadedFiles.length\n+\t\t\t\t\t\tuploadedCount: validUploadedFiles.length\n \t\t\t\t\t};\n \n+\t\t\t\t\tconsole.log('=== UPLOAD FILES COMPLETE ===');\n \t\t\t\t\treturn result;\n \t\t\t\t} catch (error) {\n+\t\t\t\t\tconsole.error('=== UPLOAD ERROR ===');\n+\t\t\t\t\tconsole.error('Error details:', error);\n+\t\t\t\t\tconsole.error('Stack trace:', error instanceof Error ? error.stack : 'N/A');\n \t\t\t\t\tlogger.error('Upload error', error, {\n \t\t\t\t\t\tmessage: error instanceof Error ? error.message : 'Unknown error',\n \t\t\t\t\t\tname: error instanceof Error ? error.name : undefined\n-- \n2.51.0\n\n\nFrom 0ea4cb15f7eac8fc3e7796a55f499789ba60e8d2 Mon Sep 17 00:00:00 2001\nFrom: \"@nekomimi.pet\" \u003cana@nekoimimi.pet\u003e\nDate: Sun, 9 Nov 2025 03:32:59 -0500\nSubject: [PATCH] index db\n\n---\n src/lib/db.ts | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 59 insertions(+)\n\ndiff --git a/src/lib/db.ts b/src/lib/db.ts\nindex b5f82c4..076b4e9 100644\n--- a/src/lib/db.ts\n+++ b/src/lib/db.ts\n@@ -108,6 +108,65 @@ await db`\n )\n `;\n \n+// Create indexes for common query patterns\n+await Promise.all([\n+ // oauth_states cleanup queries\n+ db`CREATE INDEX IF NOT EXISTS idx_oauth_states_expires_at ON oauth_states(expires_at)`.catch(err =\u003e {\n+ if (!err.message?.includes('already exists')) {\n+ console.error('Failed to create idx_oauth_states_expires_at:', err);\n+ }\n+ }),\n+\n+ // oauth_sessions cleanup queries\n+ db`CREATE INDEX IF NOT EXISTS idx_oauth_sessions_expires_at ON oauth_sessions(expires_at)`.catch(err =\u003e {\n+ if (!err.message?.includes('already exists')) {\n+ console.error('Failed to create idx_oauth_sessions_expires_at:', err);\n+ }\n+ }),\n+\n+ // oauth_keys key rotation queries\n+ db`CREATE INDEX IF NOT EXISTS idx_oauth_keys_created_at ON oauth_keys(created_at)`.catch(err =\u003e {\n+ if (!err.message?.includes('already exists')) {\n+ console.error('Failed to create idx_oauth_keys_created_at:', err);\n+ }\n+ }),\n+\n+ // domains queries by (did, rkey)\n+ db`CREATE INDEX IF NOT EXISTS idx_domains_did_rkey ON domains(did, rkey)`.catch(err =\u003e {\n+ if (!err.message?.includes('already exists')) {\n+ console.error('Failed to create idx_domains_did_rkey:', err);\n+ }\n+ }),\n+\n+ // custom_domains queries by did\n+ db`CREATE INDEX IF NOT EXISTS idx_custom_domains_did ON custom_domains(did)`.catch(err =\u003e {\n+ if (!err.message?.includes('already exists')) {\n+ console.error('Failed to create idx_custom_domains_did:', err);\n+ }\n+ }),\n+\n+ // custom_domains queries by (did, rkey)\n+ db`CREATE INDEX IF NOT EXISTS idx_custom_domains_did_rkey ON custom_domains(did, rkey)`.catch(err =\u003e {\n+ if (!err.message?.includes('already exists')) {\n+ console.error('Failed to create idx_custom_domains_did_rkey:', err);\n+ }\n+ }),\n+\n+ // custom_domains DNS verification worker queries\n+ db`CREATE INDEX IF NOT EXISTS idx_custom_domains_verified ON custom_domains(verified)`.catch(err =\u003e {\n+ if (!err.message?.includes('already exists')) {\n+ console.error('Failed to create idx_custom_domains_verified:', err);\n+ }\n+ }),\n+\n+ // sites queries by did\n+ db`CREATE INDEX IF NOT EXISTS idx_sites_did ON sites(did)`.catch(err =\u003e {\n+ if (!err.message?.includes('already exists')) {\n+ console.error('Failed to create idx_sites_did:', err);\n+ }\n+ })\n+]);\n+\n const RESERVED_HANDLES = new Set([\n \"www\",\n \"api\",\n-- \n2.51.0\n\n\n",
"source": {
"branch": "incremental-updates",
"sha": "0ea4cb15f7eac8fc3e7796a55f499789ba60e8d2"
},
"target": {
"branch": "main",
"repo": "at://did:plc:ttdrpj45ibqunmfhdsb4zdwq/sh.tangled.repo/3m4wgtddvwv22"
},
"title": "save CIDs to local metadata file, incrementally download new files, copy old"
}