From bd9f1aa93702645941dc90a640fda72304530e26 Mon Sep 17 00:00:00 2001 From: Alex Z Date: Tue, 12 May 2026 11:27:22 -0700 Subject: [PATCH 01/22] clamp down on where we allow downloads from in the base64 conversion --- packages/proxy/src/providers/util.test.ts | 129 ++++++++++++ packages/proxy/src/providers/util.ts | 246 +++++++++++++++++++--- 2 files changed, 349 insertions(+), 26 deletions(-) create mode 100644 packages/proxy/src/providers/util.test.ts diff --git a/packages/proxy/src/providers/util.test.ts b/packages/proxy/src/providers/util.test.ts new file mode 100644 index 00000000..6435d738 --- /dev/null +++ b/packages/proxy/src/providers/util.test.ts @@ -0,0 +1,129 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { lookup } from "node:dns/promises"; +import { convertMediaToBase64 } from "./util"; + +vi.mock("node:dns/promises", () => ({ + lookup: vi.fn(async () => [{ address: "93.184.216.34", family: 4 }]), +})); + +const publicImageUrl = "https://93.184.216.34/image.png"; + +function mockFetch(response: Response) { + const fetchMock = vi.fn(async () => response); + vi.stubGlobal("fetch", fetchMock); + return fetchMock; +} + +describe("convertMediaToBase64", () => { + afterEach(() => { + vi.clearAllMocks(); + vi.unstubAllGlobals(); + }); + + it("rejects non-http media URLs before fetching", async () => { + const fetchMock = mockFetch(new Response()); + + await expect( + convertMediaToBase64({ + media: "file:///etc/passwd", + allowedMediaTypes: null, + maxMediaBytes: null, + }), + ).rejects.toThrow("Media URL must use http or https"); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("rejects localhost media URLs before fetching", async () => { + const fetchMock = mockFetch(new Response()); + + await expect( + convertMediaToBase64({ + media: "http://localhost/image.png", + allowedMediaTypes: null, + maxMediaBytes: null, + }), + ).rejects.toThrow("Media URL resolves to a blocked address"); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("rejects private IP media URLs before fetching", async () => { + const fetchMock = mockFetch(new Response()); + + await expect( + convertMediaToBase64({ + media: "https://169.254.169.254/latest/meta-data", + allowedMediaTypes: null, + maxMediaBytes: null, + }), + ).rejects.toThrow("Media URL resolves to a blocked address"); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("rejects hostnames that resolve to private addresses before fetching", async () => { + const lookupMock = vi.mocked(lookup); + lookupMock.mockResolvedValueOnce([{ address: "10.0.0.5", family: 4 }]); + const fetchMock = mockFetch(new Response()); + + await expect( + convertMediaToBase64({ + media: "https://example.com/image.png", + allowedMediaTypes: null, + maxMediaBytes: null, + }), + ).rejects.toThrow("Media URL resolves to a blocked address"); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("revalidates redirect locations", async () => { + const fetchMock = mockFetch( + new Response(null, { + status: 302, + headers: { location: "http://127.0.0.1/image.png" }, + }), + ); + + await expect( + convertMediaToBase64({ + media: publicImageUrl, + allowedMediaTypes: null, + maxMediaBytes: null, + }), + ).rejects.toThrow("Media URL resolves to a blocked address"); + expect(fetchMock).toHaveBeenCalledOnce(); + }); + + it("enforces the byte cap while reading the response body", async () => { + mockFetch( + new Response(new Uint8Array([1, 2, 3]), { + headers: { "content-type": "image/png" }, + }), + ); + + await expect( + convertMediaToBase64({ + media: publicImageUrl, + allowedMediaTypes: ["image/png"], + maxMediaBytes: 2, + }), + ).rejects.toThrow("Media size exceeds"); + }); + + it("converts valid remote media responses", async () => { + mockFetch( + new Response(new Uint8Array([1, 2, 3]), { + headers: { "content-type": "image/png; charset=utf-8" }, + }), + ); + + await expect( + convertMediaToBase64({ + media: publicImageUrl, + allowedMediaTypes: ["image/png"], + maxMediaBytes: 3, + }), + ).resolves.toEqual({ + media_type: "image/png", + data: "AQID", + }); + }); +}); diff --git a/packages/proxy/src/providers/util.ts b/packages/proxy/src/providers/util.ts index 1571fe5f..5c77698d 100644 --- a/packages/proxy/src/providers/util.ts +++ b/packages/proxy/src/providers/util.ts @@ -1,7 +1,10 @@ +import { lookup } from "node:dns/promises"; import { arrayBufferToBase64 } from "utils"; const base64MediaPattern = /^data:([a-zA-Z0-9]+\/[a-zA-Z0-9+.-]+);base64,([A-Za-z0-9+/]+={0,2})$/; +const maxRedirects = 3; +const mediaFetchTimeoutMs = 30_000; export interface MediaBlock { media_type: string; @@ -21,6 +24,192 @@ export function convertBase64Media(media: string): MediaBlock | null { }; } +function normalizeHostname(hostname: string): string { + return hostname.toLowerCase().replace(/^\[|\]$/g, ""); +} + +function parseIPv4Address(address: string): number | null { + const parts = address.split("."); + if (parts.length !== 4) { + return null; + } + + let parsed = 0; + for (const part of parts) { + if (!/^\d+$/.test(part)) { + return null; + } + + const value = Number(part); + if (value < 0 || value > 255) { + return null; + } + + parsed = parsed * 256 + value; + } + + return parsed; +} + +function isIPv4InRange(address: number, base: number, prefixLength: number) { + const mask = + prefixLength === 0 ? 0 : (0xffffffff << (32 - prefixLength)) >>> 0; + return (address & mask) === (base & mask); +} + +function isBlockedIPv4Address(address: string): boolean { + const parsed = parseIPv4Address(address); + if (parsed === null) { + return false; + } + + return [ + { base: "0.0.0.0", prefixLength: 8 }, + { base: "10.0.0.0", prefixLength: 8 }, + { base: "100.64.0.0", prefixLength: 10 }, + { base: "127.0.0.0", prefixLength: 8 }, + { base: "169.254.0.0", prefixLength: 16 }, + { base: "172.16.0.0", prefixLength: 12 }, + { base: "192.0.0.0", prefixLength: 24 }, + { base: "192.168.0.0", prefixLength: 16 }, + { base: "198.18.0.0", prefixLength: 15 }, + { base: "224.0.0.0", prefixLength: 4 }, + { base: "240.0.0.0", prefixLength: 4 }, + ].some(({ base, prefixLength }) => { + const parsedBase = parseIPv4Address(base); + return ( + parsedBase !== null && isIPv4InRange(parsed, parsedBase, prefixLength) + ); + }); +} + +function parseFirstIPv6Segment(address: string): number | null { + const firstSegment = address.split(":")[0]; + if (!/^[0-9a-f]{1,4}$/i.test(firstSegment)) { + return null; + } + + return Number.parseInt(firstSegment, 16); +} + +function isBlockedIPv6Address(address: string): boolean { + const normalized = normalizeHostname(address); + if (normalized === "::" || normalized === "::1") { + return true; + } + + if (normalized.startsWith("::ffff:")) { + return isBlockedIPv4Address(normalized.slice("::ffff:".length)); + } + + const firstSegment = parseFirstIPv6Segment(normalized); + if (firstSegment === null) { + return false; + } + + return ( + (firstSegment & 0xfe00) === 0xfc00 || + (firstSegment & 0xffc0) === 0xfe80 || + (firstSegment & 0xff00) === 0xff00 + ); +} + +function isBlockedIPAddress(address: string): boolean { + return isBlockedIPv4Address(address) || isBlockedIPv6Address(address); +} + +async function validateMediaUrl(url: URL) { + if (url.protocol !== "http:" && url.protocol !== "https:") { + throw new Error("Media URL must use http or https"); + } + + const hostname = normalizeHostname(url.hostname); + if (hostname === "localhost" || isBlockedIPAddress(hostname)) { + throw new Error("Media URL resolves to a blocked address"); + } + + if (parseIPv4Address(hostname) !== null || hostname.includes(":")) { + return; + } + + const addresses = await lookup(hostname, { all: true, verbatim: true }); + if ( + addresses.length === 0 || + addresses.some(({ address }) => isBlockedIPAddress(address)) + ) { + throw new Error("Media URL resolves to a blocked address"); + } +} + +async function readResponseBytes( + response: Response, + maxMediaBytes: number | null, +): Promise { + if (!response.body) { + throw new Error("Failed to read media response body"); + } + + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let totalBytes = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + + totalBytes += value.byteLength; + if (maxMediaBytes !== null && totalBytes > maxMediaBytes) { + await reader.cancel(); + throw new Error( + `Media size exceeds the ${maxMediaBytes / 1024 / 1024} MB limit`, + ); + } + chunks.push(value); + } + + const bytes = new Uint8Array(totalBytes); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + + return bytes.buffer; +} + +async function fetchMediaUrl( + url: URL, + signal: AbortSignal, + redirectCount = 0, +): Promise { + await validateMediaUrl(url); + + const response = await fetch(url, { + redirect: "manual", + signal, + }); + + if (response.status >= 300 && response.status < 400) { + const location = response.headers.get("location"); + if (!location) { + throw new Error("Media URL redirect missing location header"); + } + if (redirectCount >= maxRedirects) { + throw new Error("Media URL exceeded redirect limit"); + } + + return await fetchMediaUrl( + new URL(location, url), + signal, + redirectCount + 1, + ); + } + + return response; +} + async function convertMediaUrl({ url, allowedMediaTypes, @@ -30,36 +219,41 @@ async function convertMediaUrl({ allowedMediaTypes: string[] | null; maxMediaBytes: number | null; }): Promise { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch media: ${response.statusText}`); - } + const parsedUrl = new URL(url); + const abortController = new AbortController(); + const timeout = setTimeout( + () => abortController.abort(), + mediaFetchTimeoutMs, + ); + try { + const response = await fetchMediaUrl(parsedUrl, abortController.signal); + if (!response.ok) { + throw new Error(`Failed to fetch media: ${response.statusText}`); + } - const contentType = response.headers.get("content-type"); - if (!contentType) { - throw new Error("Failed to get content type of the media"); - } - const baseContentType = contentType.split(";")[0].trim(); - if ( - allowedMediaTypes !== null && - !allowedMediaTypes.includes(baseContentType) - ) { - throw new Error(`Unsupported media type: ${baseContentType}`); - } + const contentType = response.headers.get("content-type"); + if (!contentType) { + throw new Error("Failed to get content type of the media"); + } + const baseContentType = contentType.split(";")[0].trim(); + if ( + allowedMediaTypes !== null && + !allowedMediaTypes.includes(baseContentType) + ) { + throw new Error(`Unsupported media type: ${baseContentType}`); + } - const arrayBuffer = await response.arrayBuffer(); - if (maxMediaBytes !== null && arrayBuffer.byteLength > maxMediaBytes) { - throw new Error( - `Media size exceeds the ${maxMediaBytes / 1024 / 1024} MB limit`, - ); - } + const arrayBuffer = await readResponseBytes(response, maxMediaBytes); - const data = arrayBufferToBase64(arrayBuffer); + const data = arrayBufferToBase64(arrayBuffer); - return { - media_type: baseContentType, - data, - }; + return { + media_type: baseContentType, + data, + }; + } finally { + clearTimeout(timeout); + } } export async function convertMediaToBase64({ From 5ff3763dee7c9f30229dbe4ced5973b3927d8275 Mon Sep 17 00:00:00 2001 From: Alex Z Date: Tue, 12 May 2026 11:46:39 -0700 Subject: [PATCH 02/22] better --- packages/proxy/src/providers/util.test.ts | 51 +++------ packages/proxy/src/providers/util.ts | 127 ++++++++++++++++++++-- 2 files changed, 134 insertions(+), 44 deletions(-) diff --git a/packages/proxy/src/providers/util.test.ts b/packages/proxy/src/providers/util.test.ts index 6435d738..244fc056 100644 --- a/packages/proxy/src/providers/util.test.ts +++ b/packages/proxy/src/providers/util.test.ts @@ -59,65 +59,47 @@ describe("convertMediaToBase64", () => { expect(fetchMock).not.toHaveBeenCalled(); }); - it("rejects hostnames that resolve to private addresses before fetching", async () => { - const lookupMock = vi.mocked(lookup); - lookupMock.mockResolvedValueOnce([{ address: "10.0.0.5", family: 4 }]); + it("rejects IPv4-mapped IPv6 localhost URLs before fetching", async () => { const fetchMock = mockFetch(new Response()); await expect( convertMediaToBase64({ - media: "https://example.com/image.png", + media: "http://[::ffff:127.0.0.1]/image.png", allowedMediaTypes: null, maxMediaBytes: null, }), ).rejects.toThrow("Media URL resolves to a blocked address"); - expect(fetchMock).not.toHaveBeenCalled(); - }); - - it("revalidates redirect locations", async () => { - const fetchMock = mockFetch( - new Response(null, { - status: 302, - headers: { location: "http://127.0.0.1/image.png" }, - }), - ); - await expect( convertMediaToBase64({ - media: publicImageUrl, + media: "http://[::ffff:7f00:1]/image.png", allowedMediaTypes: null, maxMediaBytes: null, }), ).rejects.toThrow("Media URL resolves to a blocked address"); - expect(fetchMock).toHaveBeenCalledOnce(); + expect(fetchMock).not.toHaveBeenCalled(); }); - it("enforces the byte cap while reading the response body", async () => { - mockFetch( - new Response(new Uint8Array([1, 2, 3]), { - headers: { "content-type": "image/png" }, - }), - ); + it("rejects hostnames that resolve to private addresses before fetching", async () => { + const lookupMock = vi.mocked(lookup); + lookupMock.mockResolvedValueOnce([{ address: "10.0.0.5", family: 4 }]); + const fetchMock = mockFetch(new Response()); await expect( convertMediaToBase64({ - media: publicImageUrl, - allowedMediaTypes: ["image/png"], - maxMediaBytes: 2, + media: "https://example.com/image.png", + allowedMediaTypes: null, + maxMediaBytes: null, }), - ).rejects.toThrow("Media size exceeds"); + ).rejects.toThrow("Media URL resolves to a blocked address"); + expect(fetchMock).not.toHaveBeenCalled(); }); - it("converts valid remote media responses", async () => { - mockFetch( - new Response(new Uint8Array([1, 2, 3]), { - headers: { "content-type": "image/png; charset=utf-8" }, - }), - ); + it("converts base64 data URLs without remote fetching", async () => { + const fetchMock = mockFetch(new Response()); await expect( convertMediaToBase64({ - media: publicImageUrl, + media: "data:image/png;base64,AQID", allowedMediaTypes: ["image/png"], maxMediaBytes: 3, }), @@ -125,5 +107,6 @@ describe("convertMediaToBase64", () => { media_type: "image/png", data: "AQID", }); + expect(fetchMock).not.toHaveBeenCalled(); }); }); diff --git a/packages/proxy/src/providers/util.ts b/packages/proxy/src/providers/util.ts index 5c77698d..1a7dad34 100644 --- a/packages/proxy/src/providers/util.ts +++ b/packages/proxy/src/providers/util.ts @@ -1,4 +1,8 @@ import { lookup } from "node:dns/promises"; +import { type IncomingHttpHeaders } from "node:http"; +import { request as httpRequest } from "node:http"; +import { request as httpsRequest } from "node:https"; +import { Readable } from "node:stream"; import { arrayBufferToBase64 } from "utils"; const base64MediaPattern = @@ -11,6 +15,11 @@ export interface MediaBlock { data: string; } +interface ValidatedMediaUrl { + address: string; + url: URL; +} + export function convertBase64Media(media: string): MediaBlock | null { const match = media.match(base64MediaPattern); if (!match) { @@ -92,14 +101,52 @@ function parseFirstIPv6Segment(address: string): number | null { return Number.parseInt(firstSegment, 16); } +function parseIPv4MappedIPv6Address(address: string): string | null { + const mappedPrefix = "::ffff:"; + const normalized = normalizeHostname(address); + if (!normalized.startsWith(mappedPrefix)) { + return null; + } + + const mappedAddress = normalized.slice(mappedPrefix.length); + if (parseIPv4Address(mappedAddress) !== null) { + return mappedAddress; + } + + const parts = mappedAddress.split(":"); + if (parts.length !== 2) { + return null; + } + + const parsedParts = parts.map((part) => { + if (!/^[0-9a-f]{1,4}$/i.test(part)) { + return null; + } + return Number.parseInt(part, 16); + }); + if (parsedParts.some((part) => part === null)) { + return null; + } + + const [high, low] = parsedParts; + if (high === null || low === null) { + return null; + } + + return [(high >> 8) & 0xff, high & 0xff, (low >> 8) & 0xff, low & 0xff].join( + ".", + ); +} + function isBlockedIPv6Address(address: string): boolean { const normalized = normalizeHostname(address); if (normalized === "::" || normalized === "::1") { return true; } - if (normalized.startsWith("::ffff:")) { - return isBlockedIPv4Address(normalized.slice("::ffff:".length)); + const mappedIPv4Address = parseIPv4MappedIPv6Address(normalized); + if (mappedIPv4Address !== null) { + return isBlockedIPv4Address(mappedIPv4Address); } const firstSegment = parseFirstIPv6Segment(normalized); @@ -118,7 +165,24 @@ function isBlockedIPAddress(address: string): boolean { return isBlockedIPv4Address(address) || isBlockedIPv6Address(address); } -async function validateMediaUrl(url: URL) { +function nodeHeadersToHeaders(headers: IncomingHttpHeaders): Headers { + const result = new Headers(); + for (const [name, value] of Object.entries(headers)) { + if (value === undefined) { + continue; + } + if (Array.isArray(value)) { + for (const item of value) { + result.append(name, item); + } + continue; + } + result.set(name, value); + } + return result; +} + +async function validateMediaUrl(url: URL): Promise { if (url.protocol !== "http:" && url.protocol !== "https:") { throw new Error("Media URL must use http or https"); } @@ -129,7 +193,7 @@ async function validateMediaUrl(url: URL) { } if (parseIPv4Address(hostname) !== null || hostname.includes(":")) { - return; + return { address: hostname, url }; } const addresses = await lookup(hostname, { all: true, verbatim: true }); @@ -139,6 +203,8 @@ async function validateMediaUrl(url: URL) { ) { throw new Error("Media URL resolves to a blocked address"); } + + return { address: addresses[0].address, url }; } async function readResponseBytes( @@ -184,12 +250,8 @@ async function fetchMediaUrl( signal: AbortSignal, redirectCount = 0, ): Promise { - await validateMediaUrl(url); - - const response = await fetch(url, { - redirect: "manual", - signal, - }); + const target = await validateMediaUrl(url); + const response = await fetchValidatedMediaUrl(target, signal); if (response.status >= 300 && response.status < 400) { const location = response.headers.get("location"); @@ -210,6 +272,51 @@ async function fetchMediaUrl( return response; } +async function fetchValidatedMediaUrl( + { address, url }: ValidatedMediaUrl, + signal: AbortSignal, +): Promise { + return await new Promise((resolve, reject) => { + const hostname = normalizeHostname(url.hostname); + const servername = + parseIPv4Address(hostname) === null && !hostname.includes(":") + ? url.hostname + : undefined; + const request = (url.protocol === "https:" ? httpsRequest : httpRequest)( + { + headers: { + Host: url.host, + }, + hostname: address, + method: "GET", + path: `${url.pathname}${url.search}`, + port: url.port || (url.protocol === "https:" ? 443 : 80), + protocol: url.protocol, + servername, + }, + (response) => { + resolve( + new Response(Readable.toWeb(response), { + headers: nodeHeadersToHeaders(response.headers), + status: response.statusCode ?? 500, + statusText: response.statusMessage, + }), + ); + }, + ); + + request.on("error", reject); + signal.addEventListener( + "abort", + () => { + request.destroy(new Error("Media fetch aborted")); + }, + { once: true }, + ); + request.end(); + }); +} + async function convertMediaUrl({ url, allowedMediaTypes, From c44616232398f98d156e790c83c9114843ae4f15 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 12:52:39 -0700 Subject: [PATCH 03/22] allow node functionality --- apis/vercel/pages/api/ping.ts | 4 ---- apis/vercel/pages/api/v1/[...slug].ts | 4 ---- 2 files changed, 8 deletions(-) diff --git a/apis/vercel/pages/api/ping.ts b/apis/vercel/pages/api/ping.ts index f7548c5e..d01bbecb 100644 --- a/apis/vercel/pages/api/ping.ts +++ b/apis/vercel/pages/api/ping.ts @@ -8,10 +8,6 @@ const ratelimit = new Ratelimit({ limiter: Ratelimit.slidingWindow(1000, "10 s"), }); -export const config = { - runtime: "edge", -}; - let i = 0; export default async function handler(request: NextRequest) { // You could alternatively limit based on user ID or similar diff --git a/apis/vercel/pages/api/v1/[...slug].ts b/apis/vercel/pages/api/v1/[...slug].ts index f5caddb4..ff67eb79 100644 --- a/apis/vercel/pages/api/v1/[...slug].ts +++ b/apis/vercel/pages/api/v1/[...slug].ts @@ -1,10 +1,6 @@ import { kv } from "@vercel/kv"; import { EdgeProxyV1, CacheSetOptions } from "@braintrust/proxy/edge"; -export const config = { - runtime: "edge", -}; - const KVCache = { get: kv.get, set: async (key: string, value: T, opts: CacheSetOptions) => { From 28421f1b04da5dce46069922234b2c1f04dec709 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 13:03:45 -0700 Subject: [PATCH 04/22] Revert "allow node functionality" This reverts commit c44616232398f98d156e790c83c9114843ae4f15. --- apis/vercel/pages/api/ping.ts | 4 ++++ apis/vercel/pages/api/v1/[...slug].ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/apis/vercel/pages/api/ping.ts b/apis/vercel/pages/api/ping.ts index d01bbecb..f7548c5e 100644 --- a/apis/vercel/pages/api/ping.ts +++ b/apis/vercel/pages/api/ping.ts @@ -8,6 +8,10 @@ const ratelimit = new Ratelimit({ limiter: Ratelimit.slidingWindow(1000, "10 s"), }); +export const config = { + runtime: "edge", +}; + let i = 0; export default async function handler(request: NextRequest) { // You could alternatively limit based on user ID or similar diff --git a/apis/vercel/pages/api/v1/[...slug].ts b/apis/vercel/pages/api/v1/[...slug].ts index ff67eb79..f5caddb4 100644 --- a/apis/vercel/pages/api/v1/[...slug].ts +++ b/apis/vercel/pages/api/v1/[...slug].ts @@ -1,6 +1,10 @@ import { kv } from "@vercel/kv"; import { EdgeProxyV1, CacheSetOptions } from "@braintrust/proxy/edge"; +export const config = { + runtime: "edge", +}; + const KVCache = { get: kv.get, set: async (key: string, value: T, opts: CacheSetOptions) => { From 904c55bf2431feb80b17bc0b17460d862705b572 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 13:24:19 -0700 Subject: [PATCH 05/22] temp skip cache --- packages/proxy/edge/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/proxy/edge/index.ts b/packages/proxy/edge/index.ts index 0a7f5e83..e925275c 100644 --- a/packages/proxy/edge/index.ts +++ b/packages/proxy/edge/index.ts @@ -279,7 +279,8 @@ export function makeFetchApiSecrets({ }); } - if (opts.credentialsCache && !lookupFailed) { + // temp skip the cache for testing + if (useCache && opts.credentialsCache && !lookupFailed) { ctx.waitUntil( encryptedPut( opts.credentialsCache, From 7cefb85e0da8525de4a57a2ed5c16ef3f32234c7 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 13:29:21 -0700 Subject: [PATCH 06/22] better errors --- packages/proxy/edge/index.ts | 4 ++++ packages/proxy/src/proxy.ts | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/proxy/edge/index.ts b/packages/proxy/edge/index.ts index e925275c..e81f57ec 100644 --- a/packages/proxy/edge/index.ts +++ b/packages/proxy/edge/index.ts @@ -405,6 +405,10 @@ export function EdgeProxyV1(opts: ProxyOpts) { onBillingEvent: opts.onBillingEvent, }); } catch (e) { + // Log so the underlying cause shows up in Vercel/Cloudflare function + // logs. The body still echoes the error message for the caller, but + // without this, the only signal at the proxy is "status=400". + console.error("EdgeProxyV1 request failed", e); return new Response(`${e}`, { status: 400, headers: { "Content-Type": "text/plain" }, diff --git a/packages/proxy/src/proxy.ts b/packages/proxy/src/proxy.ts index cd414cff..7651b2d7 100644 --- a/packages/proxy/src/proxy.ts +++ b/packages/proxy/src/proxy.ts @@ -1463,7 +1463,10 @@ async function fetchModelLoop( errorHttpHeaders = proxyResponse.response.headers; } } catch (e) { - const isAbortError = e instanceof DOMException && e.name === "AbortError"; + const isAbortError = + typeof DOMException !== "undefined" && + e instanceof DOMException && + e.name === "AbortError"; if (!isAbortError) { console.log("ERROR", e); } From b5572875ca450e62692d457e9155306a3fdccbdc Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 13:49:45 -0700 Subject: [PATCH 07/22] manage connections ourselves --- apis/vercel/package.json | 3 ++- apis/vercel/pages/api/ping.ts | 4 ---- apis/vercel/pages/api/v1/[...slug].ts | 34 +++++++++++++++++++++++---- pnpm-lock.yaml | 9 +++++++ 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/apis/vercel/package.json b/apis/vercel/package.json index f32c97ef..f5d1f5be 100644 --- a/apis/vercel/package.json +++ b/apis/vercel/package.json @@ -16,7 +16,8 @@ "@braintrust/proxy": "workspace:*", "next": "15.5.15", "react": "latest", - "react-dom": "latest" + "react-dom": "latest", + "undici": "^6.0.0" }, "devDependencies": { "@types/node": "^17.0.45", diff --git a/apis/vercel/pages/api/ping.ts b/apis/vercel/pages/api/ping.ts index f7548c5e..d01bbecb 100644 --- a/apis/vercel/pages/api/ping.ts +++ b/apis/vercel/pages/api/ping.ts @@ -8,10 +8,6 @@ const ratelimit = new Ratelimit({ limiter: Ratelimit.slidingWindow(1000, "10 s"), }); -export const config = { - runtime: "edge", -}; - let i = 0; export default async function handler(request: NextRequest) { // You could alternatively limit based on user ID or similar diff --git a/apis/vercel/pages/api/v1/[...slug].ts b/apis/vercel/pages/api/v1/[...slug].ts index f5caddb4..debf986b 100644 --- a/apis/vercel/pages/api/v1/[...slug].ts +++ b/apis/vercel/pages/api/v1/[...slug].ts @@ -1,9 +1,16 @@ import { kv } from "@vercel/kv"; import { EdgeProxyV1, CacheSetOptions } from "@braintrust/proxy/edge"; +import { Agent, setGlobalDispatcher } from "undici"; -export const config = { - runtime: "edge", -}; +// On the Node runtime, fetch is implemented by undici, which pools sockets +// with HTTP keep-alive by default. Don't do that, since we want to be able +// to close the connection after each request and not have it held open by the keep-alive timer. +setGlobalDispatcher( + new Agent({ + keepAliveTimeout: 1, + keepAliveMaxTimeout: 1, + }), +); const KVCache = { get: kv.get, @@ -20,7 +27,7 @@ const KVCache = { }, }; -export default EdgeProxyV1({ +const handler = EdgeProxyV1({ getRelativeURL: (request) => { const url = new URL(request.url); const params = url.searchParams.getAll("slug"); @@ -31,3 +38,22 @@ export default EdgeProxyV1({ completionsCache: KVCache, braintrustApiUrl: process.env.BRAINTRUST_APP_URL, }); + +// Wrap the EdgeProxyV1 handler so we can properly wait for any background tasks to finish before closing the connection. +export default async function route(request: Request): Promise { + const pending: Promise[] = []; + const ctx = { + waitUntil(promise: Promise) { + pending.push( + promise.catch((error) => { + console.warn("Background task failed", error); + }), + ); + }, + }; + + const response = await handler(request, ctx); + await Promise.allSettled(pending); + response.headers.set("Connection", "close"); + return response; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f340c0e0..d8d5f8ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -175,6 +175,9 @@ importers: react-dom: specifier: latest version: 19.2.5(react@19.2.5) + undici: + specifier: ^6.0.0 + version: 6.25.0 devDependencies: '@types/node': specifier: ^17.0.45 @@ -6031,6 +6034,10 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici@6.25.0: + resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==} + engines: {node: '>=18.17'} + undici@7.14.0: resolution: {integrity: sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==} engines: {node: '>=20.18.1'} @@ -12913,6 +12920,8 @@ snapshots: undici-types@5.26.5: {} + undici@6.25.0: {} + undici@7.14.0: {} unenv@2.0.0-rc.24: From 6f5d4795f91c4835e238ea1782d5fe1adc9cd04b Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 14:01:14 -0700 Subject: [PATCH 08/22] upgrade some things --- apis/vercel/next-env.d.ts | 3 +- apis/vercel/package.json | 16 +- apis/vercel/pages/api/ping.ts | 7 +- apis/vercel/pages/api/v1/[...slug].ts | 12 +- apis/vercel/tsconfig.json | 7 +- package.json | 2 +- pnpm-lock.yaml | 908 +++++++++++++------------- 7 files changed, 466 insertions(+), 489 deletions(-) diff --git a/apis/vercel/next-env.d.ts b/apis/vercel/next-env.d.ts index 725dd6f2..2d5420eb 100644 --- a/apis/vercel/next-env.d.ts +++ b/apis/vercel/next-env.d.ts @@ -1,6 +1,7 @@ /// /// /// +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apis/vercel/package.json b/apis/vercel/package.json index f5d1f5be..6b4d7096 100644 --- a/apis/vercel/package.json +++ b/apis/vercel/package.json @@ -14,20 +14,22 @@ "@vercel/examples-ui": "^1.0.5", "@vercel/kv": "^0.2.2", "@braintrust/proxy": "workspace:*", - "next": "15.5.15", - "react": "latest", - "react-dom": "latest", + "@vercel/functions": "^3.4.2", + "next": "^16.2.6", + "react": "19.2.6", + "react-dom": "19.2.6", "undici": "^6.0.0" }, "devDependencies": { "@types/node": "^17.0.45", - "@types/react": "latest", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", "autoprefixer": "^10.4.14", - "eslint": "^8.36.0", - "eslint-config-next": "canary", + "eslint": "^9.38.0", + "eslint-config-next": "^16.2.6", "postcss": "^8.5.10", "tailwindcss": "^3.2.7", "turbo": "^1.8.5", - "typescript": "4.7.4" + "typescript": "^5.5.4" } } diff --git a/apis/vercel/pages/api/ping.ts b/apis/vercel/pages/api/ping.ts index d01bbecb..54a03a00 100644 --- a/apis/vercel/pages/api/ping.ts +++ b/apis/vercel/pages/api/ping.ts @@ -1,6 +1,7 @@ import type { NextRequest } from "next/server"; import { Ratelimit } from "@upstash/ratelimit"; import { kv } from "@vercel/kv"; +import { ipAddress } from "@vercel/functions"; const ratelimit = new Ratelimit({ redis: kv, @@ -10,8 +11,10 @@ const ratelimit = new Ratelimit({ let i = 0; export default async function handler(request: NextRequest) { - // You could alternatively limit based on user ID or similar - const ip = request.ip ?? "127.0.0.1"; + // Next 16 removed `request.ip`; ipAddress() from @vercel/functions + // reads the same X-Real-IP / X-Forwarded-For headers. + // You could alternatively limit based on user ID or similar. + const ip = ipAddress(request) ?? "127.0.0.1"; /* let start = Date.now(); const { limit, reset, remaining } = await ratelimit.limit(ip); diff --git a/apis/vercel/pages/api/v1/[...slug].ts b/apis/vercel/pages/api/v1/[...slug].ts index debf986b..053fb8bc 100644 --- a/apis/vercel/pages/api/v1/[...slug].ts +++ b/apis/vercel/pages/api/v1/[...slug].ts @@ -1,10 +1,11 @@ +import dns from "node:dns"; import { kv } from "@vercel/kv"; +import { waitUntil } from "@vercel/functions"; import { EdgeProxyV1, CacheSetOptions } from "@braintrust/proxy/edge"; import { Agent, setGlobalDispatcher } from "undici"; -// On the Node runtime, fetch is implemented by undici, which pools sockets -// with HTTP keep-alive by default. Don't do that, since we want to be able -// to close the connection after each request and not have it held open by the keep-alive timer. +dns.setDefaultResultOrder("ipv4first"); + setGlobalDispatcher( new Agent({ keepAliveTimeout: 1, @@ -39,12 +40,10 @@ const handler = EdgeProxyV1({ braintrustApiUrl: process.env.BRAINTRUST_APP_URL, }); -// Wrap the EdgeProxyV1 handler so we can properly wait for any background tasks to finish before closing the connection. export default async function route(request: Request): Promise { - const pending: Promise[] = []; const ctx = { waitUntil(promise: Promise) { - pending.push( + waitUntil( promise.catch((error) => { console.warn("Background task failed", error); }), @@ -53,7 +52,6 @@ export default async function route(request: Request): Promise { }; const response = await handler(request, ctx); - await Promise.allSettled(pending); response.headers.set("Connection", "close"); return response; } diff --git a/apis/vercel/tsconfig.json b/apis/vercel/tsconfig.json index ab40fc21..f31709ea 100644 --- a/apis/vercel/tsconfig.json +++ b/apis/vercel/tsconfig.json @@ -13,10 +13,10 @@ "noEmit": true, "esModuleInterop": true, "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "paths": { "@lib/*": [ "./lib/*" @@ -42,7 +42,8 @@ "next-env.d.ts", "**/*.ts", "**/*.tsx", - ".next/types/**/*.ts" + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" ], "exclude": [ "node_modules" diff --git a/package.json b/package.json index 928dc100..8cc2ace0 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ }, "packageManager": "pnpm@10.33.0", "resolutions": { - "handlebars": "4.7.8", + "handlebars": "4.7.9", "protobufjs": "7.5.5", "simple-git": "3.32.3", "zod": "3.25.34" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8d5f8ae..1f999e08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: - handlebars: 4.7.8 + handlebars: 4.7.9 protobufjs: 7.5.5 simple-git: 3.32.3 zod: 3.25.34 @@ -89,7 +89,7 @@ importers: version: 2.32.0 ai: specifier: 2.2.22 - version: 2.2.22(react@19.2.5)(solid-js@1.9.10)(svelte@4.2.20)(vue@3.5.22(typescript@5.3.3)) + version: 2.2.22(react@19.2.6)(solid-js@1.9.10)(svelte@4.2.20)(vue@3.5.22(typescript@5.3.3)) aws-lambda: specifier: ^1.0.7 version: 1.0.7 @@ -162,19 +162,22 @@ importers: version: 0.4.3 '@vercel/examples-ui': specifier: ^1.0.5 - version: 1.0.5(next@15.5.15(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 1.0.5(next@16.2.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@vercel/functions': + specifier: ^3.4.2 + version: 3.5.1(@aws-sdk/credential-provider-web-identity@3.972.38) '@vercel/kv': specifier: ^0.2.2 version: 0.2.2 next: - specifier: 15.5.15 - version: 15.5.15(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^16.2.6 + version: 16.2.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: - specifier: latest - version: 19.2.5 + specifier: 19.2.6 + version: 19.2.6 react-dom: - specifier: latest - version: 19.2.5(react@19.2.5) + specifier: 19.2.6 + version: 19.2.6(react@19.2.6) undici: specifier: ^6.0.0 version: 6.25.0 @@ -183,17 +186,20 @@ importers: specifier: ^17.0.45 version: 17.0.45 '@types/react': - specifier: latest + specifier: ^19.0.2 version: 19.2.14 + '@types/react-dom': + specifier: ^19.0.2 + version: 19.2.3(@types/react@19.2.14) autoprefixer: specifier: ^10.4.14 version: 10.4.14(postcss@8.5.13) eslint: - specifier: ^8.36.0 - version: 8.56.0 + specifier: ^9.38.0 + version: 9.39.4 eslint-config-next: - specifier: canary - version: 16.3.0-canary.9(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0)(typescript@4.7.4) + specifier: ^16.2.6 + version: 16.2.6(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4)(typescript@5.5.4) postcss: specifier: ^8.5.10 version: 8.5.13 @@ -204,8 +210,8 @@ importers: specifier: ^1.8.5 version: 1.11.2 typescript: - specifier: 4.7.4 - version: 4.7.4 + specifier: ^5.5.4 + version: 5.5.4 packages/proxy: dependencies: @@ -238,7 +244,7 @@ importers: version: 2.1.0(@opentelemetry/api@1.9.0) ai: specifier: 2.2.37 - version: 2.2.37(react@19.2.5)(solid-js@1.9.10)(svelte@4.2.20)(vue@3.5.22(typescript@5.5.4)) + version: 2.2.37(react@19.2.6)(solid-js@1.9.10)(svelte@4.2.20)(vue@3.5.22(typescript@5.5.4)) cache-control-parser: specifier: ^2.0.6 version: 2.0.6 @@ -290,7 +296,7 @@ importers: version: 17.0.33 '@typescript-eslint/eslint-plugin': specifier: ^8.21.0 - version: 8.21.0(@typescript-eslint/parser@8.53.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(typescript@5.5.4) + version: 8.21.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4)(typescript@5.5.4) esbuild: specifier: ^0.27.0 version: 0.27.0 @@ -302,7 +308,7 @@ importers: version: 4.1.5 openapi-zod-client: specifier: ^1.18.3 - version: 1.18.3(react@19.2.5) + version: 1.18.3(react@19.2.6) skott: specifier: ^0.35.4 version: 0.35.4 @@ -327,10 +333,6 @@ importers: packages: - '@aashutoshrathi/word-wrap@1.2.6': - resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} - engines: {node: '>=0.10.0'} - '@ai-sdk/provider@1.1.3': resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} engines: {node: '>=18'} @@ -1156,12 +1158,6 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.7.0': resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1186,22 +1182,53 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@2.1.4': resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@eslint/js@8.56.0': - resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@8.57.1': resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@humanwhocodes/config-array@0.11.13': - resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} @@ -1212,14 +1239,14 @@ packages: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/object-schema@2.0.1': - resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} - deprecated: Use @eslint/object-schema instead - '@humanwhocodes/object-schema@2.0.3': resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -1454,60 +1481,60 @@ packages: '@next/env@14.2.3': resolution: {integrity: sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==} - '@next/env@15.5.15': - resolution: {integrity: sha512-vcmyu5/MyFzN7CdqRHO3uHO44p/QPCZkuTUXroeUmhNP8bL5PHFEhik22JUazt+CDDoD6EpBYRCaS2pISL+/hg==} + '@next/env@16.2.6': + resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} - '@next/eslint-plugin-next@16.3.0-canary.9': - resolution: {integrity: sha512-8nzhZX8HdW6YSNg6+aF1BDZlBydqEtHalSmvuj9hYupFuYvI/a9IQjDzGwRA85gwZA+7celFRi3UfarPnMDwgw==} + '@next/eslint-plugin-next@16.2.6': + resolution: {integrity: sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==} - '@next/swc-darwin-arm64@15.5.15': - resolution: {integrity: sha512-6PvFO2Tzt10GFK2Ro9tAVEtacMqRmTarYMFKAnV2vYMdwWc73xzmDQyAV7SwEdMhzmiRoo7+m88DuiXlJlGeaw==} + '@next/swc-darwin-arm64@16.2.6': + resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.5.15': - resolution: {integrity: sha512-G+YNV+z6FDZTp/+IdGyIMFqalBTaQSnvAA+X/hrt+eaTRFSznRMz9K7rTmzvM6tDmKegNtyzgufZW0HwVzEqaQ==} + '@next/swc-darwin-x64@16.2.6': + resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.5.15': - resolution: {integrity: sha512-eVkrMcVIBqGfXB+QUC7jjZ94Z6uX/dNStbQFabewAnk13Uy18Igd1YZ/GtPRzdhtm7QwC0e6o7zOQecul4iC1w==} + '@next/swc-linux-arm64-gnu@16.2.6': + resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@15.5.15': - resolution: {integrity: sha512-RwSHKMQ7InLy5GfkY2/n5PcFycKA08qI1VST78n09nN36nUPqCvGSMiLXlfUmzmpQpF6XeBYP2KRWHi0UW3uNg==} + '@next/swc-linux-arm64-musl@16.2.6': + resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@15.5.15': - resolution: {integrity: sha512-nplqvY86LakS+eeiuWsNWvfmK8pFcOEW7ZtVRt4QH70lL+0x6LG/m1OpJ/tvrbwjmR8HH9/fH2jzW1GlL03TIg==} + '@next/swc-linux-x64-gnu@16.2.6': + resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@15.5.15': - resolution: {integrity: sha512-eAgl9NKQ84/sww0v81DQINl/vL2IBxD7sMybd0cWRw6wqgouVI53brVRBrggqBRP/NWeIAE1dm5cbKYoiMlqDQ==} + '@next/swc-linux-x64-musl@16.2.6': + resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@15.5.15': - resolution: {integrity: sha512-GJVZC86lzSquh0MtvZT+L7G8+jMnJcldloOjA8Kf3wXvBrvb6OGe2MzPuALxFshSm/IpwUtD2mIoof39ymf52A==} + '@next/swc-win32-arm64-msvc@16.2.6': + resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.5.15': - resolution: {integrity: sha512-nFucjVdwlFqxh/JG3hWSJ4p8+YJV7Ii8aPDuBQULB6DzUF4UNZETXLfEUk+oI2zEznWWULPt7MeuTE6xtK1HSA==} + '@next/swc-win32-x64-msvc@16.2.6': + resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2296,6 +2323,11 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} @@ -2439,9 +2471,6 @@ packages: resolution: {integrity: sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.2.0': - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -2474,11 +2503,24 @@ packages: '@aws-sdk/credential-provider-web-identity': optional: true + '@vercel/functions@3.5.1': + resolution: {integrity: sha512-ndh5v+uhWqGA8033oD0i0KHvqUHcLlLCOaLOw5L+xx5zVsWUSQcZPKEYk2nm51aisnKhcnTylxnOmhx+w4UCRA==} + engines: {node: '>= 20'} + peerDependencies: + '@aws-sdk/credential-provider-web-identity': '*' + peerDependenciesMeta: + '@aws-sdk/credential-provider-web-identity': + optional: true + '@vercel/kv@0.2.2': resolution: {integrity: sha512-mqnQOB6bkp4h5eObxfLNIlhlVqOGSH8cWOlC5pDVWTjX3zL8dETO1ZBl6M74HBmeBjbD5+J7wDJklRigY6UNKw==} engines: {node: '>=14.6'} deprecated: 'Vercel KV is deprecated. If you had an existing KV store, it should have moved to Upstash Redis which you will see under Vercel Integrations. For new projects, install a Redis integration from Vercel Marketplace: https://vercel.com/marketplace?category=storage&search=redis' + '@vercel/oidc@3.4.1': + resolution: {integrity: sha512-H6B+/ig/GoahccL3WZjiHayHw1H5KhvTJNceqYulwfK9kkz5iul2hTmYzcJ7tTCQzyd0dutuL9xYFZCyLUqsog==} + engines: {node: '>= 20'} + '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -2651,6 +2693,9 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -2731,18 +2776,10 @@ packages: resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} engines: {node: '>= 0.4'} - array.prototype.flat@1.3.2: - resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} - engines: {node: '>= 0.4'} - array.prototype.flat@1.3.3: resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} engines: {node: '>= 0.4'} - array.prototype.flatmap@1.3.2: - resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} - engines: {node: '>= 0.4'} - array.prototype.flatmap@1.3.3: resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} engines: {node: '>= 0.4'} @@ -2818,6 +2855,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.29: + resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} + engines: {node: '>=6.0.0'} + hasBin: true + binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -3125,10 +3167,6 @@ packages: resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} engines: {node: '>=4.8'} - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -3188,15 +3226,6 @@ packages: supports-color: optional: true - debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -3421,9 +3450,6 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-shim-unscopables@1.0.2: - resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} - es-shim-unscopables@1.1.0: resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} engines: {node: '>= 0.4'} @@ -3480,8 +3506,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-next@16.3.0-canary.9: - resolution: {integrity: sha512-WLtGICFbfHKjuQkr6V5oushufBRnU/MD0Xnn9st8fuXkbmayxmX6dCdtDQCFH3YLNnfpZDkXkFY/H+wUT8jvlw==} + eslint-config-next@16.2.6: + resolution: {integrity: sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==} peerDependencies: eslint: '>=9.0.0' typescript: '>=3.3.1' @@ -3526,27 +3552,6 @@ packages: eslint-import-resolver-webpack: optional: true - eslint-module-utils@2.8.0: - resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - eslint-plugin-import@2.32.0: resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} engines: {node: '>=4'} @@ -3585,30 +3590,38 @@ packages: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@4.2.1: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@8.56.0: - resolution: {integrity: sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true - eslint@8.57.1: resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3618,10 +3631,6 @@ packages: engines: {node: '>=4'} hasBin: true - esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} - esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} @@ -3745,6 +3754,10 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -3776,6 +3789,10 @@ packages: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + flatted@3.2.9: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} @@ -3928,6 +3945,10 @@ packages: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + globals@16.4.0: resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} engines: {node: '>=18'} @@ -3964,8 +3985,8 @@ packages: resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} engines: {node: '>=0.4.7'} hasBin: true @@ -4006,10 +4027,6 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - hasown@2.0.3: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} @@ -4054,10 +4071,6 @@ packages: resolution: {integrity: sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ignore@5.3.0: - resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} - engines: {node: '>= 4'} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -4347,6 +4360,10 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -4608,6 +4625,9 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@7.4.6: resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} engines: {node: '>=10'} @@ -4636,9 +4656,6 @@ packages: ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -4694,9 +4711,9 @@ packages: next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - next@15.5.15: - resolution: {integrity: sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + next@16.2.6: + resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==} + engines: {node: '>=20.9.0'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -4809,10 +4826,6 @@ packages: resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} engines: {node: '>= 0.4'} - object.values@1.1.7: - resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} - engines: {node: '>= 0.4'} - object.values@1.2.1: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} @@ -4870,10 +4883,6 @@ packages: openapi3-ts@3.1.0: resolution: {integrity: sha512-1qKTvCCVoV0rkwUh1zq5o8QyghmwYPuhdvtjv1rFjuOnJToXhQyF8eGjNETQ8QmGjr9Jz/tkAKLITIl2s7dw3A==} - optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} - engines: {node: '>= 0.8.0'} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -5186,16 +5195,16 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} - react-dom@19.2.5: - resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} peerDependencies: - react: ^19.2.5 + react: ^19.2.6 react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react@19.2.5: - resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -6421,8 +6430,6 @@ packages: snapshots: - '@aashutoshrathi/word-wrap@1.2.6': {} - '@ai-sdk/provider@1.1.3': dependencies: json-schema: 0.4.0 @@ -6914,7 +6921,7 @@ snapshots: '@babel/generator@7.27.1': dependencies: - '@babel/parser': 7.27.1 + '@babel/parser': 7.29.3 '@babel/types': 7.27.1 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 @@ -6922,7 +6929,7 @@ snapshots: '@babel/generator@7.28.3': dependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.3 '@babel/types': 7.28.5 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 @@ -6994,7 +7001,7 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.3 '@babel/types': 7.28.5 '@babel/traverse@7.27.1': @@ -7014,7 +7021,7 @@ snapshots: '@babel/code-frame': 7.27.1 '@babel/generator': 7.28.3 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.3 '@babel/template': 7.27.2 '@babel/types': 7.28.5 debug: 4.4.3 @@ -7320,24 +7327,14 @@ snapshots: '@esbuild/win32-x64@0.27.0': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@8.56.0)': - dependencies: - eslint: 8.56.0 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/eslint-utils@4.4.0(eslint@8.57.1)': - dependencies: - eslint: 8.57.1 - eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': dependencies: eslint: 8.57.1 eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.1(eslint@8.56.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': dependencies: - eslint: 8.56.0 + eslint: 9.39.4 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.10.0': {} @@ -7346,6 +7343,22 @@ snapshots: '@eslint-community/regexpp@4.12.2': {} + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 @@ -7360,18 +7373,43 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@8.56.0': {} - - '@eslint/js@8.57.1': {} - - '@humanwhocodes/config-array@0.11.13': + '@eslint/eslintrc@3.3.5': dependencies: - '@humanwhocodes/object-schema': 2.0.1 + ajv: 6.15.0 debug: 4.4.3 - minimatch: 3.1.2 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color + '@eslint/js@8.57.1': {} + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -7382,10 +7420,10 @@ snapshots: '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/object-schema@2.0.1': {} - '@humanwhocodes/object-schema@2.0.3': {} + '@humanwhocodes/retry@0.4.3': {} + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.34.5': @@ -7575,34 +7613,34 @@ snapshots: '@next/env@14.2.3': {} - '@next/env@15.5.15': {} + '@next/env@16.2.6': {} - '@next/eslint-plugin-next@16.3.0-canary.9': + '@next/eslint-plugin-next@16.2.6': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.5.15': + '@next/swc-darwin-arm64@16.2.6': optional: true - '@next/swc-darwin-x64@15.5.15': + '@next/swc-darwin-x64@16.2.6': optional: true - '@next/swc-linux-arm64-gnu@15.5.15': + '@next/swc-linux-arm64-gnu@16.2.6': optional: true - '@next/swc-linux-arm64-musl@15.5.15': + '@next/swc-linux-arm64-musl@16.2.6': optional: true - '@next/swc-linux-x64-gnu@15.5.15': + '@next/swc-linux-x64-gnu@16.2.6': optional: true - '@next/swc-linux-x64-musl@15.5.15': + '@next/swc-linux-x64-musl@16.2.6': optional: true - '@next/swc-win32-arm64-msvc@15.5.15': + '@next/swc-win32-arm64-msvc@16.2.6': optional: true - '@next/swc-win32-x64-msvc@15.5.15': + '@next/swc-win32-x64-msvc@16.2.6': optional: true '@nodable/entities@2.1.0': {} @@ -8390,6 +8428,10 @@ snapshots: '@types/range-parser@1.2.7': {} + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -8421,15 +8463,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.21.0(@typescript-eslint/parser@8.53.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@8.21.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4)(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 8.53.0(eslint@8.57.1)(typescript@5.5.4) + '@typescript-eslint/parser': 8.53.0(eslint@9.39.4)(typescript@5.5.4) '@typescript-eslint/scope-manager': 8.21.0 - '@typescript-eslint/type-utils': 8.21.0(eslint@8.57.1)(typescript@5.5.4) - '@typescript-eslint/utils': 8.21.0(eslint@8.57.1)(typescript@5.5.4) + '@typescript-eslint/type-utils': 8.21.0(eslint@9.39.4)(typescript@5.5.4) + '@typescript-eslint/utils': 8.21.0(eslint@9.39.4)(typescript@5.5.4) '@typescript-eslint/visitor-keys': 8.21.0 - eslint: 8.57.1 + eslint: 9.39.4 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -8438,55 +8480,34 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0)(typescript@4.7.4)': + '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4)(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.53.0(eslint@8.56.0)(typescript@4.7.4) + '@typescript-eslint/parser': 8.53.0(eslint@9.39.4)(typescript@5.5.4) '@typescript-eslint/scope-manager': 8.53.0 - '@typescript-eslint/type-utils': 8.53.0(eslint@8.56.0)(typescript@4.7.4) - '@typescript-eslint/utils': 8.53.0(eslint@8.56.0)(typescript@4.7.4) + '@typescript-eslint/type-utils': 8.53.0(eslint@9.39.4)(typescript@5.5.4) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.4)(typescript@5.5.4) '@typescript-eslint/visitor-keys': 8.53.0 - eslint: 8.56.0 + eslint: 9.39.4 ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@4.7.4) - typescript: 4.7.4 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4)': - dependencies: - '@typescript-eslint/scope-manager': 8.53.0 - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@4.7.4) - '@typescript-eslint/visitor-keys': 8.53.0 - debug: 4.4.3 - eslint: 8.56.0 - typescript: 4.7.4 + ts-api-utils: 2.4.0(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.53.0(eslint@8.57.1)(typescript@5.5.4)': + '@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4)': dependencies: '@typescript-eslint/scope-manager': 8.53.0 '@typescript-eslint/types': 8.53.0 '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.5.4) '@typescript-eslint/visitor-keys': 8.53.0 debug: 4.4.3 - eslint: 8.57.1 + eslint: 9.39.4 typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.53.0(typescript@4.7.4)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@4.7.4) - '@typescript-eslint/types': 8.53.0 - debug: 4.4.3 - typescript: 4.7.4 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/project-service@8.53.0(typescript@5.5.4)': dependencies: '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.5.4) @@ -8506,34 +8527,30 @@ snapshots: '@typescript-eslint/types': 8.53.0 '@typescript-eslint/visitor-keys': 8.53.0 - '@typescript-eslint/tsconfig-utils@8.53.0(typescript@4.7.4)': - dependencies: - typescript: 4.7.4 - '@typescript-eslint/tsconfig-utils@8.53.0(typescript@5.5.4)': dependencies: typescript: 5.5.4 - '@typescript-eslint/type-utils@8.21.0(eslint@8.57.1)(typescript@5.5.4)': + '@typescript-eslint/type-utils@8.21.0(eslint@9.39.4)(typescript@5.5.4)': dependencies: '@typescript-eslint/typescript-estree': 8.21.0(typescript@5.5.4) - '@typescript-eslint/utils': 8.21.0(eslint@8.57.1)(typescript@5.5.4) - debug: 4.4.1 - eslint: 8.57.1 + '@typescript-eslint/utils': 8.21.0(eslint@9.39.4)(typescript@5.5.4) + debug: 4.4.3 + eslint: 9.39.4 ts-api-utils: 2.0.0(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.53.0(eslint@8.56.0)(typescript@4.7.4)': + '@typescript-eslint/type-utils@8.53.0(eslint@9.39.4)(typescript@5.5.4)': dependencies: '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@4.7.4) - '@typescript-eslint/utils': 8.53.0(eslint@8.56.0)(typescript@4.7.4) + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.4)(typescript@5.5.4) debug: 4.4.3 - eslint: 8.56.0 - ts-api-utils: 2.4.0(typescript@4.7.4) - typescript: 4.7.4 + eslint: 9.39.4 + ts-api-utils: 2.4.0(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -8572,21 +8589,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.53.0(typescript@4.7.4)': - dependencies: - '@typescript-eslint/project-service': 8.53.0(typescript@4.7.4) - '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@4.7.4) - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/visitor-keys': 8.53.0 - debug: 4.4.3 - minimatch: 9.0.5 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@4.7.4) - typescript: 4.7.4 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.53.0(typescript@5.5.4)': dependencies: '@typescript-eslint/project-service': 8.53.0(typescript@5.5.4) @@ -8602,25 +8604,25 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.21.0(eslint@8.57.1)(typescript@5.5.4)': + '@typescript-eslint/utils@8.21.0(eslint@9.39.4)(typescript@5.5.4)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) '@typescript-eslint/scope-manager': 8.21.0 '@typescript-eslint/types': 8.21.0 '@typescript-eslint/typescript-estree': 8.21.0(typescript@5.5.4) - eslint: 8.57.1 + eslint: 9.39.4 typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.53.0(eslint@8.56.0)(typescript@4.7.4)': + '@typescript-eslint/utils@8.53.0(eslint@9.39.4)(typescript@5.5.4)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.56.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) '@typescript-eslint/scope-manager': 8.53.0 '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@4.7.4) - eslint: 8.56.0 - typescript: 4.7.4 + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.5.4) + eslint: 9.39.4 + typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -8632,15 +8634,13 @@ snapshots: '@typescript-eslint/visitor-keys@8.21.0': dependencies: '@typescript-eslint/types': 8.21.0 - eslint-visitor-keys: 4.2.0 + eslint-visitor-keys: 4.2.1 '@typescript-eslint/visitor-keys@8.53.0': dependencies: '@typescript-eslint/types': 8.53.0 eslint-visitor-keys: 4.2.1 - '@ungap/structured-clone@1.2.0': {} - '@ungap/structured-clone@1.3.0': {} '@upstash/core-analytics@0.0.6': @@ -8661,25 +8661,33 @@ snapshots: dependencies: crypto-js: 4.2.0 - '@vercel/examples-ui@1.0.5(next@15.5.15(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@vercel/examples-ui@1.0.5(next@16.2.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@swc/helpers': 0.4.14 clsx: 1.2.1 - next: 15.5.15(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + next: 16.2.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) sugar-high: 0.4.7 '@vercel/functions@1.5.0(@aws-sdk/credential-provider-web-identity@3.972.38)': optionalDependencies: '@aws-sdk/credential-provider-web-identity': 3.972.38 + '@vercel/functions@3.5.1(@aws-sdk/credential-provider-web-identity@3.972.38)': + dependencies: + '@vercel/oidc': 3.4.1 + optionalDependencies: + '@aws-sdk/credential-provider-web-identity': 3.972.38 + '@vercel/kv@0.2.2': dependencies: '@upstash/redis': 1.21.0 transitivePeerDependencies: - encoding + '@vercel/oidc@3.4.1': {} + '@vitest/expect@2.1.9': dependencies: '@vitest/spy': 2.1.9 @@ -8853,32 +8861,32 @@ snapshots: dependencies: humanize-ms: 1.2.1 - ai@2.2.22(react@19.2.5)(solid-js@1.9.10)(svelte@4.2.20)(vue@3.5.22(typescript@5.3.3)): + ai@2.2.22(react@19.2.6)(solid-js@1.9.10)(svelte@4.2.20)(vue@3.5.22(typescript@5.3.3)): dependencies: eventsource-parser: 1.0.0 nanoid: 3.3.6 solid-swr-store: 0.10.7(solid-js@1.9.10)(swr-store@0.10.6) sswr: 2.0.0(svelte@4.2.20) - swr: 2.2.0(react@19.2.5) + swr: 2.2.0(react@19.2.6) swr-store: 0.10.6 swrv: 1.0.4(vue@3.5.22(typescript@5.3.3)) optionalDependencies: - react: 19.2.5 + react: 19.2.6 solid-js: 1.9.10 svelte: 4.2.20 vue: 3.5.22(typescript@5.3.3) - ai@2.2.37(react@19.2.5)(solid-js@1.9.10)(svelte@4.2.20)(vue@3.5.22(typescript@5.5.4)): + ai@2.2.37(react@19.2.6)(solid-js@1.9.10)(svelte@4.2.20)(vue@3.5.22(typescript@5.5.4)): dependencies: eventsource-parser: 1.0.0 nanoid: 3.3.6 solid-swr-store: 0.10.7(solid-js@1.9.10)(swr-store@0.10.6) sswr: 2.0.0(svelte@4.2.20) - swr: 2.2.0(react@19.2.5) + swr: 2.2.0(react@19.2.6) swr-store: 0.10.6 swrv: 1.0.4(vue@3.5.22(typescript@5.5.4)) optionalDependencies: - react: 19.2.5 + react: 19.2.6 solid-js: 1.9.10 svelte: 4.2.20 vue: 3.5.22(typescript@5.5.4) @@ -8894,6 +8902,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -8974,7 +8989,7 @@ snapshots: es-abstract: 1.24.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 array.prototype.findlastindex@1.2.6: dependencies: @@ -8986,33 +9001,19 @@ snapshots: es-object-atoms: 1.1.1 es-shim-unscopables: 1.1.0 - array.prototype.flat@1.3.2: - dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - es-shim-unscopables: 1.0.2 - array.prototype.flat@1.3.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 es-abstract: 1.24.1 - es-shim-unscopables: 1.0.2 - - array.prototype.flatmap@1.3.2: - dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 es-abstract: 1.24.1 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 array.prototype.tosorted@1.1.4: dependencies: @@ -9020,7 +9021,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.1 es-errors: 1.3.0 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 arraybuffer.prototype.slice@1.0.2: dependencies: @@ -9104,6 +9105,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.10.29: {} + binary-extensions@2.2.0: {} binary-search@1.3.6: {} @@ -9491,12 +9494,6 @@ snapshots: shebang-command: 1.2.0 which: 1.3.1 - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -9553,10 +9550,6 @@ snapshots: optionalDependencies: supports-color: 5.5.0 - debug@4.3.4: - dependencies: - ms: 2.1.2 - debug@4.3.7: dependencies: ms: 2.1.3 @@ -9858,10 +9851,6 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.3 - es-shim-unscopables@1.0.2: - dependencies: - hasown: 2.0.3 - es-shim-unscopables@1.1.0: dependencies: hasown: 2.0.3 @@ -9875,8 +9864,8 @@ snapshots: es-to-primitive@1.3.0: dependencies: is-callable: 1.2.7 - is-date-object: 1.0.5 - is-symbol: 1.0.4 + is-date-object: 1.1.0 + is-symbol: 1.1.1 es5-ext@0.10.62: dependencies: @@ -9989,20 +9978,20 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.3.0-canary.9(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0)(typescript@4.7.4): + eslint-config-next@16.2.6(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4)(typescript@5.5.4): dependencies: - '@next/eslint-plugin-next': 16.3.0-canary.9 - eslint: 8.56.0 + '@next/eslint-plugin-next': 16.2.6 + eslint: 9.39.4 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0))(eslint@8.56.0) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) - eslint-plugin-jsx-a11y: 6.10.2(eslint@8.56.0) - eslint-plugin-react: 7.37.5(eslint@8.56.0) - eslint-plugin-react-hooks: 7.0.1(eslint@8.56.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4))(eslint@9.39.4) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4) + eslint-plugin-react: 7.37.5(eslint@9.39.4) + eslint-plugin-react-hooks: 7.0.1(eslint@9.39.4) globals: 16.4.0 - typescript-eslint: 8.53.0(eslint@8.56.0)(typescript@4.7.4) + typescript-eslint: 8.53.0(eslint@9.39.4)(typescript@5.5.4) optionalDependencies: - typescript: 4.7.4 + typescript: 5.5.4 transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-webpack @@ -10017,21 +10006,21 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7(supports-color@5.5.0) - is-core-module: 2.13.1 + is-core-module: 2.16.1 resolve: 1.22.8 transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0))(eslint@8.56.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4))(eslint@9.39.4): dependencies: debug: 4.4.3 enhanced-resolve: 5.15.0 - eslint: 8.56.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) + eslint: 9.39.4 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4) fast-glob: 3.3.2 get-tsconfig: 4.7.2 - is-core-module: 2.13.1 + is-core-module: 2.16.1 is-glob: 4.0.3 transitivePeerDependencies: - '@typescript-eslint/parser' @@ -10039,29 +10028,18 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4): dependencies: debug: 3.2.7(supports-color@5.5.0) optionalDependencies: - '@typescript-eslint/parser': 8.53.0(eslint@8.56.0)(typescript@4.7.4) - eslint: 8.56.0 + '@typescript-eslint/parser': 8.53.0(eslint@9.39.4)(typescript@5.5.4) + eslint: 9.39.4 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0))(eslint@8.56.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4))(eslint@9.39.4) transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0): - dependencies: - debug: 3.2.7(supports-color@5.5.0) - optionalDependencies: - '@typescript-eslint/parser': 8.53.0(eslint@8.56.0)(typescript@4.7.4) - eslint: 8.56.0 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0))(eslint@8.56.0) - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -10070,10 +10048,10 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7(supports-color@5.5.0) doctrine: 2.1.0 - eslint: 8.56.0 + eslint: 9.39.4 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) - hasown: 2.0.2 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4) + hasown: 2.0.3 is-core-module: 2.16.1 is-glob: 4.0.3 minimatch: 3.1.2 @@ -10084,24 +10062,24 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.53.0(eslint@8.56.0)(typescript@4.7.4) + '@typescript-eslint/parser': 8.53.0(eslint@9.39.4)(typescript@5.5.4) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@8.56.0): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 - array.prototype.flatmap: 1.3.2 + array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 axe-core: 4.11.1 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 8.56.0 - hasown: 2.0.2 + eslint: 9.39.4 + hasown: 2.0.3 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 minimatch: 3.1.2 @@ -10109,18 +10087,18 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@7.0.1(eslint@8.56.0): + eslint-plugin-react-hooks@7.0.1(eslint@9.39.4): dependencies: '@babel/core': 7.28.3 - '@babel/parser': 7.28.5 - eslint: 8.56.0 + '@babel/parser': 7.29.3 + eslint: 9.39.4 hermes-parser: 0.25.1 zod: 3.25.34 zod-validation-error: 4.0.2(zod@3.25.34) transitivePeerDependencies: - supports-color - eslint-plugin-react@7.37.5(eslint@8.56.0): + eslint-plugin-react@7.37.5(eslint@9.39.4): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -10128,9 +10106,9 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.2 - eslint: 8.56.0 + eslint: 9.39.4 estraverse: 5.3.0 - hasown: 2.0.2 + hasown: 2.0.3 jsx-ast-utils: 3.3.5 minimatch: 3.1.2 object.entries: 1.1.9 @@ -10153,32 +10131,35 @@ snapshots: esrecurse: 4.3.0 estraverse: 5.3.0 - eslint-visitor-keys@3.4.3: {} + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 - eslint-visitor-keys@4.2.0: {} + eslint-visitor-keys@3.4.3: {} eslint-visitor-keys@4.2.1: {} - eslint@8.56.0: + eslint@8.57.1: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) - '@eslint-community/regexpp': 4.10.0 + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.56.0 - '@humanwhocodes/config-array': 0.11.13 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 + '@ungap/structured-clone': 1.3.0 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 + cross-spawn: 7.0.6 + debug: 4.4.1 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - esquery: 1.5.0 + esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 6.0.1 @@ -10186,7 +10167,7 @@ snapshots: glob-parent: 6.0.2 globals: 13.24.0 graphemer: 1.4.0 - ignore: 5.3.0 + ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 @@ -10196,55 +10177,57 @@ snapshots: lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 - optionator: 0.9.3 + optionator: 0.9.4 strip-ansi: 6.0.1 text-table: 0.2.0 transitivePeerDependencies: - supports-color - eslint@8.57.1: + eslint@9.39.4: dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) - '@eslint-community/regexpp': 4.12.1 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.1 - '@humanwhocodes/config-array': 0.13.0 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.0 - ajv: 6.12.6 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.15.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1 - doctrine: 3.0.0 + debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.6.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 + file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 transitivePeerDependencies: - supports-color + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + espree@9.6.1: dependencies: acorn: 8.16.0 @@ -10253,10 +10236,6 @@ snapshots: esprima@4.0.1: {} - esquery@1.5.0: - dependencies: - estraverse: 5.3.0 - esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -10426,6 +10405,10 @@ snapshots: dependencies: flat-cache: 3.2.0 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + fill-range@7.0.1: dependencies: to-regex-range: 5.0.1 @@ -10482,6 +10465,11 @@ snapshots: keyv: 4.5.4 rimraf: 3.0.2 + flat-cache@4.0.1: + dependencies: + flatted: 3.2.9 + keyv: 4.5.4 + flatted@3.2.9: {} follow-redirects@1.16.0: {} @@ -10652,6 +10640,8 @@ snapshots: dependencies: type-fest: 0.20.2 + globals@14.0.0: {} + globals@16.4.0: {} globalthis@1.0.3: @@ -10686,7 +10676,7 @@ snapshots: graphql@16.11.0: {} - handlebars@4.7.8: + handlebars@4.7.9: dependencies: minimist: 1.2.8 neo-async: 2.6.2 @@ -10723,10 +10713,6 @@ snapshots: dependencies: has-symbols: 1.1.0 - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - hasown@2.0.3: dependencies: function-bind: 1.1.2 @@ -10771,8 +10757,6 @@ snapshots: dependencies: minimatch: 9.0.5 - ignore@5.3.0: {} - ignore@5.3.2: {} ignore@7.0.5: {} @@ -11051,6 +11035,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -11106,9 +11094,9 @@ snapshots: jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 - array.prototype.flat: 1.3.2 - object.assign: 4.1.5 - object.values: 1.1.7 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 jwa@1.4.1: dependencies: @@ -11288,6 +11276,10 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.12 + minimatch@7.4.6: dependencies: brace-expansion: 2.0.2 @@ -11313,8 +11305,6 @@ snapshots: ms@2.0.0: {} - ms@2.1.2: {} - ms@2.1.3: {} msw@2.8.4(@types/node@20.10.5)(typescript@5.5.4): @@ -11374,24 +11364,25 @@ snapshots: next-tick@1.1.0: {} - next@15.5.15(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + next@16.2.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - '@next/env': 15.5.15 + '@next/env': 16.2.6 '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.29 caniuse-lite: 1.0.30001791 postcss: 8.4.31 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - styled-jsx: 5.1.6(@babel/core@7.28.3)(react@19.2.5) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + styled-jsx: 5.1.6(@babel/core@7.28.3)(react@19.2.6) optionalDependencies: - '@next/swc-darwin-arm64': 15.5.15 - '@next/swc-darwin-x64': 15.5.15 - '@next/swc-linux-arm64-gnu': 15.5.15 - '@next/swc-linux-arm64-musl': 15.5.15 - '@next/swc-linux-x64-gnu': 15.5.15 - '@next/swc-linux-x64-musl': 15.5.15 - '@next/swc-win32-arm64-msvc': 15.5.15 - '@next/swc-win32-x64-msvc': 15.5.15 + '@next/swc-darwin-arm64': 16.2.6 + '@next/swc-darwin-x64': 16.2.6 + '@next/swc-linux-arm64-gnu': 16.2.6 + '@next/swc-linux-arm64-musl': 16.2.6 + '@next/swc-linux-x64-gnu': 16.2.6 + '@next/swc-linux-x64-musl': 16.2.6 + '@next/swc-win32-arm64-msvc': 16.2.6 + '@next/swc-win32-x64-msvc': 16.2.6 '@opentelemetry/api': 1.9.0 sharp: 0.34.5 transitivePeerDependencies: @@ -11500,12 +11491,6 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.1 - object.values@1.1.7: - dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - object.values@1.2.1: dependencies: call-bind: 1.0.8 @@ -11551,17 +11536,17 @@ snapshots: openapi-types@12.1.3: {} - openapi-zod-client@1.18.3(react@19.2.5): + openapi-zod-client@1.18.3(react@19.2.6): dependencies: '@apidevtools/swagger-parser': 10.1.1(openapi-types@12.1.3) '@liuli-util/fs-extra': 0.1.0 '@zodios/core': 10.9.6(axios@1.16.0)(zod@3.25.34) axios: 1.16.0 cac: 6.7.14 - handlebars: 4.7.8 + handlebars: 4.7.9 openapi-types: 12.1.3 openapi3-ts: 3.1.0 - pastable: 2.2.1(react@19.2.5) + pastable: 2.2.1(react@19.2.6) prettier: 2.8.8 tanu: 0.1.13 ts-pattern: 5.8.0 @@ -11577,15 +11562,6 @@ snapshots: dependencies: yaml: 2.4.1 - optionator@0.9.3: - dependencies: - '@aashutoshrathi/word-wrap': 1.2.6 - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -11645,13 +11621,13 @@ snapshots: parseurl@1.3.3: {} - pastable@2.2.1(react@19.2.5): + pastable@2.2.1(react@19.2.6): dependencies: '@babel/core': 7.28.3 ts-toolbelt: 9.6.0 type-fest: 3.13.1 optionalDependencies: - react: 19.2.5 + react: 19.2.6 transitivePeerDependencies: - supports-color @@ -11855,14 +11831,14 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - react-dom@19.2.5(react@19.2.5): + react-dom@19.2.6(react@19.2.6): dependencies: - react: 19.2.5 + react: 19.2.6 scheduler: 0.27.0 react-is@16.13.1: {} - react@19.2.5: {} + react@19.2.6: {} read-cache@1.0.0: dependencies: @@ -11950,7 +11926,7 @@ snapshots: resolve@2.0.0-next.5: dependencies: - is-core-module: 2.13.1 + is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -12421,7 +12397,7 @@ snapshots: string.prototype.repeat@1.0.0: dependencies: define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.24.1 string.prototype.trim@1.2.10: dependencies: @@ -12482,10 +12458,10 @@ snapshots: strnum@2.2.3: {} - styled-jsx@5.1.6(@babel/core@7.28.3)(react@19.2.5): + styled-jsx@5.1.6(@babel/core@7.28.3)(react@19.2.6): dependencies: client-only: 0.0.1 - react: 19.2.5 + react: 19.2.6 optionalDependencies: '@babel/core': 7.28.3 @@ -12534,10 +12510,10 @@ snapshots: dependencies: dequal: 2.0.3 - swr@2.2.0(react@19.2.5): + swr@2.2.0(react@19.2.6): dependencies: - react: 19.2.5 - use-sync-external-store: 1.2.0(react@19.2.5) + react: 19.2.6 + use-sync-external-store: 1.2.0(react@19.2.6) swrev@4.0.0: {} @@ -12657,10 +12633,6 @@ snapshots: dependencies: typescript: 5.5.4 - ts-api-utils@2.4.0(typescript@4.7.4): - dependencies: - typescript: 4.7.4 - ts-api-utils@2.4.0(typescript@5.5.4): dependencies: typescript: 5.5.4 @@ -12836,7 +12808,7 @@ snapshots: typed-array-byte-length@1.0.3: dependencies: call-bind: 1.0.8 - for-each: 0.3.3 + for-each: 0.3.5 gopd: 1.2.0 has-proto: 1.2.0 is-typed-array: 1.1.15 @@ -12853,7 +12825,7 @@ snapshots: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 - for-each: 0.3.3 + for-each: 0.3.5 gopd: 1.2.0 has-proto: 1.2.0 is-typed-array: 1.1.15 @@ -12868,7 +12840,7 @@ snapshots: typed-array-length@1.0.7: dependencies: call-bind: 1.0.8 - for-each: 0.3.3 + for-each: 0.3.5 gopd: 1.2.0 is-typed-array: 1.1.15 possible-typed-array-names: 1.1.0 @@ -12878,14 +12850,14 @@ snapshots: dependencies: is-typedarray: 1.0.0 - typescript-eslint@8.53.0(eslint@8.56.0)(typescript@4.7.4): + typescript-eslint@8.53.0(eslint@9.39.4)(typescript@5.5.4): dependencies: - '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@8.56.0)(typescript@4.7.4))(eslint@8.56.0)(typescript@4.7.4) - '@typescript-eslint/parser': 8.53.0(eslint@8.56.0)(typescript@4.7.4) - '@typescript-eslint/typescript-estree': 8.53.0(typescript@4.7.4) - '@typescript-eslint/utils': 8.53.0(eslint@8.56.0)(typescript@4.7.4) - eslint: 8.56.0 - typescript: 4.7.4 + '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4)(typescript@5.5.4))(eslint@9.39.4)(typescript@5.5.4) + '@typescript-eslint/parser': 8.53.0(eslint@9.39.4)(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.4)(typescript@5.5.4) + eslint: 9.39.4 + typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -12955,7 +12927,7 @@ snapshots: uri-js@4.4.1: dependencies: - punycode: 2.1.1 + punycode: 2.3.1 url-parse@1.5.10: dependencies: @@ -12967,9 +12939,9 @@ snapshots: punycode: 1.3.2 querystring: 0.2.0 - use-sync-external-store@1.2.0(react@19.2.5): + use-sync-external-store@1.2.0(react@19.2.6): dependencies: - react: 19.2.5 + react: 19.2.6 utf-8-validate@5.0.10: dependencies: From bf3264fc2bb59317ed8b6160e5bd5065714bda02 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 15:41:04 -0700 Subject: [PATCH 09/22] deploy to vercel functions --- apis/vercel/app/api/v1/[...slug]/route.ts | 96 +++ apis/vercel/next.config.js | 3 - apis/vercel/pages/api/v1/[...slug].ts | 57 -- packages/proxy/edge/index.ts | 4 +- pnpm-lock.yaml | 990 ++++++++-------------- 5 files changed, 437 insertions(+), 713 deletions(-) create mode 100644 apis/vercel/app/api/v1/[...slug]/route.ts delete mode 100644 apis/vercel/pages/api/v1/[...slug].ts diff --git a/apis/vercel/app/api/v1/[...slug]/route.ts b/apis/vercel/app/api/v1/[...slug]/route.ts new file mode 100644 index 00000000..92822386 --- /dev/null +++ b/apis/vercel/app/api/v1/[...slug]/route.ts @@ -0,0 +1,96 @@ +import dns from "node:dns"; +import { kv } from "@vercel/kv"; +import { waitUntil } from "@vercel/functions"; +import { EdgeProxyV1, CacheSetOptions } from "@braintrust/proxy/edge"; +import { Agent, setGlobalDispatcher } from "undici"; + +dns.setDefaultResultOrder("ipv4first"); + +setGlobalDispatcher( + new Agent({ + keepAliveTimeout: 1, + keepAliveMaxTimeout: 1, + }), +); + +const KVCache = { + get: (key: string) => kv.get(key), + set: async (key: string, value: T, opts: CacheSetOptions) => { + await kv.set(key, value, opts.ttl !== undefined ? { ex: opts.ttl } : {}); + }, +}; + +const proxyHandler = EdgeProxyV1({ + getRelativeURL: (request) => { + // App Router route is /api/v1/[...slug] — strip the prefix to get + // the upstream path (e.g. "/chat/completions"). + const url = new URL(request.url); + return url.pathname.replace(/^\/api\/v1/, ""); + }, + cors: true, + credentialsCache: KVCache, + completionsCache: KVCache, + braintrustApiUrl: process.env.BRAINTRUST_APP_URL, +}); + +const ctx = { + waitUntil(promise: Promise) { + waitUntil( + promise.catch((error) => { + console.warn("Background task failed", error); + }), + ); + }, +}; + +async function proxy(request: Request): Promise { + const requestId = + request.headers.get("x-request-id") ?? + Math.random().toString(36).slice(2, 10); + const start = Date.now(); + const log = (msg: string, extra?: Record) => { + console.log( + JSON.stringify({ + requestId, + method: request.method, + path: new URL(request.url).pathname, + elapsedMs: Date.now() - start, + msg, + ...extra, + }), + ); + }; + + log("route:start", { + hasAuth: request.headers.has("authorization"), + braintrustApiUrl: process.env.BRAINTRUST_APP_URL, + }); + + try { + const response = await proxyHandler(request, ctx); + log("route:after-handler", { status: response.status }); + response.headers.set("x-request-id", requestId); + return response; + } catch (error) { + log("route:error", { error: String(error) }); + return new Response( + JSON.stringify({ + error: error instanceof Error ? error.message : String(error), + requestId, + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + "x-request-id": requestId, + }, + }, + ); + } +} + +export const GET = proxy; +export const POST = proxy; +export const OPTIONS = proxy; + +export const dynamic = "force-dynamic"; diff --git a/apis/vercel/next.config.js b/apis/vercel/next.config.js index 11317887..025ec264 100644 --- a/apis/vercel/next.config.js +++ b/apis/vercel/next.config.js @@ -8,9 +8,6 @@ const nextConfig = { typescript: { ignoreBuildErrors: true, }, - eslint: { - ignoreDuringBuilds: true, - }, }; module.exports = nextConfig; diff --git a/apis/vercel/pages/api/v1/[...slug].ts b/apis/vercel/pages/api/v1/[...slug].ts deleted file mode 100644 index 053fb8bc..00000000 --- a/apis/vercel/pages/api/v1/[...slug].ts +++ /dev/null @@ -1,57 +0,0 @@ -import dns from "node:dns"; -import { kv } from "@vercel/kv"; -import { waitUntil } from "@vercel/functions"; -import { EdgeProxyV1, CacheSetOptions } from "@braintrust/proxy/edge"; -import { Agent, setGlobalDispatcher } from "undici"; - -dns.setDefaultResultOrder("ipv4first"); - -setGlobalDispatcher( - new Agent({ - keepAliveTimeout: 1, - keepAliveMaxTimeout: 1, - }), -); - -const KVCache = { - get: kv.get, - set: async (key: string, value: T, opts: CacheSetOptions) => { - await kv.set( - key, - value, - opts.ttl !== undefined - ? { - ex: opts.ttl, - } - : {}, - ); - }, -}; - -const handler = EdgeProxyV1({ - getRelativeURL: (request) => { - const url = new URL(request.url); - const params = url.searchParams.getAll("slug"); - return "/" + params.join("/"); - }, - cors: true, - credentialsCache: KVCache, - completionsCache: KVCache, - braintrustApiUrl: process.env.BRAINTRUST_APP_URL, -}); - -export default async function route(request: Request): Promise { - const ctx = { - waitUntil(promise: Promise) { - waitUntil( - promise.catch((error) => { - console.warn("Background task failed", error); - }), - ); - }, - }; - - const response = await handler(request, ctx); - response.headers.set("Connection", "close"); - return response; -} diff --git a/packages/proxy/edge/index.ts b/packages/proxy/edge/index.ts index e81f57ec..6c99b0f5 100644 --- a/packages/proxy/edge/index.ts +++ b/packages/proxy/edge/index.ts @@ -405,9 +405,7 @@ export function EdgeProxyV1(opts: ProxyOpts) { onBillingEvent: opts.onBillingEvent, }); } catch (e) { - // Log so the underlying cause shows up in Vercel/Cloudflare function - // logs. The body still echoes the error message for the caller, but - // without this, the only signal at the proxy is "status=400". + // log error to vercel console.error("EdgeProxyV1 request failed", e); return new Response(`${e}`, { status: 400, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50f35e22..f7717ad3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,7 +15,7 @@ importers: devDependencies: '@types/node': specifier: ^20.19.0 - version: 20.19.39 + version: 20.19.40 eslint: specifier: ^9.39.4 version: 9.39.4(jiti@1.21.7) @@ -30,13 +30,13 @@ importers: version: 2.5.6 vite: specifier: 7.2.7 - version: 7.2.7(@types/node@20.19.39)(jiti@1.21.7) + version: 7.2.7(@types/node@20.19.40)(jiti@1.21.7) vite-tsconfig-paths: specifier: ^6.1.1 - version: 6.1.1(typescript@5.9.3)(vite@7.2.7(@types/node@20.19.39)(jiti@1.21.7)) + version: 6.1.1(typescript@5.5.4)(vite@7.2.7(@types/node@20.19.40)(jiti@1.21.7)) vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.39)(msw@2.8.4(@types/node@20.19.39)(typescript@5.9.3))(vite@7.2.7(@types/node@20.19.39)(jiti@1.21.7)) + version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(msw@2.8.4(@types/node@20.19.40)(typescript@5.5.4))(vite@7.2.7(@types/node@20.19.40)(jiti@1.21.7)) apis/cloudflare: dependencies: @@ -70,19 +70,19 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: ^4.20260505.1 - version: 4.20260505.1 + version: 4.20260509.1 itty-router: specifier: ^3.0.12 version: 3.0.12 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@1.21.7)(postcss@8.5.14)(typescript@5.9.3) + version: 8.5.1(jiti@1.21.7)(postcss@8.5.13)(typescript@5.3.3) typescript: specifier: ^5.0.4 - version: 5.9.3 + version: 5.3.3 wrangler: specifier: 4.88.0 - version: 4.88.0(@cloudflare/workers-types@4.20260505.1)(bufferutil@4.0.8)(utf-8-validate@5.0.10) + version: 4.88.0(@cloudflare/workers-types@4.20260509.1)(bufferutil@4.0.8)(utf-8-validate@5.0.10) apis/node: dependencies: @@ -146,7 +146,7 @@ importers: version: 5.0.6 '@types/node': specifier: ^20.19.0 - version: 20.19.39 + version: 20.19.40 typescript: specifier: ^5.0.4 version: 5.3.3 @@ -192,7 +192,7 @@ importers: version: 19.2.3(@types/react@19.2.14) autoprefixer: specifier: ^10.4.14 - version: 10.4.14(postcss@8.5.14) + version: 10.4.14(postcss@8.5.13) eslint: specifier: ^9.39.4 version: 9.39.4(jiti@1.21.7) @@ -201,7 +201,7 @@ importers: version: 16.2.6(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4) postcss: specifier: ^8.5.10 - version: 8.5.14 + version: 8.5.13 tailwindcss: specifier: ^3.4.19 version: 3.4.19 @@ -286,7 +286,7 @@ importers: version: 9.0.7 '@types/node': specifier: ^20.19.0 - version: 20.19.39 + version: 20.19.40 '@types/uuid': specifier: ^9.0.7 version: 9.0.7 @@ -304,25 +304,25 @@ importers: version: 0.27.0 msw: specifier: ^2.8.2 - version: 2.8.4(@types/node@20.19.39)(typescript@5.5.4) + version: 2.8.4(@types/node@20.19.40)(typescript@5.5.4) openapi-zod-client: specifier: ^1.18.3 version: 1.18.3(react@19.2.6) tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@1.21.7)(postcss@8.5.14)(typescript@5.5.4) + version: 8.5.1(jiti@1.21.7)(postcss@8.5.13)(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 vite: specifier: 7.2.7 - version: 7.2.7(@types/node@20.19.39)(jiti@1.21.7) + version: 7.2.7(@types/node@20.19.40)(jiti@1.21.7) vite-tsconfig-paths: specifier: ^6.1.1 - version: 6.1.1(typescript@5.5.4)(vite@7.2.7(@types/node@20.19.39)(jiti@1.21.7)) + version: 6.1.1(typescript@5.5.4)(vite@7.2.7(@types/node@20.19.40)(jiti@1.21.7)) vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.39)(msw@2.8.4(@types/node@20.19.39)(typescript@5.5.4))(vite@7.2.7(@types/node@20.19.39)(jiti@1.21.7)) + version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(msw@2.8.4(@types/node@20.19.40)(typescript@5.5.4))(vite@7.2.7(@types/node@20.19.40)(jiti@1.21.7)) yargs: specifier: ^17.7.2 version: 17.7.2 @@ -586,6 +586,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/parser@7.29.3': resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} engines: {node: '>=6.0.0'} @@ -607,6 +612,10 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -666,8 +675,8 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workers-types@4.20260505.1': - resolution: {integrity: sha512-Uz9D2hcwB4/pdnmCU7RsgknY8TQ5st0cQMMN6h/hvWt1TCt99GUkbi6dMgWdP7jXfIfh+S/EI5zQugI9RZn4Bw==} + '@cloudflare/workers-types@4.20260509.1': + resolution: {integrity: sha512-jFlTTD+0MK/01TdL5sHIsQ8RqzfmvBsGl4hSp87INv2+JIs/JF6EL9J8enuCz6z3fNdfOKISNbGCIrzZRXVrcw==} '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} @@ -680,8 +689,8 @@ packages: '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -698,8 +707,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -716,8 +725,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -734,8 +743,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -752,8 +761,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -770,8 +779,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -788,8 +797,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -806,8 +815,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -824,8 +833,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -842,8 +851,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -860,8 +869,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -878,8 +887,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -896,8 +905,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -914,8 +923,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -932,8 +941,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -950,8 +959,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -968,8 +977,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -986,8 +995,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -1004,8 +1013,8 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -1022,8 +1031,8 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -1040,8 +1049,8 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -1058,8 +1067,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -1076,8 +1085,8 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -1094,8 +1103,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -1112,8 +1121,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -1130,8 +1139,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1658,121 +1667,55 @@ packages: cpu: [arm] os: [android] - '@rollup/rollup-android-arm-eabi@4.60.3': - resolution: {integrity: sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==} - cpu: [arm] - os: [android] - '@rollup/rollup-android-arm64@4.46.4': resolution: {integrity: sha512-FGJYXvYdn8Bs6lAlBZYT5n+4x0ciEp4cmttsvKAZc/c8/JiPaQK8u0c/86vKX8lA7OY/+37lIQSe0YoAImvBAA==} cpu: [arm64] os: [android] - '@rollup/rollup-android-arm64@4.60.3': - resolution: {integrity: sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==} - cpu: [arm64] - os: [android] - '@rollup/rollup-darwin-arm64@4.46.4': resolution: {integrity: sha512-/9qwE/BM7ATw/W/OFEMTm3dmywbJyLQb4f4v5nmOjgYxPIGpw7HaxRi6LnD4Pjn/q7k55FGeHe1/OD02w63apA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-arm64@4.60.3': - resolution: {integrity: sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==} - cpu: [arm64] - os: [darwin] - '@rollup/rollup-darwin-x64@4.46.4': resolution: {integrity: sha512-QkWfNbeRuzFnv2d0aPlrzcA3Ebq2mE8kX/5Pl7VdRShbPBjSnom7dbT8E3Jmhxo2RL784hyqGvR5KHavCJQciw==} cpu: [x64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.3': - resolution: {integrity: sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==} - cpu: [x64] - os: [darwin] - '@rollup/rollup-freebsd-arm64@4.46.4': resolution: {integrity: sha512-+ToyOMYnSfV8D+ckxO6NthPln/PDNp1P6INcNypfZ7muLmEvPKXqduUiD8DlJpMMT8LxHcE5W0dK9kXfJke9Zw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-arm64@4.60.3': - resolution: {integrity: sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==} - cpu: [arm64] - os: [freebsd] - '@rollup/rollup-freebsd-x64@4.46.4': resolution: {integrity: sha512-cGT6ey/W+sje6zywbLiqmkfkO210FgRz7tepWAzzEVgQU8Hn91JJmQWNqs55IuglG8sJdzk7XfNgmGRtcYlo1w==} cpu: [x64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.60.3': - resolution: {integrity: sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==} - cpu: [x64] - os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.46.4': resolution: {integrity: sha512-9fhTJyOb275w5RofPSl8lpr4jFowd+H4oQKJ9XTYzD1JWgxdZKE8bA6d4npuiMemkecQOcigX01FNZNCYnQBdA==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-gnueabihf@4.60.3': - resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} - cpu: [arm] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.46.4': resolution: {integrity: sha512-+6kCIM5Zjvz2HwPl/udgVs07tPMIp1VU2Y0c72ezjOvSvEfAIWsUgpcSDvnC7g9NrjYR6X9bZT92mZZ90TfvXw==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm-musleabihf@4.60.3': - resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} - cpu: [arm] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.46.4': resolution: {integrity: sha512-SWuXdnsayCZL4lXoo6jn0yyAj7TTjWE4NwDVt9s7cmu6poMhtiras5c8h6Ih6Y0Zk6Z+8t/mLumvpdSPTWub2Q==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-gnu@4.60.3': - resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.46.4': resolution: {integrity: sha512-vDknMDqtMhrrroa5kyX6tuC0aRZZlQ+ipDfbXd2YGz5HeV2t8HOl/FDAd2ynhs7Ki5VooWiiZcCtxiZ4IjqZwQ==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-musl@4.60.3': - resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.60.3': - resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.60.3': - resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} - cpu: [loong64] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-loongarch64-gnu@4.46.4': resolution: {integrity: sha512-mCBkjRZWhvjtl/x+Bd4fQkWZT8canStKDxGrHlBiTnZmJnWygGcvBylzLVCZXka4dco5ymkWhZlLwKCGFF4ivw==} cpu: [loong64] @@ -1785,123 +1728,51 @@ packages: os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-gnu@4.60.3': - resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.60.3': - resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} - cpu: [ppc64] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.46.4': resolution: {integrity: sha512-r0WKLSfFAK8ucG024v2yiLSJMedoWvk8yWqfNICX28NHDGeu3F/wBf8KG6mclghx4FsLePxJr/9N8rIj1PtCnw==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-gnu@4.60.3': - resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.46.4': resolution: {integrity: sha512-IaizpPP2UQU3MNyPH1u0Xxbm73D+4OupL0bjo4Hm0496e2wg3zuvoAIhubkD1NGy9fXILEExPQy87mweujEatA==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-musl@4.60.3': - resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} - cpu: [riscv64] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.46.4': resolution: {integrity: sha512-aCM29orANR0a8wk896p6UEgIfupReupnmISz6SUwMIwTGaTI8MuKdE0OD2LvEg8ondDyZdMvnaN3bW4nFbATPA==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-s390x-gnu@4.60.3': - resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.46.4': resolution: {integrity: sha512-0Xj1vZE3cbr/wda8d/m+UeuSL+TDpuozzdD4QaSzu/xSOMK0Su5RhIkF7KVHFQsobemUNHPLEcYllL7ZTCP/Cg==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.3': - resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} - cpu: [x64] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.46.4': resolution: {integrity: sha512-kM/orjpolfA5yxsx84kI6bnK47AAZuWxglGKcNmokw2yy9i5eHY5UAjcX45jemTJnfHAWo3/hOoRqEeeTdL5hw==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-linux-x64-musl@4.60.3': - resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.60.3': - resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.60.3': - resolution: {integrity: sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==} - cpu: [arm64] - os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.46.4': resolution: {integrity: sha512-cNLH4psMEsWKILW0isbpQA2OvjXLbKvnkcJFmqAptPQbtLrobiapBJVj6RoIvg6UXVp5w0wnIfd/Q56cNpF+Ew==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-arm64-msvc@4.60.3': - resolution: {integrity: sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==} - cpu: [arm64] - os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.46.4': resolution: {integrity: sha512-OiEa5lRhiANpv4SfwYVgQ3opYWi/QmPDC5ve21m8G9pf6ZO+aX1g2EEF1/IFaM1xPSP7mK0msTRXlPs6mIagkg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.3': - resolution: {integrity: sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.60.3': - resolution: {integrity: sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==} - cpu: [x64] - os: [win32] - '@rollup/rollup-win32-x64-msvc@4.46.4': resolution: {integrity: sha512-IKL9mewGZ5UuuX4NQlwOmxPyqielvkAPUS2s1cl6yWjjQvyN3h5JTdVFGD5Jr5xMjRC8setOfGQDVgX8V+dkjg==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.3': - resolution: {integrity: sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==} - cpu: [x64] - os: [win32] - '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -2195,8 +2066,8 @@ packages: '@types/node@18.19.123': resolution: {integrity: sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==} - '@types/node@20.19.39': - resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} + '@types/node@20.19.40': + resolution: {integrity: sha512-xxx6M2IpSTnnKcR0cMvIiohkiCx20/oRPtWGbenFygKCGl3zqUzdNjQ/1V4solq1LU+dgv0nQzeGOuqkqZGg0Q==} '@types/node@22.19.18': resolution: {integrity: sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==} @@ -2689,8 +2560,8 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} binary-search@1.3.6: @@ -2720,8 +2591,8 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -3005,6 +2876,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -3150,8 +3025,8 @@ packages: es6-symbol@3.1.3: resolution: {integrity: sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==} - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} engines: {node: '>=18'} hasBin: true @@ -3344,8 +3219,8 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - express@4.22.1: - resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} express@5.2.1: @@ -3421,8 +3296,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} @@ -3505,6 +3380,9 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -3578,13 +3456,24 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + has-proto@1.2.0: resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} engines: {node: '>= 0.4'} + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -3683,6 +3572,9 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} + is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -4035,6 +3927,9 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -4144,8 +4039,8 @@ packages: encoding: optional: true - node-gyp-build@4.8.4: - resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + node-gyp-build@4.7.0: + resolution: {integrity: sha512-PbZERfeFdrHQOOXiAKOY0VPbykZy90ndPKk0d+CFDegTKmWp1VgOTz2xACVbr1BjCWxrQp68CXtvNsveFhqDJg==} hasBin: true node-releases@2.0.13: @@ -4325,18 +4220,14 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.2: - resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -4345,10 +4236,6 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -4411,10 +4298,6 @@ packages: resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.14: - resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} - engines: {node: ^10 || ^12 || >=14} - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4458,10 +4341,6 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} - qs@6.14.2: - resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} - engines: {node: '>=0.6'} - qs@6.15.1: resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} engines: {node: '>=0.6'} @@ -4565,11 +4444,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rollup@4.60.3: - resolution: {integrity: sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -4670,8 +4544,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} side-channel-map@1.0.1: @@ -4819,11 +4693,6 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - sugar-high@0.4.7: resolution: {integrity: sha512-vCg55qNhUBbBylR1DNWAYvqlwSVPebBJPh+fNzknN70b9CIyS1xoCRa0CnRzrAw4RYiqprteDfZg8ZeToZqqug==} @@ -4897,10 +4766,6 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} - engines: {node: '>=12.0.0'} - tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} @@ -4927,6 +4792,12 @@ packages: ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -5101,8 +4972,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - typescript@4.9.5: - resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + typescript@4.7.4: + resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} engines: {node: '>=4.2.0'} hasBin: true @@ -5116,11 +4987,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} @@ -5997,10 +5863,10 @@ snapshots: '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) '@babel/helpers': 7.28.3 - '@babel/parser': 7.29.3 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 '@babel/traverse': 7.28.3 - '@babel/types': 7.29.0 + '@babel/types': 7.28.5 convert-source-map: 2.0.0 debug: 4.4.3 gensync: 1.0.0-beta.2 @@ -6012,7 +5878,7 @@ snapshots: '@babel/generator@7.28.3': dependencies: '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/types': 7.28.5 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -6030,7 +5896,7 @@ snapshots: '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.28.3 - '@babel/types': 7.29.0 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -6054,12 +5920,16 @@ snapshots: '@babel/helpers@7.28.3': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.29.0 + '@babel/types': 7.28.5 '@babel/parser@7.28.3': dependencies: '@babel/types': 7.28.2 + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + '@babel/parser@7.29.3': dependencies: '@babel/types': 7.29.0 @@ -6072,7 +5942,7 @@ snapshots: dependencies: '@babel/code-frame': 7.27.1 '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/types': 7.28.5 '@babel/traverse@7.28.3': dependencies: @@ -6081,7 +5951,7 @@ snapshots: '@babel/helper-globals': 7.28.0 '@babel/parser': 7.29.3 '@babel/template': 7.27.2 - '@babel/types': 7.29.0 + '@babel/types': 7.28.5 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -6091,6 +5961,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -6104,7 +5979,7 @@ snapshots: '@bundled-es-modules/statuses@1.0.1': dependencies: - statuses: 2.0.2 + statuses: 2.0.1 '@bundled-es-modules/tough-cookie@0.1.6': dependencies: @@ -6134,7 +6009,7 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260504.1': optional: true - '@cloudflare/workers-types@4.20260505.1': {} + '@cloudflare/workers-types@4.20260509.1': {} '@colors/colors@1.5.0': optional: true @@ -6148,7 +6023,7 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.25.12': + '@esbuild/aix-ppc64@0.25.9': optional: true '@esbuild/aix-ppc64@0.27.0': @@ -6157,7 +6032,7 @@ snapshots: '@esbuild/aix-ppc64@0.27.3': optional: true - '@esbuild/android-arm64@0.25.12': + '@esbuild/android-arm64@0.25.9': optional: true '@esbuild/android-arm64@0.27.0': @@ -6166,7 +6041,7 @@ snapshots: '@esbuild/android-arm64@0.27.3': optional: true - '@esbuild/android-arm@0.25.12': + '@esbuild/android-arm@0.25.9': optional: true '@esbuild/android-arm@0.27.0': @@ -6175,7 +6050,7 @@ snapshots: '@esbuild/android-arm@0.27.3': optional: true - '@esbuild/android-x64@0.25.12': + '@esbuild/android-x64@0.25.9': optional: true '@esbuild/android-x64@0.27.0': @@ -6184,7 +6059,7 @@ snapshots: '@esbuild/android-x64@0.27.3': optional: true - '@esbuild/darwin-arm64@0.25.12': + '@esbuild/darwin-arm64@0.25.9': optional: true '@esbuild/darwin-arm64@0.27.0': @@ -6193,7 +6068,7 @@ snapshots: '@esbuild/darwin-arm64@0.27.3': optional: true - '@esbuild/darwin-x64@0.25.12': + '@esbuild/darwin-x64@0.25.9': optional: true '@esbuild/darwin-x64@0.27.0': @@ -6202,7 +6077,7 @@ snapshots: '@esbuild/darwin-x64@0.27.3': optional: true - '@esbuild/freebsd-arm64@0.25.12': + '@esbuild/freebsd-arm64@0.25.9': optional: true '@esbuild/freebsd-arm64@0.27.0': @@ -6211,7 +6086,7 @@ snapshots: '@esbuild/freebsd-arm64@0.27.3': optional: true - '@esbuild/freebsd-x64@0.25.12': + '@esbuild/freebsd-x64@0.25.9': optional: true '@esbuild/freebsd-x64@0.27.0': @@ -6220,7 +6095,7 @@ snapshots: '@esbuild/freebsd-x64@0.27.3': optional: true - '@esbuild/linux-arm64@0.25.12': + '@esbuild/linux-arm64@0.25.9': optional: true '@esbuild/linux-arm64@0.27.0': @@ -6229,7 +6104,7 @@ snapshots: '@esbuild/linux-arm64@0.27.3': optional: true - '@esbuild/linux-arm@0.25.12': + '@esbuild/linux-arm@0.25.9': optional: true '@esbuild/linux-arm@0.27.0': @@ -6238,7 +6113,7 @@ snapshots: '@esbuild/linux-arm@0.27.3': optional: true - '@esbuild/linux-ia32@0.25.12': + '@esbuild/linux-ia32@0.25.9': optional: true '@esbuild/linux-ia32@0.27.0': @@ -6247,7 +6122,7 @@ snapshots: '@esbuild/linux-ia32@0.27.3': optional: true - '@esbuild/linux-loong64@0.25.12': + '@esbuild/linux-loong64@0.25.9': optional: true '@esbuild/linux-loong64@0.27.0': @@ -6256,7 +6131,7 @@ snapshots: '@esbuild/linux-loong64@0.27.3': optional: true - '@esbuild/linux-mips64el@0.25.12': + '@esbuild/linux-mips64el@0.25.9': optional: true '@esbuild/linux-mips64el@0.27.0': @@ -6265,7 +6140,7 @@ snapshots: '@esbuild/linux-mips64el@0.27.3': optional: true - '@esbuild/linux-ppc64@0.25.12': + '@esbuild/linux-ppc64@0.25.9': optional: true '@esbuild/linux-ppc64@0.27.0': @@ -6274,7 +6149,7 @@ snapshots: '@esbuild/linux-ppc64@0.27.3': optional: true - '@esbuild/linux-riscv64@0.25.12': + '@esbuild/linux-riscv64@0.25.9': optional: true '@esbuild/linux-riscv64@0.27.0': @@ -6283,7 +6158,7 @@ snapshots: '@esbuild/linux-riscv64@0.27.3': optional: true - '@esbuild/linux-s390x@0.25.12': + '@esbuild/linux-s390x@0.25.9': optional: true '@esbuild/linux-s390x@0.27.0': @@ -6292,7 +6167,7 @@ snapshots: '@esbuild/linux-s390x@0.27.3': optional: true - '@esbuild/linux-x64@0.25.12': + '@esbuild/linux-x64@0.25.9': optional: true '@esbuild/linux-x64@0.27.0': @@ -6301,7 +6176,7 @@ snapshots: '@esbuild/linux-x64@0.27.3': optional: true - '@esbuild/netbsd-arm64@0.25.12': + '@esbuild/netbsd-arm64@0.25.9': optional: true '@esbuild/netbsd-arm64@0.27.0': @@ -6310,7 +6185,7 @@ snapshots: '@esbuild/netbsd-arm64@0.27.3': optional: true - '@esbuild/netbsd-x64@0.25.12': + '@esbuild/netbsd-x64@0.25.9': optional: true '@esbuild/netbsd-x64@0.27.0': @@ -6319,7 +6194,7 @@ snapshots: '@esbuild/netbsd-x64@0.27.3': optional: true - '@esbuild/openbsd-arm64@0.25.12': + '@esbuild/openbsd-arm64@0.25.9': optional: true '@esbuild/openbsd-arm64@0.27.0': @@ -6328,7 +6203,7 @@ snapshots: '@esbuild/openbsd-arm64@0.27.3': optional: true - '@esbuild/openbsd-x64@0.25.12': + '@esbuild/openbsd-x64@0.25.9': optional: true '@esbuild/openbsd-x64@0.27.0': @@ -6337,7 +6212,7 @@ snapshots: '@esbuild/openbsd-x64@0.27.3': optional: true - '@esbuild/openharmony-arm64@0.25.12': + '@esbuild/openharmony-arm64@0.25.9': optional: true '@esbuild/openharmony-arm64@0.27.0': @@ -6346,7 +6221,7 @@ snapshots: '@esbuild/openharmony-arm64@0.27.3': optional: true - '@esbuild/sunos-x64@0.25.12': + '@esbuild/sunos-x64@0.25.9': optional: true '@esbuild/sunos-x64@0.27.0': @@ -6355,7 +6230,7 @@ snapshots: '@esbuild/sunos-x64@0.27.3': optional: true - '@esbuild/win32-arm64@0.25.12': + '@esbuild/win32-arm64@0.25.9': optional: true '@esbuild/win32-arm64@0.27.0': @@ -6364,7 +6239,7 @@ snapshots: '@esbuild/win32-arm64@0.27.3': optional: true - '@esbuild/win32-ia32@0.25.12': + '@esbuild/win32-ia32@0.25.9': optional: true '@esbuild/win32-ia32@0.27.0': @@ -6373,7 +6248,7 @@ snapshots: '@esbuild/win32-ia32@0.27.3': optional: true - '@esbuild/win32-x64@0.25.12': + '@esbuild/win32-x64@0.25.9': optional: true '@esbuild/win32-x64@0.27.0': @@ -6540,17 +6415,17 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@inquirer/confirm@5.1.12(@types/node@20.19.39)': + '@inquirer/confirm@5.1.12(@types/node@20.19.40)': dependencies: - '@inquirer/core': 10.1.13(@types/node@20.19.39) - '@inquirer/type': 3.0.7(@types/node@20.19.39) + '@inquirer/core': 10.1.13(@types/node@20.19.40) + '@inquirer/type': 3.0.7(@types/node@20.19.40) optionalDependencies: - '@types/node': 20.19.39 + '@types/node': 20.19.40 - '@inquirer/core@10.1.13(@types/node@20.19.39)': + '@inquirer/core@10.1.13(@types/node@20.19.40)': dependencies: '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7(@types/node@20.19.39) + '@inquirer/type': 3.0.7(@types/node@20.19.40) ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 @@ -6558,13 +6433,13 @@ snapshots: wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 20.19.39 + '@types/node': 20.19.40 '@inquirer/figures@1.0.12': {} - '@inquirer/type@3.0.7(@types/node@20.19.39)': + '@inquirer/type@3.0.7(@types/node@20.19.40)': optionalDependencies: - '@types/node': 20.19.39 + '@types/node': 20.19.40 '@isaacs/cliui@8.0.2': dependencies: @@ -6819,138 +6694,63 @@ snapshots: '@rollup/rollup-android-arm-eabi@4.46.4': optional: true - '@rollup/rollup-android-arm-eabi@4.60.3': - optional: true - '@rollup/rollup-android-arm64@4.46.4': optional: true - '@rollup/rollup-android-arm64@4.60.3': - optional: true - '@rollup/rollup-darwin-arm64@4.46.4': optional: true - '@rollup/rollup-darwin-arm64@4.60.3': - optional: true - '@rollup/rollup-darwin-x64@4.46.4': optional: true - '@rollup/rollup-darwin-x64@4.60.3': - optional: true - '@rollup/rollup-freebsd-arm64@4.46.4': optional: true - '@rollup/rollup-freebsd-arm64@4.60.3': - optional: true - '@rollup/rollup-freebsd-x64@4.46.4': optional: true - '@rollup/rollup-freebsd-x64@4.60.3': - optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.46.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.3': - optional: true - '@rollup/rollup-linux-arm-musleabihf@4.46.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.3': - optional: true - '@rollup/rollup-linux-arm64-gnu@4.46.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.3': - optional: true - '@rollup/rollup-linux-arm64-musl@4.46.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.60.3': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.60.3': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.60.3': - optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.46.4': optional: true '@rollup/rollup-linux-ppc64-gnu@4.46.4': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.3': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.60.3': - optional: true - '@rollup/rollup-linux-riscv64-gnu@4.46.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.3': - optional: true - '@rollup/rollup-linux-riscv64-musl@4.46.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.3': - optional: true - '@rollup/rollup-linux-s390x-gnu@4.46.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.3': - optional: true - '@rollup/rollup-linux-x64-gnu@4.46.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.60.3': - optional: true - '@rollup/rollup-linux-x64-musl@4.46.4': optional: true - '@rollup/rollup-linux-x64-musl@4.60.3': - optional: true - - '@rollup/rollup-openbsd-x64@4.60.3': - optional: true - - '@rollup/rollup-openharmony-arm64@4.60.3': - optional: true - '@rollup/rollup-win32-arm64-msvc@4.46.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.3': - optional: true - '@rollup/rollup-win32-ia32-msvc@4.46.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.3': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.60.3': - optional: true - '@rollup/rollup-win32-x64-msvc@4.46.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.60.3': - optional: true - '@rtsao/scc@1.1.0': {} '@sindresorhus/is@7.2.0': {} @@ -7313,7 +7113,7 @@ snapshots: '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 20.19.39 + '@types/node': 20.19.40 '@types/chai@5.2.3': dependencies: @@ -7322,11 +7122,11 @@ snapshots: '@types/combined-stream@1.0.3': dependencies: - '@types/node': 20.19.39 + '@types/node': 20.19.40 '@types/connect@3.4.38': dependencies: - '@types/node': 20.19.39 + '@types/node': 20.19.40 '@types/content-disposition@0.5.8': {} @@ -7334,7 +7134,7 @@ snapshots: '@types/cors@2.8.13': dependencies: - '@types/node': 20.19.39 + '@types/node': 20.19.40 '@types/deep-eql@4.0.2': {} @@ -7346,7 +7146,7 @@ snapshots: '@types/express-serve-static-core@5.1.1': dependencies: - '@types/node': 20.19.39 + '@types/node': 20.19.40 '@types/qs': 6.9.10 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -7359,7 +7159,7 @@ snapshots: '@types/fs-extra@9.0.13': dependencies: - '@types/node': 20.19.39 + '@types/node': 20.19.40 '@types/http-errors@2.0.4': {} @@ -7369,20 +7169,20 @@ snapshots: '@types/jsonwebtoken@9.0.7': dependencies: - '@types/node': 20.19.39 + '@types/node': 20.19.40 '@types/mime@1.3.5': {} '@types/node-fetch@2.6.13': dependencies: - '@types/node': 20.19.39 + '@types/node': 20.19.40 form-data: 4.0.5 '@types/node@18.19.123': dependencies: undici-types: 5.26.5 - '@types/node@20.19.39': + '@types/node@20.19.40': dependencies: undici-types: 6.21.0 @@ -7407,12 +7207,12 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 20.19.39 + '@types/node': 20.19.40 '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 20.19.39 + '@types/node': 20.19.40 '@types/statuses@2.0.5': {} @@ -7422,7 +7222,7 @@ snapshots: '@types/websocket@1.0.10': dependencies: - '@types/node': 20.19.39 + '@types/node': 20.19.40 '@types/yargs-parser@21.0.3': {} @@ -7441,7 +7241,7 @@ snapshots: eslint: 9.39.4(jiti@1.21.7) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.5.4) + ts-api-utils: 2.4.0(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -7488,8 +7288,8 @@ snapshots: '@typescript-eslint/project-service@8.53.0(typescript@5.5.4)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.5.4) - '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.5.4) + '@typescript-eslint/types': 8.53.0 debug: 4.4.3 typescript: 5.5.4 transitivePeerDependencies: @@ -7529,7 +7329,7 @@ snapshots: '@typescript-eslint/utils': 8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4) debug: 4.4.3 eslint: 9.39.4(jiti@1.21.7) - ts-api-utils: 2.5.0(typescript@5.5.4) + ts-api-utils: 2.4.0(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -7559,8 +7359,8 @@ snapshots: debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.4 - tinyglobby: 0.2.16 - ts-api-utils: 2.5.0(typescript@5.5.4) + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -7666,23 +7466,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.5(msw@2.8.4(@types/node@20.19.39)(typescript@5.5.4))(vite@7.2.7(@types/node@20.19.39)(jiti@1.21.7))': + '@vitest/mocker@4.1.5(msw@2.8.4(@types/node@20.19.40)(typescript@5.5.4))(vite@7.2.7(@types/node@20.19.40)(jiti@1.21.7))': dependencies: '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - msw: 2.8.4(@types/node@20.19.39)(typescript@5.5.4) - vite: 7.2.7(@types/node@20.19.39)(jiti@1.21.7) - - '@vitest/mocker@4.1.5(msw@2.8.4(@types/node@20.19.39)(typescript@5.9.3))(vite@7.2.7(@types/node@20.19.39)(jiti@1.21.7))': - dependencies: - '@vitest/spy': 4.1.5 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - msw: 2.8.4(@types/node@20.19.39)(typescript@5.9.3) - vite: 7.2.7(@types/node@20.19.39)(jiti@1.21.7) + msw: 2.8.4(@types/node@20.19.40)(typescript@5.5.4) + vite: 7.2.7(@types/node@20.19.40)(jiti@1.21.7) '@vitest/pretty-format@4.1.5': dependencies: @@ -7730,7 +7521,7 @@ snapshots: '@vue/shared': 3.5.22 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.14 + postcss: 8.5.13 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.22': @@ -7868,7 +7659,7 @@ snapshots: anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.2 + picomatch: 2.3.1 arg@5.0.2: {} @@ -7957,14 +7748,14 @@ snapshots: asynckit@0.4.0: {} - autoprefixer@10.4.14(postcss@8.5.14): + autoprefixer@10.4.14(postcss@8.5.13): dependencies: browserslist: 4.22.1 caniuse-lite: 1.0.30001632 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.1 - postcss: 8.5.14 + postcss: 8.5.13 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.5: {} @@ -8013,7 +7804,7 @@ snapshots: baseline-browser-mapping@2.10.29: {} - binary-extensions@2.3.0: {} + binary-extensions@2.2.0: {} binary-search@1.3.6: {} @@ -8072,7 +7863,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.5: + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -8097,7 +7888,7 @@ snapshots: dotenv: 16.4.5 esbuild: 0.27.0 eventsource-parser: 1.1.2 - express: 4.22.1 + express: 4.21.2 graceful-fs: 4.2.11 http-errors: 2.0.0 minimatch: 9.0.5 @@ -8139,7 +7930,7 @@ snapshots: bufferutil@4.0.8: dependencies: - node-gyp-build: 4.8.4 + node-gyp-build: 4.7.0 bundle-require@5.1.0(esbuild@0.27.0): dependencies: @@ -8160,7 +7951,7 @@ snapshots: call-bind@1.0.5: dependencies: function-bind: 1.1.2 - get-intrinsic: 1.3.0 + get-intrinsic: 1.2.2 set-function-length: 1.1.1 call-bind@1.0.8: @@ -8363,6 +8154,12 @@ snapshots: deep-is@0.1.4: {} + define-data-property@1.1.1: + dependencies: + get-intrinsic: 1.3.0 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -8371,8 +8168,8 @@ snapshots: define-properties@1.2.1: dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 + define-data-property: 1.1.1 + has-property-descriptors: 1.0.1 object-keys: 1.1.1 delayed-stream@1.0.0: {} @@ -8558,34 +8355,34 @@ snapshots: d: 1.0.1 ext: 1.7.0 - esbuild@0.25.12: + esbuild@0.25.9: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 esbuild@0.27.0: optionalDependencies: @@ -8659,7 +8456,7 @@ snapshots: eslint: 9.39.4(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.4(jiti@1.21.7)) @@ -8691,8 +8488,8 @@ snapshots: debug: 4.4.3 enhanced-resolve: 5.15.0 eslint: 9.39.4(jiti@1.21.7) - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.39.4(jiti@1.21.7)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)) fast-glob: 3.3.2 get-tsconfig: 4.7.2 is-core-module: 2.16.1 @@ -8703,7 +8500,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@9.39.4(jiti@1.21.7)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: @@ -8714,7 +8511,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -8725,11 +8522,11 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@9.39.4(jiti@1.21.7)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)) hasown: 2.0.3 is-core-module: 2.16.1 is-glob: 4.0.3 - minimatch: 3.1.5 + minimatch: 3.1.2 object.fromentries: 2.0.8 object.groupby: 1.0.3 object.values: 1.2.1 @@ -8757,7 +8554,7 @@ snapshots: hasown: 2.0.3 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 - minimatch: 3.1.5 + minimatch: 3.1.2 object.fromentries: 2.0.8 safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 @@ -8785,7 +8582,7 @@ snapshots: estraverse: 5.3.0 hasown: 2.0.3 jsx-ast-utils: 3.3.5 - minimatch: 3.1.5 + minimatch: 3.1.2 object.entries: 1.1.9 object.fromentries: 2.0.8 object.values: 1.2.1 @@ -8893,7 +8690,7 @@ snapshots: expect-type@1.3.0: {} - express@4.22.1: + express@4.21.2: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 @@ -8916,7 +8713,7 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.14.2 + qs: 6.13.0 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.0 @@ -9009,10 +8806,6 @@ snapshots: optionalDependencies: picomatch: 4.0.3 - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -9057,10 +8850,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.4.2 + flatted: 3.2.9 keyv: 4.5.4 - flatted@3.4.2: {} + flatted@3.2.9: {} follow-redirects@1.16.0: {} @@ -9130,6 +8923,13 @@ snapshots: get-east-asian-width@1.4.0: {} + get-intrinsic@1.2.2: + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.3 + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -9210,14 +9010,22 @@ snapshots: has-flag@4.0.0: {} + has-property-descriptors@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.1 + has-proto@1.0.1: {} + has-proto@1.2.0: dependencies: dunder-proto: 1.0.1 + has-symbols@1.0.3: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -9308,7 +9116,7 @@ snapshots: is-binary-path@2.1.0: dependencies: - binary-extensions: 2.3.0 + binary-extensions: 2.2.0 is-boolean-object@1.2.2: dependencies: @@ -9317,6 +9125,10 @@ snapshots: is-callable@1.2.7: {} + is-core-module@2.13.1: + dependencies: + hasown: 2.0.3 + is-core-module@2.16.1: dependencies: hasown: 2.0.3 @@ -9613,7 +9425,7 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.2 + picomatch: 2.3.1 mime-db@1.52.0: {} @@ -9643,7 +9455,11 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.6 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 minimatch@3.1.5: dependencies: @@ -9670,12 +9486,12 @@ snapshots: ms@2.1.3: {} - msw@2.8.4(@types/node@20.19.39)(typescript@5.5.4): + msw@2.8.4(@types/node@20.19.40)(typescript@5.5.4): dependencies: '@bundled-es-modules/cookie': 2.0.1 '@bundled-es-modules/statuses': 1.0.1 '@bundled-es-modules/tough-cookie': 0.1.6 - '@inquirer/confirm': 5.1.12(@types/node@20.19.39) + '@inquirer/confirm': 5.1.12(@types/node@20.19.40) '@mswjs/interceptors': 0.37.6 '@open-draft/deferred-promise': 2.2.0 '@open-draft/until': 2.1.0 @@ -9695,32 +9511,6 @@ snapshots: transitivePeerDependencies: - '@types/node' - msw@2.8.4(@types/node@20.19.39)(typescript@5.9.3): - dependencies: - '@bundled-es-modules/cookie': 2.0.1 - '@bundled-es-modules/statuses': 1.0.1 - '@bundled-es-modules/tough-cookie': 0.1.6 - '@inquirer/confirm': 5.1.12(@types/node@20.19.39) - '@mswjs/interceptors': 0.37.6 - '@open-draft/deferred-promise': 2.2.0 - '@open-draft/until': 2.1.0 - '@types/cookie': 0.6.0 - '@types/statuses': 2.0.5 - graphql: 16.11.0 - headers-polyfill: 4.0.3 - is-node-process: 1.2.0 - outvariant: 1.4.3 - path-to-regexp: 6.3.0 - picocolors: 1.1.1 - strict-event-emitter: 0.5.1 - type-fest: 4.41.0 - yargs: 17.7.2 - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - '@types/node' - optional: true - mustache@4.2.0: {} mute-stream@2.0.0: {} @@ -9776,7 +9566,7 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-gyp-build@4.8.4: {} + node-gyp-build@4.7.0: {} node-releases@2.0.13: {} @@ -9962,18 +9752,14 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.2: {} + picomatch@2.3.1: {} picomatch@4.0.3: {} - picomatch@4.0.4: {} - pify@2.3.0: {} pirates@4.0.6: {} - pirates@4.0.7: {} - pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -9984,28 +9770,28 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-import@15.1.0(postcss@8.5.14): + postcss-import@15.1.0(postcss@8.5.13): dependencies: - postcss: 8.5.14 + postcss: 8.5.13 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.8 - postcss-js@4.0.1(postcss@8.5.14): + postcss-js@4.0.1(postcss@8.5.13): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.14 + postcss: 8.5.13 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.14): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.13): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 - postcss: 8.5.14 + postcss: 8.5.13 - postcss-nested@6.2.0(postcss@8.5.14): + postcss-nested@6.2.0(postcss@8.5.13): dependencies: - postcss: 8.5.14 + postcss: 8.5.13 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -10027,12 +9813,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.14: - dependencies: - nanoid: 3.3.12 - picocolors: 1.1.1 - source-map-js: 1.2.1 - prelude-ls@1.2.1: {} prettier@2.8.8: {} @@ -10057,7 +9837,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 20.19.39 + '@types/node': 22.19.18 long: 5.3.2 proxy-addr@2.0.7: @@ -10079,10 +9859,6 @@ snapshots: dependencies: side-channel: 1.1.0 - qs@6.14.2: - dependencies: - side-channel: 1.1.0 - qs@6.15.1: dependencies: side-channel: 1.1.0 @@ -10124,7 +9900,7 @@ snapshots: readdirp@3.6.0: dependencies: - picomatch: 2.3.2 + picomatch: 2.3.1 readdirp@4.1.2: {} @@ -10173,7 +9949,7 @@ snapshots: resolve@1.22.8: dependencies: - is-core-module: 2.16.1 + is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -10211,37 +9987,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.46.4 fsevents: 2.3.3 - rollup@4.60.3: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.3 - '@rollup/rollup-android-arm64': 4.60.3 - '@rollup/rollup-darwin-arm64': 4.60.3 - '@rollup/rollup-darwin-x64': 4.60.3 - '@rollup/rollup-freebsd-arm64': 4.60.3 - '@rollup/rollup-freebsd-x64': 4.60.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.3 - '@rollup/rollup-linux-arm-musleabihf': 4.60.3 - '@rollup/rollup-linux-arm64-gnu': 4.60.3 - '@rollup/rollup-linux-arm64-musl': 4.60.3 - '@rollup/rollup-linux-loong64-gnu': 4.60.3 - '@rollup/rollup-linux-loong64-musl': 4.60.3 - '@rollup/rollup-linux-ppc64-gnu': 4.60.3 - '@rollup/rollup-linux-ppc64-musl': 4.60.3 - '@rollup/rollup-linux-riscv64-gnu': 4.60.3 - '@rollup/rollup-linux-riscv64-musl': 4.60.3 - '@rollup/rollup-linux-s390x-gnu': 4.60.3 - '@rollup/rollup-linux-x64-gnu': 4.60.3 - '@rollup/rollup-linux-x64-musl': 4.60.3 - '@rollup/rollup-openbsd-x64': 4.60.3 - '@rollup/rollup-openharmony-arm64': 4.60.3 - '@rollup/rollup-win32-arm64-msvc': 4.60.3 - '@rollup/rollup-win32-ia32-msvc': 4.60.3 - '@rollup/rollup-win32-x64-gnu': 4.60.3 - '@rollup/rollup-win32-x64-msvc': 4.60.3 - fsevents: 2.3.3 - router@2.2.0: dependencies: debug: 4.4.3 @@ -10349,10 +10094,10 @@ snapshots: set-function-length@1.1.1: dependencies: - define-data-property: 1.1.4 + define-data-property: 1.1.1 get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 set-function-length@1.2.2: dependencies: @@ -10415,7 +10160,7 @@ snapshots: shebang-regex@3.0.0: {} - side-channel-list@1.0.1: + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 @@ -10439,7 +10184,7 @@ snapshots: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 - side-channel-list: 1.0.1 + side-channel-list: 1.0.0 side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 @@ -10593,16 +10338,6 @@ snapshots: pirates: 4.0.6 ts-interface-checker: 0.1.13 - sucrase@3.35.1: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.16 - ts-interface-checker: 0.1.13 - sugar-high@0.4.7: {} supports-color@10.2.2: {} @@ -10665,14 +10400,14 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.14 - postcss-import: 15.1.0(postcss@8.5.14) - postcss-js: 4.0.1(postcss@8.5.14) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.14) - postcss-nested: 6.2.0(postcss@8.5.14) + postcss: 8.5.13 + postcss-import: 15.1.0(postcss@8.5.13) + postcss-js: 4.0.1(postcss@8.5.13) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.13) + postcss-nested: 6.2.0(postcss@8.5.13) postcss-selector-parser: 6.1.2 resolve: 1.22.8 - sucrase: 3.35.1 + sucrase: 3.35.0 transitivePeerDependencies: - tsx - yaml @@ -10680,7 +10415,7 @@ snapshots: tanu@0.1.13: dependencies: tslib: 2.8.1 - typescript: 4.9.5 + typescript: 4.7.4 tapable@2.2.1: {} @@ -10705,11 +10440,6 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tinyglobby@0.2.16: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - tinyrainbow@3.1.0: {} to-regex-range@5.0.1: @@ -10731,6 +10461,10 @@ snapshots: ts-algebra@2.0.0: {} + ts-api-utils@2.4.0(typescript@5.5.4): + dependencies: + typescript: 5.5.4 + ts-api-utils@2.5.0(typescript@5.5.4): dependencies: typescript: 5.5.4 @@ -10745,10 +10479,6 @@ snapshots: optionalDependencies: typescript: 5.5.4 - tsconfck@3.1.4(typescript@5.9.3): - optionalDependencies: - typescript: 5.9.3 - tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -10758,7 +10488,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@1.21.7)(postcss@8.5.14)(typescript@5.5.4): + tsup@8.5.1(jiti@1.21.7)(postcss@8.5.13)(typescript@5.3.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.0) cac: 6.7.14 @@ -10769,7 +10499,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.14) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.13) resolve-from: 5.0.0 rollup: 4.46.4 source-map: 0.7.6 @@ -10778,15 +10508,15 @@ snapshots: tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.14 - typescript: 5.5.4 + postcss: 8.5.13 + typescript: 5.3.3 transitivePeerDependencies: - jiti - supports-color - tsx - yaml - tsup@8.5.1(jiti@1.21.7)(postcss@8.5.14)(typescript@5.9.3): + tsup@8.5.1(jiti@1.21.7)(postcss@8.5.13)(typescript@5.5.4): dependencies: bundle-require: 5.1.0(esbuild@0.27.0) cac: 6.7.14 @@ -10797,7 +10527,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.14) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.13) resolve-from: 5.0.0 rollup: 4.46.4 source-map: 0.7.6 @@ -10806,8 +10536,8 @@ snapshots: tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.14 - typescript: 5.9.3 + postcss: 8.5.13 + typescript: 5.5.4 transitivePeerDependencies: - jiti - supports-color @@ -10941,14 +10671,12 @@ snapshots: transitivePeerDependencies: - supports-color - typescript@4.9.5: {} + typescript@4.7.4: {} typescript@5.3.3: {} typescript@5.5.4: {} - typescript@5.9.3: {} - ufo@1.6.1: {} uglify-js@3.19.3: @@ -11018,7 +10746,7 @@ snapshots: utf-8-validate@5.0.10: dependencies: - node-gyp-build: 4.8.4 + node-gyp-build: 4.7.0 util-deprecate@1.0.2: {} @@ -11038,71 +10766,33 @@ snapshots: vary@1.1.2: {} - vite-tsconfig-paths@6.1.1(typescript@5.5.4)(vite@7.2.7(@types/node@20.19.39)(jiti@1.21.7)): + vite-tsconfig-paths@6.1.1(typescript@5.5.4)(vite@7.2.7(@types/node@20.19.40)(jiti@1.21.7)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.4(typescript@5.5.4) - vite: 7.2.7(@types/node@20.19.39)(jiti@1.21.7) + vite: 7.2.7(@types/node@20.19.40)(jiti@1.21.7) transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@7.2.7(@types/node@20.19.39)(jiti@1.21.7)): + vite@7.2.7(@types/node@20.19.40)(jiti@1.21.7): dependencies: - debug: 4.4.3 - globrex: 0.1.2 - tsconfck: 3.1.4(typescript@5.9.3) - vite: 7.2.7(@types/node@20.19.39)(jiti@1.21.7) - transitivePeerDependencies: - - supports-color - - typescript - - vite@7.2.7(@types/node@20.19.39)(jiti@1.21.7): - dependencies: - esbuild: 0.25.12 + esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.13 - rollup: 4.60.3 + rollup: 4.46.4 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 20.19.39 + '@types/node': 20.19.40 fsevents: 2.3.3 jiti: 1.21.7 - vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.39)(msw@2.8.4(@types/node@20.19.39)(typescript@5.5.4))(vite@7.2.7(@types/node@20.19.39)(jiti@1.21.7)): - dependencies: - '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(msw@2.8.4(@types/node@20.19.39)(typescript@5.5.4))(vite@7.2.7(@types/node@20.19.39)(jiti@1.21.7)) - '@vitest/pretty-format': 4.1.5 - '@vitest/runner': 4.1.5 - '@vitest/snapshot': 4.1.5 - '@vitest/spy': 4.1.5 - '@vitest/utils': 4.1.5 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.1.0 - tinybench: 2.9.0 - tinyexec: 1.1.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.1.0 - vite: 7.2.7(@types/node@20.19.39)(jiti@1.21.7) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@types/node': 20.19.39 - transitivePeerDependencies: - - msw - - vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.39)(msw@2.8.4(@types/node@20.19.39)(typescript@5.9.3))(vite@7.2.7(@types/node@20.19.39)(jiti@1.21.7)): + vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.40)(msw@2.8.4(@types/node@20.19.40)(typescript@5.5.4))(vite@7.2.7(@types/node@20.19.40)(jiti@1.21.7)): dependencies: '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(msw@2.8.4(@types/node@20.19.39)(typescript@5.9.3))(vite@7.2.7(@types/node@20.19.39)(jiti@1.21.7)) + '@vitest/mocker': 4.1.5(msw@2.8.4(@types/node@20.19.40)(typescript@5.5.4))(vite@7.2.7(@types/node@20.19.40)(jiti@1.21.7)) '@vitest/pretty-format': 4.1.5 '@vitest/runner': 4.1.5 '@vitest/snapshot': 4.1.5 @@ -11119,11 +10809,11 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 7.2.7(@types/node@20.19.39)(jiti@1.21.7) + vite: 7.2.7(@types/node@20.19.40)(jiti@1.21.7) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 - '@types/node': 20.19.39 + '@types/node': 20.19.40 transitivePeerDependencies: - msw @@ -11255,7 +10945,7 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260504.1 '@cloudflare/workerd-windows-64': 1.20260504.1 - wrangler@4.88.0(@cloudflare/workers-types@4.20260505.1)(bufferutil@4.0.8)(utf-8-validate@5.0.10): + wrangler@4.88.0(@cloudflare/workers-types@4.20260509.1)(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260504.1) @@ -11266,7 +10956,7 @@ snapshots: unenv: 2.0.0-rc.24 workerd: 1.20260504.1 optionalDependencies: - '@cloudflare/workers-types': 4.20260505.1 + '@cloudflare/workers-types': 4.20260509.1 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil From cc30ae07a76d5fcf63cee00c78597e9518497645 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 15:56:38 -0700 Subject: [PATCH 10/22] fix streaming --- apis/vercel/app/api/v1/[...slug]/route.ts | 31 ++++++++++++++++++++++- apis/vercel/next-env.d.ts | 2 +- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/apis/vercel/app/api/v1/[...slug]/route.ts b/apis/vercel/app/api/v1/[...slug]/route.ts index 92822386..3ce921e0 100644 --- a/apis/vercel/app/api/v1/[...slug]/route.ts +++ b/apis/vercel/app/api/v1/[...slug]/route.ts @@ -70,7 +70,36 @@ async function proxy(request: Request): Promise { const response = await proxyHandler(request, ctx); log("route:after-handler", { status: response.status }); response.headers.set("x-request-id", requestId); - return response; + + if (!response.body) { + return response; + } + const buffered = new ReadableStream({ + async start(controller) { + const reader = response.body!.getReader(); + let totalBytes = 0; + let chunkCount = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + totalBytes += value.byteLength; + chunkCount += 1; + controller.enqueue(value); + } + controller.close(); + log("route:body-complete", { totalBytes, chunkCount }); + } catch (err) { + log("route:body-error", { error: String(err), totalBytes }); + controller.error(err); + } + }, + }); + return new Response(buffered, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); } catch (error) { log("route:error", { error: String(error) }); return new Response( diff --git a/apis/vercel/next-env.d.ts b/apis/vercel/next-env.d.ts index 2d5420eb..0c7fad71 100644 --- a/apis/vercel/next-env.d.ts +++ b/apis/vercel/next-env.d.ts @@ -1,7 +1,7 @@ /// /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. From b14fae16c2976c21f90eee9df7005a8e5152d519 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 16:16:32 -0700 Subject: [PATCH 11/22] keep function alive --- apis/vercel/app/api/v1/[...slug]/route.ts | 33 +----- apis/vercel/next-env.d.ts | 2 +- packages/proxy/edge/index.ts | 121 +++++++++++++++++----- 3 files changed, 99 insertions(+), 57 deletions(-) diff --git a/apis/vercel/app/api/v1/[...slug]/route.ts b/apis/vercel/app/api/v1/[...slug]/route.ts index 3ce921e0..72d3312d 100644 --- a/apis/vercel/app/api/v1/[...slug]/route.ts +++ b/apis/vercel/app/api/v1/[...slug]/route.ts @@ -31,6 +31,8 @@ const proxyHandler = EdgeProxyV1({ credentialsCache: KVCache, completionsCache: KVCache, braintrustApiUrl: process.env.BRAINTRUST_APP_URL, + // Vercel Node Lambda tears down the function when the handler returns, so we use `waitUntil` to allow background tasks to continue after the response is sent. + streamingViaWaitUntil: true, }); const ctx = { @@ -70,36 +72,7 @@ async function proxy(request: Request): Promise { const response = await proxyHandler(request, ctx); log("route:after-handler", { status: response.status }); response.headers.set("x-request-id", requestId); - - if (!response.body) { - return response; - } - const buffered = new ReadableStream({ - async start(controller) { - const reader = response.body!.getReader(); - let totalBytes = 0; - let chunkCount = 0; - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - totalBytes += value.byteLength; - chunkCount += 1; - controller.enqueue(value); - } - controller.close(); - log("route:body-complete", { totalBytes, chunkCount }); - } catch (err) { - log("route:body-error", { error: String(err), totalBytes }); - controller.error(err); - } - }, - }); - return new Response(buffered, { - status: response.status, - statusText: response.statusText, - headers: response.headers, - }); + return response; } catch (error) { log("route:error", { error: String(error) }); return new Response( diff --git a/apis/vercel/next-env.d.ts b/apis/vercel/next-env.d.ts index 0c7fad71..2d5420eb 100644 --- a/apis/vercel/next-env.d.ts +++ b/apis/vercel/next-env.d.ts @@ -1,7 +1,7 @@ /// /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/packages/proxy/edge/index.ts b/packages/proxy/edge/index.ts index 6c99b0f5..f143ec46 100644 --- a/packages/proxy/edge/index.ts +++ b/packages/proxy/edge/index.ts @@ -41,6 +41,15 @@ export interface ProxyOpts { spanId?: string; spanExport?: string; nativeInferenceSecretKey?: string; + /** + * When true, the upstream → response body runs in the background + * and the function is kept alive via `ctx.waitUntil`. + * + * Leave unset on Cloudflare Workers as those already keep the event stream alive + * + * @default false + */ + streamingViaWaitUntil?: boolean; } const defaultWhitelist: (string | RegExp)[] = [ @@ -320,6 +329,7 @@ export function EdgeProxyV1(opts: ProxyOpts) { headers: { "Content-Type": "text/plain" }, }); } + const method: "GET" | "POST" = request.method; const relativeURL = opts.getRelativeURL(request); @@ -386,23 +396,93 @@ export function EdgeProxyV1(opts: ProxyOpts) { } }; + const requestBody = await request.text(); + const proxyV1Args = { + method, + url: relativeURL, + proxyHeaders, + body: requestBody, + setHeader, + setStatusCode: setStatus, + getApiSecrets: fetchApiSecrets, + cacheGet, + cachePut, + digest: digestMessage, + logHistogram: opts.logHistogram, + spanLogger: opts.spanLogger, + billingOrgId: opts.billingOrgId, + onBillingEvent: opts.onBillingEvent, + }; + + const meterProvider = opts.meterProvider; + const wrapWithMeter = (body: ReadableStream) => + meterProvider + ? body.pipeThrough( + new TransformStream({ + flush() { + ctx.waitUntil(flushMetrics(meterProvider)); + }, + }), + ) + : body; + + if (opts.streamingViaWaitUntil) { + let signalReady: () => void = () => {}; + const headersReady = new Promise((resolve) => { + signalReady = resolve; + }); + + const baseWriter = writable.getWriter(); + const wrappedWritable = new WritableStream({ + async write(chunk) { + signalReady(); + await baseWriter.write(chunk); + }, + async close() { + signalReady(); + await baseWriter.close(); + }, + async abort(reason) { + signalReady(); + await baseWriter.abort(reason); + }, + }); + + const backgroundKeepAlive = proxyV1({ + ...proxyV1Args, + res: wrappedWritable, + }).catch((e) => { + // Unblock the headers-ready wait so we don't hang if proxyV1 throws + // before writing anything. + signalReady(); + baseWriter.abort(e).catch(() => {}); + throw e; + }); + + // Anchor the background work to the platform's function lifetime. + ctx.waitUntil(backgroundKeepAlive.catch(() => {})); + + try { + await Promise.race([headersReady, backgroundKeepAlive]); + } catch (e) { + console.error("EdgeProxyV1 request failed", e); + return new Response(`${e}`, { + status: 400, + headers: { "Content-Type": "text/plain" }, + }); + } + + return new Response(wrapWithMeter(readable), { + status, + headers, + }); + } + + // Default path: await proxyV1 to completion before returning. try { await proxyV1({ - method: request.method, - url: relativeURL, - proxyHeaders, - body: await request.text(), - setHeader, - setStatusCode: setStatus, + ...proxyV1Args, res: writable, - getApiSecrets: fetchApiSecrets, - cacheGet, - cachePut, - digest: digestMessage, - logHistogram: opts.logHistogram, - spanLogger: opts.spanLogger, - billingOrgId: opts.billingOrgId, - onBillingEvent: opts.onBillingEvent, }); } catch (e) { // log error to vercel @@ -413,18 +493,7 @@ export function EdgeProxyV1(opts: ProxyOpts) { }); } - const meterProvider = opts.meterProvider; - const responseBody = meterProvider - ? readable.pipeThrough( - new TransformStream({ - flush() { - ctx.waitUntil(flushMetrics(meterProvider)); - }, - }), - ) - : readable; - - return new Response(responseBody, { + return new Response(wrapWithMeter(readable), { status, headers, }); From 0e87814350340698bfc48d54534682885c0cc371 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 16:36:03 -0700 Subject: [PATCH 12/22] fix --- apis/vercel/app/api/v1/[...slug]/route.ts | 1 + packages/proxy/edge/index.ts | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/apis/vercel/app/api/v1/[...slug]/route.ts b/apis/vercel/app/api/v1/[...slug]/route.ts index 72d3312d..60f9363c 100644 --- a/apis/vercel/app/api/v1/[...slug]/route.ts +++ b/apis/vercel/app/api/v1/[...slug]/route.ts @@ -1,6 +1,7 @@ import dns from "node:dns"; import { kv } from "@vercel/kv"; import { waitUntil } from "@vercel/functions"; +import { NextResponse } from "next/server"; import { EdgeProxyV1, CacheSetOptions } from "@braintrust/proxy/edge"; import { Agent, setGlobalDispatcher } from "undici"; diff --git a/packages/proxy/edge/index.ts b/packages/proxy/edge/index.ts index f143ec46..8f27a811 100644 --- a/packages/proxy/edge/index.ts +++ b/packages/proxy/edge/index.ts @@ -432,6 +432,11 @@ export function EdgeProxyV1(opts: ProxyOpts) { signalReady = resolve; }); + let signalPipeComplete: () => void = () => {}; + const pipeComplete = new Promise((resolve) => { + signalPipeComplete = resolve; + }); + const baseWriter = writable.getWriter(); const wrappedWritable = new WritableStream({ async write(chunk) { @@ -441,29 +446,34 @@ export function EdgeProxyV1(opts: ProxyOpts) { async close() { signalReady(); await baseWriter.close(); + signalPipeComplete(); }, async abort(reason) { signalReady(); await baseWriter.abort(reason); + signalPipeComplete(); }, }); - const backgroundKeepAlive = proxyV1({ + const proxyV1Promise = proxyV1({ ...proxyV1Args, res: wrappedWritable, }).catch((e) => { - // Unblock the headers-ready wait so we don't hang if proxyV1 throws - // before writing anything. signalReady(); baseWriter.abort(e).catch(() => {}); + signalPipeComplete(); throw e; }); - // Anchor the background work to the platform's function lifetime. - ctx.waitUntil(backgroundKeepAlive.catch(() => {})); + // Keep the platform from tearing the function down + ctx.waitUntil( + Promise.all([proxyV1Promise.catch(() => {}), pipeComplete]).catch( + () => {}, + ), + ); try { - await Promise.race([headersReady, backgroundKeepAlive]); + await Promise.race([headersReady, proxyV1Promise]); } catch (e) { console.error("EdgeProxyV1 request failed", e); return new Response(`${e}`, { From 51d77f1fa28224f213e7e144036d211af107e815 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 16:42:54 -0700 Subject: [PATCH 13/22] test route --- .../api/v1/test-proxy-notransform/route.ts | 27 +++++++++++++ apis/vercel/app/api/v1/test-proxy/route.ts | 27 +++++++++++++ apis/vercel/app/api/v1/test-stream/route.ts | 29 ++++++++++++++ .../vercel/app/api/v1/test-transform/route.ts | 40 +++++++++++++++++++ apis/vercel/app/api/v1/test/route.ts | 24 +++++++++++ 5 files changed, 147 insertions(+) create mode 100644 apis/vercel/app/api/v1/test-proxy-notransform/route.ts create mode 100644 apis/vercel/app/api/v1/test-proxy/route.ts create mode 100644 apis/vercel/app/api/v1/test-stream/route.ts create mode 100644 apis/vercel/app/api/v1/test-transform/route.ts create mode 100644 apis/vercel/app/api/v1/test/route.ts diff --git a/apis/vercel/app/api/v1/test-proxy-notransform/route.ts b/apis/vercel/app/api/v1/test-proxy-notransform/route.ts new file mode 100644 index 00000000..bef78983 --- /dev/null +++ b/apis/vercel/app/api/v1/test-proxy-notransform/route.ts @@ -0,0 +1,27 @@ +// Same as /test-proxy but with `Cache-Control: no-cache, no-transform`, +// matching Vercel's canonical streaming-proxy example. If this returns a +// body and /test-proxy returns empty, Cloudflare is transforming/dropping +// the body and `no-transform` fixes it. +export const dynamic = "force-dynamic"; + +async function handler(): Promise { + const upstream = await fetch("https://api.github.com/repos/vercel/next.js", { + headers: { "user-agent": "braintrust-proxy-test" }, + }); + const { readable, writable } = new TransformStream(); + upstream.body!.pipeTo(writable).catch((err) => { + console.error("test-proxy-notransform pipeTo failed", err); + }); + + return new Response(readable, { + status: upstream.status, + headers: { + "content-type": + upstream.headers.get("content-type") ?? "application/json", + "cache-control": "no-cache, no-transform", + }, + }); +} + +export const GET = handler; +export const POST = handler; diff --git a/apis/vercel/app/api/v1/test-proxy/route.ts b/apis/vercel/app/api/v1/test-proxy/route.ts new file mode 100644 index 00000000..f54d1f04 --- /dev/null +++ b/apis/vercel/app/api/v1/test-proxy/route.ts @@ -0,0 +1,27 @@ +// Isolation test #4: mirror proxyV1's exact pattern — fetch an upstream, +// pipeTo a TransformStream, return readable as Response body. NO +// Cache-Control header (mirrors what proxyV1 effectively does with +// Anthropic's headers). If this returns empty body, the issue is Cloudflare +// (or some intermediary) stripping bodies for this response shape. +export const dynamic = "force-dynamic"; + +async function handler(): Promise { + const upstream = await fetch("https://api.github.com/repos/vercel/next.js", { + headers: { "user-agent": "braintrust-proxy-test" }, + }); + const { readable, writable } = new TransformStream(); + upstream.body!.pipeTo(writable).catch((err) => { + console.error("test-proxy pipeTo failed", err); + }); + + return new Response(readable, { + status: upstream.status, + headers: { + "content-type": + upstream.headers.get("content-type") ?? "application/json", + }, + }); +} + +export const GET = handler; +export const POST = handler; diff --git a/apis/vercel/app/api/v1/test-stream/route.ts b/apis/vercel/app/api/v1/test-stream/route.ts new file mode 100644 index 00000000..951583cc --- /dev/null +++ b/apis/vercel/app/api/v1/test-stream/route.ts @@ -0,0 +1,29 @@ +// Isolation test #2: does `new Response(readableStream, ...)` deliver a +// body on this deployment? Constructs a simple ReadableStream that +// enqueues two chunks then closes — no TransformStream, no background +// writers, no waitUntil. +// +// If this returns "hello world": ReadableStream Response bodies are fine. +// If empty: this Vercel Node deployment can't deliver streaming responses. +export const dynamic = "force-dynamic"; + +async function handler(): Promise { + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode("hello ")); + controller.enqueue(encoder.encode("world")); + controller.close(); + }, + }); + return new Response(stream, { + status: 200, + headers: { + "content-type": "text/plain", + "x-test": "stream", + }, + }); +} + +export const GET = handler; +export const POST = handler; diff --git a/apis/vercel/app/api/v1/test-transform/route.ts b/apis/vercel/app/api/v1/test-transform/route.ts new file mode 100644 index 00000000..739bcf38 --- /dev/null +++ b/apis/vercel/app/api/v1/test-transform/route.ts @@ -0,0 +1,40 @@ +// Isolation test #3: mirrors EdgeProxyV1's exact pattern — a +// TransformStream where bytes are written to `writable` from a background +// task while `readable` is returned as the Response body. No upstream +// fetch, just synthetic writes. +// +// If this returns "hello world": the TransformStream + background-write +// pattern works on Vercel Node, and the issue is somewhere deeper inside +// EdgeProxyV1 / proxyV1 (interaction with the upstream fetch). +// If empty: this exact pattern (which EdgeProxyV1 uses) doesn't work on +// Vercel Node — we need a different response-construction approach. +export const dynamic = "force-dynamic"; + +async function handler(): Promise { + const { readable, writable } = new TransformStream(); + const writer = writable.getWriter(); + const encoder = new TextEncoder(); + + // Fire-and-forget background writes — same shape as proxyV1's + // stream.pipeTo(res).catch(...) + (async () => { + try { + await writer.write(encoder.encode("hello ")); + await writer.write(encoder.encode("world")); + await writer.close(); + } catch (err) { + console.error("transform write error", err); + } + })(); + + return new Response(readable, { + status: 200, + headers: { + "content-type": "text/plain", + "x-test": "transform", + }, + }); +} + +export const GET = handler; +export const POST = handler; diff --git a/apis/vercel/app/api/v1/test/route.ts b/apis/vercel/app/api/v1/test/route.ts new file mode 100644 index 00000000..acc66978 --- /dev/null +++ b/apis/vercel/app/api/v1/test/route.ts @@ -0,0 +1,24 @@ +// Isolation test: does *any* response body reach the client on this +// deployment? No EdgeProxyV1, no streaming, no upstream call. Just a +// fixed JSON body. If this comes back empty, the problem is the +// deployment pipeline (Cloudflare in front, Vercel preview protection, +// the project's domain config), not our proxy code. +export const dynamic = "force-dynamic"; + +export async function GET(): Promise { + const body = JSON.stringify({ + test: "hello", + at: new Date().toISOString(), + }); + return new Response(body, { + status: 200, + headers: { + "content-type": "application/json", + "x-test": "true", + }, + }); +} + +export async function POST(): Promise { + return GET(); +} From 8bd7924c0d475744bee537b1f880d7e8d84b28e2 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 17:06:05 -0700 Subject: [PATCH 14/22] more logs --- packages/proxy/edge/index.ts | 56 ++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/packages/proxy/edge/index.ts b/packages/proxy/edge/index.ts index 8f27a811..8b95ab91 100644 --- a/packages/proxy/edge/index.ts +++ b/packages/proxy/edge/index.ts @@ -437,18 +437,38 @@ export function EdgeProxyV1(opts: ProxyOpts) { signalPipeComplete = resolve; }); + let writeCount = 0; + let totalBytes = 0; + let closed = false; + let aborted = false; + const baseWriter = writable.getWriter(); const wrappedWritable = new WritableStream({ async write(chunk) { + writeCount += 1; + totalBytes += chunk.byteLength; + if (writeCount <= 3 || writeCount % 50 === 0) { + console.log( + `EdgeProxyV1 wrappedWritable.write #${writeCount} bytes=${chunk.byteLength} total=${totalBytes}`, + ); + } signalReady(); await baseWriter.write(chunk); }, async close() { + closed = true; + console.log( + `EdgeProxyV1 wrappedWritable.close writes=${writeCount} totalBytes=${totalBytes}`, + ); signalReady(); await baseWriter.close(); signalPipeComplete(); }, async abort(reason) { + aborted = true; + console.error( + `EdgeProxyV1 wrappedWritable.abort writes=${writeCount} totalBytes=${totalBytes} reason=${reason}`, + ); signalReady(); await baseWriter.abort(reason); signalPipeComplete(); @@ -458,18 +478,32 @@ export function EdgeProxyV1(opts: ProxyOpts) { const proxyV1Promise = proxyV1({ ...proxyV1Args, res: wrappedWritable, - }).catch((e) => { - signalReady(); - baseWriter.abort(e).catch(() => {}); - signalPipeComplete(); - throw e; - }); + }) + .then(() => { + console.log( + `EdgeProxyV1 proxyV1 resolved writes=${writeCount} totalBytes=${totalBytes} closed=${closed} aborted=${aborted}`, + ); + }) + .catch((e) => { + console.error( + `EdgeProxyV1 proxyV1 threw writes=${writeCount} totalBytes=${totalBytes} closed=${closed} aborted=${aborted}`, + e, + ); + signalReady(); + baseWriter.abort(e).catch(() => {}); + signalPipeComplete(); + throw e; + }); // Keep the platform from tearing the function down ctx.waitUntil( - Promise.all([proxyV1Promise.catch(() => {}), pipeComplete]).catch( - () => {}, - ), + Promise.all([proxyV1Promise.catch(() => {}), pipeComplete]) + .then(() => { + console.log( + `EdgeProxyV1 waitUntil settled writes=${writeCount} totalBytes=${totalBytes} closed=${closed} aborted=${aborted}`, + ); + }) + .catch(() => {}), ); try { @@ -482,6 +516,10 @@ export function EdgeProxyV1(opts: ProxyOpts) { }); } + console.log( + `EdgeProxyV1 returning Response status=${status} writes=${writeCount} totalBytes=${totalBytes}`, + ); + return new Response(wrapWithMeter(readable), { status, headers, From a133004fc2917c4d54eae99fff4c962638d7aaf8 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 17:09:55 -0700 Subject: [PATCH 15/22] fix race --- packages/proxy/edge/index.ts | 67 +++++++----------------------------- 1 file changed, 12 insertions(+), 55 deletions(-) diff --git a/packages/proxy/edge/index.ts b/packages/proxy/edge/index.ts index 8b95ab91..f54f4b46 100644 --- a/packages/proxy/edge/index.ts +++ b/packages/proxy/edge/index.ts @@ -437,89 +437,46 @@ export function EdgeProxyV1(opts: ProxyOpts) { signalPipeComplete = resolve; }); - let writeCount = 0; - let totalBytes = 0; - let closed = false; - let aborted = false; - const baseWriter = writable.getWriter(); const wrappedWritable = new WritableStream({ async write(chunk) { - writeCount += 1; - totalBytes += chunk.byteLength; - if (writeCount <= 3 || writeCount % 50 === 0) { - console.log( - `EdgeProxyV1 wrappedWritable.write #${writeCount} bytes=${chunk.byteLength} total=${totalBytes}`, - ); - } signalReady(); await baseWriter.write(chunk); }, async close() { - closed = true; - console.log( - `EdgeProxyV1 wrappedWritable.close writes=${writeCount} totalBytes=${totalBytes}`, - ); signalReady(); await baseWriter.close(); signalPipeComplete(); }, async abort(reason) { - aborted = true; - console.error( - `EdgeProxyV1 wrappedWritable.abort writes=${writeCount} totalBytes=${totalBytes} reason=${reason}`, - ); signalReady(); await baseWriter.abort(reason); signalPipeComplete(); }, }); + let proxyError: unknown = undefined; const proxyV1Promise = proxyV1({ ...proxyV1Args, res: wrappedWritable, - }) - .then(() => { - console.log( - `EdgeProxyV1 proxyV1 resolved writes=${writeCount} totalBytes=${totalBytes} closed=${closed} aborted=${aborted}`, - ); - }) - .catch((e) => { - console.error( - `EdgeProxyV1 proxyV1 threw writes=${writeCount} totalBytes=${totalBytes} closed=${closed} aborted=${aborted}`, - e, - ); - signalReady(); - baseWriter.abort(e).catch(() => {}); - signalPipeComplete(); - throw e; - }); + }).catch((e) => { + proxyError = e; + signalReady(); + baseWriter.abort(e).catch(() => {}); + signalPipeComplete(); + }); - // Keep the platform from tearing the function down - ctx.waitUntil( - Promise.all([proxyV1Promise.catch(() => {}), pipeComplete]) - .then(() => { - console.log( - `EdgeProxyV1 waitUntil settled writes=${writeCount} totalBytes=${totalBytes} closed=${closed} aborted=${aborted}`, - ); - }) - .catch(() => {}), - ); + ctx.waitUntil(Promise.all([proxyV1Promise, pipeComplete])); - try { - await Promise.race([headersReady, proxyV1Promise]); - } catch (e) { - console.error("EdgeProxyV1 request failed", e); - return new Response(`${e}`, { + await headersReady; + if (proxyError !== undefined) { + console.error("EdgeProxyV1 request failed", proxyError); + return new Response(`${proxyError}`, { status: 400, headers: { "Content-Type": "text/plain" }, }); } - console.log( - `EdgeProxyV1 returning Response status=${status} writes=${writeCount} totalBytes=${totalBytes}`, - ); - return new Response(wrapWithMeter(readable), { status, headers, From 492b186f23d999073be8a3483369c3b3f5c11ee7 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 17:41:16 -0700 Subject: [PATCH 16/22] test output --- packages/proxy/edge/index.test.ts | 118 +++++++++++++++++++++++++- packages/proxy/edge/index.ts | 136 ++++++++++++++++++------------ 2 files changed, 200 insertions(+), 54 deletions(-) diff --git a/packages/proxy/edge/index.test.ts b/packages/proxy/edge/index.test.ts index 96688e7e..8fe1b351 100644 --- a/packages/proxy/edge/index.test.ts +++ b/packages/proxy/edge/index.test.ts @@ -1,5 +1,10 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { makeFetchApiSecrets, type EdgeContext, type ProxyOpts } from "./index"; +import { + makeFetchApiSecrets, + streamingResponseViaWaitUntil, + type EdgeContext, + type ProxyOpts, +} from "./index"; function createInMemoryCache() { const store = new Map(); @@ -184,3 +189,114 @@ describe("makeFetchApiSecrets", () => { expect(fetchMock).toHaveBeenCalledTimes(1); }); }); + +describe("streamingResponseViaWaitUntil", () => { + function setup() { + const waitUntilPromises: Promise[] = []; + const ctx: EdgeContext = { + waitUntil(promise) { + waitUntilPromises.push(promise); + }, + }; + const wrapWithMeter = (b: ReadableStream) => b; + return { waitUntilPromises, ctx, wrapWithMeter }; + } + + it("waits for the first byte before returning Response", async () => { + const { ctx, wrapWithMeter } = setup(); + const encoder = new TextEncoder(); + + let returnedResponse = false; + let firstWriteAt = -1; + let responseAt = -1; + let tick = 0; + + const runProxy = async (res: WritableStream) => { + (async () => { + await new Promise((r) => setTimeout(r, 20)); + const writer = res.getWriter(); + firstWriteAt = ++tick; + await writer.write(encoder.encode("hello ")); + await writer.write(encoder.encode("world")); + await writer.close(); + })(); + }; + + const responsePromise = streamingResponseViaWaitUntil({ + ctx, + wrapWithMeter, + runProxy, + getStatus: () => 200, + getHeaders: () => ({ "content-type": "text/plain" }), + }).then((r) => { + responseAt = ++tick; + returnedResponse = true; + return r; + }); + + await new Promise((r) => setTimeout(r, 0)); + expect(returnedResponse).toBe(false); + + const response = await responsePromise; + expect(response.status).toBe(200); + expect(firstWriteAt).toBeGreaterThan(0); + expect(responseAt).toBeGreaterThan(firstWriteAt); + + const text = await response.text(); + expect(text).toBe("hello world"); + }); + + it("returns a 400 response when runProxy throws before writing", async () => { + const { ctx, wrapWithMeter } = setup(); + + const runProxy = async () => { + throw new Error("boom"); + }; + + const response = await streamingResponseViaWaitUntil({ + ctx, + wrapWithMeter, + runProxy, + getStatus: () => 200, + getHeaders: () => ({}), + }); + + expect(response.status).toBe(400); + expect(await response.text()).toContain("boom"); + }); + + it("keeps the platform alive via ctx.waitUntil until the pipe drains", async () => { + const { waitUntilPromises, ctx, wrapWithMeter } = setup(); + const encoder = new TextEncoder(); + let pipeFinished = false; + + const runProxy = async (res: WritableStream) => { + (async () => { + const writer = res.getWriter(); + await writer.write(encoder.encode("first")); + await new Promise((r) => setTimeout(r, 30)); + await writer.write(encoder.encode("-second")); + await writer.close(); + pipeFinished = true; + })(); + }; + + const response = await streamingResponseViaWaitUntil({ + ctx, + wrapWithMeter, + runProxy, + getStatus: () => 200, + getHeaders: () => ({}), + }); + + expect(pipeFinished).toBe(false); + expect(waitUntilPromises.length).toBe(1); + + const text = await response.text(); + expect(text).toBe("first-second"); + + await Promise.all(waitUntilPromises); + await new Promise((r) => setTimeout(r, 0)); + expect(pipeFinished).toBe(true); + }); +}); diff --git a/packages/proxy/edge/index.ts b/packages/proxy/edge/index.ts index f54f4b46..68e9c5c0 100644 --- a/packages/proxy/edge/index.ts +++ b/packages/proxy/edge/index.ts @@ -309,6 +309,83 @@ export function makeFetchApiSecrets({ // Metric logging functions are now created in the calling layer (e.g., Cloudflare proxy) +// readable highWaterMark is generous so the producer can prime the stream +// before any reader is attached — Response(readable) is only constructed +// after the first write, so without buffering `baseWriter.write` would +// deadlock waiting for a consumer. +export async function streamingResponseViaWaitUntil({ + ctx, + wrapWithMeter, + runProxy, + getStatus, + getHeaders, +}: { + ctx: EdgeContext; + wrapWithMeter: ( + body: ReadableStream, + ) => ReadableStream; + runProxy: (res: WritableStream) => Promise; + getStatus: () => number; + getHeaders: () => Record; +}): Promise { + const { readable, writable } = new TransformStream( + {}, + { highWaterMark: 1 }, + { highWaterMark: 64 }, + ); + + let signalReady: () => void = () => {}; + const headersReady = new Promise((resolve) => { + signalReady = resolve; + }); + + let signalPipeComplete: () => void = () => {}; + const pipeComplete = new Promise((resolve) => { + signalPipeComplete = resolve; + }); + + const baseWriter = writable.getWriter(); + const wrappedWritable = new WritableStream({ + async write(chunk) { + await baseWriter.write(chunk); + signalReady(); + }, + async close() { + await baseWriter.close(); + signalReady(); + signalPipeComplete(); + }, + async abort(reason) { + await baseWriter.abort(reason); + signalReady(); + signalPipeComplete(); + }, + }); + + let proxyError: unknown = undefined; + const proxyPromise = runProxy(wrappedWritable).catch((e) => { + proxyError = e; + signalReady(); + baseWriter.abort(e).catch(() => {}); + signalPipeComplete(); + }); + + ctx.waitUntil(Promise.all([proxyPromise, pipeComplete])); + + await headersReady; + if (proxyError !== undefined) { + return new Response(`${proxyError}`, { + status: 400, + headers: { "Content-Type": "text/plain" }, + }); + } + + return new Response(wrapWithMeter(readable), { + status: getStatus(), + headers: getHeaders(), + }); +} + export function EdgeProxyV1(opts: ProxyOpts) { return async (request: Request, ctx: EdgeContext) => { let corsHeaders = {}; @@ -427,59 +504,12 @@ export function EdgeProxyV1(opts: ProxyOpts) { : body; if (opts.streamingViaWaitUntil) { - let signalReady: () => void = () => {}; - const headersReady = new Promise((resolve) => { - signalReady = resolve; - }); - - let signalPipeComplete: () => void = () => {}; - const pipeComplete = new Promise((resolve) => { - signalPipeComplete = resolve; - }); - - const baseWriter = writable.getWriter(); - const wrappedWritable = new WritableStream({ - async write(chunk) { - signalReady(); - await baseWriter.write(chunk); - }, - async close() { - signalReady(); - await baseWriter.close(); - signalPipeComplete(); - }, - async abort(reason) { - signalReady(); - await baseWriter.abort(reason); - signalPipeComplete(); - }, - }); - - let proxyError: unknown = undefined; - const proxyV1Promise = proxyV1({ - ...proxyV1Args, - res: wrappedWritable, - }).catch((e) => { - proxyError = e; - signalReady(); - baseWriter.abort(e).catch(() => {}); - signalPipeComplete(); - }); - - ctx.waitUntil(Promise.all([proxyV1Promise, pipeComplete])); - - await headersReady; - if (proxyError !== undefined) { - console.error("EdgeProxyV1 request failed", proxyError); - return new Response(`${proxyError}`, { - status: 400, - headers: { "Content-Type": "text/plain" }, - }); - } - - return new Response(wrapWithMeter(readable), { - status, - headers, + return streamingResponseViaWaitUntil({ + ctx, + wrapWithMeter, + runProxy: (res) => proxyV1({ ...proxyV1Args, res }), + getStatus: () => status, + getHeaders: () => headers, }); } From d9425716120f7a8d3659acd0eefbe4cd3b71887d Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 17:58:43 -0700 Subject: [PATCH 17/22] more tests --- apis/vercel/app/api/v1/[...slug]/route.ts | 164 ++++++++++++++---- .../api/v1/test-proxy-notransform/route.ts | 27 --- apis/vercel/app/api/v1/test-proxy/route.ts | 27 --- apis/vercel/app/api/v1/test-stream/route.ts | 29 ---- .../vercel/app/api/v1/test-transform/route.ts | 40 ----- apis/vercel/app/api/v1/test/route.ts | 24 --- apis/vercel/lib/nodeProxy.test.ts | 71 ++++++++ apis/vercel/lib/nodeProxy.ts | 94 ++++++++++ packages/proxy/edge/index.metrics.test.ts | 99 +++++++++++ packages/proxy/edge/index.test.ts | 9 +- packages/proxy/edge/index.ts | 52 +++++- packages/proxy/src/proxy.stream.test.ts | 43 +++++ packages/proxy/src/proxy.ts | 34 ++-- 13 files changed, 511 insertions(+), 202 deletions(-) delete mode 100644 apis/vercel/app/api/v1/test-proxy-notransform/route.ts delete mode 100644 apis/vercel/app/api/v1/test-proxy/route.ts delete mode 100644 apis/vercel/app/api/v1/test-stream/route.ts delete mode 100644 apis/vercel/app/api/v1/test-transform/route.ts delete mode 100644 apis/vercel/app/api/v1/test/route.ts create mode 100644 apis/vercel/lib/nodeProxy.test.ts create mode 100644 apis/vercel/lib/nodeProxy.ts create mode 100644 packages/proxy/src/proxy.stream.test.ts diff --git a/apis/vercel/app/api/v1/[...slug]/route.ts b/apis/vercel/app/api/v1/[...slug]/route.ts index 60f9363c..91a13b89 100644 --- a/apis/vercel/app/api/v1/[...slug]/route.ts +++ b/apis/vercel/app/api/v1/[...slug]/route.ts @@ -1,10 +1,20 @@ import dns from "node:dns"; -import { kv } from "@vercel/kv"; + +import { proxyV1 } from "@braintrust/proxy"; +import { + type CacheSetOptions, + digestMessage, + encryptedGet, + encryptedPut, + getCorsHeaders, + makeFetchApiSecrets, +} from "@braintrust/proxy/edge"; import { waitUntil } from "@vercel/functions"; -import { NextResponse } from "next/server"; -import { EdgeProxyV1, CacheSetOptions } from "@braintrust/proxy/edge"; +import { kv } from "@vercel/kv"; import { Agent, setGlobalDispatcher } from "undici"; +import { nodeStreamingResponseViaPassThrough } from "../../../../lib/nodeProxy"; + dns.setDefaultResultOrder("ipv4first"); setGlobalDispatcher( @@ -21,30 +31,38 @@ const KVCache = { }, }; -const proxyHandler = EdgeProxyV1({ - getRelativeURL: (request) => { - // App Router route is /api/v1/[...slug] — strip the prefix to get - // the upstream path (e.g. "/chat/completions"). - const url = new URL(request.url); - return url.pathname.replace(/^\/api\/v1/, ""); - }, - cors: true, - credentialsCache: KVCache, - completionsCache: KVCache, - braintrustApiUrl: process.env.BRAINTRUST_APP_URL, - // Vercel Node Lambda tears down the function when the handler returns, so we use `waitUntil` to allow background tasks to continue after the response is sent. - streamingViaWaitUntil: true, -}); - -const ctx = { - waitUntil(promise: Promise) { - waitUntil( - promise.catch((error) => { - console.warn("Background task failed", error); - }), - ); - }, -}; +function safeWaitUntil(promise: Promise) { + waitUntil( + promise.catch((error) => { + console.warn("Background task failed", error); + }), + ); +} + +function handleOptions( + request: Request, + corsHeaders: Record, +): Response { + if ( + request.headers.get("Origin") !== null && + request.headers.get("Access-Control-Request-Method") !== null && + request.headers.get("Access-Control-Request-Headers") !== null + ) { + return new Response(null, { + headers: { + ...corsHeaders, + "access-control-allow-headers": + request.headers.get("Access-Control-Request-Headers") ?? "", + }, + }); + } + + return new Response(null, { + headers: { + Allow: "GET, HEAD, POST, OPTIONS", + }, + }); +} async function proxy(request: Request): Promise { const requestId = @@ -69,10 +87,97 @@ async function proxy(request: Request): Promise { braintrustApiUrl: process.env.BRAINTRUST_APP_URL, }); + let corsHeaders = {}; + try { + corsHeaders = getCorsHeaders(request, undefined); + } catch { + return new Response("Forbidden", { status: 403 }); + } + + if (request.method === "OPTIONS") { + return handleOptions(request, corsHeaders); + } + + if (request.method !== "GET" && request.method !== "POST") { + return new Response("Method not allowed", { + status: 405, + headers: { "Content-Type": "text/plain" }, + }); + } + + const method: "GET" | "POST" = request.method; + const relativeURL = new URL(request.url).pathname.replace(/^\/api\/v1/, ""); + const requestBody = await request.text(); + + let status = 200; + const headers: Record = { + ...corsHeaders, + "x-request-id": requestId, + }; + + const setStatusCode = (code: number) => { + status = code; + }; + + const setHeader = (name: string, value: string) => { + headers[name] = value; + }; + + const proxyHeaders: Record = {}; + request.headers.forEach((value, name) => { + proxyHeaders[name] = value; + }); + + const fetchApiSecrets = makeFetchApiSecrets({ + ctx: { waitUntil: safeWaitUntil }, + opts: { + getRelativeURL() { + return relativeURL; + }, + credentialsCache: KVCache, + braintrustApiUrl: process.env.BRAINTRUST_APP_URL, + nativeInferenceSecretKey: process.env.NATIVE_INFERENCE_SECRET_KEY, + }, + }); + + const cacheGet = async (encryptionKey: string, key: string) => { + return (await encryptedGet(KVCache, encryptionKey, key)) ?? null; + }; + + const cachePut = async ( + encryptionKey: string, + key: string, + value: string, + ttlSeconds?: number, + ) => { + const promise = encryptedPut(KVCache, encryptionKey, key, value, { + ttl: ttlSeconds ?? 60 * 60 * 24 * 7, + }); + safeWaitUntil(promise); + await promise; + }; + try { - const response = await proxyHandler(request, ctx); + const response = await nodeStreamingResponseViaPassThrough({ + waitUntil: safeWaitUntil, + runProxy: (res) => + proxyV1({ + method, + url: relativeURL, + proxyHeaders, + body: requestBody, + setHeader, + setStatusCode, + res, + getApiSecrets: fetchApiSecrets, + cacheGet, + cachePut, + digest: digestMessage, + }), + getStatus: () => status, + getHeaders: () => headers, + }); log("route:after-handler", { status: response.status }); - response.headers.set("x-request-id", requestId); return response; } catch (error) { log("route:error", { error: String(error) }); @@ -97,3 +202,4 @@ export const POST = proxy; export const OPTIONS = proxy; export const dynamic = "force-dynamic"; +export const runtime = "nodejs"; diff --git a/apis/vercel/app/api/v1/test-proxy-notransform/route.ts b/apis/vercel/app/api/v1/test-proxy-notransform/route.ts deleted file mode 100644 index bef78983..00000000 --- a/apis/vercel/app/api/v1/test-proxy-notransform/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Same as /test-proxy but with `Cache-Control: no-cache, no-transform`, -// matching Vercel's canonical streaming-proxy example. If this returns a -// body and /test-proxy returns empty, Cloudflare is transforming/dropping -// the body and `no-transform` fixes it. -export const dynamic = "force-dynamic"; - -async function handler(): Promise { - const upstream = await fetch("https://api.github.com/repos/vercel/next.js", { - headers: { "user-agent": "braintrust-proxy-test" }, - }); - const { readable, writable } = new TransformStream(); - upstream.body!.pipeTo(writable).catch((err) => { - console.error("test-proxy-notransform pipeTo failed", err); - }); - - return new Response(readable, { - status: upstream.status, - headers: { - "content-type": - upstream.headers.get("content-type") ?? "application/json", - "cache-control": "no-cache, no-transform", - }, - }); -} - -export const GET = handler; -export const POST = handler; diff --git a/apis/vercel/app/api/v1/test-proxy/route.ts b/apis/vercel/app/api/v1/test-proxy/route.ts deleted file mode 100644 index f54d1f04..00000000 --- a/apis/vercel/app/api/v1/test-proxy/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Isolation test #4: mirror proxyV1's exact pattern — fetch an upstream, -// pipeTo a TransformStream, return readable as Response body. NO -// Cache-Control header (mirrors what proxyV1 effectively does with -// Anthropic's headers). If this returns empty body, the issue is Cloudflare -// (or some intermediary) stripping bodies for this response shape. -export const dynamic = "force-dynamic"; - -async function handler(): Promise { - const upstream = await fetch("https://api.github.com/repos/vercel/next.js", { - headers: { "user-agent": "braintrust-proxy-test" }, - }); - const { readable, writable } = new TransformStream(); - upstream.body!.pipeTo(writable).catch((err) => { - console.error("test-proxy pipeTo failed", err); - }); - - return new Response(readable, { - status: upstream.status, - headers: { - "content-type": - upstream.headers.get("content-type") ?? "application/json", - }, - }); -} - -export const GET = handler; -export const POST = handler; diff --git a/apis/vercel/app/api/v1/test-stream/route.ts b/apis/vercel/app/api/v1/test-stream/route.ts deleted file mode 100644 index 951583cc..00000000 --- a/apis/vercel/app/api/v1/test-stream/route.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Isolation test #2: does `new Response(readableStream, ...)` deliver a -// body on this deployment? Constructs a simple ReadableStream that -// enqueues two chunks then closes — no TransformStream, no background -// writers, no waitUntil. -// -// If this returns "hello world": ReadableStream Response bodies are fine. -// If empty: this Vercel Node deployment can't deliver streaming responses. -export const dynamic = "force-dynamic"; - -async function handler(): Promise { - const encoder = new TextEncoder(); - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(encoder.encode("hello ")); - controller.enqueue(encoder.encode("world")); - controller.close(); - }, - }); - return new Response(stream, { - status: 200, - headers: { - "content-type": "text/plain", - "x-test": "stream", - }, - }); -} - -export const GET = handler; -export const POST = handler; diff --git a/apis/vercel/app/api/v1/test-transform/route.ts b/apis/vercel/app/api/v1/test-transform/route.ts deleted file mode 100644 index 739bcf38..00000000 --- a/apis/vercel/app/api/v1/test-transform/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Isolation test #3: mirrors EdgeProxyV1's exact pattern — a -// TransformStream where bytes are written to `writable` from a background -// task while `readable` is returned as the Response body. No upstream -// fetch, just synthetic writes. -// -// If this returns "hello world": the TransformStream + background-write -// pattern works on Vercel Node, and the issue is somewhere deeper inside -// EdgeProxyV1 / proxyV1 (interaction with the upstream fetch). -// If empty: this exact pattern (which EdgeProxyV1 uses) doesn't work on -// Vercel Node — we need a different response-construction approach. -export const dynamic = "force-dynamic"; - -async function handler(): Promise { - const { readable, writable } = new TransformStream(); - const writer = writable.getWriter(); - const encoder = new TextEncoder(); - - // Fire-and-forget background writes — same shape as proxyV1's - // stream.pipeTo(res).catch(...) - (async () => { - try { - await writer.write(encoder.encode("hello ")); - await writer.write(encoder.encode("world")); - await writer.close(); - } catch (err) { - console.error("transform write error", err); - } - })(); - - return new Response(readable, { - status: 200, - headers: { - "content-type": "text/plain", - "x-test": "transform", - }, - }); -} - -export const GET = handler; -export const POST = handler; diff --git a/apis/vercel/app/api/v1/test/route.ts b/apis/vercel/app/api/v1/test/route.ts deleted file mode 100644 index acc66978..00000000 --- a/apis/vercel/app/api/v1/test/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Isolation test: does *any* response body reach the client on this -// deployment? No EdgeProxyV1, no streaming, no upstream call. Just a -// fixed JSON body. If this comes back empty, the problem is the -// deployment pipeline (Cloudflare in front, Vercel preview protection, -// the project's domain config), not our proxy code. -export const dynamic = "force-dynamic"; - -export async function GET(): Promise { - const body = JSON.stringify({ - test: "hello", - at: new Date().toISOString(), - }); - return new Response(body, { - status: 200, - headers: { - "content-type": "application/json", - "x-test": "true", - }, - }); -} - -export async function POST(): Promise { - return GET(); -} diff --git a/apis/vercel/lib/nodeProxy.test.ts b/apis/vercel/lib/nodeProxy.test.ts new file mode 100644 index 00000000..64218e85 --- /dev/null +++ b/apis/vercel/lib/nodeProxy.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; + +import { nodeStreamingResponseViaPassThrough } from "./nodeProxy"; + +describe("nodeStreamingResponseViaPassThrough", () => { + it("returns application/json bodies", async () => { + const encoder = new TextEncoder(); + + const response = await nodeStreamingResponseViaPassThrough({ + runProxy: async (res) => { + const writer = res.getWriter(); + await writer.write(encoder.encode(JSON.stringify({ ok: true }))); + await writer.close(); + }, + getStatus: () => 200, + getHeaders: () => ({ "content-type": "application/json" }), + }); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("application/json"); + await expect(response.json()).resolves.toEqual({ ok: true }); + }); + + it("streams multiple chunks through a node PassThrough body", async () => { + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + const waitUntilPromises: Promise[] = []; + let returnedResponse = false; + + const responsePromise = nodeStreamingResponseViaPassThrough({ + runProxy: async (res) => { + await new Promise((resolve) => setTimeout(resolve, 20)); + const writer = res.getWriter(); + await writer.write(encoder.encode("data: first\n\n")); + await new Promise((resolve) => setTimeout(resolve, 20)); + await writer.write(encoder.encode("data: second\n\n")); + await writer.close(); + }, + getStatus: () => 200, + getHeaders: () => ({ "content-type": "text/event-stream" }), + waitUntil(promise) { + waitUntilPromises.push(promise); + }, + }).then((response) => { + returnedResponse = true; + return response; + }); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(returnedResponse).toBe(false); + + const response = await responsePromise; + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("text/event-stream"); + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Expected response body"); + } + + const first = await reader.read(); + const second = await reader.read(); + const third = await reader.read(); + + expect(decoder.decode(first.value)).toBe("data: first\n\n"); + expect(decoder.decode(second.value)).toBe("data: second\n\n"); + expect(third.done).toBe(true); + expect(waitUntilPromises).toHaveLength(1); + await expect(Promise.all(waitUntilPromises)).resolves.toBeDefined(); + }); +}); diff --git a/apis/vercel/lib/nodeProxy.ts b/apis/vercel/lib/nodeProxy.ts new file mode 100644 index 00000000..61d2a119 --- /dev/null +++ b/apis/vercel/lib/nodeProxy.ts @@ -0,0 +1,94 @@ +import { once } from "node:events"; +import { PassThrough } from "node:stream"; + +function errorFromUnknown(reason: unknown): Error { + return reason instanceof Error ? reason : new Error(String(reason)); +} + +function passThroughToReadableStream( + passThrough: PassThrough, +): ReadableStream { + return new ReadableStream({ + start(controller) { + passThrough.on("data", (chunk: Buffer) => { + controller.enqueue(new Uint8Array(chunk)); + }); + passThrough.on("end", () => { + controller.close(); + }); + passThrough.on("error", (error) => { + controller.error(error); + }); + }, + cancel(reason) { + passThrough.destroy(errorFromUnknown(reason)); + }, + }); +} + +export async function nodeStreamingResponseViaPassThrough({ + runProxy, + getStatus, + getHeaders, + waitUntil, +}: { + runProxy: (res: WritableStream) => Promise; + getStatus: () => number; + getHeaders: () => Record; + waitUntil?: (promise: Promise) => void; +}): Promise { + const passThrough = new PassThrough(); + + let signalReady: () => void = () => {}; + const ready = new Promise((resolve) => { + signalReady = resolve; + }); + + let signalDone: () => void = () => {}; + const done = new Promise((resolve) => { + signalDone = resolve; + }); + + let proxyError: unknown = undefined; + + const writable = new WritableStream({ + async write(chunk) { + if (!passThrough.write(chunk)) { + await once(passThrough, "drain"); + } + signalReady(); + }, + close() { + passThrough.end(); + signalReady(); + signalDone(); + }, + abort(reason) { + passThrough.destroy(errorFromUnknown(reason)); + signalReady(); + signalDone(); + }, + }); + + const proxyPromise = runProxy(writable).catch(async (error) => { + proxyError = error; + await writable.abort(error).catch(() => {}); + }); + + const lifetimePromise = Promise.all([proxyPromise, done]); + waitUntil?.(lifetimePromise); + + await Promise.race([ready, proxyPromise]); + + if (proxyError !== undefined) { + return new Response(`${proxyError}`, { + status: 400, + headers: { "Content-Type": "text/plain" }, + }); + } + + return new Response(passThroughToReadableStream(passThrough), { + status: getStatus(), + headers: getHeaders(), + }); +} diff --git a/packages/proxy/edge/index.metrics.test.ts b/packages/proxy/edge/index.metrics.test.ts index 88813abc..ca2fd025 100644 --- a/packages/proxy/edge/index.metrics.test.ts +++ b/packages/proxy/edge/index.metrics.test.ts @@ -71,4 +71,103 @@ describe("EdgeProxyV1 metric flushing", () => { expect(waitUntilPromises).toHaveLength(1); await Promise.all(waitUntilPromises); }); + + it("returns application/json on the default response path", async () => { + const { EdgeProxyV1 } = await import("./index"); + + proxyV1Mock.mockImplementation( + async ({ + res, + setHeader, + }: { + res: WritableStream; + setHeader: (name: string, value: string) => void; + }) => { + setHeader("content-type", "application/json"); + const writer = res.getWriter(); + queueMicrotask(() => { + void writer + .write(new TextEncoder().encode(JSON.stringify({ ok: true }))) + .then(() => writer.close()); + }); + }, + ); + + const waitUntilPromises: Promise[] = []; + const handler = EdgeProxyV1({ + getRelativeURL() { + return "/chat/completions"; + }, + }); + + const response = await handler( + new Request("https://example.com/v1/chat/completions", { + method: "POST", + body: "{}", + headers: { + Authorization: "Bearer test-token", + }, + }), + { + waitUntil(promise) { + waitUntilPromises.push(promise); + }, + }, + ); + + expect(response.headers.get("content-type")).toBe("application/json"); + await expect(response.json()).resolves.toEqual({ ok: true }); + expect(waitUntilPromises).toHaveLength(0); + }); + + it("streams text/event-stream when streamingViaWaitUntil is enabled", async () => { + const { EdgeProxyV1 } = await import("./index"); + + proxyV1Mock.mockImplementation( + async ({ + res, + setHeader, + }: { + res: WritableStream; + setHeader: (name: string, value: string) => void; + }) => { + setHeader("content-type", "text/event-stream"); + const writer = res.getWriter(); + await writer.write(new TextEncoder().encode("data: first\n\n")); + await new Promise((resolve) => setTimeout(resolve, 20)); + await writer.write(new TextEncoder().encode("data: second\n\n")); + await writer.close(); + }, + ); + + const waitUntilPromises: Promise[] = []; + const handler = EdgeProxyV1({ + getRelativeURL() { + return "/chat/completions"; + }, + streamingViaWaitUntil: true, + }); + + const response = await handler( + new Request("https://example.com/v1/chat/completions", { + method: "POST", + body: JSON.stringify({ stream: true }), + headers: { + Authorization: "Bearer test-token", + }, + }), + { + waitUntil(promise) { + waitUntilPromises.push(promise); + }, + }, + ); + + expect(response.headers.get("content-type")).toBe("text/event-stream"); + await expect(response.text()).resolves.toBe( + "data: first\n\ndata: second\n\n", + ); + expect(waitUntilPromises).toHaveLength(1); + await expect(Promise.all(waitUntilPromises)).resolves.toBeDefined(); + }); }); diff --git a/packages/proxy/edge/index.test.ts b/packages/proxy/edge/index.test.ts index 8fe1b351..a4d557df 100644 --- a/packages/proxy/edge/index.test.ts +++ b/packages/proxy/edge/index.test.ts @@ -265,10 +265,9 @@ describe("streamingResponseViaWaitUntil", () => { expect(await response.text()).toContain("boom"); }); - it("keeps the platform alive via ctx.waitUntil until the pipe drains", async () => { + it("registers a waitUntil promise that drains the pipe", async () => { const { waitUntilPromises, ctx, wrapWithMeter } = setup(); const encoder = new TextEncoder(); - let pipeFinished = false; const runProxy = async (res: WritableStream) => { (async () => { @@ -277,7 +276,6 @@ describe("streamingResponseViaWaitUntil", () => { await new Promise((r) => setTimeout(r, 30)); await writer.write(encoder.encode("-second")); await writer.close(); - pipeFinished = true; })(); }; @@ -289,14 +287,11 @@ describe("streamingResponseViaWaitUntil", () => { getHeaders: () => ({}), }); - expect(pipeFinished).toBe(false); expect(waitUntilPromises.length).toBe(1); const text = await response.text(); expect(text).toBe("first-second"); - await Promise.all(waitUntilPromises); - await new Promise((r) => setTimeout(r, 0)); - expect(pipeFinished).toBe(true); + await expect(Promise.all(waitUntilPromises)).resolves.toBeDefined(); }); }); diff --git a/packages/proxy/edge/index.ts b/packages/proxy/edge/index.ts index 68e9c5c0..501ba09c 100644 --- a/packages/proxy/edge/index.ts +++ b/packages/proxy/edge/index.ts @@ -344,18 +344,36 @@ export async function streamingResponseViaWaitUntil({ signalPipeComplete = resolve; }); + let writeCount = 0; + let totalBytes = 0; + let closed = false; + let aborted = false; + const baseWriter = writable.getWriter(); const wrappedWritable = new WritableStream({ async write(chunk) { + writeCount += 1; + totalBytes += chunk.byteLength; await baseWriter.write(chunk); + console.log( + `streamingResponseViaWaitUntil write #${writeCount} bytes=${chunk.byteLength} total=${totalBytes}`, + ); signalReady(); }, async close() { + closed = true; + console.log( + `streamingResponseViaWaitUntil close writes=${writeCount} total=${totalBytes}`, + ); await baseWriter.close(); signalReady(); signalPipeComplete(); }, async abort(reason) { + aborted = true; + console.error( + `streamingResponseViaWaitUntil abort writes=${writeCount} total=${totalBytes} reason=${reason}`, + ); await baseWriter.abort(reason); signalReady(); signalPipeComplete(); @@ -363,14 +381,30 @@ export async function streamingResponseViaWaitUntil({ }); let proxyError: unknown = undefined; - const proxyPromise = runProxy(wrappedWritable).catch((e) => { - proxyError = e; - signalReady(); - baseWriter.abort(e).catch(() => {}); - signalPipeComplete(); - }); + const proxyPromise = runProxy(wrappedWritable) + .then(() => { + console.log( + `streamingResponseViaWaitUntil runProxy resolved writes=${writeCount} total=${totalBytes} closed=${closed} aborted=${aborted}`, + ); + }) + .catch((e) => { + console.error( + `streamingResponseViaWaitUntil runProxy threw writes=${writeCount} total=${totalBytes} closed=${closed} aborted=${aborted}`, + e, + ); + proxyError = e; + signalReady(); + baseWriter.abort(e).catch(() => {}); + signalPipeComplete(); + }); - ctx.waitUntil(Promise.all([proxyPromise, pipeComplete])); + ctx.waitUntil( + Promise.all([proxyPromise, pipeComplete]).then(() => { + console.log( + `streamingResponseViaWaitUntil waitUntil settled writes=${writeCount} total=${totalBytes} closed=${closed} aborted=${aborted}`, + ); + }), + ); await headersReady; if (proxyError !== undefined) { @@ -380,6 +414,10 @@ export async function streamingResponseViaWaitUntil({ }); } + console.log( + `streamingResponseViaWaitUntil returning Response status=${getStatus()} writes=${writeCount} total=${totalBytes}`, + ); + return new Response(wrapWithMeter(readable), { status: getStatus(), headers: getHeaders(), diff --git a/packages/proxy/src/proxy.stream.test.ts b/packages/proxy/src/proxy.stream.test.ts new file mode 100644 index 00000000..257ca537 --- /dev/null +++ b/packages/proxy/src/proxy.stream.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import { pipeBodyToResponse } from "./proxy"; + +describe("pipeBodyToResponse", () => { + it("resolves after the readable stream finishes piping", async () => { + const encoder = new TextEncoder(); + const chunks: string[] = []; + let closed = false; + let finishedWriting = false; + + const stream = new ReadableStream({ + async start(controller) { + controller.enqueue(encoder.encode("first")); + await new Promise((resolve) => setTimeout(resolve, 20)); + controller.enqueue(encoder.encode("-second")); + finishedWriting = true; + controller.close(); + }, + }); + + const responsePromise = pipeBodyToResponse( + stream, + new WritableStream({ + write(chunk) { + chunks.push(new TextDecoder().decode(chunk)); + }, + close() { + closed = true; + }, + }), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(finishedWriting).toBe(false); + expect(closed).toBe(false); + + await responsePromise; + + expect(finishedWriting).toBe(true); + expect(closed).toBe(true); + expect(chunks.join("")).toBe("first-second"); + }); +}); diff --git a/packages/proxy/src/proxy.ts b/packages/proxy/src/proxy.ts index 7651b2d7..1d3e78ec 100644 --- a/packages/proxy/src/proxy.ts +++ b/packages/proxy/src/proxy.ts @@ -436,10 +436,11 @@ export async function proxyV1({ e instanceof Error ? e.message : JSON.stringify(e), ); } finally { - if (readable) { - readable.pipeTo(res).catch(console.error); - } else { - res.close().catch(console.error); + try { + await pipeBodyToResponse(readable, res); + } catch (e) { + console.error("Error writing credentials response", e); + throw e; } } return; @@ -1222,14 +1223,11 @@ export async function proxyV1({ stream = stream.pipeThrough(parseStream); } - if (stream) { - stream.pipeTo(res).catch((e) => { - console.error("Error piping stream to response", e); - }); - } else { - res.close().catch((e) => { - console.error("Error closing response", e); - }); + try { + await pipeBodyToResponse(stream, res); + } catch (e) { + console.error("Error piping stream to response", e); + throw e; } logRequest(); @@ -1240,6 +1238,18 @@ export async function proxyV1({ }); } +export async function pipeBodyToResponse( + stream: ReadableStream | null, + res: WritableStream, +) { + if (stream) { + await stream.pipeTo(res); + return; + } + + await res.close(); +} + const RATE_LIMIT_ERROR_CODE = 429; const OVERLOADED_ERROR_CODE = 503; const RATE_LIMIT_MAX_WAIT_MS = 45 * 1000; // Wait up to 45 seconds while retrying From ec9abb372ae2618b363ead7cc4ff14434cd1087b Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 18:24:48 -0700 Subject: [PATCH 18/22] flip back to pages --- apis/vercel/app/api/v1/[...slug]/route.ts | 205 --------------------- apis/vercel/lib/nodeProxy.test.ts | 123 +++++++------ apis/vercel/lib/nodeProxy.ts | 161 ++++++++--------- apis/vercel/pages/api/v1/[...slug].ts | 211 ++++++++++++++++++++++ 4 files changed, 361 insertions(+), 339 deletions(-) delete mode 100644 apis/vercel/app/api/v1/[...slug]/route.ts create mode 100644 apis/vercel/pages/api/v1/[...slug].ts diff --git a/apis/vercel/app/api/v1/[...slug]/route.ts b/apis/vercel/app/api/v1/[...slug]/route.ts deleted file mode 100644 index 91a13b89..00000000 --- a/apis/vercel/app/api/v1/[...slug]/route.ts +++ /dev/null @@ -1,205 +0,0 @@ -import dns from "node:dns"; - -import { proxyV1 } from "@braintrust/proxy"; -import { - type CacheSetOptions, - digestMessage, - encryptedGet, - encryptedPut, - getCorsHeaders, - makeFetchApiSecrets, -} from "@braintrust/proxy/edge"; -import { waitUntil } from "@vercel/functions"; -import { kv } from "@vercel/kv"; -import { Agent, setGlobalDispatcher } from "undici"; - -import { nodeStreamingResponseViaPassThrough } from "../../../../lib/nodeProxy"; - -dns.setDefaultResultOrder("ipv4first"); - -setGlobalDispatcher( - new Agent({ - keepAliveTimeout: 1, - keepAliveMaxTimeout: 1, - }), -); - -const KVCache = { - get: (key: string) => kv.get(key), - set: async (key: string, value: T, opts: CacheSetOptions) => { - await kv.set(key, value, opts.ttl !== undefined ? { ex: opts.ttl } : {}); - }, -}; - -function safeWaitUntil(promise: Promise) { - waitUntil( - promise.catch((error) => { - console.warn("Background task failed", error); - }), - ); -} - -function handleOptions( - request: Request, - corsHeaders: Record, -): Response { - if ( - request.headers.get("Origin") !== null && - request.headers.get("Access-Control-Request-Method") !== null && - request.headers.get("Access-Control-Request-Headers") !== null - ) { - return new Response(null, { - headers: { - ...corsHeaders, - "access-control-allow-headers": - request.headers.get("Access-Control-Request-Headers") ?? "", - }, - }); - } - - return new Response(null, { - headers: { - Allow: "GET, HEAD, POST, OPTIONS", - }, - }); -} - -async function proxy(request: Request): Promise { - const requestId = - request.headers.get("x-request-id") ?? - Math.random().toString(36).slice(2, 10); - const start = Date.now(); - const log = (msg: string, extra?: Record) => { - console.log( - JSON.stringify({ - requestId, - method: request.method, - path: new URL(request.url).pathname, - elapsedMs: Date.now() - start, - msg, - ...extra, - }), - ); - }; - - log("route:start", { - hasAuth: request.headers.has("authorization"), - braintrustApiUrl: process.env.BRAINTRUST_APP_URL, - }); - - let corsHeaders = {}; - try { - corsHeaders = getCorsHeaders(request, undefined); - } catch { - return new Response("Forbidden", { status: 403 }); - } - - if (request.method === "OPTIONS") { - return handleOptions(request, corsHeaders); - } - - if (request.method !== "GET" && request.method !== "POST") { - return new Response("Method not allowed", { - status: 405, - headers: { "Content-Type": "text/plain" }, - }); - } - - const method: "GET" | "POST" = request.method; - const relativeURL = new URL(request.url).pathname.replace(/^\/api\/v1/, ""); - const requestBody = await request.text(); - - let status = 200; - const headers: Record = { - ...corsHeaders, - "x-request-id": requestId, - }; - - const setStatusCode = (code: number) => { - status = code; - }; - - const setHeader = (name: string, value: string) => { - headers[name] = value; - }; - - const proxyHeaders: Record = {}; - request.headers.forEach((value, name) => { - proxyHeaders[name] = value; - }); - - const fetchApiSecrets = makeFetchApiSecrets({ - ctx: { waitUntil: safeWaitUntil }, - opts: { - getRelativeURL() { - return relativeURL; - }, - credentialsCache: KVCache, - braintrustApiUrl: process.env.BRAINTRUST_APP_URL, - nativeInferenceSecretKey: process.env.NATIVE_INFERENCE_SECRET_KEY, - }, - }); - - const cacheGet = async (encryptionKey: string, key: string) => { - return (await encryptedGet(KVCache, encryptionKey, key)) ?? null; - }; - - const cachePut = async ( - encryptionKey: string, - key: string, - value: string, - ttlSeconds?: number, - ) => { - const promise = encryptedPut(KVCache, encryptionKey, key, value, { - ttl: ttlSeconds ?? 60 * 60 * 24 * 7, - }); - safeWaitUntil(promise); - await promise; - }; - - try { - const response = await nodeStreamingResponseViaPassThrough({ - waitUntil: safeWaitUntil, - runProxy: (res) => - proxyV1({ - method, - url: relativeURL, - proxyHeaders, - body: requestBody, - setHeader, - setStatusCode, - res, - getApiSecrets: fetchApiSecrets, - cacheGet, - cachePut, - digest: digestMessage, - }), - getStatus: () => status, - getHeaders: () => headers, - }); - log("route:after-handler", { status: response.status }); - return response; - } catch (error) { - log("route:error", { error: String(error) }); - return new Response( - JSON.stringify({ - error: error instanceof Error ? error.message : String(error), - requestId, - }), - { - status: 500, - headers: { - "Content-Type": "application/json", - "x-request-id": requestId, - }, - }, - ); - } -} - -export const GET = proxy; -export const POST = proxy; -export const OPTIONS = proxy; - -export const dynamic = "force-dynamic"; -export const runtime = "nodejs"; diff --git a/apis/vercel/lib/nodeProxy.test.ts b/apis/vercel/lib/nodeProxy.test.ts index 64218e85..b9b3d5c7 100644 --- a/apis/vercel/lib/nodeProxy.test.ts +++ b/apis/vercel/lib/nodeProxy.test.ts @@ -1,71 +1,92 @@ +import { PassThrough } from "node:stream"; + import { describe, expect, it } from "vitest"; -import { nodeStreamingResponseViaPassThrough } from "./nodeProxy"; +import { proxyV1ToNodeResponse } from "./nodeProxy"; + +describe("proxyV1ToNodeResponse", () => { + it("reassembles split application/json chunks", async () => { + const headers = new Map(); + let statusCode = 200; + const res = new PassThrough(); + const bodyChunks: Buffer[] = []; -describe("nodeStreamingResponseViaPassThrough", () => { - it("returns application/json bodies", async () => { - const encoder = new TextEncoder(); + res.on("data", (chunk: Buffer) => { + bodyChunks.push(chunk); + }); - const response = await nodeStreamingResponseViaPassThrough({ - runProxy: async (res) => { + await proxyV1ToNodeResponse({ + method: "POST", + url: "/chat/completions", + proxyHeaders: {}, + body: "{}", + setHeader(name, value) { + headers.set(name, value); + }, + setStatusCode(code) { + statusCode = code; + }, + getApiSecrets: async () => [], + cacheGet: async () => null, + cachePut: async () => {}, + digest: async (message) => message, + getRes: () => res, + proxyImpl: async ({ res, setHeader }) => { + setHeader("content-type", "application/json"); const writer = res.getWriter(); - await writer.write(encoder.encode(JSON.stringify({ ok: true }))); + await writer.write(new TextEncoder().encode('{"ok":')); + await new Promise((resolve) => setTimeout(resolve, 20)); + await writer.write(new TextEncoder().encode('true,"parts":[1,2]}')); await writer.close(); }, - getStatus: () => 200, - getHeaders: () => ({ "content-type": "application/json" }), }); - expect(response.status).toBe(200); - expect(response.headers.get("content-type")).toBe("application/json"); - await expect(response.json()).resolves.toEqual({ ok: true }); + expect(statusCode).toBe(200); + expect(headers.get("content-type")).toBe("application/json"); + expect(JSON.parse(Buffer.concat(bodyChunks).toString("utf8"))).toEqual({ + ok: true, + parts: [1, 2], + }); }); - it("streams multiple chunks through a node PassThrough body", async () => { - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - const waitUntilPromises: Promise[] = []; - let returnedResponse = false; + it("streams split SSE frames for stream=true style responses", async () => { + const headers = new Map(); + const res = new PassThrough(); + const bodyChunks: Buffer[] = []; - const responsePromise = nodeStreamingResponseViaPassThrough({ - runProxy: async (res) => { - await new Promise((resolve) => setTimeout(resolve, 20)); + res.on("data", (chunk: Buffer) => { + bodyChunks.push(chunk); + }); + + await proxyV1ToNodeResponse({ + method: "POST", + url: "/chat/completions", + proxyHeaders: {}, + body: '{"stream":true}', + setHeader(name, value) { + headers.set(name, value); + }, + setStatusCode() {}, + getApiSecrets: async () => [], + cacheGet: async () => null, + cachePut: async () => {}, + digest: async (message) => message, + getRes: () => res, + proxyImpl: async ({ res, setHeader }) => { + setHeader("content-type", "text/event-stream"); const writer = res.getWriter(); - await writer.write(encoder.encode("data: first\n\n")); + await writer.write(new TextEncoder().encode('data: {"id":"chunk-1"')); await new Promise((resolve) => setTimeout(resolve, 20)); - await writer.write(encoder.encode("data: second\n\n")); + await writer.write(new TextEncoder().encode("}\n\n")); + await new Promise((resolve) => setTimeout(resolve, 20)); + await writer.write(new TextEncoder().encode("data: [DONE]\n\n")); await writer.close(); }, - getStatus: () => 200, - getHeaders: () => ({ "content-type": "text/event-stream" }), - waitUntil(promise) { - waitUntilPromises.push(promise); - }, - }).then((response) => { - returnedResponse = true; - return response; }); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(returnedResponse).toBe(false); - - const response = await responsePromise; - expect(response.status).toBe(200); - expect(response.headers.get("content-type")).toBe("text/event-stream"); - - const reader = response.body?.getReader(); - if (!reader) { - throw new Error("Expected response body"); - } - - const first = await reader.read(); - const second = await reader.read(); - const third = await reader.read(); - - expect(decoder.decode(first.value)).toBe("data: first\n\n"); - expect(decoder.decode(second.value)).toBe("data: second\n\n"); - expect(third.done).toBe(true); - expect(waitUntilPromises).toHaveLength(1); - await expect(Promise.all(waitUntilPromises)).resolves.toBeDefined(); + expect(headers.get("content-type")).toBe("text/event-stream"); + expect(Buffer.concat(bodyChunks).toString("utf8")).toBe( + 'data: {"id":"chunk-1"}\n\ndata: [DONE]\n\n', + ); }); }); diff --git a/apis/vercel/lib/nodeProxy.ts b/apis/vercel/lib/nodeProxy.ts index 61d2a119..4fdea40d 100644 --- a/apis/vercel/lib/nodeProxy.ts +++ b/apis/vercel/lib/nodeProxy.ts @@ -1,94 +1,89 @@ -import { once } from "node:events"; -import { PassThrough } from "node:stream"; +import { Readable, type Writable } from "node:stream"; +import { pipeline } from "node:stream/promises"; -function errorFromUnknown(reason: unknown): Error { - return reason instanceof Error ? reason : new Error(String(reason)); -} - -function passThroughToReadableStream( - passThrough: PassThrough, -): ReadableStream { - return new ReadableStream({ - start(controller) { - passThrough.on("data", (chunk: Buffer) => { - controller.enqueue(new Uint8Array(chunk)); - }); - passThrough.on("end", () => { - controller.close(); - }); - passThrough.on("error", (error) => { - controller.error(error); - }); - }, - cancel(reason) { - passThrough.destroy(errorFromUnknown(reason)); - }, - }); -} +import { proxyV1 } from "@braintrust/proxy"; -export async function nodeStreamingResponseViaPassThrough({ - runProxy, - getStatus, - getHeaders, - waitUntil, -}: { - runProxy: (res: WritableStream) => Promise; - getStatus: () => number; - getHeaders: () => Record; - waitUntil?: (promise: Promise) => void; -}): Promise { - const passThrough = new PassThrough(); +type GetApiSecrets = ( + useCache: boolean, + authToken: string, + model: string | null, + orgName?: string, + projectId?: string, +) => Promise< + { + secret: string; + type: string; + org_name?: string | null; + metadata?: Record | null; + }[] +>; - let signalReady: () => void = () => {}; - const ready = new Promise((resolve) => { - signalReady = resolve; - }); +export async function readRawRequestBody( + req: NodeJS.ReadableStream, +): Promise { + const chunks: Buffer[] = []; - let signalDone: () => void = () => {}; - const done = new Promise((resolve) => { - signalDone = resolve; - }); + for await (const chunk of req) { + chunks.push( + typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk), + ); + } - let proxyError: unknown = undefined; + return Buffer.concat(chunks).toString("utf8"); +} - const writable = new WritableStream({ - async write(chunk) { - if (!passThrough.write(chunk)) { - await once(passThrough, "drain"); - } - signalReady(); - }, - close() { - passThrough.end(); - signalReady(); - signalDone(); - }, - abort(reason) { - passThrough.destroy(errorFromUnknown(reason)); - signalReady(); - signalDone(); - }, - }); +export async function proxyV1ToNodeResponse({ + method, + url, + proxyHeaders, + body, + setHeader, + setStatusCode, + getApiSecrets, + cacheGet, + cachePut, + digest, + getRes, + proxyImpl = proxyV1, +}: { + method: "GET" | "POST"; + url: string; + proxyHeaders: Record; + body: string; + setHeader: (name: string, value: string) => void; + setStatusCode: (code: number) => void; + getApiSecrets: GetApiSecrets; + cacheGet: (encryptionKey: string, key: string) => Promise; + cachePut: ( + encryptionKey: string, + key: string, + value: string, + ttlSeconds?: number, + ) => Promise; + digest: (message: string) => Promise; + getRes: () => Writable; + proxyImpl?: typeof proxyV1; +}) { + const { readable, writable } = new TransformStream(); + const nodeReadable = Readable.fromWeb(readable); + const res = getRes(); - const proxyPromise = runProxy(writable).catch(async (error) => { - proxyError = error; + const proxyPromise = proxyImpl({ + method, + url, + proxyHeaders, + body, + setHeader, + setStatusCode, + res: writable, + getApiSecrets, + cacheGet, + cachePut, + digest, + }).catch(async (error) => { await writable.abort(error).catch(() => {}); + throw error; }); - const lifetimePromise = Promise.all([proxyPromise, done]); - waitUntil?.(lifetimePromise); - - await Promise.race([ready, proxyPromise]); - - if (proxyError !== undefined) { - return new Response(`${proxyError}`, { - status: 400, - headers: { "Content-Type": "text/plain" }, - }); - } - - return new Response(passThroughToReadableStream(passThrough), { - status: getStatus(), - headers: getHeaders(), - }); + await Promise.all([proxyPromise, pipeline(nodeReadable, res)]); } diff --git a/apis/vercel/pages/api/v1/[...slug].ts b/apis/vercel/pages/api/v1/[...slug].ts new file mode 100644 index 00000000..fd392545 --- /dev/null +++ b/apis/vercel/pages/api/v1/[...slug].ts @@ -0,0 +1,211 @@ +import dns from "node:dns"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import { + type CacheSetOptions, + digestMessage, + encryptedGet, + encryptedPut, + getCorsHeaders, + makeFetchApiSecrets, +} from "@braintrust/proxy/edge"; +import { kv } from "@vercel/kv"; +import { Agent, setGlobalDispatcher } from "undici"; + +import { + proxyV1ToNodeResponse, + readRawRequestBody, +} from "../../../lib/nodeProxy"; + +dns.setDefaultResultOrder("ipv4first"); + +setGlobalDispatcher( + new Agent({ + keepAliveTimeout: 1, + keepAliveMaxTimeout: 1, + }), +); + +const KVCache = { + get: (key: string) => kv.get(key), + set: async (key: string, value: T, opts: CacheSetOptions) => { + await kv.set(key, value, opts.ttl !== undefined ? { ex: opts.ttl } : {}); + }, +}; + +function normalizeHeaders( + headers: NextApiRequest["headers"], +): Record { + const normalized: Record = {}; + + for (const [name, value] of Object.entries(headers)) { + if (value === undefined) { + continue; + } + + normalized[name] = Array.isArray(value) ? value.join(",") : value; + } + + return normalized; +} + +function handleOptions( + req: NextApiRequest, + res: NextApiResponse, + corsHeaders: Record, +) { + const accessControlRequestHeaders = normalizeHeaders(req.headers)[ + "access-control-request-headers" + ]; + + if ( + req.headers.origin !== undefined && + req.headers["access-control-request-method"] !== undefined && + accessControlRequestHeaders !== undefined + ) { + res.writeHead(200, { + ...corsHeaders, + "access-control-allow-headers": accessControlRequestHeaders, + }); + res.end(); + return; + } + + res.writeHead(200, { + Allow: "GET, HEAD, POST, OPTIONS", + }); + res.end(); +} + +export const config = { + api: { + bodyParser: false, + externalResolver: true, + }, +}; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + const requestId = + (typeof req.headers["x-request-id"] === "string" + ? req.headers["x-request-id"] + : undefined) ?? Math.random().toString(36).slice(2, 10); + const start = Date.now(); + const requestUrl = new URL( + req.url ?? "/api/v1", + `https://${req.headers.host ?? "localhost"}`, + ); + const log = (msg: string, extra?: Record) => { + console.log( + JSON.stringify({ + requestId, + method: req.method, + path: requestUrl.pathname, + elapsedMs: Date.now() - start, + msg, + ...extra, + }), + ); + }; + + log("route:start", { + hasAuth: req.headers.authorization !== undefined, + braintrustApiUrl: process.env.BRAINTRUST_APP_URL, + }); + + let corsHeaders = {}; + try { + corsHeaders = getCorsHeaders( + new Request(requestUrl.toString(), { + headers: normalizeHeaders(req.headers), + method: req.method, + }), + undefined, + ); + } catch { + res.status(403).send("Forbidden"); + return; + } + + if (req.method === "OPTIONS") { + handleOptions(req, res, corsHeaders); + return; + } + + if (req.method !== "GET" && req.method !== "POST") { + res.status(405).setHeader("Content-Type", "text/plain"); + res.send("Method not allowed"); + return; + } + + const method: "GET" | "POST" = req.method; + const proxyHeaders = normalizeHeaders(req.headers); + const relativeURL = `${requestUrl.pathname.replace(/^\/api\/v1/, "")}${requestUrl.search}`; + const requestBody = method === "POST" ? await readRawRequestBody(req) : ""; + + for (const [name, value] of Object.entries(corsHeaders)) { + res.setHeader(name, value); + } + res.setHeader("x-request-id", requestId); + + const fetchApiSecrets = makeFetchApiSecrets({ + ctx: { + waitUntil(promise) { + void promise.catch((error) => { + console.warn("Background task failed", error); + }); + }, + }, + opts: { + getRelativeURL() { + return relativeURL; + }, + credentialsCache: KVCache, + braintrustApiUrl: process.env.BRAINTRUST_APP_URL, + nativeInferenceSecretKey: process.env.NATIVE_INFERENCE_SECRET_KEY, + }, + }); + + try { + await proxyV1ToNodeResponse({ + method, + url: relativeURL, + proxyHeaders, + body: requestBody, + setHeader(name, value) { + res.setHeader(name, value); + }, + setStatusCode(code) { + res.statusCode = code; + }, + getApiSecrets: fetchApiSecrets, + cacheGet: async (encryptionKey, key) => { + return (await encryptedGet(KVCache, encryptionKey, key)) ?? null; + }, + cachePut: async (encryptionKey, key, value, ttlSeconds) => { + await encryptedPut(KVCache, encryptionKey, key, value, { + ttl: ttlSeconds ?? 60 * 60 * 24 * 7, + }); + }, + digest: digestMessage, + getRes: () => res, + }); + log("route:after-handler", { status: res.statusCode }); + } catch (error) { + log("route:error", { error: String(error) }); + if (!res.headersSent) { + res + .status(500) + .setHeader("Content-Type", "application/json") + .json({ + error: error instanceof Error ? error.message : String(error), + requestId, + }); + return; + } + + res.end(); + } +} From 1e9e637251aad39f11b3d3f2efc981da17ea4ff8 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 19:04:33 -0700 Subject: [PATCH 19/22] fully move to app router --- .../api/ping.ts => app/api/ping/route.ts} | 26 +-- apis/vercel/app/api/v1/[...slug]/route.ts | 211 ++++++++++++++++++ apis/vercel/lib/appRouteProxy.test.ts | 164 ++++++++++++++ apis/vercel/lib/appRouteProxy.ts | 137 ++++++++++++ apis/vercel/lib/nodeProxy.test.ts | 92 -------- apis/vercel/lib/nodeProxy.ts | 89 -------- apis/vercel/next-env.d.ts | 2 +- apis/vercel/pages/api/v1/[...slug].ts | 211 ------------------ 8 files changed, 524 insertions(+), 408 deletions(-) rename apis/vercel/{pages/api/ping.ts => app/api/ping/route.ts} (52%) create mode 100644 apis/vercel/app/api/v1/[...slug]/route.ts create mode 100644 apis/vercel/lib/appRouteProxy.test.ts create mode 100644 apis/vercel/lib/appRouteProxy.ts delete mode 100644 apis/vercel/lib/nodeProxy.test.ts delete mode 100644 apis/vercel/lib/nodeProxy.ts delete mode 100644 apis/vercel/pages/api/v1/[...slug].ts diff --git a/apis/vercel/pages/api/ping.ts b/apis/vercel/app/api/ping/route.ts similarity index 52% rename from apis/vercel/pages/api/ping.ts rename to apis/vercel/app/api/ping/route.ts index 54a03a00..7d441614 100644 --- a/apis/vercel/pages/api/ping.ts +++ b/apis/vercel/app/api/ping/route.ts @@ -1,32 +1,28 @@ -import type { NextRequest } from "next/server"; import { Ratelimit } from "@upstash/ratelimit"; -import { kv } from "@vercel/kv"; import { ipAddress } from "@vercel/functions"; +import { kv } from "@vercel/kv"; const ratelimit = new Ratelimit({ redis: kv, - // 5 requests from the same IP in 10 seconds limiter: Ratelimit.slidingWindow(1000, "10 s"), }); let i = 0; -export default async function handler(request: NextRequest) { - // Next 16 removed `request.ip`; ipAddress() from @vercel/functions - // reads the same X-Real-IP / X-Forwarded-For headers. - // You could alternatively limit based on user ID or similar. + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +export async function GET(request: Request) { const ip = ipAddress(request) ?? "127.0.0.1"; - /* - let start = Date.now(); - const { limit, reset, remaining } = await ratelimit.limit(ip); - let end = Date.now(); - console.log("Rate limit KV latency (ms):", end - start); - */ + void ip; + void ratelimit; + await kv.set("foo", `${i}`); i += 1; - let start = Date.now(); + const start = Date.now(); const foo = await kv.get("foo"); - let end = Date.now(); + const end = Date.now(); console.log("Get ", foo, " KV latency (ms):", end - start); return new Response(JSON.stringify({ success: true }), { diff --git a/apis/vercel/app/api/v1/[...slug]/route.ts b/apis/vercel/app/api/v1/[...slug]/route.ts new file mode 100644 index 00000000..d2b61273 --- /dev/null +++ b/apis/vercel/app/api/v1/[...slug]/route.ts @@ -0,0 +1,211 @@ +import dns from "node:dns"; + +import { after } from "next/server"; + +import { + type CacheSetOptions, + digestMessage, + encryptedGet, + encryptedPut, + getCorsHeaders, + makeFetchApiSecrets, +} from "@braintrust/proxy/edge"; +import { kv } from "@vercel/kv"; +import { Agent, setGlobalDispatcher } from "undici"; + +import { proxyV1ToAppRouteResponse } from "../../../../lib/appRouteProxy"; + +dns.setDefaultResultOrder("ipv4first"); + +setGlobalDispatcher( + new Agent({ + keepAliveTimeout: 1, + keepAliveMaxTimeout: 1, + }), +); + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +const KVCache = { + get: (key: string) => kv.get(key), + set: async (key: string, value: T, opts: CacheSetOptions) => { + await kv.set(key, value, opts.ttl !== undefined ? { ex: opts.ttl } : {}); + }, +}; + +function normalizeHeaders(headers: Headers): Record { + const normalized: Record = {}; + + headers.forEach((value, name) => { + normalized[name] = value; + }); + + return normalized; +} + +function handleOptions(request: Request, corsHeaders: Record) { + const accessControlRequestHeaders = request.headers.get( + "access-control-request-headers", + ); + + if ( + request.headers.get("origin") !== null && + request.headers.get("access-control-request-method") !== null && + accessControlRequestHeaders !== null + ) { + return new Response(null, { + status: 200, + headers: { + ...corsHeaders, + "access-control-allow-headers": accessControlRequestHeaders, + }, + }); + } + + return new Response(null, { + status: 200, + headers: { + Allow: "GET, HEAD, POST, OPTIONS", + }, + }); +} + +async function handleRequest(method: "GET" | "POST", request: Request) { + const requestId = + request.headers.get("x-request-id") ?? + Math.random().toString(36).slice(2, 10); + const start = Date.now(); + const requestUrl = new URL(request.url); + const log = (msg: string, extra?: Record) => { + console.log( + JSON.stringify({ + requestId, + method, + path: requestUrl.pathname, + elapsedMs: Date.now() - start, + msg, + ...extra, + }), + ); + }; + + log("route:start", { + hasAuth: request.headers.get("authorization") !== null, + braintrustApiUrl: process.env.BRAINTRUST_APP_URL, + }); + + const backgroundTasks = new Set>(); + const trackBackgroundTask = (promise: Promise) => { + backgroundTasks.add(promise); + void promise.finally(() => { + backgroundTasks.delete(promise); + }); + }; + + after(async () => { + const tasks = [...backgroundTasks]; + const results = await Promise.allSettled(tasks); + for (const result of results) { + if (result.status === "rejected") { + console.warn("Background task failed", result.reason); + } + } + }); + + let corsHeaders = {}; + try { + corsHeaders = getCorsHeaders(request, undefined); + } catch { + return new Response("Forbidden", { status: 403 }); + } + + const proxyHeaders = normalizeHeaders(request.headers); + const relativeURL = `${requestUrl.pathname.replace(/^\/api\/v1/, "")}${requestUrl.search}`; + const requestBody = method === "POST" ? await request.text() : ""; + const initialHeaders = { + ...corsHeaders, + "x-request-id": requestId, + }; + + const fetchApiSecrets = makeFetchApiSecrets({ + ctx: { + waitUntil(promise) { + trackBackgroundTask(promise); + }, + }, + opts: { + getRelativeURL() { + return relativeURL; + }, + credentialsCache: KVCache, + braintrustApiUrl: process.env.BRAINTRUST_APP_URL, + nativeInferenceSecretKey: process.env.NATIVE_INFERENCE_SECRET_KEY, + }, + }); + + try { + const { response, completed } = await proxyV1ToAppRouteResponse({ + method, + url: relativeURL, + proxyHeaders, + body: requestBody, + initialHeaders, + getApiSecrets: fetchApiSecrets, + cacheGet: async (encryptionKey, key) => { + return (await encryptedGet(KVCache, encryptionKey, key)) ?? null; + }, + cachePut: async (encryptionKey, key, value, ttlSeconds) => { + const putPromise = encryptedPut(KVCache, encryptionKey, key, value, { + ttl: ttlSeconds ?? 60 * 60 * 24 * 7, + }); + trackBackgroundTask(putPromise); + return putPromise; + }, + digest: digestMessage, + }); + + after(async () => { + try { + await completed; + log("route:stream-finished", { status: response.status }); + } catch (error) { + log("route:stream-error", { error: String(error) }); + } + }); + + log("route:after-handler", { status: response.status }); + return response; + } catch (error) { + log("route:error", { error: String(error) }); + return Response.json( + { + error: error instanceof Error ? error.message : String(error), + requestId, + }, + { + status: 500, + headers: initialHeaders, + }, + ); + } +} + +export async function OPTIONS(request: Request) { + let corsHeaders = {}; + try { + corsHeaders = getCorsHeaders(request, undefined); + } catch { + return new Response("Forbidden", { status: 403 }); + } + + return handleOptions(request, corsHeaders); +} + +export async function GET(request: Request) { + return handleRequest("GET", request); +} + +export async function POST(request: Request) { + return handleRequest("POST", request); +} diff --git a/apis/vercel/lib/appRouteProxy.test.ts b/apis/vercel/lib/appRouteProxy.test.ts new file mode 100644 index 00000000..c9e3ebf3 --- /dev/null +++ b/apis/vercel/lib/appRouteProxy.test.ts @@ -0,0 +1,164 @@ +import { describe, expect, it } from "vitest"; + +import { proxyV1ToAppRouteResponse } from "./appRouteProxy"; + +function createSettledTracker(promise: Promise) { + let settled = false; + promise.finally(() => { + settled = true; + }); + return () => settled; +} + +describe("proxyV1ToAppRouteResponse", () => { + it("reassembles split application/json chunks", async () => { + const { response, completed } = await proxyV1ToAppRouteResponse({ + method: "POST", + url: "/chat/completions", + proxyHeaders: {}, + body: "{}", + getApiSecrets: async () => [], + cacheGet: async () => null, + cachePut: async () => {}, + digest: async (message) => message, + proxyImpl: async ({ res, setHeader }) => { + setHeader("content-type", "application/json"); + const writer = res.getWriter(); + await writer.write(new TextEncoder().encode('{"ok":')); + await new Promise((resolve) => setTimeout(resolve, 20)); + await writer.write(new TextEncoder().encode('true,"parts":[1,2]}')); + await writer.close(); + }, + }); + + expect(response.headers.get("content-type")).toBe("application/json"); + expect(await response.json()).toEqual({ + ok: true, + parts: [1, 2], + }); + await expect(completed).resolves.toBeUndefined(); + }); + + it("streams split SSE frames for stream=true style responses", async () => { + const { response, completed } = await proxyV1ToAppRouteResponse({ + method: "POST", + url: "/chat/completions", + proxyHeaders: {}, + body: '{"stream":true}', + getApiSecrets: async () => [], + cacheGet: async () => null, + cachePut: async () => {}, + digest: async (message) => message, + proxyImpl: async ({ res, setHeader }) => { + setHeader("content-type", "text/event-stream"); + const writer = res.getWriter(); + await writer.write(new TextEncoder().encode('data: {"id":"chunk-1"')); + await new Promise((resolve) => setTimeout(resolve, 20)); + await writer.write(new TextEncoder().encode("}\n\n")); + await new Promise((resolve) => setTimeout(resolve, 20)); + await writer.write(new TextEncoder().encode("data: [DONE]\n\n")); + await writer.close(); + }, + }); + + expect(response.headers.get("content-type")).toBe("text/event-stream"); + expect(await response.text()).toBe( + 'data: {"id":"chunk-1"}\n\ndata: [DONE]\n\n', + ); + await expect(completed).resolves.toBeUndefined(); + }); + + it("returns the app route response before a split JSON stream finishes", async () => { + let releaseSecondChunk: () => void = () => {}; + const secondChunkReleased = new Promise((resolve) => { + releaseSecondChunk = resolve; + }); + + const result = await proxyV1ToAppRouteResponse({ + method: "POST", + url: "/chat/completions", + proxyHeaders: {}, + body: "{}", + getApiSecrets: async () => [], + cacheGet: async () => null, + cachePut: async () => {}, + digest: async (message) => message, + proxyImpl: async ({ res, setHeader }) => { + setHeader("content-type", "application/json"); + const writer = res.getWriter(); + await writer.write(new TextEncoder().encode('{"ok":')); + await secondChunkReleased; + await writer.write(new TextEncoder().encode("true}")); + await writer.close(); + }, + }); + + const isCompletedSettled = createSettledTracker(result.completed); + expect(isCompletedSettled()).toBe(false); + + const bodyPromise = result.response.text(); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(isCompletedSettled()).toBe(false); + + releaseSecondChunk(); + expect(await bodyPromise).toBe('{"ok":true}'); + await expect(result.completed).resolves.toBeUndefined(); + }); + + it("returns the app route response before a split SSE stream finishes", async () => { + let releaseDoneFrame: () => void = () => {}; + const doneFrameReleased = new Promise((resolve) => { + releaseDoneFrame = resolve; + }); + + const result = await proxyV1ToAppRouteResponse({ + method: "POST", + url: "/chat/completions", + proxyHeaders: {}, + body: '{"stream":true}', + getApiSecrets: async () => [], + cacheGet: async () => null, + cachePut: async () => {}, + digest: async (message) => message, + proxyImpl: async ({ res, setHeader }) => { + setHeader("content-type", "text/event-stream"); + const writer = res.getWriter(); + await writer.write( + new TextEncoder().encode('data: {"id":"chunk-1"}\n\n'), + ); + await doneFrameReleased; + await writer.write(new TextEncoder().encode("data: [DONE]\n\n")); + await writer.close(); + }, + }); + + const isCompletedSettled = createSettledTracker(result.completed); + expect(result.response.headers.get("content-type")).toBe( + "text/event-stream", + ); + expect(isCompletedSettled()).toBe(false); + + const reader = result.response.body?.getReader(); + if (!reader) { + throw new Error("Expected response body reader"); + } + + const firstChunk = await reader.read(); + expect(new TextDecoder().decode(firstChunk.value)).toBe( + 'data: {"id":"chunk-1"}\n\n', + ); + expect(firstChunk.done).toBe(false); + expect(isCompletedSettled()).toBe(false); + + releaseDoneFrame(); + const secondChunk = await reader.read(); + expect(new TextDecoder().decode(secondChunk.value)).toBe( + "data: [DONE]\n\n", + ); + expect(secondChunk.done).toBe(false); + + const end = await reader.read(); + expect(end.done).toBe(true); + await expect(result.completed).resolves.toBeUndefined(); + }); +}); diff --git a/apis/vercel/lib/appRouteProxy.ts b/apis/vercel/lib/appRouteProxy.ts new file mode 100644 index 00000000..5d3e59a1 --- /dev/null +++ b/apis/vercel/lib/appRouteProxy.ts @@ -0,0 +1,137 @@ +import { proxyV1 } from "@braintrust/proxy"; + +type ProxyV1Args = Parameters[0]; +type GetApiSecrets = ProxyV1Args["getApiSecrets"]; + +function normalizeError(reason: unknown): Error { + if (reason instanceof Error) { + return reason; + } + + return new Error(String(reason)); +} + +export async function proxyV1ToAppRouteResponse({ + method, + url, + proxyHeaders, + body, + initialHeaders, + getApiSecrets, + cacheGet, + cachePut, + digest, + proxyImpl = proxyV1, +}: { + method: "GET" | "POST"; + url: string; + proxyHeaders: Record; + body: string; + initialHeaders?: HeadersInit; + getApiSecrets: GetApiSecrets; + cacheGet: (encryptionKey: string, key: string) => Promise; + cachePut: ( + encryptionKey: string, + key: string, + value: string, + ttlSeconds?: number, + ) => Promise; + digest: (message: string) => Promise; + proxyImpl?: typeof proxyV1; +}): Promise<{ + response: Response; + completed: Promise; +}> { + const headers = new Headers(initialHeaders); + let status = 200; + let proxyError: unknown = undefined; + + let resolveReady: () => void = () => {}; + const ready = new Promise((resolve) => { + resolveReady = resolve; + }); + + let resolveCompleted: () => void = () => {}; + let rejectCompleted: (error: Error) => void = () => {}; + const completed = new Promise((resolve, reject) => { + resolveCompleted = resolve; + rejectCompleted = (error) => reject(error); + }); + + let readyScheduled = false; + const scheduleReady = () => { + if (readyScheduled) { + return; + } + + readyScheduled = true; + queueMicrotask(resolveReady); + }; + + const abortController = new AbortController(); + + const readable = new ReadableStream({ + start(controller) { + const writable = new WritableStream({ + write(chunk) { + scheduleReady(); + controller.enqueue(chunk); + }, + close() { + scheduleReady(); + controller.close(); + resolveCompleted(); + }, + abort(reason) { + scheduleReady(); + const error = normalizeError(reason); + controller.error(error); + rejectCompleted(error); + }, + }); + + void proxyImpl({ + method, + url, + proxyHeaders, + body, + setHeader(name, value) { + headers.set(name, value); + }, + setStatusCode(code) { + status = code; + }, + res: writable, + getApiSecrets, + cacheGet, + cachePut, + digest, + decompressFetch: true, + signal: abortController.signal, + }).catch((error) => { + proxyError = error; + scheduleReady(); + const normalizedError = normalizeError(error); + controller.error(normalizedError); + rejectCompleted(normalizedError); + }); + }, + cancel(reason) { + abortController.abort(normalizeError(reason)); + }, + }); + + await ready; + + if (proxyError !== undefined) { + throw proxyError; + } + + return { + response: new Response(readable, { + status, + headers, + }), + completed, + }; +} diff --git a/apis/vercel/lib/nodeProxy.test.ts b/apis/vercel/lib/nodeProxy.test.ts deleted file mode 100644 index b9b3d5c7..00000000 --- a/apis/vercel/lib/nodeProxy.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { PassThrough } from "node:stream"; - -import { describe, expect, it } from "vitest"; - -import { proxyV1ToNodeResponse } from "./nodeProxy"; - -describe("proxyV1ToNodeResponse", () => { - it("reassembles split application/json chunks", async () => { - const headers = new Map(); - let statusCode = 200; - const res = new PassThrough(); - const bodyChunks: Buffer[] = []; - - res.on("data", (chunk: Buffer) => { - bodyChunks.push(chunk); - }); - - await proxyV1ToNodeResponse({ - method: "POST", - url: "/chat/completions", - proxyHeaders: {}, - body: "{}", - setHeader(name, value) { - headers.set(name, value); - }, - setStatusCode(code) { - statusCode = code; - }, - getApiSecrets: async () => [], - cacheGet: async () => null, - cachePut: async () => {}, - digest: async (message) => message, - getRes: () => res, - proxyImpl: async ({ res, setHeader }) => { - setHeader("content-type", "application/json"); - const writer = res.getWriter(); - await writer.write(new TextEncoder().encode('{"ok":')); - await new Promise((resolve) => setTimeout(resolve, 20)); - await writer.write(new TextEncoder().encode('true,"parts":[1,2]}')); - await writer.close(); - }, - }); - - expect(statusCode).toBe(200); - expect(headers.get("content-type")).toBe("application/json"); - expect(JSON.parse(Buffer.concat(bodyChunks).toString("utf8"))).toEqual({ - ok: true, - parts: [1, 2], - }); - }); - - it("streams split SSE frames for stream=true style responses", async () => { - const headers = new Map(); - const res = new PassThrough(); - const bodyChunks: Buffer[] = []; - - res.on("data", (chunk: Buffer) => { - bodyChunks.push(chunk); - }); - - await proxyV1ToNodeResponse({ - method: "POST", - url: "/chat/completions", - proxyHeaders: {}, - body: '{"stream":true}', - setHeader(name, value) { - headers.set(name, value); - }, - setStatusCode() {}, - getApiSecrets: async () => [], - cacheGet: async () => null, - cachePut: async () => {}, - digest: async (message) => message, - getRes: () => res, - proxyImpl: async ({ res, setHeader }) => { - setHeader("content-type", "text/event-stream"); - const writer = res.getWriter(); - await writer.write(new TextEncoder().encode('data: {"id":"chunk-1"')); - await new Promise((resolve) => setTimeout(resolve, 20)); - await writer.write(new TextEncoder().encode("}\n\n")); - await new Promise((resolve) => setTimeout(resolve, 20)); - await writer.write(new TextEncoder().encode("data: [DONE]\n\n")); - await writer.close(); - }, - }); - - expect(headers.get("content-type")).toBe("text/event-stream"); - expect(Buffer.concat(bodyChunks).toString("utf8")).toBe( - 'data: {"id":"chunk-1"}\n\ndata: [DONE]\n\n', - ); - }); -}); diff --git a/apis/vercel/lib/nodeProxy.ts b/apis/vercel/lib/nodeProxy.ts deleted file mode 100644 index 4fdea40d..00000000 --- a/apis/vercel/lib/nodeProxy.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Readable, type Writable } from "node:stream"; -import { pipeline } from "node:stream/promises"; - -import { proxyV1 } from "@braintrust/proxy"; - -type GetApiSecrets = ( - useCache: boolean, - authToken: string, - model: string | null, - orgName?: string, - projectId?: string, -) => Promise< - { - secret: string; - type: string; - org_name?: string | null; - metadata?: Record | null; - }[] ->; - -export async function readRawRequestBody( - req: NodeJS.ReadableStream, -): Promise { - const chunks: Buffer[] = []; - - for await (const chunk of req) { - chunks.push( - typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk), - ); - } - - return Buffer.concat(chunks).toString("utf8"); -} - -export async function proxyV1ToNodeResponse({ - method, - url, - proxyHeaders, - body, - setHeader, - setStatusCode, - getApiSecrets, - cacheGet, - cachePut, - digest, - getRes, - proxyImpl = proxyV1, -}: { - method: "GET" | "POST"; - url: string; - proxyHeaders: Record; - body: string; - setHeader: (name: string, value: string) => void; - setStatusCode: (code: number) => void; - getApiSecrets: GetApiSecrets; - cacheGet: (encryptionKey: string, key: string) => Promise; - cachePut: ( - encryptionKey: string, - key: string, - value: string, - ttlSeconds?: number, - ) => Promise; - digest: (message: string) => Promise; - getRes: () => Writable; - proxyImpl?: typeof proxyV1; -}) { - const { readable, writable } = new TransformStream(); - const nodeReadable = Readable.fromWeb(readable); - const res = getRes(); - - const proxyPromise = proxyImpl({ - method, - url, - proxyHeaders, - body, - setHeader, - setStatusCode, - res: writable, - getApiSecrets, - cacheGet, - cachePut, - digest, - }).catch(async (error) => { - await writable.abort(error).catch(() => {}); - throw error; - }); - - await Promise.all([proxyPromise, pipeline(nodeReadable, res)]); -} diff --git a/apis/vercel/next-env.d.ts b/apis/vercel/next-env.d.ts index 2d5420eb..0c7fad71 100644 --- a/apis/vercel/next-env.d.ts +++ b/apis/vercel/next-env.d.ts @@ -1,7 +1,7 @@ /// /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apis/vercel/pages/api/v1/[...slug].ts b/apis/vercel/pages/api/v1/[...slug].ts deleted file mode 100644 index fd392545..00000000 --- a/apis/vercel/pages/api/v1/[...slug].ts +++ /dev/null @@ -1,211 +0,0 @@ -import dns from "node:dns"; -import type { NextApiRequest, NextApiResponse } from "next"; - -import { - type CacheSetOptions, - digestMessage, - encryptedGet, - encryptedPut, - getCorsHeaders, - makeFetchApiSecrets, -} from "@braintrust/proxy/edge"; -import { kv } from "@vercel/kv"; -import { Agent, setGlobalDispatcher } from "undici"; - -import { - proxyV1ToNodeResponse, - readRawRequestBody, -} from "../../../lib/nodeProxy"; - -dns.setDefaultResultOrder("ipv4first"); - -setGlobalDispatcher( - new Agent({ - keepAliveTimeout: 1, - keepAliveMaxTimeout: 1, - }), -); - -const KVCache = { - get: (key: string) => kv.get(key), - set: async (key: string, value: T, opts: CacheSetOptions) => { - await kv.set(key, value, opts.ttl !== undefined ? { ex: opts.ttl } : {}); - }, -}; - -function normalizeHeaders( - headers: NextApiRequest["headers"], -): Record { - const normalized: Record = {}; - - for (const [name, value] of Object.entries(headers)) { - if (value === undefined) { - continue; - } - - normalized[name] = Array.isArray(value) ? value.join(",") : value; - } - - return normalized; -} - -function handleOptions( - req: NextApiRequest, - res: NextApiResponse, - corsHeaders: Record, -) { - const accessControlRequestHeaders = normalizeHeaders(req.headers)[ - "access-control-request-headers" - ]; - - if ( - req.headers.origin !== undefined && - req.headers["access-control-request-method"] !== undefined && - accessControlRequestHeaders !== undefined - ) { - res.writeHead(200, { - ...corsHeaders, - "access-control-allow-headers": accessControlRequestHeaders, - }); - res.end(); - return; - } - - res.writeHead(200, { - Allow: "GET, HEAD, POST, OPTIONS", - }); - res.end(); -} - -export const config = { - api: { - bodyParser: false, - externalResolver: true, - }, -}; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - const requestId = - (typeof req.headers["x-request-id"] === "string" - ? req.headers["x-request-id"] - : undefined) ?? Math.random().toString(36).slice(2, 10); - const start = Date.now(); - const requestUrl = new URL( - req.url ?? "/api/v1", - `https://${req.headers.host ?? "localhost"}`, - ); - const log = (msg: string, extra?: Record) => { - console.log( - JSON.stringify({ - requestId, - method: req.method, - path: requestUrl.pathname, - elapsedMs: Date.now() - start, - msg, - ...extra, - }), - ); - }; - - log("route:start", { - hasAuth: req.headers.authorization !== undefined, - braintrustApiUrl: process.env.BRAINTRUST_APP_URL, - }); - - let corsHeaders = {}; - try { - corsHeaders = getCorsHeaders( - new Request(requestUrl.toString(), { - headers: normalizeHeaders(req.headers), - method: req.method, - }), - undefined, - ); - } catch { - res.status(403).send("Forbidden"); - return; - } - - if (req.method === "OPTIONS") { - handleOptions(req, res, corsHeaders); - return; - } - - if (req.method !== "GET" && req.method !== "POST") { - res.status(405).setHeader("Content-Type", "text/plain"); - res.send("Method not allowed"); - return; - } - - const method: "GET" | "POST" = req.method; - const proxyHeaders = normalizeHeaders(req.headers); - const relativeURL = `${requestUrl.pathname.replace(/^\/api\/v1/, "")}${requestUrl.search}`; - const requestBody = method === "POST" ? await readRawRequestBody(req) : ""; - - for (const [name, value] of Object.entries(corsHeaders)) { - res.setHeader(name, value); - } - res.setHeader("x-request-id", requestId); - - const fetchApiSecrets = makeFetchApiSecrets({ - ctx: { - waitUntil(promise) { - void promise.catch((error) => { - console.warn("Background task failed", error); - }); - }, - }, - opts: { - getRelativeURL() { - return relativeURL; - }, - credentialsCache: KVCache, - braintrustApiUrl: process.env.BRAINTRUST_APP_URL, - nativeInferenceSecretKey: process.env.NATIVE_INFERENCE_SECRET_KEY, - }, - }); - - try { - await proxyV1ToNodeResponse({ - method, - url: relativeURL, - proxyHeaders, - body: requestBody, - setHeader(name, value) { - res.setHeader(name, value); - }, - setStatusCode(code) { - res.statusCode = code; - }, - getApiSecrets: fetchApiSecrets, - cacheGet: async (encryptionKey, key) => { - return (await encryptedGet(KVCache, encryptionKey, key)) ?? null; - }, - cachePut: async (encryptionKey, key, value, ttlSeconds) => { - await encryptedPut(KVCache, encryptionKey, key, value, { - ttl: ttlSeconds ?? 60 * 60 * 24 * 7, - }); - }, - digest: digestMessage, - getRes: () => res, - }); - log("route:after-handler", { status: res.statusCode }); - } catch (error) { - log("route:error", { error: String(error) }); - if (!res.headersSent) { - res - .status(500) - .setHeader("Content-Type", "application/json") - .json({ - error: error instanceof Error ? error.message : String(error), - requestId, - }); - return; - } - - res.end(); - } -} From 239eb06c9b1a7a83e6afd4f80803e3efe15793a4 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 20:16:01 -0700 Subject: [PATCH 20/22] cleanup code after working function --- apis/vercel/app/api/ping/route.ts | 18 +- apis/vercel/app/api/v1/[...slug]/route.ts | 84 +++------ apis/vercel/lib/appRouteProxy.test.ts | 26 +-- apis/vercel/package.json | 2 - packages/proxy/edge/index.metrics.test.ts | 51 ------ packages/proxy/edge/index.test.ts | 113 +----------- packages/proxy/edge/index.ts | 199 +++------------------- packages/proxy/src/proxy.stream.test.ts | 43 ----- packages/proxy/src/proxy.ts | 34 ++-- pnpm-lock.yaml | 54 ------ 10 files changed, 83 insertions(+), 541 deletions(-) delete mode 100644 packages/proxy/src/proxy.stream.test.ts diff --git a/apis/vercel/app/api/ping/route.ts b/apis/vercel/app/api/ping/route.ts index 7d441614..38515431 100644 --- a/apis/vercel/app/api/ping/route.ts +++ b/apis/vercel/app/api/ping/route.ts @@ -1,29 +1,15 @@ -import { Ratelimit } from "@upstash/ratelimit"; -import { ipAddress } from "@vercel/functions"; import { kv } from "@vercel/kv"; -const ratelimit = new Ratelimit({ - redis: kv, - limiter: Ratelimit.slidingWindow(1000, "10 s"), -}); - let i = 0; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; -export async function GET(request: Request) { - const ip = ipAddress(request) ?? "127.0.0.1"; - void ip; - void ratelimit; - +export async function GET() { await kv.set("foo", `${i}`); i += 1; - const start = Date.now(); - const foo = await kv.get("foo"); - const end = Date.now(); - console.log("Get ", foo, " KV latency (ms):", end - start); + await kv.get("foo"); return new Response(JSON.stringify({ success: true }), { status: 200, diff --git a/apis/vercel/app/api/v1/[...slug]/route.ts b/apis/vercel/app/api/v1/[...slug]/route.ts index d2b61273..5a6a3998 100644 --- a/apis/vercel/app/api/v1/[...slug]/route.ts +++ b/apis/vercel/app/api/v1/[...slug]/route.ts @@ -15,12 +15,22 @@ import { Agent, setGlobalDispatcher } from "undici"; import { proxyV1ToAppRouteResponse } from "../../../../lib/appRouteProxy"; -dns.setDefaultResultOrder("ipv4first"); +const DNS_RESULT_ORDER = "ipv4first"; +const REQUEST_ID_HEADER = "x-request-id"; +const ACCESS_CONTROL_REQUEST_HEADERS_HEADER = "access-control-request-headers"; +const ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "access-control-allow-headers"; +const ALLOW_METHODS_HEADER_VALUE = "GET, HEAD, POST, OPTIONS"; +const FORBIDDEN_RESPONSE_TEXT = "Forbidden"; +const API_V1_PATH_PREFIX = /^\/api\/v1/; +const DEFAULT_CACHE_TTL_SECONDS = 60 * 60 * 24 * 7; +const KEEP_ALIVE_TIMEOUT_MS = 1; + +dns.setDefaultResultOrder(DNS_RESULT_ORDER); setGlobalDispatcher( new Agent({ - keepAliveTimeout: 1, - keepAliveMaxTimeout: 1, + keepAliveTimeout: KEEP_ALIVE_TIMEOUT_MS, + keepAliveMaxTimeout: KEEP_ALIVE_TIMEOUT_MS, }), ); @@ -46,7 +56,7 @@ function normalizeHeaders(headers: Headers): Record { function handleOptions(request: Request, corsHeaders: Record) { const accessControlRequestHeaders = request.headers.get( - "access-control-request-headers", + ACCESS_CONTROL_REQUEST_HEADERS_HEADER, ); if ( @@ -58,7 +68,7 @@ function handleOptions(request: Request, corsHeaders: Record) { status: 200, headers: { ...corsHeaders, - "access-control-allow-headers": accessControlRequestHeaders, + [ACCESS_CONTROL_ALLOW_HEADERS_HEADER]: accessControlRequestHeaders, }, }); } @@ -66,66 +76,38 @@ function handleOptions(request: Request, corsHeaders: Record) { return new Response(null, { status: 200, headers: { - Allow: "GET, HEAD, POST, OPTIONS", + Allow: ALLOW_METHODS_HEADER_VALUE, }, }); } async function handleRequest(method: "GET" | "POST", request: Request) { const requestId = - request.headers.get("x-request-id") ?? - Math.random().toString(36).slice(2, 10); - const start = Date.now(); + request.headers.get(REQUEST_ID_HEADER) ?? crypto.randomUUID(); const requestUrl = new URL(request.url); - const log = (msg: string, extra?: Record) => { - console.log( - JSON.stringify({ - requestId, - method, - path: requestUrl.pathname, - elapsedMs: Date.now() - start, - msg, - ...extra, - }), - ); - }; - - log("route:start", { - hasAuth: request.headers.get("authorization") !== null, - braintrustApiUrl: process.env.BRAINTRUST_APP_URL, - }); - const backgroundTasks = new Set>(); + const backgroundTasks: Promise[] = []; const trackBackgroundTask = (promise: Promise) => { - backgroundTasks.add(promise); - void promise.finally(() => { - backgroundTasks.delete(promise); - }); + backgroundTasks.push(promise); }; after(async () => { - const tasks = [...backgroundTasks]; - const results = await Promise.allSettled(tasks); - for (const result of results) { - if (result.status === "rejected") { - console.warn("Background task failed", result.reason); - } - } + await Promise.allSettled(backgroundTasks); }); let corsHeaders = {}; try { corsHeaders = getCorsHeaders(request, undefined); } catch { - return new Response("Forbidden", { status: 403 }); + return new Response(FORBIDDEN_RESPONSE_TEXT, { status: 403 }); } const proxyHeaders = normalizeHeaders(request.headers); - const relativeURL = `${requestUrl.pathname.replace(/^\/api\/v1/, "")}${requestUrl.search}`; + const relativeURL = `${requestUrl.pathname.replace(API_V1_PATH_PREFIX, "")}${requestUrl.search}`; const requestBody = method === "POST" ? await request.text() : ""; const initialHeaders = { ...corsHeaders, - "x-request-id": requestId, + [REQUEST_ID_HEADER]: requestId, }; const fetchApiSecrets = makeFetchApiSecrets({ @@ -145,7 +127,7 @@ async function handleRequest(method: "GET" | "POST", request: Request) { }); try { - const { response, completed } = await proxyV1ToAppRouteResponse({ + const proxyResult = await proxyV1ToAppRouteResponse({ method, url: relativeURL, proxyHeaders, @@ -157,27 +139,15 @@ async function handleRequest(method: "GET" | "POST", request: Request) { }, cachePut: async (encryptionKey, key, value, ttlSeconds) => { const putPromise = encryptedPut(KVCache, encryptionKey, key, value, { - ttl: ttlSeconds ?? 60 * 60 * 24 * 7, + ttl: ttlSeconds ?? DEFAULT_CACHE_TTL_SECONDS, }); trackBackgroundTask(putPromise); return putPromise; }, digest: digestMessage, }); - - after(async () => { - try { - await completed; - log("route:stream-finished", { status: response.status }); - } catch (error) { - log("route:stream-error", { error: String(error) }); - } - }); - - log("route:after-handler", { status: response.status }); - return response; + return proxyResult.response; } catch (error) { - log("route:error", { error: String(error) }); return Response.json( { error: error instanceof Error ? error.message : String(error), @@ -196,7 +166,7 @@ export async function OPTIONS(request: Request) { try { corsHeaders = getCorsHeaders(request, undefined); } catch { - return new Response("Forbidden", { status: 403 }); + return new Response(FORBIDDEN_RESPONSE_TEXT, { status: 403 }); } return handleOptions(request, corsHeaders); diff --git a/apis/vercel/lib/appRouteProxy.test.ts b/apis/vercel/lib/appRouteProxy.test.ts index c9e3ebf3..d6266aab 100644 --- a/apis/vercel/lib/appRouteProxy.test.ts +++ b/apis/vercel/lib/appRouteProxy.test.ts @@ -10,6 +10,14 @@ function createSettledTracker(promise: Promise) { return () => settled; } +function createDeferred() { + let resolve: () => void = () => {}; + const promise = new Promise((resolver) => { + resolve = resolver; + }); + return { promise, resolve }; +} + describe("proxyV1ToAppRouteResponse", () => { it("reassembles split application/json chunks", async () => { const { response, completed } = await proxyV1ToAppRouteResponse({ @@ -69,10 +77,7 @@ describe("proxyV1ToAppRouteResponse", () => { }); it("returns the app route response before a split JSON stream finishes", async () => { - let releaseSecondChunk: () => void = () => {}; - const secondChunkReleased = new Promise((resolve) => { - releaseSecondChunk = resolve; - }); + const secondChunk = createDeferred(); const result = await proxyV1ToAppRouteResponse({ method: "POST", @@ -87,7 +92,7 @@ describe("proxyV1ToAppRouteResponse", () => { setHeader("content-type", "application/json"); const writer = res.getWriter(); await writer.write(new TextEncoder().encode('{"ok":')); - await secondChunkReleased; + await secondChunk.promise; await writer.write(new TextEncoder().encode("true}")); await writer.close(); }, @@ -100,16 +105,13 @@ describe("proxyV1ToAppRouteResponse", () => { await new Promise((resolve) => setTimeout(resolve, 10)); expect(isCompletedSettled()).toBe(false); - releaseSecondChunk(); + secondChunk.resolve(); expect(await bodyPromise).toBe('{"ok":true}'); await expect(result.completed).resolves.toBeUndefined(); }); it("returns the app route response before a split SSE stream finishes", async () => { - let releaseDoneFrame: () => void = () => {}; - const doneFrameReleased = new Promise((resolve) => { - releaseDoneFrame = resolve; - }); + const doneFrame = createDeferred(); const result = await proxyV1ToAppRouteResponse({ method: "POST", @@ -126,7 +128,7 @@ describe("proxyV1ToAppRouteResponse", () => { await writer.write( new TextEncoder().encode('data: {"id":"chunk-1"}\n\n'), ); - await doneFrameReleased; + await doneFrame.promise; await writer.write(new TextEncoder().encode("data: [DONE]\n\n")); await writer.close(); }, @@ -150,7 +152,7 @@ describe("proxyV1ToAppRouteResponse", () => { expect(firstChunk.done).toBe(false); expect(isCompletedSettled()).toBe(false); - releaseDoneFrame(); + doneFrame.resolve(); const secondChunk = await reader.read(); expect(new TextDecoder().decode(secondChunk.value)).toBe( "data: [DONE]\n\n", diff --git a/apis/vercel/package.json b/apis/vercel/package.json index d7e3ec21..1b25cde7 100644 --- a/apis/vercel/package.json +++ b/apis/vercel/package.json @@ -11,9 +11,7 @@ }, "dependencies": { "@braintrust/proxy": "workspace:*", - "@upstash/ratelimit": "^0.4.3", "@vercel/examples-ui": "^1.0.5", - "@vercel/functions": "^3.4.2", "@vercel/kv": "^0.2.2", "next": "^16.2.6", "react": "19.2.6", diff --git a/packages/proxy/edge/index.metrics.test.ts b/packages/proxy/edge/index.metrics.test.ts index ca2fd025..82f87176 100644 --- a/packages/proxy/edge/index.metrics.test.ts +++ b/packages/proxy/edge/index.metrics.test.ts @@ -119,55 +119,4 @@ describe("EdgeProxyV1 metric flushing", () => { await expect(response.json()).resolves.toEqual({ ok: true }); expect(waitUntilPromises).toHaveLength(0); }); - - it("streams text/event-stream when streamingViaWaitUntil is enabled", async () => { - const { EdgeProxyV1 } = await import("./index"); - - proxyV1Mock.mockImplementation( - async ({ - res, - setHeader, - }: { - res: WritableStream; - setHeader: (name: string, value: string) => void; - }) => { - setHeader("content-type", "text/event-stream"); - const writer = res.getWriter(); - await writer.write(new TextEncoder().encode("data: first\n\n")); - await new Promise((resolve) => setTimeout(resolve, 20)); - await writer.write(new TextEncoder().encode("data: second\n\n")); - await writer.close(); - }, - ); - - const waitUntilPromises: Promise[] = []; - const handler = EdgeProxyV1({ - getRelativeURL() { - return "/chat/completions"; - }, - streamingViaWaitUntil: true, - }); - - const response = await handler( - new Request("https://example.com/v1/chat/completions", { - method: "POST", - body: JSON.stringify({ stream: true }), - headers: { - Authorization: "Bearer test-token", - }, - }), - { - waitUntil(promise) { - waitUntilPromises.push(promise); - }, - }, - ); - - expect(response.headers.get("content-type")).toBe("text/event-stream"); - await expect(response.text()).resolves.toBe( - "data: first\n\ndata: second\n\n", - ); - expect(waitUntilPromises).toHaveLength(1); - await expect(Promise.all(waitUntilPromises)).resolves.toBeDefined(); - }); }); diff --git a/packages/proxy/edge/index.test.ts b/packages/proxy/edge/index.test.ts index a4d557df..96688e7e 100644 --- a/packages/proxy/edge/index.test.ts +++ b/packages/proxy/edge/index.test.ts @@ -1,10 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { - makeFetchApiSecrets, - streamingResponseViaWaitUntil, - type EdgeContext, - type ProxyOpts, -} from "./index"; +import { makeFetchApiSecrets, type EdgeContext, type ProxyOpts } from "./index"; function createInMemoryCache() { const store = new Map(); @@ -189,109 +184,3 @@ describe("makeFetchApiSecrets", () => { expect(fetchMock).toHaveBeenCalledTimes(1); }); }); - -describe("streamingResponseViaWaitUntil", () => { - function setup() { - const waitUntilPromises: Promise[] = []; - const ctx: EdgeContext = { - waitUntil(promise) { - waitUntilPromises.push(promise); - }, - }; - const wrapWithMeter = (b: ReadableStream) => b; - return { waitUntilPromises, ctx, wrapWithMeter }; - } - - it("waits for the first byte before returning Response", async () => { - const { ctx, wrapWithMeter } = setup(); - const encoder = new TextEncoder(); - - let returnedResponse = false; - let firstWriteAt = -1; - let responseAt = -1; - let tick = 0; - - const runProxy = async (res: WritableStream) => { - (async () => { - await new Promise((r) => setTimeout(r, 20)); - const writer = res.getWriter(); - firstWriteAt = ++tick; - await writer.write(encoder.encode("hello ")); - await writer.write(encoder.encode("world")); - await writer.close(); - })(); - }; - - const responsePromise = streamingResponseViaWaitUntil({ - ctx, - wrapWithMeter, - runProxy, - getStatus: () => 200, - getHeaders: () => ({ "content-type": "text/plain" }), - }).then((r) => { - responseAt = ++tick; - returnedResponse = true; - return r; - }); - - await new Promise((r) => setTimeout(r, 0)); - expect(returnedResponse).toBe(false); - - const response = await responsePromise; - expect(response.status).toBe(200); - expect(firstWriteAt).toBeGreaterThan(0); - expect(responseAt).toBeGreaterThan(firstWriteAt); - - const text = await response.text(); - expect(text).toBe("hello world"); - }); - - it("returns a 400 response when runProxy throws before writing", async () => { - const { ctx, wrapWithMeter } = setup(); - - const runProxy = async () => { - throw new Error("boom"); - }; - - const response = await streamingResponseViaWaitUntil({ - ctx, - wrapWithMeter, - runProxy, - getStatus: () => 200, - getHeaders: () => ({}), - }); - - expect(response.status).toBe(400); - expect(await response.text()).toContain("boom"); - }); - - it("registers a waitUntil promise that drains the pipe", async () => { - const { waitUntilPromises, ctx, wrapWithMeter } = setup(); - const encoder = new TextEncoder(); - - const runProxy = async (res: WritableStream) => { - (async () => { - const writer = res.getWriter(); - await writer.write(encoder.encode("first")); - await new Promise((r) => setTimeout(r, 30)); - await writer.write(encoder.encode("-second")); - await writer.close(); - })(); - }; - - const response = await streamingResponseViaWaitUntil({ - ctx, - wrapWithMeter, - runProxy, - getStatus: () => 200, - getHeaders: () => ({}), - }); - - expect(waitUntilPromises.length).toBe(1); - - const text = await response.text(); - expect(text).toBe("first-second"); - - await expect(Promise.all(waitUntilPromises)).resolves.toBeDefined(); - }); -}); diff --git a/packages/proxy/edge/index.ts b/packages/proxy/edge/index.ts index 501ba09c..0a7f5e83 100644 --- a/packages/proxy/edge/index.ts +++ b/packages/proxy/edge/index.ts @@ -41,15 +41,6 @@ export interface ProxyOpts { spanId?: string; spanExport?: string; nativeInferenceSecretKey?: string; - /** - * When true, the upstream → response body runs in the background - * and the function is kept alive via `ctx.waitUntil`. - * - * Leave unset on Cloudflare Workers as those already keep the event stream alive - * - * @default false - */ - streamingViaWaitUntil?: boolean; } const defaultWhitelist: (string | RegExp)[] = [ @@ -288,8 +279,7 @@ export function makeFetchApiSecrets({ }); } - // temp skip the cache for testing - if (useCache && opts.credentialsCache && !lookupFailed) { + if (opts.credentialsCache && !lookupFailed) { ctx.waitUntil( encryptedPut( opts.credentialsCache, @@ -309,121 +299,6 @@ export function makeFetchApiSecrets({ // Metric logging functions are now created in the calling layer (e.g., Cloudflare proxy) -// readable highWaterMark is generous so the producer can prime the stream -// before any reader is attached — Response(readable) is only constructed -// after the first write, so without buffering `baseWriter.write` would -// deadlock waiting for a consumer. -export async function streamingResponseViaWaitUntil({ - ctx, - wrapWithMeter, - runProxy, - getStatus, - getHeaders, -}: { - ctx: EdgeContext; - wrapWithMeter: ( - body: ReadableStream, - ) => ReadableStream; - runProxy: (res: WritableStream) => Promise; - getStatus: () => number; - getHeaders: () => Record; -}): Promise { - const { readable, writable } = new TransformStream( - {}, - { highWaterMark: 1 }, - { highWaterMark: 64 }, - ); - - let signalReady: () => void = () => {}; - const headersReady = new Promise((resolve) => { - signalReady = resolve; - }); - - let signalPipeComplete: () => void = () => {}; - const pipeComplete = new Promise((resolve) => { - signalPipeComplete = resolve; - }); - - let writeCount = 0; - let totalBytes = 0; - let closed = false; - let aborted = false; - - const baseWriter = writable.getWriter(); - const wrappedWritable = new WritableStream({ - async write(chunk) { - writeCount += 1; - totalBytes += chunk.byteLength; - await baseWriter.write(chunk); - console.log( - `streamingResponseViaWaitUntil write #${writeCount} bytes=${chunk.byteLength} total=${totalBytes}`, - ); - signalReady(); - }, - async close() { - closed = true; - console.log( - `streamingResponseViaWaitUntil close writes=${writeCount} total=${totalBytes}`, - ); - await baseWriter.close(); - signalReady(); - signalPipeComplete(); - }, - async abort(reason) { - aborted = true; - console.error( - `streamingResponseViaWaitUntil abort writes=${writeCount} total=${totalBytes} reason=${reason}`, - ); - await baseWriter.abort(reason); - signalReady(); - signalPipeComplete(); - }, - }); - - let proxyError: unknown = undefined; - const proxyPromise = runProxy(wrappedWritable) - .then(() => { - console.log( - `streamingResponseViaWaitUntil runProxy resolved writes=${writeCount} total=${totalBytes} closed=${closed} aborted=${aborted}`, - ); - }) - .catch((e) => { - console.error( - `streamingResponseViaWaitUntil runProxy threw writes=${writeCount} total=${totalBytes} closed=${closed} aborted=${aborted}`, - e, - ); - proxyError = e; - signalReady(); - baseWriter.abort(e).catch(() => {}); - signalPipeComplete(); - }); - - ctx.waitUntil( - Promise.all([proxyPromise, pipeComplete]).then(() => { - console.log( - `streamingResponseViaWaitUntil waitUntil settled writes=${writeCount} total=${totalBytes} closed=${closed} aborted=${aborted}`, - ); - }), - ); - - await headersReady; - if (proxyError !== undefined) { - return new Response(`${proxyError}`, { - status: 400, - headers: { "Content-Type": "text/plain" }, - }); - } - - console.log( - `streamingResponseViaWaitUntil returning Response status=${getStatus()} writes=${writeCount} total=${totalBytes}`, - ); - - return new Response(wrapWithMeter(readable), { - status: getStatus(), - headers: getHeaders(), - }); -} - export function EdgeProxyV1(opts: ProxyOpts) { return async (request: Request, ctx: EdgeContext) => { let corsHeaders = {}; @@ -444,7 +319,6 @@ export function EdgeProxyV1(opts: ProxyOpts) { headers: { "Content-Type": "text/plain" }, }); } - const method: "GET" | "POST" = request.method; const relativeURL = opts.getRelativeURL(request); @@ -511,62 +385,43 @@ export function EdgeProxyV1(opts: ProxyOpts) { } }; - const requestBody = await request.text(); - const proxyV1Args = { - method, - url: relativeURL, - proxyHeaders, - body: requestBody, - setHeader, - setStatusCode: setStatus, - getApiSecrets: fetchApiSecrets, - cacheGet, - cachePut, - digest: digestMessage, - logHistogram: opts.logHistogram, - spanLogger: opts.spanLogger, - billingOrgId: opts.billingOrgId, - onBillingEvent: opts.onBillingEvent, - }; - - const meterProvider = opts.meterProvider; - const wrapWithMeter = (body: ReadableStream) => - meterProvider - ? body.pipeThrough( - new TransformStream({ - flush() { - ctx.waitUntil(flushMetrics(meterProvider)); - }, - }), - ) - : body; - - if (opts.streamingViaWaitUntil) { - return streamingResponseViaWaitUntil({ - ctx, - wrapWithMeter, - runProxy: (res) => proxyV1({ ...proxyV1Args, res }), - getStatus: () => status, - getHeaders: () => headers, - }); - } - - // Default path: await proxyV1 to completion before returning. try { await proxyV1({ - ...proxyV1Args, + method: request.method, + url: relativeURL, + proxyHeaders, + body: await request.text(), + setHeader, + setStatusCode: setStatus, res: writable, + getApiSecrets: fetchApiSecrets, + cacheGet, + cachePut, + digest: digestMessage, + logHistogram: opts.logHistogram, + spanLogger: opts.spanLogger, + billingOrgId: opts.billingOrgId, + onBillingEvent: opts.onBillingEvent, }); } catch (e) { - // log error to vercel - console.error("EdgeProxyV1 request failed", e); return new Response(`${e}`, { status: 400, headers: { "Content-Type": "text/plain" }, }); } - return new Response(wrapWithMeter(readable), { + const meterProvider = opts.meterProvider; + const responseBody = meterProvider + ? readable.pipeThrough( + new TransformStream({ + flush() { + ctx.waitUntil(flushMetrics(meterProvider)); + }, + }), + ) + : readable; + + return new Response(responseBody, { status, headers, }); diff --git a/packages/proxy/src/proxy.stream.test.ts b/packages/proxy/src/proxy.stream.test.ts deleted file mode 100644 index 257ca537..00000000 --- a/packages/proxy/src/proxy.stream.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { pipeBodyToResponse } from "./proxy"; - -describe("pipeBodyToResponse", () => { - it("resolves after the readable stream finishes piping", async () => { - const encoder = new TextEncoder(); - const chunks: string[] = []; - let closed = false; - let finishedWriting = false; - - const stream = new ReadableStream({ - async start(controller) { - controller.enqueue(encoder.encode("first")); - await new Promise((resolve) => setTimeout(resolve, 20)); - controller.enqueue(encoder.encode("-second")); - finishedWriting = true; - controller.close(); - }, - }); - - const responsePromise = pipeBodyToResponse( - stream, - new WritableStream({ - write(chunk) { - chunks.push(new TextDecoder().decode(chunk)); - }, - close() { - closed = true; - }, - }), - ); - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(finishedWriting).toBe(false); - expect(closed).toBe(false); - - await responsePromise; - - expect(finishedWriting).toBe(true); - expect(closed).toBe(true); - expect(chunks.join("")).toBe("first-second"); - }); -}); diff --git a/packages/proxy/src/proxy.ts b/packages/proxy/src/proxy.ts index 1d3e78ec..7651b2d7 100644 --- a/packages/proxy/src/proxy.ts +++ b/packages/proxy/src/proxy.ts @@ -436,11 +436,10 @@ export async function proxyV1({ e instanceof Error ? e.message : JSON.stringify(e), ); } finally { - try { - await pipeBodyToResponse(readable, res); - } catch (e) { - console.error("Error writing credentials response", e); - throw e; + if (readable) { + readable.pipeTo(res).catch(console.error); + } else { + res.close().catch(console.error); } } return; @@ -1223,11 +1222,14 @@ export async function proxyV1({ stream = stream.pipeThrough(parseStream); } - try { - await pipeBodyToResponse(stream, res); - } catch (e) { - console.error("Error piping stream to response", e); - throw e; + if (stream) { + stream.pipeTo(res).catch((e) => { + console.error("Error piping stream to response", e); + }); + } else { + res.close().catch((e) => { + console.error("Error closing response", e); + }); } logRequest(); @@ -1238,18 +1240,6 @@ export async function proxyV1({ }); } -export async function pipeBodyToResponse( - stream: ReadableStream | null, - res: WritableStream, -) { - if (stream) { - await stream.pipeTo(res); - return; - } - - await res.close(); -} - const RATE_LIMIT_ERROR_CODE = 429; const OVERLOADED_ERROR_CODE = 503; const RATE_LIMIT_MAX_WAIT_MS = 45 * 1000; // Wait up to 45 seconds while retrying diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7717ad3..975b685c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,15 +156,9 @@ importers: '@braintrust/proxy': specifier: workspace:* version: link:../../packages/proxy - '@upstash/ratelimit': - specifier: ^0.4.3 - version: 0.4.3 '@vercel/examples-ui': specifier: ^1.0.5 version: 1.0.5(next@16.2.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@vercel/functions': - specifier: ^3.4.2 - version: 3.5.1(@aws-sdk/credential-provider-web-identity@3.972.38) '@vercel/kv': specifier: ^0.2.2 version: 0.2.2 @@ -2231,19 +2225,9 @@ packages: resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@upstash/core-analytics@0.0.6': - resolution: {integrity: sha512-cpPSR0XJAJs4Ddz9nq3tINlPS5aLfWVCqhhtHnXt4p7qr5+/Znlt1Es736poB/9rnl1hAHrOsOvVj46NEXcVqA==} - engines: {node: '>=16.0.0'} - - '@upstash/ratelimit@0.4.3': - resolution: {integrity: sha512-Dsp9Mw09Flg28JRklKgFiCXqr3bqv8bbG0kgpUYoHjcgPPolFFyaYOj/I2HExvYLZiogl77NUavBoNvMOK0zUQ==} - '@upstash/redis@1.21.0': resolution: {integrity: sha512-c6M+cl0LOgGK/7Gp6ooMkIZ1IDAJs8zFR+REPkoSkAq38o7CWFX5FYwYEqGZ6wJpUGBuEOr/7hTmippXGgL25A==} - '@upstash/redis@1.25.1': - resolution: {integrity: sha512-ACj0GhJ4qrQyBshwFgPod6XufVEfKX2wcaihsEvSdLYnY+m+pa13kGt1RXm/yTHKf4TQi/Dy2A8z/y6WUEOmlg==} - '@vercel/examples-ui@1.0.5': resolution: {integrity: sha512-6pj8V9MPLgrPajIUbqEkbmupEMnK6xBk5zkwsilwhA9mYQdxttqgVyDqlFMQhEVqb3tk0PshA+XxnZoDfLCpoQ==} peerDependencies: @@ -2260,24 +2244,11 @@ packages: '@aws-sdk/credential-provider-web-identity': optional: true - '@vercel/functions@3.5.1': - resolution: {integrity: sha512-ndh5v+uhWqGA8033oD0i0KHvqUHcLlLCOaLOw5L+xx5zVsWUSQcZPKEYk2nm51aisnKhcnTylxnOmhx+w4UCRA==} - engines: {node: '>= 20'} - peerDependencies: - '@aws-sdk/credential-provider-web-identity': '*' - peerDependenciesMeta: - '@aws-sdk/credential-provider-web-identity': - optional: true - '@vercel/kv@0.2.2': resolution: {integrity: sha512-mqnQOB6bkp4h5eObxfLNIlhlVqOGSH8cWOlC5pDVWTjX3zL8dETO1ZBl6M74HBmeBjbD5+J7wDJklRigY6UNKw==} engines: {node: '>=14.6'} deprecated: 'Vercel KV is deprecated. If you had an existing KV store, it should have moved to Upstash Redis which you will see under Vercel Integrations. For new projects, install a Redis integration from Vercel Marketplace: https://vercel.com/marketplace?category=storage&search=redis' - '@vercel/oidc@3.4.1': - resolution: {integrity: sha512-H6B+/ig/GoahccL3WZjiHayHw1H5KhvTJNceqYulwfK9kkz5iul2hTmYzcJ7tTCQzyd0dutuL9xYFZCyLUqsog==} - engines: {node: '>= 20'} - '@vitest/expect@4.1.5': resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} @@ -2809,9 +2780,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - crypto-js@4.2.0: - resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} - css-tree@2.3.1: resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -7412,24 +7380,12 @@ snapshots: '@typescript-eslint/types': 8.59.2 eslint-visitor-keys: 5.0.1 - '@upstash/core-analytics@0.0.6': - dependencies: - '@upstash/redis': 1.25.1 - - '@upstash/ratelimit@0.4.3': - dependencies: - '@upstash/core-analytics': 0.0.6 - '@upstash/redis@1.21.0': dependencies: isomorphic-fetch: 3.0.0 transitivePeerDependencies: - encoding - '@upstash/redis@1.25.1': - dependencies: - crypto-js: 4.2.0 - '@vercel/examples-ui@1.0.5(next@16.2.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@swc/helpers': 0.4.14 @@ -7443,20 +7399,12 @@ snapshots: optionalDependencies: '@aws-sdk/credential-provider-web-identity': 3.972.38 - '@vercel/functions@3.5.1(@aws-sdk/credential-provider-web-identity@3.972.38)': - dependencies: - '@vercel/oidc': 3.4.1 - optionalDependencies: - '@aws-sdk/credential-provider-web-identity': 3.972.38 - '@vercel/kv@0.2.2': dependencies: '@upstash/redis': 1.21.0 transitivePeerDependencies: - encoding - '@vercel/oidc@3.4.1': {} - '@vitest/expect@4.1.5': dependencies: '@standard-schema/spec': 1.1.0 @@ -8100,8 +8048,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - crypto-js@4.2.0: {} - css-tree@2.3.1: dependencies: mdn-data: 2.0.30 From b191c2d72b9e9c61088d4ea09fdd98a5e196b66c Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Tue, 12 May 2026 20:34:05 -0700 Subject: [PATCH 21/22] address review --- .../vercel/app/api/v1/[...slug]/route.test.ts | 86 +++++++++++++++++++ apis/vercel/app/api/v1/[...slug]/route.ts | 9 +- apis/vercel/next-env.d.ts | 2 +- packages/proxy/edge/index.metrics.test.ts | 48 ----------- 4 files changed, 92 insertions(+), 53 deletions(-) create mode 100644 apis/vercel/app/api/v1/[...slug]/route.test.ts diff --git a/apis/vercel/app/api/v1/[...slug]/route.test.ts b/apis/vercel/app/api/v1/[...slug]/route.test.ts new file mode 100644 index 00000000..38908e6a --- /dev/null +++ b/apis/vercel/app/api/v1/[...slug]/route.test.ts @@ -0,0 +1,86 @@ +import { ProxyBadRequestError } from "@braintrust/proxy"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { proxyV1ToAppRouteResponseMock } = vi.hoisted(() => { + return { + proxyV1ToAppRouteResponseMock: vi.fn(), + }; +}); + +vi.mock("next/server", () => ({ + after: (callback: () => void | Promise) => { + void callback(); + }, +})); + +vi.mock("@vercel/kv", () => ({ + kv: { + get: vi.fn(), + set: vi.fn(), + }, +})); + +vi.mock("@braintrust/proxy/edge", () => ({ + digestMessage: vi.fn(async (message: string) => message), + encryptedGet: vi.fn(async () => null), + encryptedPut: vi.fn(async () => {}), + getCorsHeaders: vi.fn(() => ({})), + makeFetchApiSecrets: vi.fn(() => vi.fn(async () => [])), +})); + +vi.mock("../../../../lib/appRouteProxy", () => ({ + proxyV1ToAppRouteResponse: proxyV1ToAppRouteResponseMock, +})); + +describe("vercel app route proxy handler", () => { + beforeEach(() => { + proxyV1ToAppRouteResponseMock.mockReset(); + }); + + it("returns 400 for proxy bad request errors without exposing internal details", async () => { + proxyV1ToAppRouteResponseMock.mockRejectedValueOnce( + new ProxyBadRequestError("Missing Authentication header"), + ); + + const { POST } = await import("./route"); + const response = await POST( + new Request("https://example.com/api/v1/chat/completions", { + method: "POST", + headers: { + "content-type": "application/json", + "x-request-id": "caller-controlled", + }, + body: "{}", + }), + ); + + expect(response.status).toBe(400); + expect(response.headers.get("x-request-id")).toBeTruthy(); + expect(response.headers.get("x-request-id")).not.toBe("caller-controlled"); + await expect(response.json()).resolves.toMatchObject({ + error: "Internal server error", + }); + }); + + it("returns 500 for unexpected proxy errors", async () => { + proxyV1ToAppRouteResponseMock.mockRejectedValueOnce( + new Error("database unavailable"), + ); + + const { POST } = await import("./route"); + const response = await POST( + new Request("https://example.com/api/v1/chat/completions", { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: "{}", + }), + ); + + expect(response.status).toBe(500); + await expect(response.json()).resolves.toMatchObject({ + error: "Internal server error", + }); + }); +}); diff --git a/apis/vercel/app/api/v1/[...slug]/route.ts b/apis/vercel/app/api/v1/[...slug]/route.ts index 5a6a3998..700a185d 100644 --- a/apis/vercel/app/api/v1/[...slug]/route.ts +++ b/apis/vercel/app/api/v1/[...slug]/route.ts @@ -2,6 +2,7 @@ import dns from "node:dns"; import { after } from "next/server"; +import { ProxyBadRequestError } from "@braintrust/proxy"; import { type CacheSetOptions, digestMessage, @@ -21,6 +22,7 @@ const ACCESS_CONTROL_REQUEST_HEADERS_HEADER = "access-control-request-headers"; const ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "access-control-allow-headers"; const ALLOW_METHODS_HEADER_VALUE = "GET, HEAD, POST, OPTIONS"; const FORBIDDEN_RESPONSE_TEXT = "Forbidden"; +const INTERNAL_SERVER_ERROR_TEXT = "Internal server error"; const API_V1_PATH_PREFIX = /^\/api\/v1/; const DEFAULT_CACHE_TTL_SECONDS = 60 * 60 * 24 * 7; const KEEP_ALIVE_TIMEOUT_MS = 1; @@ -82,8 +84,7 @@ function handleOptions(request: Request, corsHeaders: Record) { } async function handleRequest(method: "GET" | "POST", request: Request) { - const requestId = - request.headers.get(REQUEST_ID_HEADER) ?? crypto.randomUUID(); + const requestId = crypto.randomUUID(); const requestUrl = new URL(request.url); const backgroundTasks: Promise[] = []; @@ -150,11 +151,11 @@ async function handleRequest(method: "GET" | "POST", request: Request) { } catch (error) { return Response.json( { - error: error instanceof Error ? error.message : String(error), + error: INTERNAL_SERVER_ERROR_TEXT, requestId, }, { - status: 500, + status: error instanceof ProxyBadRequestError ? 400 : 500, headers: initialHeaders, }, ); diff --git a/apis/vercel/next-env.d.ts b/apis/vercel/next-env.d.ts index 0c7fad71..2d5420eb 100644 --- a/apis/vercel/next-env.d.ts +++ b/apis/vercel/next-env.d.ts @@ -1,7 +1,7 @@ /// /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/packages/proxy/edge/index.metrics.test.ts b/packages/proxy/edge/index.metrics.test.ts index 82f87176..88813abc 100644 --- a/packages/proxy/edge/index.metrics.test.ts +++ b/packages/proxy/edge/index.metrics.test.ts @@ -71,52 +71,4 @@ describe("EdgeProxyV1 metric flushing", () => { expect(waitUntilPromises).toHaveLength(1); await Promise.all(waitUntilPromises); }); - - it("returns application/json on the default response path", async () => { - const { EdgeProxyV1 } = await import("./index"); - - proxyV1Mock.mockImplementation( - async ({ - res, - setHeader, - }: { - res: WritableStream; - setHeader: (name: string, value: string) => void; - }) => { - setHeader("content-type", "application/json"); - const writer = res.getWriter(); - queueMicrotask(() => { - void writer - .write(new TextEncoder().encode(JSON.stringify({ ok: true }))) - .then(() => writer.close()); - }); - }, - ); - - const waitUntilPromises: Promise[] = []; - const handler = EdgeProxyV1({ - getRelativeURL() { - return "/chat/completions"; - }, - }); - - const response = await handler( - new Request("https://example.com/v1/chat/completions", { - method: "POST", - body: "{}", - headers: { - Authorization: "Bearer test-token", - }, - }), - { - waitUntil(promise) { - waitUntilPromises.push(promise); - }, - }, - ); - - expect(response.headers.get("content-type")).toBe("application/json"); - await expect(response.json()).resolves.toEqual({ ok: true }); - expect(waitUntilPromises).toHaveLength(0); - }); }); From fa79a0453a65709512d485f8d4c263909a1749a6 Mon Sep 17 00:00:00 2001 From: Caitlin Pinn Date: Wed, 13 May 2026 18:28:40 -0700 Subject: [PATCH 22/22] modify wrangler template --- apis/cloudflare/wrangler-template.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apis/cloudflare/wrangler-template.toml b/apis/cloudflare/wrangler-template.toml index 140710cc..c9867917 100644 --- a/apis/cloudflare/wrangler-template.toml +++ b/apis/cloudflare/wrangler-template.toml @@ -1,7 +1,7 @@ name = "proxy" main = "src/index.ts" -compatibility_date = "2024-09-23" -compatibility_flags = ["nodejs_compat_v2"] +compatibility_date = "2025-08-15" +compatibility_flags = ["nodejs_compat"] kv_namespaces = [ # Configure this id to map to the id returned from