Skip to content
Merged
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
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
"@streamdown/math": "^1.0.2",
"@streamdown/mermaid": "^1.0.2",
"@supabase/supabase-js": "^2.101.1",
"@tanstack/pacer": "^0.20.1",
"@tanstack/react-pacer": "^0.21.1",
Comment on lines +89 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check latest versions of TanStack pacer packages

echo "=== Checking `@tanstack/pacer` ==="
npm view `@tanstack/pacer` versions --json | jq -r '.[-5:]' 
npm view `@tanstack/pacer` version

echo -e "\n=== Checking `@tanstack/react-pacer` ==="
npm view `@tanstack/react-pacer` versions --json | jq -r '.[-5:]'
npm view `@tanstack/react-pacer` version

echo -e "\n=== Checking peer dependencies for react-pacer ==="
npm view `@tanstack/react-pacer`@0.21.1 peerDependencies

echo -e "\n=== Checking for security advisories ==="
npm audit --package-lock-only 2>/dev/null || echo "Run npm audit after installing to check for vulnerabilities"

Repository: ThinkEx-OSS/thinkex

Length of output: 1790


🌐 Web query:

@tanstack/pacer latest version npm registry

💡 Result:

Latest version of @tanstack/pacer on the npm registry is 0.15.1. [1]


🌐 Web query:

@tanstack/react-pacer 0.21.1 peer dependencies compatibility

💡 Result:

@tanstack/react-pacer@0.21.1 does not appear to be a published version of @tanstack/react-pacer (the CDN listing shows versions up to 0.20.0). [1]

For the latest available version shown there (@tanstack/react-pacer@0.20.0), the peerDependencies are: [2]

  • react: >=16.8
  • react-dom: >=16.8

This matches TanStack Pacer’s docs, which state it’s compatible with React v16.8+. [3]

Sources: [1] [2] [3]


Fix invalid TanStack pacer package versions in package.json.

The specified versions cannot be installed:

  • @tanstack/pacer@0.20.1 exceeds the latest published version (0.15.1)
  • @tanstack/react-pacer@0.21.1 does not exist as a published version (latest is 0.20.0)

Update these to valid published versions. If 0.20.1 and 0.21.1 are intended, verify they exist or use the available versions instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 89 - 90, The package.json lists invalid TanStack
pacer versions; update "@tanstack/pacer" and "@tanstack/react-pacer" to
published versions (e.g., set "@tanstack/pacer" to a valid published release
such as ^0.15.1 and "@tanstack/react-pacer" to ^0.20.0) or, if 0.20.1 / 0.21.1
were intended, verify those specific versions exist on the registry and replace
with the correct semver; ensure the package names "@tanstack/pacer" and
"@tanstack/react-pacer" in package.json are changed accordingly and then run
npm/yarn install to confirm resolution.

"@tanstack/react-query": "^5.96.1",
"@tanstack/react-query-devtools": "^5.96.1",
"@tiptap/core": "3.22.2",
Expand All @@ -108,7 +110,7 @@
"@vercel/otel": "^2.1.1",
"ai": "^6.0.146",
"assistant-stream": "^0.3.10",
"better-auth": "^1.5.6",
"better-auth": "^1.6.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.1.1",
Expand Down Expand Up @@ -160,6 +162,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.1",
"@types/bun": "^1.3.11",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for Bun-specific API usage

echo "=== Searching for Bun API usage ==="
rg -n '\bBun\.' --type ts --type tsx --type js --type jsx -C 2

echo -e "\n=== Searching for Bun imports ==="
rg -n 'from ["\']bun' --type ts --type tsx --type js --type jsx

echo -e "\n=== Checking for bun runtime references in scripts or config ==="
rg -n '\bbun\b' package.json tsconfig.json next.config.* vitest.config.* 2>/dev/null || echo "No bun references found in config files"

Repository: ThinkEx-OSS/thinkex

Length of output: 248


🏁 Script executed:

#!/bin/bash
# Search for Bun-specific API usage with corrected syntax

echo "=== Searching for Bun API usage ==="
rg -n '\bBun\.' -C 2 || echo "No Bun API calls found"

echo -e "\n=== Searching for bun imports ==="
rg -n "from ['\"]bun" || echo "No bun imports found"

echo -e "\n=== Searching for bun in package.json context ==="
rg -n "bun" package.json || echo "No bun references in package.json"

echo -e "\n=== Checking if tests use Bun ==="
find . -name "*.test.*" -o -name "*.spec.*" | head -20 | xargs grep -l "bun" 2>/dev/null || echo "No Bun references in test files"

echo -e "\n=== Listing dependencies in package.json around line 165 ==="
sed -n '160,170p' package.json

