6f108237d4
* test preview bypass 2 Co-authored-by: Cursor <cursoragent@cursor.com> * add apiFetch wrapper with Vercel bypass via query param + header the template workflow was missing VERCEL_AUTOMATION_BYPASS_SECRET, so all action API calls to preview deployments hit Vercel's deployment protection without bypass. this also consolidates the bypass logic into a single fetch wrapper that applies the secret as both a query parameter (matching server-side forwarding) and a header for belt-and-suspenders reliability. Co-authored-by: Cursor <cursoragent@cursor.com> * security hardening for Vercel bypass - redact bypass token from webhook forwarder logs and response body - remove dead x-preview-api-forward header - refactor getAllSecrets() to use SENSITIVE_PATTERNS instead of hardcoded list - enforce https:// on API_URL (localhost exempt for local dev) Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
72 lines
2.4 KiB
TypeScript
72 lines
2.4 KiB
TypeScript
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import { type } from "arktype";
|
|
import { fileTypeFromBuffer } from "file-type";
|
|
import { apiFetch } from "../utils/apiFetch.ts";
|
|
import type { ToolContext } from "./server.ts";
|
|
import { execute, tool } from "./shared.ts";
|
|
|
|
const UploadFileParams = type({
|
|
path: type.string.describe("absolute path to file to upload"),
|
|
});
|
|
|
|
export function UploadFileTool(ctx: ToolContext) {
|
|
return tool({
|
|
name: "upload_file",
|
|
description:
|
|
"upload a file to get a permanent public URL. use for screenshots, artifacts, or any files you want to reference in PRs/comments. max 10MB, images/text/archives allowed.",
|
|
parameters: UploadFileParams,
|
|
execute: execute(async (params) => {
|
|
// read file from disk eagerly on purpose to avoid its content being changed by the time it's uploaded
|
|
const buffer = fs.readFileSync(params.path);
|
|
const filename = path.basename(params.path);
|
|
const contentLength = buffer.length;
|
|
|
|
const fileType = await fileTypeFromBuffer(buffer);
|
|
const contentType = fileType?.mime || "application/octet-stream";
|
|
|
|
const response = await apiFetch({
|
|
path: "/api/upload/signed-url",
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.apiToken}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
filename,
|
|
contentType,
|
|
contentLength,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
throw new Error(`failed to get upload URL: ${error}`);
|
|
}
|
|
|
|
const { uploadUrl, publicUrl, contentDisposition } = (await response.json()) as {
|
|
uploadUrl: string;
|
|
publicUrl: string;
|
|
contentDisposition?: string | undefined;
|
|
};
|
|
|
|
const uploadResponse = await fetch(uploadUrl, {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": contentType,
|
|
// should be set automatically, but given this header is signed it's better to be explicit
|
|
"Content-Length": String(contentLength),
|
|
...(contentDisposition && { "Content-Disposition": contentDisposition }),
|
|
},
|
|
body: buffer,
|
|
});
|
|
|
|
if (!uploadResponse.ok) {
|
|
throw new Error(`failed to upload file: ${uploadResponse.statusText}`);
|
|
}
|
|
|
|
return { success: true, publicUrl, filename, contentLength, contentType };
|
|
}),
|
|
});
|
|
}
|