Skip to content

feat(world-aws): AWS World implementation for Workflow DevKit#21

Open
stewartjarod wants to merge 20 commits into
mainfrom
workflow-dev
Open

feat(world-aws): AWS World implementation for Workflow DevKit#21
stewartjarod wants to merge 20 commits into
mainfrom
workflow-dev

Conversation

@stewartjarod

Copy link
Copy Markdown
Contributor

Summary

  • New @wraps.dev/world-aws package — AWS-backed World implementation for Vercel Workflow DevKit using DynamoDB (storage + streams) and SQS (queue)
  • Storage layer — Full run/step/event/hook/wait lifecycle backed by DynamoDB with TTL, pagination, batch writes with retry, and HKDF-SHA256 encryption
  • Queue layer — SQS-based queue with dual-protocol handler (body envelope for Lambda, header-based for local dev), idempotency keys, and delay support
  • Lambda handlercreateSQSHandler() with partial batch failure reporting and configurable timeout/sleep handling
  • Streamer — DynamoDB Streams-based event streaming with cursor tracking and iterator recovery
  • Local dev SQS pollerstart() automatically bridges SQS to local HTTP server when PORT or WORKFLOW_LOCAL_BASE_URL is set
  • Setup CLIworld-aws-setup bin for creating DynamoDB tables and SQS queues
  • E2E integration — Wired up @workflow/world-testing suite (addition, idempotency, hooks, null bytes, error handling) with per-run queue isolation
  • 214 unit tests covering storage, queue, streamer, Lambda handler, config, encryption, marshal, util, tables, pagination, TTL, batch-write, duration, and errors

Test plan

  • cd packages/world-aws && pnpm test — 214 unit tests pass (no AWS needed)
  • AWS_REGION=us-east-1 AWS_ACCOUNT_ID=<id> pnpm test:world — 6 e2e tests pass (requires SST dev running + AWS credentials)
  • pnpm build — package compiles to ESM + CJS with type declarations
  • pnpm typecheck — no type errors

🤖 Generated with Claude Code

stewartjarod and others added 15 commits February 21, 2026 14:14
… SQS)

Implements @wraps.dev/world-aws package providing Storage, Queue, and
Streamer backed by DynamoDB and SQS. Enables workflow execution on
users' own AWS accounts with zero stored credentials.

Storage: 6 DynamoDB tables (runs, steps, events, hooks, waits, streams)
Queue: SQS Standard with DLQs, Lambda adapter with partial batch failure
Streamer: DynamoDB chunks with monotonic ULID ordering
Setup: idempotent bin/setup.ts for table and queue creation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace polling-based readFromStream() with DynamoDB Streams change
data capture. Two-phase approach: catch up from table, then subscribe
to INSERT events via GetRecords. Deduplicates overlap, filters by
streamId, handles shard expiry and exhaustion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ion, AbortSignal

- Fix deleteHooksAndWaitsForRun() to paginate through all results
- Add real idempotency to step_started/completed/failed handlers
- Extract shared marshal functions into storage/marshal.ts
- Make queue URL construction lazy with AWS_ACCOUNT_ID validation
- Add AbortSignal support to readFromStream() for cancellation
- Add 11 new tests covering pagination, idempotency, streams, queue
- Fix lint issues: interface→type, formatting, export patterns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Per-run AES-256 key derivation via Web Crypto HKDF from a base key
in WORKFLOW_AWS_ENCRYPTION_KEY env var. When set, workflow core
encrypts all run data (inputs, outputs, step results) with AES-256-GCM.
When absent, encryption is disabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…n, JSDoc

Add WorldError with throttling/credential detection at storage boundaries,
validate encryption key format at config time, wire AbortController through
close() to cancel active stream reads, and document public API with JSDoc.