Repository: ThinkEx-OSS/thinkex

Length of output: 697


Remove unused @types/bun devDependency.

The codebase contains no Bun API usage, imports, or references. @types/bun is an unused devDependency and should be removed from package.json (line 165).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 165, Remove the unused devDependency "@types/bun" from
package.json (delete the "@types/bun" entry), then update your lockfile by
running the package manager (npm/yarn/pnpm install) to persist the change and
run the test suite/lint to ensure nothing depended on that type package;
reference the "@types/bun" entry to locate the line to remove.

"@types/lodash.throttle": "^4.1.9",
"@types/node": "^25.3.0",
"@types/pg": "^8.16.0",
Expand Down
148 changes: 58 additions & 90 deletions src/app/api/audio/process/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { start } from "workflow/api";
import { audioTranscribeWorkflow } from "@/workflows/audio-transcribe";
import {
requireAuth,
verifyWorkspaceAccess,
withErrorHandling,
} from "@/lib/api/workspace-helpers";
import { isAllowedAssetUrl } from "@/lib/tasks/validate-asset-url";

export const dynamic = "force-dynamic";

Expand All @@ -11,106 +15,70 @@ export const dynamic = "force-dynamic";
* Receives an audio file URL, runs a durable workflow to download, upload to Gemini,
* and transcribe. Returns structured transcript + summary.
*/
export async function POST(req: NextRequest) {
try {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
async function handlePOST(req: NextRequest) {
const userId = await requireAuth();

if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
return NextResponse.json(
{ error: "GOOGLE_GENERATIVE_AI_API_KEY is not set" },
{ status: 500 }
);
}

const body = await req.json();
const { fileUrl, filename, mimeType, itemId, workspaceId } = body;

if (!fileUrl) {
return NextResponse.json(
{ error: "fileUrl is required" },
{ status: 400 }
);
}
if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
return NextResponse.json(
{ error: "GOOGLE_GENERATIVE_AI_API_KEY is not set" },
{ status: 500 },
);
}

if (!itemId || typeof itemId !== "string") {
return NextResponse.json(
{ error: "itemId is required for polling" },
{ status: 400 }
);
}
const body = await req.json();
const { fileUrl, filename, mimeType, itemId, workspaceId } = body;
Comment on lines +28 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Return 400 for malformed or non-object JSON bodies.

Line 28 can throw on invalid JSON, and Line 29 will throw for a null payload. Because withErrorHandling() maps those exceptions to a generic 500, bad requests get reported as server failures instead of 400s.

💡 Suggested fix
-  const body = await req.json();
-  const { fileUrl, filename, mimeType, itemId, workspaceId } = body;
+  let body: unknown;
+  try {
+    body = await req.json();
+  } catch {
+    return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
+  }
+
+  if (!body || typeof body !== "object" || Array.isArray(body)) {
+    return NextResponse.json(
+      { error: "JSON object body is required" },
+      { status: 400 },
+    );
+  }
+
+  const { fileUrl, filename, mimeType, itemId, workspaceId } =
+    body as Record<string, unknown>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/audio/process/route.ts` around lines 28 - 29, The handler
currently does "const body = await req.json()" and immediately destructures {
fileUrl, filename, mimeType, itemId, workspaceId }, which will throw on invalid
JSON or when body is null and get mapped to a 500; instead wrap the req.json()
call in a try/catch and validate the parsed value is a non-null object before
destructuring, returning a 400 response (or throwing a 400 HTTP error) for
malformed JSON or non-object bodies; update the logic around req.json and the
destructuring to perform this check and return the appropriate 400 error for bad
requests.


if (!workspaceId || typeof workspaceId !== "string") {
return NextResponse.json(
{ error: "workspaceId is required" },
{ status: 400 }
);
}
if (!fileUrl || typeof fileUrl !== "string") {
return NextResponse.json(
{ error: "fileUrl is required" },
{ status: 400 },
);
}

// Validate URL origin to prevent SSRF
const allowedHosts: string[] = [];
if (process.env.NEXT_PUBLIC_SUPABASE_URL) {
allowedHosts.push(new URL(process.env.NEXT_PUBLIC_SUPABASE_URL).hostname);
}
if (process.env.NEXT_PUBLIC_APP_URL) {
allowedHosts.push(new URL(process.env.NEXT_PUBLIC_APP_URL).hostname);
}
if (process.env.NODE_ENV === "development") {
allowedHosts.push("localhost"); // local storage in dev
}
if (!itemId || typeof itemId !== "string") {
return NextResponse.json(
{ error: "itemId is required for polling" },
{ status: 400 },
);
}

let parsedUrl: URL;
try {
parsedUrl = new URL(fileUrl);
} catch {
return NextResponse.json({ error: "Invalid fileUrl" }, { status: 400 });
}
if (!workspaceId || typeof workspaceId !== "string") {
return NextResponse.json(
{ error: "workspaceId is required" },
{ status: 400 },
);
}

if (
!allowedHosts.some(
(host) =>
parsedUrl.hostname === host || parsedUrl.hostname.endsWith(`.${host}`)
)
) {
return NextResponse.json(
{ error: "fileUrl origin is not allowed" },
{ status: 400 }
);
}
if (!isAllowedAssetUrl(fileUrl)) {
return NextResponse.json(
{ error: "fileUrl origin is not allowed" },
{ status: 400 },
);
}

const audioMimeType = mimeType || guessMimeType(filename || fileUrl);
await verifyWorkspaceAccess(workspaceId, userId, "editor");

const userId = session.user.id;
const audioMimeType = mimeType || guessMimeType(filename || fileUrl);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate filename and mimeType before deriving audioMimeType.

Line 61 trusts both optional fields as-is. A truthy non-string filename will throw inside guessMimeType(), and a truthy non-string mimeType is passed straight into the workflow. That makes malformed requests turn into 500s downstream instead of a local 400.

💡 Suggested fix
+  if (filename != null && typeof filename !== "string") {
+    return NextResponse.json(
+      { error: "filename must be a string" },
+      { status: 400 },
+    );
+  }
+
+  if (mimeType != null && typeof mimeType !== "string") {
+    return NextResponse.json(
+      { error: "mimeType must be a string" },
+      { status: 400 },
+    );
+  }
+
-  const audioMimeType = mimeType || guessMimeType(filename || fileUrl);
+  const audioMimeType = mimeType ?? guessMimeType(filename ?? fileUrl);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/audio/process/route.ts` at line 61, Validate the incoming
filename and mimeType before computing audioMimeType: ensure typeof filename ===
'string' before passing to guessMimeType(filename) and ensure typeof mimeType
=== 'string' before using it; compute audioMimeType = (typeof mimeType ===
'string' ? mimeType : (typeof filename === 'string' ? guessMimeType(filename) :
undefined)); if audioMimeType is still falsy or not a string/known mime, return
a 400 bad request (instead of proceeding) so downstream code in this
route/handler doesn't receive malformed values; reference the variables
filename, mimeType, audioMimeType and the helper function guessMimeType when
making the changes.


// Start durable workflow; return immediately for client to poll
const run = await start(audioTranscribeWorkflow, [
fileUrl,
audioMimeType,
workspaceId,
itemId,
userId,
]);
const run = await start(audioTranscribeWorkflow, [
fileUrl,
audioMimeType,
workspaceId,
itemId,
userId,
]);

return NextResponse.json({
runId: run.runId,
itemId,
});
} catch (error: unknown) {
console.error("[AUDIO_PROCESS] Error:", error);
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "Failed to process audio",
},
{ status: 500 }
);
}
return NextResponse.json({
runId: run.runId,
itemId,
});
}

