Add AES-256-GCM encryption primitives and HKDF key derivation - #956

Merged
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption
Feb 20, 2026
Merged

Add AES-256-GCM encryption primitives and HKDF key derivation#956
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 6, 2026

Copy link
Copy Markdown
Member

Summary

Adds the encryption foundation for end-to-end encryption of workflow user data:

  • @workflow/core: Browser-compatible encrypt()/decrypt() using Web Crypto API (AES-256-GCM), importKey() for callers to convert raw bytes to CryptoKey once per run
  • @workflow/world: Overloaded getEncryptionKeyForRun interface — accepts WorkflowRun entity when available, or (runId, context) for start() when the run doesn't exist yet
  • @workflow/world-vercel: deriveRunKey() (HKDF-SHA256), fetchRunKey() (API call with projectId + runId), getEncryptionKeyForRun implementation reading context?.deploymentId
  • All runtime callers updated to call importKey() after key resolution and pass WorkflowRun entity where available
  • handleSuspension refactored to accept WorkflowRun parameter
  • resume-hook.ts uses getHookByTokenWithKey to fetch full WorkflowRun alongside hook + key
  • Separate changesets for @workflow/core, @workflow/world, @workflow/world-vercel

@vercel

vercelBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

CopilotAI review requested due to automatic review settings February 6, 2026 02:30
@changeset-bot

changeset-botBot commented Feb 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c28baee

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelPatch
@workflow/worldPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/web-sharedPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production523049572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2486452862817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4705
✅ example4705
✅ express4705
✅ fastify4705
✅ hono4705
✅ nextjs-turbopack5002
✅ nextjs-webpack5002
✅ nitro4705
✅ nuxt4705
✅ sveltekit4705
✅ vite4705
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds AES-256-GCM encryption with HKDF-SHA256 key derivation to the @workflow/world-vercel package. The implementation provides per-run encryption isolation by deriving unique keys from a deployment key, project ID, and run ID. It integrates seamlessly with the async serialization infrastructure added in PR #955 and uses the client-generated run IDs from PR #954.

Changes:

  • Implements createEncryptor() and createEncryptorFromEnv() functions with full Encryptor interface support
  • Adds AES-256-GCM encryption with random nonces and 128-bit authentication tags
  • Uses HKDF-SHA256 for per-run key derivation with projectId and runId as context
  • Wires encryption into createVercelWorld() via environment variables (VERCEL_DEPLOYMENT_KEY, VERCEL_PROJECT_ID)
  • Includes 18 comprehensive tests covering round-trip, format validation, isolation, and tamper detection

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.

FileDescription
packages/world-vercel/src/encryption.tsCore encryption implementation with key derivation, encrypt/decrypt, and key material access
packages/world-vercel/src/encryption.test.tsComprehensive test suite with 18 tests covering functionality and security properties
packages/world-vercel/src/index.tsIntegration into World creation and public API exports
Comments suppressed due to low confidence (1)

packages/world-vercel/src/index.ts:12

  • The VercelEncryptionConfig interface is exported from encryption.ts but not re-exported from index.ts. Users who want to use createEncryptor() directly would need to import from the internal encryption module, which is not a typical pattern.

Consider adding to index.ts:

export type { VercelEncryptionConfig } from './encryption.js';

This follows the pattern already established with APIConfig and makes the public API more discoverable.

export { createEncryptor, createEncryptorFromEnv } from './encryption.js';
export { createQueue } from './queue.js';
export { createStorage } from './storage.js';
export { createStreamer } from './streamer.js';
export type { APIConfig } from './utils.js';

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #956 - Add Vercel encryption implementation (AES-256-GCM with HKDF)

Summary: Implements AES-256-GCM encryption with HKDF-SHA256 per-run key derivation in @workflow/world-vercel. This is the core crypto PR and the most security-sensitive in the stack.

Crypto Design Review

Algorithm choices are sound:

  • AES-256-GCM: authenticated encryption, industry standard, hardware-accelerated
  • HKDF-SHA256 for key derivation: correct use of KDF for deriving per-run keys from a deployment key
  • 12-byte (96-bit) nonce: standard for GCM
  • 128-bit auth tag: standard for GCM
  • Random nonce per encryption: correct, avoids nonce reuse

Key derivation design:

  • HKDF(key=deploymentKey, salt=zeros, info="${projectId}|${runId}")
  • Zero salt is acceptable here since the deployment key is already random key material (HKDF specification allows this)
  • Using projectId|runId as info provides good domain separation between runs and projects

Concerns

  1. Zero salt vs random salt: While zero salt is acceptable per RFC 5869 when the input key material is already uniformly random, using a random salt stored alongside the ciphertext would provide additional protection against certain attacks (e.g., if the deployment key has less entropy than expected). This is a minor concern given that VERCEL_DEPLOYMENT_KEY should be high-entropy, but worth considering for defense in depth.

  2. Key derivation per encryption call: The deriveKey function is called on every encrypt() and decrypt() call. For workflows with many steps, this means the same HKDF derivation is repeated for each serialization. Consider caching the derived key per (projectId, runId) pair:

    // Cache derived keys to avoid re-deriving per encrypt/decrypt callconstkeyCache=newMap<string,CryptoKey>();

    The cache key would be ${projectId}|${runId}. This is a performance optimization, not a correctness issue.

  3. getKeyMaterial returns raw key bytes: The getKeyMaterial() method returns the raw deploymentKey bytes. This is documented as being for external decryption (o11y tooling), but it means any code with access to the encryptor can extract the master key. Consider:

    • Adding a warning comment about this being sensitive
    • Possibly scoping the returned key material to the derived per-run key rather than the deployment key
  4. createEncryptorFromEnv validation: When VERCEL_DEPLOYMENT_KEY is set but decodes to fewer than 32 bytes, createEncryptor will throw. However, createEncryptorFromEnv doesn't catch this, meaning the world creation will fail hard. Consider catching and logging a warning instead, falling back to no encryption, so a misconfigured key doesn't crash the entire application.

  5. Format prefix encr is 4 bytes of overhead per encrypted payload. This is fine for data payloads but for stream chunks (which may be small), it adds overhead per chunk. Not a major concern but worth noting.

Test Coverage

The 18 tests are comprehensive:

  • Round-trip tests (basic, empty, large, all byte values)
  • Format validation (prefix, structure, nonce randomness)
  • Per-run key isolation
  • Tamper detection
  • Cross-project/cross-key isolation
  • Environment variable handling (missing vars, valid vars)

One test case that could be added: concurrent encryption -- verifying that multiple concurrent encrypt() calls with the same encryptor produce correct results (nonce uniqueness under concurrency).

Minor

  • The Buffer.from(deploymentKeyBase64, 'base64') in createEncryptorFromEnv will silently ignore invalid base64 characters. Consider validating the decoded length matches expectations, or at minimum let createEncryptor's 32-byte check catch it.

Overall, the crypto implementation is solid and follows best practices. The concerns above are mostly optimizations and hardening suggestions rather than security issues.

Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the crypto design is solid — AES-256-GCM with HKDF-SHA256 per-run key derivation is the right approach, the separation between browser-compatible core encryption and Node.js-specific key management is clean, and the World interface integration is well thought out.

A few concerns below, mostly around performance during replay and a potentially unreachable code path.

Comment threadpackages/core/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts
@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.026s (-1.2%)1.004s (~)0.979s101.00x
💻 LocalExpress0.032s (-1.8%)1.005s (~)0.973s101.25x
💻 LocalNext.js (Turbopack)0.040s (-9.9% 🟢)1.005s (~)0.965s101.56x
🌐 RedisNext.js (Turbopack)0.046s (-2.1%)1.005s (~)0.958s101.81x
🌐 MongoDBNext.js (Turbopack)0.078s (-15.4% 🟢)1.007s (~)0.929s103.04x
🐘 PostgresExpress0.127s (-0.8%)1.010s (~)0.883s104.94x
🐘 PostgresNitro0.377s (+317.5% 🔺)1.009s (~)0.632s1014.67x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.612s (+25.2% 🔺)1.929s (-4.1%)1.318s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (~)2.005s (~)0.931s101.00x
🌐 RedisNext.js (Turbopack)1.103s (~)2.006s (~)0.903s101.03x
💻 LocalNext.js (Turbopack)1.105s (+0.7%)2.005s (~)0.900s101.03x
💻 LocalExpress1.105s (-0.5%)2.006s (~)0.900s101.03x
🌐 MongoDBNext.js (Turbopack)1.315s (~)2.007s (~)0.692s101.22x
🐘 PostgresNitro2.403s (-2.6%)3.014s (~)0.611s102.24x
🐘 PostgresExpress2.404s (~)3.013s (~)0.609s102.24x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.087s (+4.6%)3.098s (-5.3% 🟢)1.011s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.543s (~)11.021s (~)0.478s31.00x
🌐 RedisNext.js (Turbopack)10.698s (~)11.021s (~)0.323s31.01x
💻 LocalNext.js (Turbopack)10.749s (~)11.023s (~)0.274s31.02x
💻 LocalExpress10.837s (~)11.022s (~)0.186s31.03x
🌐 MongoDBNext.js (Turbopack)12.209s (-0.7%)13.020s (~)0.810s31.16x
🐘 PostgresNitro20.252s (~)21.062s (~)0.810s21.92x
🐘 PostgresExpress20.368s (+13.9% 🔺)21.056s (+13.5% 🔺)0.688s21.93x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)15.943s (-1.2%)16.835s (-2.1%)0.892s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro26.759s (~)27.047s (~)0.288s31.00x
🌐 RedisNext.js (Turbopack)26.847s (~)27.050s (~)0.203s31.00x
💻 LocalNext.js (Turbopack)27.221s (~)28.053s (~)0.832s31.02x
💻 LocalExpress27.502s (~)28.051s (~)0.549s31.03x
🌐 MongoDBNext.js (Turbopack)30.465s (~)31.038s (~)0.573s21.14x
🐘 PostgresExpress50.386s (+31.9% 🔺)51.129s (+30.8% 🔺)0.743s21.88x
🐘 PostgresNitro50.532s (~)51.128s (~)0.597s21.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)42.878s (+10.0% 🔺)43.689s (+7.4% 🔺)0.811s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.206s (~)55.095s (~)0.888s21.00x
💻 LocalNitro55.559s (~)56.099s (~)0.539s21.02x
💻 LocalNext.js (Turbopack)56.587s (~)57.114s (~)0.527s21.04x
💻 LocalExpress57.264s (~)58.103s (~)0.838s21.06x
🌐 MongoDBNext.js (Turbopack)61.036s (~)61.072s (~)0.035s21.13x
🐘 PostgresExpress78.183s (+3.8%)78.675s (+3.3%)0.492s21.44x
🐘 PostgresNitro100.149s (~)100.224s (~)0.075s11.85x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)81.685s (-1.1%)82.625s (-1.8%)0.940s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.245s (+1.5%)2.006s (~)0.761s151.00x
💻 LocalNitro1.352s (~)2.005s (~)0.653s151.09x
💻 LocalNext.js (Turbopack)1.413s (+0.6%)2.005s (~)0.592s151.13x
💻 LocalExpress1.415s (+1.0%)2.006s (~)0.590s151.14x
🐘 PostgresExpress1.927s (+3.7%)2.515s (+25.0% 🔺)0.588s121.55x
🌐 MongoDBNext.js (Turbopack)2.118s (-1.8%)3.008s (~)0.890s101.70x
🐘 PostgresNitro2.253s (+1.4%)3.013s (~)0.760s101.81x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.246s (-7.6% 🟢)3.144s (-11.0% 🟢)0.898s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.280s (+1.8%)3.006s (~)0.727s101.00x
💻 LocalNext.js (Turbopack)2.431s (-2.2%)2.918s (-3.0%)0.487s111.07x
🌐 RedisNext.js (Turbopack)2.466s (-1.2%)3.007s (~)0.542s101.08x
💻 LocalExpress2.648s (+7.7% 🔺)3.008s (~)0.359s101.16x
🌐 MongoDBNext.js (Turbopack)4.696s (-0.6%)5.176s (~)0.479s62.06x
🐘 PostgresExpress8.660s (-13.7% 🟢)9.537s (-8.0% 🟢)0.877s43.80x
🐘 PostgresNitro9.498s (+33.6% 🔺)10.057s (+33.5% 🔺)0.559s34.17x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.689s (-36.9% 🟢)3.374s (-38.7% 🟢)0.685s91.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.042s (-0.8%)4.725s (-3.0%)0.682s71.00x
💻 LocalNitro6.167s (+2.8%)7.016s (+9.4% 🔺)0.850s51.53x
💻 LocalNext.js (Turbopack)6.445s (-5.7% 🟢)7.016s (-5.4% 🟢)0.571s51.59x
💻 LocalExpress7.857s (+16.2% 🔺)8.272s (+17.9% 🔺)0.414s41.94x
🌐 MongoDBNext.js (Turbopack)9.925s (+1.2%)10.348s (~)0.423s32.46x
🐘 PostgresNitro46.235s (-8.3% 🟢)47.132s (-7.8% 🟢)0.897s111.44x
🐘 PostgresExpress47.330s (-4.8%)48.095s (-4.1%)0.765s111.71x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.494s (-20.5% 🟢)4.448s (-19.5% 🟢)0.953s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.263s (+2.0%)2.006s (~)0.742s151.00x
💻 LocalNitro1.374s (~)2.004s (~)0.631s151.09x
💻 LocalNext.js (Turbopack)1.432s (~)2.005s (~)0.573s151.13x
💻 LocalExpress1.449s (+1.5%)2.006s (~)0.557s151.15x
🐘 PostgresExpress1.837s (-9.5% 🟢)2.011s (-16.1% 🟢)0.174s151.45x
🐘 PostgresNitro2.095s (-5.5% 🟢)2.679s (+3.2%)0.585s121.66x
🌐 MongoDBNext.js (Turbopack)2.127s (-0.6%)3.008s (~)0.881s101.68x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.212s (+0.9%)3.124s (-13.6% 🟢)0.912s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.353s (~)3.007s (~)0.654s101.00x
🌐 RedisNext.js (Turbopack)2.488s (~)3.008s (~)0.520s101.06x
💻 LocalNext.js (Turbopack)2.606s (-3.3%)3.007s (~)0.401s101.11x
💻 LocalExpress2.754s (+6.7% 🔺)3.008s (~)0.255s101.17x
🌐 MongoDBNext.js (Turbopack)4.742s (~)5.177s (~)0.434s62.02x
🐘 PostgresExpress9.968s (-16.0% 🟢)10.373s (-13.8% 🟢)0.405s34.24x
🐘 PostgresNitro10.288s (-7.0% 🟢)11.029s (-5.7% 🟢)0.742s34.37x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.275s (-11.9% 🟢)3.197s (-12.8% 🟢)0.922s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.020s (-0.6%)4.725s (~)0.705s71.00x
💻 LocalNext.js (Turbopack)6.608s (-5.0%)7.216s (-4.0%)0.608s51.64x
💻 LocalNitro6.888s (~)7.015s (-2.8%)0.127s51.71x
💻 LocalExpress8.103s (+12.5% 🔺)8.775s (+9.5% 🔺)0.672s42.02x
🌐 MongoDBNext.js (Turbopack)10.056s (+1.1%)10.348s (-3.1%)0.292s32.50x
🐘 PostgresNitro46.096s (-8.6% 🟢)47.125s (-7.8% 🟢)1.029s111.47x
🐘 PostgresExpress48.860s (-1.0%)49.106s (-2.0%)0.246s112.16x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.233s (-4.2%)4.059s (-22.3% 🟢)0.826s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.114s (+3.2%)1.002s (~)0.009s (-3.2%)1.014s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)0.145s (-2.8%)1.000s (~)0.001s (~)1.007s (~)0.862s101.27x
💻 LocalNext.js (Turbopack)0.151s (+6.9% 🔺)1.001s (~)0.010s (-9.4% 🟢)1.015s (~)0.865s101.32x
💻 LocalExpress0.171s (-6.4% 🟢)1.003s (~)0.011s (+10.4% 🔺)1.016s (~)0.845s101.50x
🌐 MongoDBNext.js (Turbopack)0.510s (+2.5%)0.935s (-1.6%)0.001s (-6.7% 🟢)1.008s (~)0.498s104.46x
🐘 PostgresExpress1.317s (-9.1% 🟢)1.800s (+8.6% 🔺)0.001s (-15.4% 🟢)2.012s (~)0.695s1011.51x
🐘 PostgresNitro2.275s (+75.8% 🔺)2.768s (+58.7% 🔺)0.001s (-23.1% 🟢)3.014s (+49.7% 🔺)0.739s1019.89x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.481s (-26.7% 🟢)2.272s (-18.6% 🟢)0.121s (-6.9% 🟢)2.773s (-21.9% 🟢)1.292s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro11/12
🐘 PostgresExpress8/12
▲ VercelNext.js (Turbopack)12/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local12/12
Next.js (Turbopack)🌐 Redis7/12
Nitro💻 Local12/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

