Integrate Canvas V2 subgraphs, editable edges and AI runs
This commit is contained in:
@@ -3,10 +3,12 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const allowed =
|
||||
/^(profile|auth\/(login|register|verify-email|exchange|me|logout)|workspaces(?:\/[a-zA-Z0-9_-]+(?:\/(?:materials(?:\/[a-zA-Z0-9_-]+(?:\/canvas)?)?|search))?)?)$/;
|
||||
const canvasRoute = /^workspaces\/[a-zA-Z0-9_-]+\/materials\/[a-zA-Z0-9_-]+\/canvas(?:\/(?:connections|capabilities|subgraphs(?:\/(?:migrate|[a-zA-Z0-9_-]+\/(?:members|dissolve)))?|transactions\/[a-zA-Z0-9_-]+\/undo|ai\/(?:models|runs(?:\/[a-zA-Z0-9_-]+(?:\/(?:cancel|apply))?)?)))?$/;
|
||||
const assetRoute = /^workspaces\/[a-zA-Z0-9_-]+\/assets(?:\/[a-zA-Z0-9_-]+)?$/;
|
||||
const bodyLimit = 1024 * 1024;
|
||||
|
||||
// Route Handlers do not have the Pages Router bodyParser size limit.
|
||||
async function readBody(req: NextRequest, limit = bodyLimit) {
|
||||
async function readBody(req: NextRequest, limit = bodyLimit, binary = false) {
|
||||
const reader = req.body?.getReader();
|
||||
if (!reader) return {};
|
||||
const chunks: Uint8Array[] = [];
|
||||
@@ -25,8 +27,9 @@ async function readBody(req: NextRequest, limit = bodyLimit) {
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
if (!size) return {};
|
||||
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
||||
if (!size) return binary ? new Uint8Array() : {};
|
||||
const bytes = Buffer.concat(chunks);
|
||||
return binary ? bytes : JSON.parse(bytes.toString("utf8"));
|
||||
}
|
||||
|
||||
async function handler(
|
||||
@@ -46,11 +49,13 @@ async function handler(
|
||||
}
|
||||
}
|
||||
const path = (await params).path.join("/");
|
||||
if (!allowed.test(path)) return json({ error: "not_found" }, 404);
|
||||
if (!allowed.test(path) && !canvasRoute.test(path) && !assetRoute.test(path)) return json({ error: "not_found" }, 404);
|
||||
const asset = assetRoute.test(path);
|
||||
const canvas = canvasRoute.test(path);
|
||||
let requestBody: unknown;
|
||||
if (req.method !== "GET") {
|
||||
try {
|
||||
requestBody = await readBody(req, path.endsWith("/canvas") ? 5 * 1024 * 1024 : bodyLimit);
|
||||
requestBody = await readBody(req, asset ? 21 * 1024 * 1024 : path.endsWith("/canvas") ? 5 * 1024 * 1024 : bodyLimit, asset);
|
||||
} catch (error) {
|
||||
const tooLarge = error instanceof Error && error.message === "body_too_large";
|
||||
return json({ error: tooLarge ? "body_too_large" : "invalid_json" }, tooLarge ? 413 : 400);
|
||||
@@ -58,15 +63,16 @@ async function handler(
|
||||
}
|
||||
const origin = process.env.FOUNDRY_API_ORIGIN || "https://api.foundry.entroia.com";
|
||||
const project = process.env.NEXT_PUBLIC_FOUNDRY_PROJECT || "endoleap";
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||
const headers: Record<string, string> = { "Content-Type": asset ? (req.headers.get("content-type") || "application/octet-stream") : "application/json" };
|
||||
if (canvas) headers.Accept = "application/vnd.foundry.canvas+json;version=2";
|
||||
const session = req.cookies.get("foundry_session")?.value;
|
||||
if (session) headers.Authorization = "Bearer " + session;
|
||||
for (const h of ["x-foundry-code", "x-foundry-verifier", "if-match"]) {
|
||||
for (const h of ["x-foundry-code", "x-foundry-verifier", "if-match", "idempotency-key"]) {
|
||||
const value = req.headers.get(h);
|
||||
if (value !== null) headers[h] = value;
|
||||
}
|
||||
const query = new URLSearchParams();
|
||||
for (const key of ["kind", "sort", "q", "limit", "offset"]) {
|
||||
for (const key of ["kind", "sort", "q", "limit", "offset", "after_seq", "status", "include_descendants", "include_subgraphs", "cursor"]) {
|
||||
const values = req.nextUrl.searchParams.getAll(key);
|
||||
if (values.length === 1 && values[0].length < 300) query.set(key, values[0]);
|
||||
}
|
||||
@@ -78,14 +84,29 @@ async function handler(
|
||||
{
|
||||
method: req.method,
|
||||
headers,
|
||||
body: req.method === "GET" ? undefined : JSON.stringify(requestBody || {}),
|
||||
body: req.method === "GET" ? undefined : asset ? requestBody as BodyInit : JSON.stringify(requestBody || {}),
|
||||
signal: AbortSignal.any([AbortSignal.timeout(15000), req.signal]),
|
||||
redirect: "manual",
|
||||
cache: "no-store",
|
||||
},
|
||||
);
|
||||
const retryAfter = upstream.headers.get("retry-after");
|
||||
if (retryAfter) responseHeaders.set("Retry-After", retryAfter);
|
||||
const etag = upstream.headers.get("etag");
|
||||
if (etag) responseHeaders.set("ETag", etag);
|
||||
const schema = upstream.headers.get("x-canvas-schema-version");
|
||||
if (schema) responseHeaders.set("X-Canvas-Schema-Version", schema);
|
||||
const renewal = upstream.headers.get(SESSION_EXPIRY_HEADER);
|
||||
if (session && upstream.status !== 401 && renewal !== null && /^\d+$/.test(renewal)) {
|
||||
const secure = process.env.NODE_ENV === "production" && req.headers.get("x-forwarded-proto") === "https";
|
||||
responseHeaders.set("Set-Cookie", sessionCookie(session, sessionMaxAge(renewal), secure));
|
||||
}
|
||||
if (asset && upstream.ok && req.method === "GET") {
|
||||
const type = upstream.headers.get("content-type") || "";
|
||||
if (!/^image\/(png|jpeg|webp|gif)(?:;|$)/i.test(type)) return json({ error: "invalid_asset" }, 502);
|
||||
responseHeaders.set("Content-Type", type); responseHeaders.set("X-Content-Type-Options", "nosniff");
|
||||
return new NextResponse(upstream.body, { status: upstream.status, headers: responseHeaders });
|
||||
}
|
||||
const headersReceived = performance.now();
|
||||
const body = upstream.status === 204 ? null : await upstream.json();
|
||||
responseHeaders.set(
|
||||
|
||||
Reference in New Issue
Block a user