export const POST = withErrorHandling(
handlePOST,
"POST /api/audio/process",
);

function guessMimeType(filenameOrUrl: string): string {
const lower = filenameOrUrl.toLowerCase();
if (lower.endsWith(".mp3")) return "audio/mp3";
Expand Down
33 changes: 21 additions & 12 deletions src/app/api/workspaces/[id]/events/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { hasDuplicateName } from "@/lib/workspace/unique-name";
import { db, workspaceEvents } from "@/lib/db/client";
import { eq, sql } from "drizzle-orm";
import {
requireAuth,
requireAuthWithUserInfo,
verifyWorkspaceAccess,
withErrorHandling,
} from "@/lib/api/workspace-helpers";
Expand All @@ -25,11 +25,11 @@ async function handleGET(
{ params }: { params: Promise<{ id: string }> },
) {
const paramsPromise = params;
const authPromise = requireAuth();
const authPromise = requireAuthWithUserInfo();

const paramsResolved = await paramsPromise;
const id = paramsResolved.id;
const userId = await authPromise;
const { userId } = await authPromise;
await verifyWorkspaceAccess(id, userId, "viewer");
const eventCountResult = await db
.select({ count: sql<number>`count(*)::int` })
Expand Down Expand Up @@ -153,36 +153,45 @@ async function handlePOST(
const startTime = Date.now();
const timings: Record<string, number> = {};

// Start independent operations in parallel
const paramsPromise = params;
const authPromise = requireAuth();
const authPromise = requireAuthWithUserInfo();
const bodyPromise = request.json();

const paramsResolved = await paramsPromise;
const id = paramsResolved.id;

const authStart = Date.now();
const userId = await authPromise;
const { userId, name: userName } = await authPromise;
timings.auth = Date.now() - authStart;

const bodyStart = Date.now();
const body = await bodyPromise;
timings.bodyParse = Date.now() - bodyStart;
const { event, baseVersion } = body;

if (!event || baseVersion === undefined || isNaN(baseVersion)) {
const { event: rawEvent, baseVersion } = body ?? {};
if (
!rawEvent ||
typeof rawEvent.type !== "string" ||
typeof rawEvent.id !== "string" ||
baseVersion === undefined ||
isNaN(baseVersion)
) {
Comment on lines +171 to +178

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Require baseVersion to be an actual number.

isNaN(baseVersion) coerces before checking, so "1" and even "" pass this guard. That lets malformed requests reach appendWorkspaceEventWithBaseVersion with a non-numeric version. Prefer a strict numeric check here.

Suggested fix
   if (
     !rawEvent ||
     typeof rawEvent.type !== "string" ||
     typeof rawEvent.id !== "string" ||
-    baseVersion === undefined ||
-    isNaN(baseVersion)
+    typeof baseVersion !== "number" ||
+    !Number.isFinite(baseVersion)
   ) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { event: rawEvent, baseVersion } = body ?? {};
if (
!rawEvent ||
typeof rawEvent.type !== "string" ||
typeof rawEvent.id !== "string" ||
baseVersion === undefined ||
isNaN(baseVersion)
) {
const { event: rawEvent, baseVersion } = body ?? {};
if (
!rawEvent ||
typeof rawEvent.type !== "string" ||
typeof rawEvent.id !== "string" ||
typeof baseVersion !== "number" ||
!Number.isFinite(baseVersion)
) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/workspaces/`[id]/events/route.ts around lines 171 - 178, The
current guard uses isNaN(baseVersion) which coerces strings to numbers and
allows values like "1" or "" through; update the validation to require
baseVersion to be a real number by checking typeof baseVersion === "number" and
that it is not NaN (e.g., use Number.isNaN or Number.isFinite as appropriate).
Replace the existing baseVersion checks in the route (the block that currently
uses isNaN(baseVersion)) so malformed string versions are rejected before
calling appendWorkspaceEventWithBaseVersion.

return NextResponse.json(
{ error: "Event and valid baseVersion are required" },
{ status: 400 },
);
}

// Check if user has editor access (owner or editor collaborator)
const event: WorkspaceEvent = {
...rawEvent,
userId,
userName,
};

const workspaceCheckStart = Date.now();
await verifyWorkspaceAccess(id, userId, "editor");
timings.workspaceCheck = Date.now() - workspaceCheckStart;

// Validate unique name for ITEM_CREATED and ITEM_UPDATED (when name changes)
if (event.type === "ITEM_CREATED") {
const item = event.payload?.item;
if (item?.name != null && item?.type) {
Expand All @@ -201,13 +210,13 @@ async function handlePOST(
}
if (event.type === "ITEM_UPDATED" && event.payload?.changes?.name != null) {
const itemId = event.payload?.id;
const newName = event.payload.changes.name;
const newName = event.payload.changes.name as string;
if (itemId && newName) {
const state = await loadWorkspaceState(id, { userId });
const existingItem = state.find((i: { id: string }) => i.id === itemId);
if (existingItem) {
const newFolderId =
event.payload.changes.folderId ?? existingItem.folderId ?? null;
(event.payload.changes as Record<string, unknown>).folderId as string | undefined ?? existingItem.folderId ?? null;
Comment on lines +213 to +219

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Folder-only moves still bypass duplicate-name validation.

This block now computes folderId, but it only runs when changes.name is present. An ITEM_UPDATED that changes only folderId can still move an item into a folder that already contains the same name/type, breaking the uniqueness invariant. Run this check when either name or folderId changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/workspaces/`[id]/events/route.ts around lines 213 - 219, The
duplicate-name validation currently runs only when changes.name is present;
update the conditional to run when either event.payload.changes.name or
event.payload.changes.folderId is present (e.g., check newName ||
folderIdChange) so moves that only change folderId also trigger validation, and
compute newFolderId using (event.payload.changes as Record<string,
unknown>).folderId as string | undefined along with the resolved name (use
newName if present else existingItem.name) before checking for duplicates;
adjust the check around loadWorkspaceState, existingItem, and newFolderId
accordingly (references: newName, itemId, loadWorkspaceState, existingItem,
newFolderId, event.payload.changes).

if (
hasDuplicateName(
state,
Expand Down
Loading
Loading