if (encryptionKeyOverride) {
encryptionKey = encryptionKeyOverride;
} else {
const rawKey = await world.getEncryptionKeyForRun?.(workflowRun);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For cross-deployment hook resumptions / fetching of the encryption key, we should consider enriching the "source" of the request for the key (i.e. resumeHook in this case) for the audit log to include the context.

Comment threadpackages/world-vercel/src/index.ts Outdated
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add AES-256-GCM encryption primitives and HKDF key derivation - #956

Merged
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption
Feb 20, 2026
Merged

Add AES-256-GCM encryption primitives and HKDF key derivation#956
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 6, 2026

Copy link
Copy Markdown
Member

Summary

Adds the encryption foundation for end-to-end encryption of workflow user data:

  • @workflow/core: Browser-compatible encrypt()/decrypt() using Web Crypto API (AES-256-GCM), importKey() for callers to convert raw bytes to CryptoKey once per run
  • @workflow/world: Overloaded getEncryptionKeyForRun interface — accepts WorkflowRun entity when available, or (runId, context) for start() when the run doesn't exist yet
  • @workflow/world-vercel: deriveRunKey() (HKDF-SHA256), fetchRunKey() (API call with projectId + runId), getEncryptionKeyForRun implementation reading context?.deploymentId
  • All runtime callers updated to call importKey() after key resolution and pass WorkflowRun entity where available
  • handleSuspension refactored to accept WorkflowRun parameter
  • resume-hook.ts uses getHookByTokenWithKey to fetch full WorkflowRun alongside hook + key
  • Separate changesets for @workflow/core, @workflow/world, @workflow/world-vercel

@vercel

vercelBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

CopilotAI review requested due to automatic review settings February 6, 2026 02:30
@changeset-bot

changeset-botBot commented Feb 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c28baee

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelPatch
@workflow/worldPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/web-sharedPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production523049572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2486452862817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4705
✅ example4705
✅ express4705
✅ fastify4705
✅ hono4705
✅ nextjs-turbopack5002
✅ nextjs-webpack5002
✅ nitro4705
✅ nuxt4705
✅ sveltekit4705
✅ vite4705
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds AES-256-GCM encryption with HKDF-SHA256 key derivation to the @workflow/world-vercel package. The implementation provides per-run encryption isolation by deriving unique keys from a deployment key, project ID, and run ID. It integrates seamlessly with the async serialization infrastructure added in PR #955 and uses the client-generated run IDs from PR #954.

Changes:

  • Implements createEncryptor() and createEncryptorFromEnv() functions with full Encryptor interface support
  • Adds AES-256-GCM encryption with random nonces and 128-bit authentication tags
  • Uses HKDF-SHA256 for per-run key derivation with projectId and runId as context
  • Wires encryption into createVercelWorld() via environment variables (VERCEL_DEPLOYMENT_KEY, VERCEL_PROJECT_ID)
  • Includes 18 comprehensive tests covering round-trip, format validation, isolation, and tamper detection

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.

FileDescription
packages/world-vercel/src/encryption.tsCore encryption implementation with key derivation, encrypt/decrypt, and key material access
packages/world-vercel/src/encryption.test.tsComprehensive test suite with 18 tests covering functionality and security properties
packages/world-vercel/src/index.tsIntegration into World creation and public API exports
Comments suppressed due to low confidence (1)

packages/world-vercel/src/index.ts:12

  • The VercelEncryptionConfig interface is exported from encryption.ts but not re-exported from index.ts. Users who want to use createEncryptor() directly would need to import from the internal encryption module, which is not a typical pattern.

Consider adding to index.ts:

export type { VercelEncryptionConfig } from './encryption.js';

This follows the pattern already established with APIConfig and makes the public API more discoverable.

export { createEncryptor, createEncryptorFromEnv } from './encryption.js';
export { createQueue } from './queue.js';
export { createStorage } from './storage.js';
export { createStreamer } from './streamer.js';
export type { APIConfig } from './utils.js';

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #956 - Add Vercel encryption implementation (AES-256-GCM with HKDF)

Summary: Implements AES-256-GCM encryption with HKDF-SHA256 per-run key derivation in @workflow/world-vercel. This is the core crypto PR and the most security-sensitive in the stack.

Crypto Design Review

Algorithm choices are sound:

  • AES-256-GCM: authenticated encryption, industry standard, hardware-accelerated
  • HKDF-SHA256 for key derivation: correct use of KDF for deriving per-run keys from a deployment key
  • 12-byte (96-bit) nonce: standard for GCM
  • 128-bit auth tag: standard for GCM
  • Random nonce per encryption: correct, avoids nonce reuse

Key derivation design:

  • HKDF(key=deploymentKey, salt=zeros, info="${projectId}|${runId}")
  • Zero salt is acceptable here since the deployment key is already random key material (HKDF specification allows this)
  • Using projectId|runId as info provides good domain separation between runs and projects

Concerns

  1. Zero salt vs random salt: While zero salt is acceptable per RFC 5869 when the input key material is already uniformly random, using a random salt stored alongside the ciphertext would provide additional protection against certain attacks (e.g., if the deployment key has less entropy than expected). This is a minor concern given that VERCEL_DEPLOYMENT_KEY should be high-entropy, but worth considering for defense in depth.

  2. Key derivation per encryption call: The deriveKey function is called on every encrypt() and decrypt() call. For workflows with many steps, this means the same HKDF derivation is repeated for each serialization. Consider caching the derived key per (projectId, runId) pair:

    // Cache derived keys to avoid re-deriving per encrypt/decrypt callconstkeyCache=newMap<string,CryptoKey>();

    The cache key would be ${projectId}|${runId}. This is a performance optimization, not a correctness issue.

  3. getKeyMaterial returns raw key bytes: The getKeyMaterial() method returns the raw deploymentKey bytes. This is documented as being for external decryption (o11y tooling), but it means any code with access to the encryptor can extract the master key. Consider:

    • Adding a warning comment about this being sensitive
    • Possibly scoping the returned key material to the derived per-run key rather than the deployment key
  4. createEncryptorFromEnv validation: When VERCEL_DEPLOYMENT_KEY is set but decodes to fewer than 32 bytes, createEncryptor will throw. However, createEncryptorFromEnv doesn't catch this, meaning the world creation will fail hard. Consider catching and logging a warning instead, falling back to no encryption, so a misconfigured key doesn't crash the entire application.

  5. Format prefix encr is 4 bytes of overhead per encrypted payload. This is fine for data payloads but for stream chunks (which may be small), it adds overhead per chunk. Not a major concern but worth noting.

Test Coverage

The 18 tests are comprehensive:

  • Round-trip tests (basic, empty, large, all byte values)
  • Format validation (prefix, structure, nonce randomness)
  • Per-run key isolation
  • Tamper detection
  • Cross-project/cross-key isolation
  • Environment variable handling (missing vars, valid vars)

One test case that could be added: concurrent encryption -- verifying that multiple concurrent encrypt() calls with the same encryptor produce correct results (nonce uniqueness under concurrency).

Minor

  • The Buffer.from(deploymentKeyBase64, 'base64') in createEncryptorFromEnv will silently ignore invalid base64 characters. Consider validating the decoded length matches expectations, or at minimum let createEncryptor's 32-byte check catch it.

Overall, the crypto implementation is solid and follows best practices. The concerns above are mostly optimizations and hardening suggestions rather than security issues.

Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the crypto design is solid — AES-256-GCM with HKDF-SHA256 per-run key derivation is the right approach, the separation between browser-compatible core encryption and Node.js-specific key management is clean, and the World interface integration is well thought out.

A few concerns below, mostly around performance during replay and a potentially unreachable code path.

Comment threadpackages/core/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts
@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.026s (-1.2%)1.004s (~)0.979s101.00x
💻 LocalExpress0.032s (-1.8%)1.005s (~)0.973s101.25x
💻 LocalNext.js (Turbopack)0.040s (-9.9% 🟢)1.005s (~)0.965s101.56x
🌐 RedisNext.js (Turbopack)0.046s (-2.1%)1.005s (~)0.958s101.81x
🌐 MongoDBNext.js (Turbopack)0.078s (-15.4% 🟢)1.007s (~)0.929s103.04x
🐘 PostgresExpress0.127s (-0.8%)1.010s (~)0.883s104.94x
🐘 PostgresNitro0.377s (+317.5% 🔺)1.009s (~)0.632s1014.67x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.612s (+25.2% 🔺)1.929s (-4.1%)1.318s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (~)2.005s (~)0.931s101.00x
🌐 RedisNext.js (Turbopack)1.103s (~)2.006s (~)0.903s101.03x
💻 LocalNext.js (Turbopack)1.105s (+0.7%)2.005s (~)0.900s101.03x
💻 LocalExpress1.105s (-0.5%)2.006s (~)0.900s101.03x
🌐 MongoDBNext.js (Turbopack)1.315s (~)2.007s (~)0.692s101.22x
🐘 PostgresNitro2.403s (-2.6%)3.014s (~)0.611s102.24x
🐘 PostgresExpress2.404s (~)3.013s (~)0.609s102.24x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.087s (+4.6%)3.098s (-5.3% 🟢)1.011s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.543s (~)11.021s (~)0.478s31.00x
🌐 RedisNext.js (Turbopack)10.698s (~)11.021s (~)0.323s31.01x
💻 LocalNext.js (Turbopack)10.749s (~)11.023s (~)0.274s31.02x
💻 LocalExpress10.837s (~)11.022s (~)0.186s31.03x
🌐 MongoDBNext.js (Turbopack)12.209s (-0.7%)13.020s (~)0.810s31.16x
🐘 PostgresNitro20.252s (~)21.062s (~)0.810s21.92x
🐘 PostgresExpress20.368s (+13.9% 🔺)21.056s (+13.5% 🔺)0.688s21.93x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)15.943s (-1.2%)16.835s (-2.1%)0.892s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro26.759s (~)27.047s (~)0.288s31.00x
🌐 RedisNext.js (Turbopack)26.847s (~)27.050s (~)0.203s31.00x
💻 LocalNext.js (Turbopack)27.221s (~)28.053s (~)0.832s31.02x
💻 LocalExpress27.502s (~)28.051s (~)0.549s31.03x
🌐 MongoDBNext.js (Turbopack)30.465s (~)31.038s (~)0.573s21.14x
🐘 PostgresExpress50.386s (+31.9% 🔺)51.129s (+30.8% 🔺)0.743s21.88x
🐘 PostgresNitro50.532s (~)51.128s (~)0.597s21.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)42.878s (+10.0% 🔺)43.689s (+7.4% 🔺)0.811s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.206s (~)55.095s (~)0.888s21.00x
💻 LocalNitro55.559s (~)56.099s (~)0.539s21.02x
💻 LocalNext.js (Turbopack)56.587s (~)57.114s (~)0.527s21.04x
💻 LocalExpress57.264s (~)58.103s (~)0.838s21.06x
🌐 MongoDBNext.js (Turbopack)61.036s (~)61.072s (~)0.035s21.13x
🐘 PostgresExpress78.183s (+3.8%)78.675s (+3.3%)0.492s21.44x
🐘 PostgresNitro100.149s (~)100.224s (~)0.075s11.85x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)81.685s (-1.1%)82.625s (-1.8%)0.940s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.245s (+1.5%)2.006s (~)0.761s151.00x
💻 LocalNitro1.352s (~)2.005s (~)0.653s151.09x
💻 LocalNext.js (Turbopack)1.413s (+0.6%)2.005s (~)0.592s151.13x
💻 LocalExpress1.415s (+1.0%)2.006s (~)0.590s151.14x
🐘 PostgresExpress1.927s (+3.7%)2.515s (+25.0% 🔺)0.588s121.55x
🌐 MongoDBNext.js (Turbopack)2.118s (-1.8%)3.008s (~)0.890s101.70x
🐘 PostgresNitro2.253s (+1.4%)3.013s (~)0.760s101.81x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.246s (-7.6% 🟢)3.144s (-11.0% 🟢)0.898s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.280s (+1.8%)3.006s (~)0.727s101.00x
💻 LocalNext.js (Turbopack)2.431s (-2.2%)2.918s (-3.0%)0.487s111.07x
🌐 RedisNext.js (Turbopack)2.466s (-1.2%)3.007s (~)0.542s101.08x
💻 LocalExpress2.648s (+7.7% 🔺)3.008s (~)0.359s101.16x
🌐 MongoDBNext.js (Turbopack)4.696s (-0.6%)5.176s (~)0.479s62.06x
🐘 PostgresExpress8.660s (-13.7% 🟢)9.537s (-8.0% 🟢)0.877s43.80x
🐘 PostgresNitro9.498s (+33.6% 🔺)10.057s (+33.5% 🔺)0.559s34.17x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.689s (-36.9% 🟢)3.374s (-38.7% 🟢)0.685s91.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.042s (-0.8%)4.725s (-3.0%)0.682s71.00x
💻 LocalNitro6.167s (+2.8%)7.016s (+9.4% 🔺)0.850s51.53x
💻 LocalNext.js (Turbopack)6.445s (-5.7% 🟢)7.016s (-5.4% 🟢)0.571s51.59x
💻 LocalExpress7.857s (+16.2% 🔺)8.272s (+17.9% 🔺)0.414s41.94x
🌐 MongoDBNext.js (Turbopack)9.925s (+1.2%)10.348s (~)0.423s32.46x
🐘 PostgresNitro46.235s (-8.3% 🟢)47.132s (-7.8% 🟢)0.897s111.44x
🐘 PostgresExpress47.330s (-4.8%)48.095s (-4.1%)0.765s111.71x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.494s (-20.5% 🟢)4.448s (-19.5% 🟢)0.953s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.263s (+2.0%)2.006s (~)0.742s151.00x
💻 LocalNitro1.374s (~)2.004s (~)0.631s151.09x
💻 LocalNext.js (Turbopack)1.432s (~)2.005s (~)0.573s151.13x
💻 LocalExpress1.449s (+1.5%)2.006s (~)0.557s151.15x
🐘 PostgresExpress1.837s (-9.5% 🟢)2.011s (-16.1% 🟢)0.174s151.45x
🐘 PostgresNitro2.095s (-5.5% 🟢)2.679s (+3.2%)0.585s121.66x
🌐 MongoDBNext.js (Turbopack)2.127s (-0.6%)3.008s (~)0.881s101.68x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.212s (+0.9%)3.124s (-13.6% 🟢)0.912s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.353s (~)3.007s (~)0.654s101.00x
🌐 RedisNext.js (Turbopack)2.488s (~)3.008s (~)0.520s101.06x
💻 LocalNext.js (Turbopack)2.606s (-3.3%)3.007s (~)0.401s101.11x
💻 LocalExpress2.754s (+6.7% 🔺)3.008s (~)0.255s101.17x
🌐 MongoDBNext.js (Turbopack)4.742s (~)5.177s (~)0.434s62.02x
🐘 PostgresExpress9.968s (-16.0% 🟢)10.373s (-13.8% 🟢)0.405s34.24x
🐘 PostgresNitro10.288s (-7.0% 🟢)11.029s (-5.7% 🟢)0.742s34.37x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.275s (-11.9% 🟢)3.197s (-12.8% 🟢)0.922s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.020s (-0.6%)4.725s (~)0.705s71.00x
💻 LocalNext.js (Turbopack)6.608s (-5.0%)7.216s (-4.0%)0.608s51.64x
💻 LocalNitro6.888s (~)7.015s (-2.8%)0.127s51.71x
💻 LocalExpress8.103s (+12.5% 🔺)8.775s (+9.5% 🔺)0.672s42.02x
🌐 MongoDBNext.js (Turbopack)10.056s (+1.1%)10.348s (-3.1%)0.292s32.50x
🐘 PostgresNitro46.096s (-8.6% 🟢)47.125s (-7.8% 🟢)1.029s111.47x
🐘 PostgresExpress48.860s (-1.0%)49.106s (-2.0%)0.246s112.16x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.233s (-4.2%)4.059s (-22.3% 🟢)0.826s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.114s (+3.2%)1.002s (~)0.009s (-3.2%)1.014s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)0.145s (-2.8%)1.000s (~)0.001s (~)1.007s (~)0.862s101.27x
💻 LocalNext.js (Turbopack)0.151s (+6.9% 🔺)1.001s (~)0.010s (-9.4% 🟢)1.015s (~)0.865s101.32x
💻 LocalExpress0.171s (-6.4% 🟢)1.003s (~)0.011s (+10.4% 🔺)1.016s (~)0.845s101.50x
🌐 MongoDBNext.js (Turbopack)0.510s (+2.5%)0.935s (-1.6%)0.001s (-6.7% 🟢)1.008s (~)0.498s104.46x
🐘 PostgresExpress1.317s (-9.1% 🟢)1.800s (+8.6% 🔺)0.001s (-15.4% 🟢)2.012s (~)0.695s1011.51x
🐘 PostgresNitro2.275s (+75.8% 🔺)2.768s (+58.7% 🔺)0.001s (-23.1% 🟢)3.014s (+49.7% 🔺)0.739s1019.89x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.481s (-26.7% 🟢)2.272s (-18.6% 🟢)0.121s (-6.9% 🟢)2.773s (-21.9% 🟢)1.292s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro11/12
🐘 PostgresExpress8/12
▲ VercelNext.js (Turbopack)12/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local12/12
Next.js (Turbopack)🌐 Redis7/12
Nitro💻 Local12/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

