Skip to content

Conversation

@TooTallNate
Copy link
Member

@TooTallNate TooTallNate commented Feb 8, 2026

Summary

Adds the Encryptor interface and threads it through the serialization layer, preparing the codebase for E2E encryption without yet wiring in any actual encryption logic.

New types (@workflow/world)

  • Encryptor — optional encrypt(), decrypt(), and getKeyMaterial() methods
  • EncryptionContext — contains runId for per-run key derivation
  • KeyMaterial — key bytes + derivation metadata for external tooling (o11y)
  • World now extends Encryptor — all methods optional, so existing implementations are unaffected
  • World.getEncryptorForRun?(runId) — resolves an Encryptor for a specific run's deployment context, enabling cross-deployment encryption (e.g., resumeHook() from a newer deployment)

Serialization signature changes

All 8 dehydrate/hydrate functions gain encryptor: Encryptor as a new parameter (prefixed with _ since it's unused in this PR):

Before After
dehydrateWorkflowArguments(value, ops, runId, ...) dehydrateWorkflowArguments(value, runId, encryptor, ops, ...)
hydrateWorkflowArguments(value, global, ...) hydrateWorkflowArguments(value, runId, encryptor, global, ...)
dehydrateStepReturnValue(value, ops, runId, ...) dehydrateStepReturnValue(value, runId, encryptor, ops, ...)
(and so on for all 8 functions)

Runtime changes

  • WorkflowOrchestratorContext — adds runId: string and encryptor: Encryptor
  • runWorkflow() — accepts encryptor as 4th parameter
  • resumeHook() — restructured with getHookByTokenWithEncryptor() to resolve the encryptor once and reuse for both metadata decryption and payload encryption (zero redundant key resolutions)
  • hydrateResourceIO() — accepts encryptor parameter, threaded from CLI/web callers

Cross-deployment design

When resumeHook() is called from a different deployment than the target workflow run, the encryption keys differ. The new World.getEncryptorForRun(runId) method allows the World implementation to resolve the correct key (e.g., via an authenticated API endpoint). The resumeHook flow resolves the encryptor once via getHookByTokenWithEncryptor() and reuses it for both operations.

Test plan

All 305 core tests pass. Build succeeds. The encryptor parameter is unused (_encryptor) in this PR — actual encryption is wired in PR #957.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 8, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 490 0 38 528
✅ 💻 Local Development 418 0 62 480
✅ 📦 Local Production 418 0 62 480
✅ 🐘 Local Postgres 418 0 62 480
✅ 🪟 Windows 45 0 3 48
❌ 🌍 Community Worlds 102 42 9 153
✅ 📋 Other 123 0 21 144
Total 2014 42 257 2313

❌ Failed Tests

🌍 Community Worlds (42 failed)

mongodb (1 failed):

  • webhookWorkflow

turso (41 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • 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 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
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 44 0 4
✅ example 44 0 4
✅ express 44 0 4
✅ fastify 44 0 4
✅ hono 44 0 4
✅ nextjs-turbopack 47 0 1
✅ nextjs-webpack 47 0 1
✅ nitro 44 0 4
✅ nuxt 44 0 4
✅ sveltekit 44 0 4
✅ vite 44 0 4
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 41 0 7
✅ express-stable 41 0 7
✅ fastify-stable 41 0 7
✅ hono-stable 41 0 7
✅ nextjs-turbopack-stable 45 0 3
✅ nextjs-webpack-stable 45 0 3
✅ nitro-stable 41 0 7
✅ nuxt-stable 41 0 7
✅ sveltekit-stable 41 0 7
✅ vite-stable 41 0 7
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 41 0 7
✅ express-stable 41 0 7
✅ fastify-stable 41 0 7
✅ hono-stable 41 0 7
✅ nextjs-turbopack-stable 45 0 3
✅ nextjs-webpack-stable 45 0 3
✅ nitro-stable 41 0 7
✅ nuxt-stable 41 0 7
✅ sveltekit-stable 41 0 7
✅ vite-stable 41 0 7
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 41 0 7
✅ express-stable 41 0 7
✅ fastify-stable 41 0 7
✅ hono-stable 41 0 7
✅ nextjs-turbopack-stable 45 0 3
✅ nextjs-webpack-stable 45 0 3
✅ nitro-stable 41 0 7
✅ nuxt-stable 41 0 7
✅ sveltekit-stable 41 0 7
✅ vite-stable 41 0 7
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 45 0 3
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 3 0 0
❌ mongodb 44 1 3
✅ redis-dev 3 0 0
✅ redis 45 0 3
✅ turso-dev 3 0 0
❌ turso 4 41 3
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 41 0 7
✅ e2e-local-postgres-nest-stable 41 0 7
✅ e2e-local-prod-nest-stable 41 0 7

📋 View full workflow run

@vercel
Copy link
Contributor

vercel bot commented Feb 8, 2026

@changeset-bot
Copy link

changeset-bot bot commented Feb 8, 2026

🦋 Changeset detected

Latest commit: fc25b9c

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

This PR includes changesets to release 19 packages
Name Type
@workflow/core Patch
@workflow/world Patch
@workflow/cli Patch
@workflow/web Patch
@workflow/world-testing Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/web-shared Patch
workflow Patch
@workflow/world-local Patch
@workflow/world-postgres Patch
@workflow/world-vercel Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

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

Copy link
Member Author

TooTallNate commented Feb 8, 2026

Copy link
Collaborator

@pranaygp pranaygp left a comment

Choose a reason for hiding this comment

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

Review: PR #979 - Add Encryptor interface and thread through serialization layer

Summary: Adds the Encryptor, EncryptionContext, and KeyMaterial interfaces to @workflow/world, makes World extend Encryptor, and threads the encryptor parameter through all serialization functions. This is a no-op refactor -- the encryptor parameter is unused (_encryptor) throughout.

Strengths:

  • Clean interface design: Encryptor has all-optional methods, so existing World implementations don't break
  • EncryptionContext is minimal (just runId) -- good for forward compatibility
  • KeyMaterial interface for o11y tooling is a thoughtful addition
  • The getEncryptorForRun() method on World is a well-designed escape hatch for cross-deployment encryption (e.g., resumeHook() from newer deployment)
  • getHookByTokenWithEncryptor() resolves the encryptor once and reuses it -- avoids redundant key resolution

Concerns:

  1. resolveEncryptorForRun type safety: In resume-hook.ts line 29-31, getEncryptorForRun is accessed via (world as any).getEncryptorForRun. Since World already extends Encryptor and getEncryptorForRun is defined on World, you should be able to use optional chaining directly: world.getEncryptorForRun?.(runId). The 'getEncryptorForRun' in world + as any pattern bypasses type checking unnecessarily.

  2. Serialization parameter ordering: The PR reorders parameters in the dehydrate/hydrate functions. For example, dehydrateWorkflowArguments goes from (value, ops, runId, ...) to (value, runId, encryptor, ops, ...). This is a breaking change to the internal API. While these aren't public, any external code calling these directly would break. The reorder makes sense semantically (runId + encryptor are conceptually paired), but consider documenting this in the changeset.

  3. _encryptor unused parameter pattern: All 8 functions have _encryptor: Encryptor that is unused. This is expected since the actual wiring happens in #957. However, this means if #979 lands but #957 doesn't (or is delayed), there's dead parameter threading throughout the codebase. A minor code smell but acceptable for a PR stack.

  4. hydrateResourceIO now requires encryptor: In observability.ts, hydrateResourceIO now takes an Encryptor parameter, and all callers pass world. This means the observability layer now has a dependency on the World instance. Previously it was a pure data transformation. This is a reasonable tradeoff for encryption support, but worth noting the coupling increase.

Overall, well-structured interface design. The cross-deployment encryption support via getEncryptorForRun shows good foresight for production scenarios.

@TooTallNate TooTallNate marked this pull request as ready for review February 9, 2026 23:12
Copilot AI review requested due to automatic review settings February 9, 2026 23:12
@TooTallNate TooTallNate force-pushed the nate/encryptor-interface branch from 30fc9e5 to fc25b9c Compare February 11, 2026 05:23
Sign up for free to 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.

2 participants