Tests: pagination edge cases, error utilities, config validation, encryption
determinism (135 total).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
npm strips bin entries with ./ prefix paths (npm/cli#7302). Fix by
using a thin bin/ wrapper and letting npm pkg fix normalize paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… handler

Add encryption configuration and HKDF-SHA256 docs to README, SQS Lambda
handler to Next.js example, and Lambda handler section to Next.js README.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Architecture overview with event flow diagram, table schemas, queue
config, and streaming internals. Local development with LocalStack
and DynamoDB Local. Migration guide from Vercel World with comparison
table and zero-code-change setup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the queue handler returns { timeoutSeconds }, re-deliver the
message after a delay instead of silently dropping it. Accepts an
optional onTimeout callback for custom scheduling (e.g. EventBridge);
defaults to SQS re-queue with DelaySeconds capped at 900s.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make handleHookCreated atomic with TransactWriteCommand, wrap
deleteHooksAndWaitsForRun with wrapAWSError, parallelize batch
deletes and SQS record processing, refactor list/listByCorrelationId
to build QueryCommand params inline, add batchWriteWithRetry helper
for UnprocessedItems, remove unused DLQ URL variables in setup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Duration type + computeTTL helper, threaded through config, storage,
and streamer. Setup enables TTL on all tables. Items without the ttl
attribute are unaffected (opt-in via config.ttl or WORKFLOW_AWS_TTL).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ization

- Add world-aws-poll CLI for local development (polls SQS, forwards to local HTTP)
- Add CJS output format alongside ESM, fix export condition ordering
- Add retry/timeout config to all AWS SDK clients (maxAttempts: 5)
- Fix Date serialization: convert Date objects to ISO strings before DynamoDB storage
- Fix resumeAt deserialization: convert ISO string back to Date on read
- Fix DynamoDB reserved word conflict: alias `output` in UpdateExpression
- Fix SQS queue existence check for both error name variants
- Add cursor validation with proper error messages
- Bump to 0.2.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…poller, unit tests

- Add dual-protocol support to createQueueHandler (header-based for local
  dev + body envelope for Lambda SQS)
- Add SQS poller to start() for local dev/test bridging
- Wire up @workflow/world-testing e2e suite with per-run queue isolation
- Add unit tests for util, tables, and marshal modules
- Fix lint/formatting across package

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Feb 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
wraps-web Ready Ready Preview, Comment Feb 25, 2026 4:17pm
wraps-website Ready Ready Preview, Comment Feb 25, 2026 4:17pm

Request Review

Comment thread packages/world-aws/src/bin/poll.ts Fixed
Comment thread packages/world-aws/src/bin/poll.ts Fixed
Comment thread packages/world-aws/examples/nextjs/app/workflows/onboarding.ts Fixed
stewartjarod and others added 3 commits February 24, 2026 22:02
…script)

SST dev infra wires SQS → Lambda → local dev server for full-path e2e
testing against the workflow devkit test suite. Non-production stages only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clean split: "Automations" = no-code visual builder (platform),
"Workflows" = code-first durable execution (world-aws SDK).

DB, API, frontend, CLI, and tests renamed. SQL table names kept
as-is (aliased at Drizzle level). Old files re-export from new
locations for backward compat.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Auto-fix import sorting and formatting from rename. Delete old
workflow-builder node/edge/test files that were duplicated (not
shimmed) into automation-builder — fixes baseline ratchet overages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +198 to +202
const response = await fetch(url, {
method,
headers: baseHeaders,
body: fetchBody,
});

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 4 months ago