if (encryptionKeyOverride) {
encryptionKey = encryptionKeyOverride;
} else {
const rawKey = await world.getEncryptionKeyForRun?.(workflowRun);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For cross-deployment hook resumptions / fetching of the encryption key, we should consider enriching the "source" of the request for the key (i.e. resumeHook in this case) for the audit log to include the context.

Comment threadpackages/world-vercel/src/index.ts Outdated
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add AES-256-GCM encryption primitives and HKDF key derivation - #956

Merged
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption
Feb 20, 2026
Merged

Add AES-256-GCM encryption primitives and HKDF key derivation#956
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 6, 2026

Copy link
Copy Markdown
Member

Summary

Adds the encryption foundation for end-to-end encryption of workflow user data:

  • @workflow/core: Browser-compatible encrypt()/decrypt() using Web Crypto API (AES-256-GCM), importKey() for callers to convert raw bytes to CryptoKey once per run
  • @workflow/world: Overloaded getEncryptionKeyForRun interface — accepts WorkflowRun entity when available, or (runId, context) for start() when the run doesn't exist yet
  • @workflow/world-vercel: deriveRunKey() (HKDF-SHA256), fetchRunKey() (API call with projectId + runId), getEncryptionKeyForRun implementation reading context?.deploymentId
  • All runtime callers updated to call importKey() after key resolution and pass WorkflowRun entity where available
  • handleSuspension refactored to accept WorkflowRun parameter
  • resume-hook.ts uses getHookByTokenWithKey to fetch full WorkflowRun alongside hook + key
  • Separate changesets for @workflow/core, @workflow/world, @workflow/world-vercel

@vercel

vercelBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

CopilotAI review requested due to automatic review settings February 6, 2026 02:30
@changeset-bot

changeset-botBot commented Feb 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c28baee

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelPatch
@workflow/worldPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/web-sharedPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production523049572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2486452862817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4705
✅ example4705
✅ express4705
✅ fastify4705
✅ hono4705
✅ nextjs-turbopack5002
✅ nextjs-webpack5002
✅ nitro4705
✅ nuxt4705
✅ sveltekit4705
✅ vite4705
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds AES-256-GCM encryption with HKDF-SHA256 key derivation to the @workflow/world-vercel package. The implementation provides per-run encryption isolation by deriving unique keys from a deployment key, project ID, and run ID. It integrates seamlessly with the async serialization infrastructure added in PR #955 and uses the client-generated run IDs from PR #954.

Changes:

  • Implements createEncryptor() and createEncryptorFromEnv() functions with full Encryptor interface support
  • Adds AES-256-GCM encryption with random nonces and 128-bit authentication tags
  • Uses HKDF-SHA256 for per-run key derivation with projectId and runId as context
  • Wires encryption into createVercelWorld() via environment variables (VERCEL_DEPLOYMENT_KEY, VERCEL_PROJECT_ID)
  • Includes 18 comprehensive tests covering round-trip, format validation, isolation, and tamper detection

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.

FileDescription
packages/world-vercel/src/encryption.tsCore encryption implementation with key derivation, encrypt/decrypt, and key material access
packages/world-vercel/src/encryption.test.tsComprehensive test suite with 18 tests covering functionality and security properties
packages/world-vercel/src/index.tsIntegration into World creation and public API exports
Comments suppressed due to low confidence (1)

packages/world-vercel/src/index.ts:12

  • The VercelEncryptionConfig interface is exported from encryption.ts but not re-exported from index.ts. Users who want to use createEncryptor() directly would need to import from the internal encryption module, which is not a typical pattern.

Consider adding to index.ts:

export type { VercelEncryptionConfig } from './encryption.js';

This follows the pattern already established with APIConfig and makes the public API more discoverable.

export { createEncryptor, createEncryptorFromEnv } from './encryption.js';
export { createQueue } from './queue.js';
export { createStorage } from './storage.js';
export { createStreamer } from './streamer.js';
export type { APIConfig } from './utils.js';

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #956 - Add Vercel encryption implementation (AES-256-GCM with HKDF)

Summary: Implements AES-256-GCM encryption with HKDF-SHA256 per-run key derivation in @workflow/world-vercel. This is the core crypto PR and the most security-sensitive in the stack.

Crypto Design Review

Algorithm choices are sound:

  • AES-256-GCM: authenticated encryption, industry standard, hardware-accelerated
  • HKDF-SHA256 for key derivation: correct use of KDF for deriving per-run keys from a deployment key
  • 12-byte (96-bit) nonce: standard for GCM
  • 128-bit auth tag: standard for GCM
  • Random nonce per encryption: correct, avoids nonce reuse

Key derivation design:

  • HKDF(key=deploymentKey, salt=zeros, info="${projectId}|${runId}")
  • Zero salt is acceptable here since the deployment key is already random key material (HKDF specification allows this)
  • Using projectId|runId as info provides good domain separation between runs and projects

Concerns

  1. Zero salt vs random salt: While zero salt is acceptable per RFC 5869 when the input key material is already uniformly random, using a random salt stored alongside the ciphertext would provide additional protection against certain attacks (e.g., if the deployment key has less entropy than expected). This is a minor concern given that VERCEL_DEPLOYMENT_KEY should be high-entropy, but worth considering for defense in depth.

  2. Key derivation per encryption call: The deriveKey function is called on every encrypt() and decrypt() call. For workflows with many steps, this means the same HKDF derivation is repeated for each serialization. Consider caching the derived key per (projectId, runId) pair:

    // Cache derived keys to avoid re-deriving per encrypt/decrypt callconstkeyCache=newMap<string,CryptoKey>();

    The cache key would be ${projectId}|${runId}. This is a performance optimization, not a correctness issue.

  3. getKeyMaterial returns raw key bytes: The getKeyMaterial() method returns the raw deploymentKey bytes. This is documented as being for external decryption (o11y tooling), but it means any code with access to the encryptor can extract the master key. Consider:

    • Adding a warning comment about this being sensitive
    • Possibly scoping the returned key material to the derived per-run key rather than the deployment key
  4. createEncryptorFromEnv validation: When VERCEL_DEPLOYMENT_KEY is set but decodes to fewer than 32 bytes, createEncryptor will throw. However, createEncryptorFromEnv doesn't catch this, meaning the world creation will fail hard. Consider catching and logging a warning instead, falling back to no encryption, so a misconfigured key doesn't crash the entire application.

  5. Format prefix encr is 4 bytes of overhead per encrypted payload. This is fine for data payloads but for stream chunks (which may be small), it adds overhead per chunk. Not a major concern but worth noting.

Test Coverage

The 18 tests are comprehensive:

  • Round-trip tests (basic, empty, large, all byte values)
  • Format validation (prefix, structure, nonce randomness)
  • Per-run key isolation
  • Tamper detection
  • Cross-project/cross-key isolation
  • Environment variable handling (missing vars, valid vars)

One test case that could be added: concurrent encryption -- verifying that multiple concurrent encrypt() calls with the same encryptor produce correct results (nonce uniqueness under concurrency).

Minor

  • The Buffer.from(deploymentKeyBase64, 'base64') in createEncryptorFromEnv will silently ignore invalid base64 characters. Consider validating the decoded length matches expectations, or at minimum let createEncryptor's 32-byte check catch it.

Overall, the crypto implementation is solid and follows best practices. The concerns above are mostly optimizations and hardening suggestions rather than security issues.

Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the crypto design is solid — AES-256-GCM with HKDF-SHA256 per-run key derivation is the right approach, the separation between browser-compatible core encryption and Node.js-specific key management is clean, and the World interface integration is well thought out.

A few concerns below, mostly around performance during replay and a potentially unreachable code path.

Comment threadpackages/core/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts
@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.026s (-1.2%)1.004s (~)0.979s101.00x
💻 LocalExpress0.032s (-1.8%)1.005s (~)0.973s101.25x
💻 LocalNext.js (Turbopack)0.040s (-9.9% 🟢)1.005s (~)0.965s101.56x
🌐 RedisNext.js (Turbopack)0.046s (-2.1%)1.005s (~)0.958s101.81x
🌐 MongoDBNext.js (Turbopack)0.078s (-15.4% 🟢)1.007s (~)0.929s103.04x
🐘 PostgresExpress0.127s (-0.8%)1.010s (~)0.883s104.94x
🐘 PostgresNitro0.377s (+317.5% 🔺)1.009s (~)0.632s1014.67x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.612s (+25.2% 🔺)1.929s (-4.1%)1.318s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (~)2.005s (~)0.931s101.00x
🌐 RedisNext.js (Turbopack)1.103s (~)2.006s (~)0.903s101.03x
💻 LocalNext.js (Turbopack)1.105s (+0.7%)2.005s (~)0.900s101.03x
💻 LocalExpress1.105s (-0.5%)2.006s (~)0.900s101.03x
🌐 MongoDBNext.js (Turbopack)1.315s (~)2.007s (~)0.692s101.22x
🐘 PostgresNitro2.403s (-2.6%)3.014s (~)0.611s102.24x
🐘 PostgresExpress2.404s (~)3.013s (~)0.609s102.24x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.087s (+4.6%)3.098s (-5.3% 🟢)1.011s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.543s (~)11.021s (~)0.478s31.00x
🌐 RedisNext.js (Turbopack)10.698s (~)11.021s (~)0.323s31.01x
💻 LocalNext.js (Turbopack)10.749s (~)11.023s (~)0.274s31.02x
💻 LocalExpress10.837s (~)11.022s (~)0.186s31.03x
🌐 MongoDBNext.js (Turbopack)12.209s (-0.7%)13.020s (~)0.810s31.16x
🐘 PostgresNitro20.252s (~)21.062s (~)0.810s21.92x
🐘 PostgresExpress20.368s (+13.9% 🔺)21.056s (+13.5% 🔺)0.688s21.93x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)15.943s (-1.2%)16.835s (-2.1%)0.892s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro26.759s (~)27.047s (~)0.288s31.00x
🌐 RedisNext.js (Turbopack)26.847s (~)27.050s (~)0.203s31.00x
💻 LocalNext.js (Turbopack)27.221s (~)28.053s (~)0.832s31.02x
💻 LocalExpress27.502s (~)28.051s (~)0.549s31.03x
🌐 MongoDBNext.js (Turbopack)30.465s (~)31.038s (~)0.573s21.14x
🐘 PostgresExpress50.386s (+31.9% 🔺)51.129s (+30.8% 🔺)0.743s21.88x
🐘 PostgresNitro50.532s (~)51.128s (~)0.597s21.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)42.878s (+10.0% 🔺)43.689s (+7.4% 🔺)0.811s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.206s (~)55.095s (~)0.888s21.00x
💻 LocalNitro55.559s (~)56.099s (~)0.539s21.02x
💻 LocalNext.js (Turbopack)56.587s (~)57.114s (~)0.527s21.04x
💻 LocalExpress57.264s (~)58.103s (~)0.838s21.06x
🌐 MongoDBNext.js (Turbopack)61.036s (~)61.072s (~)0.035s21.13x
🐘 PostgresExpress78.183s (+3.8%)78.675s (+3.3%)0.492s21.44x
🐘 PostgresNitro100.149s (~)100.224s (~)0.075s11.85x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)81.685s (-1.1%)82.625s (-1.8%)0.940s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.245s (+1.5%)2.006s (~)0.761s151.00x
💻 LocalNitro1.352s (~)2.005s (~)0.653s151.09x
💻 LocalNext.js (Turbopack)1.413s (+0.6%)2.005s (~)0.592s151.13x
💻 LocalExpress1.415s (+1.0%)2.006s (~)0.590s151.14x
🐘 PostgresExpress1.927s (+3.7%)2.515s (+25.0% 🔺)0.588s121.55x
🌐 MongoDBNext.js (Turbopack)2.118s (-1.8%)3.008s (~)0.890s101.70x
🐘 PostgresNitro2.253s (+1.4%)3.013s (~)0.760s101.81x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.246s (-7.6% 🟢)3.144s (-11.0% 🟢)0.898s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.280s (+1.8%)3.006s (~)0.727s101.00x
💻 LocalNext.js (Turbopack)2.431s (-2.2%)2.918s (-3.0%)0.487s111.07x
🌐 RedisNext.js (Turbopack)2.466s (-1.2%)3.007s (~)0.542s101.08x
💻 LocalExpress2.648s (+7.7% 🔺)3.008s (~)0.359s101.16x
🌐 MongoDBNext.js (Turbopack)4.696s (-0.6%)5.176s (~)0.479s62.06x
🐘 PostgresExpress8.660s (-13.7% 🟢)9.537s (-8.0% 🟢)0.877s43.80x
🐘 PostgresNitro9.498s (+33.6% 🔺)10.057s (+33.5% 🔺)0.559s34.17x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.689s (-36.9% 🟢)3.374s (-38.7% 🟢)0.685s91.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.042s (-0.8%)4.725s (-3.0%)0.682s71.00x
💻 LocalNitro6.167s (+2.8%)7.016s (+9.4% 🔺)0.850s51.53x
💻 LocalNext.js (Turbopack)6.445s (-5.7% 🟢)7.016s (-5.4% 🟢)0.571s51.59x
💻 LocalExpress7.857s (+16.2% 🔺)8.272s (+17.9% 🔺)0.414s41.94x
🌐 MongoDBNext.js (Turbopack)9.925s (+1.2%)10.348s (~)0.423s32.46x
🐘 PostgresNitro46.235s (-8.3% 🟢)47.132s (-7.8% 🟢)0.897s111.44x
🐘 PostgresExpress47.330s (-4.8%)48.095s (-4.1%)0.765s111.71x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.494s (-20.5% 🟢)4.448s (-19.5% 🟢)0.953s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.263s (+2.0%)2.006s (~)0.742s151.00x
💻 LocalNitro1.374s (~)2.004s (~)0.631s151.09x
💻 LocalNext.js (Turbopack)1.432s (~)2.005s (~)0.573s151.13x
💻 LocalExpress1.449s (+1.5%)2.006s (~)0.557s151.15x
🐘 PostgresExpress1.837s (-9.5% 🟢)2.011s (-16.1% 🟢)0.174s151.45x
🐘 PostgresNitro2.095s (-5.5% 🟢)2.679s (+3.2%)0.585s121.66x
🌐 MongoDBNext.js (Turbopack)2.127s (-0.6%)3.008s (~)0.881s101.68x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.212s (+0.9%)3.124s (-13.6% 🟢)0.912s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.353s (~)3.007s (~)0.654s101.00x
🌐 RedisNext.js (Turbopack)2.488s (~)3.008s (~)0.520s101.06x
💻 LocalNext.js (Turbopack)2.606s (-3.3%)3.007s (~)0.401s101.11x
💻 LocalExpress2.754s (+6.7% 🔺)3.008s (~)0.255s101.17x
🌐 MongoDBNext.js (Turbopack)4.742s (~)5.177s (~)0.434s62.02x
🐘 PostgresExpress9.968s (-16.0% 🟢)10.373s (-13.8% 🟢)0.405s34.24x
🐘 PostgresNitro10.288s (-7.0% 🟢)11.029s (-5.7% 🟢)0.742s34.37x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.275s (-11.9% 🟢)3.197s (-12.8% 🟢)0.922s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.020s (-0.6%)4.725s (~)0.705s71.00x
💻 LocalNext.js (Turbopack)6.608s (-5.0%)7.216s (-4.0%)0.608s51.64x
💻 LocalNitro6.888s (~)7.015s (-2.8%)0.127s51.71x
💻 LocalExpress8.103s (+12.5% 🔺)8.775s (+9.5% 🔺)0.672s42.02x
🌐 MongoDBNext.js (Turbopack)10.056s (+1.1%)10.348s (-3.1%)0.292s32.50x
🐘 PostgresNitro46.096s (-8.6% 🟢)47.125s (-7.8% 🟢)1.029s111.47x
🐘 PostgresExpress48.860s (-1.0%)49.106s (-2.0%)0.246s112.16x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.233s (-4.2%)4.059s (-22.3% 🟢)0.826s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.114s (+3.2%)1.002s (~)0.009s (-3.2%)1.014s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)0.145s (-2.8%)1.000s (~)0.001s (~)1.007s (~)0.862s101.27x
💻 LocalNext.js (Turbopack)0.151s (+6.9% 🔺)1.001s (~)0.010s (-9.4% 🟢)1.015s (~)0.865s101.32x
💻 LocalExpress0.171s (-6.4% 🟢)1.003s (~)0.011s (+10.4% 🔺)1.016s (~)0.845s101.50x
🌐 MongoDBNext.js (Turbopack)0.510s (+2.5%)0.935s (-1.6%)0.001s (-6.7% 🟢)1.008s (~)0.498s104.46x
🐘 PostgresExpress1.317s (-9.1% 🟢)1.800s (+8.6% 🔺)0.001s (-15.4% 🟢)2.012s (~)0.695s1011.51x
🐘 PostgresNitro2.275s (+75.8% 🔺)2.768s (+58.7% 🔺)0.001s (-23.1% 🟢)3.014s (+49.7% 🔺)0.739s1019.89x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.481s (-26.7% 🟢)2.272s (-18.6% 🟢)0.121s (-6.9% 🟢)2.773s (-21.9% 🟢)1.292s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro11/12
🐘 PostgresExpress8/12
▲ VercelNext.js (Turbopack)12/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local12/12
Next.js (Turbopack)🌐 Redis7/12
Nitro💻 Local12/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

