Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 0 additions & 39 deletions app/api/upload/route.ts

This file was deleted.

23 changes: 10 additions & 13 deletions components/ui/TxtFileResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -24,12 +21,12 @@ export function TxtFileResult({ result }: TxtFileResultProps) {
const [fetchError, setFetchError] = useState<string | null>(null);

useEffect(() => {
if (result.arweaveUrl && !fileContent) {
if (result.txtUrl && !fileContent) {
setLoading(true);
setFetchError(null);
Comment on lines +24 to 26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A new txtUrl can be ignored because the fetch is blocked when prior fileContent exists, causing stale file preview data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/ui/TxtFileResult.tsx, line 24:

<comment>A new `txtUrl` can be ignored because the fetch is blocked when prior `fileContent` exists, causing stale file preview data.</comment>

<file context>
@@ -24,12 +21,12 @@ export function TxtFileResult({ result }: TxtFileResultProps) {
 
   useEffect(() => {
-    if (result.arweaveUrl && !fileContent) {
+    if (result.txtUrl && !fileContent) {
       setLoading(true);
       setFetchError(null);
</file context>
Suggested change
if (result.txtUrl && !fileContent) {
setLoading(true);
setFetchError(null);
if (result.txtUrl) {
setFileContent(null);
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) => {
Expand All @@ -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 (
Expand All @@ -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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Use noopener,noreferrer when opening external URLs in a new tab to avoid opener-based tabnabbing risks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/ui/TxtFileResult.tsx, line 61:

<comment>Use `noopener,noreferrer` when opening external URLs in a new tab to avoid opener-based tabnabbing risks.</comment>

<file context>
@@ -60,13 +57,13 @@ export function TxtFileResult({ result }: TxtFileResultProps) {
-    if (result.arweaveUrl) {
-      window.open(result.arweaveUrl, "_blank");
+    if (result.txtUrl) {
+      window.open(result.txtUrl, "_blank");
     }
   };
</file context>
Suggested change
window.open(result.txtUrl, "_blank");
window.open(result.txtUrl, "_blank", "noopener,noreferrer");

}
Comment on lines +60 to 62

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the reported file around the target lines
echo "== TxtFileResult.tsx (around lines 40-90) =="
sed -n '40,90p' components/ui/TxtFileResult.tsx | cat -n

echo
echo "== Search for window.open in the repo (limit output) =="
rg -n "window\.open\(" --glob='**/*.{ts,tsx,js,jsx}' -S . || true

echo
echo "== Search for noopener/noreferrer usage patterns =="
rg -n "noopener|noreferrer" --glob='**/*.{ts,tsx,js,jsx}' -S components . || true

Repository: recoupable/chat

Length of output: 6895


Harden external window.open calls against reverse-tabnabbing.

components/ui/TxtFileResult.tsx opens result.txtUrl with window.open(result.txtUrl, "_blank") without noopener,noreferrer.

💡 Proposed fix
-      window.open(result.txtUrl, "_blank");
+      window.open(result.txtUrl, "_blank", "noopener,noreferrer");

Also found an unprotected window.open in components/VercelChat/dialogs/tasks/TaskRecentRunsSection.tsx ("_blank" without noopener/noreferrer).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/ui/TxtFileResult.tsx` around lines 60 - 62, The window.open calls
in the TxtFileResult component (window.open(result.txtUrl, "_blank")) and in
TaskRecentRunsSection that open links with "_blank" are vulnerable to
reverse-tabnabbing; change each call to include noopener and noreferrer and null
out the opener as a fallback—e.g., replace window.open(url, "_blank") with:
const w = window.open(url, "_blank", "noopener,noreferrer"); if (w) w.opener =
null;—update the calls in the TxtFileResult component (result.txtUrl) and the
TaskRecentRunsSection window.open usage accordingly.

};

let displayText: string | JSX.Element = "TXT file generated.";
if (result.arweaveUrl) {
if (result.txtUrl) {
if (loading) {
displayText = "Loading file contents...";
} else if (fetchError) {
Expand All @@ -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"
>
<Download className="w-4 h-4" />{" "}
Expand All @@ -101,7 +98,7 @@ export function TxtFileResult({ result }: TxtFileResultProps) {
<div
className={cn(
"mb-4 whitespace-pre-wrap font-mono text-sm p-3 bg-muted/50 rounded-md overflow-auto",
"max-h-[200px] md:max-h-[400px] scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-600"
"max-h-[200px] md:max-h-[400px] scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-600",
)}
style={{
transition: "max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
Expand Down
18 changes: 11 additions & 7 deletions hooks/useArtistSetting.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { uploadFile } from "@/lib/arweave/uploadFile";
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<HTMLInputElement>(null);
const baseRef = useRef<HTMLInputElement>(null);
const [image, setImage] = useState("");
Expand All @@ -25,7 +27,7 @@ const useArtistSetting = () => {
const [knowledgeUploading, setKnowledgeUploading] = useState(false);
const [question, setQuestion] = useState("");
const [editableArtist, setEditableArtist] = useState<ArtistRecord | null>(
null
null,
);

const handleDeleteKnowledge = (index: number) => {
Expand All @@ -35,7 +37,7 @@ const useArtistSetting = () => {
};

const handleImageSelected = async (
e: React.ChangeEvent<HTMLInputElement>
e: React.ChangeEvent<HTMLInputElement>,
) => {
setImageUploading(true);
const file = e.target.files?.[0];
Expand All @@ -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);
Expand All @@ -53,17 +56,18 @@ const useArtistSetting = () => {
};

const handleKnowledgesSelected = async (
e: React.ChangeEvent<HTMLInputElement>
e: React.ChangeEvent<HTMLInputElement>,
) => {
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,
Expand Down Expand Up @@ -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 || "");
});
Expand Down
30 changes: 21 additions & 9 deletions hooks/useOrgSettings.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
Expand All @@ -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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Handle the null-token case before uploading knowledges to avoid throwing from uploadFile mid-loop.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useOrgSettings.ts, line 98:

<comment>Handle the null-token case before uploading knowledges to avoid throwing from `uploadFile` mid-loop.</comment>

<file context>
@@ -94,13 +94,14 @@ const useOrgSettings = (orgId: string | null) => {
       try {
-        const { uri } = await uploadFile(file);
+        const accessToken = await getAccessToken();
+        const { uri } = await uploadFile(file, accessToken);
         setImage(uri);
       } finally {
</file context>

setImage(uri);
} finally {
setImageUploading(false);
}
},
[]
[getAccessToken],
);

const removeImage = useCallback(() => {
Expand All @@ -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]);
Expand All @@ -132,7 +134,7 @@ const useOrgSettings = (orgId: string | null) => {
}
}
},
[]
[getAccessToken],
);

const handleDeleteKnowledge = useCallback((index: number) => {
Expand All @@ -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,
Expand Down
28 changes: 15 additions & 13 deletions hooks/usePureFileAttachments.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
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";
import { getFileExtension } from "@/lib/files/getFileExtension";
import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl";

export function usePureFileAttachments() {
const { setAttachments, addTextAttachment } = useVercelChatContext();
const { getAccessToken } = usePrivy();
const fileInputRef = useRef<HTMLInputElement>(null);
const MAX_FILES = 10;
const allowedTypes = Object.keys(CHAT_INPUT_SUPPORTED_FILE);
Expand Down Expand Up @@ -53,26 +56,25 @@ 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("/api/upload", {
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
// Update the attachment with the uploaded URL
setAttachments((prev: FileUIPart[]) =>
prev.map((attachment: FileUIPart) =>
// Compare by URL since object references won't match
Expand All @@ -83,8 +85,8 @@ export function usePureFileAttachments() {
mediaType: data.fileType,
url: data.url,
} as FileUIPart)
: attachment
)
: attachment,
),
);

// Revoke the temporary object URL to avoid memory leaks
Expand All @@ -93,7 +95,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);
Expand Down
7 changes: 5 additions & 2 deletions hooks/useSaveKnowledgeEdit.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useArtistProvider } from "@/providers/ArtistProvider";
import { usePrivy } from "@privy-io/react-auth";
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 = {
Expand All @@ -14,6 +15,7 @@ export const useSaveKnowledgeEdit = ({
url,
editedText,
}: UseSaveKnowledgeEditArgs) => {
const { getAccessToken } = usePrivy();
const {
knowledgeUploading,
setKnowledgeUploading,
Expand All @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions hooks/useUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.");
Expand Down
Loading
Loading