In general, to fix this problem, we should ensure that user-controlled data used in request URLs is strictly validated and/or encoded, especially when used as a path segment. We want to prevent characters that could alter the URL structure (like /, \, ?, #) or inject additional path elements, while preserving the existing functionality of referencing automations by their IDs.

The best approach here is to introduce a small, local validator for automation IDs (for example, restricting them to a sane character set such as alphanumerics, dashes, and underscores) and use its sanitized / validated result when constructing the URL. If the provided automationId does not match the allowed pattern, we can fail early and return an error result instead of issuing the HTTP request. This keeps the hostname and base path unchanged, prevents any path traversal or malformed ID injection, and minimally impacts behavior for legitimate IDs.

Concretely, within apps/web/src/actions/automations.ts:

  1. Add a helper function near callAutomationScheduleApi (or above it) such as sanitizeAutomationId or isValidAutomationId that:
    • Checks that the ID is non-empty and matches a safe regex, e.g. /^[A-Za-z0-9_-]{1,128}$/.
    • Returns a boolean or throws/returns an error string.
  2. In callAutomationScheduleApi, before constructing url, validate automationId using that helper.
    • If invalid, log (optionally) and return { success: false, error: "Invalid automation ID" } without calling fetch.
    • If valid, use the original automationId (the same string) when building the URL, since we've ensured it can't contain dangerous characters.
  3. Because all the tainted flows (from updateAutomation, deleteAutomation, enableAutomation, disableAutomation) share the same sink (callAutomationScheduleApi), validating in this one place will fix all four alert variants without changing the public function signatures.

No extra imports are strictly required; this can be done with basic TypeScript and a regex.

Suggested changeset 1
apps/web/src/actions/automations.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/apps/web/src/actions/automations.ts b/apps/web/src/actions/automations.ts
--- a/apps/web/src/actions/automations.ts
+++ b/apps/web/src/actions/automations.ts
@@ -146,6 +146,15 @@
 // ═══════════════════════════════════════════════════════════════════════════
 
 /**
+ * Validate that an automation ID is safe to use in a URL path segment.
+ * Only allow a restricted character set to avoid path manipulation.
+ */
+function isValidAutomationId(automationId: string): boolean {
+  // Allow common ID characters (alphanumeric, dash, underscore) and cap length.
+  return /^[A-Za-z0-9_-]{1,128}$/.test(automationId);
+}
+
+/**
  * Call the automation schedule API to manage EventBridge schedules.
  * Follows the same pattern as batch.ts for auth + org headers.
  */
@@ -155,6 +164,13 @@
   action: "enable" | "disable" | "update",
   body?: { cronExpression: string; timezone?: string }
 ): Promise<{ success: boolean; error?: string }> {
+  if (!isValidAutomationId(automationId)) {
+    console.error(
+      `[automation-schedule] Invalid automationId provided: ${automationId}`
+    );
+    return { success: false, error: "Invalid automation ID" };
+  }
+
   const apiUrl = process.env.NEXT_PUBLIC_API_URL;
   if (!apiUrl) {
     console.error("[automation-schedule] NEXT_PUBLIC_API_URL not configured");
EOF
@@ -146,6 +146,15 @@
// ═══════════════════════════════════════════════════════════════════════════

/**
* Validate that an automation ID is safe to use in a URL path segment.
* Only allow a restricted character set to avoid path manipulation.
*/
function isValidAutomationId(automationId: string): boolean {
// Allow common ID characters (alphanumeric, dash, underscore) and cap length.
return /^[A-Za-z0-9_-]{1,128}$/.test(automationId);
}

/**
* Call the automation schedule API to manage EventBridge schedules.
* Follows the same pattern as batch.ts for auth + org headers.
*/
@@ -155,6 +164,13 @@
action: "enable" | "disable" | "update",
body?: { cronExpression: string; timezone?: string }
): Promise<{ success: boolean; error?: string }> {
if (!isValidAutomationId(automationId)) {
console.error(
`[automation-schedule] Invalid automationId provided: ${automationId}`
);
return { success: false, error: "Invalid automation ID" };
}

const apiUrl = process.env.NEXT_PUBLIC_API_URL;
if (!apiUrl) {
console.error("[automation-schedule] NEXT_PUBLIC_API_URL not configured");
Copilot is powered by AI and may make mistakes. Always verify output.
return { success: true };
} catch (error) {
console.error(
`[automation-schedule] API ${action} error for ${automationId}:`,

Check failure

Code scanning / CodeQL

Use of externally-controlled format string High

Format string depends on a
user-provided value
.
Format string depends on a
user-provided value
.
Format string depends on a
user-provided value
.
Format string depends on a
user-provided value
.

Copilot Autofix

AI 4 months ago

General fix: Avoid letting untrusted data influence the format string of logging/formatting functions. Either (a) place untrusted data into subsequent arguments while using %s placeholders in a constant format string, or (b) avoid format-string-style logging entirely and log structured data (objects) or concatenate strings safely.

Best fix here: We can keep the existing logging behavior but move all dynamic/tainted values into a separate argument rather than interpolating them into the first string. For example, instead of:

console.error(
  `[automation-schedule] API ${action} error for ${automationId}:`,
  error
);

we can log a constant string as the first argument and pass details as an object:

console.error(
  "[automation-schedule] API error",
  { action, automationId, error }
);

This removes any dependency of the format string on untrusted input (automationId) and also addresses all CodeQL variants associated with the same call site (no matter whether automationId came from updateAutomation, deleteAutomation, enableAutomation, or disableAutomation). This change preserves functionality (we still log the same conceptual information, arguably more clearly) and requires no new imports or helpers, and is confined to the provided snippet in apps/web/src/actions/automations.ts.

Specific location:

  • File: apps/web/src/actions/automations.ts
  • Function: callAutomationScheduleApi
  • Lines: 213–217, replacing the console.error call to avoid a template literal that embeds action/automationId.

No additional methods or imports are needed.

Suggested changeset 1
apps/web/src/actions/automations.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/apps/web/src/actions/automations.ts b/apps/web/src/actions/automations.ts
--- a/apps/web/src/actions/automations.ts
+++ b/apps/web/src/actions/automations.ts
@@ -212,8 +212,8 @@
     return { success: true };
   } catch (error) {
     console.error(
-      `[automation-schedule] API ${action} error for ${automationId}:`,
-      error
+      "[automation-schedule] API error",
+      { action, automationId, error }
     );
     return {
       success: false,
EOF
@@ -212,8 +212,8 @@
return { success: true };
} catch (error) {
console.error(
`[automation-schedule] API ${action} error for ${automationId}:`,
error
"[automation-schedule] API error",
{ action, automationId, error }
);
return {
success: false,
Copilot is powered by AI and may make mistakes. Always verify output.
stewartjarod and others added 2 commits February 26, 2026 12:26
Complete the rename inside automation-builder components, store,
tests, and API workers. Rename page components and route segment
[workflowId] → [automationId]. Update toast messages and CLAUDE.md
docs to reflect new paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
API tests: update mock paths (workflow-queue→automation-queue,
workflow-scheduler→automation-scheduler), add automation table exports
to @wraps/db mocks, fix error message expectations. Rename 3
DB-dependent tests to .integration.test.ts to match exclusion pattern.

Auth tests: skip Stripe config assertions when env vars are missing,
fix conditional Stripe plugin test to handle both cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@stewartjarod stewartjarod added the enhancement New feature or request label Mar 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants