diff --git a/.changeset/sandbox-eve-client-header.md b/.changeset/sandbox-eve-client-header.md new file mode 100644 index 000000000..bf43dd934 --- /dev/null +++ b/.changeset/sandbox-eve-client-header.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Sandbox API requests now send an `x-eve-client: eve/` header. diff --git a/packages/eve/src/execution/sandbox/bindings/vercel-client-header.test.ts b/packages/eve/src/execution/sandbox/bindings/vercel-client-header.test.ts new file mode 100644 index 000000000..f443e4783 --- /dev/null +++ b/packages/eve/src/execution/sandbox/bindings/vercel-client-header.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from "vitest"; + +import { + EVE_SANDBOX_CLIENT_HEADER, + withEveSandboxClientHeader, +} from "#execution/sandbox/bindings/vercel-client-header.js"; + +function headerOf(init: RequestInit | undefined): string | null { + return new Headers(init?.headers).get(EVE_SANDBOX_CLIENT_HEADER); +} + +describe("withEveSandboxClientHeader", () => { + it("stamps the eve client header as eve/", async () => { + const inner = vi.fn().mockResolvedValue(new Response()); + const wrapped = withEveSandboxClientHeader(inner); + + await wrapped("https://api.vercel.com/sandboxes"); + + const [, init] = inner.mock.calls[0]!; + expect(headerOf(init)).toMatch(/^eve\/.+/); + }); + + it("preserves existing headers", async () => { + const inner = vi.fn().mockResolvedValue(new Response()); + const wrapped = withEveSandboxClientHeader(inner); + + await wrapped("https://api.vercel.com/sandboxes", { + headers: { "user-agent": "vercel/sandbox/1.0.0" }, + }); + + const [, init] = inner.mock.calls[0]!; + const headers = new Headers(init?.headers); + expect(headers.get("user-agent")).toBe("vercel/sandbox/1.0.0"); + expect(headers.get(EVE_SANDBOX_CLIENT_HEADER)).toMatch(/^eve\/.+/); + }); + + it("delegates to globalThis.fetch when no inner fetch is supplied", () => { + const wrapped = withEveSandboxClientHeader(); + expect(typeof wrapped).toBe("function"); + }); +}); diff --git a/packages/eve/src/execution/sandbox/bindings/vercel-client-header.ts b/packages/eve/src/execution/sandbox/bindings/vercel-client-header.ts new file mode 100644 index 000000000..48a9a222e --- /dev/null +++ b/packages/eve/src/execution/sandbox/bindings/vercel-client-header.ts @@ -0,0 +1,29 @@ +import { resolveInstalledPackageInfo } from "#internal/application/package.js"; + +/** + * Request header eve stamps on every Vercel Sandbox API call so the sandbox + * control plane can attribute traffic to eve and its version. + */ +export const EVE_SANDBOX_CLIENT_HEADER = "x-eve-client"; + +/** + * Wraps a `fetch` implementation so every request carries the + * {@link EVE_SANDBOX_CLIENT_HEADER} identifying the eve client and version. + */ +export function withEveSandboxClientHeader( + inner: typeof globalThis.fetch = globalThis.fetch, +): typeof globalThis.fetch { + const { name, version } = resolveInstalledPackageInfo(); + const clientId = `${name}/${version}`; + + return (input, init) => { + const headers = new Headers( + init?.headers ?? + (typeof input === "object" && input !== null && "headers" in input + ? (input as Request).headers + : undefined), + ); + headers.set(EVE_SANDBOX_CLIENT_HEADER, clientId); + return inner(input, { ...init, headers }); + }; +} diff --git a/packages/eve/src/execution/sandbox/bindings/vercel-create-sdk.ts b/packages/eve/src/execution/sandbox/bindings/vercel-create-sdk.ts index 80a6933b2..f3992508f 100644 --- a/packages/eve/src/execution/sandbox/bindings/vercel-create-sdk.ts +++ b/packages/eve/src/execution/sandbox/bindings/vercel-create-sdk.ts @@ -1,3 +1,4 @@ +import { getVercelSandboxFetch } from "#execution/sandbox/bindings/vercel-credentials.js"; import type { VercelCreateOptions, VercelModule, @@ -27,6 +28,7 @@ export async function createVercelEveImageSandbox(input: { const createOptions: VercelSandboxCreateParams = { ...input.createOptions, __image: VERCEL_EVE_SANDBOX_IMAGE, + fetch: getVercelSandboxFetch(input.createOptions), }; return await input.sandboxModule.Sandbox.create(createOptions); } diff --git a/packages/eve/src/execution/sandbox/bindings/vercel-credentials.ts b/packages/eve/src/execution/sandbox/bindings/vercel-credentials.ts index c0b5578f4..f6b051135 100644 --- a/packages/eve/src/execution/sandbox/bindings/vercel-credentials.ts +++ b/packages/eve/src/execution/sandbox/bindings/vercel-credentials.ts @@ -1,9 +1,10 @@ import { getVercelOidcToken } from "#compiled/@vercel/oidc/index.js"; +import { withEveSandboxClientHeader } from "#execution/sandbox/bindings/vercel-client-header.js"; import type { VercelCreateOptions } from "#execution/sandbox/bindings/vercel-sdk-types.js"; export function getVercelSandboxFetch(createOptions: VercelCreateOptions): typeof globalThis.fetch { const fetchOverride = (createOptions as { readonly fetch?: typeof globalThis.fetch }).fetch; - return fetchOverride ?? globalThis.fetch; + return withEveSandboxClientHeader(fetchOverride ?? globalThis.fetch); } export async function getVercelSandboxCredentials(