diff --git a/.changeset/cloudflare-world.md b/.changeset/cloudflare-world.md new file mode 100644 index 0000000000..8ff851b501 --- /dev/null +++ b/.changeset/cloudflare-world.md @@ -0,0 +1,5 @@ +--- +"@workflow/world-cloudflare": patch +--- + +Add Cloudflare Workers world implementation using Durable Objects, D1, and Cloudflare Queues diff --git a/packages/world-cloudflare/README.md b/packages/world-cloudflare/README.md new file mode 100644 index 0000000000..2c02cfabf7 --- /dev/null +++ b/packages/world-cloudflare/README.md @@ -0,0 +1,88 @@ +# @workflow/world-cloudflare + +A [Workflow DevKit](https://useworkflow.dev) world implementation for Cloudflare Workers, using **Durable Objects** for per-run state, **D1** for cross-run queries, and **Cloudflare Queues** for message dispatch. + +## Architecture + +- **Durable Objects**: Each workflow run maps to a single Durable Object instance. All run state (events, steps, hooks, waits, stream chunks) lives in the DO's transactional storage. +- **D1 (SQLite)**: A lightweight global index enables cross-run queries like `runs.list()` and `hooks.getByToken()`. The DO writes index updates to D1 after each mutation. +- **Cloudflare Queues**: Workflow and step invocations are dispatched through Cloudflare Queues for reliable, at-least-once delivery. + +## Setup + +### 1. Install + +```bash +pnpm add @workflow/world-cloudflare +``` + +### 2. Configure bindings + +Copy `wrangler.example.toml` and configure your D1 database ID: + +```toml +[durable_objects] +bindings = [{ name = "WORKFLOW_RUNS", class_name = "WorkflowRunDO" }] + +[[d1_databases]] +binding = "WORKFLOW_DB" +database_name = "workflow" +database_id = "" + +[[queues.producers]] +binding = "WORKFLOW_QUEUE" +queue = "workflow-jobs" + +[[queues.consumers]] +queue = "workflow-jobs" +max_batch_size = 10 +max_retries = 3 +``` + +### 3. Run D1 migration + +```ts +import { migrate } from '@workflow/world-cloudflare/d1'; + +// Run once during setup or in a migration script +await migrate(env.WORKFLOW_DB); +``` + +### 4. Create the world + +```ts +import { createWorld, WorkflowRunDO } from '@workflow/world-cloudflare'; + +// Re-export the DO class so Cloudflare can instantiate it +export { WorkflowRunDO }; + +export default { + async fetch(request, env) { + const world = createWorld({ + db: env.WORKFLOW_DB, + runs: env.WORKFLOW_RUNS, + queue: env.WORKFLOW_QUEUE, + }); + // Use world with your workflow runtime... + }, + + async queue(batch, env) { + const world = createWorld({ + db: env.WORKFLOW_DB, + runs: env.WORKFLOW_RUNS, + queue: env.WORKFLOW_QUEUE, + }); + await world.handleQueueBatch(batch); + }, +}; +``` + +## Limitations + +- `events.listByCorrelationId()` requires the `runId` context (inherent to DO-per-run architecture) +- `steps.get()` requires `runId` (cannot look up steps without knowing which DO to query) +- `readFromStream()` expects stream names in `runId:streamName` format + +## License + +Apache-2.0 diff --git a/packages/world-cloudflare/package.json b/packages/world-cloudflare/package.json new file mode 100644 index 0000000000..e4e48386cf --- /dev/null +++ b/packages/world-cloudflare/package.json @@ -0,0 +1,61 @@ +{ + "name": "@workflow/world-cloudflare", + "version": "0.1.0", + "description": "Cloudflare Workers world implementation for Workflow DevKit using Durable Objects, D1, and Cloudflare Queues", + "type": "module", + "main": "dist/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./durable-object": { + "types": "./dist/durable-object.d.ts", + "default": "./dist/durable-object.js" + }, + "./d1": { + "types": "./dist/d1.d.ts", + "default": "./dist/d1.js" + } + }, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/vercel/workflow.git", + "directory": "packages/world-cloudflare" + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "tsc --build --clean && rm -rf dist", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@workflow/errors": "workspace:*", + "@workflow/world": "workspace:*", + "@workflow/world-local": "workspace:*", + "ulid": "catalog:", + "zod": "catalog:" + }, + "devDependencies": { + "@cloudflare/workers-types": "4.20250312.0", + "@types/node": "catalog:", + "@workflow/tsconfig": "workspace:*", + "vitest": "catalog:" + }, + "keywords": [ + "cloudflare", + "workers", + "durable-objects", + "workflow" + ], + "author": "", + "packageManager": "pnpm@10.15.1" +} diff --git a/packages/world-cloudflare/src/config.ts b/packages/world-cloudflare/src/config.ts new file mode 100644 index 0000000000..59e84a3a2d --- /dev/null +++ b/packages/world-cloudflare/src/config.ts @@ -0,0 +1,10 @@ +export interface CloudflareWorldConfig { + /** D1 database binding for the global index */ + db: D1Database; + /** Durable Object namespace binding for WorkflowRunDO */ + runs: DurableObjectNamespace; + /** Cloudflare Queue binding for message dispatch */ + queue: globalThis.Queue; + /** Optional: port for local executor (same pattern as world-postgres) */ + port?: number; +} diff --git a/packages/world-cloudflare/src/d1.test.ts b/packages/world-cloudflare/src/d1.test.ts new file mode 100644 index 0000000000..c4487e1fdb --- /dev/null +++ b/packages/world-cloudflare/src/d1.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + deleteHookIndex, + deleteHooksForRunIndex, + insertHookIndex, + upsertRunIndex, +} from './d1.js'; + +function createMockDb() { + const run = vi.fn().mockResolvedValue({ success: true }); + const bind = vi.fn().mockReturnValue({ run }); + const prepare = vi.fn().mockReturnValue({ bind }); + const exec = vi.fn().mockResolvedValue({ success: true }); + return { prepare, exec, run, bind } as unknown as D1Database & { + run: ReturnType; + bind: ReturnType; + }; +} + +describe('d1 index helpers', () => { + it('upserts a run index row', async () => { + const db = createMockDb(); + await upsertRunIndex(db, { + runId: 'wrun_01ABC', + workflowName: 'my-workflow', + status: 'running', + deploymentId: 'cloudflare', + specVersion: 2, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:01.000Z', + startedAt: '2024-01-01T00:00:00.500Z', + }); + expect(db.prepare).toHaveBeenCalledWith( + expect.stringContaining('INSERT INTO workflow_runs_index') + ); + }); + + it('inserts a hook index row', async () => { + const db = createMockDb(); + await insertHookIndex(db, { + hookId: 'hook_01ABC', + runId: 'wrun_01ABC', + token: 'tok_secret', + ownerId: 'owner1', + projectId: 'proj1', + environment: 'production', + createdAt: '2024-01-01T00:00:00.000Z', + isWebhook: true, + specVersion: 2, + }); + expect(db.prepare).toHaveBeenCalledWith( + expect.stringContaining('INSERT OR IGNORE INTO workflow_hooks_index') + ); + }); + + it('deletes a hook index row', async () => { + const db = createMockDb(); + await deleteHookIndex(db, 'hook_01ABC'); + expect(db.prepare).toHaveBeenCalledWith( + expect.stringContaining('DELETE FROM workflow_hooks_index WHERE hook_id') + ); + }); + + it('deletes all hooks for a run from the index', async () => { + const db = createMockDb(); + await deleteHooksForRunIndex(db, 'wrun_01ABC'); + expect(db.prepare).toHaveBeenCalledWith( + expect.stringContaining('DELETE FROM workflow_hooks_index WHERE run_id') + ); + }); +}); diff --git a/packages/world-cloudflare/src/d1.ts b/packages/world-cloudflare/src/d1.ts new file mode 100644 index 0000000000..1f03d778bc --- /dev/null +++ b/packages/world-cloudflare/src/d1.ts @@ -0,0 +1,150 @@ +/** + * D1 index schema for cross-run queries. + * + * The D1 database stores lightweight index data only (no input/output/event payloads). + * The source of truth for all run data lives in Durable Objects. + */ + +export const MIGRATION_SQL = ` +CREATE TABLE IF NOT EXISTS workflow_runs_index ( + run_id TEXT PRIMARY KEY, + workflow_name TEXT NOT NULL, + status TEXT NOT NULL, + deployment_id TEXT NOT NULL, + spec_version INTEGER, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + started_at TEXT, + completed_at TEXT, + expired_at TEXT +); +CREATE INDEX IF NOT EXISTS idx_runs_name ON workflow_runs_index(workflow_name); +CREATE INDEX IF NOT EXISTS idx_runs_status ON workflow_runs_index(status); + +CREATE TABLE IF NOT EXISTS workflow_hooks_index ( + hook_id TEXT PRIMARY KEY, + run_id TEXT NOT NULL, + token TEXT NOT NULL UNIQUE, + owner_id TEXT NOT NULL, + project_id TEXT NOT NULL, + environment TEXT NOT NULL, + created_at TEXT NOT NULL, + is_webhook INTEGER DEFAULT 1, + spec_version INTEGER +); +CREATE INDEX IF NOT EXISTS idx_hooks_run ON workflow_hooks_index(run_id); +CREATE INDEX IF NOT EXISTS idx_hooks_token ON workflow_hooks_index(token); +`; + +/** + * Run the D1 index migration. Safe to call multiple times (uses IF NOT EXISTS). + */ +export async function migrate(db: D1Database): Promise { + const statements = MIGRATION_SQL.split(';') + .map((s) => s.trim()) + .filter(Boolean); + + for (const sql of statements) { + await db.exec(`${sql};`); + } +} + +/** Upsert a run's index row in D1 */ +export async function upsertRunIndex( + db: D1Database, + run: { + runId: string; + workflowName: string; + status: string; + deploymentId: string; + specVersion?: number; + createdAt: string; + updatedAt: string; + startedAt?: string; + completedAt?: string; + expiredAt?: string; + } +): Promise { + await db + .prepare( + `INSERT INTO workflow_runs_index + (run_id, workflow_name, status, deployment_id, spec_version, created_at, updated_at, started_at, completed_at, expired_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(run_id) DO UPDATE SET + status = excluded.status, + updated_at = excluded.updated_at, + started_at = COALESCE(excluded.started_at, started_at), + completed_at = COALESCE(excluded.completed_at, completed_at), + expired_at = COALESCE(excluded.expired_at, expired_at)` + ) + .bind( + run.runId, + run.workflowName, + run.status, + run.deploymentId, + run.specVersion ?? null, + run.createdAt, + run.updatedAt, + run.startedAt ?? null, + run.completedAt ?? null, + run.expiredAt ?? null + ) + .run(); +} + +/** Insert a hook into the D1 index */ +export async function insertHookIndex( + db: D1Database, + hook: { + hookId: string; + runId: string; + token: string; + ownerId: string; + projectId: string; + environment: string; + createdAt: string; + isWebhook?: boolean; + specVersion?: number; + } +): Promise { + await db + .prepare( + `INSERT OR IGNORE INTO workflow_hooks_index + (hook_id, run_id, token, owner_id, project_id, environment, created_at, is_webhook, spec_version) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) + .bind( + hook.hookId, + hook.runId, + hook.token, + hook.ownerId, + hook.projectId, + hook.environment, + hook.createdAt, + hook.isWebhook !== false ? 1 : 0, + hook.specVersion ?? null + ) + .run(); +} + +/** Delete a hook from the D1 index */ +export async function deleteHookIndex( + db: D1Database, + hookId: string +): Promise { + await db + .prepare('DELETE FROM workflow_hooks_index WHERE hook_id = ?') + .bind(hookId) + .run(); +} + +/** Delete all hooks for a run from the D1 index */ +export async function deleteHooksForRunIndex( + db: D1Database, + runId: string +): Promise { + await db + .prepare('DELETE FROM workflow_hooks_index WHERE run_id = ?') + .bind(runId) + .run(); +} diff --git a/packages/world-cloudflare/src/durable-object.ts b/packages/world-cloudflare/src/durable-object.ts new file mode 100644 index 0000000000..36f0c9a556 --- /dev/null +++ b/packages/world-cloudflare/src/durable-object.ts @@ -0,0 +1,1244 @@ +import { + HookNotFoundError, + RunNotSupportedError, + WorkflowAPIError, +} from '@workflow/errors'; +import type { + Event, + EventResult, + Hook, + PaginatedResponse, + ResolveData, + Step, + StepWithoutData, + Wait, + WorkflowRun, + WorkflowRunWithoutData, +} from '@workflow/world'; +import { + EventSchema, + HookSchema, + isLegacySpecVersion, + requiresNewerWorld, + SPEC_VERSION_CURRENT, + StepSchema, + validateUlidTimestamp, + WorkflowRunSchema, +} from '@workflow/world'; +import { monotonicFactory } from 'ulid'; +import { + deleteHookIndex, + deleteHooksForRunIndex, + insertHookIndex, + upsertRunIndex, +} from './d1.js'; +import { toISOOrUndef } from './util.js'; + +const ulid = monotonicFactory(); + +// ============================================================ +// Data filtering helpers (strip data for resolveData='none') +// ============================================================ + +function filterRunData( + run: WorkflowRun, + resolveData: ResolveData +): WorkflowRun | WorkflowRunWithoutData { + if (resolveData === 'none') { + const { input: _, output: __, ...rest } = run; + return { input: undefined, output: undefined, ...rest }; + } + return run; +} + +function filterStepData( + step: Step, + resolveData: ResolveData +): Step | StepWithoutData { + if (resolveData === 'none') { + const { input: _, output: __, ...rest } = step; + return { input: undefined, output: undefined, ...rest }; + } + return step; +} + +function filterHookData(hook: Hook, resolveData: ResolveData): Hook { + if (resolveData === 'none' && 'metadata' in hook) { + const { metadata: _, ...rest } = hook; + return { metadata: undefined, ...rest }; + } + return hook; +} + +function filterEventData(event: Event, resolveData: ResolveData): Event { + if (resolveData === 'none' && 'eventData' in event) { + const { eventData: _, ...rest } = event; + return rest as Event; + } + return event; +} + +// ============================================================ +// DO storage key prefixes +// ============================================================ +const KEY = { + run: 'run', + event: (id: string) => `evt:${id}`, + step: (id: string) => `step:${id}`, + hook: (id: string) => `hook:${id}`, + hookToken: (token: string) => `hook_tok:${token}`, + wait: (id: string) => `wait:${id}`, + streamChunk: (streamId: string, chunkId: string) => + `strm:${streamId}:chnk:${chunkId}`, + streamMeta: (streamId: string) => `strm:${streamId}:meta`, + streamIndex: 'strm_idx', +} as const; + +interface StreamChunkData { + data: number[]; + eof: boolean; +} + +// ============================================================ +// WorkflowRunDO — one instance per workflow run +// ============================================================ + +interface Env { + WORKFLOW_DB: D1Database; +} + +export class WorkflowRunDO implements DurableObject { + private storage: DurableObjectStorage; + private db: D1Database; + + constructor(state: DurableObjectState, env: Env) { + this.storage = state.storage; + this.db = env.WORKFLOW_DB; + } + + async fetch(request: Request): Promise { + const url = new URL(request.url); + const method = request.method; + const path = url.pathname; + + try { + // POST /events — create event + if (method === 'POST' && path === '/events') { + const body = (await request.json()) as { + runId: string | null; + data: any; + params?: any; + }; + const result = await this.createEvent( + body.runId, + body.data, + body.params + ); + return Response.json(result); + } + + // GET /run — get run entity + if (method === 'GET' && path === '/run') { + const resolveData = + (url.searchParams.get('resolveData') as ResolveData) ?? 'all'; + const run = await this.getRun(resolveData); + return Response.json(run); + } + + // GET /events — list events + if (method === 'GET' && path === '/events') { + const result = await this.listEvents({ + limit: parseInt(url.searchParams.get('limit') ?? '100', 10), + cursor: url.searchParams.get('cursor') ?? undefined, + sortOrder: + (url.searchParams.get('sortOrder') as 'asc' | 'desc') ?? 'asc', + resolveData: + (url.searchParams.get('resolveData') as ResolveData) ?? 'all', + }); + return Response.json(result); + } + + // GET /events/:eventId — get single event + if (method === 'GET' && path.startsWith('/events/')) { + const eventId = path.slice('/events/'.length); + const resolveData = + (url.searchParams.get('resolveData') as ResolveData) ?? 'all'; + const event = await this.getEvent(eventId, resolveData); + return Response.json(event); + } + + // GET /events-by-correlation — list events by correlation ID + if (method === 'GET' && path === '/events-by-correlation') { + const result = await this.listEventsByCorrelationId({ + correlationId: url.searchParams.get('correlationId') ?? '', + limit: parseInt(url.searchParams.get('limit') ?? '100', 10), + cursor: url.searchParams.get('cursor') ?? undefined, + sortOrder: + (url.searchParams.get('sortOrder') as 'asc' | 'desc') ?? 'asc', + resolveData: + (url.searchParams.get('resolveData') as ResolveData) ?? 'all', + }); + return Response.json(result); + } + + // GET /steps — list steps + if (method === 'GET' && path === '/steps') { + const result = await this.listSteps({ + limit: parseInt(url.searchParams.get('limit') ?? '20', 10), + cursor: url.searchParams.get('cursor') ?? undefined, + resolveData: + (url.searchParams.get('resolveData') as ResolveData) ?? 'all', + }); + return Response.json(result); + } + + // GET /steps/:stepId — get single step + if (method === 'GET' && path.startsWith('/steps/')) { + const stepId = path.slice('/steps/'.length); + const resolveData = + (url.searchParams.get('resolveData') as ResolveData) ?? 'all'; + const step = await this.getStep(stepId, resolveData); + return Response.json(step); + } + + // GET /hooks — list hooks + if (method === 'GET' && path === '/hooks') { + const result = await this.listHooks({ + limit: parseInt(url.searchParams.get('limit') ?? '100', 10), + cursor: url.searchParams.get('cursor') ?? undefined, + sortOrder: + (url.searchParams.get('sortOrder') as 'asc' | 'desc') ?? 'asc', + resolveData: + (url.searchParams.get('resolveData') as ResolveData) ?? 'all', + }); + return Response.json(result); + } + + // GET /hooks/:hookId — get single hook + if (method === 'GET' && path.startsWith('/hooks/')) { + const hookId = path.slice('/hooks/'.length); + const resolveData = + (url.searchParams.get('resolveData') as ResolveData) ?? 'all'; + const hook = await this.getHook(hookId, resolveData); + return Response.json(hook); + } + + // POST /streams/:name/write — write stream chunk + if (method === 'POST' && path.match(/^\/streams\/[^/]+\/write$/)) { + const name = path.split('/')[2]; + const body = (await request.json()) as { + chunk: number[] | string; + runId: string; + }; + await this.writeStreamChunk(name, body.chunk, body.runId); + return new Response(null, { status: 204 }); + } + + // POST /streams/:name/write-multi — write multiple stream chunks + if (method === 'POST' && path.match(/^\/streams\/[^/]+\/write-multi$/)) { + const name = path.split('/')[2]; + const body = (await request.json()) as { + chunks: (number[] | string)[]; + runId: string; + }; + await this.writeStreamChunks(name, body.chunks, body.runId); + return new Response(null, { status: 204 }); + } + + // POST /streams/:name/close — close stream + if (method === 'POST' && path.match(/^\/streams\/[^/]+\/close$/)) { + const name = path.split('/')[2]; + const body = (await request.json()) as { runId: string }; + await this.closeStream(name, body.runId); + return new Response(null, { status: 204 }); + } + + // GET /streams/:name/read — read stream + if (method === 'GET' && path.match(/^\/streams\/[^/]+\/read$/)) { + const name = path.split('/')[2]; + const startIndex = parseInt( + url.searchParams.get('startIndex') ?? '0', + 10 + ); + const stream = await this.readStream(name, startIndex); + return new Response(stream, { + headers: { 'Content-Type': 'application/octet-stream' }, + }); + } + + // GET /streams — list stream names + if (method === 'GET' && path === '/streams') { + const names = await this.listStreams(); + return Response.json(names); + } + + return new Response('Not Found', { status: 404 }); + } catch (err: any) { + const status = err.status ?? err.statusCode ?? 500; + return Response.json( + { + error: err.message, + code: err.code, + meta: err.meta, + }, + { status } + ); + } + } + + // ============================================================ + // Run CRUD + // ============================================================ + + private async getRun( + resolveData: ResolveData + ): Promise { + const run = await this.storage.get(KEY.run); + if (!run) { + throw new WorkflowAPIError('Run not found', { status: 404 }); + } + const parsed = WorkflowRunSchema.parse(run); + return filterRunData(parsed, resolveData); + } + + // ============================================================ + // Event processing — core logic ported from world-postgres + // ============================================================ + + private async createEvent( + runId: string | null, + data: any, + params?: { resolveData?: ResolveData; v1Compat?: boolean } + ): Promise { + const eventId = `wevt_${ulid()}`; + const resolveData = params?.resolveData ?? 'all'; + + // For run_created: generate runId if not provided + let effectiveRunId: string; + if (data.eventType === 'run_created' && (!runId || runId === '')) { + effectiveRunId = `wrun_${ulid()}`; + } else if (!runId) { + throw new Error('runId is required for non-run_created events'); + } else { + effectiveRunId = runId; + } + + // Validate client-provided runId timestamp + if (data.eventType === 'run_created' && runId && runId !== '') { + const validationError = validateUlidTimestamp(effectiveRunId, 'wrun_'); + if (validationError) { + throw new WorkflowAPIError(validationError, { status: 400 }); + } + } + + const effectiveSpecVersion = data.specVersion ?? SPEC_VERSION_CURRENT; + const now = new Date(); + + let run: WorkflowRun | undefined; + let step: Step | undefined; + let hook: Hook | undefined; + let wait: Wait | undefined; + + const isRunTerminal = (status: string) => + ['completed', 'failed', 'cancelled'].includes(status); + const isStepTerminal = (status: string) => + ['completed', 'failed'].includes(status); + + // ============================================================ + // VALIDATION + // ============================================================ + + let currentRun: WorkflowRun | undefined; + const skipRunValidationEvents = ['step_completed', 'step_retrying']; + if ( + data.eventType !== 'run_created' && + !skipRunValidationEvents.includes(data.eventType) + ) { + currentRun = await this.storage.get(KEY.run); + } + + // Version compatibility check + if (currentRun) { + if (requiresNewerWorld(currentRun.specVersion)) { + throw new RunNotSupportedError( + currentRun.specVersion ?? 0, + SPEC_VERSION_CURRENT + ); + } + + if (isLegacySpecVersion(currentRun.specVersion)) { + return this.handleLegacyEvent( + effectiveRunId, + eventId, + data, + currentRun, + params + ); + } + } + + // Run terminal state validation + if (currentRun && isRunTerminal(currentRun.status)) { + const runTerminalEvents = ['run_started', 'run_completed', 'run_failed']; + + // Idempotent: run_cancelled on already cancelled run + if ( + data.eventType === 'run_cancelled' && + currentRun.status === 'cancelled' + ) { + const event = await this.insertEvent( + effectiveRunId, + eventId, + data, + effectiveSpecVersion + ); + return { + event: filterEventData(event, resolveData), + run: filterRunData(currentRun, resolveData) as WorkflowRun, + }; + } + + if ( + runTerminalEvents.includes(data.eventType) || + data.eventType === 'run_cancelled' + ) { + throw new WorkflowAPIError( + `Cannot transition run from terminal state "${currentRun.status}"`, + { status: 409 } + ); + } + + if ( + data.eventType === 'step_created' || + data.eventType === 'hook_created' || + data.eventType === 'wait_created' + ) { + throw new WorkflowAPIError( + `Cannot create new entities on run in terminal state "${currentRun.status}"`, + { status: 409 } + ); + } + } + + // Step validation for step_started, step_retrying + let validatedStep: Step | undefined; + const stepEventsNeedingValidation = ['step_started', 'step_retrying']; + if ( + stepEventsNeedingValidation.includes(data.eventType) && + data.correlationId + ) { + validatedStep = await this.storage.get( + KEY.step(data.correlationId) + ); + if (!validatedStep) { + throw new WorkflowAPIError(`Step "${data.correlationId}" not found`, { + status: 404, + }); + } + if (isStepTerminal(validatedStep.status)) { + throw new WorkflowAPIError( + `Cannot modify step in terminal state "${validatedStep.status}"`, + { status: 409 } + ); + } + if (currentRun && isRunTerminal(currentRun.status)) { + if (validatedStep.status !== 'running') { + throw new WorkflowAPIError( + `Cannot modify non-running step on run in terminal state "${currentRun.status}"`, + { status: 410 } + ); + } + } + } + + // Hook validation for hook_disposed, hook_received + const hookEventsRequiringExistence = ['hook_disposed', 'hook_received']; + if ( + hookEventsRequiringExistence.includes(data.eventType) && + data.correlationId + ) { + const existingHook = await this.storage.get( + KEY.hook(data.correlationId) + ); + if (!existingHook) { + throw new WorkflowAPIError(`Hook "${data.correlationId}" not found`, { + status: 404, + }); + } + } + + // ============================================================ + // Entity creation/updates + // ============================================================ + + if (data.eventType === 'run_created') { + const eventData = data.eventData as { + deploymentId: string; + workflowName: string; + input: any; + executionContext?: Record; + }; + const newRun: WorkflowRun = WorkflowRunSchema.parse({ + runId: effectiveRunId, + deploymentId: eventData.deploymentId, + workflowName: eventData.workflowName, + specVersion: effectiveSpecVersion, + input: eventData.input, + executionContext: eventData.executionContext, + status: 'pending', + createdAt: now, + updatedAt: now, + }); + await this.storage.put(KEY.run, newRun); + run = newRun; + + // Update D1 index (fire-and-forget) + this.updateRunIndex(newRun); + } + + if (data.eventType === 'run_started') { + const existing = + currentRun ?? (await this.storage.get(KEY.run)); + if (existing) { + const updated: WorkflowRun = WorkflowRunSchema.parse({ + ...existing, + status: 'running', + startedAt: now, + updatedAt: now, + }); + await this.storage.put(KEY.run, updated); + run = updated; + this.updateRunIndex(updated); + } + } + + if (data.eventType === 'run_completed') { + const eventData = data.eventData as { output?: any }; + const existing = + currentRun ?? (await this.storage.get(KEY.run)); + if (existing) { + const updated: WorkflowRun = WorkflowRunSchema.parse({ + ...existing, + status: 'completed', + output: eventData.output, + completedAt: now, + updatedAt: now, + }); + await this.storage.put(KEY.run, updated); + run = updated; + this.updateRunIndex(updated); + await this.disposeAllHooksAndWaits(effectiveRunId); + } + } + + if (data.eventType === 'run_failed') { + const eventData = data.eventData as { + error: any; + errorCode?: string; + }; + const errorMessage = + typeof eventData.error === 'string' + ? eventData.error + : (eventData.error?.message ?? 'Unknown error'); + const existing = + currentRun ?? (await this.storage.get(KEY.run)); + if (existing) { + const updated: WorkflowRun = WorkflowRunSchema.parse({ + ...existing, + status: 'failed', + error: { + message: errorMessage, + stack: eventData.error?.stack, + code: eventData.errorCode, + }, + completedAt: now, + updatedAt: now, + }); + await this.storage.put(KEY.run, updated); + run = updated; + this.updateRunIndex(updated); + await this.disposeAllHooksAndWaits(effectiveRunId); + } + } + + if (data.eventType === 'run_cancelled') { + const existing = + currentRun ?? (await this.storage.get(KEY.run)); + if (existing) { + const updated: WorkflowRun = WorkflowRunSchema.parse({ + ...existing, + status: 'cancelled', + completedAt: now, + updatedAt: now, + }); + await this.storage.put(KEY.run, updated); + run = updated; + this.updateRunIndex(updated); + await this.disposeAllHooksAndWaits(effectiveRunId); + } + } + + if (data.eventType === 'step_created') { + const eventData = data.eventData as { + stepName: string; + input: any; + }; + const newStep: Step = StepSchema.parse({ + runId: effectiveRunId, + stepId: data.correlationId, + stepName: eventData.stepName, + input: eventData.input, + status: 'pending', + attempt: 0, + specVersion: effectiveSpecVersion, + createdAt: now, + updatedAt: now, + }); + // Only create if doesn't exist (onConflictDoNothing equivalent) + const existing = await this.storage.get(KEY.step(data.correlationId)); + if (!existing) { + await this.storage.put(KEY.step(data.correlationId), newStep); + step = newStep; + } + } + + if (data.eventType === 'step_started') { + if (!validatedStep) { + throw new WorkflowAPIError(`Step "${data.correlationId}" not found`, { + status: 404, + }); + } + + // Check retryAfter + if ( + validatedStep.retryAfter && + validatedStep.retryAfter.getTime() > Date.now() + ) { + const err = new WorkflowAPIError( + `Cannot start step "${data.correlationId}": retryAfter timestamp has not been reached yet`, + { status: 425 } + ); + (err as any).meta = { + stepId: data.correlationId, + retryAfter: validatedStep.retryAfter.toISOString(), + }; + throw err; + } + + const isFirstStart = !validatedStep.startedAt; + const updated: Step = StepSchema.parse({ + ...validatedStep, + status: 'running', + attempt: validatedStep.attempt + 1, + ...(isFirstStart ? { startedAt: now } : {}), + retryAfter: undefined, + updatedAt: now, + }); + await this.storage.put(KEY.step(data.correlationId), updated); + step = updated; + } + + if (data.eventType === 'step_completed') { + const eventData = data.eventData as { result?: any }; + const existingStep = await this.storage.get( + KEY.step(data.correlationId) + ); + if (!existingStep) { + throw new WorkflowAPIError(`Step "${data.correlationId}" not found`, { + status: 404, + }); + } + if (isStepTerminal(existingStep.status)) { + throw new WorkflowAPIError( + `Cannot modify step in terminal state "${existingStep.status}"`, + { status: 409 } + ); + } + const updated: Step = StepSchema.parse({ + ...existingStep, + status: 'completed', + output: eventData.result, + completedAt: now, + updatedAt: now, + }); + await this.storage.put(KEY.step(data.correlationId), updated); + step = updated; + } + + if (data.eventType === 'step_failed') { + const eventData = data.eventData as { + error?: any; + stack?: string; + }; + const errorMessage = + typeof eventData.error === 'string' + ? eventData.error + : (eventData.error?.message ?? 'Unknown error'); + const existingStep = await this.storage.get( + KEY.step(data.correlationId) + ); + if (!existingStep) { + throw new WorkflowAPIError(`Step "${data.correlationId}" not found`, { + status: 404, + }); + } + if (isStepTerminal(existingStep.status)) { + throw new WorkflowAPIError( + `Cannot modify step in terminal state "${existingStep.status}"`, + { status: 409 } + ); + } + const updated: Step = StepSchema.parse({ + ...existingStep, + status: 'failed', + error: { message: errorMessage, stack: eventData.stack }, + completedAt: now, + updatedAt: now, + }); + await this.storage.put(KEY.step(data.correlationId), updated); + step = updated; + } + + if (data.eventType === 'step_retrying') { + const eventData = data.eventData as { + error?: any; + stack?: string; + retryAfter?: string; + }; + const errorMessage = + typeof eventData.error === 'string' + ? eventData.error + : (eventData.error?.message ?? 'Unknown error'); + const existingStep = + validatedStep ?? + (await this.storage.get(KEY.step(data.correlationId))); + if (existingStep) { + const updated: Step = StepSchema.parse({ + ...existingStep, + status: 'pending', + error: { message: errorMessage, stack: eventData.stack }, + retryAfter: eventData.retryAfter + ? new Date(eventData.retryAfter) + : undefined, + updatedAt: now, + }); + await this.storage.put(KEY.step(data.correlationId), updated); + step = updated; + } + } + + if (data.eventType === 'hook_created') { + const eventData = data.eventData as { + token: string; + metadata?: any; + isWebhook?: boolean; + }; + + // Check for duplicate token + const existingTokenHookId = await this.storage.get( + KEY.hookToken(eventData.token) + ); + if (existingTokenHookId) { + // Create hook_conflict event + const conflictEventData = { token: eventData.token }; + const conflictEvent = EventSchema.parse({ + eventType: 'hook_conflict', + correlationId: data.correlationId, + eventData: conflictEventData, + runId: effectiveRunId, + eventId, + createdAt: now, + specVersion: effectiveSpecVersion, + }); + await this.storage.put(KEY.event(eventId), conflictEvent); + return { + event: filterEventData(conflictEvent, resolveData), + hook: undefined, + }; + } + + const newHook: Hook = HookSchema.parse({ + runId: effectiveRunId, + hookId: data.correlationId, + token: eventData.token, + metadata: eventData.metadata, + ownerId: '', + projectId: '', + environment: '', + specVersion: effectiveSpecVersion, + isWebhook: eventData.isWebhook, + createdAt: now, + }); + + // Only create if doesn't exist + const existing = await this.storage.get(KEY.hook(data.correlationId)); + if (!existing) { + await this.storage.put(KEY.hook(data.correlationId), newHook); + await this.storage.put( + KEY.hookToken(eventData.token), + data.correlationId + ); + hook = newHook; + + // Update D1 index (fire-and-forget) + insertHookIndex(this.db, { + hookId: data.correlationId, + runId: effectiveRunId, + token: eventData.token, + ownerId: '', + projectId: '', + environment: '', + createdAt: now.toISOString(), + isWebhook: eventData.isWebhook, + specVersion: effectiveSpecVersion, + }).catch(() => {}); + } + } + + if (data.eventType === 'hook_disposed' && data.correlationId) { + const existingHook = await this.storage.get( + KEY.hook(data.correlationId) + ); + if (existingHook) { + await this.storage.delete(KEY.hook(data.correlationId)); + await this.storage.delete(KEY.hookToken(existingHook.token)); + deleteHookIndex(this.db, data.correlationId).catch(() => {}); + } + } + + if (data.eventType === 'wait_created') { + const eventData = data.eventData as { resumeAt?: string }; + const waitId = `${effectiveRunId}-${data.correlationId}`; + const existing = await this.storage.get(KEY.wait(waitId)); + if (existing) { + throw new WorkflowAPIError( + `Wait "${data.correlationId}" already exists`, + { status: 409 } + ); + } + const newWait: Wait = { + waitId, + runId: effectiveRunId, + status: 'waiting', + resumeAt: eventData.resumeAt ? new Date(eventData.resumeAt) : undefined, + createdAt: now, + updatedAt: now, + specVersion: effectiveSpecVersion, + }; + await this.storage.put(KEY.wait(waitId), newWait); + wait = newWait; + } + + if (data.eventType === 'wait_completed') { + const waitId = `${effectiveRunId}-${data.correlationId}`; + const existingWait = await this.storage.get(KEY.wait(waitId)); + if (!existingWait) { + throw new WorkflowAPIError(`Wait "${data.correlationId}" not found`, { + status: 404, + }); + } + if (existingWait.status === 'completed') { + throw new WorkflowAPIError( + `Wait "${data.correlationId}" already completed`, + { status: 409 } + ); + } + const updated: Wait = { + ...existingWait, + status: 'completed', + completedAt: now, + updatedAt: now, + }; + await this.storage.put(KEY.wait(waitId), updated); + wait = updated; + } + + // Insert the event into DO storage + const event = await this.insertEvent( + effectiveRunId, + eventId, + data, + effectiveSpecVersion + ); + + return { + event: filterEventData(event, resolveData), + run: run ? (filterRunData(run, resolveData) as WorkflowRun) : undefined, + step, + hook, + wait, + }; + } + + private async insertEvent( + runId: string, + eventId: string, + data: any, + specVersion: number + ): Promise { + const now = new Date(); + const event = EventSchema.parse({ + ...data, + runId, + eventId, + createdAt: now, + specVersion, + }); + await this.storage.put(KEY.event(eventId), event); + return event; + } + + private async handleLegacyEvent( + runId: string, + eventId: string, + data: any, + currentRun: WorkflowRun, + params?: { resolveData?: ResolveData } + ): Promise { + const resolveData = params?.resolveData ?? 'all'; + const now = new Date(); + + switch (data.eventType) { + case 'run_cancelled': { + const updated: WorkflowRun = WorkflowRunSchema.parse({ + ...currentRun, + status: 'cancelled', + completedAt: now, + updatedAt: now, + }); + await this.storage.put(KEY.run, updated); + await this.disposeAllHooksAndWaits(runId); + this.updateRunIndex(updated); + return { + run: filterRunData(updated, resolveData) as WorkflowRun, + }; + } + + case 'wait_completed': + case 'hook_received': { + const event = await this.insertEvent( + runId, + eventId, + data, + SPEC_VERSION_CURRENT + ); + return { event: filterEventData(event, resolveData) }; + } + + default: + throw new Error( + `Event type '${data.eventType}' not supported for legacy runs ` + + `(specVersion: ${currentRun.specVersion || 'undefined'}). ` + + `Please upgrade @workflow packages.` + ); + } + } + + private async disposeAllHooksAndWaits(runId: string): Promise { + // Delete all hooks + const hookEntries = await this.storage.list({ prefix: 'hook:' }); + const keysToDelete: string[] = []; + for (const [key, hookValue] of hookEntries) { + keysToDelete.push(key); + if (hookValue && typeof hookValue === 'object' && 'token' in hookValue) { + keysToDelete.push(KEY.hookToken(hookValue.token)); + } + } + + // Delete all waits + const waitEntries = await this.storage.list({ prefix: 'wait:' }); + for (const [key] of waitEntries) { + keysToDelete.push(key); + } + + if (keysToDelete.length > 0) { + await this.storage.delete(keysToDelete); + } + + // Delete from D1 index + deleteHooksForRunIndex(this.db, runId).catch(() => {}); + } + + private updateRunIndex(run: WorkflowRun): void { + upsertRunIndex(this.db, { + runId: run.runId, + workflowName: run.workflowName, + status: run.status, + deploymentId: run.deploymentId, + specVersion: run.specVersion, + createdAt: run.createdAt.toISOString(), + updatedAt: run.updatedAt.toISOString(), + startedAt: toISOOrUndef(run.startedAt), + completedAt: toISOOrUndef(run.completedAt), + expiredAt: toISOOrUndef(run.expiredAt), + }).catch(() => {}); + } + + // ============================================================ + // Event reads + // ============================================================ + + private async getEvent( + eventId: string, + resolveData: ResolveData + ): Promise { + const event = await this.storage.get(KEY.event(eventId)); + if (!event) { + throw new WorkflowAPIError(`Event not found: ${eventId}`, { + status: 404, + }); + } + return filterEventData(event, resolveData); + } + + private async listEvents(params: { + limit: number; + cursor?: string; + sortOrder: 'asc' | 'desc'; + resolveData: ResolveData; + }): Promise> { + const all = await this.storage.list({ prefix: 'evt:' }); + let entries = [...all.entries()].map(([, v]) => v); + + if (params.sortOrder === 'desc') { + entries.reverse(); + } + + // Apply cursor + if (params.cursor) { + const idx = entries.findIndex((e) => e.eventId === params.cursor); + if (idx >= 0) { + entries = entries.slice(idx + 1); + } + } + + const data = entries.slice(0, params.limit); + const hasMore = entries.length > params.limit; + + return { + data: data.map((e) => filterEventData(e, params.resolveData)), + cursor: data.at(-1)?.eventId ?? null, + hasMore, + }; + } + + private async listEventsByCorrelationId(params: { + correlationId: string; + limit: number; + cursor?: string; + sortOrder: 'asc' | 'desc'; + resolveData: ResolveData; + }): Promise> { + const all = await this.storage.list({ prefix: 'evt:' }); + let entries = [...all.entries()] + .map(([, v]) => v) + .filter((e) => e.correlationId === params.correlationId); + + if (params.sortOrder === 'desc') { + entries.reverse(); + } + + if (params.cursor) { + const idx = entries.findIndex((e) => e.eventId === params.cursor); + if (idx >= 0) { + entries = entries.slice(idx + 1); + } + } + + const data = entries.slice(0, params.limit); + const hasMore = entries.length > params.limit; + + return { + data: data.map((e) => filterEventData(e, params.resolveData)), + cursor: data.at(-1)?.eventId ?? null, + hasMore, + }; + } + + // ============================================================ + // Step reads + // ============================================================ + + private async getStep( + stepId: string, + resolveData: ResolveData + ): Promise { + const step = await this.storage.get(KEY.step(stepId)); + if (!step) { + throw new WorkflowAPIError(`Step not found: ${stepId}`, { + status: 404, + }); + } + const parsed = StepSchema.parse(step); + return filterStepData(parsed, resolveData); + } + + private async listSteps(params: { + limit: number; + cursor?: string; + resolveData: ResolveData; + }): Promise> { + const all = await this.storage.list({ prefix: 'step:' }); + let entries = [...all.entries()] + .map(([, v]) => v) + .sort((a, b) => (b.stepId > a.stepId ? 1 : -1)); // desc by stepId + + if (params.cursor) { + const idx = entries.findIndex((e) => e.stepId === params.cursor); + if (idx >= 0) { + entries = entries.slice(idx + 1); + } + } + + const data = entries.slice(0, params.limit); + const hasMore = entries.length > params.limit; + + return { + data: data.map((s) => { + const parsed = StepSchema.parse(s); + return filterStepData(parsed, params.resolveData); + }), + cursor: data.at(-1)?.stepId ?? null, + hasMore, + }; + } + + // ============================================================ + // Hook reads + // ============================================================ + + private async getHook( + hookId: string, + resolveData: ResolveData + ): Promise { + const hook = await this.storage.get(KEY.hook(hookId)); + if (!hook) { + throw new HookNotFoundError(hookId); + } + const parsed = HookSchema.parse(hook); + parsed.isWebhook ??= true; + return filterHookData(parsed, resolveData); + } + + private async listHooks(params: { + limit: number; + cursor?: string; + sortOrder: 'asc' | 'desc'; + resolveData: ResolveData; + }): Promise> { + const all = await this.storage.list({ prefix: 'hook:' }); + let entries = [...all.entries()] + .filter(([key]) => !key.startsWith('hook_tok:')) + .map(([, v]) => v); + + if (params.sortOrder === 'desc') { + entries.reverse(); + } + + if (params.cursor) { + const idx = entries.findIndex((e) => e.hookId === params.cursor); + if (idx >= 0) { + entries = entries.slice(idx + 1); + } + } + + const data = entries.slice(0, params.limit); + const hasMore = entries.length > params.limit; + + return { + data: data.map((h) => { + const parsed = HookSchema.parse(h); + parsed.isWebhook ??= true; + return filterHookData(parsed, params.resolveData); + }), + cursor: data.at(-1)?.hookId ?? null, + hasMore, + }; + } + + // ============================================================ + // Stream operations + // ============================================================ + + private async writeStreamChunk( + streamId: string, + chunk: number[] | string, + _runId: string + ): Promise { + const chunkId = `chnk_${ulid()}`; + const data: StreamChunkData = { + data: + typeof chunk === 'string' + ? [...new TextEncoder().encode(chunk)] + : chunk, + eof: false, + }; + await this.storage.put(KEY.streamChunk(streamId, chunkId), data); + await this.ensureStreamIndexed(streamId); + } + + private async writeStreamChunks( + streamId: string, + chunks: (number[] | string)[], + _runId: string + ): Promise { + if (chunks.length === 0) return; + const entries: Record = {}; + for (const chunk of chunks) { + const chunkId = `chnk_${ulid()}`; + entries[KEY.streamChunk(streamId, chunkId)] = { + data: + typeof chunk === 'string' + ? [...new TextEncoder().encode(chunk)] + : chunk, + eof: false, + }; + } + await this.storage.put(entries); + await this.ensureStreamIndexed(streamId); + } + + private async closeStream(streamId: string, _runId: string): Promise { + const chunkId = `chnk_${ulid()}`; + const data: StreamChunkData = { data: [], eof: true }; + await this.storage.put(KEY.streamChunk(streamId, chunkId), data); + await this.storage.put(KEY.streamMeta(streamId), { eof: true }); + } + + private async readStream( + streamId: string, + startIndex: number + ): Promise> { + const prefix = `strm:${streamId}:chnk:`; + const all = await this.storage.list({ prefix }); + const chunks = [...all.entries()].map(([, v]) => v).slice(startIndex); + + return new ReadableStream({ + start(controller) { + for (const chunk of chunks) { + if (chunk.data.length > 0) { + controller.enqueue(new Uint8Array(chunk.data)); + } + if (chunk.eof) { + controller.close(); + return; + } + } + // If no EOF chunk found, close anyway (stream may still be open) + controller.close(); + }, + }); + } + + private async listStreams(): Promise { + return (await this.storage.get(KEY.streamIndex)) ?? []; + } + + private async ensureStreamIndexed(streamId: string): Promise { + const streams = (await this.storage.get(KEY.streamIndex)) ?? []; + if (!streams.includes(streamId)) { + streams.push(streamId); + await this.storage.put(KEY.streamIndex, streams); + } + } +} diff --git a/packages/world-cloudflare/src/index.ts b/packages/world-cloudflare/src/index.ts new file mode 100644 index 0000000000..c6ecc16000 --- /dev/null +++ b/packages/world-cloudflare/src/index.ts @@ -0,0 +1,33 @@ +import type { World } from '@workflow/world'; +import type { CloudflareWorldConfig } from './config.js'; +import { type CloudflareQueue, createQueue } from './queue.js'; +import { createStorage } from './storage.js'; +import { createStreamer } from './streamer.js'; + +export type CloudflareWorld = World & { + /** Process a batch of messages from the Cloudflare Queue consumer */ + handleQueueBatch: CloudflareQueue['handleQueueBatch']; +}; + +/** + * Creates a Cloudflare Workers world implementation using: + * - Durable Objects for per-run state (events, steps, hooks, waits, streams) + * - D1 for cross-run index queries (runs.list, hooks.getByToken) + * - Cloudflare Queues for message dispatch + */ +export function createWorld(config: CloudflareWorldConfig): CloudflareWorld { + const storage = createStorage(config); + const queue = createQueue(config); + const streamer = createStreamer(config); + + return { + ...storage, + ...queue, + ...streamer, + }; +} + +export type { CloudflareWorldConfig } from './config.js'; +export { migrate } from './d1.js'; +export { WorkflowRunDO } from './durable-object.js'; +export type { CloudflareQueue } from './queue.js'; diff --git a/packages/world-cloudflare/src/queue.test.ts b/packages/world-cloudflare/src/queue.test.ts new file mode 100644 index 0000000000..7764a331a6 --- /dev/null +++ b/packages/world-cloudflare/src/queue.test.ts @@ -0,0 +1,211 @@ +import type { QueuePayload } from '@workflow/world'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createLocalWorld, createQueueExecutor } from '@workflow/world-local'; +import { createQueue } from './queue.js'; + +vi.mock('@workflow/world-local', () => ({ + createLocalWorld: vi.fn(), + createQueueExecutor: vi.fn(), +})); + +describe('cloudflare queue', () => { + const executeMessage = vi.fn(); + const registerHandler = vi.fn(); + const executorClose = vi.fn(); + const wrappedHandler = vi.fn(async () => Response.json({ ok: true })); + const localWorldClose = vi.fn(); + const createQueueHandler = vi.fn(() => wrappedHandler); + const queueSend = vi.fn(); + + const mockConfig = { + db: {} as any, + runs: {} as any, + queue: { send: queueSend } as any, + }; + + beforeEach(() => { + vi.clearAllMocks(); + + vi.mocked(createQueueExecutor).mockReturnValue({ + executeMessage, + registerHandler, + close: executorClose, + }); + vi.mocked(createLocalWorld).mockReturnValue({ + createQueueHandler, + close: localWorldClose, + } as any); + }); + + it('registers queue handlers with the shared executor', () => { + const queue = createQueue(mockConfig); + const handler = vi.fn(async () => undefined); + + const wrapped = queue.createQueueHandler('__wkf_step_', handler); + + expect(createQueueHandler).toHaveBeenCalledWith('__wkf_step_', handler); + expect(registerHandler).toHaveBeenCalledWith('__wkf_step_', wrappedHandler); + expect(wrapped).toBe(wrappedHandler); + }); + + it('sends messages to the cloudflare queue', async () => { + const queue = createQueue(mockConfig); + + const message = { + runId: 'run_01ABC', + } satisfies QueuePayload; + + const result = await queue.queue('__wkf_workflow_test-flow', message, { + headers: { traceparent: 'trace-parent' }, + idempotencyKey: 'idem-key', + delaySeconds: 5, + }); + + expect(result.messageId).toBeTruthy(); + expect(queueSend).toHaveBeenCalledWith( + expect.objectContaining({ + queueName: '__wkf_workflow_test-flow', + payload: message, + attempt: 1, + idempotencyKey: 'idem-key', + headers: { traceparent: 'trace-parent' }, + }), + expect.objectContaining({ + delaySeconds: 5, + }) + ); + }); + + it('sends messages without delay when delaySeconds not specified', async () => { + const queue = createQueue(mockConfig); + + await queue.queue('__wkf_step_test-step', { + workflowName: 'test-workflow', + workflowRunId: 'run_01ABC', + workflowStartedAt: Date.now(), + stepId: 'step_01ABC', + }); + + expect(queueSend).toHaveBeenCalledWith( + expect.objectContaining({ + queueName: '__wkf_step_test-step', + attempt: 1, + }), + expect.not.objectContaining({ + delaySeconds: expect.anything(), + }) + ); + }); + + it('returns cloudflare as deployment id', async () => { + const queue = createQueue(mockConfig); + const deploymentId = await queue.getDeploymentId(); + expect(deploymentId).toBe('cloudflare'); + }); + + it('processes queue batch messages and acks on completion', async () => { + executeMessage.mockResolvedValueOnce({ type: 'completed' }); + + const queue = createQueue(mockConfig); + + const ack = vi.fn(); + const retry = vi.fn(); + const mockBatch = { + messages: [ + { + body: { + queueName: '__wkf_workflow_test-flow' as const, + payload: { runId: 'run_01ABC' }, + messageId: 'msg_01ABC', + attempt: 1, + }, + ack, + retry, + }, + ], + } as any; + + await queue.handleQueueBatch(mockBatch); + + expect(executeMessage).toHaveBeenCalledWith( + expect.objectContaining({ + queueName: '__wkf_workflow_test-flow', + messageId: expect.any(String), + attempt: 1, + }) + ); + expect(ack).toHaveBeenCalled(); + expect(retry).not.toHaveBeenCalled(); + }); + + it('reschedules messages on reschedule result', async () => { + executeMessage.mockResolvedValueOnce({ + type: 'reschedule', + timeoutSeconds: 5, + }); + + const queue = createQueue(mockConfig); + + const ack = vi.fn(); + const retry = vi.fn(); + const mockBatch = { + messages: [ + { + body: { + queueName: '__wkf_step_test-step' as const, + payload: { + workflowName: 'test-workflow', + workflowRunId: 'run_01ABC', + workflowStartedAt: Date.now(), + stepId: 'step_01ABC', + }, + messageId: 'msg_01ABC', + attempt: 1, + }, + ack, + retry, + }, + ], + } as any; + + await queue.handleQueueBatch(mockBatch); + + expect(queueSend).toHaveBeenCalledWith( + expect.objectContaining({ + attempt: 2, + }), + expect.objectContaining({ + delaySeconds: 5, + }) + ); + expect(ack).toHaveBeenCalled(); + }); + + it('retries messages on failure', async () => { + executeMessage.mockRejectedValueOnce(new Error('boom')); + + const queue = createQueue(mockConfig); + + const ack = vi.fn(); + const retry = vi.fn(); + const mockBatch = { + messages: [ + { + body: { + queueName: '__wkf_workflow_test-flow' as const, + payload: { runId: 'run_01ABC' }, + messageId: 'msg_01ABC', + attempt: 1, + }, + ack, + retry, + }, + ], + } as any; + + await queue.handleQueueBatch(mockBatch); + + expect(ack).not.toHaveBeenCalled(); + expect(retry).toHaveBeenCalled(); + }); +}); diff --git a/packages/world-cloudflare/src/queue.ts b/packages/world-cloudflare/src/queue.ts new file mode 100644 index 0000000000..34da2cad6b --- /dev/null +++ b/packages/world-cloudflare/src/queue.ts @@ -0,0 +1,111 @@ +import type { + Queue as QueueInterface, + QueuePayload, + ValidQueueName, +} from '@workflow/world'; +import { + MessageId as MessageIdSchema, + QueuePayloadSchema, +} from '@workflow/world'; +import { createLocalWorld, createQueueExecutor } from '@workflow/world-local'; +import { monotonicFactory } from 'ulid'; +import type { CloudflareWorldConfig } from './config.js'; + +/** Message envelope stored in Cloudflare Queues */ +interface QueueMessageEnvelope { + queueName: ValidQueueName; + payload: QueuePayload; + messageId: string; + attempt: number; + idempotencyKey?: string; + headers?: Record; +} + +export interface CloudflareQueue extends QueueInterface { + /** Process a batch of messages from the Cloudflare Queue consumer */ + handleQueueBatch(batch: MessageBatch): Promise; +} + +export function createQueue(config: CloudflareWorldConfig): CloudflareQueue { + const port = config.port; + const localWorld = createLocalWorld({ dataDir: undefined, port }); + const executor = createQueueExecutor({ port }); + const generateMessageId = monotonicFactory(); + + const createQueueHandler: QueueInterface['createQueueHandler'] = ( + prefix, + handler + ) => { + const wrappedHandler = localWorld.createQueueHandler(prefix, handler); + executor.registerHandler(prefix, wrappedHandler); + return wrappedHandler; + }; + + const getDeploymentId: QueueInterface['getDeploymentId'] = async () => { + return 'cloudflare'; + }; + + const queue: QueueInterface['queue'] = async (queueName, message, opts) => { + const messageId = MessageIdSchema.parse(`msg_${generateMessageId()}`); + + const envelope: QueueMessageEnvelope = { + queueName, + payload: message, + messageId, + attempt: 1, + idempotencyKey: opts?.idempotencyKey, + headers: opts?.headers, + }; + + await config.queue.send(envelope, { + // Cloudflare Queues supports delaySeconds natively + ...(opts?.delaySeconds ? { delaySeconds: opts.delaySeconds } : {}), + }); + + return { messageId }; + }; + + const handleQueueBatch = async ( + batch: MessageBatch + ): Promise => { + for (const msg of batch.messages) { + const envelope = msg.body; + try { + QueuePayloadSchema.parse(envelope.payload); + + const result = await executor.executeMessage({ + queueName: envelope.queueName, + messageId: MessageIdSchema.parse(envelope.messageId), + attempt: envelope.attempt, + body: new TextEncoder().encode(JSON.stringify(envelope.payload)), + headers: envelope.headers, + }); + + if (result.type === 'completed') { + msg.ack(); + } else if (result.type === 'reschedule') { + // Re-enqueue with delay for reschedule + const rescheduledEnvelope: QueueMessageEnvelope = { + ...envelope, + attempt: envelope.attempt + 1, + }; + await config.queue.send(rescheduledEnvelope, { + delaySeconds: result.timeoutSeconds, + }); + msg.ack(); + } else { + msg.retry(); + } + } catch { + msg.retry(); + } + } + }; + + return { + createQueueHandler, + getDeploymentId, + queue, + handleQueueBatch, + }; +} diff --git a/packages/world-cloudflare/src/storage.ts b/packages/world-cloudflare/src/storage.ts new file mode 100644 index 0000000000..0a82392a47 --- /dev/null +++ b/packages/world-cloudflare/src/storage.ts @@ -0,0 +1,371 @@ +import { HookNotFoundError, WorkflowAPIError } from '@workflow/errors'; +import type { + CreateEventParams, + CreateEventRequest, + Event, + EventResult, + GetEventParams, + GetHookParams, + GetStepParams, + GetWorkflowRunParams, + Hook, + ListEventsByCorrelationIdParams, + ListEventsParams, + ListHooksParams, + ListWorkflowRunStepsParams, + ListWorkflowRunsParams, + PaginatedResponse, + RunCreatedEventRequest, + Step, + StepWithoutData, + Storage, + WorkflowRun, + WorkflowRunWithoutData, +} from '@workflow/world'; +import { monotonicFactory } from 'ulid'; +import type { CloudflareWorldConfig } from './config.js'; +import { doFetch, getRunStub } from './util.js'; + +const ulid = monotonicFactory(); + +/** + * Creates the Storage implementation that routes requests to Durable Objects + * and D1 for cross-run queries. + */ +export function createStorage(config: CloudflareWorldConfig): Storage { + return { + runs: createRunsStorage(config), + events: createEventsStorage(config), + hooks: createHooksStorage(config), + steps: createStepsStorage(config), + }; +} + +function createRunsStorage(config: CloudflareWorldConfig): Storage['runs'] { + return { + get: (async (id: string, params?: GetWorkflowRunParams) => { + const stub = getRunStub(config.runs, id); + const resolveData = params?.resolveData ?? 'all'; + return doFetch( + stub, + `/run?resolveData=${resolveData}` + ); + }) as Storage['runs']['get'], + + list: (async (params?: ListWorkflowRunsParams) => { + const limit = params?.pagination?.limit ?? 20; + const cursor = params?.pagination?.cursor; + const resolveData = params?.resolveData ?? 'all'; + + // Query D1 index for the list + let sql = 'SELECT * FROM workflow_runs_index WHERE 1=1'; + const bindings: any[] = []; + + if (params?.workflowName) { + sql += ' AND workflow_name = ?'; + bindings.push(params.workflowName); + } + if (params?.status) { + sql += ' AND status = ?'; + bindings.push(params.status); + } + if (cursor) { + sql += ' AND run_id < ?'; + bindings.push(cursor); + } + sql += ' ORDER BY run_id DESC'; + sql += ` LIMIT ${limit + 1}`; + + const result = await config.db + .prepare(sql) + .bind(...bindings) + .all(); + + const rows = result.results.slice(0, limit) as any[]; + const hasMore = result.results.length > limit; + + if (resolveData === 'none') { + // Return index data only (no input/output) + const data: WorkflowRunWithoutData[] = rows.map((row) => ({ + runId: row.run_id, + workflowName: row.workflow_name, + status: row.status, + deploymentId: row.deployment_id, + specVersion: row.spec_version, + input: undefined, + output: undefined, + error: undefined, + createdAt: new Date(row.created_at), + updatedAt: new Date(row.updated_at), + startedAt: row.started_at ? new Date(row.started_at) : undefined, + completedAt: row.completed_at + ? new Date(row.completed_at) + : undefined, + expiredAt: row.expired_at ? new Date(row.expired_at) : undefined, + })); + return { data, cursor: rows.at(-1)?.run_id ?? null, hasMore }; + } + + // For resolveData='all', fan out to DOs for full data + const data = await Promise.all( + rows.map(async (row) => { + const stub = getRunStub(config.runs, row.run_id); + try { + return await doFetch( + stub, + `/run?resolveData=${resolveData}` + ); + } catch { + // If DO is unavailable, fall back to index data + return { + runId: row.run_id, + workflowName: row.workflow_name, + status: row.status, + deploymentId: row.deployment_id, + specVersion: row.spec_version, + input: undefined, + output: undefined, + createdAt: new Date(row.created_at), + updatedAt: new Date(row.updated_at), + startedAt: row.started_at ? new Date(row.started_at) : undefined, + completedAt: row.completed_at + ? new Date(row.completed_at) + : undefined, + expiredAt: row.expired_at ? new Date(row.expired_at) : undefined, + } as WorkflowRun; + } + }) + ); + + return { + data, + cursor: rows.at(-1)?.run_id ?? null, + hasMore, + }; + }) as Storage['runs']['list'], + }; +} + +function createEventsStorage(config: CloudflareWorldConfig): Storage['events'] { + return { + async create( + runId: string | null, + data: RunCreatedEventRequest | CreateEventRequest, + params?: CreateEventParams + ): Promise { + // For run_created with null runId, generate the runId client-side so + // the routing key and the stored runId map to the same DO instance. + // If we used a random temp key and let the DO generate its own runId, + // that runId would route to a different DO instance, making the run + // data permanently unretrievable. + let effectiveRunId = runId; + if (data.eventType === 'run_created' && !runId) { + effectiveRunId = `wrun_${ulid()}`; + } + + const stub = getRunStub(config.runs, effectiveRunId as string); + return doFetch(stub, '/events', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + runId: effectiveRunId, + data, + params, + }), + }); + }, + + async get( + runId: string, + eventId: string, + params?: GetEventParams + ): Promise { + const stub = getRunStub(config.runs, runId); + const resolveData = params?.resolveData ?? 'all'; + return doFetch( + stub, + `/events/${encodeURIComponent(eventId)}?resolveData=${resolveData}` + ); + }, + + async list(params: ListEventsParams): Promise> { + const stub = getRunStub(config.runs, params.runId); + const qs = new URLSearchParams({ + limit: String(params.pagination?.limit ?? 100), + sortOrder: params.pagination?.sortOrder ?? 'asc', + resolveData: params.resolveData ?? 'all', + }); + if (params.pagination?.cursor) { + qs.set('cursor', params.pagination.cursor); + } + return doFetch>(stub, `/events?${qs}`); + }, + + async listByCorrelationId( + _params: ListEventsByCorrelationIdParams + ): Promise> { + // We need the runId to route to the correct DO. + // correlationId alone doesn't tell us which DO to query. + // This is a limitation of the DO-per-run architecture. + // For now, we need the caller to provide the runId via pagination cursor + // or we query D1 to find the run. + // Since listByCorrelationId is typically called with a known run context, + // we'll need the caller to include this. For now, throw an informative error. + throw new WorkflowAPIError( + 'listByCorrelationId requires routing context. Use events.list with runId instead.', + { status: 501 } + ); + }, + }; +} + +function createHooksStorage(config: CloudflareWorldConfig): Storage['hooks'] { + return { + async get(hookId: string, params?: GetHookParams): Promise { + // Look up runId from D1 index + const result = await config.db + .prepare('SELECT run_id FROM workflow_hooks_index WHERE hook_id = ?') + .bind(hookId) + .first<{ run_id: string }>(); + + if (!result) { + throw new HookNotFoundError(hookId); + } + + const stub = getRunStub(config.runs, result.run_id); + const resolveData = params?.resolveData ?? 'all'; + return doFetch( + stub, + `/hooks/${encodeURIComponent(hookId)}?resolveData=${resolveData}` + ); + }, + + async getByToken(token: string, params?: GetHookParams): Promise { + const result = await config.db + .prepare( + 'SELECT hook_id, run_id FROM workflow_hooks_index WHERE token = ?' + ) + .bind(token) + .first<{ hook_id: string; run_id: string }>(); + + if (!result) { + throw new HookNotFoundError(token); + } + + const stub = getRunStub(config.runs, result.run_id); + const resolveData = params?.resolveData ?? 'all'; + return doFetch( + stub, + `/hooks/${encodeURIComponent(result.hook_id)}?resolveData=${resolveData}` + ); + }, + + async list(params: ListHooksParams): Promise> { + if (params.runId) { + // Route directly to DO + const stub = getRunStub(config.runs, params.runId); + const qs = new URLSearchParams({ + limit: String(params.pagination?.limit ?? 100), + sortOrder: params.pagination?.sortOrder ?? 'asc', + resolveData: params.resolveData ?? 'all', + }); + if (params.pagination?.cursor) { + qs.set('cursor', params.pagination.cursor); + } + return doFetch>(stub, `/hooks?${qs}`); + } + + // Without runId, query D1 index then fan out to DOs + const limit = params.pagination?.limit ?? 100; + const cursor = params.pagination?.cursor; + const sortOrder = params.pagination?.sortOrder ?? 'asc'; + + let sql = 'SELECT * FROM workflow_hooks_index WHERE 1=1'; + const bindings: any[] = []; + if (cursor) { + sql += sortOrder === 'asc' ? ' AND hook_id > ?' : ' AND hook_id < ?'; + bindings.push(cursor); + } + sql += ` ORDER BY hook_id ${sortOrder === 'asc' ? 'ASC' : 'DESC'}`; + sql += ` LIMIT ${limit + 1}`; + + const result = await config.db + .prepare(sql) + .bind(...bindings) + .all(); + + const rows = result.results.slice(0, limit) as any[]; + const hasMore = result.results.length > limit; + + const resolveData = params.resolveData ?? 'all'; + const data = await Promise.all( + rows.map(async (row) => { + const stub = getRunStub(config.runs, row.run_id); + try { + return await doFetch( + stub, + `/hooks/${encodeURIComponent(row.hook_id)}?resolveData=${resolveData}` + ); + } catch { + // Fallback to index data + return { + hookId: row.hook_id, + runId: row.run_id, + token: row.token, + ownerId: row.owner_id, + projectId: row.project_id, + environment: row.environment, + createdAt: new Date(row.created_at), + isWebhook: row.is_webhook === 1, + specVersion: row.spec_version, + } as Hook; + } + }) + ); + + return { + data, + cursor: rows.at(-1)?.hook_id ?? null, + hasMore, + }; + }, + }; +} + +function createStepsStorage(config: CloudflareWorldConfig): Storage['steps'] { + return { + get: (async ( + runId: string | undefined, + stepId: string, + params?: GetStepParams + ) => { + if (!runId) { + throw new WorkflowAPIError( + 'runId is required for Cloudflare world step lookups', + { status: 400 } + ); + } + const stub = getRunStub(config.runs, runId); + const resolveData = params?.resolveData ?? 'all'; + return doFetch( + stub, + `/steps/${encodeURIComponent(stepId)}?resolveData=${resolveData}` + ); + }) as Storage['steps']['get'], + + list: (async (params: ListWorkflowRunStepsParams) => { + const stub = getRunStub(config.runs, params.runId); + const qs = new URLSearchParams({ + limit: String(params.pagination?.limit ?? 20), + resolveData: params.resolveData ?? 'all', + }); + if (params.pagination?.cursor) { + qs.set('cursor', params.pagination.cursor); + } + return doFetch>( + stub, + `/steps?${qs}` + ); + }) as Storage['steps']['list'], + }; +} diff --git a/packages/world-cloudflare/src/streamer.ts b/packages/world-cloudflare/src/streamer.ts new file mode 100644 index 0000000000..e49c72ea59 --- /dev/null +++ b/packages/world-cloudflare/src/streamer.ts @@ -0,0 +1,99 @@ +import type { Streamer } from '@workflow/world'; +import type { CloudflareWorldConfig } from './config.js'; +import { doFetch, doFetchStream, getRunStub } from './util.js'; + +/** + * Creates a Streamer implementation backed by Durable Objects. + * Stream chunks are stored in the run's DO storage. + */ +export function createStreamer(config: CloudflareWorldConfig): Streamer { + return { + async writeToStream( + name: string, + _runId: string | Promise, + chunk: string | Uint8Array + ): Promise { + const runId = await _runId; + const stub = getRunStub(config.runs, runId); + const chunkData = + typeof chunk === 'string' + ? [...new TextEncoder().encode(chunk)] + : [...chunk]; + + await stub.fetch(`http://do/streams/${encodeURIComponent(name)}/write`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ chunk: chunkData, runId }), + }); + }, + + async writeToStreamMulti( + name: string, + _runId: string | Promise, + chunks: (string | Uint8Array)[] + ): Promise { + if (chunks.length === 0) return; + const runId = await _runId; + const stub = getRunStub(config.runs, runId); + const serializedChunks = chunks.map((chunk) => + typeof chunk === 'string' + ? [...new TextEncoder().encode(chunk)] + : [...chunk] + ); + + await stub.fetch( + `http://do/streams/${encodeURIComponent(name)}/write-multi`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ chunks: serializedChunks, runId }), + } + ); + }, + + async closeStream( + name: string, + _runId: string | Promise + ): Promise { + const runId = await _runId; + const stub = getRunStub(config.runs, runId); + await stub.fetch(`http://do/streams/${encodeURIComponent(name)}/close`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ runId }), + }); + }, + + async readFromStream( + name: string, + startIndex?: number + ): Promise> { + // readFromStream doesn't receive runId in the interface. + // The stream name typically encodes the runId. We need a convention. + // For Cloudflare, we expect stream names to be formatted as `{runId}:{streamName}`. + const parts = name.split(':'); + if (parts.length < 2) { + throw new Error( + 'Stream name must be in format "runId:streamName" for Cloudflare world' + ); + } + const runId = parts[0]; + const streamName = parts.slice(1).join(':'); + + const stub = getRunStub(config.runs, runId); + const qs = startIndex ? `?startIndex=${startIndex}` : ''; + return doFetchStream( + stub, + `/streams/${encodeURIComponent(streamName)}/read${qs}` + ); + }, + + async listStreamsByRunId(runId: string): Promise { + const stub = getRunStub(config.runs, runId); + const names = await doFetch(stub, '/streams'); + // Prefix each stream name with `runId:` so the result can be passed + // directly to readFromStream, which expects that format. + return names.map((name) => `${runId}:${name}`); + }, + }; +} diff --git a/packages/world-cloudflare/src/util.ts b/packages/world-cloudflare/src/util.ts new file mode 100644 index 0000000000..cc56513a8c --- /dev/null +++ b/packages/world-cloudflare/src/util.ts @@ -0,0 +1,52 @@ +/** + * Get a Durable Object stub for a given runId. + */ +export function getRunStub( + namespace: DurableObjectNamespace, + runId: string +): DurableObjectStub { + const id = namespace.idFromName(runId); + return namespace.get(id); +} + +/** + * Make a typed fetch request to a Durable Object and parse the JSON response. + */ +export async function doFetch( + stub: DurableObjectStub, + path: string, + init?: RequestInit +): Promise { + const res = await stub.fetch(`http://do${path}`, init); + if (!res.ok) { + const text = await res.text(); + throw new Error(`DO request failed (${res.status}): ${path} - ${text}`); + } + return res.json() as Promise; +} + +/** + * Make a fetch request to a DO that returns a ReadableStream. + */ +export async function doFetchStream( + stub: DurableObjectStub, + path: string, + init?: RequestInit +): Promise> { + const res = await stub.fetch(`http://do${path}`, init); + if (!res.ok) { + const text = await res.text(); + throw new Error( + `DO stream request failed (${res.status}): ${path} - ${text}` + ); + } + if (!res.body) { + throw new Error(`DO stream request returned no body: ${path}`); + } + return res.body; +} + +/** ISO string or undefined */ +export function toISOOrUndef(d: Date | undefined | null): string | undefined { + return d ? d.toISOString() : undefined; +} diff --git a/packages/world-cloudflare/tsconfig.json b/packages/world-cloudflare/tsconfig.json new file mode 100644 index 0000000000..0cb8b46950 --- /dev/null +++ b/packages/world-cloudflare/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@workflow/tsconfig/base.json", + "compilerOptions": { + "moduleResolution": "NodeNext", + "outDir": "dist", + "types": ["@cloudflare/workers-types"] + }, + "include": ["src"], + "exclude": ["node_modules", "**/*.test.ts"] +} diff --git a/packages/world-cloudflare/wrangler.example.toml b/packages/world-cloudflare/wrangler.example.toml new file mode 100644 index 0000000000..42dff075b4 --- /dev/null +++ b/packages/world-cloudflare/wrangler.example.toml @@ -0,0 +1,36 @@ +# Example wrangler.toml for @workflow/world-cloudflare +# +# This demonstrates the required Cloudflare bindings for the workflow world. +# Copy this file to wrangler.toml and fill in your own values. + +name = "my-workflow-worker" +main = "src/worker.ts" +compatibility_date = "2024-12-01" + +# Durable Objects — one DO class per workflow run +[durable_objects] +bindings = [ + { name = "WORKFLOW_RUNS", class_name = "WorkflowRunDO" } +] + +# D1 Database — lightweight index for cross-run queries +[[d1_databases]] +binding = "WORKFLOW_DB" +database_name = "workflow" +database_id = "" + +# Queue producer — for dispatching workflow/step messages +[[queues.producers]] +binding = "WORKFLOW_QUEUE" +queue = "workflow-jobs" + +# Queue consumer — processes workflow/step messages +[[queues.consumers]] +queue = "workflow-jobs" +max_batch_size = 10 +max_retries = 3 + +# DO migration (required when first deploying) +[[migrations]] +tag = "v1" +new_classes = ["WorkflowRunDO"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c23103142e..deaec20754 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -384,7 +384,7 @@ importers: version: link:../tsconfig astro: specifier: 5.18.0 - version: 5.18.0(@netlify/blobs@9.1.2)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + version: 5.18.0(@netlify/blobs@9.1.2)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) packages/builders: dependencies: @@ -788,7 +788,7 @@ importers: version: link:../tsconfig nitro: specifier: 'catalog:' - version: 3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) vite: specifier: 7.1.12 version: 7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) @@ -816,7 +816,7 @@ importers: version: link:../tsconfig nuxt: specifier: 4.0.0 - version: 4.0.0(@biomejs/biome@2.4.4)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(@vue/compiler-sfc@3.5.22)(better-sqlite3@11.10.0)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) + version: 4.0.0(@biomejs/biome@2.4.4)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(@vue/compiler-sfc@3.5.22)(better-sqlite3@11.10.0)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) packages/rollup: dependencies: @@ -1279,6 +1279,37 @@ importers: specifier: 'catalog:' version: 4.3.6 + packages/world-cloudflare: + dependencies: + '@workflow/errors': + specifier: workspace:* + version: link:../errors + '@workflow/world': + specifier: workspace:* + version: link:../world + '@workflow/world-local': + specifier: workspace:* + version: link:../world-local + ulid: + specifier: 'catalog:' + version: 3.0.1 + zod: + specifier: 'catalog:' + version: 4.3.6 + devDependencies: + '@cloudflare/workers-types': + specifier: 4.20250312.0 + version: 4.20250312.0 + '@types/node': + specifier: 'catalog:' + version: 22.19.0 + '@workflow/tsconfig': + specifier: workspace:* + version: link:../tsconfig + vitest: + specifier: 'catalog:' + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + packages/world-local: dependencies: '@vercel/queue': @@ -1347,7 +1378,7 @@ importers: version: 17.3.1 drizzle-orm: specifier: 0.45.1 - version: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8) + version: 0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8) graphile-worker: specifier: 0.16.6 version: 0.16.6(typescript@5.9.3) @@ -1467,10 +1498,10 @@ importers: dependencies: '@astrojs/node': specifier: 9.5.0 - version: 9.5.0(astro@5.16.3(@netlify/blobs@9.1.2)(@types/node@24.6.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1)) + version: 9.5.0(astro@5.16.3(@netlify/blobs@9.1.2)(@types/node@24.6.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1)) '@astrojs/vercel': specifier: ^9.0.0 - version: 9.0.2(5bdc065253a3063b2ad85e26c57abf6a) + version: 9.0.2(2e8b9a575f4f5c96375a8dbcffcb6fda) '@workflow/world-postgres': specifier: workspace:* version: link:../../packages/world-postgres @@ -1479,7 +1510,7 @@ importers: version: 5.0.104(zod@4.3.6) astro: specifier: ^5.15.6 - version: 5.16.3(@netlify/blobs@9.1.2)(@types/node@24.6.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + version: 5.16.3(@netlify/blobs@9.1.2)(@types/node@24.6.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) lodash.chunk: specifier: ^4.2.0 version: 4.2.0 @@ -1543,7 +1574,7 @@ importers: version: 5.1.0 nitro: specifier: 'catalog:' - version: 3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) devDependencies: '@types/express': specifier: ^5.0.5 @@ -1574,7 +1605,7 @@ importers: version: 5.6.2 nitro: specifier: 'catalog:' - version: 3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) devDependencies: '@types/node': specifier: 'catalog:' @@ -1617,7 +1648,7 @@ importers: version: 4.2.0 nitro: specifier: 'catalog:' - version: 3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) openai: specifier: ^6.6.0 version: 6.6.0(ws@8.18.3)(zod@4.3.6) @@ -1881,7 +1912,7 @@ importers: version: 4.2.0 nitropack: specifier: ^2.12.7 - version: 2.12.7(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) + version: 2.12.7(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) openai: specifier: ^6.6.0 version: 6.6.0(ws@8.18.3)(zod@4.3.6) @@ -1909,7 +1940,7 @@ importers: version: 4.2.0 nitro: specifier: 'catalog:' - version: 3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) openai: specifier: ^6.1.0 version: 6.6.0(ws@8.18.3)(zod@4.3.6) @@ -1946,7 +1977,7 @@ importers: version: 4.2.0 nuxt: specifier: ^4.1.3 - version: 4.1.3(@biomejs/biome@2.4.4)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(@vue/compiler-sfc@3.5.22)(better-sqlite3@11.10.0)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) + version: 4.1.3(@biomejs/biome@2.4.4)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(@vue/compiler-sfc@3.5.22)(better-sqlite3@11.10.0)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) openai: specifier: ^6.6.0 version: 6.6.0(ws@8.18.3)(zod@4.3.6) @@ -2037,7 +2068,7 @@ importers: version: 1.15.3 '@vercel/analytics': specifier: latest - version: 2.0.1(3eb18ee0ef09bb7b6ddb50c31f32f06d) + version: 2.0.1(39da96e252239d589fe31fcbb1912d1e) '@workflow/swc-plugin': specifier: workspace:* version: link:../../packages/swc-plugin-workflow @@ -2120,7 +2151,7 @@ importers: version: 4.2.0 nitro: specifier: 'catalog:' - version: 3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) openai: specifier: ^6.6.0 version: 6.6.0(ws@8.18.3)(zod@4.3.6) @@ -2770,6 +2801,9 @@ packages: resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} engines: {node: '>=18.0.0'} + '@cloudflare/workers-types@4.20250312.0': + resolution: {integrity: sha512-LQBDkrXxm/L0FM4NoT8EXaKCA7+2roOAZAWg+31RGxLKcoAWWSQpbf0PFMBAyFIN/eNADu5RKKrt4qHWNsztHQ==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -16114,10 +16148,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/node@9.5.0(astro@5.16.3(@netlify/blobs@9.1.2)(@types/node@24.6.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))': + '@astrojs/node@9.5.0(astro@5.16.3(@netlify/blobs@9.1.2)(@types/node@24.6.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))': dependencies: '@astrojs/internal-helpers': 0.7.4 - astro: 5.16.3(@netlify/blobs@9.1.2)(@types/node@24.6.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + astro: 5.16.3(@netlify/blobs@9.1.2)(@types/node@24.6.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) send: 1.2.0 server-destroy: 1.0.1 transitivePeerDependencies: @@ -16139,14 +16173,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/vercel@9.0.2(5bdc065253a3063b2ad85e26c57abf6a)': + '@astrojs/vercel@9.0.2(2e8b9a575f4f5c96375a8dbcffcb6fda)': dependencies: '@astrojs/internal-helpers': 0.7.5 '@vercel/analytics': 1.6.1(@sveltejs/kit@2.48.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@6.4.1(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.3)(vite@6.4.1(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(svelte@5.43.3)(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) '@vercel/functions': 2.2.13(@aws-sdk/credential-provider-web-identity@3.972.13) '@vercel/nft': 0.30.4(rollup@4.53.2) '@vercel/routing-utils': 5.3.0 - astro: 5.16.3(@netlify/blobs@9.1.2)(@types/node@24.6.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + astro: 5.16.3(@netlify/blobs@9.1.2)(@types/node@24.6.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) esbuild: 0.25.12 tinyglobby: 0.2.15 transitivePeerDependencies: @@ -16847,6 +16881,8 @@ snapshots: dependencies: mime: 3.0.0 + '@cloudflare/workers-types@4.20250312.0': {} + '@colors/colors@1.5.0': optional: true @@ -18625,7 +18661,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/vite-builder@4.0.0(@biomejs/biome@2.4.4)(@netlify/blobs@9.1.2)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(eslint@9.38.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)': + '@nuxt/vite-builder@4.0.0(@biomejs/biome@2.4.4)(@netlify/blobs@9.1.2)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(eslint@9.38.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)': dependencies: '@nuxt/kit': 4.0.0(magicast@0.3.5) '@rollup/plugin-replace': 6.0.2(rollup@4.53.2) @@ -18645,7 +18681,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.0 mocked-exports: 0.1.1 - nitropack: 2.12.0(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) + nitropack: 2.12.0(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) pathe: 2.0.3 pkg-types: 2.3.0 postcss: 8.5.6 @@ -22971,11 +23007,11 @@ snapshots: vue: 3.5.22(typescript@5.9.3) vue-router: 4.6.3(vue@3.5.22(typescript@5.9.3)) - '@vercel/analytics@2.0.1(3eb18ee0ef09bb7b6ddb50c31f32f06d)': + '@vercel/analytics@2.0.1(39da96e252239d589fe31fcbb1912d1e)': optionalDependencies: '@sveltejs/kit': 2.48.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.3)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) next: 16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - nuxt: 4.1.3(@biomejs/biome@2.4.4)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(@vue/compiler-sfc@3.5.22)(better-sqlite3@11.10.0)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) + nuxt: 4.1.3(@biomejs/biome@2.4.4)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(@vue/compiler-sfc@3.5.22)(better-sqlite3@11.10.0)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) react: 19.2.4 svelte: 5.43.3 vue: 3.5.22(typescript@5.9.3) @@ -23831,7 +23867,7 @@ snapshots: astring@1.9.0: {} - astro@5.16.3(@netlify/blobs@9.1.2)(@types/node@24.6.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): + astro@5.16.3(@netlify/blobs@9.1.2)(@types/node@24.6.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.13.0 '@astrojs/internal-helpers': 0.7.5 @@ -23886,7 +23922,7 @@ snapshots: ultrahtml: 1.6.0 unifont: 0.6.0 unist-util-visit: 5.0.0 - unstorage: 1.17.3(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2) + unstorage: 1.17.3(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2) vfile: 6.0.3 vite: 6.4.1(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vitefu: 1.1.1(vite@6.4.1(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) @@ -23933,7 +23969,7 @@ snapshots: - uploadthing - yaml - astro@5.18.0(@netlify/blobs@9.1.2)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): + astro@5.18.0(@netlify/blobs@9.1.2)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.13.0 '@astrojs/internal-helpers': 0.7.5 @@ -23988,7 +24024,7 @@ snapshots: ultrahtml: 1.6.0 unifont: 0.7.4 unist-util-visit: 5.0.0 - unstorage: 1.17.4(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2) + unstorage: 1.17.4(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2) vfile: 6.0.3 vite: 6.4.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vitefu: 1.1.1(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) @@ -25056,10 +25092,10 @@ snapshots: dayjs@1.11.18: {} - db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)): + db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)): optionalDependencies: better-sqlite3: 11.10.0 - drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8) + drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8) debug@2.6.9: dependencies: @@ -25311,8 +25347,9 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8): + drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8): optionalDependencies: + '@cloudflare/workers-types': 4.20250312.0 '@opentelemetry/api': 1.9.0 '@types/pg': 8.16.0 better-sqlite3: 11.10.0 @@ -28396,11 +28433,11 @@ snapshots: nf3@0.1.10: {} - nitro@3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + nitro@3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: consola: 3.4.2 crossws: 0.4.1(srvx@0.9.6) - db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) + db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) h3: 2.0.1-rc.5(crossws@0.4.1(srvx@0.9.6)) jiti: 2.6.1 nf3: 0.1.10 @@ -28411,7 +28448,7 @@ snapshots: srvx: 0.9.6 undici: 7.22.0 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.4(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(chokidar@4.0.3)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.4(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(chokidar@4.0.3)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3) optionalDependencies: rollup: 4.53.2 vite: 7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) @@ -28444,11 +28481,11 @@ snapshots: - sqlite3 - uploadthing - nitro@3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + nitro@3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: consola: 3.4.2 crossws: 0.4.1(srvx@0.9.6) - db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) + db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) h3: 2.0.1-rc.5(crossws@0.4.1(srvx@0.9.6)) jiti: 2.6.1 nf3: 0.1.10 @@ -28459,7 +28496,7 @@ snapshots: srvx: 0.9.6 undici: 7.22.0 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.4(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(chokidar@4.0.3)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.4(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(chokidar@4.0.3)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3) optionalDependencies: rollup: 4.53.2 vite: 7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) @@ -28492,7 +28529,7 @@ snapshots: - sqlite3 - uploadthing - nitropack@2.12.0(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)): + nitropack@2.12.0(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)): dependencies: '@cloudflare/kv-asset-handler': 0.4.0 '@netlify/functions': 3.1.10(rollup@4.53.2) @@ -28514,7 +28551,7 @@ snapshots: cookie-es: 2.0.0 croner: 9.1.0 crossws: 0.3.5 - db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) + db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) defu: 6.1.4 destr: 2.0.5 dot-prop: 9.0.0 @@ -28560,7 +28597,7 @@ snapshots: unenv: 2.0.0-rc.24 unimport: 5.5.0 unplugin-utils: 0.2.5 - unstorage: 1.17.3(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2) + unstorage: 1.17.3(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2) untyped: 2.0.0 unwasm: 0.3.11 youch: 4.1.0-beta.8 @@ -28595,7 +28632,7 @@ snapshots: - supports-color - uploadthing - nitropack@2.12.7(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)): + nitropack@2.12.7(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)): dependencies: '@cloudflare/kv-asset-handler': 0.4.0 '@rollup/plugin-alias': 5.1.1(rollup@4.53.2) @@ -28616,7 +28653,7 @@ snapshots: cookie-es: 2.0.0 croner: 9.1.0 crossws: 0.3.5 - db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) + db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) defu: 6.1.4 destr: 2.0.5 dot-prop: 10.1.0 @@ -28662,7 +28699,7 @@ snapshots: unenv: 2.0.0-rc.21 unimport: 5.5.0 unplugin-utils: 0.3.1 - unstorage: 1.17.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2) + unstorage: 1.17.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2) untyped: 2.0.0 unwasm: 0.3.11 youch: 4.1.0-beta.11 @@ -28786,7 +28823,7 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt@4.0.0(@biomejs/biome@2.4.4)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(@vue/compiler-sfc@3.5.22)(better-sqlite3@11.10.0)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1): + nuxt@4.0.0(@biomejs/biome@2.4.4)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(@vue/compiler-sfc@3.5.22)(better-sqlite3@11.10.0)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1): dependencies: '@nuxt/cli': 3.29.3(magicast@0.3.5) '@nuxt/devalue': 2.0.2 @@ -28794,7 +28831,7 @@ snapshots: '@nuxt/kit': 4.0.0(magicast@0.3.5) '@nuxt/schema': 4.0.0 '@nuxt/telemetry': 2.6.6(magicast@0.3.5) - '@nuxt/vite-builder': 4.0.0(@biomejs/biome@2.4.4)(@netlify/blobs@9.1.2)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(eslint@9.38.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1) + '@nuxt/vite-builder': 4.0.0(@biomejs/biome@2.4.4)(@netlify/blobs@9.1.2)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(eslint@9.38.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1) '@unhead/vue': 2.0.19(vue@3.5.22(typescript@5.9.3)) '@vue/shared': 3.5.22 c12: 3.3.1(magicast@0.3.5) @@ -28821,7 +28858,7 @@ snapshots: mlly: 1.8.0 mocked-exports: 0.1.1 nanotar: 0.2.0 - nitropack: 2.12.0(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) + nitropack: 2.12.0(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) nypm: 0.6.2 ofetch: 1.5.1 ohash: 2.0.11 @@ -28846,7 +28883,7 @@ snapshots: unimport: 5.5.0 unplugin: 2.3.10 unplugin-vue-router: 0.14.0(@vue/compiler-sfc@3.5.22)(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) - unstorage: 1.17.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2) + unstorage: 1.17.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2) untyped: 2.0.0 vue: 3.5.22(typescript@5.9.3) vue-bundle-renderer: 2.2.0 @@ -28912,7 +28949,7 @@ snapshots: - xml2js - yaml - nuxt@4.1.3(@biomejs/biome@2.4.4)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(@vue/compiler-sfc@3.5.22)(better-sqlite3@11.10.0)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1): + nuxt@4.1.3(@biomejs/biome@2.4.4)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.19.0)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(@vue/compiler-sfc@3.5.22)(better-sqlite3@11.10.0)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8))(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1): dependencies: '@nuxt/cli': 3.29.3(magicast@0.3.5) '@nuxt/devalue': 2.0.2 @@ -28947,7 +28984,7 @@ snapshots: mlly: 1.8.0 mocked-exports: 0.1.1 nanotar: 0.2.0 - nitropack: 2.12.7(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) + nitropack: 2.12.7(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) nypm: 0.6.2 ofetch: 1.4.1 ohash: 2.0.11 @@ -28971,7 +29008,7 @@ snapshots: unimport: 5.5.0 unplugin: 2.3.10 unplugin-vue-router: 0.15.0(@vue/compiler-sfc@3.5.22)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) - unstorage: 1.17.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2) + unstorage: 1.17.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2) untyped: 2.0.0 vue: 3.5.22(typescript@5.9.3) vue-bundle-renderer: 2.2.0 @@ -32005,7 +32042,7 @@ snapshots: picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 - unstorage@1.17.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2): + unstorage@1.17.1(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -32019,10 +32056,10 @@ snapshots: '@netlify/blobs': 9.1.2 '@vercel/blob': 2.0.0 '@vercel/functions': 3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13) - db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) + db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) ioredis: 5.8.2 - unstorage@1.17.3(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2): + unstorage@1.17.3(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -32036,10 +32073,10 @@ snapshots: '@netlify/blobs': 9.1.2 '@vercel/blob': 2.0.0 '@vercel/functions': 3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13) - db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) + db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) ioredis: 5.8.2 - unstorage@1.17.4(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2): + unstorage@1.17.4(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2): dependencies: anymatch: 3.1.3 chokidar: 5.0.0 @@ -32053,16 +32090,16 @@ snapshots: '@netlify/blobs': 9.1.2 '@vercel/blob': 2.0.0 '@vercel/functions': 3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13) - db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) + db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) ioredis: 5.8.2 - unstorage@2.0.0-alpha.4(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(chokidar@4.0.3)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3): + unstorage@2.0.0-alpha.4(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13))(chokidar@4.0.3)(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)))(ioredis@5.8.2)(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3): optionalDependencies: '@netlify/blobs': 9.1.2 '@vercel/blob': 2.0.0 '@vercel/functions': 3.4.3(@aws-sdk/credential-provider-web-identity@3.972.13) chokidar: 4.0.3 - db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) + db0: 0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20250312.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.8)) ioredis: 5.8.2 lru-cache: 11.2.2 ofetch: 2.0.0-alpha.3