if (encryptionKeyOverride) {
encryptionKey = encryptionKeyOverride;
} else {
const rawKey = await world.getEncryptionKeyForRun?.(workflowRun);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For cross-deployment hook resumptions / fetching of the encryption key, we should consider enriching the "source" of the request for the key (i.e. resumeHook in this case) for the audit log to include the context.

Comment threadpackages/world-vercel/src/index.ts Outdated
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add AES-256-GCM encryption primitives and HKDF key derivation - #956

Merged
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption
Feb 20, 2026
Merged

Add AES-256-GCM encryption primitives and HKDF key derivation#956
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 6, 2026

Copy link
Copy Markdown
Member

Summary

Adds the encryption foundation for end-to-end encryption of workflow user data:

  • @workflow/core: Browser-compatible encrypt()/decrypt() using Web Crypto API (AES-256-GCM), importKey() for callers to convert raw bytes to CryptoKey once per run
  • @workflow/world: Overloaded getEncryptionKeyForRun interface — accepts WorkflowRun entity when available, or (runId, context) for start() when the run doesn't exist yet
  • @workflow/world-vercel: deriveRunKey() (HKDF-SHA256), fetchRunKey() (API call with projectId + runId), getEncryptionKeyForRun implementation reading context?.deploymentId
  • All runtime callers updated to call importKey() after key resolution and pass WorkflowRun entity where available
  • handleSuspension refactored to accept WorkflowRun parameter
  • resume-hook.ts uses getHookByTokenWithKey to fetch full WorkflowRun alongside hook + key
  • Separate changesets for @workflow/core, @workflow/world, @workflow/world-vercel

@vercel

vercelBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

CopilotAI review requested due to automatic review settings February 6, 2026 02:30
@changeset-bot

changeset-botBot commented Feb 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c28baee

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelPatch
@workflow/worldPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/web-sharedPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production523049572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2486452862817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4705
✅ example4705
✅ express4705
✅ fastify4705
✅ hono4705
✅ nextjs-turbopack5002
✅ nextjs-webpack5002
✅ nitro4705
✅ nuxt4705
✅ sveltekit4705
✅ vite4705
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds AES-256-GCM encryption with HKDF-SHA256 key derivation to the @workflow/world-vercel package. The implementation provides per-run encryption isolation by deriving unique keys from a deployment key, project ID, and run ID. It integrates seamlessly with the async serialization infrastructure added in PR #955 and uses the client-generated run IDs from PR #954.

Changes:

  • Implements createEncryptor() and createEncryptorFromEnv() functions with full Encryptor interface support
  • Adds AES-256-GCM encryption with random nonces and 128-bit authentication tags
  • Uses HKDF-SHA256 for per-run key derivation with projectId and runId as context
  • Wires encryption into createVercelWorld() via environment variables (VERCEL_DEPLOYMENT_KEY, VERCEL_PROJECT_ID)
  • Includes 18 comprehensive tests covering round-trip, format validation, isolation, and tamper detection

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.

FileDescription
packages/world-vercel/src/encryption.tsCore encryption implementation with key derivation, encrypt/decrypt, and key material access
packages/world-vercel/src/encryption.test.tsComprehensive test suite with 18 tests covering functionality and security properties
packages/world-vercel/src/index.tsIntegration into World creation and public API exports
Comments suppressed due to low confidence (1)

packages/world-vercel/src/index.ts:12

  • The VercelEncryptionConfig interface is exported from encryption.ts but not re-exported from index.ts. Users who want to use createEncryptor() directly would need to import from the internal encryption module, which is not a typical pattern.

Consider adding to index.ts:

export type { VercelEncryptionConfig } from './encryption.js';

This follows the pattern already established with APIConfig and makes the public API more discoverable.

export { createEncryptor, createEncryptorFromEnv } from './encryption.js';
export { createQueue } from './queue.js';
export { createStorage } from './storage.js';
export { createStreamer } from './streamer.js';
export type { APIConfig } from './utils.js';

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #956 - Add Vercel encryption implementation (AES-256-GCM with HKDF)

Summary: Implements AES-256-GCM encryption with HKDF-SHA256 per-run key derivation in @workflow/world-vercel. This is the core crypto PR and the most security-sensitive in the stack.

Crypto Design Review

Algorithm choices are sound:

  • AES-256-GCM: authenticated encryption, industry standard, hardware-accelerated
  • HKDF-SHA256 for key derivation: correct use of KDF for deriving per-run keys from a deployment key
  • 12-byte (96-bit) nonce: standard for GCM
  • 128-bit auth tag: standard for GCM
  • Random nonce per encryption: correct, avoids nonce reuse

Key derivation design:

  • HKDF(key=deploymentKey, salt=zeros, info="${projectId}|${runId}")
  • Zero salt is acceptable here since the deployment key is already random key material (HKDF specification allows this)
  • Using projectId|runId as info provides good domain separation between runs and projects

Concerns

  1. Zero salt vs random salt: While zero salt is acceptable per RFC 5869 when the input key material is already uniformly random, using a random salt stored alongside the ciphertext would provide additional protection against certain attacks (e.g., if the deployment key has less entropy than expected). This is a minor concern given that VERCEL_DEPLOYMENT_KEY should be high-entropy, but worth considering for defense in depth.

  2. Key derivation per encryption call: The deriveKey function is called on every encrypt() and decrypt() call. For workflows with many steps, this means the same HKDF derivation is repeated for each serialization. Consider caching the derived key per (projectId, runId) pair:

    // Cache derived keys to avoid re-deriving per encrypt/decrypt callconstkeyCache=newMap<string,CryptoKey>();

    The cache key would be ${projectId}|${runId}. This is a performance optimization, not a correctness issue.

  3. getKeyMaterial returns raw key bytes: The getKeyMaterial() method returns the raw deploymentKey bytes. This is documented as being for external decryption (o11y tooling), but it means any code with access to the encryptor can extract the master key. Consider:

    • Adding a warning comment about this being sensitive
    • Possibly scoping the returned key material to the derived per-run key rather than the deployment key
  4. createEncryptorFromEnv validation: When VERCEL_DEPLOYMENT_KEY is set but decodes to fewer than 32 bytes, createEncryptor will throw. However, createEncryptorFromEnv doesn't catch this, meaning the world creation will fail hard. Consider catching and logging a warning instead, falling back to no encryption, so a misconfigured key doesn't crash the entire application.

  5. Format prefix encr is 4 bytes of overhead per encrypted payload. This is fine for data payloads but for stream chunks (which may be small), it adds overhead per chunk. Not a major concern but worth noting.

Test Coverage

The 18 tests are comprehensive:

  • Round-trip tests (basic, empty, large, all byte values)
  • Format validation (prefix, structure, nonce randomness)
  • Per-run key isolation
  • Tamper detection
  • Cross-project/cross-key isolation
  • Environment variable handling (missing vars, valid vars)

One test case that could be added: concurrent encryption -- verifying that multiple concurrent encrypt() calls with the same encryptor produce correct results (nonce uniqueness under concurrency).

Minor

  • The Buffer.from(deploymentKeyBase64, 'base64') in createEncryptorFromEnv will silently ignore invalid base64 characters. Consider validating the decoded length matches expectations, or at minimum let createEncryptor's 32-byte check catch it.

Overall, the crypto implementation is solid and follows best practices. The concerns above are mostly optimizations and hardening suggestions rather than security issues.

Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the crypto design is solid — AES-256-GCM with HKDF-SHA256 per-run key derivation is the right approach, the separation between browser-compatible core encryption and Node.js-specific key management is clean, and the World interface integration is well thought out.

A few concerns below, mostly around performance during replay and a potentially unreachable code path.

Comment threadpackages/core/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts
@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.026s (-1.2%)1.004s (~)0.979s101.00x
💻 LocalExpress0.032s (-1.8%)1.005s (~)0.973s101.25x
💻 LocalNext.js (Turbopack)0.040s (-9.9% 🟢)1.005s (~)0.965s101.56x
🌐 RedisNext.js (Turbopack)0.046s (-2.1%)1.005s (~)0.958s101.81x
🌐 MongoDBNext.js (Turbopack)0.078s (-15.4% 🟢)1.007s (~)0.929s103.04x
🐘 PostgresExpress0.127s (-0.8%)1.010s (~)0.883s104.94x
🐘 PostgresNitro0.377s (+317.5% 🔺)1.009s (~)0.632s1014.67x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.612s (+25.2% 🔺)1.929s (-4.1%)1.318s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (~)2.005s (~)0.931s101.00x
🌐 RedisNext.js (Turbopack)1.103s (~)2.006s (~)0.903s101.03x
💻 LocalNext.js (Turbopack)1.105s (+0.7%)2.005s (~)0.900s101.03x
💻 LocalExpress1.105s (-0.5%)2.006s (~)0.900s101.03x
🌐 MongoDBNext.js (Turbopack)1.315s (~)2.007s (~)0.692s101.22x
🐘 PostgresNitro2.403s (-2.6%)3.014s (~)0.611s102.24x
🐘 PostgresExpress2.404s (~)3.013s (~)0.609s102.24x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.087s (+4.6%)3.098s (-5.3% 🟢)1.011s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.543s (~)11.021s (~)0.478s31.00x
🌐 RedisNext.js (Turbopack)10.698s (~)11.021s (~)0.323s31.01x
💻 LocalNext.js (Turbopack)10.749s (~)11.023s (~)0.274s31.02x
💻 LocalExpress10.837s (~)11.022s (~)0.186s31.03x
🌐 MongoDBNext.js (Turbopack)12.209s (-0.7%)13.020s (~)0.810s31.16x
🐘 PostgresNitro20.252s (~)21.062s (~)0.810s21.92x
🐘 PostgresExpress20.368s (+13.9% 🔺)21.056s (+13.5% 🔺)0.688s21.93x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)15.943s (-1.2%)16.835s (-2.1%)0.892s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro26.759s (~)27.047s (~)0.288s31.00x
🌐 RedisNext.js (Turbopack)26.847s (~)27.050s (~)0.203s31.00x
💻 LocalNext.js (Turbopack)27.221s (~)28.053s (~)0.832s31.02x
💻 LocalExpress27.502s (~)28.051s (~)0.549s31.03x
🌐 MongoDBNext.js (Turbopack)30.465s (~)31.038s (~)0.573s21.14x
🐘 PostgresExpress50.386s (+31.9% 🔺)51.129s (+30.8% 🔺)0.743s21.88x
🐘 PostgresNitro50.532s (~)51.128s (~)0.597s21.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)42.878s (+10.0% 🔺)43.689s (+7.4% 🔺)0.811s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.206s (~)55.095s (~)0.888s21.00x
💻 LocalNitro55.559s (~)56.099s (~)0.539s21.02x
💻 LocalNext.js (Turbopack)56.587s (~)57.114s (~)0.527s21.04x
💻 LocalExpress57.264s (~)58.103s (~)0.838s21.06x
🌐 MongoDBNext.js (Turbopack)61.036s (~)61.072s (~)0.035s21.13x
🐘 PostgresExpress78.183s (+3.8%)78.675s (+3.3%)0.492s21.44x
🐘 PostgresNitro100.149s (~)100.224s (~)0.075s11.85x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)81.685s (-1.1%)82.625s (-1.8%)0.940s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.245s (+1.5%)2.006s (~)0.761s151.00x
💻 LocalNitro1.352s (~)2.005s (~)0.653s151.09x
💻 LocalNext.js (Turbopack)1.413s (+0.6%)2.005s (~)0.592s151.13x
💻 LocalExpress1.415s (+1.0%)2.006s (~)0.590s151.14x
🐘 PostgresExpress1.927s (+3.7%)2.515s (+25.0% 🔺)0.588s121.55x
🌐 MongoDBNext.js (Turbopack)2.118s (-1.8%)3.008s (~)0.890s101.70x
🐘 PostgresNitro2.253s (+1.4%)3.013s (~)0.760s101.81x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.246s (-7.6% 🟢)3.144s (-11.0% 🟢)0.898s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.280s (+1.8%)3.006s (~)0.727s101.00x
💻 LocalNext.js (Turbopack)2.431s (-2.2%)2.918s (-3.0%)0.487s111.07x
🌐 RedisNext.js (Turbopack)2.466s (-1.2%)3.007s (~)0.542s101.08x
💻 LocalExpress2.648s (+7.7% 🔺)3.008s (~)0.359s101.16x
🌐 MongoDBNext.js (Turbopack)4.696s (-0.6%)5.176s (~)0.479s62.06x
🐘 PostgresExpress8.660s (-13.7% 🟢)9.537s (-8.0% 🟢)0.877s43.80x
🐘 PostgresNitro9.498s (+33.6% 🔺)10.057s (+33.5% 🔺)0.559s34.17x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.689s (-36.9% 🟢)3.374s (-38.7% 🟢)0.685s91.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.042s (-0.8%)4.725s (-3.0%)0.682s71.00x
💻 LocalNitro6.167s (+2.8%)7.016s (+9.4% 🔺)0.850s51.53x
💻 LocalNext.js (Turbopack)6.445s (-5.7% 🟢)7.016s (-5.4% 🟢)0.571s51.59x
💻 LocalExpress7.857s (+16.2% 🔺)8.272s (+17.9% 🔺)0.414s41.94x
🌐 MongoDBNext.js (Turbopack)9.925s (+1.2%)10.348s (~)0.423s32.46x
🐘 PostgresNitro46.235s (-8.3% 🟢)47.132s (-7.8% 🟢)0.897s111.44x
🐘 PostgresExpress47.330s (-4.8%)48.095s (-4.1%)0.765s111.71x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.494s (-20.5% 🟢)4.448s (-19.5% 🟢)0.953s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.263s (+2.0%)2.006s (~)0.742s151.00x
💻 LocalNitro1.374s (~)2.004s (~)0.631s151.09x
💻 LocalNext.js (Turbopack)1.432s (~)2.005s (~)0.573s151.13x
💻 LocalExpress1.449s (+1.5%)2.006s (~)0.557s151.15x
🐘 PostgresExpress1.837s (-9.5% 🟢)2.011s (-16.1% 🟢)0.174s151.45x
🐘 PostgresNitro2.095s (-5.5% 🟢)2.679s (+3.2%)0.585s121.66x
🌐 MongoDBNext.js (Turbopack)2.127s (-0.6%)3.008s (~)0.881s101.68x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.212s (+0.9%)3.124s (-13.6% 🟢)0.912s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.353s (~)3.007s (~)0.654s101.00x
🌐 RedisNext.js (Turbopack)2.488s (~)3.008s (~)0.520s101.06x
💻 LocalNext.js (Turbopack)2.606s (-3.3%)3.007s (~)0.401s101.11x
💻 LocalExpress2.754s (+6.7% 🔺)3.008s (~)0.255s101.17x
🌐 MongoDBNext.js (Turbopack)4.742s (~)5.177s (~)0.434s62.02x
🐘 PostgresExpress9.968s (-16.0% 🟢)10.373s (-13.8% 🟢)0.405s34.24x
🐘 PostgresNitro10.288s (-7.0% 🟢)11.029s (-5.7% 🟢)0.742s34.37x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.275s (-11.9% 🟢)3.197s (-12.8% 🟢)0.922s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.020s (-0.6%)4.725s (~)0.705s71.00x
💻 LocalNext.js (Turbopack)6.608s (-5.0%)7.216s (-4.0%)0.608s51.64x
💻 LocalNitro6.888s (~)7.015s (-2.8%)0.127s51.71x
💻 LocalExpress8.103s (+12.5% 🔺)8.775s (+9.5% 🔺)0.672s42.02x
🌐 MongoDBNext.js (Turbopack)10.056s (+1.1%)10.348s (-3.1%)0.292s32.50x
🐘 PostgresNitro46.096s (-8.6% 🟢)47.125s (-7.8% 🟢)1.029s111.47x
🐘 PostgresExpress48.860s (-1.0%)49.106s (-2.0%)0.246s112.16x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.233s (-4.2%)4.059s (-22.3% 🟢)0.826s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.114s (+3.2%)1.002s (~)0.009s (-3.2%)1.014s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)0.145s (-2.8%)1.000s (~)0.001s (~)1.007s (~)0.862s101.27x
💻 LocalNext.js (Turbopack)0.151s (+6.9% 🔺)1.001s (~)0.010s (-9.4% 🟢)1.015s (~)0.865s101.32x
💻 LocalExpress0.171s (-6.4% 🟢)1.003s (~)0.011s (+10.4% 🔺)1.016s (~)0.845s101.50x
🌐 MongoDBNext.js (Turbopack)0.510s (+2.5%)0.935s (-1.6%)0.001s (-6.7% 🟢)1.008s (~)0.498s104.46x
🐘 PostgresExpress1.317s (-9.1% 🟢)1.800s (+8.6% 🔺)0.001s (-15.4% 🟢)2.012s (~)0.695s1011.51x
🐘 PostgresNitro2.275s (+75.8% 🔺)2.768s (+58.7% 🔺)0.001s (-23.1% 🟢)3.014s (+49.7% 🔺)0.739s1019.89x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.481s (-26.7% 🟢)2.272s (-18.6% 🟢)0.121s (-6.9% 🟢)2.773s (-21.9% 🟢)1.292s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro11/12
🐘 PostgresExpress8/12
▲ VercelNext.js (Turbopack)12/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local12/12
Next.js (Turbopack)🌐 Redis7/12
Nitro💻 Local12/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

