From 080e3f10e8fc1c692c8611561a7769f10951d390 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Wed, 29 Apr 2026 03:17:18 +0530 Subject: [PATCH 1/6] migrate(chat): repoint /api/upload to recoup-api Cuts over `usePureFileAttachments` and `lib/arweave/uploadFile.tsx` to the new dedicated `POST /api/upload` on api (Arweave bytes proxy parity), and removes the local route. The API response shape is 1:1, so this is a base-URL swap. `lib/arweave/uploadToArweave.ts` is kept since it's still imported by `lib/txtGeneration.ts` and `lib/ai/generateImage.ts`. --- app/api/upload/route.ts | 39 --------------------------------- hooks/usePureFileAttachments.ts | 9 ++++---- lib/arweave/uploadFile.tsx | 4 +++- 3 files changed, 8 insertions(+), 44 deletions(-) delete mode 100644 app/api/upload/route.ts diff --git a/app/api/upload/route.ts b/app/api/upload/route.ts deleted file mode 100644 index 2f42d3aa0..000000000 --- a/app/api/upload/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { NextResponse } from "next/server"; -import uploadToArweave from "@/lib/arweave/uploadToArweave"; -import { getFetchableUrl } from "@/lib/arweave/gateway"; - -export async function POST(request: Request) { - try { - const formData = await request.formData(); - const file = formData.get("file") as File; - - if (!file) { - throw new Error("No file provided"); - } - - const fileBuffer = Buffer.from(await file.arrayBuffer()); - const fileSize = fileBuffer.length; - - const arweaveUrl = await uploadToArweave({ - base64Data: fileBuffer.toString("base64"), - mimeType: file.type || "application/octet-stream", - }); - - return NextResponse.json({ - success: true, - fileName: file.name, - fileType: file.type, - fileSize, - url: getFetchableUrl(arweaveUrl), - }); - } catch (error) { - console.error("Upload failed:", error); - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : "Unknown error", - }, - { status: 500 } - ); - } -} diff --git a/hooks/usePureFileAttachments.ts b/hooks/usePureFileAttachments.ts index 0515cfcdf..60228c39b 100644 --- a/hooks/usePureFileAttachments.ts +++ b/hooks/usePureFileAttachments.ts @@ -4,6 +4,7 @@ import { useVercelChatContext } from "@/providers/VercelChatProvider"; import { CHAT_INPUT_SUPPORTED_FILE } from "@/lib/chat/config"; import { isAllowedByExtension } from "@/lib/files/isAllowedByExtension"; import { getFileExtension } from "@/lib/files/getFileExtension"; +import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; export function usePureFileAttachments() { const { setAttachments, addTextAttachment } = useVercelChatContext(); @@ -57,7 +58,7 @@ export function usePureFileAttachments() { const formData = new FormData(); formData.append("file", file); - const response = await fetch("/api/upload", { + const response = await fetch(`${getClientApiBaseUrl()}/api/upload`, { method: "POST", body: formData, }); @@ -83,8 +84,8 @@ export function usePureFileAttachments() { mediaType: data.fileType, url: data.url, } as FileUIPart) - : attachment - ) + : attachment, + ), ); // Revoke the temporary object URL to avoid memory leaks @@ -93,7 +94,7 @@ export function usePureFileAttachments() { console.error("Error uploading file:", error); // Remove the failed attachment setAttachments((prev: FileUIPart[]) => - prev.filter((a: FileUIPart) => a.url !== tempUrl) + prev.filter((a: FileUIPart) => a.url !== tempUrl), ); // Revoke the temporary object URL URL.revokeObjectURL(tempUrl); diff --git a/lib/arweave/uploadFile.tsx b/lib/arweave/uploadFile.tsx index f6ea2a5ab..da6897968 100644 --- a/lib/arweave/uploadFile.tsx +++ b/lib/arweave/uploadFile.tsx @@ -1,3 +1,5 @@ +import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; + export type ArweaveUploadResponse = { id: string; uri: string; @@ -10,7 +12,7 @@ export const uploadFile = async ( const data = new FormData(); data.set("file", file); - const res = await fetch("/api/upload", { + const res = await fetch(`${getClientApiBaseUrl()}/api/upload`, { method: "POST", body: data, }); From 889363c58a86a02bd3386162e171500f2288c98f Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Fri, 8 May 2026 20:54:20 +0530 Subject: [PATCH 2/6] feat(images): allow *.supabase.co public CDN URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase-1 of the api-side Arweave→Supabase upload migration (recoupable/api#492) returns Supabase public bucket URLs from POST /api/upload. Add the hostname to next.config.mjs remotePatterns so Next/Image can render them. --- next.config.mjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/next.config.mjs b/next.config.mjs index c5387a5b5..2f2eab1a5 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -58,6 +58,12 @@ const nextConfig = { port: '', pathname: '/**', }, + { + protocol: 'https', + hostname: '*.supabase.co', + port: '', + pathname: '/storage/v1/object/public/**', + }, ], }, }; From e1241feb90a2cb270c8a3364cac92c8c7d2b5e32 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Sat, 9 May 2026 07:58:13 +0530 Subject: [PATCH 3/6] chore: refresh PR head ref after branch rename From d72cd0e2b920e6e0911f22f9851d8bde968d1073 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Sat, 9 May 2026 08:14:51 +0530 Subject: [PATCH 4/6] chore(chat): remove dead lib/arweave; move uploadFile to lib/files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The api migration broke every direct Arweave consumer in chat. The remaining callers of lib/arweave/* were three orphans (lib/txtGeneration.ts, lib/ai/generateImage.ts, lib/email/generateTxtFileEmail.ts) imported by nothing, plus uploadFile.tsx itself which was repointed to api in this PR and has nothing Arweave-specific left. - Move lib/arweave/uploadFile.tsx → lib/files/uploadFile.tsx (and rename ArweaveUploadResponse → UploadFileResponse). - Update the 4 hook imports (useUser, useOrgSettings, useSaveKnowledgeEdit, useArtistSetting). - Delete the orphaned consumers. - Delete lib/arweave/ entirely. Net: -415 / +7 lines. --- hooks/useArtistSetting.tsx | 2 +- hooks/useOrgSettings.ts | 2 +- hooks/useSaveKnowledgeEdit.ts | 2 +- hooks/useUser.tsx | 2 +- lib/ai/generateImage.ts | 34 ------------ lib/arweave/arweave.ts | 5 -- lib/arweave/gateway.ts | 48 ----------------- lib/arweave/ipfs.ts | 73 ------------------------- lib/arweave/uploadLinkToArweave.ts | 28 ---------- lib/arweave/uploadMetadataJson.ts | 29 ---------- lib/arweave/uploadToArweave.ts | 60 --------------------- lib/email/generateTxtFileEmail.ts | 53 ------------------- lib/{arweave => files}/uploadFile.tsx | 8 ++- lib/txtGeneration.ts | 76 --------------------------- 14 files changed, 7 insertions(+), 415 deletions(-) delete mode 100644 lib/ai/generateImage.ts delete mode 100644 lib/arweave/arweave.ts delete mode 100644 lib/arweave/gateway.ts delete mode 100644 lib/arweave/ipfs.ts delete mode 100644 lib/arweave/uploadLinkToArweave.ts delete mode 100644 lib/arweave/uploadMetadataJson.ts delete mode 100644 lib/arweave/uploadToArweave.ts delete mode 100644 lib/email/generateTxtFileEmail.ts rename lib/{arweave => files}/uploadFile.tsx (74%) delete mode 100644 lib/txtGeneration.ts diff --git a/hooks/useArtistSetting.tsx b/hooks/useArtistSetting.tsx index 2440cb3cb..705af355b 100644 --- a/hooks/useArtistSetting.tsx +++ b/hooks/useArtistSetting.tsx @@ -1,4 +1,4 @@ -import { uploadFile } from "@/lib/arweave/uploadFile"; +import { uploadFile } from "@/lib/files/uploadFile"; import { useEffect, useRef, useState } from "react"; import { ArtistRecord } from "@/types/Artist"; import { getFileMimeType } from "@/utils/getFileMimeType"; diff --git a/hooks/useOrgSettings.ts b/hooks/useOrgSettings.ts index a4cde19e5..75ec9b544 100644 --- a/hooks/useOrgSettings.ts +++ b/hooks/useOrgSettings.ts @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { useQueryClient } from "@tanstack/react-query"; -import { uploadFile } from "@/lib/arweave/uploadFile"; +import { uploadFile } from "@/lib/files/uploadFile"; import { getFileMimeType } from "@/utils/getFileMimeType"; import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; import useAccountOrganizations from "./useAccountOrganizations"; diff --git a/hooks/useSaveKnowledgeEdit.ts b/hooks/useSaveKnowledgeEdit.ts index 370a9a4a0..b5f3488b7 100644 --- a/hooks/useSaveKnowledgeEdit.ts +++ b/hooks/useSaveKnowledgeEdit.ts @@ -1,6 +1,6 @@ import { useArtistProvider } from "@/providers/ArtistProvider"; import getMimeFromPath from "@/lib/files/getMimeFromPath"; -import { uploadFile } from "@/lib/arweave/uploadFile"; +import { uploadFile } from "@/lib/files/uploadFile"; import { toast } from "react-toastify"; type UseSaveKnowledgeEditArgs = { diff --git a/hooks/useUser.tsx b/hooks/useUser.tsx index d5434e00f..642813c15 100644 --- a/hooks/useUser.tsx +++ b/hooks/useUser.tsx @@ -2,7 +2,7 @@ import { usePrivy } from "@privy-io/react-auth"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { Address } from "viem"; -import { uploadFile } from "@/lib/arweave/uploadFile"; +import { uploadFile } from "@/lib/files/uploadFile"; import { useAccount } from "wagmi"; import { toast } from "sonner"; import { AccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails"; diff --git a/lib/ai/generateImage.ts b/lib/ai/generateImage.ts deleted file mode 100644 index 224b8ccfc..000000000 --- a/lib/ai/generateImage.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { experimental_generateImage as generate } from "ai"; -import { openai } from "@ai-sdk/openai"; -import uploadToArweave from "../arweave/uploadToArweave"; - -const generateImage = async (prompt: string): Promise => { - const response = await generate({ - model: openai.image("gpt-image-1"), - prompt, - providerOptions: { - openai: { quality: "high" }, - }, - }); - - // The base64Data isn't properly typed in the ai SDK, so we need to cast the response - // @ts-expect-error The 'image' object from generateImage includes base64Data but it's not in the type - const base64Data: string = response.image.base64Data; - - const imageData = { - base64Data, - mimeType: response.image.mediaType, - }; - - // Upload the generated image to Arweave - let arweaveData = null; - try { - arweaveData = await uploadToArweave(imageData); - } catch (arweaveError) { - console.error("Error uploading to Arweave:", arweaveError); - // We'll continue and return the image even if Arweave upload fails - } - return arweaveData; -}; - -export default generateImage; diff --git a/lib/arweave/arweave.ts b/lib/arweave/arweave.ts deleted file mode 100644 index 2dec77c86..000000000 --- a/lib/arweave/arweave.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type ArweaveURL = `ar://${string}`; - -export function isArweaveURL(url: string | null | undefined): boolean { - return url && typeof url === "string" ? url.startsWith("ar://") : false; -} diff --git a/lib/arweave/gateway.ts b/lib/arweave/gateway.ts deleted file mode 100644 index c8c38153c..000000000 --- a/lib/arweave/gateway.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { isArweaveURL } from "./arweave"; -import { isNormalizeableIPFSUrl, normalizeIPFSUrl } from "./ipfs"; - -const IPFS_GATEWAY = "https://magic.decentralized-content.com"; - -const ARWEAVE_GATEWAY = "https://arweave.net"; - -export function arweaveGatewayUrl(normalizedArweaveUrl: string | null) { - if (!normalizedArweaveUrl || typeof normalizedArweaveUrl !== "string") - return null; - return normalizedArweaveUrl.replace("ar://", `${ARWEAVE_GATEWAY}/`); -} - -export function ipfsGatewayUrl(url: string | null) { - if (!url || typeof url !== "string") return null; - const normalizedIPFSUrl = normalizeIPFSUrl(url); - if (normalizedIPFSUrl) { - return normalizedIPFSUrl.replace("ipfs://", `${IPFS_GATEWAY}/ipfs/`); - } - return null; -} - -export function getFetchableUrl(uri: string | null | undefined): string | null { - if (!uri || typeof uri !== "string") return null; - - // Prevent fetching from insecure URLs - if (uri.startsWith("http://")) return null; - - // If it is an IPFS HTTP or ipfs:// url - if (isNormalizeableIPFSUrl(uri)) { - // Return a fetchable gateway url - return ipfsGatewayUrl(uri); - } - - // If it is a ar:// url - if (isArweaveURL(uri)) { - // Return a fetchable gateway url - return arweaveGatewayUrl(uri); - } - - // If it is already a url (or blob or data-uri) - if (/^(https|data|blob):/.test(uri)) { - // Return the URI - return uri; - } - - return null; -} diff --git a/lib/arweave/ipfs.ts b/lib/arweave/ipfs.ts deleted file mode 100644 index 9df2c3742..000000000 --- a/lib/arweave/ipfs.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { CID } from "multiformats/cid"; - -export type IPFSUrl = `ipfs://${string}`; - -export function isCID(str: string | null | undefined): boolean { - if (!str) return false; - - try { - CID.parse(str); - return true; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { - if (/^(bafy|Qm)/.test(str)) return true; - return false; - } -} - -export function normalizeIPFSUrl(url: string | null | undefined): IPFSUrl | null { - if (!url || typeof url !== "string") return null; - - // Handle urls wrapped in quotes - url = url.replace(/"/g, ""); - - // Check if already a normalized IPFS url - if (isNormalizedIPFSURL(url)) return url as IPFSUrl; - - // Check if url is a CID string - if (isCID(url)) return `ipfs://${url}`; - - // If url is not either an ipfs gateway or protocol url - if (!isIPFSUrl(url)) return null; - - // If url is already a gateway url, parse and normalize - if (isGatewayIPFSUrl(url)) { - // Replace leading double-slashes and parse URL - const parsed = new URL(url.replace(/^\/\//, "http://")); - // Remove IPFS from the URL - // http://gateway/ipfs/?x=y#z -> http://gateway/?x=y#z - parsed.pathname = parsed.pathname.replace(/^\/ipfs\//, ""); - // Remove the protocol and host from the URL - // http://gateway/?x=y#z -> ?x=y#z - const cid = parsed.toString().replace(`${parsed.protocol}//${parsed.host}/`, ""); - // Prepend ipfs protocol - return `ipfs://${cid}`; - } - - return null; -} - -function isNormalizedIPFSURL(url: string | null | undefined): boolean { - return url && typeof url === "string" ? url.startsWith("ipfs://") : false; -} - -function isGatewayIPFSUrl(url: string | null | undefined): boolean { - if (url && typeof url === "string") { - try { - const parsed = new URL(url.replace(/^"|'(.*)"|'$/, "$1")); - return !isNormalizedIPFSURL(url) && parsed && parsed.pathname.startsWith("/ipfs/"); - } catch { - return false; - } - } - - return false; -} - -function isIPFSUrl(url: string | null | undefined): boolean { - return url ? isNormalizedIPFSURL(url) || isGatewayIPFSUrl(url) : false; -} - -export function isNormalizeableIPFSUrl(url: string | null | undefined): boolean { - return url ? isIPFSUrl(url) || isCID(url) : false; -} diff --git a/lib/arweave/uploadLinkToArweave.ts b/lib/arweave/uploadLinkToArweave.ts deleted file mode 100644 index db42f1b74..000000000 --- a/lib/arweave/uploadLinkToArweave.ts +++ /dev/null @@ -1,28 +0,0 @@ -import uploadToArweave from "./uploadToArweave"; - -/** - * Uploads an image from a remote URL to Arweave and returns the Arweave URL. - * Falls back to the original URL if upload fails. - * - * @param imageUrl The remote image URL - * @returns The Arweave URL or the original URL if upload fails - */ -export default async function uploadLinkToArweave( - imageUrl: string -): Promise { - try { - const imgRes = await fetch(imageUrl); - const imgBuffer = Buffer.from(await imgRes.arrayBuffer()); - const imgBase64 = imgBuffer.toString("base64"); - const mimeType = imgRes.headers.get("content-type") || "image/png"; - - const arweaveResult = await uploadToArweave({ - base64Data: imgBase64, - mimeType, - }); - return arweaveResult; - } catch (err) { - console.error("Failed to upload image to Arweave, using original:", err); - return null; - } -} diff --git a/lib/arweave/uploadMetadataJson.ts b/lib/arweave/uploadMetadataJson.ts deleted file mode 100644 index bb52705ae..000000000 --- a/lib/arweave/uploadMetadataJson.ts +++ /dev/null @@ -1,29 +0,0 @@ -import uploadToArweave from "./uploadToArweave"; - -interface CreateMetadataArgs { - name: string; - image?: string; - animation_url?: string; - description?: string; - external_url?: string; - content?: { - mime: string; - uri: string; - }; -} - -/** - * Uploads a metadata JSON object to Arweave as a base64-encoded file. - * @param args The metadata creation arguments - * @returns The result from uploadToArweave - */ -export async function uploadMetadataJson(metadata: CreateMetadataArgs) { - const metadataBase64 = Buffer.from(JSON.stringify(metadata)).toString( - "base64" - ); - const metadataResult = await uploadToArweave({ - base64Data: metadataBase64, - mimeType: "application/json", - }); - return metadataResult; -} diff --git a/lib/arweave/uploadToArweave.ts b/lib/arweave/uploadToArweave.ts deleted file mode 100644 index 27ebc37ad..000000000 --- a/lib/arweave/uploadToArweave.ts +++ /dev/null @@ -1,60 +0,0 @@ -import Arweave from "arweave"; -import type { JWKInterface } from "arweave/node/lib/wallet"; - -const rawArweaveKey = process.env.ARWEAVE_KEY; - -if (!rawArweaveKey) { - throw new Error( - "ARWEAVE_KEY environment variable is missing. Please set it to a base64-encoded JSON key." - ); -} - -const ARWEAVE_KEY: JWKInterface = (() => { - try { - const decodedKey = Buffer.from(rawArweaveKey, "base64").toString(); - return JSON.parse(decodedKey) as JWKInterface; - } catch (error) { - throw new Error( - `Failed to decode ARWEAVE_KEY. Ensure it is base64-encoded JSON. ${ - error instanceof Error ? error.message : error - }` - ); - } -})(); - -const arweave = Arweave.init({ - host: "arweave.net", - port: 443, - protocol: "https", - timeout: 20000, - logging: false, -}); - -const uploadToArweave = async ( - imageData: { base64Data: string; mimeType: string }, - getProgress: (progress: number) => void = () => {} -): Promise => { - const buffer = Buffer.from(imageData.base64Data, "base64"); - - const transaction = await arweave.createTransaction( - { - data: buffer, - }, - ARWEAVE_KEY - ); - transaction.addTag("Content-Type", imageData.mimeType); - await arweave.transactions.sign(transaction, ARWEAVE_KEY); - const uploader = await arweave.transactions.getUploader(transaction); - - while (!uploader.isComplete) { - console.log( - `${uploader.pctComplete}% complete, ${uploader.uploadedChunks}/${uploader.totalChunks}` - ); - getProgress(uploader.pctComplete); - await uploader.uploadChunk(); - } - - return `ar://${transaction.id}`; -}; - -export default uploadToArweave; diff --git a/lib/email/generateTxtFileEmail.ts b/lib/email/generateTxtFileEmail.ts deleted file mode 100644 index 6e2635e26..000000000 --- a/lib/email/generateTxtFileEmail.ts +++ /dev/null @@ -1,53 +0,0 @@ -import generateText from "@/lib/ai/generateText"; -import sendEmail from "@/lib/email/sendEmail"; -import { RECOUP_FROM_EMAIL } from "../consts"; -import { getFetchableUrl } from "../arweave/gateway"; - -/** - * Sends a Recoup Apify webhook email to a list of emails, summarizing the dataset and using a strong CTA. - * @param dataset - Array of dataset objects (from Apify) - * @param emails - Array of email addresses to send to - * @returns The result of sendEmail - */ -export default async function generateTxtFileEmail({ - rawTextFile, - arweaveFile, - emails, - conversationId, -}: { - rawTextFile: string; - arweaveFile: string; - emails: string[]; - conversationId: string; -}) { - if (!emails?.length) return null; - const ctaUrl = `https://chat.recoupable.com/chat/${conversationId}`; - const prompt = `You have a newly generated TXT file. Here is the data: - -Key Data -Raw Text File: ${rawTextFile} -Link to view the full TXT File: ${getFetchableUrl(arweaveFile)} -CTA URL: ${ctaUrl} -`; - - const { text } = await generateText({ - system: `you are a record label services manager for Recoup. - write beautiful html email. - subject: New TXT File Notification. you're notifying music managers about a new txt file they've generated using Recoup Chat (AI Agents). - include a button to view the txt file (onClick open the CTA URL). - do not mention arweave in your email. - call to action is to open the original conversation where the txt file was generated using Recoup Chat (AI Agents): ${ctaUrl} - You'll be passed a raw text file and a link to view the full TXT File. - your goal is to get the recipient to click a cta link to open the original conversation where the txt file was generated using Recoup Chat (AI Agents). - only include the email body html. - no headers or subject`, - prompt, - }); - - return await sendEmail({ - from: RECOUP_FROM_EMAIL, - to: emails, - subject: `Recoup - New TXT File Created`, - html: text, - }); -} diff --git a/lib/arweave/uploadFile.tsx b/lib/files/uploadFile.tsx similarity index 74% rename from lib/arweave/uploadFile.tsx rename to lib/files/uploadFile.tsx index da6897968..f5ee65f59 100644 --- a/lib/arweave/uploadFile.tsx +++ b/lib/files/uploadFile.tsx @@ -1,13 +1,11 @@ import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; -export type ArweaveUploadResponse = { +export type UploadFileResponse = { id: string; uri: string; }; -export const uploadFile = async ( - file: File, -): Promise => { +export const uploadFile = async (file: File): Promise => { try { const data = new FormData(); data.set("file", file); @@ -28,7 +26,7 @@ export const uploadFile = async ( uri: json.url, }; } catch (error) { - console.error("Arweave upload failed:", error); + console.error("File upload failed:", error); throw error; } }; diff --git a/lib/txtGeneration.ts b/lib/txtGeneration.ts deleted file mode 100644 index 5d9e963c2..000000000 --- a/lib/txtGeneration.ts +++ /dev/null @@ -1,76 +0,0 @@ -import createCollection from "@/app/api/in_process/createCollection"; -import { uploadMetadataJson } from "./arweave/uploadMetadataJson"; -import uploadToArweave from "./arweave/uploadToArweave"; - -export interface GeneratedTxtResponse { - txt: { - base64Data: string; - mimeType: string; - }; - arweave?: string | null; - smartAccount: { - address: string; - [key: string]: unknown; - }; - transactionHash: string | null; -} - -export async function generateAndStoreTxtFile( - contents: string -): Promise { - if (!contents) { - throw new Error("Contents are required"); - } - - // Encode contents to base64 - const base64Data = Buffer.from(contents, "utf-8").toString("base64"); - const mimeType = "text/plain"; - - // Upload the TXT file to Arweave - let txtFile = null; - try { - txtFile = await uploadToArweave({ - base64Data, - mimeType, - }); - } catch (arweaveError) { - console.error("Error uploading TXT to Arweave:", arweaveError); - // Continue and return the TXT even if Arweave upload fails - } - - const image = "ar://EXwe2peizXKxjUMop6W-JPflC5sWyeQR1y0JiRDwUB0"; - - // Upload metadata JSON to Arweave - let metadataArweave = null; - try { - metadataArweave = await uploadMetadataJson({ - image, - animation_url: txtFile || undefined, - content: { - mime: mimeType, - uri: txtFile || "", - }, - description: contents, - name: contents, - }); - } catch (metadataError) { - console.error("Error uploading metadata to Arweave:", metadataError); - } - - // Create a collection on the blockchain using the metadata id - const result = await createCollection({ - collectionName: contents, - uri: metadataArweave || "", - }); - const transactionHash = result.transactionHash || null; - - return { - txt: { - base64Data, - mimeType, - }, - arweave: txtFile, - smartAccount: result.smartAccount, - transactionHash, - }; -} From fd320a5bc5f24afde490dcdbb996484d7880cbb4 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Wed, 27 May 2026 23:24:33 +0530 Subject: [PATCH 5/6] feat(upload): send Privy Bearer token on POST /api/upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Threads accessToken through lib/files/uploadFile, which now requires it and attaches Authorization: Bearer to the cross-origin fetch. Updates the five hooks that call uploadFile (useUser, useOrgSettings × 2, useArtistSetting × 2, useSaveKnowledgeEdit) to grab the token via usePrivy().getAccessToken() at call time. Inline fetch in usePureFileAttachments gets the same Bearer header, plus a robust error parse (json().catch(() => null), check both !res.ok and !data.success) consistent with the rest of the upload flow. Pairs with recoupable/api#540 which now requires auth and stamps uploaded_by metadata onto the storage object. Co-Authored-By: Claude Opus 4.7 (1M context) --- hooks/useArtistSetting.tsx | 16 ++++++++++------ hooks/useOrgSettings.ts | 28 ++++++++++++++++++++-------- hooks/usePureFileAttachments.ts | 17 +++++++++-------- hooks/useSaveKnowledgeEdit.ts | 5 ++++- hooks/useUser.tsx | 3 ++- lib/files/uploadFile.tsx | 14 ++++++++++---- 6 files changed, 55 insertions(+), 28 deletions(-) diff --git a/hooks/useArtistSetting.tsx b/hooks/useArtistSetting.tsx index 705af355b..d2bde85c6 100644 --- a/hooks/useArtistSetting.tsx +++ b/hooks/useArtistSetting.tsx @@ -1,9 +1,11 @@ import { uploadFile } from "@/lib/files/uploadFile"; import { useEffect, useRef, useState } from "react"; +import { usePrivy } from "@privy-io/react-auth"; import { ArtistRecord } from "@/types/Artist"; import { getFileMimeType } from "@/utils/getFileMimeType"; const useArtistSetting = () => { + const { getAccessToken } = usePrivy(); const imageRef = useRef(null); const baseRef = useRef(null); const [image, setImage] = useState(""); @@ -25,7 +27,7 @@ const useArtistSetting = () => { const [knowledgeUploading, setKnowledgeUploading] = useState(false); const [question, setQuestion] = useState(""); const [editableArtist, setEditableArtist] = useState( - null + null, ); const handleDeleteKnowledge = (index: number) => { @@ -35,7 +37,7 @@ const useArtistSetting = () => { }; const handleImageSelected = async ( - e: React.ChangeEvent + e: React.ChangeEvent, ) => { setImageUploading(true); const file = e.target.files?.[0]; @@ -44,7 +46,8 @@ const useArtistSetting = () => { return; } try { - const { uri } = await uploadFile(file); + const accessToken = await getAccessToken(); + const { uri } = await uploadFile(file, accessToken); setImage(uri); } catch (error) { console.error("Failed to upload image:", error); @@ -53,17 +56,18 @@ const useArtistSetting = () => { }; const handleKnowledgesSelected = async ( - e: React.ChangeEvent + e: React.ChangeEvent, ) => { setKnowledgeUploading(true); const files = e.target.files; const temp = []; try { if (files) { + const accessToken = await getAccessToken(); for (const file of files) { const name = file.name; const type = getFileMimeType(file); - const { uri } = await uploadFile(file); + const { uri } = await uploadFile(file, accessToken); temp.push({ name, url: uri, @@ -114,7 +118,7 @@ const useArtistSetting = () => { }; Object.entries(socialMediaTypes).forEach(([type, setter]) => { const link = editableArtist?.account_socials?.find( - (item) => item.type === type + (item) => item.type === type, )?.link; setter(link || ""); }); diff --git a/hooks/useOrgSettings.ts b/hooks/useOrgSettings.ts index 75ec9b544..b04e2d556 100644 --- a/hooks/useOrgSettings.ts +++ b/hooks/useOrgSettings.ts @@ -50,7 +50,7 @@ const useOrgSettings = (orgId: string | null) => { // Find the organization from the list (same as button does) const selectedOrg = organizations.find( - (org) => org.organization_id === orgId + (org) => org.organization_id === orgId, ); if (!selectedOrg) { @@ -67,7 +67,7 @@ const useOrgSettings = (orgId: string | null) => { setIsLoading(true); try { const response = await fetch( - `${getClientApiBaseUrl()}/api/accounts/${orgId}` + `${getClientApiBaseUrl()}/api/accounts/${orgId}`, ); if (response.ok) { const data = await response.json(); @@ -94,13 +94,14 @@ const useOrgSettings = (orgId: string | null) => { setImageUploading(true); try { - const { uri } = await uploadFile(file); + const accessToken = await getAccessToken(); + const { uri } = await uploadFile(file, accessToken); setImage(uri); } finally { setImageUploading(false); } }, - [] + [getAccessToken], ); const removeImage = useCallback(() => { @@ -118,10 +119,11 @@ const useOrgSettings = (orgId: string | null) => { setKnowledgeUploading(true); const newKnowledges: KnowledgeItem[] = []; try { + const accessToken = await getAccessToken(); for (const file of files) { const name = file.name; const type = getFileMimeType(file); - const { uri } = await uploadFile(file); + const { uri } = await uploadFile(file, accessToken); newKnowledges.push({ name, url: uri, type }); } setKnowledges((prev) => [...prev, ...newKnowledges]); @@ -132,7 +134,7 @@ const useOrgSettings = (orgId: string | null) => { } } }, - [] + [getAccessToken], ); const handleDeleteKnowledge = useCallback((index: number) => { @@ -158,12 +160,22 @@ const useOrgSettings = (orgId: string | null) => { knowledges, }); setOrgData(data); - await queryClient.invalidateQueries({ queryKey: ["accountOrganizations"] }); + await queryClient.invalidateQueries({ + queryKey: ["accountOrganizations"], + }); return true; } finally { setIsSaving(false); } - }, [orgId, name, image, instruction, knowledges, queryClient, getAccessToken]); + }, [ + orgId, + name, + image, + instruction, + knowledges, + queryClient, + getAccessToken, + ]); return { orgData, diff --git a/hooks/usePureFileAttachments.ts b/hooks/usePureFileAttachments.ts index 60228c39b..f0281ca12 100644 --- a/hooks/usePureFileAttachments.ts +++ b/hooks/usePureFileAttachments.ts @@ -1,5 +1,6 @@ import { useRef } from "react"; import { FileUIPart } from "ai"; +import { usePrivy } from "@privy-io/react-auth"; import { useVercelChatContext } from "@/providers/VercelChatProvider"; import { CHAT_INPUT_SUPPORTED_FILE } from "@/lib/chat/config"; import { isAllowedByExtension } from "@/lib/files/isAllowedByExtension"; @@ -8,6 +9,7 @@ import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; export function usePureFileAttachments() { const { setAttachments, addTextAttachment } = useVercelChatContext(); + const { getAccessToken } = usePrivy(); const fileInputRef = useRef(null); const MAX_FILES = 10; const allowedTypes = Object.keys(CHAT_INPUT_SUPPORTED_FILE); @@ -54,23 +56,22 @@ export function usePureFileAttachments() { setAttachments((prev: FileUIPart[]) => [...prev, pendingAttachment]); try { - // Upload the file to Arweave + const accessToken = await getAccessToken(); + if (!accessToken) throw new Error("Not authenticated"); + const formData = new FormData(); formData.append("file", file); const response = await fetch(`${getClientApiBaseUrl()}/api/upload`, { method: "POST", + headers: { Authorization: `Bearer ${accessToken}` }, body: formData, }); - if (!response.ok) { - throw new Error("Failed to upload file"); - } - - const data = await response.json(); + const data = await response.json().catch(() => null); - if (!data.success) { - throw new Error(data.error || "Upload failed"); + if (!response.ok || !data?.success) { + throw new Error(data?.error || "Upload failed"); } // Update the attachment with the Arweave URL diff --git a/hooks/useSaveKnowledgeEdit.ts b/hooks/useSaveKnowledgeEdit.ts index b5f3488b7..3f4586516 100644 --- a/hooks/useSaveKnowledgeEdit.ts +++ b/hooks/useSaveKnowledgeEdit.ts @@ -1,4 +1,5 @@ import { useArtistProvider } from "@/providers/ArtistProvider"; +import { usePrivy } from "@privy-io/react-auth"; import getMimeFromPath from "@/lib/files/getMimeFromPath"; import { uploadFile } from "@/lib/files/uploadFile"; import { toast } from "react-toastify"; @@ -14,6 +15,7 @@ export const useSaveKnowledgeEdit = ({ url, editedText, }: UseSaveKnowledgeEditArgs) => { + const { getAccessToken } = usePrivy(); const { knowledgeUploading, setKnowledgeUploading, @@ -33,8 +35,9 @@ export const useSaveKnowledgeEdit = ({ } try { setKnowledgeUploading(true); + const accessToken = await getAccessToken(); const file = new File([editedText], name || "file.txt", { type: mime }); - const { uri } = await uploadFile(file); + const { uri } = await uploadFile(file, accessToken); const next = bases.map((b) => ({ ...b })); const idx = next.findIndex((b) => b.url === url && b.name === name); if (idx >= 0) { diff --git a/hooks/useUser.tsx b/hooks/useUser.tsx index 642813c15..b12dd8314 100644 --- a/hooks/useUser.tsx +++ b/hooks/useUser.tsx @@ -40,7 +40,8 @@ const useUser = () => { return; } try { - const { uri } = await uploadFile(file); + const accessToken = await getAccessToken(); + const { uri } = await uploadFile(file, accessToken); setImage(uri); } catch { toast.error("Failed to upload image. Please try again."); diff --git a/lib/files/uploadFile.tsx b/lib/files/uploadFile.tsx index f5ee65f59..26291c8f7 100644 --- a/lib/files/uploadFile.tsx +++ b/lib/files/uploadFile.tsx @@ -5,20 +5,26 @@ export type UploadFileResponse = { uri: string; }; -export const uploadFile = async (file: File): Promise => { +export const uploadFile = async ( + file: File, + accessToken: string | null, +): Promise => { + if (!accessToken) throw new Error("Not authenticated"); + try { const data = new FormData(); data.set("file", file); const res = await fetch(`${getClientApiBaseUrl()}/api/upload`, { method: "POST", + headers: { Authorization: `Bearer ${accessToken}` }, body: data, }); - const json = await res.json(); + const json = await res.json().catch(() => null); - if (!json.success) { - throw new Error(json.error || "Upload failed"); + if (!res.ok || !json?.success) { + throw new Error(json?.error || "Upload failed"); } return { From 23e9e23eff15e0dcf19ca20e820e002bbd69eaa6 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Thu, 28 May 2026 05:37:46 +0530 Subject: [PATCH 6/6] chore(arweave): remove residual references after Supabase cutover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the migration to the dedicated /api/upload (api#540 + chat#1740 Bundle A), no chat code uploads to Arweave anymore. Removes the leftover code that still referenced it: - delete lib/generateAndProcessImage.ts (orphan helper; only file importing the `arweave` npm package, no internal consumers) - drop the `arweave` dep from package.json - rename TxtFileGenerationResult.arweaveUrl → txtUrl to match the MCP tool's actual response shape (which returns { success, txtUrl, message }); also drop the now-unused smartAccountAddress / transactionHash / blockExplorerUrl fields it never populates. The card was rendering nothing before this fix because the prop name didn't match. - strip the stale "e.g., arweave.net" example from the audio transcription prompt - update the obsolete "Arweave URL" comment in usePureFileAttachments Kept: next.config.mjs `arweave.net` entry in images.remotePatterns — legacy ar:// URLs persisted in the DB (chat history, account avatars predating the migration) still need to render via the api's arweaveGatewayUrl read-side helper. Co-Authored-By: Claude Opus 4.7 (1M context) --- components/ui/TxtFileResult.tsx | 23 +++++----- hooks/usePureFileAttachments.ts | 2 +- lib/generateAndProcessImage.ts | 77 --------------------------------- lib/prompts/getSystemPrompt.ts | 2 +- package.json | 1 - pnpm-lock.yaml | 37 ++-------------- 6 files changed, 15 insertions(+), 127 deletions(-) delete mode 100644 lib/generateAndProcessImage.ts diff --git a/components/ui/TxtFileResult.tsx b/components/ui/TxtFileResult.tsx index c5a5aafed..9b30aa62a 100644 --- a/components/ui/TxtFileResult.tsx +++ b/components/ui/TxtFileResult.tsx @@ -6,10 +6,7 @@ import { Download } from "lucide-react"; export interface TxtFileGenerationResult { success: boolean; - arweaveUrl: string | null; - smartAccountAddress?: string; - transactionHash?: string | null; - blockExplorerUrl?: string | null; + txtUrl: string | null; message?: string; error?: string; } @@ -24,12 +21,12 @@ export function TxtFileResult({ result }: TxtFileResultProps) { const [fetchError, setFetchError] = useState(null); useEffect(() => { - if (result.arweaveUrl && !fileContent) { + if (result.txtUrl && !fileContent) { setLoading(true); setFetchError(null); - fetch(result.arweaveUrl) + fetch(result.txtUrl) .then((res) => { - if (!res.ok) throw new Error("Failed to fetch file from Arweave"); + if (!res.ok) throw new Error("Failed to fetch file"); return res.text(); }) .then((text) => { @@ -42,7 +39,7 @@ export function TxtFileResult({ result }: TxtFileResultProps) { }); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [result.arweaveUrl]); + }, [result.txtUrl]); if (!result.success) { return ( @@ -60,13 +57,13 @@ export function TxtFileResult({ result }: TxtFileResultProps) { } const handleDownload = () => { - if (result.arweaveUrl) { - window.open(result.arweaveUrl, "_blank"); + if (result.txtUrl) { + window.open(result.txtUrl, "_blank"); } }; let displayText: string | JSX.Element = "TXT file generated."; - if (result.arweaveUrl) { + if (result.txtUrl) { if (loading) { displayText = "Loading file contents..."; } else if (fetchError) { @@ -88,7 +85,7 @@ export function TxtFileResult({ result }: TxtFileResultProps) { variant="outline" size="sm" onClick={handleDownload} - disabled={!result.arweaveUrl} + disabled={!result.txtUrl} className="h-8 px-3 text-xs rounded-xl ml-auto" > {" "} @@ -101,7 +98,7 @@ export function TxtFileResult({ result }: TxtFileResultProps) {
prev.map((attachment: FileUIPart) => // Compare by URL since object references won't match diff --git a/lib/generateAndProcessImage.ts b/lib/generateAndProcessImage.ts deleted file mode 100644 index 026ca30ce..000000000 --- a/lib/generateAndProcessImage.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Experimental_GenerateImageResult } from "ai"; -import Transaction from "arweave/node/lib/transaction"; -import { Address, Hash } from "viem"; -import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; - -interface RecoupImageGenerateResponse extends Experimental_GenerateImageResult { - imageUrl: string; - arweaveResult: Transaction; - moment: { - contractAddress: Address; - tokenId: string; - hash: Hash; - chainId: number; - }; -} - -export interface GeneratedImageResponse { - imageUrl: string | null; -} - -interface FileInput { - url: string; - type: string; -} - -export async function generateAndProcessImage( - prompt: string, - accountId: string, - files?: FileInput[] -): Promise { - if (!prompt) { - throw new Error("Prompt is required"); - } - - if (!accountId) { - throw new Error("Account ID is required"); - } - - const apiUrl = new URL(`${getClientApiBaseUrl()}/api/image/generate`); - apiUrl.searchParams.set("prompt", prompt); - apiUrl.searchParams.set("account_id", accountId); - - // Format files parameter: files=url1:type1|url2:type2 - if (files && files.length > 0) { - const filesParam = files - .map((file) => `${file.url}:${file.type}`) - .join("|"); - apiUrl.searchParams.set("files", filesParam); - } - - const response = await fetch(apiUrl.toString(), { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorText = await response.text(); - let errorMessage = `HTTP error! status: ${response.status}`; - - try { - const errorData = JSON.parse(errorText); - errorMessage = errorData.message || errorData.error || errorMessage; - } catch { - errorMessage = errorText || errorMessage; - } - - throw new Error(errorMessage); - } - - const data: RecoupImageGenerateResponse = await response.json(); - - return { - imageUrl: data.imageUrl || null, - }; -} diff --git a/lib/prompts/getSystemPrompt.ts b/lib/prompts/getSystemPrompt.ts index 49fef8f31..bed0cec9b 100644 --- a/lib/prompts/getSystemPrompt.ts +++ b/lib/prompts/getSystemPrompt.ts @@ -47,7 +47,7 @@ export async function getSystemPrompt({ - DO NOT ask the user for any information - you have everything you need **AUDIO TRANSCRIPTION INSTRUCTIONS:** - When the user shares an audio file URL (e.g., arweave.net, or any audio URL) and asks to transcribe, get lyrics, or analyze audio: + When the user shares an audio file URL and asks to transcribe, get lyrics, or analyze audio: **IMMEDIATELY call transcribe_audio with:** - audio_url: The audio URL from the message diff --git a/package.json b/package.json index fdba41814..6dbe75137 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "@vercel/blob": "^2.3.2", "ai": "6.0.0-beta.99", "apify-client": "^2.9.7", - "arweave": "^1.15.7", "class-variance-authority": "^0.7.1", "classnames": "^2.5.1", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eeabfdd37..d2fa9194f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -140,9 +140,6 @@ importers: apify-client: specifier: ^2.9.7 version: 2.20.0 - arweave: - specifier: ^1.15.7 - version: 1.15.7 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -4448,9 +4445,6 @@ packages: apify-client@2.20.0: resolution: {integrity: sha512-oEMTImVVRZ5n8JkFV6dgbBFL3Xqz+GTwjUCjn/hwSNkow31Q8VNGk4qYDfRjkoqNQJ3ZirhtCwTnhkSXn1Tf+g==} - arconnect@0.4.2: - resolution: {integrity: sha512-Jkpd4QL3TVqnd3U683gzXmZUVqBUy17DdJDuL/3D9rkysLgX6ymJ2e+sR+xyZF5Rh42CBqDXWNMmCjBXeP7Gbw==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -4513,13 +4507,6 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} - arweave@1.15.7: - resolution: {integrity: sha512-F+Y4iWU1qea9IsKQ/YNmLsY4DHQVsaJBuhEbFxQn9cfGHOmtXE+bwo14oY8xqymsqSNf/e1PeIfLk7G7qN/hVA==} - engines: {node: '>=18'} - - asn1.js@5.4.1: - resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} - asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} @@ -9263,7 +9250,7 @@ packages: uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@8.3.2: @@ -14867,7 +14854,7 @@ snapshots: '@vitest/pretty-format': 4.0.15 tinyrainbow: 3.0.3 - '@wagmi/connectors@6.2.0(34c7fdd01b715715548043cbba06572f)': + '@wagmi/connectors@6.2.0(znuhikr6vncq2ajqjte5hoasy4)': dependencies: '@base-org/account': 2.4.0(@types/react@18.3.27)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) '@coinbase/wallet-sdk': 4.3.6(@types/react@18.3.27)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) @@ -16182,10 +16169,6 @@ snapshots: transitivePeerDependencies: - debug - arconnect@0.4.2: - dependencies: - arweave: 1.15.7 - argparse@2.0.1: {} aria-hidden@1.2.6: @@ -16282,20 +16265,6 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 - arweave@1.15.7: - dependencies: - arconnect: 0.4.2 - asn1.js: 5.4.1 - base64-js: 1.5.1 - bignumber.js: 9.3.1 - - asn1.js@5.4.1: - dependencies: - bn.js: 4.12.2 - inherits: 2.0.4 - minimalistic-assert: 1.0.1 - safer-buffer: 2.1.2 - asn1@0.2.6: dependencies: safer-buffer: 2.1.2 @@ -22145,7 +22114,7 @@ snapshots: wagmi@2.19.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.1))(@types/react@18.3.27)(@vercel/blob@2.3.2)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): dependencies: '@tanstack/react-query': 5.90.12(react@19.2.1) - '@wagmi/connectors': 6.2.0(34c7fdd01b715715548043cbba06572f) + '@wagmi/connectors': 6.2.0(znuhikr6vncq2ajqjte5hoasy4) '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.12)(@types/react@18.3.27)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.41.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) react: 19.2.1 use-sync-external-store: 1.4.0(react@19.2.1)