-
Notifications
You must be signed in to change notification settings - Fork 0
feat : use private bucket for vizualizations #24
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| import { signUrl } from "@/lib/s3/signer"; | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
|
|
||
| export async function GET( | ||
| req: NextRequest, | ||
| context: { params: Promise<{ chatId: string }> } | ||
| ) { | ||
| try { | ||
| const { chatId } = await context.params; | ||
|
|
||
|
|
||
| const upstreamHeaders: Record<string, string> = {}; | ||
|
|
||
| req.headers.forEach((value, key) => { | ||
| if (key.toLowerCase() === "host") return; | ||
| upstreamHeaders[key] = value; | ||
| }); | ||
|
|
||
| // Ensure JSON if not provided | ||
| if (!upstreamHeaders["content-type"]) { | ||
| upstreamHeaders["content-type"] = "application/json"; | ||
| } | ||
|
|
||
| const queryString = req.nextUrl.search; | ||
|
|
||
| // Forward request to Go API | ||
| const gopieRes = await fetch( | ||
| `${process.env.GOPIE_API_URL}/v1/api/chat/${chatId}/messages${queryString}`, | ||
| { | ||
| method: "GET", | ||
| headers: upstreamHeaders, | ||
| cache: "no-store", | ||
| } | ||
| ); | ||
|
|
||
| if (!gopieRes.ok) { | ||
| const errorText = await gopieRes.text(); | ||
| console.error("GOPIE error:", errorText); | ||
| return NextResponse.json( | ||
| { error: "Upstream error" }, | ||
| { status: gopieRes.status } | ||
| ); | ||
| } | ||
|
|
||
| const json = await gopieRes.json(); | ||
| const data = json.data || []; | ||
|
|
||
| // ---- Mutate ONLY URLs (signing step) ---- | ||
| for (const msg of data) { | ||
| // upstream messages have msg.choices[].delta.tool_calls[] | ||
| if (!Array.isArray(msg.choices)) continue; | ||
|
|
||
| for (const choice of msg.choices) { | ||
| const delta = choice.delta; | ||
| if (!delta?.tool_calls) continue; | ||
|
|
||
| for (const tc of delta.tool_calls) { | ||
| // Only process function calls | ||
| if (!tc.function) continue; | ||
|
|
||
| const fn = tc.function; | ||
|
|
||
| // Parse args safely | ||
| let args; | ||
| try { | ||
| args = JSON.parse(fn.arguments || "{}"); | ||
| } catch { | ||
| continue; | ||
| } | ||
|
|
||
| // ---- visualization_result ---- | ||
| if (fn.name === "visualization_result") { | ||
| const arr = args.visualization_json_paths; | ||
| if (Array.isArray(arr)) { | ||
| for (const entry of arr) { | ||
| if (entry.json_path) { | ||
| entry.json_path = await signUrl(entry.json_path); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn.arguments = JSON.stringify(args); | ||
| } | ||
|
|
||
| // ---- visualization_paths ---- | ||
| if (fn.name === "visualization_paths") { | ||
| const arr = args.paths; | ||
| if (Array.isArray(arr)) { | ||
| args.paths = await Promise.all(arr.map((p) => signUrl(p))); | ||
| } | ||
|
|
||
| fn.arguments = JSON.stringify(args); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| return NextResponse.json(json); | ||
| } catch (err) { | ||
| console.error("Proxy error:", err); | ||
| return NextResponse.json( | ||
| { error: "Internal server error" }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,57 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // lib/s3-signer.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const s3 = new S3Client({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| region: process.env.S3_REGION || "us-east-1", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| endpoint: process.env.S3_ENDPOINT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| credentials: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accessKeyId: process.env.S3_ACCESS_KEY!, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| secretAccessKey: process.env.S3_SECRET_KEY!, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| forcePathStyle: true, // for MinIO | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+6
to
+14
Contributor
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 required environment variables before use. The non-null assertions on Apply this diff to add validation: +// Validate required environment variables
+const requiredEnvVars = {
+ S3_ACCESS_KEY: process.env.S3_ACCESS_KEY,
+ S3_SECRET_KEY: process.env.S3_SECRET_KEY,
+ S3_ENDPOINT: process.env.S3_ENDPOINT,
+};
+
+for (const [key, value] of Object.entries(requiredEnvVars)) {
+ if (!value) {
+ throw new Error(`Missing required environment variable: ${key}`);
+ }
+}
+
const s3 = new S3Client({
region: process.env.S3_REGION || "us-east-1",
- endpoint: process.env.S3_ENDPOINT,
+ endpoint: process.env.S3_ENDPOINT!,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY!,
secretAccessKey: process.env.S3_SECRET_KEY!,
},
forcePathStyle: true, // for MinIO
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function normalizePath(raw: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If it's a full URL, extract only the path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = new URL(raw); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return url.pathname; // e.g. "/gopie/visualizations/file.json" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Not a URL — return as-is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return raw; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ---- Signs a single path (e.g. `/mybucket/reports/viz.png`) ---- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function signUrl(rawPath: string): Promise<string> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!rawPath) return rawPath; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Strip host if present | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const normalized = normalizePath(rawPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Ensure no leading slash | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cleaned = normalized.startsWith("/") ? normalized.slice(1) : normalized; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const slashIndex = cleaned.indexOf("/"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (slashIndex === -1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("signUrl error: Invalid path, no bucket/key", cleaned); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return rawPath; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const bucket = cleaned.substring(0, slashIndex); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const key = cleaned.substring(slashIndex + 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const command = new GetObjectCommand({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Bucket: bucket, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Key: key, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return await getSignedUrl(s3, command, { expiresIn: 86400 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("signUrl error:", err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return rawPath; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+57
Contributor
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. Consider throwing errors instead of silently returning the original path. The current implementation returns the original Consider one of these approaches: Option 1: Throw errors for genuine failures (recommended) export async function signUrl(rawPath: string): Promise<string> {
- try {
- if (!rawPath) return rawPath;
+ if (!rawPath) return rawPath;
- // Strip host if present
- const normalized = normalizePath(rawPath);
+ // Strip host if present
+ const normalized = normalizePath(rawPath);
- // Ensure no leading slash
- const cleaned = normalized.startsWith("/") ? normalized.slice(1) : normalized;
+ // Ensure no leading slash
+ const cleaned = normalized.startsWith("/") ? normalized.slice(1) : normalized;
- const slashIndex = cleaned.indexOf("/");
- if (slashIndex === -1) {
- console.error("signUrl error: Invalid path, no bucket/key", cleaned);
- return rawPath;
- }
+ const slashIndex = cleaned.indexOf("/");
+ if (slashIndex === -1) {
+ throw new Error(`Invalid S3 path format (no bucket/key): ${cleaned}`);
+ }
- const bucket = cleaned.substring(0, slashIndex);
- const key = cleaned.substring(slashIndex + 1);
+ const bucket = cleaned.substring(0, slashIndex);
+ const key = cleaned.substring(slashIndex + 1);
+ try {
const command = new GetObjectCommand({
Bucket: bucket,
Key: key,
});
return await getSignedUrl(s3, command, { expiresIn: 86400 });
} catch (err) {
- console.error("signUrl error:", err);
- return rawPath;
+ console.error("Failed to sign S3 URL:", err);
+ throw new Error(`Failed to sign S3 URL for ${bucket}/${key}: ${err}`);
}
}Option 2: Log warnings for debugging If silent fallback is intentional, at least log at warning level: } catch (err) {
- console.error("signUrl error:", err);
+ console.warn("Failed to sign S3 URL, returning original path:", err);
return rawPath;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
Add missing semicolon for consistency.
Line 110 is missing a semicolon after the function call, which is inconsistent with the rest of the codebase style.
Apply this diff:
📝 Committable suggestion
🤖 Prompt for AI Agents