diff --git a/.changeset/aws-world-initial.md b/.changeset/aws-world-initial.md new file mode 100644 index 0000000000..517b2dffde --- /dev/null +++ b/.changeset/aws-world-initial.md @@ -0,0 +1,5 @@ +--- +'@workflow/world-aws': patch +--- + +Add AWS World implementation using DynamoDB for storage and SQS for message queuing diff --git a/packages/world-aws/README.md b/packages/world-aws/README.md new file mode 100644 index 0000000000..bea5ee7266 --- /dev/null +++ b/packages/world-aws/README.md @@ -0,0 +1,165 @@ +# @workflow/world-aws + +An AWS World implementation for the [Workflow DevKit](https://useworkflow.dev), using **DynamoDB** for storage and **SQS** for message queuing. + +## Architecture + +This package uses AWS managed services to provide a production-ready, serverless-compatible World backend: + +| Concern | AWS Service | Details | +|---------|-------------|---------| +| **Storage** | DynamoDB | All entities (runs, events, steps, hooks, waits, stream chunks) are stored in DynamoDB tables with on-demand billing | +| **Queue** | SQS | Standard queues with per-message delay (up to 15 min) for workflow and step invocations | +| **Streaming** | DynamoDB | Stream chunks stored in DynamoDB with polling-based real-time delivery | + +### Why SQS over Postgres-backed queues? + +- **Fully managed** - No connection pools, no worker processes to manage +- **Per-message delay** - Native support for delayed delivery (up to 15 minutes), useful for step retries +- **Elastic scaling** - Automatically scales with traffic, no need to provision workers +- **Dead letter queues** - Built-in support for failed message routing + +## Installation + +```bash +npm install @workflow/world-aws +# or +pnpm add @workflow/world-aws +``` + +## Configuration + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `AWS_REGION` | AWS region | `us-east-1` | +| `WORKFLOW_AWS_TABLE_PREFIX` | DynamoDB table name prefix | `workflow` | +| `WORKFLOW_AWS_SQS_WORKFLOW_QUEUE_URL` | SQS queue URL for workflow invocations | (required) | +| `WORKFLOW_AWS_SQS_STEP_QUEUE_URL` | SQS queue URL for step invocations | (required) | +| `WORKFLOW_AWS_QUEUE_CONCURRENCY` | Max concurrent message processing | `10` | +| `WORKFLOW_AWS_POLL_INTERVAL_MS` | SQS polling interval (ms) | `1000` | +| `WORKFLOW_AWS_DYNAMODB_ENDPOINT` | Custom DynamoDB endpoint (for local dev) | - | +| `WORKFLOW_AWS_SQS_ENDPOINT` | Custom SQS endpoint (for local dev) | - | + +### Programmatic Configuration + +```typescript +import { createWorld } from '@workflow/world-aws'; + +const world = createWorld({ + region: 'us-west-2', + tablePrefix: 'myapp', + sqsWorkflowQueueUrl: 'https://sqs.us-west-2.amazonaws.com/123456789/myapp-workflows', + sqsStepQueueUrl: 'https://sqs.us-west-2.amazonaws.com/123456789/myapp-steps', + queueConcurrency: 20, +}); +``` + +## Setup + +### 1. Create DynamoDB Tables + +Run the setup CLI to create all required DynamoDB tables: + +```bash +npx workflow-aws-setup +``` + +This creates six tables with on-demand (PAY_PER_REQUEST) billing: +- `{prefix}_runs` - Workflow run entities +- `{prefix}_events` - Append-only event log +- `{prefix}_steps` - Step entities +- `{prefix}_hooks` - Webhook/notification hooks +- `{prefix}_waits` - Durable delay/sleep tracking +- `{prefix}_streams` - Streaming output chunks + +### 2. Create SQS Queues + +Create two standard SQS queues in your AWS account: + +```bash +aws sqs create-queue --queue-name myapp-workflows +aws sqs create-queue --queue-name myapp-steps +``` + +Set the queue URLs in your environment or configuration. + +### 3. IAM Permissions + +The following IAM permissions are required: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:DescribeTable", + "dynamodb:CreateTable" + ], + "Resource": "arn:aws:dynamodb:*:*:table/workflow_*" + }, + { + "Effect": "Allow", + "Action": [ + "sqs:SendMessage", + "sqs:ReceiveMessage", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes" + ], + "Resource": "arn:aws:sqs:*:*:myapp-*" + } + ] +} +``` + +## Local Development + +For local development, use [LocalStack](https://localstack.cloud/) or [DynamoDB Local](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html): + +```bash +# Start LocalStack +docker run -d -p 4566:4566 localstack/localstack + +# Configure endpoints +export WORKFLOW_AWS_DYNAMODB_ENDPOINT=http://localhost:4566 +export WORKFLOW_AWS_SQS_ENDPOINT=http://localhost:4566 + +# Create tables +npx workflow-aws-setup + +# Create SQS queues +aws --endpoint-url http://localhost:4566 sqs create-queue --queue-name workflow-workflows +aws --endpoint-url http://localhost:4566 sqs create-queue --queue-name workflow-steps +``` + +## Usage with Next.js + +```typescript +// workflow.config.ts +import { createWorld } from '@workflow/world-aws'; + +export const world = createWorld(); +``` + +## DynamoDB Table Design + +All tables use on-demand capacity (PAY_PER_REQUEST) and include Global Secondary Indexes for efficient querying: + +- **Runs**: PK=`runId`, GSIs on `workflowName` and `status` +- **Events**: PK=`runId`, SK=`eventId`, GSI on `correlationId` +- **Steps**: PK=`stepId`, GSIs on `runId` and `status` +- **Hooks**: PK=`hookId`, GSIs on `runId` and `token` +- **Waits**: PK=`waitId`, GSI on `runId` +- **Streams**: PK=`streamId`, SK=`chunkId`, GSI on `runId` + +Binary data (inputs, outputs, errors) is encoded using CBOR for efficient storage in DynamoDB's binary attribute type. diff --git a/packages/world-aws/bin/setup.js b/packages/world-aws/bin/setup.js new file mode 100644 index 0000000000..7c3d1548c8 --- /dev/null +++ b/packages/world-aws/bin/setup.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +import '../dist/cli.js'; diff --git a/packages/world-aws/package.json b/packages/world-aws/package.json new file mode 100644 index 0000000000..ab3185441a --- /dev/null +++ b/packages/world-aws/package.json @@ -0,0 +1,68 @@ +{ + "name": "@workflow/world-aws", + "version": "4.1.0-beta.1", + "description": "An AWS World implementation using DynamoDB and SQS", + "type": "module", + "main": "dist/index.js", + "bin": { + "workflow-aws-setup": "./bin/setup.js" + }, + "files": [ + "dist", + "bin" + ], + "publishConfig": { + "access": "public" + }, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/vercel/workflow.git", + "directory": "packages/world-aws" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./cli": { + "types": "./dist/cli.d.ts", + "default": "./dist/cli.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "tsc --build --clean && rm -rf dist", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@aws-sdk/client-dynamodb": "3.750.0", + "@aws-sdk/client-sqs": "3.750.0", + "@aws-sdk/client-s3": "3.750.0", + "@aws-sdk/util-dynamodb": "3.750.0", + "@vercel/queue": "catalog:", + "@workflow/errors": "workspace:*", + "@workflow/world": "workspace:*", + "@workflow/world-local": "workspace:*", + "cbor-x": "1.6.0", + "ulid": "catalog:", + "zod": "catalog:" + }, + "devDependencies": { + "@types/node": "catalog:", + "@workflow/tsconfig": "workspace:*", + "@workflow/world-testing": "workspace:*", + "vitest": "catalog:" + }, + "keywords": [ + "aws", + "dynamodb", + "sqs", + "workflow", + "durable-functions" + ], + "author": "", + "packageManager": "pnpm@10.15.1" +} diff --git a/packages/world-aws/src/cli.ts b/packages/world-aws/src/cli.ts new file mode 100644 index 0000000000..7c2280b4e6 --- /dev/null +++ b/packages/world-aws/src/cli.ts @@ -0,0 +1,40 @@ +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { resolveConfig, tableNames } from './config.js'; +import { ensureTables } from './dynamo.js'; + +async function main() { + console.log('[workflow-aws-setup] Setting up DynamoDB tables...'); + + const config = resolveConfig(); + const tables = tableNames(config.tablePrefix); + + console.log(`[workflow-aws-setup] Region: ${config.region}`); + console.log(`[workflow-aws-setup] Table prefix: ${config.tablePrefix}`); + if (config.dynamoDbEndpoint) { + console.log( + `[workflow-aws-setup] DynamoDB endpoint: ${config.dynamoDbEndpoint}` + ); + } + console.log('[workflow-aws-setup] Tables to create:'); + for (const [key, name] of Object.entries(tables)) { + console.log(` - ${key}: ${name}`); + } + + const client = new DynamoDBClient({ + region: config.region, + endpoint: config.dynamoDbEndpoint, + ...config.dynamoDbConfig, + }); + + try { + await ensureTables(client, config.tablePrefix); + console.log('[workflow-aws-setup] All tables created successfully.'); + } catch (err) { + console.error('[workflow-aws-setup] Failed to create tables:', err); + process.exit(1); + } finally { + client.destroy(); + } +} + +main(); diff --git a/packages/world-aws/src/config.ts b/packages/world-aws/src/config.ts new file mode 100644 index 0000000000..eddf6d5f70 --- /dev/null +++ b/packages/world-aws/src/config.ts @@ -0,0 +1,99 @@ +import type { DynamoDBClientConfig } from '@aws-sdk/client-dynamodb'; +import type { SQSClientConfig } from '@aws-sdk/client-sqs'; +import type { S3ClientConfig } from '@aws-sdk/client-s3'; + +export interface AwsWorldConfig { + /** AWS region (default: from AWS_REGION env var or 'us-east-1') */ + region?: string; + + /** Prefix for all DynamoDB table names (default: 'workflow') */ + tablePrefix?: string; + + /** + * SQS queue URLs. If not provided, they will be derived from the queue prefix + * and region. For FIFO queues, append '.fifo' to the URL. + */ + sqsWorkflowQueueUrl?: string; + sqsStepQueueUrl?: string; + + /** Concurrency limit for SQS message polling (default: 10) */ + queueConcurrency?: number; + + /** Polling interval in milliseconds for SQS long-polling (default: 1000) */ + pollIntervalMs?: number; + + /** Optional custom DynamoDB client configuration */ + dynamoDbConfig?: DynamoDBClientConfig; + + /** Optional custom SQS client configuration */ + sqsConfig?: SQSClientConfig; + + /** Optional custom S3 client configuration (for stream chunks, optional) */ + s3Config?: S3ClientConfig; + + /** + * Optional DynamoDB endpoint override (useful for local development with + * DynamoDB Local or LocalStack) + */ + dynamoDbEndpoint?: string; + + /** Optional SQS endpoint override */ + sqsEndpoint?: string; +} + +export function resolveConfig( + config?: Partial +): Required< + Pick< + AwsWorldConfig, + 'region' | 'tablePrefix' | 'queueConcurrency' | 'pollIntervalMs' + > +> & + AwsWorldConfig { + const region = + config?.region || + process.env.AWS_REGION || + process.env.AWS_DEFAULT_REGION || + 'us-east-1'; + + const tablePrefix = + config?.tablePrefix || process.env.WORKFLOW_AWS_TABLE_PREFIX || 'workflow'; + + const queueConcurrency = + config?.queueConcurrency || + parseInt(process.env.WORKFLOW_AWS_QUEUE_CONCURRENCY || '10', 10) || + 10; + + const pollIntervalMs = + config?.pollIntervalMs || + parseInt(process.env.WORKFLOW_AWS_POLL_INTERVAL_MS || '1000', 10) || + 1000; + + return { + ...config, + region, + tablePrefix, + queueConcurrency, + pollIntervalMs, + sqsWorkflowQueueUrl: + config?.sqsWorkflowQueueUrl || + process.env.WORKFLOW_AWS_SQS_WORKFLOW_QUEUE_URL, + sqsStepQueueUrl: + config?.sqsStepQueueUrl || process.env.WORKFLOW_AWS_SQS_STEP_QUEUE_URL, + dynamoDbEndpoint: + config?.dynamoDbEndpoint || process.env.WORKFLOW_AWS_DYNAMODB_ENDPOINT, + sqsEndpoint: config?.sqsEndpoint || process.env.WORKFLOW_AWS_SQS_ENDPOINT, + }; +} + +/** DynamoDB table names derived from a prefix. */ +export function tableNames(prefix: string) { + return { + runs: `${prefix}_runs`, + events: `${prefix}_events`, + steps: `${prefix}_steps`, + hooks: `${prefix}_hooks`, + waits: `${prefix}_waits`, + streams: `${prefix}_streams`, + } as const; +} diff --git a/packages/world-aws/src/dynamo.test.ts b/packages/world-aws/src/dynamo.test.ts new file mode 100644 index 0000000000..d9f85a66b5 --- /dev/null +++ b/packages/world-aws/src/dynamo.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from 'vitest'; +import { getTableDefinitions } from './dynamo.js'; +import { tableNames } from './config.js'; + +describe('DynamoDB table definitions', () => { + it('generates correct table names with prefix', () => { + const names = tableNames('myapp'); + expect(names.runs).toBe('myapp_runs'); + expect(names.events).toBe('myapp_events'); + expect(names.steps).toBe('myapp_steps'); + expect(names.hooks).toBe('myapp_hooks'); + expect(names.waits).toBe('myapp_waits'); + expect(names.streams).toBe('myapp_streams'); + }); + + it('generates table definitions for all tables', () => { + const defs = getTableDefinitions('workflow'); + expect(defs).toHaveLength(6); + + const tableNamesList = defs.map((d) => d.TableName); + expect(tableNamesList).toContain('workflow_runs'); + expect(tableNamesList).toContain('workflow_events'); + expect(tableNamesList).toContain('workflow_steps'); + expect(tableNamesList).toContain('workflow_hooks'); + expect(tableNamesList).toContain('workflow_waits'); + expect(tableNamesList).toContain('workflow_streams'); + }); + + it('uses PAY_PER_REQUEST billing for all tables', () => { + const defs = getTableDefinitions('workflow'); + for (const def of defs) { + expect(def.BillingMode).toBe('PAY_PER_REQUEST'); + } + }); + + it('creates runs table with correct GSIs', () => { + const defs = getTableDefinitions('workflow'); + const runsDef = defs.find((d) => d.TableName === 'workflow_runs')!; + + expect(runsDef.KeySchema).toEqual([ + { AttributeName: 'runId', KeyType: 'HASH' }, + ]); + + const gsiNames = runsDef.GlobalSecondaryIndexes?.map((g) => g.IndexName); + expect(gsiNames).toContain('gsi_workflowName'); + expect(gsiNames).toContain('gsi_status'); + }); + + it('creates events table with composite key', () => { + const defs = getTableDefinitions('workflow'); + const eventsDef = defs.find((d) => d.TableName === 'workflow_events')!; + + expect(eventsDef.KeySchema).toEqual([ + { AttributeName: 'runId', KeyType: 'HASH' }, + { AttributeName: 'eventId', KeyType: 'RANGE' }, + ]); + + const gsiNames = eventsDef.GlobalSecondaryIndexes?.map((g) => g.IndexName); + expect(gsiNames).toContain('gsi_correlationId'); + }); + + it('creates streams table with composite key and runId GSI', () => { + const defs = getTableDefinitions('workflow'); + const streamsDef = defs.find((d) => d.TableName === 'workflow_streams')!; + + expect(streamsDef.KeySchema).toEqual([ + { AttributeName: 'streamId', KeyType: 'HASH' }, + { AttributeName: 'chunkId', KeyType: 'RANGE' }, + ]); + + const gsiNames = streamsDef.GlobalSecondaryIndexes?.map((g) => g.IndexName); + expect(gsiNames).toContain('gsi_runId'); + }); +}); diff --git a/packages/world-aws/src/dynamo.ts b/packages/world-aws/src/dynamo.ts new file mode 100644 index 0000000000..0282b8a0c6 --- /dev/null +++ b/packages/world-aws/src/dynamo.ts @@ -0,0 +1,175 @@ +import { + CreateTableCommand, + type CreateTableInput, + DescribeTableCommand, + DynamoDBClient, + type GlobalSecondaryIndex, +} from '@aws-sdk/client-dynamodb'; +import type { AwsWorldConfig } from './config.js'; +import { tableNames } from './config.js'; + +export function createDynamoClient(config: AwsWorldConfig): DynamoDBClient { + return new DynamoDBClient({ + region: config.region, + endpoint: config.dynamoDbEndpoint, + ...config.dynamoDbConfig, + }); +} + +/** + * Table definitions for all DynamoDB tables used by the AWS world. + * Uses on-demand (PAY_PER_REQUEST) billing by default. + */ +export function getTableDefinitions(prefix: string): CreateTableInput[] { + const names = tableNames(prefix); + + return [ + // Runs table + { + TableName: names.runs, + KeySchema: [{ AttributeName: 'runId', KeyType: 'HASH' }], + AttributeDefinitions: [ + { AttributeName: 'runId', AttributeType: 'S' }, + { AttributeName: 'workflowName', AttributeType: 'S' }, + { AttributeName: 'status', AttributeType: 'S' }, + { AttributeName: 'createdAt', AttributeType: 'S' }, + ], + GlobalSecondaryIndexes: [ + gsi('gsi_workflowName', 'workflowName', 'createdAt'), + gsi('gsi_status', 'status', 'createdAt'), + ], + BillingMode: 'PAY_PER_REQUEST', + }, + + // Events table + { + TableName: names.events, + KeySchema: [ + { AttributeName: 'runId', KeyType: 'HASH' }, + { AttributeName: 'eventId', KeyType: 'RANGE' }, + ], + AttributeDefinitions: [ + { AttributeName: 'runId', AttributeType: 'S' }, + { AttributeName: 'eventId', AttributeType: 'S' }, + { AttributeName: 'correlationId', AttributeType: 'S' }, + { AttributeName: 'createdAt', AttributeType: 'S' }, + ], + GlobalSecondaryIndexes: [ + gsi('gsi_correlationId', 'correlationId', 'createdAt'), + ], + BillingMode: 'PAY_PER_REQUEST', + }, + + // Steps table + { + TableName: names.steps, + KeySchema: [{ AttributeName: 'stepId', KeyType: 'HASH' }], + AttributeDefinitions: [ + { AttributeName: 'stepId', AttributeType: 'S' }, + { AttributeName: 'runId', AttributeType: 'S' }, + { AttributeName: 'status', AttributeType: 'S' }, + ], + GlobalSecondaryIndexes: [ + gsi('gsi_runId', 'runId', 'stepId'), + gsi('gsi_status', 'status', 'stepId'), + ], + BillingMode: 'PAY_PER_REQUEST', + }, + + // Hooks table + { + TableName: names.hooks, + KeySchema: [{ AttributeName: 'hookId', KeyType: 'HASH' }], + AttributeDefinitions: [ + { AttributeName: 'hookId', AttributeType: 'S' }, + { AttributeName: 'runId', AttributeType: 'S' }, + { AttributeName: 'token', AttributeType: 'S' }, + ], + GlobalSecondaryIndexes: [ + gsi('gsi_runId', 'runId', 'hookId'), + gsi('gsi_token', 'token', 'hookId'), + ], + BillingMode: 'PAY_PER_REQUEST', + }, + + // Waits table + { + TableName: names.waits, + KeySchema: [{ AttributeName: 'waitId', KeyType: 'HASH' }], + AttributeDefinitions: [ + { AttributeName: 'waitId', AttributeType: 'S' }, + { AttributeName: 'runId', AttributeType: 'S' }, + ], + GlobalSecondaryIndexes: [gsi('gsi_runId', 'runId', 'waitId')], + BillingMode: 'PAY_PER_REQUEST', + }, + + // Streams table + { + TableName: names.streams, + KeySchema: [ + { AttributeName: 'streamId', KeyType: 'HASH' }, + { AttributeName: 'chunkId', KeyType: 'RANGE' }, + ], + AttributeDefinitions: [ + { AttributeName: 'streamId', AttributeType: 'S' }, + { AttributeName: 'chunkId', AttributeType: 'S' }, + { AttributeName: 'runId', AttributeType: 'S' }, + ], + GlobalSecondaryIndexes: [gsi('gsi_runId', 'runId', 'streamId')], + BillingMode: 'PAY_PER_REQUEST', + }, + ]; +} + +function gsi( + name: string, + hashKey: string, + rangeKey: string +): GlobalSecondaryIndex { + return { + IndexName: name, + KeySchema: [ + { AttributeName: hashKey, KeyType: 'HASH' }, + { AttributeName: rangeKey, KeyType: 'RANGE' }, + ], + Projection: { ProjectionType: 'ALL' }, + }; +} + +/** Create all DynamoDB tables if they don't already exist. */ +export async function ensureTables( + client: DynamoDBClient, + prefix: string +): Promise { + const definitions = getTableDefinitions(prefix); + + for (const def of definitions) { + try { + await client.send(new DescribeTableCommand({ TableName: def.TableName })); + } catch (err: any) { + if (err.name === 'ResourceNotFoundException') { + await client.send(new CreateTableCommand(def)); + // Wait for table to become active + await waitForTable(client, def.TableName!); + } else { + throw err; + } + } + } +} + +async function waitForTable( + client: DynamoDBClient, + tableName: string, + maxAttempts = 30 +): Promise { + for (let i = 0; i < maxAttempts; i++) { + const { Table } = await client.send( + new DescribeTableCommand({ TableName: tableName }) + ); + if (Table?.TableStatus === 'ACTIVE') return; + await new Promise((r) => setTimeout(r, 1000)); + } + throw new Error(`Table ${tableName} did not become ACTIVE`); +} diff --git a/packages/world-aws/src/index.ts b/packages/world-aws/src/index.ts new file mode 100644 index 0000000000..10595fdcdf --- /dev/null +++ b/packages/world-aws/src/index.ts @@ -0,0 +1,67 @@ +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { SQSClient } from '@aws-sdk/client-sqs'; +import type { Storage, World } from '@workflow/world'; +import type { AwsWorldConfig } from './config.js'; +import { resolveConfig, tableNames } from './config.js'; +import { createQueue } from './queue.js'; +import { + createEventsStorage, + createHooksStorage, + createRunsStorage, + createStepsStorage, +} from './storage.js'; +import { createStreamer } from './streamer.js'; + +function createStorage( + dynamo: DynamoDBClient, + tables: ReturnType +): Storage { + return { + runs: createRunsStorage(dynamo, tables), + events: createEventsStorage(dynamo, tables), + hooks: createHooksStorage(dynamo, tables), + steps: createStepsStorage(dynamo, tables), + }; +} + +export function createWorld( + config?: Partial +): World & { start(): Promise } { + const resolved = resolveConfig(config); + const tables = tableNames(resolved.tablePrefix); + + const dynamo = new DynamoDBClient({ + region: resolved.region, + endpoint: resolved.dynamoDbEndpoint, + ...config?.dynamoDbConfig, + }); + + const sqs = new SQSClient({ + region: resolved.region, + endpoint: resolved.sqsEndpoint, + ...config?.sqsConfig, + }); + + const storage = createStorage(dynamo, tables); + const queue = createQueue(resolved, sqs); + const streamer = createStreamer(dynamo, tables); + + return { + ...storage, + ...streamer, + ...queue, + async start() { + await queue.start(); + }, + async close() { + await streamer.close(); + await queue.close(); + dynamo.destroy(); + sqs.destroy(); + }, + }; +} + +export type { AwsWorldConfig } from './config.js'; +export { ensureTables } from './dynamo.js'; +export { tableNames } from './config.js'; diff --git a/packages/world-aws/src/queue.test.ts b/packages/world-aws/src/queue.test.ts new file mode 100644 index 0000000000..d7e482996d --- /dev/null +++ b/packages/world-aws/src/queue.test.ts @@ -0,0 +1,153 @@ +import { JsonTransport } from '@vercel/queue'; +import { MessageId, type QueuePayload } from '@workflow/world'; +import { + DeleteMessageCommand, + ReceiveMessageCommand, + SQSClient, + SendMessageCommand, +} from '@aws-sdk/client-sqs'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createLocalWorld, createQueueExecutor } from '@workflow/world-local'; +import { createQueue } from './queue.js'; + +const transport = new JsonTransport(); + +vi.mock('@aws-sdk/client-sqs', () => { + const SQSClient = vi.fn(); + SQSClient.prototype.send = vi.fn(); + return { + SQSClient, + SendMessageCommand: vi.fn(), + ReceiveMessageCommand: vi.fn(), + DeleteMessageCommand: vi.fn(), + }; +}); + +vi.mock('@workflow/world-local', () => ({ + createLocalWorld: vi.fn(), + createQueueExecutor: vi.fn(), +})); + +describe('aws queue', () => { + const sqsSendMock = vi.fn(); + 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); + + beforeEach(() => { + vi.clearAllMocks(); + + const mockSqs = { send: sqsSendMock, destroy: vi.fn() } as any; + vi.mocked(createQueueExecutor).mockReturnValue({ + executeMessage, + registerHandler, + close: executorClose, + }); + vi.mocked(createLocalWorld).mockReturnValue({ + createQueueHandler, + close: localWorldClose, + } as any); + }); + + function createTestQueue() { + const mockSqs = { send: sqsSendMock, destroy: vi.fn() } as any; + return createQueue( + { + region: 'us-east-1', + tablePrefix: 'workflow', + queueConcurrency: 10, + pollIntervalMs: 1000, + sqsWorkflowQueueUrl: + 'https://sqs.us-east-1.amazonaws.com/123/workflow-wf', + sqsStepQueueUrl: + 'https://sqs.us-east-1.amazonaws.com/123/workflow-step', + }, + mockSqs + ); + } + + it('registers queue handlers with the shared executor', () => { + const queue = createTestQueue(); + 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('returns deployment id as "aws"', async () => { + const queue = createTestQueue(); + const deploymentId = await queue.getDeploymentId(); + expect(deploymentId).toBe('aws'); + }); + + it('sends messages to SQS with delay', async () => { + sqsSendMock.mockResolvedValue({}); + + const queue = createTestQueue(); + await queue.start(); + + await queue.queue( + '__wkf_step_test-step', + { + workflowName: 'test-workflow', + workflowRunId: 'run_01ABC', + workflowStartedAt: Date.now(), + stepId: 'step_01ABC', + }, + { + delaySeconds: 5, + headers: { traceparent: 'trace-parent' }, + idempotencyKey: 'step_01ABC', + } + ); + + expect(sqsSendMock).toHaveBeenCalled(); + // Verify SendMessageCommand was called with the right queue URL + const sendCall = vi.mocked(SendMessageCommand).mock.calls[0]; + expect(sendCall[0]).toMatchObject({ + QueueUrl: 'https://sqs.us-east-1.amazonaws.com/123/workflow-step', + DelaySeconds: 5, + }); + // Verify message body contains the right data + const body = JSON.parse(sendCall[0].MessageBody!); + expect(body.id).toBe('test-step'); + expect(body.attempt).toBe(1); + expect(body.idempotencyKey).toBe('step_01ABC'); + expect(body.headers).toEqual({ traceparent: 'trace-parent' }); + }); + + it('caps delay to 900 seconds (SQS max)', async () => { + sqsSendMock.mockResolvedValue({}); + + const queue = createTestQueue(); + await queue.start(); + + await queue.queue( + '__wkf_workflow_test-wf', + { + runId: 'run_01ABC', + }, + { + delaySeconds: 2000, + } + ); + + const sendCall = vi.mocked(SendMessageCommand).mock.calls[0]; + expect(sendCall[0].DelaySeconds).toBe(900); + }); + + it('closes cleanly', async () => { + const queue = createTestQueue(); + await queue.start(); + await queue.close(); + + expect(executorClose).toHaveBeenCalled(); + expect(localWorldClose).toHaveBeenCalled(); + }); +}); diff --git a/packages/world-aws/src/queue.ts b/packages/world-aws/src/queue.ts new file mode 100644 index 0000000000..8ac3a52855 --- /dev/null +++ b/packages/world-aws/src/queue.ts @@ -0,0 +1,374 @@ +import * as Stream from 'node:stream'; +import { + DeleteMessageCommand, + ReceiveMessageCommand, + SQSClient, + SendMessageCommand, +} from '@aws-sdk/client-sqs'; +import { JsonTransport } from '@vercel/queue'; +import { + MessageId, + type Queue, + QueuePayloadSchema, + type QueuePrefix, + type ValidQueueName, +} from '@workflow/world'; +import { createLocalWorld, createQueueExecutor } from '@workflow/world-local'; +import { monotonicFactory } from 'ulid'; +import z from 'zod'; +import type { AwsWorldConfig } from './config.js'; + +/** Maximum SQS message delay (15 minutes). */ +const MAX_SQS_DELAY_SECONDS = 900; + +const MessageData = z.object({ + attempt: z.number().describe('The attempt number of the message'), + messageId: z.string().describe('The unique ID of the message'), + idempotencyKey: z.string().optional(), + headers: z.record(z.string(), z.string()).optional(), + id: z.string().describe('The ID of the sub-queue (workflow/step name)'), + data: z.string().describe('Base64-encoded message body'), +}); +type MessageData = z.infer; + +const COMPLETED_IDEMPOTENCY_CACHE_LIMIT = 10_000; + +/** + * The AWS queue works by creating two SQS standard queues: + * - One for workflow invocations + * - One for step invocations + * + * When a message is queued, it is sent to SQS with the appropriate queue URL. + * A background poller receives messages, deserializes them, and executes them + * via the local world queue executor (same pattern as the Postgres world). + * + * SQS standard queues support per-message delay (up to 15 minutes), which maps + * naturally to the workflow retry/sleep mechanism. + */ +export type AwsQueue = Queue & { + start(): Promise; + close(): Promise; +}; + +export function createQueue( + config: AwsWorldConfig, + sqsClient: SQSClient +): AwsQueue { + const port = process.env.PORT ? Number(process.env.PORT) : undefined; + const localWorld = createLocalWorld({ dataDir: undefined, port }); + const executor = createQueueExecutor({ port }); + + const transport = new JsonTransport(); + const generateMessageId = monotonicFactory(); + + const Queues: Record = { + __wkf_workflow_: config.sqsWorkflowQueueUrl, + __wkf_step_: config.sqsStepQueueUrl, + }; + + function getQueueUrl(prefix: QueuePrefix): string { + const url = Queues[prefix]; + if (!url) { + throw new Error( + `SQS queue URL not configured for prefix "${prefix}". ` + + `Set WORKFLOW_AWS_SQS_WORKFLOW_QUEUE_URL and WORKFLOW_AWS_SQS_STEP_QUEUE_URL.` + ); + } + return url; + } + + const createQueueHandler: Queue['createQueueHandler'] = (prefix, handler) => { + const wrappedHandler = localWorld.createQueueHandler(prefix, handler); + executor.registerHandler(prefix, wrappedHandler); + return wrappedHandler; + }; + + const getDeploymentId: Queue['getDeploymentId'] = async () => { + return 'aws'; + }; + + const completedMessages = new Set(); + const inflightMessages = new Map>(); + let polling = false; + let pollTimers: ReturnType[] = []; + let startPromise: Promise | null = null; + + function markMessageCompleted(idempotencyKey: string) { + completedMessages.delete(idempotencyKey); + completedMessages.add(idempotencyKey); + if (completedMessages.size > COMPLETED_IDEMPOTENCY_CACHE_LIMIT) { + const oldestKey = completedMessages.values().next().value; + if (oldestKey) { + completedMessages.delete(oldestKey); + } + } + } + + async function sendSqsMessage({ + queuePrefix, + queueId, + body, + messageId, + attempt, + idempotencyKey, + headers, + delaySeconds, + }: { + queuePrefix: QueuePrefix; + queueId: string; + body: Buffer | Uint8Array; + messageId: MessageId; + attempt: number; + idempotencyKey?: string; + headers?: Record; + delaySeconds?: number; + }) { + const queueUrl = getQueueUrl(queuePrefix); + const messageData: MessageData = { + id: queueId, + data: Buffer.from(body).toString('base64'), + attempt, + messageId, + idempotencyKey, + headers, + }; + + const effectiveDelay = Math.min( + Math.max(0, delaySeconds ?? 0), + MAX_SQS_DELAY_SECONDS + ); + + await sqsClient.send( + new SendMessageCommand({ + QueueUrl: queueUrl, + MessageBody: JSON.stringify(messageData), + DelaySeconds: effectiveDelay > 0 ? effectiveDelay : undefined, + MessageGroupId: undefined, // standard queues don't use this + }) + ); + } + + const queue: Queue['queue'] = async (queueName, message, opts) => { + await start(); + const [queuePrefix, queueId] = parseQueueName(queueName); + const body = transport.serialize(message); + const messageId = MessageId.parse(`msg_${generateMessageId()}`); + + await sendSqsMessage({ + queuePrefix, + queueId, + body, + messageId, + attempt: 1, + idempotencyKey: opts?.idempotencyKey, + headers: opts?.headers, + delaySeconds: opts?.delaySeconds, + }); + return { messageId }; + }; + + async function processMessage( + queuePrefix: QueuePrefix, + rawMessage: string, + receiptHandle: string + ): Promise { + const messageData = MessageData.parse(JSON.parse(rawMessage)); + const queueUrl = getQueueUrl(queuePrefix); + + const executeTask = async (): Promise<'completed' | 'rescheduled'> => { + const bodyBuffer = Buffer.from(messageData.data, 'base64'); + const bodyStream = Stream.Readable.toWeb( + Stream.Readable.from([bodyBuffer]) + ); + const body = await transport.deserialize( + bodyStream as ReadableStream + ); + QueuePayloadSchema.parse(body); + const queueName = `${queuePrefix}${messageData.id}` as const; + const result = await executor.executeMessage({ + queueName, + messageId: MessageId.parse(messageData.messageId), + attempt: messageData.attempt, + body: bodyBuffer, + headers: messageData.headers, + }); + + if (result.type === 'completed') { + return 'completed'; + } + + if (result.type === 'reschedule') { + // Schedule the follow-up message before deleting the current one + await sendSqsMessage({ + queuePrefix, + queueId: messageData.id, + body: bodyBuffer, + messageId: MessageId.parse(messageData.messageId), + attempt: messageData.attempt + 1, + idempotencyKey: messageData.idempotencyKey, + headers: messageData.headers, + delaySeconds: result.timeoutSeconds, + }); + return 'rescheduled'; + } + + throw new Error( + `[aws world] Queue execution failed (${result.status}): ${result.text}` + ); + }; + + const idempotencyKey = messageData.idempotencyKey; + if (!idempotencyKey) { + await executeTask(); + // Delete message from SQS after processing + await sqsClient.send( + new DeleteMessageCommand({ + QueueUrl: queueUrl, + ReceiptHandle: receiptHandle, + }) + ); + return; + } + + if (completedMessages.has(idempotencyKey)) { + await sqsClient.send( + new DeleteMessageCommand({ + QueueUrl: queueUrl, + ReceiptHandle: receiptHandle, + }) + ); + return; + } + + const existing = inflightMessages.get(idempotencyKey); + if (existing) { + await existing; + await sqsClient.send( + new DeleteMessageCommand({ + QueueUrl: queueUrl, + ReceiptHandle: receiptHandle, + }) + ); + return; + } + + const execution = executeTask() + .then((result) => { + if (result === 'completed') { + markMessageCompleted(idempotencyKey); + } + }) + .finally(() => { + inflightMessages.delete(idempotencyKey); + }); + inflightMessages.set(idempotencyKey, execution); + await execution; + await sqsClient.send( + new DeleteMessageCommand({ + QueueUrl: queueUrl, + ReceiptHandle: receiptHandle, + }) + ); + } + + function startPolling(queuePrefix: QueuePrefix) { + const queueUrl = Queues[queuePrefix]; + if (!queueUrl) return; + + const concurrency = config.queueConcurrency ?? 10; + const pollInterval = config.pollIntervalMs ?? 1000; + + async function poll() { + if (!polling) return; + + try { + const result = await sqsClient.send( + new ReceiveMessageCommand({ + QueueUrl: queueUrl, + MaxNumberOfMessages: Math.min(concurrency, 10), // SQS max is 10 + WaitTimeSeconds: 20, // Long polling + }) + ); + + if (result.Messages?.length) { + await Promise.all( + result.Messages.map(async (msg) => { + if (msg.Body && msg.ReceiptHandle) { + try { + await processMessage( + queuePrefix, + msg.Body, + msg.ReceiptHandle + ); + } catch (err) { + // Log error but don't crash the poller + const pipe = + process.env.WORKFLOW_JSON_MODE === '1' + ? process.stdout + : process.stderr; + pipe.write(`[aws world] Error processing message: ${err}\n`); + } + } + }) + ); + } + } catch (err: any) { + if (polling) { + const pipe = + process.env.WORKFLOW_JSON_MODE === '1' + ? process.stdout + : process.stderr; + pipe.write(`[aws world] Polling error: ${err}\n`); + } + } + + if (polling) { + const timer = setTimeout(poll, pollInterval); + pollTimers.push(timer); + } + } + + // Start immediately + poll(); + } + + async function start(): Promise { + if (!startPromise) { + startPromise = (async () => { + polling = true; + const prefixes: QueuePrefix[] = ['__wkf_workflow_', '__wkf_step_']; + for (const prefix of prefixes) { + startPolling(prefix); + } + })(); + } + await startPromise; + } + + return { + createQueueHandler, + getDeploymentId, + queue, + start, + async close() { + polling = false; + for (const timer of pollTimers) { + clearTimeout(timer); + } + pollTimers = []; + startPromise = null; + await executor.close(); + await localWorld.close?.(); + }, + }; +} + +const parseQueueName = (name: ValidQueueName): [QueuePrefix, string] => { + const prefixes: QueuePrefix[] = ['__wkf_step_', '__wkf_workflow_']; + for (const prefix of prefixes) { + if (name.startsWith(prefix)) { + return [prefix, name.slice(prefix.length)]; + } + } + throw new Error(`Invalid queue name: ${name}`); +}; diff --git a/packages/world-aws/src/storage.ts b/packages/world-aws/src/storage.ts new file mode 100644 index 0000000000..76d6de2f0c --- /dev/null +++ b/packages/world-aws/src/storage.ts @@ -0,0 +1,1688 @@ +import { + HookNotFoundError, + RunNotSupportedError, + WorkflowAPIError, +} from '@workflow/errors'; +import type { + Event, + EventResult, + GetEventParams, + Hook, + ListEventsParams, + ListEventsByCorrelationIdParams, + ListHooksParams, + PaginatedResponse, + ResolveData, + Step, + StepWithoutData, + Storage, + StructuredError, + Wait, + WorkflowRun, + WorkflowRunWithoutData, +} from '@workflow/world'; +import { + EventSchema, + HookSchema, + isLegacySpecVersion, + requiresNewerWorld, + SPEC_VERSION_CURRENT, + StepSchema, + validateUlidTimestamp, + WaitSchema, + WorkflowRunSchema, +} from '@workflow/world'; +import { + BatchWriteItemCommand, + ConditionalCheckFailedException, + DeleteItemCommand, + DynamoDBClient, + GetItemCommand, + PutItemCommand, + QueryCommand, + ScanCommand, + UpdateItemCommand, +} from '@aws-sdk/client-dynamodb'; +import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; +import { monotonicFactory } from 'ulid'; +import { tableNames } from './config.js'; +import { cborDecode, cborEncode, compact, fromIso, toIso } from './util.js'; + +// ============================================================ +// DynamoDB Item marshalling helpers +// ============================================================ + +// Marshall a JS object into DynamoDB attribute map, stripping undefined values +function marshallItem(item: Record) { + return marshall(item, { removeUndefinedValues: true }); +} + +// ============================================================ +// Run serialization helpers +// ============================================================ + +function dynamoToRun(item: Record): WorkflowRun { + const raw = { + runId: item.runId, + deploymentId: item.deploymentId, + workflowName: item.workflowName, + status: item.status, + specVersion: item.specVersion, + input: item.input ? cborDecode(item.input) : undefined, + output: item.output ? cborDecode(item.output) : undefined, + error: item.error ? cborDecode(item.error) : undefined, + executionContext: item.executionContext + ? cborDecode(item.executionContext) + : undefined, + createdAt: item.createdAt ? fromIso(item.createdAt) : new Date(), + updatedAt: item.updatedAt ? fromIso(item.updatedAt) : new Date(), + startedAt: item.startedAt ? fromIso(item.startedAt) : undefined, + completedAt: item.completedAt ? fromIso(item.completedAt) : undefined, + expiredAt: item.expiredAt ? fromIso(item.expiredAt) : undefined, + }; + return WorkflowRunSchema.parse(compact(raw)); +} + +function dynamoToStep(item: Record): Step { + const raw = { + runId: item.runId, + stepId: item.stepId, + stepName: item.stepName, + status: item.status, + attempt: item.attempt ?? 0, + specVersion: item.specVersion, + input: item.input ? cborDecode(item.input) : undefined, + output: item.output ? cborDecode(item.output) : undefined, + error: item.error ? cborDecode(item.error) : undefined, + createdAt: item.createdAt ? fromIso(item.createdAt) : new Date(), + updatedAt: item.updatedAt ? fromIso(item.updatedAt) : new Date(), + startedAt: item.startedAt ? fromIso(item.startedAt) : undefined, + completedAt: item.completedAt ? fromIso(item.completedAt) : undefined, + retryAfter: item.retryAfter ? fromIso(item.retryAfter) : undefined, + }; + return StepSchema.parse(compact(raw)); +} + +function dynamoToEvent(item: Record): Event { + const raw = { + runId: item.runId, + eventId: item.eventId, + eventType: item.eventType, + correlationId: item.correlationId, + eventData: item.eventData ? cborDecode(item.eventData) : undefined, + specVersion: item.specVersion, + createdAt: item.createdAt ? fromIso(item.createdAt) : new Date(), + }; + return EventSchema.parse(compact(raw)); +} + +function dynamoToHook(item: Record): Hook { + const raw = { + runId: item.runId, + hookId: item.hookId, + token: item.token, + ownerId: item.ownerId ?? '', + projectId: item.projectId ?? '', + environment: item.environment ?? '', + metadata: item.metadata ? cborDecode(item.metadata) : undefined, + specVersion: item.specVersion, + isWebhook: item.isWebhook ?? true, + createdAt: item.createdAt ? fromIso(item.createdAt) : new Date(), + }; + const parsed = HookSchema.parse(compact(raw)); + parsed.isWebhook ??= true; + return parsed; +} + +function dynamoToWait(item: Record): Wait { + const raw = { + waitId: item.waitId, + runId: item.runId, + status: item.status, + resumeAt: item.resumeAt ? fromIso(item.resumeAt) : undefined, + completedAt: item.completedAt ? fromIso(item.completedAt) : undefined, + createdAt: item.createdAt ? fromIso(item.createdAt) : new Date(), + updatedAt: item.updatedAt ? fromIso(item.updatedAt) : new Date(), + specVersion: item.specVersion, + }; + return WaitSchema.parse(compact(raw)); +} + +// ============================================================ +// Data filtering helpers +// ============================================================ + +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; +} + +// ============================================================ +// Runs Storage +// ============================================================ + +export function createRunsStorage( + dynamo: DynamoDBClient, + tables: ReturnType +): Storage['runs'] { + return { + get: (async (id: string, params?: any) => { + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.runs, + Key: marshall({ runId: id }), + }) + ); + if (!result.Item) { + throw new WorkflowAPIError(`Run not found: ${id}`, { status: 404 }); + } + const item = unmarshall(result.Item); + const run = dynamoToRun(item); + const resolveData = params?.resolveData ?? 'all'; + return filterRunData(run, resolveData); + }) as Storage['runs']['get'], + + list: (async (params?: any) => { + const limit = params?.pagination?.limit ?? 20; + const fromCursor = params?.pagination?.cursor; + const resolveData = params?.resolveData ?? 'all'; + + // Build query or scan depending on filters + let items: Record[]; + + if (params?.workflowName) { + // Use GSI on workflowName + const queryParams: any = { + TableName: tables.runs, + IndexName: 'gsi_workflowName', + KeyConditionExpression: 'workflowName = :wn', + ExpressionAttributeValues: marshall({ + ':wn': params.workflowName, + }), + ScanIndexForward: false, + Limit: limit + 1, + }; + if (fromCursor) { + const cursorItem = await dynamo.send( + new GetItemCommand({ + TableName: tables.runs, + Key: marshall({ runId: fromCursor }), + ProjectionExpression: 'runId, workflowName, createdAt', + }) + ); + if (cursorItem.Item) { + queryParams.ExclusiveStartKey = cursorItem.Item; + } + } + const result = await dynamo.send(new QueryCommand(queryParams)); + items = (result.Items ?? []).map((i) => unmarshall(i)); + } else if (params?.status) { + // Use GSI on status + const queryParams: any = { + TableName: tables.runs, + IndexName: 'gsi_status', + KeyConditionExpression: '#s = :status', + ExpressionAttributeNames: { '#s': 'status' }, + ExpressionAttributeValues: marshall({ ':status': params.status }), + ScanIndexForward: false, + Limit: limit + 1, + }; + if (fromCursor) { + const cursorItem = await dynamo.send( + new GetItemCommand({ + TableName: tables.runs, + Key: marshall({ runId: fromCursor }), + ProjectionExpression: 'runId, #s, createdAt', + ExpressionAttributeNames: { '#s': 'status' }, + }) + ); + if (cursorItem.Item) { + queryParams.ExclusiveStartKey = cursorItem.Item; + } + } + const result = await dynamo.send(new QueryCommand(queryParams)); + items = (result.Items ?? []).map((i) => unmarshall(i)); + } else { + // Scan (no filter) - sorted by runId descending + const scanParams: any = { + TableName: tables.runs, + Limit: limit + 1, + }; + if (fromCursor) { + scanParams.ExclusiveStartKey = marshall({ runId: fromCursor }); + } + const result = await dynamo.send(new ScanCommand(scanParams)); + items = (result.Items ?? []) + .map((i) => unmarshall(i)) + .sort((a, b) => (b.runId > a.runId ? 1 : -1)); + } + + const values = items.slice(0, limit); + const hasMore = items.length > limit; + + return { + data: values.map((item) => + filterRunData(dynamoToRun(item), resolveData) + ), + hasMore, + cursor: values.at(-1)?.runId ?? null, + }; + }) as Storage['runs']['list'], + }; +} + +// ============================================================ +// Events Storage +// ============================================================ + +export function createEventsStorage( + dynamo: DynamoDBClient, + tables: ReturnType +): Storage['events'] { + const ulid = monotonicFactory(); + + // Helper to get run for validation + async function getRunForValidation( + runId: string + ): Promise<{ status: string; specVersion: number | null } | null> { + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.runs, + Key: marshall({ runId }), + ProjectionExpression: '#s, specVersion', + ExpressionAttributeNames: { '#s': 'status' }, + }) + ); + if (!result.Item) return null; + const item = unmarshall(result.Item); + return { status: item.status, specVersion: item.specVersion ?? null }; + } + + // Helper to get step for validation + async function getStepForValidation(stepId: string): Promise<{ + status: string; + startedAt: Date | null; + retryAfter: Date | null; + } | null> { + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.steps, + Key: marshall({ stepId }), + ProjectionExpression: '#s, startedAt, retryAfter', + ExpressionAttributeNames: { '#s': 'status' }, + }) + ); + if (!result.Item) return null; + const item = unmarshall(result.Item); + return { + status: item.status, + startedAt: item.startedAt ? fromIso(item.startedAt) : null, + retryAfter: item.retryAfter ? fromIso(item.retryAfter) : null, + }; + } + + // Helper to get hook by token + async function getHookByToken( + token: string + ): Promise<{ hookId: string } | null> { + const result = await dynamo.send( + new QueryCommand({ + TableName: tables.hooks, + IndexName: 'gsi_token', + KeyConditionExpression: '#t = :token', + ExpressionAttributeNames: { '#t': 'token' }, + ExpressionAttributeValues: marshall({ ':token': token }), + Limit: 1, + }) + ); + if (!result.Items?.length) return null; + const item = unmarshall(result.Items[0]); + return { hookId: item.hookId }; + } + + // Helper to get wait for validation + async function getWaitForValidation( + waitId: string + ): Promise<{ status: string } | null> { + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.waits, + Key: marshall({ waitId }), + ProjectionExpression: '#s', + ExpressionAttributeNames: { '#s': 'status' }, + }) + ); + if (!result.Item) return null; + const item = unmarshall(result.Item); + return { status: item.status }; + } + + // Helper to insert an event into the events table + async function insertEvent( + runId: string, + eventId: string, + data: any, + specVersion: number + ): Promise { + const now = new Date(); + await dynamo.send( + new PutItemCommand({ + TableName: tables.events, + Item: marshallItem({ + runId, + eventId, + eventType: data.eventType, + correlationId: data.correlationId, + eventData: + 'eventData' in data && data.eventData != null + ? cborEncode(data.eventData) + : undefined, + specVersion, + createdAt: toIso(now), + }), + }) + ); + return now; + } + + // Helper to delete all hooks for a run + async function deleteHooksForRun(runId: string): Promise { + const result = await dynamo.send( + new QueryCommand({ + TableName: tables.hooks, + IndexName: 'gsi_runId', + KeyConditionExpression: 'runId = :runId', + ExpressionAttributeValues: marshall({ ':runId': runId }), + ProjectionExpression: 'hookId', + }) + ); + if (result.Items?.length) { + // DynamoDB BatchWriteItem supports up to 25 items + const items = result.Items.map((i) => unmarshall(i)); + for (let i = 0; i < items.length; i += 25) { + const batch = items.slice(i, i + 25); + await dynamo.send( + new BatchWriteItemCommand({ + RequestItems: { + [tables.hooks]: batch.map((item) => ({ + DeleteRequest: { + Key: marshall({ hookId: item.hookId }), + }, + })), + }, + }) + ); + } + } + } + + // Helper to delete all waits for a run + async function deleteWaitsForRun(runId: string): Promise { + const result = await dynamo.send( + new QueryCommand({ + TableName: tables.waits, + IndexName: 'gsi_runId', + KeyConditionExpression: 'runId = :runId', + ExpressionAttributeValues: marshall({ ':runId': runId }), + ProjectionExpression: 'waitId', + }) + ); + if (result.Items?.length) { + const items = result.Items.map((i) => unmarshall(i)); + for (let i = 0; i < items.length; i += 25) { + const batch = items.slice(i, i + 25); + await dynamo.send( + new BatchWriteItemCommand({ + RequestItems: { + [tables.waits]: batch.map((item) => ({ + DeleteRequest: { + Key: marshall({ waitId: item.waitId }), + }, + })), + }, + }) + ); + } + } + } + + const isRunTerminal = (status: string) => + ['completed', 'failed', 'cancelled'].includes(status); + + const isStepTerminal = (status: string) => + ['completed', 'failed'].includes(status); + + return { + async create(runId: any, data: any, params?: any): Promise { + const eventId = `wevt_${ulid()}`; + + // For run_created events, use client-provided runId or generate one + 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(); + const nowIso = toIso(now); + + let run: WorkflowRun | undefined; + let step: Step | undefined; + let hook: Hook | undefined; + let wait: Wait | undefined; + + // ============================================================ + // VALIDATION + // ============================================================ + + let currentRun: { status: string; specVersion: number | null } | null = + null; + const skipRunValidationEvents = ['step_completed', 'step_retrying']; + if ( + data.eventType !== 'run_created' && + !skipRunValidationEvents.includes(data.eventType) + ) { + currentRun = await getRunForValidation(effectiveRunId); + } + + // Version compatibility check + if (currentRun) { + if (requiresNewerWorld(currentRun.specVersion)) { + throw new RunNotSupportedError( + currentRun.specVersion!, + SPEC_VERSION_CURRENT + ); + } + + if (isLegacySpecVersion(currentRun.specVersion)) { + return handleLegacyEvent( + dynamo, + tables, + 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 runResult = await dynamo.send( + new GetItemCommand({ + TableName: tables.runs, + Key: marshall({ runId: effectiveRunId }), + }) + ); + const createdAt = await insertEvent( + effectiveRunId, + eventId, + data, + effectiveSpecVersion + ); + const result = { + ...data, + createdAt, + runId: effectiveRunId, + eventId, + }; + const parsed = EventSchema.parse(result); + const resolveData = params?.resolveData ?? 'all'; + return { + event: filterEventData(parsed, resolveData), + run: runResult.Item + ? dynamoToRun(unmarshall(runResult.Item)) + : undefined, + }; + } + + 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-related event validation + let validatedStep: { + status: string; + startedAt: Date | null; + retryAfter: Date | null; + } | null = null; + const stepEventsNeedingValidation = ['step_started', 'step_retrying']; + if ( + stepEventsNeedingValidation.includes(data.eventType) && + data.correlationId + ) { + validatedStep = await getStepForValidation(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-related event validation + const hookEventsRequiringExistence = ['hook_disposed', 'hook_received']; + if ( + hookEventsRequiringExistence.includes(data.eventType) && + data.correlationId + ) { + const existingHook = await dynamo.send( + new GetItemCommand({ + TableName: tables.hooks, + Key: marshall({ hookId: data.correlationId }), + ProjectionExpression: 'hookId', + }) + ); + if (!existingHook.Item) { + throw new WorkflowAPIError(`Hook "${data.correlationId}" not found`, { + status: 404, + }); + } + } + + // ============================================================ + // Entity creation/updates based on event type + // ============================================================ + + if (data.eventType === 'run_created') { + const eventData = data.eventData as { + deploymentId: string; + workflowName: string; + input: any; + executionContext?: Record; + }; + try { + await dynamo.send( + new PutItemCommand({ + TableName: tables.runs, + Item: marshallItem({ + runId: effectiveRunId, + deploymentId: eventData.deploymentId, + workflowName: eventData.workflowName, + specVersion: effectiveSpecVersion, + input: cborEncode(eventData.input), + executionContext: eventData.executionContext + ? cborEncode(eventData.executionContext) + : undefined, + status: 'pending', + createdAt: nowIso, + updatedAt: nowIso, + }), + ConditionExpression: 'attribute_not_exists(runId)', + }) + ); + // Retrieve the created run + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.runs, + Key: marshall({ runId: effectiveRunId }), + }) + ); + if (result.Item) { + run = dynamoToRun(unmarshall(result.Item)); + } + } catch (err) { + if (err instanceof ConditionalCheckFailedException) { + // Run already exists, idempotent + } else { + throw err; + } + } + } + + if (data.eventType === 'run_started') { + await dynamo.send( + new UpdateItemCommand({ + TableName: tables.runs, + Key: marshall({ runId: effectiveRunId }), + UpdateExpression: + 'SET #s = :status, startedAt = :startedAt, updatedAt = :updatedAt', + ExpressionAttributeNames: { '#s': 'status' }, + ExpressionAttributeValues: marshall({ + ':status': 'running', + ':startedAt': nowIso, + ':updatedAt': nowIso, + }), + }) + ); + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.runs, + Key: marshall({ runId: effectiveRunId }), + }) + ); + if (result.Item) { + run = dynamoToRun(unmarshall(result.Item)); + } + } + + if (data.eventType === 'run_completed') { + const eventData = data.eventData as { output?: any }; + const updateExpr = + eventData.output !== undefined + ? 'SET #s = :status, #out = :output, completedAt = :completedAt, updatedAt = :updatedAt' + : 'SET #s = :status, completedAt = :completedAt, updatedAt = :updatedAt'; + const exprNames: Record = { '#s': 'status' }; + const exprValues: Record = { + ':status': 'completed', + ':completedAt': nowIso, + ':updatedAt': nowIso, + }; + if (eventData.output !== undefined) { + exprNames['#out'] = 'output'; + exprValues[':output'] = cborEncode(eventData.output); + } + await dynamo.send( + new UpdateItemCommand({ + TableName: tables.runs, + Key: marshall({ runId: effectiveRunId }), + UpdateExpression: updateExpr, + ExpressionAttributeNames: exprNames, + ExpressionAttributeValues: marshall(exprValues), + }) + ); + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.runs, + Key: marshall({ runId: effectiveRunId }), + }) + ); + if (result.Item) { + run = dynamoToRun(unmarshall(result.Item)); + } + await Promise.all([ + deleteHooksForRun(effectiveRunId), + deleteWaitsForRun(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'); + await dynamo.send( + new UpdateItemCommand({ + TableName: tables.runs, + Key: marshall({ runId: effectiveRunId }), + UpdateExpression: + 'SET #s = :status, #err = :error, completedAt = :completedAt, updatedAt = :updatedAt', + ExpressionAttributeNames: { '#s': 'status', '#err': 'error' }, + ExpressionAttributeValues: marshall({ + ':status': 'failed', + ':error': cborEncode({ + message: errorMessage, + stack: eventData.error?.stack, + code: eventData.errorCode, + }), + ':completedAt': nowIso, + ':updatedAt': nowIso, + }), + }) + ); + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.runs, + Key: marshall({ runId: effectiveRunId }), + }) + ); + if (result.Item) { + run = dynamoToRun(unmarshall(result.Item)); + } + await Promise.all([ + deleteHooksForRun(effectiveRunId), + deleteWaitsForRun(effectiveRunId), + ]); + } + + if (data.eventType === 'run_cancelled') { + await dynamo.send( + new UpdateItemCommand({ + TableName: tables.runs, + Key: marshall({ runId: effectiveRunId }), + UpdateExpression: + 'SET #s = :status, completedAt = :completedAt, updatedAt = :updatedAt', + ExpressionAttributeNames: { '#s': 'status' }, + ExpressionAttributeValues: marshall({ + ':status': 'cancelled', + ':completedAt': nowIso, + ':updatedAt': nowIso, + }), + }) + ); + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.runs, + Key: marshall({ runId: effectiveRunId }), + }) + ); + if (result.Item) { + run = dynamoToRun(unmarshall(result.Item)); + } + await Promise.all([ + deleteHooksForRun(effectiveRunId), + deleteWaitsForRun(effectiveRunId), + ]); + } + + if (data.eventType === 'step_created') { + const eventData = data.eventData as { + stepName: string; + input: any; + }; + try { + await dynamo.send( + new PutItemCommand({ + TableName: tables.steps, + Item: marshallItem({ + runId: effectiveRunId, + stepId: data.correlationId!, + stepName: eventData.stepName, + input: cborEncode(eventData.input), + status: 'pending', + attempt: 0, + specVersion: effectiveSpecVersion, + createdAt: nowIso, + updatedAt: nowIso, + }), + ConditionExpression: 'attribute_not_exists(stepId)', + }) + ); + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.steps, + Key: marshall({ stepId: data.correlationId! }), + }) + ); + if (result.Item) { + step = dynamoToStep(unmarshall(result.Item)); + } + } catch (err) { + if (err instanceof ConditionalCheckFailedException) { + // Step already exists, idempotent + } else { + throw err; + } + } + } + + if (data.eventType === 'step_started') { + // Check if retryAfter timestamp hasn't been reached yet + 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 hadRetryAfter = !!validatedStep?.retryAfter; + + let updateExpr = + 'SET #s = :status, attempt = attempt + :one, updatedAt = :updatedAt'; + const exprNames: Record = { '#s': 'status' }; + const exprValues: Record = { + ':status': 'running', + ':one': 1, + ':updatedAt': nowIso, + }; + + if (isFirstStart) { + updateExpr += ', startedAt = :startedAt'; + exprValues[':startedAt'] = nowIso; + } + if (hadRetryAfter) { + updateExpr += ' REMOVE retryAfter'; + } + + await dynamo.send( + new UpdateItemCommand({ + TableName: tables.steps, + Key: marshall({ stepId: data.correlationId! }), + UpdateExpression: updateExpr, + ExpressionAttributeNames: exprNames, + ExpressionAttributeValues: marshall(exprValues), + }) + ); + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.steps, + Key: marshall({ stepId: data.correlationId! }), + }) + ); + if (result.Item) { + step = dynamoToStep(unmarshall(result.Item)); + } + } + + if (data.eventType === 'step_completed') { + const eventData = data.eventData as { result?: any }; + try { + const updateExpr = + eventData.result !== undefined + ? 'SET #s = :status, #out = :output, completedAt = :completedAt, updatedAt = :updatedAt' + : 'SET #s = :status, completedAt = :completedAt, updatedAt = :updatedAt'; + const exprNames: Record = { '#s': 'status' }; + const exprValues: Record = { + ':status': 'completed', + ':completedAt': nowIso, + ':updatedAt': nowIso, + ':terminalCompleted': 'completed', + ':terminalFailed': 'failed', + }; + if (eventData.result !== undefined) { + exprNames['#out'] = 'output'; + exprValues[':output'] = cborEncode(eventData.result); + } + await dynamo.send( + new UpdateItemCommand({ + TableName: tables.steps, + Key: marshall({ stepId: data.correlationId! }), + UpdateExpression: updateExpr, + ConditionExpression: + '#s <> :terminalCompleted AND #s <> :terminalFailed', + ExpressionAttributeNames: exprNames, + ExpressionAttributeValues: marshall(exprValues), + }) + ); + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.steps, + Key: marshall({ stepId: data.correlationId! }), + }) + ); + if (result.Item) { + step = dynamoToStep(unmarshall(result.Item)); + } + } catch (err) { + if (err instanceof ConditionalCheckFailedException) { + // Check why it failed + const existing = await getStepForValidation(data.correlationId!); + if (!existing) { + throw new WorkflowAPIError( + `Step "${data.correlationId}" not found`, + { status: 404 } + ); + } + if (isStepTerminal(existing.status)) { + throw new WorkflowAPIError( + `Cannot modify step in terminal state "${existing.status}"`, + { status: 409 } + ); + } + } else { + throw err; + } + } + } + + 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'); + + try { + await dynamo.send( + new UpdateItemCommand({ + TableName: tables.steps, + Key: marshall({ stepId: data.correlationId! }), + UpdateExpression: + 'SET #s = :status, #err = :error, completedAt = :completedAt, updatedAt = :updatedAt', + ConditionExpression: + '#s <> :terminalCompleted AND #s <> :terminalFailed', + ExpressionAttributeNames: { '#s': 'status', '#err': 'error' }, + ExpressionAttributeValues: marshall({ + ':status': 'failed', + ':error': cborEncode({ + message: errorMessage, + stack: eventData.stack, + }), + ':completedAt': nowIso, + ':updatedAt': nowIso, + ':terminalCompleted': 'completed', + ':terminalFailed': 'failed', + }), + }) + ); + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.steps, + Key: marshall({ stepId: data.correlationId! }), + }) + ); + if (result.Item) { + step = dynamoToStep(unmarshall(result.Item)); + } + } catch (err) { + if (err instanceof ConditionalCheckFailedException) { + const existing = await getStepForValidation(data.correlationId!); + if (!existing) { + throw new WorkflowAPIError( + `Step "${data.correlationId}" not found`, + { status: 404 } + ); + } + if (isStepTerminal(existing.status)) { + throw new WorkflowAPIError( + `Cannot modify step in terminal state "${existing.status}"`, + { status: 409 } + ); + } + } else { + throw err; + } + } + } + + if (data.eventType === 'step_retrying') { + const eventData = data.eventData as { + error?: any; + stack?: string; + retryAfter?: Date; + }; + const errorMessage = + typeof eventData.error === 'string' + ? eventData.error + : (eventData.error?.message ?? 'Unknown error'); + + let updateExpr = + 'SET #s = :status, #err = :error, updatedAt = :updatedAt'; + const exprNames: Record = { + '#s': 'status', + '#err': 'error', + }; + const exprValues: Record = { + ':status': 'pending', + ':error': cborEncode({ + message: errorMessage, + stack: eventData.stack, + }), + ':updatedAt': nowIso, + }; + + if (eventData.retryAfter) { + updateExpr += ', retryAfter = :retryAfter'; + exprValues[':retryAfter'] = toIso( + eventData.retryAfter instanceof Date + ? eventData.retryAfter + : new Date(eventData.retryAfter) + ); + } + + await dynamo.send( + new UpdateItemCommand({ + TableName: tables.steps, + Key: marshall({ stepId: data.correlationId! }), + UpdateExpression: updateExpr, + ExpressionAttributeNames: exprNames, + ExpressionAttributeValues: marshall(exprValues), + }) + ); + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.steps, + Key: marshall({ stepId: data.correlationId! }), + }) + ); + if (result.Item) { + step = dynamoToStep(unmarshall(result.Item)); + } + } + + if (data.eventType === 'hook_created') { + const eventData = data.eventData as { + token: string; + metadata?: any; + isWebhook?: boolean; + }; + + // Check for duplicate token + const existingHookResult = await getHookByToken(eventData.token); + if (existingHookResult) { + // Create hook_conflict event + const conflictEventData = { token: eventData.token }; + const createdAt = await insertEvent( + effectiveRunId, + eventId, + { + eventType: 'hook_conflict', + correlationId: data.correlationId, + eventData: conflictEventData, + }, + effectiveSpecVersion + ); + const conflictResult = { + eventType: 'hook_conflict' as const, + correlationId: data.correlationId, + eventData: conflictEventData, + createdAt, + runId: effectiveRunId, + eventId, + }; + const parsedConflict = EventSchema.parse(conflictResult); + const resolveData = params?.resolveData ?? 'all'; + return { + event: filterEventData(parsedConflict, resolveData), + run, + step, + hook: undefined, + }; + } + + try { + await dynamo.send( + new PutItemCommand({ + TableName: tables.hooks, + Item: marshallItem({ + runId: effectiveRunId, + hookId: data.correlationId!, + token: eventData.token, + metadata: eventData.metadata + ? cborEncode(eventData.metadata) + : undefined, + ownerId: '', + projectId: '', + environment: '', + specVersion: effectiveSpecVersion, + isWebhook: eventData.isWebhook, + createdAt: nowIso, + }), + ConditionExpression: 'attribute_not_exists(hookId)', + }) + ); + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.hooks, + Key: marshall({ hookId: data.correlationId! }), + }) + ); + if (result.Item) { + hook = dynamoToHook(unmarshall(result.Item)); + } + } catch (err) { + if (err instanceof ConditionalCheckFailedException) { + // Hook already exists, idempotent + } else { + throw err; + } + } + } + + if (data.eventType === 'hook_disposed' && data.correlationId) { + await dynamo.send( + new DeleteItemCommand({ + TableName: tables.hooks, + Key: marshall({ hookId: data.correlationId }), + }) + ); + } + + if (data.eventType === 'wait_created') { + const eventData = data.eventData as { resumeAt?: Date }; + const waitId = `${effectiveRunId}-${data.correlationId}`; + try { + await dynamo.send( + new PutItemCommand({ + TableName: tables.waits, + Item: marshallItem({ + waitId, + runId: effectiveRunId, + status: 'waiting', + resumeAt: eventData.resumeAt + ? toIso( + eventData.resumeAt instanceof Date + ? eventData.resumeAt + : new Date(eventData.resumeAt) + ) + : undefined, + specVersion: effectiveSpecVersion, + createdAt: nowIso, + updatedAt: nowIso, + }), + ConditionExpression: 'attribute_not_exists(waitId)', + }) + ); + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.waits, + Key: marshall({ waitId }), + }) + ); + if (result.Item) { + wait = dynamoToWait(unmarshall(result.Item)); + } + } catch (err) { + if (err instanceof ConditionalCheckFailedException) { + throw new WorkflowAPIError( + `Wait "${data.correlationId}" already exists`, + { status: 409 } + ); + } + throw err; + } + } + + if (data.eventType === 'wait_completed') { + const waitId = `${effectiveRunId}-${data.correlationId}`; + try { + await dynamo.send( + new UpdateItemCommand({ + TableName: tables.waits, + Key: marshall({ waitId }), + UpdateExpression: + 'SET #s = :status, completedAt = :completedAt, updatedAt = :updatedAt', + ConditionExpression: '#s = :waiting', + ExpressionAttributeNames: { '#s': 'status' }, + ExpressionAttributeValues: marshall({ + ':status': 'completed', + ':completedAt': nowIso, + ':updatedAt': nowIso, + ':waiting': 'waiting', + }), + }) + ); + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.waits, + Key: marshall({ waitId }), + }) + ); + if (result.Item) { + wait = dynamoToWait(unmarshall(result.Item)); + } + } catch (err) { + if (err instanceof ConditionalCheckFailedException) { + const existing = await getWaitForValidation(waitId); + if (!existing) { + throw new WorkflowAPIError( + `Wait "${data.correlationId}" not found`, + { status: 404 } + ); + } + if (existing.status === 'completed') { + throw new WorkflowAPIError( + `Wait "${data.correlationId}" already completed`, + { status: 409 } + ); + } + } else { + throw err; + } + } + } + + // Insert the event + const eventCreatedAt = await insertEvent( + effectiveRunId, + eventId, + data, + effectiveSpecVersion + ); + + const result = { + ...data, + createdAt: eventCreatedAt, + runId: effectiveRunId, + eventId, + }; + const parsed = EventSchema.parse(result); + const resolveData = params?.resolveData ?? 'all'; + return { + event: filterEventData(parsed, resolveData), + run, + step, + hook, + wait, + }; + }, + + async get( + runId: string, + eventId: string, + params?: GetEventParams + ): Promise { + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.events, + Key: marshall({ runId, eventId }), + }) + ); + if (!result.Item) { + throw new WorkflowAPIError(`Event not found: ${eventId}`, { + status: 404, + }); + } + const item = unmarshall(result.Item); + const event = dynamoToEvent(item); + const resolveData = params?.resolveData ?? 'all'; + return filterEventData(event, resolveData); + }, + + async list(params: ListEventsParams): Promise> { + const limit = params?.pagination?.limit ?? 100; + const sortOrder = params.pagination?.sortOrder || 'asc'; + const resolveData = params?.resolveData ?? 'all'; + + const queryParams: any = { + TableName: tables.events, + KeyConditionExpression: params.pagination?.cursor + ? sortOrder === 'desc' + ? 'runId = :runId AND eventId < :cursor' + : 'runId = :runId AND eventId > :cursor' + : 'runId = :runId', + ExpressionAttributeValues: marshall( + params.pagination?.cursor + ? { ':runId': params.runId, ':cursor': params.pagination.cursor } + : { ':runId': params.runId } + ), + ScanIndexForward: sortOrder !== 'desc', + Limit: limit + 1, + }; + + const result = await dynamo.send(new QueryCommand(queryParams)); + const items = (result.Items ?? []).map((i) => unmarshall(i)); + const values = items.slice(0, limit); + const hasMore = items.length > limit; + + return { + data: values.map((item) => + filterEventData(dynamoToEvent(item), resolveData) + ), + cursor: values.at(-1)?.eventId ?? null, + hasMore, + }; + }, + + async listByCorrelationId( + params: ListEventsByCorrelationIdParams + ): Promise> { + const limit = params?.pagination?.limit ?? 100; + const sortOrder = params.pagination?.sortOrder || 'asc'; + const resolveData = params?.resolveData ?? 'all'; + + const queryParams: any = { + TableName: tables.events, + IndexName: 'gsi_correlationId', + KeyConditionExpression: params.pagination?.cursor + ? sortOrder === 'desc' + ? 'correlationId = :cid AND createdAt < :cursor' + : 'correlationId = :cid AND createdAt > :cursor' + : 'correlationId = :cid', + ExpressionAttributeValues: marshall( + params.pagination?.cursor + ? { + ':cid': params.correlationId, + ':cursor': params.pagination.cursor, + } + : { ':cid': params.correlationId } + ), + ScanIndexForward: sortOrder !== 'desc', + Limit: limit + 1, + }; + + const result = await dynamo.send(new QueryCommand(queryParams)); + const items = (result.Items ?? []).map((i) => unmarshall(i)); + const values = items.slice(0, limit); + const hasMore = items.length > limit; + + return { + data: values.map((item) => + filterEventData(dynamoToEvent(item), resolveData) + ), + cursor: values.at(-1)?.eventId ?? null, + hasMore, + }; + }, + }; +} + +// ============================================================ +// Hooks Storage +// ============================================================ + +export function createHooksStorage( + dynamo: DynamoDBClient, + tables: ReturnType +): Storage['hooks'] { + return { + async get(hookId: string, params?: any): Promise { + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.hooks, + Key: marshall({ hookId }), + }) + ); + if (!result.Item) { + throw new WorkflowAPIError(`Hook not found: ${hookId}`, { + status: 404, + }); + } + const hook = dynamoToHook(unmarshall(result.Item)); + const resolveData = params?.resolveData ?? 'all'; + return filterHookData(hook, resolveData); + }, + + async getByToken(token: string, params?: any): Promise { + const result = await dynamo.send( + new QueryCommand({ + TableName: tables.hooks, + IndexName: 'gsi_token', + KeyConditionExpression: '#t = :token', + ExpressionAttributeNames: { '#t': 'token' }, + ExpressionAttributeValues: marshall({ ':token': token }), + Limit: 1, + }) + ); + if (!result.Items?.length) { + throw new HookNotFoundError(token); + } + const hook = dynamoToHook(unmarshall(result.Items[0])); + const resolveData = params?.resolveData ?? 'all'; + return filterHookData(hook, resolveData); + }, + + async list(params: ListHooksParams): Promise> { + const limit = params?.pagination?.limit ?? 100; + const resolveData = params?.resolveData ?? 'all'; + + let items: Record[]; + + if (params.runId) { + const queryParams: any = { + TableName: tables.hooks, + IndexName: 'gsi_runId', + KeyConditionExpression: 'runId = :runId', + ExpressionAttributeValues: marshall({ ':runId': params.runId }), + Limit: limit + 1, + }; + if (params.pagination?.cursor) { + queryParams.KeyConditionExpression += ' AND hookId > :cursor'; + queryParams.ExpressionAttributeValues = marshall({ + ':runId': params.runId, + ':cursor': params.pagination.cursor, + }); + } + const result = await dynamo.send(new QueryCommand(queryParams)); + items = (result.Items ?? []).map((i) => unmarshall(i)); + } else { + const scanParams: any = { + TableName: tables.hooks, + Limit: limit + 1, + }; + const result = await dynamo.send(new ScanCommand(scanParams)); + items = (result.Items ?? []).map((i) => unmarshall(i)); + } + + const values = items.slice(0, limit); + const hasMore = items.length > limit; + + return { + data: values.map((item) => + filterHookData(dynamoToHook(item), resolveData) + ), + cursor: values.at(-1)?.hookId ?? null, + hasMore, + }; + }, + }; +} + +// ============================================================ +// Steps Storage +// ============================================================ + +export function createStepsStorage( + dynamo: DynamoDBClient, + tables: ReturnType +): Storage['steps'] { + return { + get: (async (runId: string | undefined, stepId: string, params?: any) => { + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.steps, + Key: marshall({ stepId }), + }) + ); + if (!result.Item) { + throw new WorkflowAPIError(`Step not found: ${stepId}`, { + status: 404, + }); + } + const item = unmarshall(result.Item); + // If runId was provided, verify it matches + if (runId && item.runId !== runId) { + throw new WorkflowAPIError(`Step not found: ${stepId}`, { + status: 404, + }); + } + const step = dynamoToStep(item); + const resolveData = params?.resolveData ?? 'all'; + return filterStepData(step, resolveData); + }) as Storage['steps']['get'], + + list: (async (params: any) => { + const limit = params?.pagination?.limit ?? 20; + const resolveData = params?.resolveData ?? 'all'; + + const queryParams: any = { + TableName: tables.steps, + IndexName: 'gsi_runId', + KeyConditionExpression: 'runId = :runId', + ExpressionAttributeValues: marshall({ ':runId': params.runId }), + ScanIndexForward: false, + Limit: limit + 1, + }; + if (params.pagination?.cursor) { + queryParams.KeyConditionExpression += ' AND stepId < :cursor'; + queryParams.ExpressionAttributeValues = marshall({ + ':runId': params.runId, + ':cursor': params.pagination.cursor, + }); + } + + const result = await dynamo.send(new QueryCommand(queryParams)); + const items = (result.Items ?? []).map((i) => unmarshall(i)); + const values = items.slice(0, limit); + const hasMore = items.length > limit; + + return { + data: values.map((item) => + filterStepData(dynamoToStep(item), resolveData) + ), + hasMore, + cursor: values.at(-1)?.stepId ?? null, + }; + }) as Storage['steps']['list'], + }; +} + +// ============================================================ +// Legacy event handler (pre-event-sourcing runs) +// ============================================================ + +async function handleLegacyEvent( + dynamo: DynamoDBClient, + tables: ReturnType, + runId: string, + eventId: string, + data: any, + currentRun: { status: string; specVersion: number | null }, + params?: { resolveData?: ResolveData } +): Promise { + const resolveData = params?.resolveData ?? 'all'; + const now = new Date(); + const nowIso = toIso(now); + + switch (data.eventType) { + case 'run_cancelled': { + await dynamo.send( + new UpdateItemCommand({ + TableName: tables.runs, + Key: marshall({ runId }), + UpdateExpression: + 'SET #s = :status, completedAt = :completedAt, updatedAt = :updatedAt', + ExpressionAttributeNames: { '#s': 'status' }, + ExpressionAttributeValues: marshall({ + ':status': 'cancelled', + ':completedAt': nowIso, + ':updatedAt': nowIso, + }), + }) + ); + const result = await dynamo.send( + new GetItemCommand({ + TableName: tables.runs, + Key: marshall({ runId }), + }) + ); + return { + run: result.Item + ? (filterRunData( + dynamoToRun(unmarshall(result.Item)), + resolveData + ) as WorkflowRun) + : undefined, + }; + } + + case 'wait_completed': + case 'hook_received': { + await dynamo.send( + new PutItemCommand({ + TableName: tables.events, + Item: marshallItem({ + runId, + eventId, + correlationId: data.correlationId, + eventType: data.eventType, + eventData: + 'eventData' in data && data.eventData != null + ? cborEncode(data.eventData) + : undefined, + specVersion: SPEC_VERSION_CURRENT, + createdAt: nowIso, + }), + }) + ); + const event = EventSchema.parse({ + ...data, + createdAt: now, + runId, + eventId, + }); + 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.` + ); + } +} diff --git a/packages/world-aws/src/streamer.ts b/packages/world-aws/src/streamer.ts new file mode 100644 index 0000000000..b042e0d597 --- /dev/null +++ b/packages/world-aws/src/streamer.ts @@ -0,0 +1,303 @@ +import { EventEmitter } from 'node:events'; +import { + DynamoDBClient, + PutItemCommand, + QueryCommand, +} from '@aws-sdk/client-dynamodb'; +import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; +import type { Streamer } from '@workflow/world'; +import { monotonicFactory } from 'ulid'; +import { tableNames } from './config.js'; + +interface StreamChunkEvent { + id: `chnk_${string}`; + data: Uint8Array; + eof: boolean; +} + +export type AwsStreamer = Streamer & { + close(): Promise; +}; + +/** + * Streamer implementation backed by DynamoDB. + * + * Stream chunks are stored in a DynamoDB table with composite key (streamId, chunkId). + * ULID-based chunkIds maintain ordering across process boundaries. + * + * For real-time streaming, we use a polling approach rather than DynamoDB Streams, + * since the latter requires additional IAM permissions and Lambda configuration. + * For most workflow use cases, the slight latency from polling is acceptable. + */ +export function createStreamer( + dynamo: DynamoDBClient, + tables: ReturnType +): AwsStreamer { + const ulid = monotonicFactory(); + const events = new EventEmitter<{ + [key: `strm:${string}`]: [StreamChunkEvent]; + }>(); + const genChunkId = () => `chnk_${ulid()}` as const; + + // Polling for active stream readers + let pollActive = true; + const activeStreams = new Map< + string, + { lastChunkId: string; interval: ReturnType } + >(); + + function startStreamPoll(streamId: string) { + if (activeStreams.has(streamId)) return; + + const state = { + lastChunkId: '', + interval: setInterval(async () => { + if (!pollActive) return; + + const key = `strm:${streamId}` as const; + if (!events.listenerCount(key)) { + // No listeners, clean up + clearInterval(state.interval); + activeStreams.delete(streamId); + return; + } + + try { + // Query for new chunks since last seen + const queryParams: any = { + TableName: tables.streams, + KeyConditionExpression: state.lastChunkId + ? 'streamId = :sid AND chunkId > :lastId' + : 'streamId = :sid', + ExpressionAttributeValues: state.lastChunkId + ? marshall({ + ':sid': streamId, + ':lastId': state.lastChunkId, + }) + : marshall({ ':sid': streamId }), + ScanIndexForward: true, + }; + + const result = await dynamo.send(new QueryCommand(queryParams)); + if (result.Items?.length) { + for (const rawItem of result.Items) { + const item = unmarshall(rawItem); + const chunk: StreamChunkEvent = { + id: item.chunkId as `chnk_${string}`, + data: item.chunkData + ? new Uint8Array(item.chunkData) + : new Uint8Array(0), + eof: item.eof ?? false, + }; + state.lastChunkId = item.chunkId; + events.emit(key, chunk); + + if (chunk.eof) { + clearInterval(state.interval); + activeStreams.delete(streamId); + return; + } + } + } + } catch { + // Ignore polling errors + } + }, 500), + }; + activeStreams.set(streamId, state); + } + + const toBuffer = (chunk: string | Uint8Array): Uint8Array => + typeof chunk === 'string' ? new TextEncoder().encode(chunk) : chunk; + + return { + async writeToStream( + name: string, + _runId: string | Promise, + chunk: string | Uint8Array + ) { + const runId = await _runId; + const chunkId = genChunkId(); + await dynamo.send( + new PutItemCommand({ + TableName: tables.streams, + Item: marshall( + { + streamId: name, + chunkId, + runId, + chunkData: toBuffer(chunk), + eof: false, + createdAt: new Date().toISOString(), + }, + { removeUndefinedValues: true } + ), + }) + ); + }, + + async writeToStreamMulti( + name: string, + _runId: string | Promise, + chunks: (string | Uint8Array)[] + ) { + if (chunks.length === 0) return; + + const chunkIds = chunks.map(() => genChunkId()); + const runId = await _runId; + + // Write chunks sequentially to maintain order + for (let i = 0; i < chunks.length; i++) { + await dynamo.send( + new PutItemCommand({ + TableName: tables.streams, + Item: marshall( + { + streamId: name, + chunkId: chunkIds[i], + runId, + chunkData: toBuffer(chunks[i]), + eof: false, + createdAt: new Date().toISOString(), + }, + { removeUndefinedValues: true } + ), + }) + ); + } + }, + + async closeStream( + name: string, + _runId: string | Promise + ): Promise { + const runId = await _runId; + const chunkId = genChunkId(); + await dynamo.send( + new PutItemCommand({ + TableName: tables.streams, + Item: marshall( + { + streamId: name, + chunkId, + runId, + chunkData: new Uint8Array(0), + eof: true, + createdAt: new Date().toISOString(), + }, + { removeUndefinedValues: true } + ), + }) + ); + }, + + async readFromStream( + name: string, + startIndex?: number + ): Promise> { + const cleanups: (() => void)[] = []; + + return new ReadableStream({ + async start(controller) { + let lastChunkId = ''; + let offset = startIndex ?? 0; + let buffer = [] as StreamChunkEvent[] | null; + + function enqueue(msg: { + id: string; + data: Uint8Array; + eof: boolean; + }) { + if (lastChunkId >= msg.id) { + return; + } + + if (offset > 0) { + offset--; + return; + } + + if (msg.data.byteLength) { + controller.enqueue(new Uint8Array(msg.data)); + } + if (msg.eof) { + controller.close(); + } + lastChunkId = msg.id; + } + + function onData(data: StreamChunkEvent) { + if (buffer) { + buffer.push(data); + return; + } + enqueue(data); + } + events.on(`strm:${name}`, onData); + cleanups.push(() => { + events.off(`strm:${name}`, onData); + }); + + // Start polling for this stream + startStreamPoll(name); + + // Load existing chunks + const result = await dynamo.send( + new QueryCommand({ + TableName: tables.streams, + KeyConditionExpression: 'streamId = :sid', + ExpressionAttributeValues: marshall({ ':sid': name }), + ScanIndexForward: true, + }) + ); + + const chunks = (result.Items ?? []).map((rawItem) => { + const item = unmarshall(rawItem); + return { + id: item.chunkId as `chnk_${string}`, + data: item.chunkData + ? new Uint8Array(item.chunkData) + : new Uint8Array(0), + eof: item.eof ?? false, + }; + }); + + for (const chunk of [...chunks, ...(buffer ?? [])]) { + enqueue(chunk); + } + buffer = null; + }, + cancel() { + cleanups.forEach((fn) => fn()); + }, + }); + }, + + async listStreamsByRunId(runId: string): Promise { + const result = await dynamo.send( + new QueryCommand({ + TableName: tables.streams, + IndexName: 'gsi_runId', + KeyConditionExpression: 'runId = :runId', + ExpressionAttributeValues: marshall({ ':runId': runId }), + ProjectionExpression: 'streamId', + }) + ); + + const streamIds = new Set(); + for (const rawItem of result.Items ?? []) { + const item = unmarshall(rawItem); + streamIds.add(item.streamId); + } + return [...streamIds]; + }, + + async close() { + pollActive = false; + for (const [, state] of activeStreams) { + clearInterval(state.interval); + } + activeStreams.clear(); + }, + }; +} diff --git a/packages/world-aws/src/util.ts b/packages/world-aws/src/util.ts new file mode 100644 index 0000000000..b8c9b72b06 --- /dev/null +++ b/packages/world-aws/src/util.ts @@ -0,0 +1,49 @@ +import { decode, encode } from 'cbor-x'; + +/** Encode a value to CBOR binary. */ +export function cborEncode(value: unknown): Uint8Array { + return encode(value); +} + +/** Decode CBOR binary to a value. */ +export function cborDecode(data: Uint8Array | Buffer): T { + return decode(Buffer.from(data)); +} + +/** Convert a Date to an ISO string for DynamoDB storage. */ +export function toIso(date: Date): string { + return date.toISOString(); +} + +/** Parse an ISO string from DynamoDB back to a Date. */ +export function fromIso(iso: string): Date { + return new Date(iso); +} + +/** Convert null values to undefined. */ +export function compact(obj: T) { + const value = {} as { + [key in keyof T]: null extends T[key] + ? undefined | NonNullable + : T[key]; + }; + for (const key in obj) { + if (obj[key] !== null && obj[key] !== undefined) { + value[key] = obj[key] as any; + } else { + value[key] = undefined as any; + } + } + return value; +} + +export class Mutex { + promise: Promise = Promise.resolve(); + andThen(fn: () => Promise | T): Promise { + this.promise = this.promise.then( + () => fn(), + () => fn() + ); + return this.promise as Promise; + } +} diff --git a/packages/world-aws/tsconfig.json b/packages/world-aws/tsconfig.json new file mode 100644 index 0000000000..a0867446f5 --- /dev/null +++ b/packages/world-aws/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@workflow/tsconfig/base.json", + "compilerOptions": { + "moduleResolution": "NodeNext", + "outDir": "dist" + }, + "include": ["src"], + "exclude": ["node_modules", "**/*.test.ts"] +} diff --git a/packages/world-aws/turbo.json b/packages/world-aws/turbo.json new file mode 100644 index 0000000000..7ca2cbaa65 --- /dev/null +++ b/packages/world-aws/turbo.json @@ -0,0 +1,14 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "inputs": ["src"], + "outputs": ["dist"] + }, + "test": { + "dependsOn": ["build"], + "outputs": [] + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c23103142e..cc840939f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1279,6 +1279,55 @@ importers: specifier: 'catalog:' version: 4.3.6 + packages/world-aws: + dependencies: + '@aws-sdk/client-dynamodb': + specifier: 3.750.0 + version: 3.750.0 + '@aws-sdk/client-s3': + specifier: 3.750.0 + version: 3.750.0 + '@aws-sdk/client-sqs': + specifier: 3.750.0 + version: 3.750.0 + '@aws-sdk/util-dynamodb': + specifier: 3.750.0 + version: 3.750.0(@aws-sdk/client-dynamodb@3.750.0) + '@vercel/queue': + specifier: 'catalog:' + version: 0.1.1 + '@workflow/errors': + specifier: workspace:* + version: link:../errors + '@workflow/world': + specifier: workspace:* + version: link:../world + '@workflow/world-local': + specifier: workspace:* + version: link:../world-local + cbor-x: + specifier: 1.6.0 + version: 1.6.0 + ulid: + specifier: 'catalog:' + version: 3.0.1 + zod: + specifier: 'catalog:' + version: 4.3.6 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 22.19.0 + '@workflow/tsconfig': + specifier: workspace:* + version: link:../tsconfig + '@workflow/world-testing': + specifier: workspace:* + version: link:../world-testing + 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': @@ -2327,6 +2376,16 @@ packages: peerDependencies: astro: ^5.0.0 + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + '@aws-crypto/sha256-browser@5.2.0': resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} @@ -2340,42 +2399,176 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + '@aws-sdk/client-dynamodb@3.750.0': + resolution: {integrity: sha512-DVLpoSsD0t/tXPS6XhD0rPv5Q5gV+Wee8jH6W9q/4pyQO666COrFEAuiSVFDWZP5Nh84cP4naUJwBjg86QDCBw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/client-s3@3.750.0': + resolution: {integrity: sha512-S9G9noCeBxchoMVkHYrRi1A1xW/VOTP2W7X34lP+Y7Wpl32yMA7IJo0fAGAuTc0q1Nu6/pXDm+oDG7rhTCA1tg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/client-sqs@3.750.0': + resolution: {integrity: sha512-UelUCqyE0hTXBm1nMXX36mHSZnnVhRcTQjxux8mgHnlRppMRz1qYNkgV7uOEPuRFeFxxgMxOROnSEDUqfoOuHA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/client-sso@3.750.0': + resolution: {integrity: sha512-y0Rx6pTQXw0E61CaptpZF65qNggjqOgymq/RYZU5vWba5DGQ+iqGt8Yq8s+jfBoBBNXshxq8l8Dl5Uq/JTY1wg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/core@3.750.0': + resolution: {integrity: sha512-bZ5K7N5L4+Pa2epbVpUQqd1XLG2uU8BGs/Sd+2nbgTf+lNQJyIxAg/Qsrjz9MzmY8zzQIeRQEkNmR6yVAfCmmQ==} + engines: {node: '>=18.0.0'} + '@aws-sdk/core@3.973.15': resolution: {integrity: sha512-AlC0oQ1/mdJ8vCIqu524j5RB7M8i8E24bbkZmya1CuiQxkY7SdIZAyw7NDNMGaNINQFq/8oGRMX0HeOfCVsl/A==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-env@3.750.0': + resolution: {integrity: sha512-In6bsG0p/P31HcH4DBRKBbcDS/3SHvEPjfXV8ODPWZO/l3/p7IRoYBdQ07C9R+VMZU2D0+/Sc/DWK/TUNDk1+Q==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-http@3.750.0': + resolution: {integrity: sha512-wFB9qqfa20AB0dElsQz5ZlZT5o+a+XzpEpmg0erylmGYqEOvh8NQWfDUVpRmQuGq9VbvW/8cIbxPoNqEbPtuWQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-ini@3.750.0': + resolution: {integrity: sha512-2YIZmyEr5RUd3uxXpxOLD9G67Bibm4I/65M6vKFP17jVMUT+R1nL7mKqmhEVO2p+BoeV+bwMyJ/jpTYG368PCg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-node@3.750.0': + resolution: {integrity: sha512-THWHHAceLwsOiowPEmKyhWVDlEUxH07GHSw5AQFDvNQtGKOQl0HSIFO1mKObT2Q2Vqzji9Bq8H58SO5BFtNPRw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-process@3.750.0': + resolution: {integrity: sha512-Q78SCH1n0m7tpu36sJwfrUSxI8l611OyysjQeMiIOliVfZICEoHcLHLcLkiR+tnIpZ3rk7d2EQ6R1jwlXnalMQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-sso@3.750.0': + resolution: {integrity: sha512-FGYrDjXN/FOQVi/t8fHSv8zCk+NEvtFnuc4cZUj5OIbM4vrfFc5VaPyn41Uza3iv6Qq9rZg0QOwWnqK8lNrqUw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.750.0': + resolution: {integrity: sha512-Nz8zs3YJ+GOTSrq+LyzbbC1Ffpt7pK38gcOyNZv76pP5MswKTUKNYBJehqwa+i7FcFQHsCk3TdhR8MT1ZR23uA==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-web-identity@3.972.13': resolution: {integrity: sha512-a6iFMh1pgUH0TdcouBppLJUfPM7Yd3R9S1xFodPtCRoLqCz2RQFA3qjA8x4112PVYXEd4/pHX2eihapq39w0rA==} engines: {node: '>=20.0.0'} + '@aws-sdk/endpoint-cache@3.723.0': + resolution: {integrity: sha512-2+a4WXRc+07uiPR+zJiPGKSOWaNJQNqitkks+6Hhm/haTLJqNVTgY2OWDh2PXvwMNpKB+AlGdhE65Oy6NzUgXg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-bucket-endpoint@3.734.0': + resolution: {integrity: sha512-etC7G18aF7KdZguW27GE/wpbrNmYLVT755EsFc8kXpZj8D6AFKxc7OuveinJmiy0bYXAMspJUWsF6CrGpOw6CQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-endpoint-discovery@3.734.0': + resolution: {integrity: sha512-hE3x9Sbqy64g/lcFIq7BF9IS1tSOyfBCyHf1xBgevWeFIDTWh647URuCNWoEwtw4HMEhO2MDUQcKf1PFh1dNDA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-expect-continue@3.734.0': + resolution: {integrity: sha512-P38/v1l6HjuB2aFUewt7ueAW5IvKkFcv5dalPtbMGRhLeyivBOHwbCyuRKgVs7z7ClTpu9EaViEGki2jEQqEsQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.750.0': + resolution: {integrity: sha512-ach0d2buDnX2TUausUbiXXFWFo3IegLnCrA+Rw8I9AYVpLN9lTaRwAYJwYC6zEuW9Golff8MwkYsp/OaC5tKMw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-host-header@3.734.0': + resolution: {integrity: sha512-LW7RRgSOHHBzWZnigNsDIzu3AiwtjeI2X66v+Wn1P1u+eXssy1+up4ZY/h+t2sU4LU36UvEf+jrZti9c6vRnFw==} + engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-host-header@3.972.6': resolution: {integrity: sha512-5XHwjPH1lHB+1q4bfC7T8Z5zZrZXfaLcjSMwTd1HPSPrCmPFMbg3UQ5vgNWcVj0xoX4HWqTGkSf2byrjlnRg5w==} engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-location-constraint@3.734.0': + resolution: {integrity: sha512-EJEIXwCQhto/cBfHdm3ZOeLxd2NlJD+X2F+ZTOxzokuhBtY0IONfC/91hOo5tWQweerojwshSMHRCKzRv1tlwg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-logger@3.734.0': + resolution: {integrity: sha512-mUMFITpJUW3LcKvFok176eI5zXAUomVtahb9IQBwLzkqFYOrMJvWAvoV4yuxrJ8TlQBG8gyEnkb9SnhZvjg67w==} + engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-logger@3.972.6': resolution: {integrity: sha512-iFnaMFMQdljAPrvsCVKYltPt2j40LQqukAbXvW7v0aL5I+1GO7bZ/W8m12WxW3gwyK5p5u1WlHg8TSAizC5cZw==} engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-recursion-detection@3.734.0': + resolution: {integrity: sha512-CUat2d9ITsFc2XsmeiRQO96iWpxSKYFjxvj27Hc7vo87YUHRnfMfnc8jw1EpxEwMcvBD7LsRa6vDNky6AjcrFA==} + engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-recursion-detection@3.972.6': resolution: {integrity: sha512-dY4v3of5EEMvik6+UDwQ96KfUFDk8m1oZDdkSc5lwi4o7rFrjnv0A+yTV+gu230iybQZnKgDLg/rt2P3H+Vscw==} engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-sdk-s3@3.750.0': + resolution: {integrity: sha512-3H6Z46cmAQCHQ0z8mm7/cftY5ifiLfCjbObrbyyp2fhQs9zk6gCKzIX8Zjhw0RMd93FZi3ebRuKJWmMglf4Itw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-sdk-sqs@3.750.0': + resolution: {integrity: sha512-+JEpz0P9wU92N0KsLFgOHAkipx8F3HO6YztycMwdLM+oKkurpE04q6d+N1WdBMIZwJ+YXlBU22L0dnR83hghtg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-ssec@3.734.0': + resolution: {integrity: sha512-d4yd1RrPW/sspEXizq2NSOUivnheac6LPeLSLnaeTbBG9g1KqIqvCzP1TfXEqv2CrWfHEsWtJpX7oyjySSPvDQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-user-agent@3.750.0': + resolution: {integrity: sha512-YYcslDsP5+2NZoN3UwuhZGkhAHPSli7HlJHBafBrvjGV/I9f8FuOO1d1ebxGdEP4HyRXUGyh+7Ur4q+Psk0ryw==} + engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-user-agent@3.972.15': resolution: {integrity: sha512-ABlFVcIMmuRAwBT+8q5abAxOr7WmaINirDJBnqGY5b5jSDo00UMlg/G4a0xoAgwm6oAECeJcwkvDlxDwKf58fQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/nested-clients@3.750.0': + resolution: {integrity: sha512-OH68BRF0rt9nDloq4zsfeHI0G21lj11a66qosaljtEP66PWm7tQ06feKbFkXHT5E1K3QhJW3nVyK8v2fEBY5fg==} + engines: {node: '>=18.0.0'} + '@aws-sdk/nested-clients@3.996.3': resolution: {integrity: sha512-AU5TY1V29xqwg/MxmA2odwysTez+ccFAhmfRJk+QZT5HNv90UTA9qKd1J9THlsQkvmH7HWTEV1lDNxkQO5PzNw==} engines: {node: '>=20.0.0'} + '@aws-sdk/region-config-resolver@3.734.0': + resolution: {integrity: sha512-Lvj1kPRC5IuJBr9DyJ9T9/plkh+EfKLy+12s/mykOy1JaKHDpvj+XGy2YO6YgYVOb8JFtaqloid+5COtje4JTQ==} + engines: {node: '>=18.0.0'} + '@aws-sdk/region-config-resolver@3.972.6': resolution: {integrity: sha512-Aa5PusHLXAqLTX1UKDvI3pHQJtIsF7Q+3turCHqfz/1F61/zDMWfbTC8evjhrrYVAtz9Vsv3SJ/waSUeu7B6gw==} engines: {node: '>=20.0.0'} + '@aws-sdk/signature-v4-multi-region@3.750.0': + resolution: {integrity: sha512-RA9hv1Irro/CrdPcOEXKwJ0DJYJwYCsauGEdRXihrRfy8MNSR9E+mD5/Fr5Rxjaq5AHM05DYnN3mg/DU6VwzSw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/token-providers@3.750.0': + resolution: {integrity: sha512-X/KzqZw41iWolwNdc8e3RMcNSMR364viHv78u6AefXOO5eRM40c4/LuST1jDzq35/LpnqRhL7/MuixOetw+sFw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/types@3.734.0': + resolution: {integrity: sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==} + engines: {node: '>=18.0.0'} + '@aws-sdk/types@3.973.4': resolution: {integrity: sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==} engines: {node: '>=20.0.0'} + '@aws-sdk/util-arn-parser@3.723.0': + resolution: {integrity: sha512-ZhEfvUwNliOQROcAk34WJWVYTlTa4694kSVhDSjW6lE1bMataPnIN8A0ycukEzBXmd8ZSoBcQLn6lKGl7XIJ5w==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-dynamodb@3.750.0': + resolution: {integrity: sha512-xIxkwMYQiNwQ6GsPvOxJnG3+MK9sV8Io1qiKaBYaa4ktoSGi9QTm7BKN9LVZqM+JkiIT2Do6eVzvefY556+Q4g==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@aws-sdk/client-dynamodb': ^3.750.0 + + '@aws-sdk/util-endpoints@3.743.0': + resolution: {integrity: sha512-sN1l559zrixeh5x+pttrnd0A3+r34r0tmPkJ/eaaMaAzXqsmKU/xYre9K3FNnsSS1J1k4PEfk/nHDTVUgFYjnw==} + engines: {node: '>=18.0.0'} + '@aws-sdk/util-endpoints@3.996.3': resolution: {integrity: sha512-yWIQSNiCjykLL+ezN5A+DfBb1gfXTytBxm57e64lYmwxDHNmInYHRJYYRAGWG1o77vKEiWaw4ui28e3yb1k5aQ==} engines: {node: '>=20.0.0'} @@ -2384,9 +2577,21 @@ packages: resolution: {integrity: sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==} engines: {node: '>=18.0.0'} + '@aws-sdk/util-user-agent-browser@3.734.0': + resolution: {integrity: sha512-xQTCus6Q9LwUuALW+S76OL0jcWtMOVu14q+GoLnWPUM7QeUw963oQcLhF7oq0CtaLLKyl4GOUfcwc773Zmwwng==} + '@aws-sdk/util-user-agent-browser@3.972.6': resolution: {integrity: sha512-Fwr/llD6GOrFgQnKaI2glhohdGuBDfHfora6iG9qsBBBR8xv1SdCSwbtf5CWlUdCw5X7g76G/9Hf0Inh0EmoxA==} + '@aws-sdk/util-user-agent-node@3.750.0': + resolution: {integrity: sha512-84HJj9G9zbrHX2opLk9eHfDceB+UIHVrmflMzWHpsmo9fDuro/flIBqaVDlE021Osj6qIM0SJJcnL6s23j7JEw==} + engines: {node: '>=18.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + '@aws-sdk/util-user-agent-node@3.973.0': resolution: {integrity: sha512-A9J2G4Nf236e9GpaC1JnA8wRn6u6GjnOXiTwBLA6NUJhlBTIGfrTy+K1IazmF8y+4OFdW3O5TZlhyspJMqiqjA==} engines: {node: '>=20.0.0'} @@ -2396,6 +2601,10 @@ packages: aws-crt: optional: true + '@aws-sdk/xml-builder@3.734.0': + resolution: {integrity: sha512-Zrjxi5qwGEcUsJ0ru7fRtW74WcTS0rbLcehoFB+rN1GRi2hbLcFaYs4PwVA5diLeAJH0gszv3x4Hr/S87MfbKQ==} + engines: {node: '>=18.0.0'} + '@aws-sdk/xml-builder@3.972.8': resolution: {integrity: sha512-Ql8elcUdYCha83Ol7NznBsgN5GVZnv3vUd86fEc6waU6oUdY0T1O9NODkEEOS/Uaogr87avDrUC6DSeM4oXjZg==} engines: {node: '>=20.0.0'} @@ -7107,6 +7316,18 @@ packages: resolution: {integrity: sha512-qocxM/X4XGATqQtUkbE9SPUB6wekBi+FyJOMbPj0AhvyvFGYEmOlz6VB22iMePCQsFmMIvFSeViDvA7mZJG47g==} engines: {node: '>=18.0.0'} + '@smithy/abort-controller@4.2.12': + resolution: {integrity: sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader-native@4.2.3': + resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader@5.2.2': + resolution: {integrity: sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==} + engines: {node: '>=18.0.0'} + '@smithy/config-resolver@4.4.9': resolution: {integrity: sha512-ejQvXqlcU30h7liR9fXtj7PIAau1t/sFbJpgWPfiYDs7zd16jpH0IsSXKcba2jF6ChTXvIjACs27kNMc5xxE2Q==} engines: {node: '>=18.0.0'} @@ -7119,14 +7340,42 @@ packages: resolution: {integrity: sha512-3bsMLJJLTZGZqVGGeBVFfLzuRulVsGTj12BzRKODTHqUABpIr0jMN1vN3+u6r2OfyhAQ2pXaMZWX/swBK5I6PQ==} engines: {node: '>=18.0.0'} + '@smithy/eventstream-codec@4.2.12': + resolution: {integrity: sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.12': + resolution: {integrity: sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.3.12': + resolution: {integrity: sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.2.12': + resolution: {integrity: sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.2.12': + resolution: {integrity: sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==} + engines: {node: '>=18.0.0'} + '@smithy/fetch-http-handler@5.3.11': resolution: {integrity: sha512-wbTRjOxdFuyEg0CpumjZO0hkUl+fetJFqxNROepuLIoijQh51aMBmzFLfoQdwRjxsuuS2jizzIUTjPWgd8pd7g==} engines: {node: '>=18.0.0'} + '@smithy/hash-blob-browser@4.2.13': + resolution: {integrity: sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==} + engines: {node: '>=18.0.0'} + '@smithy/hash-node@4.2.10': resolution: {integrity: sha512-1VzIOI5CcsvMDvP3iv1vG/RfLJVVVc67dCRyLSB2Hn9SWCZrDO3zvcIzj3BfEtqRW5kcMg5KAeVf1K3dR6nD3w==} engines: {node: '>=18.0.0'} + '@smithy/hash-stream-node@4.2.12': + resolution: {integrity: sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==} + engines: {node: '>=18.0.0'} + '@smithy/invalid-dependency@4.2.10': resolution: {integrity: sha512-vy9KPNSFUU0ajFYk0sDZIYiUlAWGEAhRfehIr5ZkdFrRFTAuXEPUd41USuqHU6vvLX4r6Q9X7MKBco5+Il0Org==} engines: {node: '>=18.0.0'} @@ -7139,6 +7388,14 @@ packages: resolution: {integrity: sha512-Yfu664Qbf1B4IYIsYgKoABt010daZjkaCRvdU/sPnZG6TtHOB0md0RjNdLGzxe5UIdn9js4ftPICzmkRa9RJ4Q==} engines: {node: '>=18.0.0'} + '@smithy/is-array-buffer@4.2.2': + resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} + engines: {node: '>=18.0.0'} + + '@smithy/md5-js@4.2.12': + resolution: {integrity: sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-content-length@4.2.10': resolution: {integrity: sha512-TQZ9kX5c6XbjhaEBpvhSvMEZ0klBs1CFtOdPFwATZSbC9UeQfKHPLPN9Y+I6wZGMOavlYTOlHEPDrt42PMSH9w==} engines: {node: '>=18.0.0'} @@ -7203,6 +7460,10 @@ packages: resolution: {integrity: sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==} engines: {node: '>=18.0.0'} + '@smithy/types@4.13.1': + resolution: {integrity: sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==} + engines: {node: '>=18.0.0'} + '@smithy/url-parser@4.2.10': resolution: {integrity: sha512-uypjF7fCDsRk26u3qHmFI/ePL7bxxB9vKkE+2WKEciHhz+4QtbzWiHRVNRJwU3cKhrYDYQE3b0MRFtqfLYdA4A==} engines: {node: '>=18.0.0'} @@ -7211,6 +7472,10 @@ packages: resolution: {integrity: sha512-BKGuawX4Doq/bI/uEmg+Zyc36rJKWuin3py89PquXBIBqmbnJwBBsmKhdHfNEp0+A4TDgLmT/3MSKZ1SxHcR6w==} engines: {node: '>=18.0.0'} + '@smithy/util-base64@4.3.2': + resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} + engines: {node: '>=18.0.0'} + '@smithy/util-body-length-browser@4.2.1': resolution: {integrity: sha512-SiJeLiozrAoCrgDBUgsVbmqHmMgg/2bA15AzcbcW+zan7SuyAVHN4xTSbq0GlebAIwlcaX32xacnrG488/J/6g==} engines: {node: '>=18.0.0'} @@ -7227,6 +7492,10 @@ packages: resolution: {integrity: sha512-/swhmt1qTiVkaejlmMPPDgZhEaWb/HWMGRBheaxwuVkusp/z+ErJyQxO6kaXumOciZSWlmq6Z5mNylCd33X7Ig==} engines: {node: '>=18.0.0'} + '@smithy/util-buffer-from@4.2.2': + resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} + engines: {node: '>=18.0.0'} + '@smithy/util-config-provider@4.2.1': resolution: {integrity: sha512-462id/00U8JWFw6qBuTSWfN5TxOHvDu4WliI97qOIOnuC/g+NDAknTU8eoGXEPlLkRVgWEr03jJBLV4o2FL8+A==} engines: {node: '>=18.0.0'} @@ -7247,6 +7516,10 @@ packages: resolution: {integrity: sha512-c1hHtkgAWmE35/50gmdKajgGAKV3ePJ7t6UtEmpfCWJmQE9BQAQPz0URUVI89eSkcDqCtzqllxzG28IQoZPvwA==} engines: {node: '>=18.0.0'} + '@smithy/util-hex-encoding@4.2.2': + resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} + engines: {node: '>=18.0.0'} + '@smithy/util-middleware@4.2.10': resolution: {integrity: sha512-LxaQIWLp4y0r72eA8mwPNQ9va4h5KeLM0I3M/HV9klmFaY2kN766wf5vsTzmaOpNNb7GgXAd9a25P3h8T49PSA==} engines: {node: '>=18.0.0'} @@ -7271,6 +7544,14 @@ packages: resolution: {integrity: sha512-DSIwNaWtmzrNQHv8g7DBGR9mulSit65KSj5ymGEIAknmIN8IpbZefEep10LaMG/P/xquwbmJ1h9ectz8z6mV6g==} engines: {node: '>=18.0.0'} + '@smithy/util-utf8@4.2.2': + resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-waiter@4.2.13': + resolution: {integrity: sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ==} + engines: {node: '>=18.0.0'} + '@smithy/uuid@1.1.1': resolution: {integrity: sha512-dSfDCeihDmZlV2oyr0yWPTUfh07suS+R5OB+FZGiv/hHyK3hrFBW5rR1UYjfa57vBsrP9lciFkRPzebaV1Qujw==} engines: {node: '>=18.0.0'} @@ -7930,6 +8211,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@9.0.8': + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + '@types/watchpack@2.4.4': resolution: {integrity: sha512-SbuSavsPxfOPZwVHBgQUVuzYBe6+8KL7dwiJLXaj5rmv3DxktOMwX5WP1J6UontwUbewjVoc7pCgZvqy6rPn+A==} @@ -8549,10 +8833,6 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} - engines: {node: '>=12'} - ansi-regex@6.2.2: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} @@ -10374,6 +10654,10 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-xml-parser@4.4.1: + resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} + hasBin: true + fast-xml-parser@5.3.6: resolution: {integrity: sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==} hasBin: true @@ -12235,6 +12519,9 @@ packages: mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + mnemonist@0.38.3: + resolution: {integrity: sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==} + mocked-exports@0.1.1: resolution: {integrity: sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==} @@ -12594,6 +12881,9 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + obliterator@1.6.1: + resolution: {integrity: sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -14307,6 +14597,9 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strnum@1.1.2: + resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} + strnum@2.1.2: resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==} @@ -15240,6 +15533,10 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + valibot@1.2.0: resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} peerDependencies: @@ -16150,71 +16447,447 @@ snapshots: esbuild: 0.25.12 tinyglobby: 0.2.15 transitivePeerDependencies: - - '@aws-sdk/credential-provider-web-identity' - - '@remix-run/react' - - '@sveltejs/kit' - - encoding - - next - - react - - rollup - - supports-color - - svelte - - vue - - vue-router + - '@aws-sdk/credential-provider-web-identity' + - '@remix-run/react' + - '@sveltejs/kit' + - encoding + - next + - react + - rollup + - supports-color + - svelte + - vue + - vue-router + + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.4 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.4 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.4 + '@aws-sdk/util-locate-window': 3.893.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.4 + '@aws-sdk/util-locate-window': 3.893.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.4 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.4 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-dynamodb@3.750.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.750.0 + '@aws-sdk/credential-provider-node': 3.750.0 + '@aws-sdk/middleware-endpoint-discovery': 3.734.0 + '@aws-sdk/middleware-host-header': 3.734.0 + '@aws-sdk/middleware-logger': 3.734.0 + '@aws-sdk/middleware-recursion-detection': 3.734.0 + '@aws-sdk/middleware-user-agent': 3.750.0 + '@aws-sdk/region-config-resolver': 3.734.0 + '@aws-sdk/types': 3.734.0 + '@aws-sdk/util-endpoints': 3.743.0 + '@aws-sdk/util-user-agent-browser': 3.734.0 + '@aws-sdk/util-user-agent-node': 3.750.0 + '@smithy/config-resolver': 4.4.9 + '@smithy/core': 3.23.6 + '@smithy/fetch-http-handler': 5.3.11 + '@smithy/hash-node': 4.2.10 + '@smithy/invalid-dependency': 4.2.10 + '@smithy/middleware-content-length': 4.2.10 + '@smithy/middleware-endpoint': 4.4.20 + '@smithy/middleware-retry': 4.4.37 + '@smithy/middleware-serde': 4.2.11 + '@smithy/middleware-stack': 4.2.10 + '@smithy/node-config-provider': 4.3.10 + '@smithy/node-http-handler': 4.4.12 + '@smithy/protocol-http': 5.3.10 + '@smithy/smithy-client': 4.12.0 + '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.10 + '@smithy/util-base64': 4.3.1 + '@smithy/util-body-length-browser': 4.2.1 + '@smithy/util-body-length-node': 4.2.2 + '@smithy/util-defaults-mode-browser': 4.3.36 + '@smithy/util-defaults-mode-node': 4.2.39 + '@smithy/util-endpoints': 3.3.1 + '@smithy/util-middleware': 4.2.10 + '@smithy/util-retry': 4.2.10 + '@smithy/util-utf8': 4.2.1 + '@smithy/util-waiter': 4.2.13 + '@types/uuid': 9.0.8 + tslib: 2.8.1 + uuid: 9.0.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-s3@3.750.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.750.0 + '@aws-sdk/credential-provider-node': 3.750.0 + '@aws-sdk/middleware-bucket-endpoint': 3.734.0 + '@aws-sdk/middleware-expect-continue': 3.734.0 + '@aws-sdk/middleware-flexible-checksums': 3.750.0 + '@aws-sdk/middleware-host-header': 3.734.0 + '@aws-sdk/middleware-location-constraint': 3.734.0 + '@aws-sdk/middleware-logger': 3.734.0 + '@aws-sdk/middleware-recursion-detection': 3.734.0 + '@aws-sdk/middleware-sdk-s3': 3.750.0 + '@aws-sdk/middleware-ssec': 3.734.0 + '@aws-sdk/middleware-user-agent': 3.750.0 + '@aws-sdk/region-config-resolver': 3.734.0 + '@aws-sdk/signature-v4-multi-region': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@aws-sdk/util-endpoints': 3.743.0 + '@aws-sdk/util-user-agent-browser': 3.734.0 + '@aws-sdk/util-user-agent-node': 3.750.0 + '@aws-sdk/xml-builder': 3.734.0 + '@smithy/config-resolver': 4.4.9 + '@smithy/core': 3.23.6 + '@smithy/eventstream-serde-browser': 4.2.12 + '@smithy/eventstream-serde-config-resolver': 4.3.12 + '@smithy/eventstream-serde-node': 4.2.12 + '@smithy/fetch-http-handler': 5.3.11 + '@smithy/hash-blob-browser': 4.2.13 + '@smithy/hash-node': 4.2.10 + '@smithy/hash-stream-node': 4.2.12 + '@smithy/invalid-dependency': 4.2.10 + '@smithy/md5-js': 4.2.12 + '@smithy/middleware-content-length': 4.2.10 + '@smithy/middleware-endpoint': 4.4.20 + '@smithy/middleware-retry': 4.4.37 + '@smithy/middleware-serde': 4.2.11 + '@smithy/middleware-stack': 4.2.10 + '@smithy/node-config-provider': 4.3.10 + '@smithy/node-http-handler': 4.4.12 + '@smithy/protocol-http': 5.3.10 + '@smithy/smithy-client': 4.12.0 + '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.10 + '@smithy/util-base64': 4.3.1 + '@smithy/util-body-length-browser': 4.2.1 + '@smithy/util-body-length-node': 4.2.2 + '@smithy/util-defaults-mode-browser': 4.3.36 + '@smithy/util-defaults-mode-node': 4.2.39 + '@smithy/util-endpoints': 3.3.1 + '@smithy/util-middleware': 4.2.10 + '@smithy/util-retry': 4.2.10 + '@smithy/util-stream': 4.5.15 + '@smithy/util-utf8': 4.2.1 + '@smithy/util-waiter': 4.2.13 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sqs@3.750.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.750.0 + '@aws-sdk/credential-provider-node': 3.750.0 + '@aws-sdk/middleware-host-header': 3.734.0 + '@aws-sdk/middleware-logger': 3.734.0 + '@aws-sdk/middleware-recursion-detection': 3.734.0 + '@aws-sdk/middleware-sdk-sqs': 3.750.0 + '@aws-sdk/middleware-user-agent': 3.750.0 + '@aws-sdk/region-config-resolver': 3.734.0 + '@aws-sdk/types': 3.734.0 + '@aws-sdk/util-endpoints': 3.743.0 + '@aws-sdk/util-user-agent-browser': 3.734.0 + '@aws-sdk/util-user-agent-node': 3.750.0 + '@smithy/config-resolver': 4.4.9 + '@smithy/core': 3.23.6 + '@smithy/fetch-http-handler': 5.3.11 + '@smithy/hash-node': 4.2.10 + '@smithy/invalid-dependency': 4.2.10 + '@smithy/md5-js': 4.2.12 + '@smithy/middleware-content-length': 4.2.10 + '@smithy/middleware-endpoint': 4.4.20 + '@smithy/middleware-retry': 4.4.37 + '@smithy/middleware-serde': 4.2.11 + '@smithy/middleware-stack': 4.2.10 + '@smithy/node-config-provider': 4.3.10 + '@smithy/node-http-handler': 4.4.12 + '@smithy/protocol-http': 5.3.10 + '@smithy/smithy-client': 4.12.0 + '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.10 + '@smithy/util-base64': 4.3.1 + '@smithy/util-body-length-browser': 4.2.1 + '@smithy/util-body-length-node': 4.2.2 + '@smithy/util-defaults-mode-browser': 4.3.36 + '@smithy/util-defaults-mode-node': 4.2.39 + '@smithy/util-endpoints': 3.3.1 + '@smithy/util-middleware': 4.2.10 + '@smithy/util-retry': 4.2.10 + '@smithy/util-utf8': 4.2.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.750.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.750.0 + '@aws-sdk/middleware-host-header': 3.734.0 + '@aws-sdk/middleware-logger': 3.734.0 + '@aws-sdk/middleware-recursion-detection': 3.734.0 + '@aws-sdk/middleware-user-agent': 3.750.0 + '@aws-sdk/region-config-resolver': 3.734.0 + '@aws-sdk/types': 3.734.0 + '@aws-sdk/util-endpoints': 3.743.0 + '@aws-sdk/util-user-agent-browser': 3.734.0 + '@aws-sdk/util-user-agent-node': 3.750.0 + '@smithy/config-resolver': 4.4.9 + '@smithy/core': 3.23.6 + '@smithy/fetch-http-handler': 5.3.11 + '@smithy/hash-node': 4.2.10 + '@smithy/invalid-dependency': 4.2.10 + '@smithy/middleware-content-length': 4.2.10 + '@smithy/middleware-endpoint': 4.4.20 + '@smithy/middleware-retry': 4.4.37 + '@smithy/middleware-serde': 4.2.11 + '@smithy/middleware-stack': 4.2.10 + '@smithy/node-config-provider': 4.3.10 + '@smithy/node-http-handler': 4.4.12 + '@smithy/protocol-http': 5.3.10 + '@smithy/smithy-client': 4.12.0 + '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.10 + '@smithy/util-base64': 4.3.1 + '@smithy/util-body-length-browser': 4.2.1 + '@smithy/util-body-length-node': 4.2.2 + '@smithy/util-defaults-mode-browser': 4.3.36 + '@smithy/util-defaults-mode-node': 4.2.39 + '@smithy/util-endpoints': 3.3.1 + '@smithy/util-middleware': 4.2.10 + '@smithy/util-retry': 4.2.10 + '@smithy/util-utf8': 4.2.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.750.0': + dependencies: + '@aws-sdk/types': 3.734.0 + '@smithy/core': 3.23.6 + '@smithy/node-config-provider': 4.3.10 + '@smithy/property-provider': 4.2.10 + '@smithy/protocol-http': 5.3.10 + '@smithy/signature-v4': 5.3.10 + '@smithy/smithy-client': 4.12.0 + '@smithy/types': 4.13.0 + '@smithy/util-middleware': 4.2.10 + fast-xml-parser: 4.4.1 + tslib: 2.8.1 + + '@aws-sdk/core@3.973.15': + dependencies: + '@aws-sdk/types': 3.973.4 + '@aws-sdk/xml-builder': 3.972.8 + '@smithy/core': 3.23.6 + '@smithy/node-config-provider': 4.3.10 + '@smithy/property-provider': 4.2.10 + '@smithy/protocol-http': 5.3.10 + '@smithy/signature-v4': 5.3.10 + '@smithy/smithy-client': 4.12.0 + '@smithy/types': 4.13.0 + '@smithy/util-base64': 4.3.1 + '@smithy/util-middleware': 4.2.10 + '@smithy/util-utf8': 4.2.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.750.0': + dependencies: + '@aws-sdk/core': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@smithy/property-provider': 4.2.10 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.750.0': + dependencies: + '@aws-sdk/core': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@smithy/fetch-http-handler': 5.3.11 + '@smithy/node-http-handler': 4.4.12 + '@smithy/property-provider': 4.2.10 + '@smithy/protocol-http': 5.3.10 + '@smithy/smithy-client': 4.12.0 + '@smithy/types': 4.13.0 + '@smithy/util-stream': 4.5.15 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.750.0': + dependencies: + '@aws-sdk/core': 3.750.0 + '@aws-sdk/credential-provider-env': 3.750.0 + '@aws-sdk/credential-provider-http': 3.750.0 + '@aws-sdk/credential-provider-process': 3.750.0 + '@aws-sdk/credential-provider-sso': 3.750.0 + '@aws-sdk/credential-provider-web-identity': 3.750.0 + '@aws-sdk/nested-clients': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@smithy/credential-provider-imds': 4.2.10 + '@smithy/property-provider': 4.2.10 + '@smithy/shared-ini-file-loader': 4.4.5 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.750.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.750.0 + '@aws-sdk/credential-provider-http': 3.750.0 + '@aws-sdk/credential-provider-ini': 3.750.0 + '@aws-sdk/credential-provider-process': 3.750.0 + '@aws-sdk/credential-provider-sso': 3.750.0 + '@aws-sdk/credential-provider-web-identity': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@smithy/credential-provider-imds': 4.2.10 + '@smithy/property-provider': 4.2.10 + '@smithy/shared-ini-file-loader': 4.4.5 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.750.0': + dependencies: + '@aws-sdk/core': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@smithy/property-provider': 4.2.10 + '@smithy/shared-ini-file-loader': 4.4.5 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.750.0': + dependencies: + '@aws-sdk/client-sso': 3.750.0 + '@aws-sdk/core': 3.750.0 + '@aws-sdk/token-providers': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@smithy/property-provider': 4.2.10 + '@smithy/shared-ini-file-loader': 4.4.5 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.750.0': + dependencies: + '@aws-sdk/core': 3.750.0 + '@aws-sdk/nested-clients': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@smithy/property-provider': 4.2.10 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt - '@aws-crypto/sha256-browser@5.2.0': + '@aws-sdk/credential-provider-web-identity@3.972.13': dependencies: - '@aws-crypto/sha256-js': 5.2.0 - '@aws-crypto/supports-web-crypto': 5.2.0 - '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.973.15 + '@aws-sdk/nested-clients': 3.996.3 '@aws-sdk/types': 3.973.4 - '@aws-sdk/util-locate-window': 3.893.0 - '@smithy/util-utf8': 2.3.0 + '@smithy/property-provider': 4.2.10 + '@smithy/shared-ini-file-loader': 4.4.5 + '@smithy/types': 4.13.0 tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt - '@aws-crypto/sha256-js@5.2.0': + '@aws-sdk/endpoint-cache@3.723.0': dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.4 + mnemonist: 0.38.3 tslib: 2.8.1 - '@aws-crypto/supports-web-crypto@5.2.0': + '@aws-sdk/middleware-bucket-endpoint@3.734.0': + dependencies: + '@aws-sdk/types': 3.734.0 + '@aws-sdk/util-arn-parser': 3.723.0 + '@smithy/node-config-provider': 4.3.10 + '@smithy/protocol-http': 5.3.10 + '@smithy/types': 4.13.0 + '@smithy/util-config-provider': 4.2.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-endpoint-discovery@3.734.0': dependencies: + '@aws-sdk/endpoint-cache': 3.723.0 + '@aws-sdk/types': 3.734.0 + '@smithy/node-config-provider': 4.3.10 + '@smithy/protocol-http': 5.3.10 + '@smithy/types': 4.13.0 tslib: 2.8.1 - '@aws-crypto/util@5.2.0': + '@aws-sdk/middleware-expect-continue@3.734.0': dependencies: - '@aws-sdk/types': 3.973.4 - '@smithy/util-utf8': 2.3.0 + '@aws-sdk/types': 3.734.0 + '@smithy/protocol-http': 5.3.10 + '@smithy/types': 4.13.0 tslib: 2.8.1 - '@aws-sdk/core@3.973.15': + '@aws-sdk/middleware-flexible-checksums@3.750.0': dependencies: - '@aws-sdk/types': 3.973.4 - '@aws-sdk/xml-builder': 3.972.8 - '@smithy/core': 3.23.6 + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@smithy/is-array-buffer': 4.2.1 '@smithy/node-config-provider': 4.3.10 - '@smithy/property-provider': 4.2.10 '@smithy/protocol-http': 5.3.10 - '@smithy/signature-v4': 5.3.10 - '@smithy/smithy-client': 4.12.0 '@smithy/types': 4.13.0 - '@smithy/util-base64': 4.3.1 '@smithy/util-middleware': 4.2.10 + '@smithy/util-stream': 4.5.15 '@smithy/util-utf8': 4.2.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-web-identity@3.972.13': + '@aws-sdk/middleware-host-header@3.734.0': dependencies: - '@aws-sdk/core': 3.973.15 - '@aws-sdk/nested-clients': 3.996.3 - '@aws-sdk/types': 3.973.4 - '@smithy/property-provider': 4.2.10 - '@smithy/shared-ini-file-loader': 4.4.5 + '@aws-sdk/types': 3.734.0 + '@smithy/protocol-http': 5.3.10 '@smithy/types': 4.13.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt '@aws-sdk/middleware-host-header@3.972.6': dependencies: @@ -16223,12 +16896,31 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 + '@aws-sdk/middleware-location-constraint@3.734.0': + dependencies: + '@aws-sdk/types': 3.734.0 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.734.0': + dependencies: + '@aws-sdk/types': 3.734.0 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@aws-sdk/middleware-logger@3.972.6': dependencies: '@aws-sdk/types': 3.973.4 '@smithy/types': 4.13.0 tslib: 2.8.1 + '@aws-sdk/middleware-recursion-detection@3.734.0': + dependencies: + '@aws-sdk/types': 3.734.0 + '@smithy/protocol-http': 5.3.10 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@aws-sdk/middleware-recursion-detection@3.972.6': dependencies: '@aws-sdk/types': 3.973.4 @@ -16237,6 +16929,48 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 + '@aws-sdk/middleware-sdk-s3@3.750.0': + dependencies: + '@aws-sdk/core': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@aws-sdk/util-arn-parser': 3.723.0 + '@smithy/core': 3.23.6 + '@smithy/node-config-provider': 4.3.10 + '@smithy/protocol-http': 5.3.10 + '@smithy/signature-v4': 5.3.10 + '@smithy/smithy-client': 4.12.0 + '@smithy/types': 4.13.0 + '@smithy/util-config-provider': 4.2.1 + '@smithy/util-middleware': 4.2.10 + '@smithy/util-stream': 4.5.15 + '@smithy/util-utf8': 4.2.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-sqs@3.750.0': + dependencies: + '@aws-sdk/types': 3.734.0 + '@smithy/smithy-client': 4.12.0 + '@smithy/types': 4.13.0 + '@smithy/util-hex-encoding': 4.2.1 + '@smithy/util-utf8': 4.2.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.734.0': + dependencies: + '@aws-sdk/types': 3.734.0 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.750.0': + dependencies: + '@aws-sdk/core': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@aws-sdk/util-endpoints': 3.743.0 + '@smithy/core': 3.23.6 + '@smithy/protocol-http': 5.3.10 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@aws-sdk/middleware-user-agent@3.972.15': dependencies: '@aws-sdk/core': 3.973.15 @@ -16247,6 +16981,49 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 + '@aws-sdk/nested-clients@3.750.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.750.0 + '@aws-sdk/middleware-host-header': 3.734.0 + '@aws-sdk/middleware-logger': 3.734.0 + '@aws-sdk/middleware-recursion-detection': 3.734.0 + '@aws-sdk/middleware-user-agent': 3.750.0 + '@aws-sdk/region-config-resolver': 3.734.0 + '@aws-sdk/types': 3.734.0 + '@aws-sdk/util-endpoints': 3.743.0 + '@aws-sdk/util-user-agent-browser': 3.734.0 + '@aws-sdk/util-user-agent-node': 3.750.0 + '@smithy/config-resolver': 4.4.9 + '@smithy/core': 3.23.6 + '@smithy/fetch-http-handler': 5.3.11 + '@smithy/hash-node': 4.2.10 + '@smithy/invalid-dependency': 4.2.10 + '@smithy/middleware-content-length': 4.2.10 + '@smithy/middleware-endpoint': 4.4.20 + '@smithy/middleware-retry': 4.4.37 + '@smithy/middleware-serde': 4.2.11 + '@smithy/middleware-stack': 4.2.10 + '@smithy/node-config-provider': 4.3.10 + '@smithy/node-http-handler': 4.4.12 + '@smithy/protocol-http': 5.3.10 + '@smithy/smithy-client': 4.12.0 + '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.10 + '@smithy/util-base64': 4.3.1 + '@smithy/util-body-length-browser': 4.2.1 + '@smithy/util-body-length-node': 4.2.2 + '@smithy/util-defaults-mode-browser': 4.3.36 + '@smithy/util-defaults-mode-node': 4.2.39 + '@smithy/util-endpoints': 3.3.1 + '@smithy/util-middleware': 4.2.10 + '@smithy/util-retry': 4.2.10 + '@smithy/util-utf8': 4.2.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/nested-clients@3.996.3': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -16290,6 +17067,15 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/region-config-resolver@3.734.0': + dependencies: + '@aws-sdk/types': 3.734.0 + '@smithy/node-config-provider': 4.3.10 + '@smithy/types': 4.13.0 + '@smithy/util-config-provider': 4.2.1 + '@smithy/util-middleware': 4.2.10 + tslib: 2.8.1 + '@aws-sdk/region-config-resolver@3.972.6': dependencies: '@aws-sdk/types': 3.973.4 @@ -16298,11 +17084,52 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 + '@aws-sdk/signature-v4-multi-region@3.750.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@smithy/protocol-http': 5.3.10 + '@smithy/signature-v4': 5.3.10 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.750.0': + dependencies: + '@aws-sdk/nested-clients': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@smithy/property-provider': 4.2.10 + '@smithy/shared-ini-file-loader': 4.4.5 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.734.0': + dependencies: + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@aws-sdk/types@3.973.4': dependencies: '@smithy/types': 4.13.0 tslib: 2.8.1 + '@aws-sdk/util-arn-parser@3.723.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-dynamodb@3.750.0(@aws-sdk/client-dynamodb@3.750.0)': + dependencies: + '@aws-sdk/client-dynamodb': 3.750.0 + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.743.0': + dependencies: + '@aws-sdk/types': 3.734.0 + '@smithy/types': 4.13.0 + '@smithy/util-endpoints': 3.3.1 + tslib: 2.8.1 + '@aws-sdk/util-endpoints@3.996.3': dependencies: '@aws-sdk/types': 3.973.4 @@ -16315,6 +17142,13 @@ snapshots: dependencies: tslib: 2.8.1 + '@aws-sdk/util-user-agent-browser@3.734.0': + dependencies: + '@aws-sdk/types': 3.734.0 + '@smithy/types': 4.13.0 + bowser: 2.12.1 + tslib: 2.8.1 + '@aws-sdk/util-user-agent-browser@3.972.6': dependencies: '@aws-sdk/types': 3.973.4 @@ -16322,6 +17156,14 @@ snapshots: bowser: 2.12.1 tslib: 2.8.1 + '@aws-sdk/util-user-agent-node@3.750.0': + dependencies: + '@aws-sdk/middleware-user-agent': 3.750.0 + '@aws-sdk/types': 3.734.0 + '@smithy/node-config-provider': 4.3.10 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@aws-sdk/util-user-agent-node@3.973.0': dependencies: '@aws-sdk/middleware-user-agent': 3.972.15 @@ -16330,6 +17172,11 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 + '@aws-sdk/xml-builder@3.734.0': + dependencies: + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@aws-sdk/xml-builder@3.972.8': dependencies: '@smithy/types': 4.13.0 @@ -21840,6 +22687,20 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 + '@smithy/abort-controller@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader-native@4.2.3': + dependencies: + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader@5.2.2': + dependencies: + tslib: 2.8.1 + '@smithy/config-resolver@4.4.9': dependencies: '@smithy/node-config-provider': 4.3.10 @@ -21870,6 +22731,36 @@ snapshots: '@smithy/url-parser': 4.2.10 tslib: 2.8.1 + '@smithy/eventstream-codec@4.2.12': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.13.1 + '@smithy/util-hex-encoding': 4.2.2 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.2.12': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.3.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.2.12': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.2.12': + dependencies: + '@smithy/eventstream-codec': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + '@smithy/fetch-http-handler@5.3.11': dependencies: '@smithy/protocol-http': 5.3.10 @@ -21878,6 +22769,13 @@ snapshots: '@smithy/util-base64': 4.3.1 tslib: 2.8.1 + '@smithy/hash-blob-browser@4.2.13': + dependencies: + '@smithy/chunked-blob-reader': 5.2.2 + '@smithy/chunked-blob-reader-native': 4.2.3 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + '@smithy/hash-node@4.2.10': dependencies: '@smithy/types': 4.13.0 @@ -21885,6 +22783,12 @@ snapshots: '@smithy/util-utf8': 4.2.1 tslib: 2.8.1 + '@smithy/hash-stream-node@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + '@smithy/invalid-dependency@4.2.10': dependencies: '@smithy/types': 4.13.0 @@ -21898,6 +22802,16 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/is-array-buffer@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/md5-js@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + '@smithy/middleware-content-length@4.2.10': dependencies: '@smithy/protocol-http': 5.3.10 @@ -22008,6 +22922,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/types@4.13.1': + dependencies: + tslib: 2.8.1 + '@smithy/url-parser@4.2.10': dependencies: '@smithy/querystring-parser': 4.2.10 @@ -22020,6 +22938,12 @@ snapshots: '@smithy/util-utf8': 4.2.1 tslib: 2.8.1 + '@smithy/util-base64@4.3.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + '@smithy/util-body-length-browser@4.2.1': dependencies: tslib: 2.8.1 @@ -22038,6 +22962,11 @@ snapshots: '@smithy/is-array-buffer': 4.2.1 tslib: 2.8.1 + '@smithy/util-buffer-from@4.2.2': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + tslib: 2.8.1 + '@smithy/util-config-provider@4.2.1': dependencies: tslib: 2.8.1 @@ -22069,6 +22998,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/util-hex-encoding@4.2.2': + dependencies: + tslib: 2.8.1 + '@smithy/util-middleware@4.2.10': dependencies: '@smithy/types': 4.13.0 @@ -22105,6 +23038,17 @@ snapshots: '@smithy/util-buffer-from': 4.2.1 tslib: 2.8.1 + '@smithy/util-utf8@4.2.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-waiter@4.2.13': + dependencies: + '@smithy/abort-controller': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + '@smithy/uuid@1.1.1': dependencies: tslib: 2.8.1 @@ -22899,6 +23843,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/uuid@9.0.8': {} + '@types/watchpack@2.4.4': dependencies: '@types/graceful-fs': 4.1.9 @@ -23096,7 +24042,7 @@ snapshots: '@vercel/queue@0.1.1': dependencies: - '@vercel/oidc': 3.0.5 + '@vercel/oidc': 3.2.0 mixpart: 0.0.5 '@vercel/routing-utils@5.3.0': @@ -23731,8 +24677,6 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} - ansi-regex@6.2.2: {} ansi-styles@4.3.0: @@ -25949,6 +26893,10 @@ snapshots: fast-uri@3.1.0: {} + fast-xml-parser@4.4.1: + dependencies: + strnum: 1.1.2 + fast-xml-parser@5.3.6: dependencies: strnum: 2.1.2 @@ -28198,6 +29146,10 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 + mnemonist@0.38.3: + dependencies: + obliterator: 1.6.1 + mocked-exports@0.1.1: {} module-definition@6.0.1: @@ -29052,6 +30004,8 @@ snapshots: object-inspect@1.13.4: {} + obliterator@1.6.1: {} + obug@2.1.1: {} ofetch@1.4.1: @@ -31253,7 +32207,7 @@ snapshots: strip-ansi@7.1.0: dependencies: - ansi-regex: 6.1.0 + ansi-regex: 6.2.2 strip-ansi@7.2.0: dependencies: @@ -31284,6 +32238,8 @@ snapshots: dependencies: js-tokens: 9.0.1 + strnum@1.1.2: {} + strnum@2.1.2: {} strtok3@10.3.4: @@ -32175,6 +33131,8 @@ snapshots: uuid@11.1.0: {} + uuid@9.0.1: {} + valibot@1.2.0(typescript@5.9.3): optionalDependencies: typescript: 5.9.3