if (encryptionKeyOverride) {
encryptionKey = encryptionKeyOverride;
} else {
const rawKey = await world.getEncryptionKeyForRun?.(workflowRun);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For cross-deployment hook resumptions / fetching of the encryption key, we should consider enriching the "source" of the request for the key (i.e. resumeHook in this case) for the audit log to include the context.

Comment threadpackages/world-vercel/src/index.ts Outdated
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add AES-256-GCM encryption primitives and HKDF key derivation - #956

Merged
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption
Feb 20, 2026
Merged

Add AES-256-GCM encryption primitives and HKDF key derivation#956
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 6, 2026

Copy link
Copy Markdown
Member

Summary

Adds the encryption foundation for end-to-end encryption of workflow user data:

  • @workflow/core: Browser-compatible encrypt()/decrypt() using Web Crypto API (AES-256-GCM), importKey() for callers to convert raw bytes to CryptoKey once per run
  • @workflow/world: Overloaded getEncryptionKeyForRun interface — accepts WorkflowRun entity when available, or (runId, context) for start() when the run doesn't exist yet
  • @workflow/world-vercel: deriveRunKey() (HKDF-SHA256), fetchRunKey() (API call with projectId + runId), getEncryptionKeyForRun implementation reading context?.deploymentId
  • All runtime callers updated to call importKey() after key resolution and pass WorkflowRun entity where available
  • handleSuspension refactored to accept WorkflowRun parameter
  • resume-hook.ts uses getHookByTokenWithKey to fetch full WorkflowRun alongside hook + key
  • Separate changesets for @workflow/core, @workflow/world, @workflow/world-vercel

@vercel

vercelBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

CopilotAI review requested due to automatic review settings February 6, 2026 02:30
@changeset-bot

changeset-botBot commented Feb 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c28baee

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelPatch
@workflow/worldPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/web-sharedPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production523049572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2486452862817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4705
✅ example4705
✅ express4705
✅ fastify4705
✅ hono4705
✅ nextjs-turbopack5002
✅ nextjs-webpack5002
✅ nitro4705
✅ nuxt4705
✅ sveltekit4705
✅ vite4705
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds AES-256-GCM encryption with HKDF-SHA256 key derivation to the @workflow/world-vercel package. The implementation provides per-run encryption isolation by deriving unique keys from a deployment key, project ID, and run ID. It integrates seamlessly with the async serialization infrastructure added in PR #955 and uses the client-generated run IDs from PR #954.

Changes:

  • Implements createEncryptor() and createEncryptorFromEnv() functions with full Encryptor interface support
  • Adds AES-256-GCM encryption with random nonces and 128-bit authentication tags
  • Uses HKDF-SHA256 for per-run key derivation with projectId and runId as context
  • Wires encryption into createVercelWorld() via environment variables (VERCEL_DEPLOYMENT_KEY, VERCEL_PROJECT_ID)
  • Includes 18 comprehensive tests covering round-trip, format validation, isolation, and tamper detection

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.

FileDescription
packages/world-vercel/src/encryption.tsCore encryption implementation with key derivation, encrypt/decrypt, and key material access
packages/world-vercel/src/encryption.test.tsComprehensive test suite with 18 tests covering functionality and security properties
packages/world-vercel/src/index.tsIntegration into World creation and public API exports
Comments suppressed due to low confidence (1)

packages/world-vercel/src/index.ts:12

  • The VercelEncryptionConfig interface is exported from encryption.ts but not re-exported from index.ts. Users who want to use createEncryptor() directly would need to import from the internal encryption module, which is not a typical pattern.

Consider adding to index.ts:

export type { VercelEncryptionConfig } from './encryption.js';

This follows the pattern already established with APIConfig and makes the public API more discoverable.

export { createEncryptor, createEncryptorFromEnv } from './encryption.js';
export { createQueue } from './queue.js';
export { createStorage } from './storage.js';
export { createStreamer } from './streamer.js';
export type { APIConfig } from './utils.js';

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #956 - Add Vercel encryption implementation (AES-256-GCM with HKDF)

Summary: Implements AES-256-GCM encryption with HKDF-SHA256 per-run key derivation in @workflow/world-vercel. This is the core crypto PR and the most security-sensitive in the stack.

Crypto Design Review

Algorithm choices are sound:

  • AES-256-GCM: authenticated encryption, industry standard, hardware-accelerated
  • HKDF-SHA256 for key derivation: correct use of KDF for deriving per-run keys from a deployment key
  • 12-byte (96-bit) nonce: standard for GCM
  • 128-bit auth tag: standard for GCM
  • Random nonce per encryption: correct, avoids nonce reuse

Key derivation design:

  • HKDF(key=deploymentKey, salt=zeros, info="${projectId}|${runId}")
  • Zero salt is acceptable here since the deployment key is already random key material (HKDF specification allows this)
  • Using projectId|runId as info provides good domain separation between runs and projects

Concerns

  1. Zero salt vs random salt: While zero salt is acceptable per RFC 5869 when the input key material is already uniformly random, using a random salt stored alongside the ciphertext would provide additional protection against certain attacks (e.g., if the deployment key has less entropy than expected). This is a minor concern given that VERCEL_DEPLOYMENT_KEY should be high-entropy, but worth considering for defense in depth.

  2. Key derivation per encryption call: The deriveKey function is called on every encrypt() and decrypt() call. For workflows with many steps, this means the same HKDF derivation is repeated for each serialization. Consider caching the derived key per (projectId, runId) pair:

    // Cache derived keys to avoid re-deriving per encrypt/decrypt callconstkeyCache=newMap<string,CryptoKey>();

    The cache key would be ${projectId}|${runId}. This is a performance optimization, not a correctness issue.

  3. getKeyMaterial returns raw key bytes: The getKeyMaterial() method returns the raw deploymentKey bytes. This is documented as being for external decryption (o11y tooling), but it means any code with access to the encryptor can extract the master key. Consider:

    • Adding a warning comment about this being sensitive
    • Possibly scoping the returned key material to the derived per-run key rather than the deployment key
  4. createEncryptorFromEnv validation: When VERCEL_DEPLOYMENT_KEY is set but decodes to fewer than 32 bytes, createEncryptor will throw. However, createEncryptorFromEnv doesn't catch this, meaning the world creation will fail hard. Consider catching and logging a warning instead, falling back to no encryption, so a misconfigured key doesn't crash the entire application.

  5. Format prefix encr is 4 bytes of overhead per encrypted payload. This is fine for data payloads but for stream chunks (which may be small), it adds overhead per chunk. Not a major concern but worth noting.

Test Coverage

The 18 tests are comprehensive:

  • Round-trip tests (basic, empty, large, all byte values)
  • Format validation (prefix, structure, nonce randomness)
  • Per-run key isolation
  • Tamper detection
  • Cross-project/cross-key isolation
  • Environment variable handling (missing vars, valid vars)

One test case that could be added: concurrent encryption -- verifying that multiple concurrent encrypt() calls with the same encryptor produce correct results (nonce uniqueness under concurrency).

Minor

  • The Buffer.from(deploymentKeyBase64, 'base64') in createEncryptorFromEnv will silently ignore invalid base64 characters. Consider validating the decoded length matches expectations, or at minimum let createEncryptor's 32-byte check catch it.

Overall, the crypto implementation is solid and follows best practices. The concerns above are mostly optimizations and hardening suggestions rather than security issues.

Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the crypto design is solid — AES-256-GCM with HKDF-SHA256 per-run key derivation is the right approach, the separation between browser-compatible core encryption and Node.js-specific key management is clean, and the World interface integration is well thought out.

A few concerns below, mostly around performance during replay and a potentially unreachable code path.

Comment threadpackages/core/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts
@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.026s (-1.2%)1.004s (~)0.979s101.00x
💻 LocalExpress0.032s (-1.8%)1.005s (~)0.973s101.25x
💻 LocalNext.js (Turbopack)0.040s (-9.9% 🟢)1.005s (~)0.965s101.56x
🌐 RedisNext.js (Turbopack)0.046s (-2.1%)1.005s (~)0.958s101.81x
🌐 MongoDBNext.js (Turbopack)0.078s (-15.4% 🟢)1.007s (~)0.929s103.04x
🐘 PostgresExpress0.127s (-0.8%)1.010s (~)0.883s104.94x
🐘 PostgresNitro0.377s (+317.5% 🔺)1.009s (~)0.632s1014.67x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.612s (+25.2% 🔺)1.929s (-4.1%)1.318s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (~)2.005s (~)0.931s101.00x
🌐 RedisNext.js (Turbopack)1.103s (~)2.006s (~)0.903s101.03x
💻 LocalNext.js (Turbopack)1.105s (+0.7%)2.005s (~)0.900s101.03x
💻 LocalExpress1.105s (-0.5%)2.006s (~)0.900s101.03x
🌐 MongoDBNext.js (Turbopack)1.315s (~)2.007s (~)0.692s101.22x
🐘 PostgresNitro2.403s (-2.6%)3.014s (~)0.611s102.24x
🐘 PostgresExpress2.404s (~)3.013s (~)0.609s102.24x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.087s (+4.6%)3.098s (-5.3% 🟢)1.011s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.543s (~)11.021s (~)0.478s31.00x
🌐 RedisNext.js (Turbopack)10.698s (~)11.021s (~)0.323s31.01x
💻 LocalNext.js (Turbopack)10.749s (~)11.023s (~)0.274s31.02x
💻 LocalExpress10.837s (~)11.022s (~)0.186s31.03x
🌐 MongoDBNext.js (Turbopack)12.209s (-0.7%)13.020s (~)0.810s31.16x
🐘 PostgresNitro20.252s (~)21.062s (~)0.810s21.92x
🐘 PostgresExpress20.368s (+13.9% 🔺)21.056s (+13.5% 🔺)0.688s21.93x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)15.943s (-1.2%)16.835s (-2.1%)0.892s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro26.759s (~)27.047s (~)0.288s31.00x
🌐 RedisNext.js (Turbopack)26.847s (~)27.050s (~)0.203s31.00x
💻 LocalNext.js (Turbopack)27.221s (~)28.053s (~)0.832s31.02x
💻 LocalExpress27.502s (~)28.051s (~)0.549s31.03x
🌐 MongoDBNext.js (Turbopack)30.465s (~)31.038s (~)0.573s21.14x
🐘 PostgresExpress50.386s (+31.9% 🔺)51.129s (+30.8% 🔺)0.743s21.88x
🐘 PostgresNitro50.532s (~)51.128s (~)0.597s21.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)42.878s (+10.0% 🔺)43.689s (+7.4% 🔺)0.811s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.206s (~)55.095s (~)0.888s21.00x
💻 LocalNitro55.559s (~)56.099s (~)0.539s21.02x
💻 LocalNext.js (Turbopack)56.587s (~)57.114s (~)0.527s21.04x
💻 LocalExpress57.264s (~)58.103s (~)0.838s21.06x
🌐 MongoDBNext.js (Turbopack)61.036s (~)61.072s (~)0.035s21.13x
🐘 PostgresExpress78.183s (+3.8%)78.675s (+3.3%)0.492s21.44x
🐘 PostgresNitro100.149s (~)100.224s (~)0.075s11.85x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)81.685s (-1.1%)82.625s (-1.8%)0.940s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.245s (+1.5%)2.006s (~)0.761s151.00x
💻 LocalNitro1.352s (~)2.005s (~)0.653s151.09x
💻 LocalNext.js (Turbopack)1.413s (+0.6%)2.005s (~)0.592s151.13x
💻 LocalExpress1.415s (+1.0%)2.006s (~)0.590s151.14x
🐘 PostgresExpress1.927s (+3.7%)2.515s (+25.0% 🔺)0.588s121.55x
🌐 MongoDBNext.js (Turbopack)2.118s (-1.8%)3.008s (~)0.890s101.70x
🐘 PostgresNitro2.253s (+1.4%)3.013s (~)0.760s101.81x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.246s (-7.6% 🟢)3.144s (-11.0% 🟢)0.898s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.280s (+1.8%)3.006s (~)0.727s101.00x
💻 LocalNext.js (Turbopack)2.431s (-2.2%)2.918s (-3.0%)0.487s111.07x
🌐 RedisNext.js (Turbopack)2.466s (-1.2%)3.007s (~)0.542s101.08x
💻 LocalExpress2.648s (+7.7% 🔺)3.008s (~)0.359s101.16x
🌐 MongoDBNext.js (Turbopack)4.696s (-0.6%)5.176s (~)0.479s62.06x
🐘 PostgresExpress8.660s (-13.7% 🟢)9.537s (-8.0% 🟢)0.877s43.80x
🐘 PostgresNitro9.498s (+33.6% 🔺)10.057s (+33.5% 🔺)0.559s34.17x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.689s (-36.9% 🟢)3.374s (-38.7% 🟢)0.685s91.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.042s (-0.8%)4.725s (-3.0%)0.682s71.00x
💻 LocalNitro6.167s (+2.8%)7.016s (+9.4% 🔺)0.850s51.53x
💻 LocalNext.js (Turbopack)6.445s (-5.7% 🟢)7.016s (-5.4% 🟢)0.571s51.59x
💻 LocalExpress7.857s (+16.2% 🔺)8.272s (+17.9% 🔺)0.414s41.94x
🌐 MongoDBNext.js (Turbopack)9.925s (+1.2%)10.348s (~)0.423s32.46x
🐘 PostgresNitro46.235s (-8.3% 🟢)47.132s (-7.8% 🟢)0.897s111.44x
🐘 PostgresExpress47.330s (-4.8%)48.095s (-4.1%)0.765s111.71x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.494s (-20.5% 🟢)4.448s (-19.5% 🟢)0.953s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.263s (+2.0%)2.006s (~)0.742s151.00x
💻 LocalNitro1.374s (~)2.004s (~)0.631s151.09x
💻 LocalNext.js (Turbopack)1.432s (~)2.005s (~)0.573s151.13x
💻 LocalExpress1.449s (+1.5%)2.006s (~)0.557s151.15x
🐘 PostgresExpress1.837s (-9.5% 🟢)2.011s (-16.1% 🟢)0.174s151.45x
🐘 PostgresNitro2.095s (-5.5% 🟢)2.679s (+3.2%)0.585s121.66x
🌐 MongoDBNext.js (Turbopack)2.127s (-0.6%)3.008s (~)0.881s101.68x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.212s (+0.9%)3.124s (-13.6% 🟢)0.912s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.353s (~)3.007s (~)0.654s101.00x
🌐 RedisNext.js (Turbopack)2.488s (~)3.008s (~)0.520s101.06x
💻 LocalNext.js (Turbopack)2.606s (-3.3%)3.007s (~)0.401s101.11x
💻 LocalExpress2.754s (+6.7% 🔺)3.008s (~)0.255s101.17x
🌐 MongoDBNext.js (Turbopack)4.742s (~)5.177s (~)0.434s62.02x
🐘 PostgresExpress9.968s (-16.0% 🟢)10.373s (-13.8% 🟢)0.405s34.24x
🐘 PostgresNitro10.288s (-7.0% 🟢)11.029s (-5.7% 🟢)0.742s34.37x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.275s (-11.9% 🟢)3.197s (-12.8% 🟢)0.922s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.020s (-0.6%)4.725s (~)0.705s71.00x
💻 LocalNext.js (Turbopack)6.608s (-5.0%)7.216s (-4.0%)0.608s51.64x
💻 LocalNitro6.888s (~)7.015s (-2.8%)0.127s51.71x
💻 LocalExpress8.103s (+12.5% 🔺)8.775s (+9.5% 🔺)0.672s42.02x
🌐 MongoDBNext.js (Turbopack)10.056s (+1.1%)10.348s (-3.1%)0.292s32.50x
🐘 PostgresNitro46.096s (-8.6% 🟢)47.125s (-7.8% 🟢)1.029s111.47x
🐘 PostgresExpress48.860s (-1.0%)49.106s (-2.0%)0.246s112.16x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.233s (-4.2%)4.059s (-22.3% 🟢)0.826s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.114s (+3.2%)1.002s (~)0.009s (-3.2%)1.014s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)0.145s (-2.8%)1.000s (~)0.001s (~)1.007s (~)0.862s101.27x
💻 LocalNext.js (Turbopack)0.151s (+6.9% 🔺)1.001s (~)0.010s (-9.4% 🟢)1.015s (~)0.865s101.32x
💻 LocalExpress0.171s (-6.4% 🟢)1.003s (~)0.011s (+10.4% 🔺)1.016s (~)0.845s101.50x
🌐 MongoDBNext.js (Turbopack)0.510s (+2.5%)0.935s (-1.6%)0.001s (-6.7% 🟢)1.008s (~)0.498s104.46x
🐘 PostgresExpress1.317s (-9.1% 🟢)1.800s (+8.6% 🔺)0.001s (-15.4% 🟢)2.012s (~)0.695s1011.51x
🐘 PostgresNitro2.275s (+75.8% 🔺)2.768s (+58.7% 🔺)0.001s (-23.1% 🟢)3.014s (+49.7% 🔺)0.739s1019.89x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.481s (-26.7% 🟢)2.272s (-18.6% 🟢)0.121s (-6.9% 🟢)2.773s (-21.9% 🟢)1.292s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro11/12
🐘 PostgresExpress8/12
▲ VercelNext.js (Turbopack)12/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local12/12
Next.js (Turbopack)🌐 Redis7/12
Nitro💻 Local12/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

