-
Notifications
You must be signed in to change notification settings - Fork 11
Feat/improvements #322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/improvements #322
Changes from all commits
8e901c7
9bac0d7
fea3341
3344194
763b405
2d88e0c
aba1a4d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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", | ||
| "@tanstack/react-query": "^5.96.1", | ||
| "@tanstack/react-query-devtools": "^5.96.1", | ||
| "@tiptap/core": "3.22.2", | ||
|
|
@@ -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", | ||
|
|
@@ -160,6 +162,7 @@ | |
| }, | ||
| "devDependencies": { | ||
| "@tailwindcss/postcss": "^4.2.1", | ||
| "@types/bun": "^1.3.11", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.jsonRepository: ThinkEx-OSS/thinkex Length of output: 697 Remove unused The codebase contains no Bun API usage, imports, or references. 🤖 Prompt for AI Agents |
||
| "@types/lodash.throttle": "^4.1.9", | ||
| "@types/node": "^25.3.0", | ||
| "@types/pg": "^8.16.0", | ||
|
|
||
| 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"; | ||
|
|
||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Return 400 for malformed or non-object JSON bodies. Line 28 can throw on invalid JSON, and Line 29 will throw for a 💡 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 |
||
|
|
||
| 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate Line 61 trusts both optional fields as-is. A truthy non-string 💡 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 |
||
|
|
||
| // 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"; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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"; | ||||||||||||||||||||||||||||||||||
|
|
@@ -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` }) | ||||||||||||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Require
Suggested fix if (
!rawEvent ||
typeof rawEvent.type !== "string" ||
typeof rawEvent.id !== "string" ||
- baseVersion === undefined ||
- isNaN(baseVersion)
+ typeof baseVersion !== "number" ||
+ !Number.isFinite(baseVersion)
) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| 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) { | ||||||||||||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Folder-only moves still bypass duplicate-name validation. This block now computes 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||
| hasDuplicateName( | ||||||||||||||||||||||||||||||||||
| state, | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: ThinkEx-OSS/thinkex
Length of output: 1790
🌐 Web query:
@tanstack/pacer latest version npm registry💡 Result:
Latest version of
@tanstack/paceron the npm registry is0.15.1. [1]🌐 Web query:
@tanstack/react-pacer 0.21.1 peer dependencies compatibility💡 Result:
@tanstack/react-pacer@0.21.1does not appear to be a published version of@tanstack/react-pacer(the CDN listing shows versions up to0.20.0). [1]For the latest available version shown there (
@tanstack/react-pacer@0.20.0), the peerDependencies are: [2]react:>=16.8react-dom:>=16.8This 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.1exceeds the latest published version (0.15.1)@tanstack/react-pacer@0.21.1does not exist as a published version (latest is0.20.0)Update these to valid published versions. If
0.20.1and0.21.1are intended, verify they exist or use the available versions instead.🤖 Prompt for AI Agents