diff --git a/.changeset/worker-loader-port.md b/.changeset/worker-loader-port.md new file mode 100644 index 000000000..6f7339a5f --- /dev/null +++ b/.changeset/worker-loader-port.md @@ -0,0 +1,24 @@ +--- +'@tanstack/ai-isolate-cloudflare': minor +--- + +Port the Cloudflare worker driver from `unsafe_eval` to `worker_loader` (Dynamic Workers). + +Cloudflare gates the `unsafe_eval` binding for all customer prod accounts; the previous driver was unusable in production and broken in `wrangler dev` on current Wrangler 4.x. The supported replacement is the `worker_loader` binding (GA-beta'd 2026-03-24). + +**Breaking:** the worker now requires the `LOADER` binding instead of `UNSAFE_EVAL`. Update your `wrangler.toml`: + +```toml +# before +[[unsafe.bindings]] +name = "UNSAFE_EVAL" +type = "unsafe_eval" + +# after +[[worker_loaders]] +binding = "LOADER" +``` + +The HTTP tool-callback protocol and public driver API are unchanged. Workers Paid plan is required for any edge usage (deploy or `wrangler dev --remote`); local `wrangler dev` works on the Free plan. + +Closes #522. diff --git a/.gitignore b/.gitignore index 4c8ee6364..6c359c884 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ stats.html .tsup .vinxi temp +.wrangler vite.config.js.timestamp-* vite.config.ts.timestamp-* diff --git a/packages/typescript/ai-isolate-cloudflare/README.md b/packages/typescript/ai-isolate-cloudflare/README.md index aeb922a7a..7fbb8da3f 100644 --- a/packages/typescript/ai-isolate-cloudflare/README.md +++ b/packages/typescript/ai-isolate-cloudflare/README.md @@ -12,9 +12,9 @@ pnpm add @tanstack/ai-isolate-cloudflare ## Environment Guidance -- **Local development:** supported with the package's Miniflare dev server (`pnpm dev:worker`) -- **Remote dev:** supported with `wrangler dev --remote` -- **Production:** supported on Cloudflare accounts with the `unsafe_eval` binding enabled. Before rollout, put the Worker behind authentication (e.g. Cloudflare Access or the `authorization` driver option), rate limiting, and CORS restrictions — running LLM-authored code is a high-trust operation. +- **Local development:** supported with `wrangler dev` (the `worker_loader` binding works in local workerd on the Workers Free plan). +- **Remote dev:** supported with `wrangler dev --remote` on a Workers Paid plan. +- **Production:** supported on Cloudflare accounts on the Workers Paid plan ($5/mo). The Free plan rejects `worker_loader` deploys at the API level. Before rollout, put the Worker behind authentication (e.g. Cloudflare Access or the `authorization` driver option), rate limiting, and CORS restrictions — running LLM-authored code is a high-trust operation. If you want a self-contained host without Cloudflare infrastructure, prefer `@tanstack/ai-isolate-node` or `@tanstack/ai-isolate-quickjs`. @@ -60,31 +60,31 @@ const result = await chat({ ## Worker Setup -### Option 1: Local Miniflare server +### Option 1: Local dev with `wrangler dev` From this package directory: ```bash -pnpm dev:worker +wrangler dev ``` -This starts a local Worker endpoint (default `http://localhost:8787`) with the `UNSAFE_EVAL` binding configured in `wrangler.toml`. +This starts a local Worker endpoint (default `http://localhost:8787`) with the `worker_loader` binding from `wrangler.toml`. Local workerd accepts the binding on the Workers Free plan, so no upgrade is needed for inner-loop iteration. -### Option 3: Production deployment +### Option 2: Wrangler remote dev ```bash -wrangler deploy +wrangler dev --remote ``` -The same `wrangler.toml` `[[unsafe.bindings]]` configuration applies in production. Deploying requires that your Cloudflare account has `unsafe_eval` enabled; without it, the Worker returns an `UnsafeEvalNotAvailable` error. Because this Worker executes LLM-generated code, only deploy it behind authentication, rate limiting, and an allow-listed origin. +Runs through Cloudflare's network for validation against the hosted runtime. Requires a Workers Paid plan because `worker_loader` is gated to Paid for any edge usage. -### Option 2: Wrangler remote dev +### Option 3: Production deployment ```bash -wrangler dev --remote +wrangler deploy ``` -This runs through Cloudflare's network and can be useful when validating behavior against the hosted runtime. +Requires a Workers Paid plan. The Free plan rejects deploys with `worker_loader` (`code: 10195` — "In order to use Dynamic Workers, you must switch to a paid plan."). Without the binding, the Worker returns a `WorkerLoaderNotAvailable` error. Because this Worker executes LLM-generated code, only deploy it behind authentication, rate limiting, and an allow-listed origin. ## API diff --git a/packages/typescript/ai-isolate-cloudflare/dev-server.mjs b/packages/typescript/ai-isolate-cloudflare/dev-server.mjs deleted file mode 100644 index eec0a4463..000000000 --- a/packages/typescript/ai-isolate-cloudflare/dev-server.mjs +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Local dev server using miniflare directly. - * - * wrangler dev does NOT translate [[unsafe.bindings]] type = "unsafe_eval" - * into miniflare's unsafeEvalBinding option, so the binding is always - * undefined in local dev. This script bundles the Worker with esbuild - * and runs it via miniflare with unsafeEvalBinding configured correctly. - * - * Usage: node dev-server.mjs [--port 8787] - */ - -import { Miniflare } from 'miniflare' -import { build } from 'esbuild' -import { resolve, dirname } from 'node:path' -import { fileURLToPath } from 'node:url' - -const __dirname = dirname(fileURLToPath(import.meta.url)) -const ENTRY = resolve(__dirname, 'src/worker/index.ts') -const PORT = Number(process.env.PORT) || 8787 - -const result = await build({ - entryPoints: [ENTRY], - bundle: true, - format: 'esm', - target: 'esnext', - write: false, -}) - -const mf = new Miniflare({ - modules: [ - { - type: 'ESModule', - path: 'worker.js', - contents: result.outputFiles[0].text, - }, - ], - unsafeEvalBinding: 'UNSAFE_EVAL', - compatibilityDate: '2024-12-01', - compatibilityFlags: ['nodejs_compat'], - port: PORT, -}) - -const url = await mf.ready -console.log(`Worker ready on ${url}`) - -process.on('SIGINT', async () => { - await mf.dispose() - process.exit(0) -}) diff --git a/packages/typescript/ai-isolate-cloudflare/package.json b/packages/typescript/ai-isolate-cloudflare/package.json index 55b619181..df1b5d02c 100644 --- a/packages/typescript/ai-isolate-cloudflare/package.json +++ b/packages/typescript/ai-isolate-cloudflare/package.json @@ -35,7 +35,7 @@ "scripts": { "build": "vite build", "clean": "premove ./build ./dist", - "dev:worker": "node dev-server.mjs", + "dev:worker": "wrangler dev", "deploy:worker": "wrangler deploy", "lint:fix": "eslint ./src --fix", "test:build": "publint --strict", @@ -59,8 +59,6 @@ "devDependencies": { "@cloudflare/workers-types": "^4.20241230.0", "@vitest/coverage-v8": "4.0.14", - "esbuild": "^0.25.12", - "miniflare": "^4.20260305.0", - "wrangler": "^4.19.1" + "wrangler": "^4.88.0" } } diff --git a/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts b/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts index 2e6d4ff79..073b89252 100644 --- a/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts +++ b/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts @@ -1,41 +1,88 @@ /** * Cloudflare Worker for Code Mode execution * - * This Worker executes JavaScript code in a V8 isolate on Cloudflare's edge network. - * Tool calls are handled via a request/response loop with the driver. + * Executes JavaScript code in a fresh V8 isolate on Cloudflare's edge network + * using the `worker_loader` (Dynamic Workers) binding. Tool calls round-trip + * to the driver via the same request/response protocol as before. * * Flow: * 1. Receive code + tool schemas - * 2. Execute code, collecting any tool calls - * 3. If tool calls are needed, return them to the driver - * 4. Driver executes tools locally, sends results back - * 5. Re-execute with tool results injected - * 6. Return final result + * 2. Wrap user code in an ES module exporting a `fetch` handler that returns + * the IIFE result as JSON + * 3. Load the module into a child Worker via `env.LOADER.load(...)` and + * invoke its entrypoint + * 4. If tool calls are needed, return them to the driver + * 5. Driver executes tools locally, sends results back + * 6. Re-execute with tool results injected + * 7. Return final result + * + * `worker_loader` replaces the previous `unsafe_eval` binding, which is gated + * by Cloudflare for all customer accounts and unusable in production. See + * https://developers.cloudflare.com/dynamic-workers/ for the supported API. */ import { wrapCode } from './wrap-code' import type { ExecuteRequest, ExecuteResponse, ToolCallRequest } from '../types' /** - * UnsafeEval binding type. + * Compatibility date for the loaded child Worker. Pinned at this layer so + * sandbox semantics don't drift with the parent Worker's compat date. + */ +const SANDBOX_COMPAT_DATE = '2026-05-01' + +/** + * Worker Loader binding type. * - * Provides dynamic-code execution against the Worker's V8 isolate. Available - * locally (via wrangler dev) and in production deployments where the - * `unsafe_eval` binding has been enabled on the Cloudflare account. + * Provides dynamic-code execution by loading a module into a fresh V8 + * isolate. Configure in wrangler.toml under `[[worker_loaders]]`. Requires a + * Workers Paid plan; see https://developers.cloudflare.com/dynamic-workers/. */ -interface UnsafeEval { - eval: (code: string) => unknown +interface WorkerLoaderEntrypoint { + fetch: (request: Request) => Promise +} + +interface LoadedWorker { + getEntrypoint: (name?: string) => WorkerLoaderEntrypoint +} + +interface WorkerLoader { + load: (options: { + compatibilityDate: string + mainModule: string + modules: Record + globalOutbound?: unknown + env?: Record + }) => LoadedWorker } interface Env { /** - * UnsafeEval binding. Configured in wrangler.toml as an unsafe binding. + * worker_loader (Dynamic Workers) binding. Configured in wrangler.toml + * under `[[worker_loaders]] binding = "LOADER"`. */ - UNSAFE_EVAL?: UnsafeEval + LOADER?: WorkerLoader } /** - * Execute code in the Worker's V8 isolate + * Wrap the existing IIFE-returning string in an ES module that exposes a + * `fetch` handler. The child Worker's entrypoint runs the IIFE on each + * invocation and returns the structured result as JSON. + */ +function wrapAsSandboxModule(wrappedCode: string): string { + return ` +export default { + async fetch() { + const __result = await ${wrappedCode}; + return new Response(JSON.stringify(__result), { + headers: { 'Content-Type': 'application/json' }, + }); + } +}; +` +} + +/** + * Execute code in a freshly loaded child Worker isolate. */ async function executeCode( request: ExecuteRequest, @@ -43,41 +90,61 @@ async function executeCode( ): Promise { const { code, tools, toolResults, timeout = 30000 } = request - // Check if UNSAFE_EVAL binding is available - if (!env.UNSAFE_EVAL) { + if (!env.LOADER) { return { status: 'error', error: { - name: 'UnsafeEvalNotAvailable', + name: 'WorkerLoaderNotAvailable', message: - 'UNSAFE_EVAL binding is not available. ' + - 'This Worker requires the unsafe_eval binding. ' + - 'Declare it in wrangler.toml under [[unsafe.bindings]] ' + - '(works for local development and production where the ' + - 'account has unsafe_eval enabled).', + 'LOADER binding is not available. ' + + 'This Worker requires the worker_loader (Dynamic Workers) binding. ' + + 'Declare it in wrangler.toml under [[worker_loaders]] with ' + + 'binding = "LOADER" (Workers Paid plan required).', }, } } try { const wrappedCode = wrapCode(code, tools, toolResults) + const moduleSource = wrapAsSandboxModule(wrappedCode) - // Execute with timeout + // AbortController propagates into the loaded Worker via Request.signal so + // a timeout actually cancels the in-flight fetch instead of leaking the + // child isolate. The Promise.race remains as a belt-and-suspenders guard + // for runtimes that ignore the signal. const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), timeout) + let timeoutId: ReturnType | undefined + const TIMEOUT_SENTINEL = '__SANDBOX_TIMEOUT__' + const timeoutPromise = new Promise((_resolve, reject) => { + timeoutId = setTimeout(() => { + controller.abort() + reject(new Error(TIMEOUT_SENTINEL)) + }, timeout) + }) try { - // Execute the wrapped code through the UNSAFE_EVAL binding. - const result = (await env.UNSAFE_EVAL.eval(wrappedCode)) as { + const loaded = env.LOADER.load({ + compatibilityDate: SANDBOX_COMPAT_DATE, + mainModule: 'main.js', + modules: { 'main.js': moduleSource }, + globalOutbound: null, + env: {}, + }) + const entrypoint = loaded.getEntrypoint() + const fetchPromise = entrypoint.fetch( + new Request('https://sandbox.invalid/', { signal: controller.signal }), + ) + const response = await Promise.race([fetchPromise, timeoutPromise]) + if (timeoutId) clearTimeout(timeoutId) + + const result: { status: string success?: boolean value?: unknown error?: { name: string; message: string; stack?: string } logs: Array toolCalls?: Array - } - - clearTimeout(timeoutId) + } = await response.json() if (result.status === 'need_tools') { return { @@ -96,9 +163,13 @@ async function executeCode( logs: result.logs, } } catch (evalError: unknown) { - clearTimeout(timeoutId) + if (timeoutId) clearTimeout(timeoutId) + const error = evalError as Error - if (controller.signal.aborted) { + // Either branch of the Promise.race may win on timeout: timeoutPromise + // rejects with TIMEOUT_SENTINEL, while the AbortController.abort() call + // can race-reject the in-flight fetch first. Treat both as a timeout. + if (error.message === TIMEOUT_SENTINEL || controller.signal.aborted) { return { status: 'error', error: { @@ -108,7 +179,6 @@ async function executeCode( } } - const error = evalError as Error return { status: 'done', success: false, diff --git a/packages/typescript/ai-isolate-cloudflare/src/worker/wrap-code.ts b/packages/typescript/ai-isolate-cloudflare/src/worker/wrap-code.ts index 72aafb6c8..3a711058e 100644 --- a/packages/typescript/ai-isolate-cloudflare/src/worker/wrap-code.ts +++ b/packages/typescript/ai-isolate-cloudflare/src/worker/wrap-code.ts @@ -1,6 +1,6 @@ /** * Code wrapping utilities for the Cloudflare Worker. - * Extracted for testability without UNSAFE_EVAL. + * Extracted for testability without a live worker_loader binding. */ import type { ToolResultPayload, ToolSchema } from '../types' diff --git a/packages/typescript/ai-isolate-cloudflare/tests/escape-attempts.test.ts b/packages/typescript/ai-isolate-cloudflare/tests/escape-attempts.test.ts index f68ab8109..7cfcfcabe 100644 --- a/packages/typescript/ai-isolate-cloudflare/tests/escape-attempts.test.ts +++ b/packages/typescript/ai-isolate-cloudflare/tests/escape-attempts.test.ts @@ -4,9 +4,9 @@ import type { ToolResultPayload, ToolSchema } from '../src/types' /** * The CF Worker delegates actual sandboxing to Workers' V8 isolate via the - * UNSAFE_EVAL binding, so we can't perform a real escape attempt in Node. What - * we verify here instead is structural — the wrapper must not let user inputs - * break out of their intended quoting/scoping. + * worker_loader binding, so we can't perform a real escape attempt in Node. + * What we verify here instead is structural — the wrapper must not let user + * inputs break out of their intended quoting/scoping. */ const benignTool: ToolSchema = { diff --git a/packages/typescript/ai-isolate-cloudflare/tests/isolate-driver.test.ts b/packages/typescript/ai-isolate-cloudflare/tests/isolate-driver.test.ts index 42e2dc2d5..848f0244d 100644 --- a/packages/typescript/ai-isolate-cloudflare/tests/isolate-driver.test.ts +++ b/packages/typescript/ai-isolate-cloudflare/tests/isolate-driver.test.ts @@ -424,8 +424,8 @@ describe('createCloudflareIsolateDriver', () => { ({ status: 'error', error: { - name: 'UnsafeEvalNotAvailable', - message: 'UNSAFE_EVAL binding is not available', + name: 'WorkerLoaderNotAvailable', + message: 'LOADER binding is not available', }, }) as ExecuteResponse, }) @@ -436,8 +436,8 @@ describe('createCloudflareIsolateDriver', () => { const result = await context.execute('return 1') expect(result.success).toBe(false) - expect(result.error?.name).toBe('UnsafeEvalNotAvailable') - expect(result.error?.message).toContain('UNSAFE_EVAL') + expect(result.error?.name).toBe('WorkerLoaderNotAvailable') + expect(result.error?.message).toContain('LOADER') }) it('returns error when Worker returns status: done with success: false', async () => { diff --git a/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts b/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts index 27534e1b9..1b692ef63 100644 --- a/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts +++ b/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts @@ -6,7 +6,21 @@ import workerModule from '../src/worker/index' const worker = workerModule as { fetch: ( request: Request, - env: { UNSAFE_EVAL?: { eval: (code: string) => unknown } }, + env: { + LOADER?: { + load: (options: { + compatibilityDate: string + mainModule: string + modules: Record + globalOutbound?: unknown + env?: Record + }) => { + getEntrypoint: (name?: string) => { + fetch: (request: Request) => Promise + } + } + } + }, ctx: ExecutionContext, ) => Promise } @@ -174,7 +188,7 @@ describe('Worker fetch handler', () => { expect(json).toHaveProperty('error', 'Code is required') }) - it('returns 200 with UnsafeEvalNotAvailable when env has no UNSAFE_EVAL', async () => { + it('returns 200 with WorkerLoaderNotAvailable when env has no LOADER', async () => { const request = new Request('https://worker.test/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -188,11 +202,149 @@ describe('Worker fetch handler', () => { expect(response.status).toBe(200) const json = await response.json() expect(json.status).toBe('error') - expect(json.error.name).toBe('UnsafeEvalNotAvailable') - expect(json.error.message).toContain('UNSAFE_EVAL') + expect(json.error.name).toBe('WorkerLoaderNotAvailable') + expect(json.error.message).toContain('LOADER') + expect(json.error.message).toContain('worker_loaders') expect(json.error.message).toContain('wrangler.toml') - // No longer steers users to Workers for Platforms - expect(json.error.message).not.toContain('Workers for Platforms') + }) + + it('exercises the LOADER.load → getEntrypoint → fetch chain on the happy path', async () => { + // Capture mock state for post-fetch assertions. Asserting inside the + // synchronous `load()` mock would be swallowed by the outer worker's + // try/catch and surface as a generic 500, masking the real failure. + let loadCalled = false + type LoadOptions = { + compatibilityDate: string + mainModule: string + modules: Record + globalOutbound?: unknown + env?: Record + } + let capturedOptions: LoadOptions | null = null + const env = { + LOADER: { + load: (options: LoadOptions) => { + loadCalled = true + capturedOptions = options + return { + getEntrypoint: () => ({ + fetch: (_req: Request) => + Promise.resolve( + new Response( + JSON.stringify({ + status: 'done', + success: true, + value: 42, + logs: ['hello from sandbox'], + }), + { headers: { 'Content-Type': 'application/json' } }, + ), + ), + }), + } + }, + }, + } + + const request = new Request('https://worker.test/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: 'return 42', tools: [], timeout: 5000 }), + }) + const response = await worker.fetch(request, env, mockExecutionContext) + + expect(loadCalled).toBe(true) + expect(capturedOptions).not.toBeNull() + expect(capturedOptions!.mainModule).toBe('main.js') + expect(capturedOptions!.modules).toHaveProperty('main.js') + expect(capturedOptions!.modules['main.js']).toContain('export default') + expect(capturedOptions!.globalOutbound).toBeNull() + expect(response.status).toBe(200) + const json = await response.json() + expect(json.status).toBe('done') + expect(json.success).toBe(true) + expect(json.value).toBe(42) + expect(json.logs).toEqual(['hello from sandbox']) + }) + + it('forwards need_tools status from the loaded Worker back to the driver', async () => { + const env = { + LOADER: { + load: () => ({ + getEntrypoint: () => ({ + fetch: async () => + new Response( + JSON.stringify({ + status: 'need_tools', + toolCalls: [{ id: 'tc_0', name: 'fetchData', args: {} }], + logs: [], + }), + { headers: { 'Content-Type': 'application/json' } }, + ), + }), + }), + }, + } + + const request = new Request('https://worker.test/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + code: 'return await fetchData({})', + tools: [{ name: 'fetchData', description: 'd', inputSchema: {} }], + }), + }) + const response = await worker.fetch(request, env, mockExecutionContext) + + expect(response.status).toBe(200) + const json = await response.json() + expect(json.status).toBe('need_tools') + expect(json.toolCalls).toHaveLength(1) + expect(json.toolCalls[0].name).toBe('fetchData') + expect(typeof json.continuationId).toBe('string') + }) + + it('returns TimeoutError when entrypoint.fetch exceeds timeout', async () => { + // Capture the AbortSignal seen by the loaded Worker so we can assert that + // the outer worker's AbortController actually fires `abort` on timeout. + // (Request.signal is always non-null per spec, so `not.toBeNull()` would + // be trivially true and prove nothing.) + let receivedSignal: AbortSignal | null = null + const env = { + LOADER: { + load: () => ({ + getEntrypoint: () => ({ + fetch: (req: Request) => + new Promise((_resolve, reject) => { + receivedSignal = req.signal + req.signal.addEventListener('abort', () => { + reject(new Error('aborted')) + }) + // Never resolves on its own; relies on AbortSignal. + }), + }), + }), + }, + } + + const request = new Request('https://worker.test/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + code: 'while(true){}', + tools: [], + timeout: 50, + }), + }) + const response = await worker.fetch(request, env, mockExecutionContext) + expect(receivedSignal).not.toBeNull() + expect(receivedSignal!.aborted).toBe(true) + + expect(response.status).toBe(200) + const json = await response.json() + expect(json.status).toBe('error') + expect(json.error.name).toBe('TimeoutError') + expect(json.error.message).toContain('50ms') }) it('returns 500 with RequestError when body is invalid JSON', async () => { diff --git a/packages/typescript/ai-isolate-cloudflare/wrangler.toml b/packages/typescript/ai-isolate-cloudflare/wrangler.toml index 39dab3696..87752bad8 100644 --- a/packages/typescript/ai-isolate-cloudflare/wrangler.toml +++ b/packages/typescript/ai-isolate-cloudflare/wrangler.toml @@ -1,22 +1,24 @@ #:schema node_modules/wrangler/config-schema.json # Cloudflare Worker configuration for Code Mode execution -# Run locally: pnpm dev:worker (or wrangler dev) -# Deploy: pnpm deploy:worker (or wrangler deploy) +# Run locally: wrangler dev (Dynamic Workers binding works in local workerd +# on the Free plan; deploy/--remote require Paid) +# Deploy: wrangler deploy name = "tanstack-ai-code-mode" main = "src/worker/index.ts" -compatibility_date = "2024-12-01" +compatibility_date = "2026-05-01" compatibility_flags = ["nodejs_compat"] -# UnsafeEval binding - enables dynamic code execution inside the Worker's V8 isolate. -# Works in both local dev (wrangler dev) and production deployments where the -# Cloudflare account has the unsafe_eval binding enabled. Because this lets the -# Worker evaluate arbitrary JavaScript, protect the Worker's public endpoint -# with authentication and rate limiting before deploying. -[[unsafe.bindings]] -name = "UNSAFE_EVAL" -type = "unsafe_eval" +# worker_loader (Dynamic Workers) binding — loads LLM-generated code into a +# fresh V8 isolate via env.LOADER.load(...). Replaces the previous +# `unsafe_eval` binding, which Cloudflare gates for all customer prod +# deploys. Workers Paid plan required to deploy or run --remote; local +# wrangler dev works on the Free plan. Because this Worker evaluates +# arbitrary JavaScript, protect the public endpoint with authentication and +# rate limiting before deploying. +[[worker_loaders]] +binding = "LOADER" # Local development settings [dev] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9192e0e4..30003271c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -589,7 +589,7 @@ importers: version: 0.561.0(react@19.2.3) nitro: specifier: latest - version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@17.2.3)(giget@2.0.0)(jiti@2.6.1)(rollup@4.60.1)(vite@7.3.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@17.2.3)(giget@2.0.0)(jiti@2.6.1)(miniflare@4.20260504.0)(rollup@4.60.1)(vite@7.3.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -1245,15 +1245,9 @@ importers: '@vitest/coverage-v8': specifier: 4.0.14 version: 4.0.14(vitest@4.1.4) - esbuild: - specifier: ^0.25.12 - version: 0.25.12 - miniflare: - specifier: ^4.20260305.0 - version: 4.20260317.0 wrangler: - specifier: ^4.19.1 - version: 4.75.0(@cloudflare/workers-types@4.20260317.1) + specifier: ^4.88.0 + version: 4.88.0(@cloudflare/workers-types@4.20260317.1) packages/typescript/ai-isolate-node: dependencies: @@ -2141,49 +2135,49 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@cloudflare/kv-asset-handler@0.4.1': - resolution: {integrity: sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==} - engines: {node: '>=18.0.0'} - '@cloudflare/kv-asset-handler@0.4.2': resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} engines: {node: '>=18.0.0'} - '@cloudflare/unenv-preset@2.15.0': - resolution: {integrity: sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw==} + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} peerDependencies: unenv: 2.0.0-rc.24 - workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 + workerd: '>1.20260305.0 <2.0.0-0' peerDependenciesMeta: workerd: optional: true - '@cloudflare/workerd-darwin-64@1.20260317.1': - resolution: {integrity: sha512-8hjh3sPMwY8M/zedq3/sXoA2Q4BedlGufn3KOOleIG+5a4ReQKLlUah140D7J6zlKmYZAFMJ4tWC7hCuI/s79g==} + '@cloudflare/workerd-darwin-64@1.20260504.1': + resolution: {integrity: sha512-IOMjYoftNRXabFt+QzY2Bo2mR2TNl8xsGvE0HnQ+K0S2c61VOUGUkr9gpJjnwrJ65yA9Qed4xfg0RRqXHO+nfA==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260317.1': - resolution: {integrity: sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg==} + '@cloudflare/workerd-darwin-arm64@1.20260504.1': + resolution: {integrity: sha512-7iMXxIU0N5KklZpQm2kuwTm0XtrpHXNqhejJyGquky8gSTnm31zBdutjMekH8VRr6ckbvZIl6lvqXzXdfOEojg==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20260317.1': - resolution: {integrity: sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug==} + '@cloudflare/workerd-linux-64@1.20260504.1': + resolution: {integrity: sha512-YLB0EH5FQV++oWlalFgPF3p2Bp3dn/D6RWNMw0ukEC8gKnNX6o61A+dlFUl8hRD35ja1zKRxGFUojs4U2+MoJA==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260317.1': - resolution: {integrity: sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw==} + '@cloudflare/workerd-linux-arm64@1.20260504.1': + resolution: {integrity: sha512-FAh/82jDXDArfn9xDih6f/IJfF2SHXBb4nFeQAyHyvXrn18zM6Q3yl2Vj0U7LybbNbmu7TNGghwaM2NoSQS+0A==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20260317.1': - resolution: {integrity: sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ==} + '@cloudflare/workerd-windows-64@1.20260504.1': + resolution: {integrity: sha512-QUg/B3dfrK/KHHHhiJzdkLkTg5mG7lA3t8iplbBoUa3XKCLOHOOXhbU4WSYlLqg8YnsQ6XLZ1HVA99fmZhJh7A==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -9386,9 +9380,9 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} - miniflare@4.20260317.0: - resolution: {integrity: sha512-xuwk5Kjv+shi5iUBAdCrRl9IaWSGnTU8WuTQzsUS2GlSDIMCJuu8DiF/d9ExjMXYiQG5ml+k9SVKnMj8cRkq0w==} - engines: {node: '>=18.0.0'} + miniflare@4.20260504.0: + resolution: {integrity: sha512-HeI/HLx+rbeo/UB4qb6NsNcFdUVD7xDzyCexZJTVtFMlfpfexUKEDmdeTRRpzeHrJseZFGua+v9JO1kfPublUw==} + engines: {node: '>=22.0.0'} hasBin: true minimatch@10.1.1: @@ -11147,8 +11141,8 @@ packages: resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==} engines: {node: '>=20.18.1'} - undici@7.24.4: - resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} + undici@7.24.8: + resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} engines: {node: '>=20.18.1'} unenv@1.10.0: @@ -11883,17 +11877,17 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - workerd@1.20260317.1: - resolution: {integrity: sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g==} + workerd@1.20260504.1: + resolution: {integrity: sha512-AQTXSHbYNP9tLPgJNn0TmizyE4aDh2VuZZXlTAL0uu4fbCY436NAnQSJIzZbaFHM3DnAtVs9G8tkiJztSdYqDg==} engines: {node: '>=16'} hasBin: true - wrangler@4.75.0: - resolution: {integrity: sha512-Efk1tcnm4eduBYpH1sSjMYydXMnIFPns/qABI3+fsbDrUk5GksNYX8nYGVP4sFygvGPO7kJc36YJKB5ooA7JAg==} - engines: {node: '>=20.0.0'} + wrangler@4.88.0: + resolution: {integrity: sha512-f470QwbeT/JM1S0duq+sLtkss7UBxIFDtYHgujv9tdQUyA/dLGDq51am0rqrsuFtCi97lTM1P5sqtt8xra1AlA==} + engines: {node: '>=22.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20260317.1 + '@cloudflare/workers-types': ^4.20260504.1 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -12425,31 +12419,29 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@cloudflare/kv-asset-handler@0.4.1': - dependencies: - mime: 3.0.0 - '@cloudflare/kv-asset-handler@0.4.2': {} - '@cloudflare/unenv-preset@2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1)': + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260504.1)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: - workerd: 1.20260317.1 + workerd: 1.20260504.1 - '@cloudflare/workerd-darwin-64@1.20260317.1': + '@cloudflare/workerd-darwin-64@1.20260504.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260317.1': + '@cloudflare/workerd-darwin-arm64@1.20260504.1': optional: true - '@cloudflare/workerd-linux-64@1.20260317.1': + '@cloudflare/workerd-linux-64@1.20260504.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260317.1': + '@cloudflare/workerd-linux-arm64@1.20260504.1': optional: true - '@cloudflare/workerd-windows-64@1.20260317.1': + '@cloudflare/workerd-windows-64@1.20260504.1': optional: true '@cloudflare/workers-types@4.20260317.1': {} @@ -18449,12 +18441,14 @@ snapshots: env-paths@2.2.1: {} - env-runner@0.1.7: + env-runner@0.1.7(miniflare@4.20260504.0): dependencies: crossws: 0.4.5(srvx@0.11.15) exsolve: 1.0.8 httpxy: 0.5.1 srvx: 0.11.15 + optionalDependencies: + miniflare: 4.20260504.0 error-ex@1.3.4: dependencies: @@ -20583,12 +20577,12 @@ snapshots: mimic-fn@4.0.0: {} - miniflare@4.20260317.0: + miniflare@4.20260504.0: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.34.5 - undici: 7.24.4 - workerd: 1.20260317.1 + undici: 7.24.8 + workerd: 1.20260504.1 ws: 8.18.0 youch: 4.1.0-beta.10 transitivePeerDependencies: @@ -20729,12 +20723,12 @@ snapshots: - sqlite3 - uploadthing - nitro@3.0.260415-beta(chokidar@5.0.0)(dotenv@17.2.3)(giget@2.0.0)(jiti@2.6.1)(rollup@4.60.1)(vite@7.3.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + nitro@3.0.260415-beta(chokidar@5.0.0)(dotenv@17.2.3)(giget@2.0.0)(jiti@2.6.1)(miniflare@4.20260504.0)(rollup@4.60.1)(vite@7.3.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: consola: 3.4.2 crossws: 0.4.5(srvx@0.11.15) db0: 0.3.4 - env-runner: 0.1.7 + env-runner: 0.1.7(miniflare@4.20260504.0) h3: 2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)) hookable: 6.1.1 nf3: 0.3.16 @@ -20784,7 +20778,7 @@ snapshots: nitropack@2.12.9(rolldown@1.0.0-rc.17): dependencies: - '@cloudflare/kv-asset-handler': 0.4.1 + '@cloudflare/kv-asset-handler': 0.4.2 '@rollup/plugin-alias': 5.1.1(rollup@4.57.1) '@rollup/plugin-commonjs': 28.0.9(rollup@4.57.1) '@rollup/plugin-inject': 5.0.5(rollup@4.57.1) @@ -22955,7 +22949,7 @@ snapshots: undici@7.21.0: {} - undici@7.24.4: {} + undici@7.24.8: {} unenv@1.10.0: dependencies: @@ -23737,24 +23731,24 @@ snapshots: word-wrap@1.2.5: {} - workerd@1.20260317.1: + workerd@1.20260504.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260317.1 - '@cloudflare/workerd-darwin-arm64': 1.20260317.1 - '@cloudflare/workerd-linux-64': 1.20260317.1 - '@cloudflare/workerd-linux-arm64': 1.20260317.1 - '@cloudflare/workerd-windows-64': 1.20260317.1 + '@cloudflare/workerd-darwin-64': 1.20260504.1 + '@cloudflare/workerd-darwin-arm64': 1.20260504.1 + '@cloudflare/workerd-linux-64': 1.20260504.1 + '@cloudflare/workerd-linux-arm64': 1.20260504.1 + '@cloudflare/workerd-windows-64': 1.20260504.1 - wrangler@4.75.0(@cloudflare/workers-types@4.20260317.1): + wrangler@4.88.0(@cloudflare/workers-types@4.20260317.1): dependencies: - '@cloudflare/kv-asset-handler': 0.4.2 - '@cloudflare/unenv-preset': 2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1) + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260504.1) blake3-wasm: 2.1.5 esbuild: 0.27.3 - miniflare: 4.20260317.0 + miniflare: 4.20260504.0 path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 - workerd: 1.20260317.1 + workerd: 1.20260504.1 optionalDependencies: '@cloudflare/workers-types': 4.20260317.1 fsevents: 2.3.3