if (encryptionKeyOverride) {
encryptionKey = encryptionKeyOverride;
} else {
const rawKey = await world.getEncryptionKeyForRun?.(workflowRun);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For cross-deployment hook resumptions / fetching of the encryption key, we should consider enriching the "source" of the request for the key (i.e. resumeHook in this case) for the audit log to include the context.

Comment threadpackages/world-vercel/src/index.ts Outdated
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add AES-256-GCM encryption primitives and HKDF key derivation - #956

Merged
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption
Feb 20, 2026
Merged

Add AES-256-GCM encryption primitives and HKDF key derivation#956
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 6, 2026

Copy link
Copy Markdown
Member

Summary

Adds the encryption foundation for end-to-end encryption of workflow user data:

  • @workflow/core: Browser-compatible encrypt()/decrypt() using Web Crypto API (AES-256-GCM), importKey() for callers to convert raw bytes to CryptoKey once per run
  • @workflow/world: Overloaded getEncryptionKeyForRun interface — accepts WorkflowRun entity when available, or (runId, context) for start() when the run doesn't exist yet
  • @workflow/world-vercel: deriveRunKey() (HKDF-SHA256), fetchRunKey() (API call with projectId + runId), getEncryptionKeyForRun implementation reading context?.deploymentId
  • All runtime callers updated to call importKey() after key resolution and pass WorkflowRun entity where available
  • handleSuspension refactored to accept WorkflowRun parameter
  • resume-hook.ts uses getHookByTokenWithKey to fetch full WorkflowRun alongside hook + key
  • Separate changesets for @workflow/core, @workflow/world, @workflow/world-vercel

@vercel

vercelBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

CopilotAI review requested due to automatic review settings February 6, 2026 02:30
@changeset-bot

changeset-botBot commented Feb 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c28baee

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelPatch
@workflow/worldPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/web-sharedPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production523049572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2486452862817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4705
✅ example4705
✅ express4705
✅ fastify4705
✅ hono4705
✅ nextjs-turbopack5002
✅ nextjs-webpack5002
✅ nitro4705
✅ nuxt4705
✅ sveltekit4705
✅ vite4705
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds AES-256-GCM encryption with HKDF-SHA256 key derivation to the @workflow/world-vercel package. The implementation provides per-run encryption isolation by deriving unique keys from a deployment key, project ID, and run ID. It integrates seamlessly with the async serialization infrastructure added in PR #955 and uses the client-generated run IDs from PR #954.

Changes:

  • Implements createEncryptor() and createEncryptorFromEnv() functions with full Encryptor interface support
  • Adds AES-256-GCM encryption with random nonces and 128-bit authentication tags
  • Uses HKDF-SHA256 for per-run key derivation with projectId and runId as context
  • Wires encryption into createVercelWorld() via environment variables (VERCEL_DEPLOYMENT_KEY, VERCEL_PROJECT_ID)
  • Includes 18 comprehensive tests covering round-trip, format validation, isolation, and tamper detection

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.

FileDescription
packages/world-vercel/src/encryption.tsCore encryption implementation with key derivation, encrypt/decrypt, and key material access
packages/world-vercel/src/encryption.test.tsComprehensive test suite with 18 tests covering functionality and security properties
packages/world-vercel/src/index.tsIntegration into World creation and public API exports
Comments suppressed due to low confidence (1)

packages/world-vercel/src/index.ts:12

  • The VercelEncryptionConfig interface is exported from encryption.ts but not re-exported from index.ts. Users who want to use createEncryptor() directly would need to import from the internal encryption module, which is not a typical pattern.

Consider adding to index.ts:

export type { VercelEncryptionConfig } from './encryption.js';

This follows the pattern already established with APIConfig and makes the public API more discoverable.

export { createEncryptor, createEncryptorFromEnv } from './encryption.js';
export { createQueue } from './queue.js';
export { createStorage } from './storage.js';
export { createStreamer } from './streamer.js';
export type { APIConfig } from './utils.js';

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #956 - Add Vercel encryption implementation (AES-256-GCM with HKDF)

Summary: Implements AES-256-GCM encryption with HKDF-SHA256 per-run key derivation in @workflow/world-vercel. This is the core crypto PR and the most security-sensitive in the stack.

Crypto Design Review

Algorithm choices are sound:

  • AES-256-GCM: authenticated encryption, industry standard, hardware-accelerated
  • HKDF-SHA256 for key derivation: correct use of KDF for deriving per-run keys from a deployment key
  • 12-byte (96-bit) nonce: standard for GCM
  • 128-bit auth tag: standard for GCM
  • Random nonce per encryption: correct, avoids nonce reuse

Key derivation design:

  • HKDF(key=deploymentKey, salt=zeros, info="${projectId}|${runId}")
  • Zero salt is acceptable here since the deployment key is already random key material (HKDF specification allows this)
  • Using projectId|runId as info provides good domain separation between runs and projects

Concerns

  1. Zero salt vs random salt: While zero salt is acceptable per RFC 5869 when the input key material is already uniformly random, using a random salt stored alongside the ciphertext would provide additional protection against certain attacks (e.g., if the deployment key has less entropy than expected). This is a minor concern given that VERCEL_DEPLOYMENT_KEY should be high-entropy, but worth considering for defense in depth.

  2. Key derivation per encryption call: The deriveKey function is called on every encrypt() and decrypt() call. For workflows with many steps, this means the same HKDF derivation is repeated for each serialization. Consider caching the derived key per (projectId, runId) pair:

    // Cache derived keys to avoid re-deriving per encrypt/decrypt callconstkeyCache=newMap<string,CryptoKey>();

    The cache key would be ${projectId}|${runId}. This is a performance optimization, not a correctness issue.

  3. getKeyMaterial returns raw key bytes: The getKeyMaterial() method returns the raw deploymentKey bytes. This is documented as being for external decryption (o11y tooling), but it means any code with access to the encryptor can extract the master key. Consider:

    • Adding a warning comment about this being sensitive
    • Possibly scoping the returned key material to the derived per-run key rather than the deployment key
  4. createEncryptorFromEnv validation: When VERCEL_DEPLOYMENT_KEY is set but decodes to fewer than 32 bytes, createEncryptor will throw. However, createEncryptorFromEnv doesn't catch this, meaning the world creation will fail hard. Consider catching and logging a warning instead, falling back to no encryption, so a misconfigured key doesn't crash the entire application.

  5. Format prefix encr is 4 bytes of overhead per encrypted payload. This is fine for data payloads but for stream chunks (which may be small), it adds overhead per chunk. Not a major concern but worth noting.

Test Coverage

The 18 tests are comprehensive:

  • Round-trip tests (basic, empty, large, all byte values)
  • Format validation (prefix, structure, nonce randomness)
  • Per-run key isolation
  • Tamper detection
  • Cross-project/cross-key isolation
  • Environment variable handling (missing vars, valid vars)

One test case that could be added: concurrent encryption -- verifying that multiple concurrent encrypt() calls with the same encryptor produce correct results (nonce uniqueness under concurrency).

Minor

  • The Buffer.from(deploymentKeyBase64, 'base64') in createEncryptorFromEnv will silently ignore invalid base64 characters. Consider validating the decoded length matches expectations, or at minimum let createEncryptor's 32-byte check catch it.

Overall, the crypto implementation is solid and follows best practices. The concerns above are mostly optimizations and hardening suggestions rather than security issues.

Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the crypto design is solid — AES-256-GCM with HKDF-SHA256 per-run key derivation is the right approach, the separation between browser-compatible core encryption and Node.js-specific key management is clean, and the World interface integration is well thought out.

A few concerns below, mostly around performance during replay and a potentially unreachable code path.

Comment threadpackages/core/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts
@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.026s (-1.2%)1.004s (~)0.979s101.00x
💻 LocalExpress0.032s (-1.8%)1.005s (~)0.973s101.25x
💻 LocalNext.js (Turbopack)0.040s (-9.9% 🟢)1.005s (~)0.965s101.56x
🌐 RedisNext.js (Turbopack)0.046s (-2.1%)1.005s (~)0.958s101.81x
🌐 MongoDBNext.js (Turbopack)0.078s (-15.4% 🟢)1.007s (~)0.929s103.04x
🐘 PostgresExpress0.127s (-0.8%)1.010s (~)0.883s104.94x
🐘 PostgresNitro0.377s (+317.5% 🔺)1.009s (~)0.632s1014.67x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.612s (+25.2% 🔺)1.929s (-4.1%)1.318s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (~)2.005s (~)0.931s101.00x
🌐 RedisNext.js (Turbopack)1.103s (~)2.006s (~)0.903s101.03x
💻 LocalNext.js (Turbopack)1.105s (+0.7%)2.005s (~)0.900s101.03x
💻 LocalExpress1.105s (-0.5%)2.006s (~)0.900s101.03x
🌐 MongoDBNext.js (Turbopack)1.315s (~)2.007s (~)0.692s101.22x
🐘 PostgresNitro2.403s (-2.6%)3.014s (~)0.611s102.24x
🐘 PostgresExpress2.404s (~)3.013s (~)0.609s102.24x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.087s (+4.6%)3.098s (-5.3% 🟢)1.011s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.543s (~)11.021s (~)0.478s31.00x
🌐 RedisNext.js (Turbopack)10.698s (~)11.021s (~)0.323s31.01x
💻 LocalNext.js (Turbopack)10.749s (~)11.023s (~)0.274s31.02x
💻 LocalExpress10.837s (~)11.022s (~)0.186s31.03x
🌐 MongoDBNext.js (Turbopack)12.209s (-0.7%)13.020s (~)0.810s31.16x
🐘 PostgresNitro20.252s (~)21.062s (~)0.810s21.92x
🐘 PostgresExpress20.368s (+13.9% 🔺)21.056s (+13.5% 🔺)0.688s21.93x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)15.943s (-1.2%)16.835s (-2.1%)0.892s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro26.759s (~)27.047s (~)0.288s31.00x
🌐 RedisNext.js (Turbopack)26.847s (~)27.050s (~)0.203s31.00x
💻 LocalNext.js (Turbopack)27.221s (~)28.053s (~)0.832s31.02x
💻 LocalExpress27.502s (~)28.051s (~)0.549s31.03x
🌐 MongoDBNext.js (Turbopack)30.465s (~)31.038s (~)0.573s21.14x
🐘 PostgresExpress50.386s (+31.9% 🔺)51.129s (+30.8% 🔺)0.743s21.88x
🐘 PostgresNitro50.532s (~)51.128s (~)0.597s21.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)42.878s (+10.0% 🔺)43.689s (+7.4% 🔺)0.811s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.206s (~)55.095s (~)0.888s21.00x
💻 LocalNitro55.559s (~)56.099s (~)0.539s21.02x
💻 LocalNext.js (Turbopack)56.587s (~)57.114s (~)0.527s21.04x
💻 LocalExpress57.264s (~)58.103s (~)0.838s21.06x
🌐 MongoDBNext.js (Turbopack)61.036s (~)61.072s (~)0.035s21.13x
🐘 PostgresExpress78.183s (+3.8%)78.675s (+3.3%)0.492s21.44x
🐘 PostgresNitro100.149s (~)100.224s (~)0.075s11.85x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)81.685s (-1.1%)82.625s (-1.8%)0.940s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.245s (+1.5%)2.006s (~)0.761s151.00x
💻 LocalNitro1.352s (~)2.005s (~)0.653s151.09x
💻 LocalNext.js (Turbopack)1.413s (+0.6%)2.005s (~)0.592s151.13x
💻 LocalExpress1.415s (+1.0%)2.006s (~)0.590s151.14x
🐘 PostgresExpress1.927s (+3.7%)2.515s (+25.0% 🔺)0.588s121.55x
🌐 MongoDBNext.js (Turbopack)2.118s (-1.8%)3.008s (~)0.890s101.70x
🐘 PostgresNitro2.253s (+1.4%)3.013s (~)0.760s101.81x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.246s (-7.6% 🟢)3.144s (-11.0% 🟢)0.898s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.280s (+1.8%)3.006s (~)0.727s101.00x
💻 LocalNext.js (Turbopack)2.431s (-2.2%)2.918s (-3.0%)0.487s111.07x
🌐 RedisNext.js (Turbopack)2.466s (-1.2%)3.007s (~)0.542s101.08x
💻 LocalExpress2.648s (+7.7% 🔺)3.008s (~)0.359s101.16x
🌐 MongoDBNext.js (Turbopack)4.696s (-0.6%)5.176s (~)0.479s62.06x
🐘 PostgresExpress8.660s (-13.7% 🟢)9.537s (-8.0% 🟢)0.877s43.80x
🐘 PostgresNitro9.498s (+33.6% 🔺)10.057s (+33.5% 🔺)0.559s34.17x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.689s (-36.9% 🟢)3.374s (-38.7% 🟢)0.685s91.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.042s (-0.8%)4.725s (-3.0%)0.682s71.00x
💻 LocalNitro6.167s (+2.8%)7.016s (+9.4% 🔺)0.850s51.53x
💻 LocalNext.js (Turbopack)6.445s (-5.7% 🟢)7.016s (-5.4% 🟢)0.571s51.59x
💻 LocalExpress7.857s (+16.2% 🔺)8.272s (+17.9% 🔺)0.414s41.94x
🌐 MongoDBNext.js (Turbopack)9.925s (+1.2%)10.348s (~)0.423s32.46x
🐘 PostgresNitro46.235s (-8.3% 🟢)47.132s (-7.8% 🟢)0.897s111.44x
🐘 PostgresExpress47.330s (-4.8%)48.095s (-4.1%)0.765s111.71x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.494s (-20.5% 🟢)4.448s (-19.5% 🟢)0.953s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.263s (+2.0%)2.006s (~)0.742s151.00x
💻 LocalNitro1.374s (~)2.004s (~)0.631s151.09x
💻 LocalNext.js (Turbopack)1.432s (~)2.005s (~)0.573s151.13x
💻 LocalExpress1.449s (+1.5%)2.006s (~)0.557s151.15x
🐘 PostgresExpress1.837s (-9.5% 🟢)2.011s (-16.1% 🟢)0.174s151.45x
🐘 PostgresNitro2.095s (-5.5% 🟢)2.679s (+3.2%)0.585s121.66x
🌐 MongoDBNext.js (Turbopack)2.127s (-0.6%)3.008s (~)0.881s101.68x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.212s (+0.9%)3.124s (-13.6% 🟢)0.912s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.353s (~)3.007s (~)0.654s101.00x
🌐 RedisNext.js (Turbopack)2.488s (~)3.008s (~)0.520s101.06x
💻 LocalNext.js (Turbopack)2.606s (-3.3%)3.007s (~)0.401s101.11x
💻 LocalExpress2.754s (+6.7% 🔺)3.008s (~)0.255s101.17x
🌐 MongoDBNext.js (Turbopack)4.742s (~)5.177s (~)0.434s62.02x
🐘 PostgresExpress9.968s (-16.0% 🟢)10.373s (-13.8% 🟢)0.405s34.24x
🐘 PostgresNitro10.288s (-7.0% 🟢)11.029s (-5.7% 🟢)0.742s34.37x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.275s (-11.9% 🟢)3.197s (-12.8% 🟢)0.922s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.020s (-0.6%)4.725s (~)0.705s71.00x
💻 LocalNext.js (Turbopack)6.608s (-5.0%)7.216s (-4.0%)0.608s51.64x
💻 LocalNitro6.888s (~)7.015s (-2.8%)0.127s51.71x
💻 LocalExpress8.103s (+12.5% 🔺)8.775s (+9.5% 🔺)0.672s42.02x
🌐 MongoDBNext.js (Turbopack)10.056s (+1.1%)10.348s (-3.1%)0.292s32.50x
🐘 PostgresNitro46.096s (-8.6% 🟢)47.125s (-7.8% 🟢)1.029s111.47x
🐘 PostgresExpress48.860s (-1.0%)49.106s (-2.0%)0.246s112.16x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.233s (-4.2%)4.059s (-22.3% 🟢)0.826s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.114s (+3.2%)1.002s (~)0.009s (-3.2%)1.014s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)0.145s (-2.8%)1.000s (~)0.001s (~)1.007s (~)0.862s101.27x
💻 LocalNext.js (Turbopack)0.151s (+6.9% 🔺)1.001s (~)0.010s (-9.4% 🟢)1.015s (~)0.865s101.32x
💻 LocalExpress0.171s (-6.4% 🟢)1.003s (~)0.011s (+10.4% 🔺)1.016s (~)0.845s101.50x
🌐 MongoDBNext.js (Turbopack)0.510s (+2.5%)0.935s (-1.6%)0.001s (-6.7% 🟢)1.008s (~)0.498s104.46x
🐘 PostgresExpress1.317s (-9.1% 🟢)1.800s (+8.6% 🔺)0.001s (-15.4% 🟢)2.012s (~)0.695s1011.51x
🐘 PostgresNitro2.275s (+75.8% 🔺)2.768s (+58.7% 🔺)0.001s (-23.1% 🟢)3.014s (+49.7% 🔺)0.739s1019.89x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.481s (-26.7% 🟢)2.272s (-18.6% 🟢)0.121s (-6.9% 🟢)2.773s (-21.9% 🟢)1.292s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro11/12
🐘 PostgresExpress8/12
▲ VercelNext.js (Turbopack)12/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local12/12
Next.js (Turbopack)🌐 Redis7/12
Nitro💻 Local12/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

if (encryptionKeyOverride) {
encryptionKey = encryptionKeyOverride;
} else {
const rawKey = await world.getEncryptionKeyForRun?.(workflowRun);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For cross-deployment hook resumptions / fetching of the encryption key, we should consider enriching the "source" of the request for the key (i.e. resumeHook in this case) for the audit log to include the context.

Comment threadpackages/world-vercel/src/index.ts Outdated
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add AES-256-GCM encryption primitives and HKDF key derivation - #956

Merged
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption
Feb 20, 2026
Merged

Add AES-256-GCM encryption primitives and HKDF key derivation#956
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 6, 2026

Copy link
Copy Markdown
Member

Summary

Adds the encryption foundation for end-to-end encryption of workflow user data:

  • @workflow/core: Browser-compatible encrypt()/decrypt() using Web Crypto API (AES-256-GCM), importKey() for callers to convert raw bytes to CryptoKey once per run
  • @workflow/world: Overloaded getEncryptionKeyForRun interface — accepts WorkflowRun entity when available, or (runId, context) for start() when the run doesn't exist yet
  • @workflow/world-vercel: deriveRunKey() (HKDF-SHA256), fetchRunKey() (API call with projectId + runId), getEncryptionKeyForRun implementation reading context?.deploymentId
  • All runtime callers updated to call importKey() after key resolution and pass WorkflowRun entity where available
  • handleSuspension refactored to accept WorkflowRun parameter
  • resume-hook.ts uses getHookByTokenWithKey to fetch full WorkflowRun alongside hook + key
  • Separate changesets for @workflow/core, @workflow/world, @workflow/world-vercel

@vercel

vercelBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

CopilotAI review requested due to automatic review settings February 6, 2026 02:30
@changeset-bot

changeset-botBot commented Feb 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c28baee

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelPatch
@workflow/worldPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/web-sharedPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production523049572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2486452862817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4705
✅ example4705
✅ express4705
✅ fastify4705
✅ hono4705
✅ nextjs-turbopack5002
✅ nextjs-webpack5002
✅ nitro4705
✅ nuxt4705
✅ sveltekit4705
✅ vite4705
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds AES-256-GCM encryption with HKDF-SHA256 key derivation to the @workflow/world-vercel package. The implementation provides per-run encryption isolation by deriving unique keys from a deployment key, project ID, and run ID. It integrates seamlessly with the async serialization infrastructure added in PR #955 and uses the client-generated run IDs from PR #954.

Changes:

  • Implements createEncryptor() and createEncryptorFromEnv() functions with full Encryptor interface support
  • Adds AES-256-GCM encryption with random nonces and 128-bit authentication tags
  • Uses HKDF-SHA256 for per-run key derivation with projectId and runId as context
  • Wires encryption into createVercelWorld() via environment variables (VERCEL_DEPLOYMENT_KEY, VERCEL_PROJECT_ID)
  • Includes 18 comprehensive tests covering round-trip, format validation, isolation, and tamper detection

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.

FileDescription
packages/world-vercel/src/encryption.tsCore encryption implementation with key derivation, encrypt/decrypt, and key material access
packages/world-vercel/src/encryption.test.tsComprehensive test suite with 18 tests covering functionality and security properties
packages/world-vercel/src/index.tsIntegration into World creation and public API exports
Comments suppressed due to low confidence (1)

packages/world-vercel/src/index.ts:12

  • The VercelEncryptionConfig interface is exported from encryption.ts but not re-exported from index.ts. Users who want to use createEncryptor() directly would need to import from the internal encryption module, which is not a typical pattern.

Consider adding to index.ts:

export type { VercelEncryptionConfig } from './encryption.js';

This follows the pattern already established with APIConfig and makes the public API more discoverable.

export { createEncryptor, createEncryptorFromEnv } from './encryption.js';
export { createQueue } from './queue.js';
export { createStorage } from './storage.js';
export { createStreamer } from './streamer.js';
export type { APIConfig } from './utils.js';

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #956 - Add Vercel encryption implementation (AES-256-GCM with HKDF)

Summary: Implements AES-256-GCM encryption with HKDF-SHA256 per-run key derivation in @workflow/world-vercel. This is the core crypto PR and the most security-sensitive in the stack.

Crypto Design Review

Algorithm choices are sound:

  • AES-256-GCM: authenticated encryption, industry standard, hardware-accelerated
  • HKDF-SHA256 for key derivation: correct use of KDF for deriving per-run keys from a deployment key
  • 12-byte (96-bit) nonce: standard for GCM
  • 128-bit auth tag: standard for GCM
  • Random nonce per encryption: correct, avoids nonce reuse

Key derivation design:

  • HKDF(key=deploymentKey, salt=zeros, info="${projectId}|${runId}")
  • Zero salt is acceptable here since the deployment key is already random key material (HKDF specification allows this)
  • Using projectId|runId as info provides good domain separation between runs and projects

Concerns

  1. Zero salt vs random salt: While zero salt is acceptable per RFC 5869 when the input key material is already uniformly random, using a random salt stored alongside the ciphertext would provide additional protection against certain attacks (e.g., if the deployment key has less entropy than expected). This is a minor concern given that VERCEL_DEPLOYMENT_KEY should be high-entropy, but worth considering for defense in depth.

  2. Key derivation per encryption call: The deriveKey function is called on every encrypt() and decrypt() call. For workflows with many steps, this means the same HKDF derivation is repeated for each serialization. Consider caching the derived key per (projectId, runId) pair:

    // Cache derived keys to avoid re-deriving per encrypt/decrypt callconstkeyCache=newMap<string,CryptoKey>();

    The cache key would be ${projectId}|${runId}. This is a performance optimization, not a correctness issue.

  3. getKeyMaterial returns raw key bytes: The getKeyMaterial() method returns the raw deploymentKey bytes. This is documented as being for external decryption (o11y tooling), but it means any code with access to the encryptor can extract the master key. Consider:

    • Adding a warning comment about this being sensitive
    • Possibly scoping the returned key material to the derived per-run key rather than the deployment key
  4. createEncryptorFromEnv validation: When VERCEL_DEPLOYMENT_KEY is set but decodes to fewer than 32 bytes, createEncryptor will throw. However, createEncryptorFromEnv doesn't catch this, meaning the world creation will fail hard. Consider catching and logging a warning instead, falling back to no encryption, so a misconfigured key doesn't crash the entire application.

  5. Format prefix encr is 4 bytes of overhead per encrypted payload. This is fine for data payloads but for stream chunks (which may be small), it adds overhead per chunk. Not a major concern but worth noting.

Test Coverage

The 18 tests are comprehensive:

  • Round-trip tests (basic, empty, large, all byte values)
  • Format validation (prefix, structure, nonce randomness)
  • Per-run key isolation
  • Tamper detection
  • Cross-project/cross-key isolation
  • Environment variable handling (missing vars, valid vars)

One test case that could be added: concurrent encryption -- verifying that multiple concurrent encrypt() calls with the same encryptor produce correct results (nonce uniqueness under concurrency).

Minor

  • The Buffer.from(deploymentKeyBase64, 'base64') in createEncryptorFromEnv will silently ignore invalid base64 characters. Consider validating the decoded length matches expectations, or at minimum let createEncryptor's 32-byte check catch it.

Overall, the crypto implementation is solid and follows best practices. The concerns above are mostly optimizations and hardening suggestions rather than security issues.

Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the crypto design is solid — AES-256-GCM with HKDF-SHA256 per-run key derivation is the right approach, the separation between browser-compatible core encryption and Node.js-specific key management is clean, and the World interface integration is well thought out.

A few concerns below, mostly around performance during replay and a potentially unreachable code path.

Comment threadpackages/core/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts
@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.026s (-1.2%)1.004s (~)0.979s101.00x
💻 LocalExpress0.032s (-1.8%)1.005s (~)0.973s101.25x
💻 LocalNext.js (Turbopack)0.040s (-9.9% 🟢)1.005s (~)0.965s101.56x
🌐 RedisNext.js (Turbopack)0.046s (-2.1%)1.005s (~)0.958s101.81x
🌐 MongoDBNext.js (Turbopack)0.078s (-15.4% 🟢)1.007s (~)0.929s103.04x
🐘 PostgresExpress0.127s (-0.8%)1.010s (~)0.883s104.94x
🐘 PostgresNitro0.377s (+317.5% 🔺)1.009s (~)0.632s1014.67x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.612s (+25.2% 🔺)1.929s (-4.1%)1.318s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (~)2.005s (~)0.931s101.00x
🌐 RedisNext.js (Turbopack)1.103s (~)2.006s (~)0.903s101.03x
💻 LocalNext.js (Turbopack)1.105s (+0.7%)2.005s (~)0.900s101.03x
💻 LocalExpress1.105s (-0.5%)2.006s (~)0.900s101.03x
🌐 MongoDBNext.js (Turbopack)1.315s (~)2.007s (~)0.692s101.22x
🐘 PostgresNitro2.403s (-2.6%)3.014s (~)0.611s102.24x
🐘 PostgresExpress2.404s (~)3.013s (~)0.609s102.24x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.087s (+4.6%)3.098s (-5.3% 🟢)1.011s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.543s (~)11.021s (~)0.478s31.00x
🌐 RedisNext.js (Turbopack)10.698s (~)11.021s (~)0.323s31.01x
💻 LocalNext.js (Turbopack)10.749s (~)11.023s (~)0.274s31.02x
💻 LocalExpress10.837s (~)11.022s (~)0.186s31.03x
🌐 MongoDBNext.js (Turbopack)12.209s (-0.7%)13.020s (~)0.810s31.16x
🐘 PostgresNitro20.252s (~)21.062s (~)0.810s21.92x
🐘 PostgresExpress20.368s (+13.9% 🔺)21.056s (+13.5% 🔺)0.688s21.93x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)15.943s (-1.2%)16.835s (-2.1%)0.892s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro26.759s (~)27.047s (~)0.288s31.00x
🌐 RedisNext.js (Turbopack)26.847s (~)27.050s (~)0.203s31.00x
💻 LocalNext.js (Turbopack)27.221s (~)28.053s (~)0.832s31.02x
💻 LocalExpress27.502s (~)28.051s (~)0.549s31.03x
🌐 MongoDBNext.js (Turbopack)30.465s (~)31.038s (~)0.573s21.14x
🐘 PostgresExpress50.386s (+31.9% 🔺)51.129s (+30.8% 🔺)0.743s21.88x
🐘 PostgresNitro50.532s (~)51.128s (~)0.597s21.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)42.878s (+10.0% 🔺)43.689s (+7.4% 🔺)0.811s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.206s (~)55.095s (~)0.888s21.00x
💻 LocalNitro55.559s (~)56.099s (~)0.539s21.02x
💻 LocalNext.js (Turbopack)56.587s (~)57.114s (~)0.527s21.04x
💻 LocalExpress57.264s (~)58.103s (~)0.838s21.06x
🌐 MongoDBNext.js (Turbopack)61.036s (~)61.072s (~)0.035s21.13x
🐘 PostgresExpress78.183s (+3.8%)78.675s (+3.3%)0.492s21.44x
🐘 PostgresNitro100.149s (~)100.224s (~)0.075s11.85x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)81.685s (-1.1%)82.625s (-1.8%)0.940s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.245s (+1.5%)2.006s (~)0.761s151.00x
💻 LocalNitro1.352s (~)2.005s (~)0.653s151.09x
💻 LocalNext.js (Turbopack)1.413s (+0.6%)2.005s (~)0.592s151.13x
💻 LocalExpress1.415s (+1.0%)2.006s (~)0.590s151.14x
🐘 PostgresExpress1.927s (+3.7%)2.515s (+25.0% 🔺)0.588s121.55x
🌐 MongoDBNext.js (Turbopack)2.118s (-1.8%)3.008s (~)0.890s101.70x
🐘 PostgresNitro2.253s (+1.4%)3.013s (~)0.760s101.81x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.246s (-7.6% 🟢)3.144s (-11.0% 🟢)0.898s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.280s (+1.8%)3.006s (~)0.727s101.00x
💻 LocalNext.js (Turbopack)2.431s (-2.2%)2.918s (-3.0%)0.487s111.07x
🌐 RedisNext.js (Turbopack)2.466s (-1.2%)3.007s (~)0.542s101.08x
💻 LocalExpress2.648s (+7.7% 🔺)3.008s (~)0.359s101.16x
🌐 MongoDBNext.js (Turbopack)4.696s (-0.6%)5.176s (~)0.479s62.06x
🐘 PostgresExpress8.660s (-13.7% 🟢)9.537s (-8.0% 🟢)0.877s43.80x
🐘 PostgresNitro9.498s (+33.6% 🔺)10.057s (+33.5% 🔺)0.559s34.17x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.689s (-36.9% 🟢)3.374s (-38.7% 🟢)0.685s91.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.042s (-0.8%)4.725s (-3.0%)0.682s71.00x
💻 LocalNitro6.167s (+2.8%)7.016s (+9.4% 🔺)0.850s51.53x
💻 LocalNext.js (Turbopack)6.445s (-5.7% 🟢)7.016s (-5.4% 🟢)0.571s51.59x
💻 LocalExpress7.857s (+16.2% 🔺)8.272s (+17.9% 🔺)0.414s41.94x
🌐 MongoDBNext.js (Turbopack)9.925s (+1.2%)10.348s (~)0.423s32.46x
🐘 PostgresNitro46.235s (-8.3% 🟢)47.132s (-7.8% 🟢)0.897s111.44x
🐘 PostgresExpress47.330s (-4.8%)48.095s (-4.1%)0.765s111.71x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.494s (-20.5% 🟢)4.448s (-19.5% 🟢)0.953s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.263s (+2.0%)2.006s (~)0.742s151.00x
💻 LocalNitro1.374s (~)2.004s (~)0.631s151.09x
💻 LocalNext.js (Turbopack)1.432s (~)2.005s (~)0.573s151.13x
💻 LocalExpress1.449s (+1.5%)2.006s (~)0.557s151.15x
🐘 PostgresExpress1.837s (-9.5% 🟢)2.011s (-16.1% 🟢)0.174s151.45x
🐘 PostgresNitro2.095s (-5.5% 🟢)2.679s (+3.2%)0.585s121.66x
🌐 MongoDBNext.js (Turbopack)2.127s (-0.6%)3.008s (~)0.881s101.68x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.212s (+0.9%)3.124s (-13.6% 🟢)0.912s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.353s (~)3.007s (~)0.654s101.00x
🌐 RedisNext.js (Turbopack)2.488s (~)3.008s (~)0.520s101.06x
💻 LocalNext.js (Turbopack)2.606s (-3.3%)3.007s (~)0.401s101.11x
💻 LocalExpress2.754s (+6.7% 🔺)3.008s (~)0.255s101.17x
🌐 MongoDBNext.js (Turbopack)4.742s (~)5.177s (~)0.434s62.02x
🐘 PostgresExpress9.968s (-16.0% 🟢)10.373s (-13.8% 🟢)0.405s34.24x
🐘 PostgresNitro10.288s (-7.0% 🟢)11.029s (-5.7% 🟢)0.742s34.37x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.275s (-11.9% 🟢)3.197s (-12.8% 🟢)0.922s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.020s (-0.6%)4.725s (~)0.705s71.00x
💻 LocalNext.js (Turbopack)6.608s (-5.0%)7.216s (-4.0%)0.608s51.64x
💻 LocalNitro6.888s (~)7.015s (-2.8%)0.127s51.71x
💻 LocalExpress8.103s (+12.5% 🔺)8.775s (+9.5% 🔺)0.672s42.02x
🌐 MongoDBNext.js (Turbopack)10.056s (+1.1%)10.348s (-3.1%)0.292s32.50x
🐘 PostgresNitro46.096s (-8.6% 🟢)47.125s (-7.8% 🟢)1.029s111.47x
🐘 PostgresExpress48.860s (-1.0%)49.106s (-2.0%)0.246s112.16x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.233s (-4.2%)4.059s (-22.3% 🟢)0.826s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.114s (+3.2%)1.002s (~)0.009s (-3.2%)1.014s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)0.145s (-2.8%)1.000s (~)0.001s (~)1.007s (~)0.862s101.27x
💻 LocalNext.js (Turbopack)0.151s (+6.9% 🔺)1.001s (~)0.010s (-9.4% 🟢)1.015s (~)0.865s101.32x
💻 LocalExpress0.171s (-6.4% 🟢)1.003s (~)0.011s (+10.4% 🔺)1.016s (~)0.845s101.50x
🌐 MongoDBNext.js (Turbopack)0.510s (+2.5%)0.935s (-1.6%)0.001s (-6.7% 🟢)1.008s (~)0.498s104.46x
🐘 PostgresExpress1.317s (-9.1% 🟢)1.800s (+8.6% 🔺)0.001s (-15.4% 🟢)2.012s (~)0.695s1011.51x
🐘 PostgresNitro2.275s (+75.8% 🔺)2.768s (+58.7% 🔺)0.001s (-23.1% 🟢)3.014s (+49.7% 🔺)0.739s1019.89x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.481s (-26.7% 🟢)2.272s (-18.6% 🟢)0.121s (-6.9% 🟢)2.773s (-21.9% 🟢)1.292s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro11/12
🐘 PostgresExpress8/12
▲ VercelNext.js (Turbopack)12/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local12/12
Next.js (Turbopack)🌐 Redis7/12
Nitro💻 Local12/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

if (encryptionKeyOverride) {
encryptionKey = encryptionKeyOverride;
} else {
const rawKey = await world.getEncryptionKeyForRun?.(workflowRun);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For cross-deployment hook resumptions / fetching of the encryption key, we should consider enriching the "source" of the request for the key (i.e. resumeHook in this case) for the audit log to include the context.

Comment threadpackages/world-vercel/src/index.ts Outdated
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add AES-256-GCM encryption primitives and HKDF key derivation - #956

Merged
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption
Feb 20, 2026
Merged

Add AES-256-GCM encryption primitives and HKDF key derivation#956
TooTallNate merged 12 commits into
mainfrom
nate/vercel-encryption

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Feb 6, 2026

Copy link
Copy Markdown
Member

Summary

Adds the encryption foundation for end-to-end encryption of workflow user data:

  • @workflow/core: Browser-compatible encrypt()/decrypt() using Web Crypto API (AES-256-GCM), importKey() for callers to convert raw bytes to CryptoKey once per run
  • @workflow/world: Overloaded getEncryptionKeyForRun interface — accepts WorkflowRun entity when available, or (runId, context) for start() when the run doesn't exist yet
  • @workflow/world-vercel: deriveRunKey() (HKDF-SHA256), fetchRunKey() (API call with projectId + runId), getEncryptionKeyForRun implementation reading context?.deploymentId
  • All runtime callers updated to call importKey() after key resolution and pass WorkflowRun entity where available
  • handleSuspension refactored to accept WorkflowRun parameter
  • resume-hook.ts uses getHookByTokenWithKey to fetch full WorkflowRun alongside hook + key
  • Separate changesets for @workflow/core, @workflow/world, @workflow/world-vercel

@vercel

vercelBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

CopilotAI review requested due to automatic review settings February 6, 2026 02:30
@changeset-bot

changeset-botBot commented Feb 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c28baee

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
NameType
@workflow/world-vercelPatch
@workflow/worldPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/web-sharedPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production523049572
✅ 💻 Local Development556068624
✅ 📦 Local Production556068624
✅ 🐘 Local Postgres556068624
✅ 🪟 Windows490352
❌ 🌍 Community Worlds111459165
✅ 📋 Other135021156
Total2486452862817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4705
✅ example4705
✅ express4705
✅ fastify4705
✅ hono4705
✅ nextjs-turbopack5002
✅ nextjs-webpack5002
✅ nitro4705
✅ nuxt4705
✅ sveltekit4705
✅ vite4705
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4507
✅ express-stable4507
✅ fastify-stable4507
✅ hono-stable4507
✅ nextjs-turbopack-canary4903
✅ nextjs-turbopack-stable4903
✅ nextjs-webpack-canary4903
✅ nextjs-webpack-stable4903
✅ nitro-stable4507
✅ nuxt-stable4507
✅ sveltekit-stable4507
✅ vite-stable4507
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4903
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
✅ mongodb4903
✅ redis-dev300
✅ redis4903
✅ turso-dev300
❌ turso4453
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4507
✅ e2e-local-postgres-nest-stable4507
✅ e2e-local-prod-nest-stable4507

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds AES-256-GCM encryption with HKDF-SHA256 key derivation to the @workflow/world-vercel package. The implementation provides per-run encryption isolation by deriving unique keys from a deployment key, project ID, and run ID. It integrates seamlessly with the async serialization infrastructure added in PR #955 and uses the client-generated run IDs from PR #954.

Changes:

  • Implements createEncryptor() and createEncryptorFromEnv() functions with full Encryptor interface support
  • Adds AES-256-GCM encryption with random nonces and 128-bit authentication tags
  • Uses HKDF-SHA256 for per-run key derivation with projectId and runId as context
  • Wires encryption into createVercelWorld() via environment variables (VERCEL_DEPLOYMENT_KEY, VERCEL_PROJECT_ID)
  • Includes 18 comprehensive tests covering round-trip, format validation, isolation, and tamper detection

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.

FileDescription
packages/world-vercel/src/encryption.tsCore encryption implementation with key derivation, encrypt/decrypt, and key material access
packages/world-vercel/src/encryption.test.tsComprehensive test suite with 18 tests covering functionality and security properties
packages/world-vercel/src/index.tsIntegration into World creation and public API exports
Comments suppressed due to low confidence (1)

packages/world-vercel/src/index.ts:12

  • The VercelEncryptionConfig interface is exported from encryption.ts but not re-exported from index.ts. Users who want to use createEncryptor() directly would need to import from the internal encryption module, which is not a typical pattern.

Consider adding to index.ts:

export type { VercelEncryptionConfig } from './encryption.js';

This follows the pattern already established with APIConfig and makes the public API more discoverable.

export { createEncryptor, createEncryptorFromEnv } from './encryption.js';
export { createQueue } from './queue.js';
export { createStorage } from './storage.js';
export { createStreamer } from './streamer.js';
export type { APIConfig } from './utils.js';

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #956 - Add Vercel encryption implementation (AES-256-GCM with HKDF)

Summary: Implements AES-256-GCM encryption with HKDF-SHA256 per-run key derivation in @workflow/world-vercel. This is the core crypto PR and the most security-sensitive in the stack.

Crypto Design Review

Algorithm choices are sound:

  • AES-256-GCM: authenticated encryption, industry standard, hardware-accelerated
  • HKDF-SHA256 for key derivation: correct use of KDF for deriving per-run keys from a deployment key
  • 12-byte (96-bit) nonce: standard for GCM
  • 128-bit auth tag: standard for GCM
  • Random nonce per encryption: correct, avoids nonce reuse

Key derivation design:

  • HKDF(key=deploymentKey, salt=zeros, info="${projectId}|${runId}")
  • Zero salt is acceptable here since the deployment key is already random key material (HKDF specification allows this)
  • Using projectId|runId as info provides good domain separation between runs and projects

Concerns

  1. Zero salt vs random salt: While zero salt is acceptable per RFC 5869 when the input key material is already uniformly random, using a random salt stored alongside the ciphertext would provide additional protection against certain attacks (e.g., if the deployment key has less entropy than expected). This is a minor concern given that VERCEL_DEPLOYMENT_KEY should be high-entropy, but worth considering for defense in depth.

  2. Key derivation per encryption call: The deriveKey function is called on every encrypt() and decrypt() call. For workflows with many steps, this means the same HKDF derivation is repeated for each serialization. Consider caching the derived key per (projectId, runId) pair:

    // Cache derived keys to avoid re-deriving per encrypt/decrypt callconstkeyCache=newMap<string,CryptoKey>();

    The cache key would be ${projectId}|${runId}. This is a performance optimization, not a correctness issue.

  3. getKeyMaterial returns raw key bytes: The getKeyMaterial() method returns the raw deploymentKey bytes. This is documented as being for external decryption (o11y tooling), but it means any code with access to the encryptor can extract the master key. Consider:

    • Adding a warning comment about this being sensitive
    • Possibly scoping the returned key material to the derived per-run key rather than the deployment key
  4. createEncryptorFromEnv validation: When VERCEL_DEPLOYMENT_KEY is set but decodes to fewer than 32 bytes, createEncryptor will throw. However, createEncryptorFromEnv doesn't catch this, meaning the world creation will fail hard. Consider catching and logging a warning instead, falling back to no encryption, so a misconfigured key doesn't crash the entire application.

  5. Format prefix encr is 4 bytes of overhead per encrypted payload. This is fine for data payloads but for stream chunks (which may be small), it adds overhead per chunk. Not a major concern but worth noting.

Test Coverage

The 18 tests are comprehensive:

  • Round-trip tests (basic, empty, large, all byte values)
  • Format validation (prefix, structure, nonce randomness)
  • Per-run key isolation
  • Tamper detection
  • Cross-project/cross-key isolation
  • Environment variable handling (missing vars, valid vars)

One test case that could be added: concurrent encryption -- verifying that multiple concurrent encrypt() calls with the same encryptor produce correct results (nonce uniqueness under concurrency).

Minor

  • The Buffer.from(deploymentKeyBase64, 'base64') in createEncryptorFromEnv will silently ignore invalid base64 characters. Consider validating the decoded length matches expectations, or at minimum let createEncryptor's 32-byte check catch it.

Overall, the crypto implementation is solid and follows best practices. The concerns above are mostly optimizations and hardening suggestions rather than security issues.

Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts Outdated
Comment threadpackages/world-vercel/src/encryption.ts Outdated

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the crypto design is solid — AES-256-GCM with HKDF-SHA256 per-run key derivation is the right approach, the separation between browser-compatible core encryption and Node.js-specific key management is clean, and the World interface integration is well thought out.

A few concerns below, mostly around performance during replay and a potentially unreachable code path.

Comment threadpackages/core/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/encryption.ts
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/index.ts Outdated
Comment threadpackages/world-vercel/src/encryption.test.ts
@github-actions

github-actionsBot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.026s (-1.2%)1.004s (~)0.979s101.00x
💻 LocalExpress0.032s (-1.8%)1.005s (~)0.973s101.25x
💻 LocalNext.js (Turbopack)0.040s (-9.9% 🟢)1.005s (~)0.965s101.56x
🌐 RedisNext.js (Turbopack)0.046s (-2.1%)1.005s (~)0.958s101.81x
🌐 MongoDBNext.js (Turbopack)0.078s (-15.4% 🟢)1.007s (~)0.929s103.04x
🐘 PostgresExpress0.127s (-0.8%)1.010s (~)0.883s104.94x
🐘 PostgresNitro0.377s (+317.5% 🔺)1.009s (~)0.632s1014.67x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.612s (+25.2% 🔺)1.929s (-4.1%)1.318s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (~)2.005s (~)0.931s101.00x
🌐 RedisNext.js (Turbopack)1.103s (~)2.006s (~)0.903s101.03x
💻 LocalNext.js (Turbopack)1.105s (+0.7%)2.005s (~)0.900s101.03x
💻 LocalExpress1.105s (-0.5%)2.006s (~)0.900s101.03x
🌐 MongoDBNext.js (Turbopack)1.315s (~)2.007s (~)0.692s101.22x
🐘 PostgresNitro2.403s (-2.6%)3.014s (~)0.611s102.24x
🐘 PostgresExpress2.404s (~)3.013s (~)0.609s102.24x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.087s (+4.6%)3.098s (-5.3% 🟢)1.011s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.543s (~)11.021s (~)0.478s31.00x
🌐 RedisNext.js (Turbopack)10.698s (~)11.021s (~)0.323s31.01x
💻 LocalNext.js (Turbopack)10.749s (~)11.023s (~)0.274s31.02x
💻 LocalExpress10.837s (~)11.022s (~)0.186s31.03x
🌐 MongoDBNext.js (Turbopack)12.209s (-0.7%)13.020s (~)0.810s31.16x
🐘 PostgresNitro20.252s (~)21.062s (~)0.810s21.92x
🐘 PostgresExpress20.368s (+13.9% 🔺)21.056s (+13.5% 🔺)0.688s21.93x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)15.943s (-1.2%)16.835s (-2.1%)0.892s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro26.759s (~)27.047s (~)0.288s31.00x
🌐 RedisNext.js (Turbopack)26.847s (~)27.050s (~)0.203s31.00x
💻 LocalNext.js (Turbopack)27.221s (~)28.053s (~)0.832s31.02x
💻 LocalExpress27.502s (~)28.051s (~)0.549s31.03x
🌐 MongoDBNext.js (Turbopack)30.465s (~)31.038s (~)0.573s21.14x
🐘 PostgresExpress50.386s (+31.9% 🔺)51.129s (+30.8% 🔺)0.743s21.88x
🐘 PostgresNitro50.532s (~)51.128s (~)0.597s21.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)42.878s (+10.0% 🔺)43.689s (+7.4% 🔺)0.811s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)54.206s (~)55.095s (~)0.888s21.00x
💻 LocalNitro55.559s (~)56.099s (~)0.539s21.02x
💻 LocalNext.js (Turbopack)56.587s (~)57.114s (~)0.527s21.04x
💻 LocalExpress57.264s (~)58.103s (~)0.838s21.06x
🌐 MongoDBNext.js (Turbopack)61.036s (~)61.072s (~)0.035s21.13x
🐘 PostgresExpress78.183s (+3.8%)78.675s (+3.3%)0.492s21.44x
🐘 PostgresNitro100.149s (~)100.224s (~)0.075s11.85x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)81.685s (-1.1%)82.625s (-1.8%)0.940s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.245s (+1.5%)2.006s (~)0.761s151.00x
💻 LocalNitro1.352s (~)2.005s (~)0.653s151.09x
💻 LocalNext.js (Turbopack)1.413s (+0.6%)2.005s (~)0.592s151.13x
💻 LocalExpress1.415s (+1.0%)2.006s (~)0.590s151.14x
🐘 PostgresExpress1.927s (+3.7%)2.515s (+25.0% 🔺)0.588s121.55x
🌐 MongoDBNext.js (Turbopack)2.118s (-1.8%)3.008s (~)0.890s101.70x
🐘 PostgresNitro2.253s (+1.4%)3.013s (~)0.760s101.81x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.246s (-7.6% 🟢)3.144s (-11.0% 🟢)0.898s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.280s (+1.8%)3.006s (~)0.727s101.00x
💻 LocalNext.js (Turbopack)2.431s (-2.2%)2.918s (-3.0%)0.487s111.07x
🌐 RedisNext.js (Turbopack)2.466s (-1.2%)3.007s (~)0.542s101.08x
💻 LocalExpress2.648s (+7.7% 🔺)3.008s (~)0.359s101.16x
🌐 MongoDBNext.js (Turbopack)4.696s (-0.6%)5.176s (~)0.479s62.06x
🐘 PostgresExpress8.660s (-13.7% 🟢)9.537s (-8.0% 🟢)0.877s43.80x
🐘 PostgresNitro9.498s (+33.6% 🔺)10.057s (+33.5% 🔺)0.559s34.17x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.689s (-36.9% 🟢)3.374s (-38.7% 🟢)0.685s91.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.042s (-0.8%)4.725s (-3.0%)0.682s71.00x
💻 LocalNitro6.167s (+2.8%)7.016s (+9.4% 🔺)0.850s51.53x
💻 LocalNext.js (Turbopack)6.445s (-5.7% 🟢)7.016s (-5.4% 🟢)0.571s51.59x
💻 LocalExpress7.857s (+16.2% 🔺)8.272s (+17.9% 🔺)0.414s41.94x
🌐 MongoDBNext.js (Turbopack)9.925s (+1.2%)10.348s (~)0.423s32.46x
🐘 PostgresNitro46.235s (-8.3% 🟢)47.132s (-7.8% 🟢)0.897s111.44x
🐘 PostgresExpress47.330s (-4.8%)48.095s (-4.1%)0.765s111.71x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.494s (-20.5% 🟢)4.448s (-19.5% 🟢)0.953s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.263s (+2.0%)2.006s (~)0.742s151.00x
💻 LocalNitro1.374s (~)2.004s (~)0.631s151.09x
💻 LocalNext.js (Turbopack)1.432s (~)2.005s (~)0.573s151.13x
💻 LocalExpress1.449s (+1.5%)2.006s (~)0.557s151.15x
🐘 PostgresExpress1.837s (-9.5% 🟢)2.011s (-16.1% 🟢)0.174s151.45x
🐘 PostgresNitro2.095s (-5.5% 🟢)2.679s (+3.2%)0.585s121.66x
🌐 MongoDBNext.js (Turbopack)2.127s (-0.6%)3.008s (~)0.881s101.68x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.212s (+0.9%)3.124s (-13.6% 🟢)0.912s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.353s (~)3.007s (~)0.654s101.00x
🌐 RedisNext.js (Turbopack)2.488s (~)3.008s (~)0.520s101.06x
💻 LocalNext.js (Turbopack)2.606s (-3.3%)3.007s (~)0.401s101.11x
💻 LocalExpress2.754s (+6.7% 🔺)3.008s (~)0.255s101.17x
🌐 MongoDBNext.js (Turbopack)4.742s (~)5.177s (~)0.434s62.02x
🐘 PostgresExpress9.968s (-16.0% 🟢)10.373s (-13.8% 🟢)0.405s34.24x
🐘 PostgresNitro10.288s (-7.0% 🟢)11.029s (-5.7% 🟢)0.742s34.37x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.275s (-11.9% 🟢)3.197s (-12.8% 🟢)0.922s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)4.020s (-0.6%)4.725s (~)0.705s71.00x
💻 LocalNext.js (Turbopack)6.608s (-5.0%)7.216s (-4.0%)0.608s51.64x
💻 LocalNitro6.888s (~)7.015s (-2.8%)0.127s51.71x
💻 LocalExpress8.103s (+12.5% 🔺)8.775s (+9.5% 🔺)0.672s42.02x
🌐 MongoDBNext.js (Turbopack)10.056s (+1.1%)10.348s (-3.1%)0.292s32.50x
🐘 PostgresNitro46.096s (-8.6% 🟢)47.125s (-7.8% 🟢)1.029s111.47x
🐘 PostgresExpress48.860s (-1.0%)49.106s (-2.0%)0.246s112.16x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.233s (-4.2%)4.059s (-22.3% 🟢)0.826s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.114s (+3.2%)1.002s (~)0.009s (-3.2%)1.014s (~)0.899s101.00x
🌐 RedisNext.js (Turbopack)0.145s (-2.8%)1.000s (~)0.001s (~)1.007s (~)0.862s101.27x
💻 LocalNext.js (Turbopack)0.151s (+6.9% 🔺)1.001s (~)0.010s (-9.4% 🟢)1.015s (~)0.865s101.32x
💻 LocalExpress0.171s (-6.4% 🟢)1.003s (~)0.011s (+10.4% 🔺)1.016s (~)0.845s101.50x
🌐 MongoDBNext.js (Turbopack)0.510s (+2.5%)0.935s (-1.6%)0.001s (-6.7% 🟢)1.008s (~)0.498s104.46x
🐘 PostgresExpress1.317s (-9.1% 🟢)1.800s (+8.6% 🔺)0.001s (-15.4% 🟢)2.012s (~)0.695s1011.51x
🐘 PostgresNitro2.275s (+75.8% 🔺)2.768s (+58.7% 🔺)0.001s (-23.1% 🟢)3.014s (+49.7% 🔺)0.739s1019.89x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.481s (-26.7% 🟢)2.272s (-18.6% 🟢)0.121s (-6.9% 🟢)2.773s (-21.9% 🟢)1.292s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro11/12
🐘 PostgresExpress8/12
▲ VercelNext.js (Turbopack)12/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local12/12
Next.js (Turbopack)🌐 Redis7/12
Nitro💻 Local12/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

if (encryptionKeyOverride) {
encryptionKey = encryptionKeyOverride;
} else {
const rawKey = await world.getEncryptionKeyForRun?.(workflowRun);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For cross-deployment hook resumptions / fetching of the encryption key, we should consider enriching the "source" of the request for the key (i.e. resumeHook in this case) for the audit log to include the context.

Comment threadpackages/world-vercel/src/index.ts Outdated
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@VaguelySerious