From 2d8e9fbdefb32af7f45c522616b3e628374bbf87 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Sun, 8 Feb 2026 11:58:34 -0800 Subject: [PATCH 1/4] Replace Encryptor interface with getEncryptionKeyForRun on World --- .changeset/encryptor-interface.md | 13 + packages/cli/src/lib/inspect/hydration.ts | 8 +- packages/cli/src/lib/inspect/output.ts | 43 +- packages/core/src/private.ts | 2 + packages/core/src/runtime.ts | 6 +- packages/core/src/runtime/resume-hook.ts | 63 +- packages/core/src/runtime/run.ts | 9 +- packages/core/src/runtime/runs.ts | 8 +- packages/core/src/runtime/start.ts | 8 +- packages/core/src/runtime/step-handler.ts | 14 +- .../core/src/runtime/suspension-handler.ts | 7 + packages/core/src/serialization.test.ts | 550 +++++++-- packages/core/src/serialization.ts | 23 +- packages/core/src/step.test.ts | 12 +- packages/core/src/step.ts | 4 +- packages/core/src/workflow.test.ts | 1040 +++++++++++++---- packages/core/src/workflow.ts | 14 +- packages/core/src/workflow/hook.test.ts | 22 +- packages/core/src/workflow/hook.ts | 14 +- packages/world-testing/src/addition.mts | 6 +- packages/world-testing/src/errors.mts | 6 +- packages/world-testing/src/hooks.mts | 6 +- packages/world-testing/src/idempotency.mts | 6 +- packages/world-testing/src/null-byte.mts | 6 +- packages/world/src/interfaces.ts | 21 + 25 files changed, 1525 insertions(+), 386 deletions(-) create mode 100644 .changeset/encryptor-interface.md diff --git a/.changeset/encryptor-interface.md b/.changeset/encryptor-interface.md new file mode 100644 index 0000000000..64dc0fa0c3 --- /dev/null +++ b/.changeset/encryptor-interface.md @@ -0,0 +1,13 @@ +--- +"@workflow/core": patch +"@workflow/world": patch +"@workflow/cli": patch +"@workflow/web": patch +"@workflow/world-testing": patch +--- + +Add encryption key interface and thread through serialization layer + +Adds `World.getEncryptionKeyForRun()` to `@workflow/world` — the World retrieves and derives the per-run AES-256 encryption key. The 8 dehydrate/hydrate serialization functions now accept an optional `key: Uint8Array | undefined` parameter for future encryption support. Restructures `resumeHook` to resolve the encryption key once and reuse for both metadata decryption and payload encryption. + +**Breaking change (internal API):** The dehydrate/hydrate function parameter order has changed. For example, `dehydrateWorkflowArguments(value, ops, runId, ...)` is now `dehydrateWorkflowArguments(value, runId, key, ops, ...)`. The `runId` and `key` parameters are now grouped together as the 2nd and 3rd arguments across all 8 functions. These are internal APIs not intended for external consumption. diff --git a/packages/cli/src/lib/inspect/hydration.ts b/packages/cli/src/lib/inspect/hydration.ts index 7e1dbde6f4..c2d6dbea11 100644 --- a/packages/cli/src/lib/inspect/hydration.ts +++ b/packages/cli/src/lib/inspect/hydration.ts @@ -130,7 +130,13 @@ function getRevivers(): Revivers { /** * Hydrate the serialized data fields of a resource for CLI display. + * + * The optional `_encryptorResolver` parameter is accepted for forward + * compatibility with encryption support but is not yet used. */ -export function hydrateResourceIO(resource: T): T { +export function hydrateResourceIO( + resource: T, + _encryptorResolver?: unknown +): T { return hydrateResourceIOGeneric(resource as any, getRevivers()) as T; } diff --git a/packages/cli/src/lib/inspect/output.ts b/packages/cli/src/lib/inspect/output.ts index e52eeb7157..bc620d1877 100644 --- a/packages/cli/src/lib/inspect/output.ts +++ b/packages/cli/src/lib/inspect/output.ts @@ -16,6 +16,18 @@ import type { World, } from '@workflow/world'; import chalk from 'chalk'; + +/** A function that resolves an encryption key for a given runId. */ +export type EncryptionKeyResolver = + | ((runId: string) => Promise) + | null; + +/** Create an EncryptionKeyResolver from a World instance */ +function createResolver(world: World): EncryptionKeyResolver { + if (!world.getEncryptionKeyForRun) return null; + return (runId: string) => world.getEncryptionKeyForRun!(runId); +} + import { formatDistance } from 'date-fns'; import Table from 'easy-table'; import { logger } from '../config/log.js'; @@ -507,6 +519,7 @@ const inlineFormatIO = (io: T, topLevel: boolean = true): string => { }; export const listRuns = async (world: World, opts: InspectCLIOptions = {}) => { + const resolveKey = createResolver(world); if (opts.stepId || opts.runId) { logger.warn( 'Filtering by step-id or run-id is not supported in list calls, ignoring filter.' @@ -535,7 +548,7 @@ export const listRuns = async (world: World, opts: InspectCLIOptions = {}) => { resolveData, }); const runsWithHydratedIO = await Promise.all( - runs.data.map(hydrateResourceIO) + runs.data.map((r) => hydrateResourceIO(r, resolveKey)) ); showJson({ ...runs, data: runsWithHydratedIO }); return; @@ -574,7 +587,9 @@ export const listRuns = async (world: World, opts: InspectCLIOptions = {}) => { } }, displayPage: async (runs) => { - const runsWithHydratedIO = await Promise.all(runs.map(hydrateResourceIO)); + const runsWithHydratedIO = await Promise.all( + runs.map((r) => hydrateResourceIO(r, resolveKey)) + ); logger.log(showTable(runsWithHydratedIO, props, opts)); }, }); @@ -584,13 +599,16 @@ export const getRecentRun = async ( world: World, opts: InspectCLIOptions = {} ) => { + const resolveKey = createResolver(world); logger.warn(`No runId provided, fetching data for latest run instead.`); try { const runs = await world.runs.list({ pagination: { limit: 1, sortOrder: opts.sort || 'desc' }, resolveData: 'none', // Don't need data for just getting the ID }); - runs.data = await Promise.all(runs.data.map(hydrateResourceIO)); + runs.data = await Promise.all( + runs.data.map((r) => hydrateResourceIO(r, resolveKey)) + ); return runs.data[0]; } catch (error) { if (handleApiError(error, opts.backend)) { @@ -605,12 +623,13 @@ export const showRun = async ( runId: string, opts: InspectCLIOptions = {} ) => { + const resolveKey = createResolver(world); if (opts.withData) { logger.warn('`withData` flag is ignored when showing individual resources'); } try { const run = await world.runs.get(runId, { resolveData: 'all' }); - const runWithHydratedIO = await hydrateResourceIO(run); + const runWithHydratedIO = await hydrateResourceIO(run, resolveKey); if (opts.json) { showJson(runWithHydratedIO); return; @@ -636,6 +655,7 @@ export const listSteps = async ( runId: undefined, } ) => { + const resolveKey = createResolver(world); if (opts.stepId) { logger.warn( 'Filtering by step-id is not supported in list calls, ignoring filter.' @@ -714,7 +734,7 @@ export const listSteps = async ( }, displayPage: async (steps) => { const stepsWithHydratedIO = await Promise.all( - steps.map(hydrateResourceIO) + steps.map((s) => hydrateResourceIO(s, resolveKey)) ); logger.log(showTable(stepsWithHydratedIO, props, opts)); showInspectInfoBox('step'); @@ -727,6 +747,7 @@ export const showStep = async ( stepId: string, opts: InspectCLIOptions = {} ) => { + const resolveKey = createResolver(world); if (opts.withData) { logger.warn('`withData` flag is ignored when showing individual resources'); } @@ -739,7 +760,7 @@ export const showStep = async ( const step = await world.steps.get(opts.runId, stepId, { resolveData: 'all', }); - const stepWithHydratedIO = await hydrateResourceIO(step); + const stepWithHydratedIO = await hydrateResourceIO(step, resolveKey); if (opts.json) { showJson(stepWithHydratedIO); return; @@ -923,6 +944,7 @@ export const listEvents = async ( }; export const listHooks = async (world: World, opts: InspectCLIOptions = {}) => { + const resolveKey = createResolver(world); if (opts.workflowName) { logger.warn( 'Filtering by workflow-name is not supported for hooks, ignoring filter.' @@ -955,7 +977,7 @@ export const listHooks = async (world: World, opts: InspectCLIOptions = {}) => { resolveData, }); const hydratedHooks = await Promise.all( - hooks.data.map(hydrateResourceIO) + hooks.data.map((h) => hydrateResourceIO(h, resolveKey)) ); showJson({ ...hooks, data: hydratedHooks }); return; @@ -1000,7 +1022,9 @@ export const listHooks = async (world: World, opts: InspectCLIOptions = {}) => { } }, displayPage: async (hooks) => { - const hydratedHooks = await Promise.all(hooks.map(hydrateResourceIO)); + const hydratedHooks = await Promise.all( + hooks.map((h) => hydrateResourceIO(h, resolveKey)) + ); logger.log(showTable(hydratedHooks, HOOK_LISTED_PROPS, opts)); showInspectInfoBox('hook'); }, @@ -1012,6 +1036,7 @@ export const showHook = async ( hookId: string, opts: InspectCLIOptions = {} ) => { + const resolveKey = createResolver(world); if (opts.withData) { logger.warn('`withData` flag is ignored when showing individual resources'); } @@ -1019,7 +1044,7 @@ export const showHook = async ( const hook = await world.hooks.get(hookId, { resolveData: 'all', }); - const hydratedHook = await hydrateResourceIO(hook); + const hydratedHook = await hydrateResourceIO(hook, resolveKey); if (opts.json) { showJson(hydratedHook); return; diff --git a/packages/core/src/private.ts b/packages/core/src/private.ts index ee6f7afbc4..99796dbb96 100644 --- a/packages/core/src/private.ts +++ b/packages/core/src/private.ts @@ -88,6 +88,8 @@ export function getStepFunction(stepId: string): StepFunction | undefined { export { __private_getClosureVars } from './step/get-closure-vars.js'; export interface WorkflowOrchestratorContext { + runId: string; + encryptionKey: Uint8Array | undefined; globalThis: typeof globalThis; eventsConsumer: EventsConsumer; /** diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index 653a9d2026..c06955752d 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -249,10 +249,14 @@ export function workflowEntrypoint( replaySpan?.setAttributes({ ...Attribute.WorkflowEventsCount(events.length), }); + // Resolve the encryption key for this run's deployment + const encryptionKey = + await world.getEncryptionKeyForRun?.(runId); return await runWorkflow( workflowCode, workflowRun, - events + events, + encryptionKey ); } ); diff --git a/packages/core/src/runtime/resume-hook.ts b/packages/core/src/runtime/resume-hook.ts index 1853a6f77f..dfe28071dd 100644 --- a/packages/core/src/runtime/resume-hook.ts +++ b/packages/core/src/runtime/resume-hook.ts @@ -18,22 +18,44 @@ import { getWorkflowQueueName } from './helpers.js'; import { getWorld } from './world.js'; /** - * Get the hook by token to find the associated workflow run, - * and hydrate the `metadata` property if it was set from within - * the workflow run. - * - * @param token - The unique token identifying the hook + * Resolve the encryption key for a given run ID. + * Returns undefined if the World doesn't support encryption. */ -export async function getHookByToken(token: string): Promise { +async function resolveEncryptionKeyForRun( + runId: string +): Promise { + const world = getWorld(); + return world.getEncryptionKeyForRun?.(runId); +} + +/** + * Internal helper that returns both the hook and the resolved encryption key. + */ +async function getHookByTokenWithKey( + token: string +): Promise<{ hook: Hook; encryptionKey: Uint8Array | undefined }> { const world = getWorld(); const hook = await world.hooks.getByToken(token); + const encryptionKey = await resolveEncryptionKeyForRun(hook.runId); if (typeof hook.metadata !== 'undefined') { hook.metadata = await hydrateStepArguments( hook.metadata as any, - [], - hook.runId + hook.runId, + encryptionKey ); } + return { hook, encryptionKey }; +} + +/** + * Get the hook by token to find the associated workflow run, + * and hydrate the `metadata` property if it was set from within + * the workflow run. + * + * @param token - The unique token identifying the hook + */ +export async function getHookByToken(token: string): Promise { + const { hook } = await getHookByTokenWithKey(token); return hook; } @@ -68,17 +90,25 @@ export async function getHookByToken(token: string): Promise { */ export async function resumeHook( tokenOrHook: string | Hook, - payload: T + payload: T, + _encryptionKey?: Uint8Array | undefined ): Promise { return await waitedUntil(() => { return trace('hook.resume', async (span) => { const world = getWorld(); try { - const hook = - typeof tokenOrHook === 'string' - ? await getHookByToken(tokenOrHook) - : tokenOrHook; + let hook: Hook; + let encryptionKey: Uint8Array | undefined; + if (typeof tokenOrHook === 'string') { + const result = await getHookByTokenWithKey(tokenOrHook); + hook = result.hook; + encryptionKey = _encryptionKey ?? result.encryptionKey; + } else { + hook = tokenOrHook; + encryptionKey = + _encryptionKey ?? (await resolveEncryptionKeyForRun(hook.runId)); + } span?.setAttributes({ ...Attribute.HookToken(hook.token), @@ -91,8 +121,9 @@ export async function resumeHook( const v1Compat = isLegacySpecVersion(hook.specVersion); const dehydratedPayload = await dehydrateStepReturnValue( payload, - ops, hook.runId, + encryptionKey, + ops, globalThis, v1Compat ); @@ -200,7 +231,7 @@ export async function resumeWebhook( token: string, request: Request ): Promise { - const hook = await getHookByToken(token); + const { hook, encryptionKey } = await getHookByTokenWithKey(token); let response: Response | undefined; let responseReadable: ReadableStream | undefined; @@ -229,7 +260,7 @@ export async function resumeWebhook( response = new Response(null, { status: 202 }); } - await resumeHook(hook, request); + await resumeHook(hook, request, encryptionKey); if (responseReadable) { // Wait for the readable stream to emit one chunk, diff --git a/packages/core/src/runtime/run.ts b/packages/core/src/runtime/run.ts index 9e5ad1aaf7..2ed8667c09 100644 --- a/packages/core/src/runtime/run.ts +++ b/packages/core/src/runtime/run.ts @@ -153,7 +153,14 @@ export class Run { const run = await this.world.runs.get(this.runId); if (run.status === 'completed') { - return await hydrateWorkflowReturnValue(run.output, [], this.runId); + const encryptionKey = await this.world.getEncryptionKeyForRun?.( + this.runId + ); + return await hydrateWorkflowReturnValue( + run.output, + this.runId, + encryptionKey + ); } if (run.status === 'cancelled') { diff --git a/packages/core/src/runtime/runs.ts b/packages/core/src/runtime/runs.ts index 17f3f83b33..9707ac72b5 100644 --- a/packages/core/src/runtime/runs.ts +++ b/packages/core/src/runtime/runs.ts @@ -49,8 +49,14 @@ export async function recreateRunFromExisting( ): Promise { try { const run = await world.runs.get(runId, { resolveData: 'all' }); + const encryptionKey = await world.getEncryptionKeyForRun?.(runId); const workflowArgs = normalizeWorkflowArgs( - await hydrateWorkflowArguments(run.input, globalThis) + await hydrateWorkflowArguments( + run.input, + runId, + encryptionKey, + globalThis + ) ); const specVersion = options.specVersion ?? run.specVersion ?? SPEC_VERSION_LEGACY; diff --git a/packages/core/src/runtime/start.ts b/packages/core/src/runtime/start.ts index 27353cf1ec..99b41ae2bb 100644 --- a/packages/core/src/runtime/start.ts +++ b/packages/core/src/runtime/start.ts @@ -117,12 +117,18 @@ export async function start( const specVersion = opts.specVersion ?? SPEC_VERSION_CURRENT; const v1Compat = isLegacySpecVersion(specVersion); + // Resolve encryption key for the new run. Since start() always runs on the + // current deployment, we can use a placeholder runId — the implementation + // will resolve to the local deployment's key regardless. + const encryptionKey = await world.getEncryptionKeyForRun?.(runId); + // Create run via run_created event (event-sourced architecture) // Pass client-generated runId - server will accept and use it const workflowArguments = await dehydrateWorkflowArguments( args, - ops, runId, + encryptionKey, + ops, globalThis, v1Compat ); diff --git a/packages/core/src/runtime/step-handler.ts b/packages/core/src/runtime/step-handler.ts index cf4ead2b83..7d83f5eb18 100644 --- a/packages/core/src/runtime/step-handler.ts +++ b/packages/core/src/runtime/step-handler.ts @@ -298,10 +298,13 @@ const stepHandler = getWorldHandlers().createQueueHandler( {}, async (hydrateSpan) => { const startTime = Date.now(); + const encryptionKey = + await world.getEncryptionKeyForRun?.(workflowRunId); const result = await hydrateStepArguments( step.input, - ops, - workflowRunId + workflowRunId, + encryptionKey, + ops ); const durationMs = Date.now() - startTime; hydrateSpan?.setAttributes({ @@ -354,10 +357,13 @@ const stepHandler = getWorldHandlers().createQueueHandler( {}, async (dehydrateSpan) => { const startTime = Date.now(); + const stepEncryptionKey = + await world.getEncryptionKeyForRun?.(workflowRunId); const dehydrated = await dehydrateStepReturnValue( result, - ops, - workflowRunId + workflowRunId, + stepEncryptionKey, + ops ); const durationMs = Date.now() - startTime; dehydrateSpan?.setAttributes({ diff --git a/packages/core/src/runtime/suspension-handler.ts b/packages/core/src/runtime/suspension-handler.ts index a6ca5b2f97..23c3611c67 100644 --- a/packages/core/src/runtime/suspension-handler.ts +++ b/packages/core/src/runtime/suspension-handler.ts @@ -77,6 +77,9 @@ export async function handleSuspension({ (item): item is WaitInvocationQueueItem => item.type === 'wait' ); + // Resolve encryption key for this run + const encryptionKey = await world.getEncryptionKeyForRun?.(runId); + // Build hook_created events (World will atomically create hook entities) const hookEvents: CreateEventRequest[] = await Promise.all( hookItems.map(async (queueItem) => { @@ -85,6 +88,8 @@ export async function handleSuspension({ ? undefined : ((await dehydrateStepArguments( queueItem.metadata, + runId, + encryptionKey, suspension.globalThis )) as SerializedData); return { @@ -161,6 +166,8 @@ export async function handleSuspension({ closureVars: queueItem.closureVars, thisVal: queueItem.thisVal, }, + runId, + encryptionKey, suspension.globalThis ); const stepEvent: CreateEventRequest = { diff --git a/packages/core/src/serialization.test.ts b/packages/core/src/serialization.test.ts index e91fd70585..186c113966 100644 --- a/packages/core/src/serialization.test.ts +++ b/packages/core/src/serialization.test.ts @@ -25,6 +25,7 @@ import { STABLE_ULID, STREAM_NAME_SYMBOL } from './symbols.js'; import { createContext } from './vm/index.js'; const mockRunId = 'wrun_mockidnumber0001'; +const noEncryptionKey = undefined; describe('getStreamType', () => { it('should return `undefined` for a regular stream', () => { @@ -52,7 +53,12 @@ describe('workflow arguments', () => { it('should work with Date', async () => { const date = new Date('2025-07-17T04:30:34.824Z'); - const serialized = await dehydrateWorkflowArguments(date, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + date, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ 100, @@ -101,7 +107,12 @@ describe('workflow arguments', () => { ] `); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); vmGlobalThis.val = hydrated; expect(runInContext('val instanceof Date', context)).toBe(true); @@ -110,7 +121,12 @@ describe('workflow arguments', () => { it('should work with invalid Date', async () => { const date = new Date('asdf'); - const serialized = await dehydrateWorkflowArguments(date, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + date, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ 100, @@ -136,7 +152,12 @@ describe('workflow arguments', () => { ] `); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); vmGlobalThis.val = hydrated; expect(runInContext('val instanceof Date', context)).toBe(true); @@ -145,7 +166,12 @@ describe('workflow arguments', () => { it('should work with BigInt', async () => { const bigInt = BigInt('9007199254740992'); - const serialized = await dehydrateWorkflowArguments(bigInt, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + bigInt, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ 100, @@ -188,14 +214,24 @@ describe('workflow arguments', () => { ] `); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); expect(hydrated).toBe(BigInt(9007199254740992)); expect(typeof hydrated).toBe('bigint'); }); it('should work with BigInt negative', async () => { const bigInt = BigInt('-12345678901234567890'); - const serialized = await dehydrateWorkflowArguments(bigInt, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + bigInt, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ 100, @@ -243,7 +279,12 @@ describe('workflow arguments', () => { ] `); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); expect(hydrated).toBe(BigInt('-12345678901234567890')); expect(typeof hydrated).toBe('bigint'); }); @@ -253,7 +294,12 @@ describe('workflow arguments', () => { [2, 'foo'], [6, 'bar'], ]); - const serialized = await dehydrateWorkflowArguments(map, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + map, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ 100, @@ -308,7 +354,12 @@ describe('workflow arguments', () => { ] `); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); vmGlobalThis.val = hydrated; expect(runInContext('val instanceof Map', context)).toBe(true); @@ -316,7 +367,12 @@ describe('workflow arguments', () => { it('should work with Set', async () => { const set = new Set([1, '2', true]); - const serialized = await dehydrateWorkflowArguments(set, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + set, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ 100, @@ -356,7 +412,12 @@ describe('workflow arguments', () => { ] `); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); vmGlobalThis.val = hydrated; expect(runInContext('val instanceof Set', context)).toBe(true); @@ -364,16 +425,26 @@ describe('workflow arguments', () => { it('should work with WritableStream', async () => { const stream = new WritableStream(); - const serialized = await dehydrateWorkflowArguments(stream, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + stream, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized instanceof Uint8Array).toBe(true); // Verify the serialized data contains WritableStream reference const serializedStr = new TextDecoder().decode(serialized); expect(serializedStr).toContain('WritableStream'); class OurWritableStream {} - const hydrated = await hydrateWorkflowArguments(serialized, { - WritableStream: OurWritableStream, - }); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + { + WritableStream: OurWritableStream, + } + ); expect(hydrated).toBeInstanceOf(OurWritableStream); const streamName = hydrated[STREAM_NAME_SYMBOL]; expect(streamName).toMatch(/^strm_[0-9A-Z]{26}$/); @@ -381,16 +452,26 @@ describe('workflow arguments', () => { it('should work with ReadableStream', async () => { const stream = new ReadableStream(); - const serialized = await dehydrateWorkflowArguments(stream, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + stream, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized instanceof Uint8Array).toBe(true); // Verify the serialized data contains ReadableStream reference const serializedStr = new TextDecoder().decode(serialized); expect(serializedStr).toContain('ReadableStream'); class OurReadableStream {} - const hydrated = await hydrateWorkflowArguments(serialized, { - ReadableStream: OurReadableStream, - }); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + { + ReadableStream: OurReadableStream, + } + ); expect(hydrated).toBeInstanceOf(OurReadableStream); const streamName = hydrated[STREAM_NAME_SYMBOL]; expect(streamName).toMatch(/^strm_[0-9A-Z]{26}$/); @@ -401,7 +482,12 @@ describe('workflow arguments', () => { headers.set('foo', 'bar'); headers.append('set-cookie', 'a'); headers.append('set-cookie', 'b'); - const serialized = await dehydrateWorkflowArguments(headers, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + headers, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ 100, @@ -485,7 +571,12 @@ describe('workflow arguments', () => { ] `); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); expect(hydrated).toBeInstanceOf(Headers); expect(hydrated.get('foo')).toEqual('bar'); expect(hydrated.get('set-cookie')).toEqual('a, b'); @@ -503,8 +594,9 @@ describe('workflow arguments', () => { }); const serialized = await dehydrateWorkflowArguments( response, - [], - mockRunId + mockRunId, + noEncryptionKey, + [] ); expect(serialized instanceof Uint8Array).toBe(true); // Verify the serialized data contains Response reference @@ -522,11 +614,16 @@ describe('workflow arguments', () => { } class OurReadableStream {} class OurHeaders {} - const hydrated = await hydrateWorkflowArguments(serialized, { - Headers: OurHeaders, - Response: OurResponse, - ReadableStream: OurReadableStream, - }); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + { + Headers: OurHeaders, + Response: OurResponse, + ReadableStream: OurReadableStream, + } + ); expect(hydrated).toBeInstanceOf(OurResponse); expect(hydrated.headers).toBeInstanceOf(OurHeaders); expect(hydrated.body).toBeInstanceOf(OurReadableStream); @@ -538,7 +635,12 @@ describe('workflow arguments', () => { it('should work with URLSearchParams', async () => { const params = new URLSearchParams('a=1&b=2&a=3'); - const serialized = await dehydrateWorkflowArguments(params, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + params, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ 100, @@ -585,7 +687,12 @@ describe('workflow arguments', () => { ] `); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); vmGlobalThis.val = hydrated; expect(runInContext('val instanceof URLSearchParams', context)).toBe(true); expect(hydrated.getAll('a')).toEqual(['1', '3']); @@ -601,7 +708,12 @@ describe('workflow arguments', () => { it('should work with empty URLSearchParams', async () => { const params = new URLSearchParams(); - const serialized = await dehydrateWorkflowArguments(params, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + params, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ 100, @@ -638,7 +750,12 @@ describe('workflow arguments', () => { ] `); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); vmGlobalThis.val = hydrated; expect(runInContext('val instanceof URLSearchParams', context)).toBe(true); expect(hydrated.toString()).toEqual(''); @@ -648,7 +765,12 @@ describe('workflow arguments', () => { it('should work with empty ArrayBuffer', async () => { const buffer = new ArrayBuffer(0); - const serialized = await dehydrateWorkflowArguments(buffer, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + buffer, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ 100, @@ -681,7 +803,12 @@ describe('workflow arguments', () => { ] `); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); vmGlobalThis.val = hydrated; expect(runInContext('val instanceof ArrayBuffer', context)).toBe(true); expect(hydrated.byteLength).toEqual(0); @@ -690,7 +817,12 @@ describe('workflow arguments', () => { it('should work with empty Uint8Array', async () => { const array = new Uint8Array(0); - const serialized = await dehydrateWorkflowArguments(array, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + array, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ 100, @@ -722,7 +854,12 @@ describe('workflow arguments', () => { ] `); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); vmGlobalThis.val = hydrated; expect(runInContext('val instanceof Uint8Array', context)).toBe(true); expect(hydrated.length).toEqual(0); @@ -732,7 +869,12 @@ describe('workflow arguments', () => { it('should work with empty Int32Array', async () => { const array = new Int32Array(0); - const serialized = await dehydrateWorkflowArguments(array, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + array, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ 100, @@ -764,7 +906,12 @@ describe('workflow arguments', () => { ] `); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); vmGlobalThis.val = hydrated; expect(runInContext('val instanceof Int32Array', context)).toBe(true); expect(hydrated.length).toEqual(0); @@ -774,7 +921,12 @@ describe('workflow arguments', () => { it('should work with empty Float64Array', async () => { const array = new Float64Array(0); - const serialized = await dehydrateWorkflowArguments(array, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + array, + mockRunId, + noEncryptionKey, + [] + ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ 100, @@ -808,7 +960,12 @@ describe('workflow arguments', () => { ] `); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); vmGlobalThis.val = hydrated; expect(runInContext('val instanceof Float64Array', context)).toBe(true); expect(hydrated.length).toEqual(0); @@ -833,8 +990,9 @@ describe('workflow arguments', () => { const serialized = await dehydrateWorkflowArguments( request, - [], - mockRunId + mockRunId, + noEncryptionKey, + [] ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ @@ -1144,11 +1302,16 @@ describe('workflow arguments', () => { } class OurReadableStream {} class OurHeaders {} - const hydrated = await hydrateWorkflowArguments(serialized, { - Request: OurRequest, - Headers: OurHeaders, - ReadableStream: OurReadableStream, - }); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + { + Request: OurRequest, + Headers: OurHeaders, + ReadableStream: OurReadableStream, + } + ); expect(hydrated).toBeInstanceOf(OurRequest); expect(hydrated.method).toBe('POST'); expect(hydrated.url).toBe('https://example.com/api'); @@ -1186,8 +1349,9 @@ describe('workflow arguments', () => { const serialized = await dehydrateWorkflowArguments( request, - [], - mockRunId + mockRunId, + noEncryptionKey, + [] ); expect(serialized).toMatchInlineSnapshot(` Uint8Array [ @@ -1564,12 +1728,17 @@ describe('workflow arguments', () => { class OurReadableStream {} class OurWritableStream {} class OurHeaders {} - const hydrated = await hydrateWorkflowArguments(serialized, { - Request: OurRequest, - Headers: OurHeaders, - ReadableStream: OurReadableStream, - WritableStream: OurWritableStream, - }); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + { + Request: OurRequest, + Headers: OurHeaders, + ReadableStream: OurReadableStream, + WritableStream: OurWritableStream, + } + ); expect(hydrated).toBeInstanceOf(OurRequest); expect(hydrated.method).toBe('POST'); expect(hydrated.url).toBe('https://example.com/webhook'); @@ -1595,7 +1764,12 @@ describe('workflow arguments', () => { class Foo {} let err: WorkflowRuntimeError | undefined; try { - await dehydrateWorkflowArguments(new Foo(), [], mockRunId); + await dehydrateWorkflowArguments( + new Foo(), + mockRunId, + noEncryptionKey, + [] + ); } catch (err_) { err = err_ as WorkflowRuntimeError; } @@ -1611,7 +1785,7 @@ describe('workflow return value', () => { class Foo {} let err: WorkflowRuntimeError | undefined; try { - await dehydrateWorkflowReturnValue(new Foo()); + await dehydrateWorkflowReturnValue(new Foo(), mockRunId, noEncryptionKey); } catch (err_) { err = err_ as WorkflowRuntimeError; } @@ -1627,7 +1801,12 @@ describe('step arguments', () => { class Foo {} let err: WorkflowRuntimeError | undefined; try { - await dehydrateStepArguments(new Foo(), globalThis); + await dehydrateStepArguments( + new Foo(), + mockRunId, + noEncryptionKey, + globalThis + ); } catch (err_) { err = err_ as WorkflowRuntimeError; } @@ -1643,7 +1822,7 @@ describe('step return value', () => { class Foo {} let err: WorkflowRuntimeError | undefined; try { - await dehydrateStepReturnValue(new Foo(), [], mockRunId); + await dehydrateStepReturnValue(new Foo(), mockRunId, noEncryptionKey, []); } catch (err_) { err = err_ as WorkflowRuntimeError; } @@ -1718,14 +1897,20 @@ describe('step function serialization', () => { }); // Serialize using workflow reducers (which handle StepFunction) - const dehydrated = await dehydrateStepArguments([fnWithStepId], globalThis); + const dehydrated = await dehydrateStepArguments( + [fnWithStepId], + mockRunId, + noEncryptionKey, + globalThis + ); // Hydrate it back using step revivers const ops: Promise[] = []; const hydrated = await hydrateStepArguments( dehydrated, - ops, mockRunId, + noEncryptionKey, + ops, globalThis ); @@ -1800,6 +1985,8 @@ describe('step function serialization', () => { // Serialize the step function reference const dehydrated = await dehydrateStepArguments( [fnWithNonExistentStepId], + mockRunId, + noEncryptionKey, globalThis ); @@ -1807,7 +1994,13 @@ describe('step function serialization', () => { const ops: Promise[] = []; let err: Error | undefined; try { - await hydrateStepArguments(dehydrated, ops, mockRunId, globalThis); + await hydrateStepArguments( + dehydrated, + mockRunId, + noEncryptionKey, + ops, + globalThis + ); } catch (err_) { err = err_ as Error; } @@ -1837,7 +2030,12 @@ describe('step function serialization', () => { const args = [stepFn, 42]; // This should serialize the step function by its name using the reducer - const dehydrated = await dehydrateStepArguments(args, globalThis); + const dehydrated = await dehydrateStepArguments( + args, + mockRunId, + noEncryptionKey, + globalThis + ); // Verify it dehydrated successfully expect(dehydrated).toBeDefined(); @@ -1883,7 +2081,12 @@ describe('step function serialization', () => { // Serialize the step function with closure variables const args = [stepFn, 7]; - const dehydrated = await dehydrateStepArguments(args, globalThis); + const dehydrated = await dehydrateStepArguments( + args, + mockRunId, + noEncryptionKey, + globalThis + ); // Verify it serialized expect(dehydrated).toBeDefined(); @@ -1896,8 +2099,9 @@ describe('step function serialization', () => { // Now hydrate it back const hydrated = await hydrateStepArguments( dehydrated, - [], 'test-run-123', + noEncryptionKey, + [], vmGlobalThis ); expect(Array.isArray(hydrated)).toBe(true); @@ -1986,13 +2190,19 @@ describe('step function serialization', () => { const ops: Promise[] = []; const dehydrated = await dehydrateWorkflowArguments( [clientStepFn, 3, 5], - ops, mockRunId, + noEncryptionKey, + ops, globalThis ); // Hydrate in workflow context using VM's globalThis - const hydrated = await hydrateWorkflowArguments(dehydrated, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + dehydrated, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); // Verify the hydrated result expect(Array.isArray(hydrated)).toBe(true); @@ -2031,14 +2241,20 @@ describe('step function serialization', () => { const ops: Promise[] = []; const dehydrated = await dehydrateWorkflowArguments( [clientStepFn], - ops, mockRunId, + noEncryptionKey, + ops, globalThis ); // Hydrating should throw because WORKFLOW_USE_STEP is not set await expect( - hydrateWorkflowArguments(dehydrated, vmGlobalThis) + hydrateWorkflowArguments( + dehydrated, + mockRunId, + noEncryptionKey, + vmGlobalThis + ) ).rejects.toThrow('WORKFLOW_USE_STEP not found on global object'); }); }); @@ -2124,7 +2340,12 @@ describe('custom class serialization', () => { ); const point = new Point(10, 20); - const serialized = await dehydrateWorkflowArguments(point, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + point, + mockRunId, + noEncryptionKey, + [] + ); // Verify it serialized with the Instance type expect(serialized).toBeDefined(); @@ -2134,7 +2355,12 @@ describe('custom class serialization', () => { expect(serializedStr).toContain('test/Point'); // Hydrate it back (inside the VM context) - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); // Note: hydrated is an instance of the VM's Point class, not the host's // so we check constructor.name instead of instanceof expect(hydrated.constructor.name).toBe('Point'); @@ -2194,8 +2420,18 @@ describe('custom class serialization', () => { }, }; - const serialized = await dehydrateWorkflowArguments(data, [], mockRunId); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const serialized = await dehydrateWorkflowArguments( + data, + mockRunId, + noEncryptionKey, + [] + ); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); expect(hydrated.name).toBe('test'); expect(hydrated.vector.constructor.name).toBe('Vector'); @@ -2248,8 +2484,18 @@ describe('custom class serialization', () => { const items = [new Item('a'), new Item('b'), new Item('c')]; - const serialized = await dehydrateWorkflowArguments(items, [], mockRunId); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const serialized = await dehydrateWorkflowArguments( + items, + mockRunId, + noEncryptionKey, + [] + ); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); expect(Array.isArray(hydrated)).toBe(true); expect(hydrated).toHaveLength(3); @@ -2284,11 +2530,17 @@ describe('custom class serialization', () => { registerSerializationClass('test/Config', Config); const config = new Config('maxRetries', 3); - const serialized = await dehydrateStepArguments([config], globalThis); + const serialized = await dehydrateStepArguments( + [config], + mockRunId, + noEncryptionKey, + globalThis + ); const hydrated = await hydrateStepArguments( serialized, - [], mockRunId, + noEncryptionKey, + [], globalThis ); @@ -2321,9 +2573,19 @@ describe('custom class serialization', () => { registerSerializationClass('test/Result', Result); const result = new Result(true, 'completed'); - const serialized = await dehydrateStepReturnValue(result, [], mockRunId); + const serialized = await dehydrateStepReturnValue( + result, + mockRunId, + noEncryptionKey, + [] + ); // Step return values are hydrated with workflow revivers - const hydrated = await hydrateWorkflowArguments(serialized, globalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + globalThis + ); expect(hydrated).toBeInstanceOf(Result); expect(hydrated.success).toBe(true); @@ -2339,7 +2601,7 @@ describe('custom class serialization', () => { // Should throw because PlainClass is not serializable await expect( - dehydrateWorkflowArguments(instance, [], mockRunId) + dehydrateWorkflowArguments(instance, mockRunId, noEncryptionKey, []) ).rejects.toThrow(); }); @@ -2362,7 +2624,12 @@ describe('custom class serialization', () => { // Should throw with our specific error message about missing classId let errorMessage = ''; try { - await dehydrateWorkflowArguments(instance, [], mockRunId); + await dehydrateWorkflowArguments( + instance, + mockRunId, + noEncryptionKey, + [] + ); } catch (e: any) { errorMessage = e.cause?.message || e.message; } @@ -2401,8 +2668,18 @@ describe('custom class serialization', () => { const date = new Date('2025-01-01T00:00:00.000Z'); const complex = new ComplexData(map, date); - const serialized = await dehydrateWorkflowArguments(complex, [], mockRunId); - const hydrated = await hydrateWorkflowArguments(serialized, globalThis); + const serialized = await dehydrateWorkflowArguments( + complex, + mockRunId, + noEncryptionKey, + [] + ); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + globalThis + ); expect(hydrated).toBeInstanceOf(ComplexData); expect(hydrated.items).toBeInstanceOf(Map); @@ -2453,13 +2730,23 @@ describe('custom class serialization', () => { // Serialize an instance - this should increment serializedCount via `this` const counter = new Counter(42); - const serialized = await dehydrateWorkflowArguments(counter, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + counter, + mockRunId, + noEncryptionKey, + [] + ); // Verify serialization used `this` correctly expect(Counter.serializedCount).toBe(1); // Deserialize - this should increment deserializedCount via `this` - const hydrated = await hydrateWorkflowArguments(serialized, globalThis); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + globalThis + ); // Verify deserialization used `this` correctly expect(Counter.deserializedCount).toBe(1); @@ -2468,7 +2755,7 @@ describe('custom class serialization', () => { // Serialize another instance to verify counter increments const counter2 = new Counter(100); - await dehydrateWorkflowArguments(counter2, [], mockRunId); + await dehydrateWorkflowArguments(counter2, mockRunId, noEncryptionKey, []); expect(Counter.serializedCount).toBe(2); }); }); @@ -2481,7 +2768,12 @@ describe('format prefix system', () => { it('should encode data with format prefix', async () => { const data = { message: 'hello' }; - const serialized = await dehydrateWorkflowArguments(data, [], mockRunId); + const serialized = await dehydrateWorkflowArguments( + data, + mockRunId, + noEncryptionKey, + [] + ); // Check that the first 4 bytes are the format prefix "devl" const prefix = new TextDecoder().decode(serialized.subarray(0, 4)); @@ -2490,8 +2782,18 @@ describe('format prefix system', () => { it('should decode prefixed data correctly', async () => { const data = { message: 'hello', count: 42 }; - const serialized = await dehydrateWorkflowArguments(data, [], mockRunId); - const hydrated = await hydrateWorkflowArguments(serialized, vmGlobalThis); + const serialized = await dehydrateWorkflowArguments( + data, + mockRunId, + noEncryptionKey, + [] + ); + const hydrated = await hydrateWorkflowArguments( + serialized, + mockRunId, + noEncryptionKey, + vmGlobalThis + ); expect(hydrated).toEqual({ message: 'hello', count: 42 }); }); @@ -2502,17 +2804,25 @@ describe('format prefix system', () => { // Workflow arguments const workflowArgs = await dehydrateWorkflowArguments( testData, - [], - mockRunId + mockRunId, + noEncryptionKey, + [] ); expect(new TextDecoder().decode(workflowArgs.subarray(0, 4))).toBe('devl'); - expect(await hydrateWorkflowArguments(workflowArgs, vmGlobalThis)).toEqual( - testData - ); + expect( + await hydrateWorkflowArguments( + workflowArgs, + mockRunId, + noEncryptionKey, + vmGlobalThis + ) + ).toEqual(testData); // Workflow return value const workflowReturn = await dehydrateWorkflowReturnValue( testData, + mockRunId, + noEncryptionKey, globalThis ); expect(new TextDecoder().decode(workflowReturn.subarray(0, 4))).toBe( @@ -2521,25 +2831,47 @@ describe('format prefix system', () => { expect( await hydrateWorkflowReturnValue( workflowReturn, - [], mockRunId, + noEncryptionKey, + [], vmGlobalThis ) ).toEqual(testData); // Step arguments - const stepArgs = await dehydrateStepArguments(testData, globalThis); + const stepArgs = await dehydrateStepArguments( + testData, + mockRunId, + noEncryptionKey, + globalThis + ); expect(new TextDecoder().decode(stepArgs.subarray(0, 4))).toBe('devl'); expect( - await hydrateStepArguments(stepArgs, [], mockRunId, vmGlobalThis) + await hydrateStepArguments( + stepArgs, + mockRunId, + noEncryptionKey, + [], + vmGlobalThis + ) ).toEqual(testData); // Step return value - const stepReturn = await dehydrateStepReturnValue(testData, [], mockRunId); - expect(new TextDecoder().decode(stepReturn.subarray(0, 4))).toBe('devl'); - expect(await hydrateStepReturnValue(stepReturn, vmGlobalThis)).toEqual( - testData + const stepReturn = await dehydrateStepReturnValue( + testData, + mockRunId, + noEncryptionKey, + [] ); + expect(new TextDecoder().decode(stepReturn.subarray(0, 4))).toBe('devl'); + expect( + await hydrateStepReturnValue( + stepReturn, + mockRunId, + noEncryptionKey, + vmGlobalThis + ) + ).toEqual(testData); }); it('should throw error for unknown format prefix', async () => { @@ -2547,7 +2879,12 @@ describe('format prefix system', () => { const unknownFormat = new TextEncoder().encode('unkn{"test":true}'); await expect( - hydrateWorkflowArguments(unknownFormat, vmGlobalThis) + hydrateWorkflowArguments( + unknownFormat, + mockRunId, + noEncryptionKey, + vmGlobalThis + ) ).rejects.toThrow(/Unknown serialization format/); }); @@ -2555,7 +2892,12 @@ describe('format prefix system', () => { const tooShort = new TextEncoder().encode('dev'); await expect( - hydrateWorkflowArguments(tooShort, vmGlobalThis) + hydrateWorkflowArguments( + tooShort, + mockRunId, + noEncryptionKey, + vmGlobalThis + ) ).rejects.toThrow(/Data too short to contain format prefix/); }); }); diff --git a/packages/core/src/serialization.ts b/packages/core/src/serialization.ts index 9694915ea0..d5282490bb 100644 --- a/packages/core/src/serialization.ts +++ b/packages/core/src/serialization.ts @@ -1,5 +1,6 @@ import { WorkflowRuntimeError } from '@workflow/errors'; import { WORKFLOW_DESERIALIZE, WORKFLOW_SERIALIZE } from '@workflow/serde'; + import { DevalueError, parse, stringify, unflatten } from 'devalue'; import { monotonicFactory } from 'ulid'; import { getSerializationClass } from './class-serialization.js'; @@ -1377,8 +1378,9 @@ function getStepRevivers( */ export async function dehydrateWorkflowArguments( value: unknown, - ops: Promise[], runId: string, + _key: Uint8Array | undefined, + ops: Promise[] = [], global: Record = globalThis, v1Compat = false ): Promise { @@ -1408,6 +1410,8 @@ export async function dehydrateWorkflowArguments( */ export async function hydrateWorkflowArguments( value: Uint8Array | unknown, + _runId: string, + _key: Uint8Array | undefined, global: Record = globalThis, extraRevivers: Record any> = {} ) { @@ -1442,6 +1446,8 @@ export async function hydrateWorkflowArguments( */ export async function dehydrateWorkflowReturnValue( value: unknown, + _runId: string, + _key: Uint8Array | undefined, global: Record = globalThis, v1Compat = false ): Promise { @@ -1474,8 +1480,9 @@ export async function dehydrateWorkflowReturnValue( */ export async function hydrateWorkflowReturnValue( value: Uint8Array | unknown, - ops: Promise[], runId: string, + _key: Uint8Array | undefined, + ops: Promise[] = [], global: Record = globalThis, extraRevivers: Record any> = {} ) { @@ -1511,7 +1518,9 @@ export async function hydrateWorkflowReturnValue( */ export async function dehydrateStepArguments( value: unknown, - global: Record, + _runId: string, + _key: Uint8Array | undefined, + global: Record = globalThis, v1Compat = false ): Promise { try { @@ -1542,8 +1551,9 @@ export async function dehydrateStepArguments( */ export async function hydrateStepArguments( value: Uint8Array | unknown, - ops: Promise[], runId: string, + _key: Uint8Array | undefined, + ops: Promise[] = [], global: Record = globalThis, extraRevivers: Record any> = {} ) { @@ -1581,8 +1591,9 @@ export async function hydrateStepArguments( */ export async function dehydrateStepReturnValue( value: unknown, - ops: Promise[], runId: string, + _key: Uint8Array | undefined, + ops: Promise[] = [], global: Record = globalThis, v1Compat = false ): Promise { @@ -1612,6 +1623,8 @@ export async function dehydrateStepReturnValue( */ export async function hydrateStepReturnValue( value: Uint8Array | unknown, + _runId: string, + _key: Uint8Array | undefined, global: Record = globalThis, extraRevivers: Record any> = {} ) { diff --git a/packages/core/src/step.test.ts b/packages/core/src/step.test.ts index 834e391588..23bea043f7 100644 --- a/packages/core/src/step.test.ts +++ b/packages/core/src/step.test.ts @@ -19,6 +19,8 @@ function setupWorkflowContext(events: Event[]): WorkflowOrchestratorContext { const ulid = monotonicFactory(() => context.globalThis.Math.random()); const workflowStartedAt = context.globalThis.Date.now(); return { + runId: 'wrun_test', + encryptionKey: undefined, globalThis: context.globalThis, eventsConsumer: new EventsConsumer(events, { onUnconsumedEvent: () => {}, @@ -41,7 +43,7 @@ describe('createUseStep', () => { eventType: 'step_completed', correlationId: 'step_01K11TFZ62YS0YYFDQ3E8B9YCV', eventData: { - result: await dehydrateStepReturnValue(3, [], 'wrun_test'), + result: await dehydrateStepReturnValue(3, 'wrun_test', undefined), }, createdAt: new Date(), }, @@ -192,7 +194,11 @@ describe('createUseStep', () => { eventType: 'step_completed', correlationId: 'step_01K11TFZ62YS0YYFDQ3E8B9YCV', eventData: { - result: await dehydrateStepReturnValue(undefined, [], 'wrun_test'), + result: await dehydrateStepReturnValue( + undefined, + 'wrun_test', + undefined + ), }, createdAt: new Date(), }, @@ -411,7 +417,7 @@ describe('createUseStep', () => { eventType: 'step_completed', correlationId: 'step_01K11TFZ62YS0YYFDQ3E8B9YCV', eventData: { - result: await dehydrateStepReturnValue(42, [], 'wrun_test'), + result: await dehydrateStepReturnValue(42, 'wrun_test', undefined), }, createdAt: new Date(), }, diff --git a/packages/core/src/step.ts b/packages/core/src/step.ts index 1f5eda6cb9..b31d211ce0 100644 --- a/packages/core/src/step.ts +++ b/packages/core/src/step.ts @@ -151,9 +151,11 @@ export function createUseStep(ctx: WorkflowOrchestratorContext) { try { const hydratedResult = await hydrateStepReturnValue( event.eventData.result, + ctx.runId, + ctx.encryptionKey, ctx.globalThis ); - resolve(hydratedResult); + resolve(hydratedResult as Result); } catch (error) { reject(error); } diff --git a/packages/core/src/workflow.test.ts b/packages/core/src/workflow.test.ts index 13b31a3d40..158f70f1f0 100644 --- a/packages/core/src/workflow.test.ts +++ b/packages/core/src/workflow.test.ts @@ -10,6 +10,9 @@ import { } from './serialization.js'; import { runWorkflow } from './workflow.js'; +// No encryption key = encryption disabled +const noEncryptionKey = undefined; + describe('runWorkflow', () => { const getWorkflowTransformCode = (workflowName?: string) => `;globalThis.__private_workflows = new Map(); @@ -31,7 +34,12 @@ describe('runWorkflow', () => { runId: 'wrun_123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -40,9 +48,19 @@ describe('runWorkflow', () => { const events: Event[] = []; - const result = await runWorkflow(workflowCode, workflowRun, events); + const result = await runWorkflow( + workflowCode, + workflowRun, + events, + noEncryptionKey + ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual('success'); }); @@ -54,7 +72,12 @@ describe('runWorkflow', () => { runId: 'wrun_123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([1, 2], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [1, 2], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -63,9 +86,19 @@ describe('runWorkflow', () => { const events: Event[] = []; - const result = await runWorkflow(workflowCode, workflowRun, events); + const result = await runWorkflow( + workflowCode, + workflowRun, + events, + noEncryptionKey + ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual(3); }); @@ -83,7 +116,12 @@ describe('runWorkflow', () => { runId: 'wrun_123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -93,9 +131,15 @@ describe('runWorkflow', () => { const events: Event[] = []; const result = await hydrateWorkflowReturnValue( - (await runWorkflow(workflowCode, workflowRun, events)) as any, - ops, - 'wrun_test' + (await runWorkflow( + workflowCode, + workflowRun, + events, + noEncryptionKey + )) as any, + 'wrun_test', + noEncryptionKey, + ops ); assert(types.isNativeError(result)); expect(result.name).toEqual('TypeError'); @@ -110,7 +154,12 @@ describe('runWorkflow', () => { runId: workflowRunId, workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -131,7 +180,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X00Y11PCQTCHQRK34HF', eventData: { - result: await dehydrateStepReturnValue(3, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 3, + 'wrun_test', + noEncryptionKey, + ops + ), }, createdAt: new Date(), }, @@ -145,10 +199,16 @@ describe('runWorkflow', () => { return a; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual(3); }); @@ -160,7 +220,12 @@ describe('runWorkflow', () => { runId: workflowRunId, workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -201,7 +266,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X00Y11PCQTCHQRK34HF', eventData: { - result: await dehydrateStepReturnValue(3, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 3, + 'wrun_test', + noEncryptionKey, + ops + ), }, createdAt: new Date('2024-01-01T00:00:02.000Z'), }, @@ -225,7 +295,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X00Y11PCQTCHQRK34HG', eventData: { - result: await dehydrateStepReturnValue(3, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 3, + 'wrun_test', + noEncryptionKey, + ops + ), }, createdAt: new Date('2024-01-01T00:00:04.000Z'), }, @@ -249,7 +324,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X00Y11PCQTCHQRK34HH', eventData: { - result: await dehydrateStepReturnValue(3, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 3, + 'wrun_test', + noEncryptionKey, + ops + ), }, createdAt: new Date('2024-01-01T00:00:06.000Z'), }, @@ -269,7 +349,8 @@ describe('runWorkflow', () => { return timestamps; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); // Timestamps: // - Initial: 0s (from startedAt) @@ -277,7 +358,12 @@ describe('runWorkflow', () => { // - After step 2 completes (at 4s), timestamp advances to step3_created (4.5s) // - After step 3 completes: 6s expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual([ new Date('2024-01-01T00:00:00.000Z'), 1704067202500, // 2.5s (step2_created timestamp) @@ -296,7 +382,12 @@ describe('runWorkflow', () => { runId: workflowRunId, workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -341,30 +432,42 @@ describe('runWorkflow', () => { }${getWorkflowTransformCode('workflow')}`; // Execute the workflow with only sleep(1) resolved - const result1 = await runWorkflow(workflowCode, workflowRun, events); + const result1 = await runWorkflow( + workflowCode, + workflowRun, + events, + noEncryptionKey + ); // Execute again with both sleeps resolved this time - const result2 = await runWorkflow(workflowCode, workflowRun, [ - ...events, - { - eventId: 'event-3', - runId: workflowRunId, - eventType: 'wait_completed', - correlationId: 'wait_01HK153X008RT6YEW43G8QX6JY', - createdAt: new Date('2024-01-01T00:00:04.000Z'), - }, - ]); + const result2 = await runWorkflow( + workflowCode, + workflowRun, + [ + ...events, + { + eventId: 'event-3', + runId: workflowRunId, + eventType: 'wait_completed', + correlationId: 'wait_01HK153X008RT6YEW43G8QX6JY', + createdAt: new Date('2024-01-01T00:00:04.000Z'), + }, + ], + noEncryptionKey + ); // The date should be the same const date1 = await hydrateWorkflowReturnValue( result1 as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); const date2 = await hydrateWorkflowReturnValue( result2 as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); expect(date1).toEqual(date2); } @@ -378,7 +481,12 @@ describe('runWorkflow', () => { runId: workflowRunId, workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -406,7 +514,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X008RT6YEW43G8QX6JX', eventData: { - result: await dehydrateStepReturnValue(3, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 3, + 'wrun_test', + noEncryptionKey, + ops + ), }, createdAt: new Date(), }, @@ -416,7 +529,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X008RT6YEW43G8QX6JY', eventData: { - result: await dehydrateStepReturnValue(7, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 7, + 'wrun_test', + noEncryptionKey, + ops + ), }, createdAt: new Date(), }, @@ -429,10 +547,16 @@ describe('runWorkflow', () => { return a; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual([3, 7]); }); @@ -443,7 +567,12 @@ describe('runWorkflow', () => { runId: workflowRunId, workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -471,7 +600,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X008RT6YEW43G8QX6JX', eventData: { - result: await dehydrateStepReturnValue(3, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 3, + 'wrun_test', + noEncryptionKey, + ops + ), }, createdAt: new Date(), }, @@ -481,7 +615,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X008RT6YEW43G8QX6JY', eventData: { - result: await dehydrateStepReturnValue(7, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 7, + 'wrun_test', + noEncryptionKey, + ops + ), }, createdAt: new Date(), }, @@ -494,10 +633,16 @@ describe('runWorkflow', () => { return a; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual(3); }); @@ -508,7 +653,12 @@ describe('runWorkflow', () => { runId: workflowRunId, workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -536,7 +686,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X008RT6YEW43G8QX6JY', eventData: { - result: await dehydrateStepReturnValue(7, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 7, + 'wrun_test', + noEncryptionKey, + ops + ), }, createdAt: new Date(), }, @@ -546,7 +701,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X008RT6YEW43G8QX6JX', eventData: { - result: await dehydrateStepReturnValue(3, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 3, + 'wrun_test', + noEncryptionKey, + ops + ), }, createdAt: new Date(), }, @@ -559,10 +719,16 @@ describe('runWorkflow', () => { return a; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual(7); }); @@ -572,7 +738,12 @@ describe('runWorkflow', () => { runId: 'wrun_01K75533W56DAE35VY3082DN3P', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -619,7 +790,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X00DKMJB5AQEJZ3FQGH', eventData: { - result: await dehydrateStepReturnValue(4, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 4, + 'wrun_test', + noEncryptionKey, + ops + ), }, runId: 'wrun_01K75533W56DAE35VY3082DN3P', eventId: 'evnt_01K7553EABWCK00JQ9R8P1FTK7', @@ -629,7 +805,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X00DKMJB5AQEJZ3FQGG', eventData: { - result: await dehydrateStepReturnValue(3, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 3, + 'wrun_test', + noEncryptionKey, + ops + ), }, runId: 'wrun_01K75533W56DAE35VY3082DN3P', eventId: 'evnt_01K7553F31YS6C94NG23WGEEMV', @@ -639,7 +820,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X00DKMJB5AQEJZ3FQGF', eventData: { - result: await dehydrateStepReturnValue(2, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 2, + 'wrun_test', + noEncryptionKey, + ops + ), }, runId: 'wrun_01K75533W56DAE35VY3082DN3P', eventId: 'evnt_01K7553G0XEE4R440QS5SV89YE', @@ -649,7 +835,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X00DKMJB5AQEJZ3FQGE', eventData: { - result: await dehydrateStepReturnValue(1, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 1, + 'wrun_test', + noEncryptionKey, + ops + ), }, runId: 'wrun_01K75533W56DAE35VY3082DN3P', eventId: 'evnt_01K7553HS9R1XJQKVVW0ZRCMNP', @@ -659,7 +850,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X00DKMJB5AQEJZ3FQGD', eventData: { - result: await dehydrateStepReturnValue(0, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 0, + 'wrun_test', + noEncryptionKey, + ops + ), }, runId: 'wrun_01K75533W56DAE35VY3082DN3P', eventId: 'evnt_01K7553K67FQG02YCFE9QDKJ90', @@ -687,10 +883,16 @@ describe('runWorkflow', () => { return done; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual([4, 3, 2, 1, 0]); }); }); @@ -704,7 +906,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'value', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -716,7 +923,8 @@ describe('runWorkflow', () => { await runWorkflow( `const value = "test"${getWorkflowTransformCode()}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -736,7 +944,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -748,7 +961,8 @@ describe('runWorkflow', () => { await runWorkflow( `function workflow() { throw new Error("test"); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -766,7 +980,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'testWorkflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -778,7 +997,8 @@ describe('runWorkflow', () => { await runWorkflow( `function testWorkflow() { throw new Error("test error"); }${getWorkflowTransformCode('testWorkflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -799,7 +1019,12 @@ describe('runWorkflow', () => { runId: 'test-run-nested', workflowName: 'nestedWorkflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -821,7 +1046,7 @@ describe('runWorkflow', () => { } ${getWorkflowTransformCode('nestedWorkflow')}`; - await runWorkflow(workflowCode, workflowRun, events); + await runWorkflow(workflowCode, workflowRun, events, noEncryptionKey); } catch (err) { error = err as Error; } @@ -844,7 +1069,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -861,7 +1091,8 @@ describe('runWorkflow', () => { return a; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -887,7 +1118,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -912,7 +1148,8 @@ describe('runWorkflow', () => { return a; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -932,7 +1169,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -948,7 +1190,8 @@ describe('runWorkflow', () => { return a; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -980,7 +1223,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1001,7 +1249,8 @@ describe('runWorkflow', () => { } }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -1019,7 +1268,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1035,7 +1289,8 @@ describe('runWorkflow', () => { return 'done'; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ) ).rejects.toThrow( 'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions' @@ -1048,7 +1303,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1064,7 +1324,8 @@ describe('runWorkflow', () => { return 'done'; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ) ).rejects.toThrow( 'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions' @@ -1077,7 +1338,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1093,7 +1359,8 @@ describe('runWorkflow', () => { return 'done'; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ) ).rejects.toThrow( 'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions' @@ -1106,7 +1373,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1122,7 +1394,8 @@ describe('runWorkflow', () => { return 'done'; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ) ).rejects.toThrow( 'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions' @@ -1135,7 +1408,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1151,7 +1429,8 @@ describe('runWorkflow', () => { return 'done'; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ) ).rejects.toThrow( 'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions' @@ -1164,7 +1443,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1180,7 +1464,8 @@ describe('runWorkflow', () => { return 'done'; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ) ).rejects.toThrow( 'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions' @@ -1195,7 +1480,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1210,7 +1500,8 @@ describe('runWorkflow', () => { return 'done'; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -1234,7 +1525,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1251,7 +1547,8 @@ describe('runWorkflow', () => { return payload.message; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -1269,7 +1566,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1285,8 +1587,9 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { message: 'Hello from hook' }, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ), }, createdAt: new Date(), @@ -1301,10 +1604,16 @@ describe('runWorkflow', () => { return payload.message; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual('Hello from hook'); }); @@ -1314,7 +1623,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1330,8 +1644,9 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { message: 'First payload' }, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ), }, createdAt: new Date(), @@ -1344,8 +1659,9 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { message: 'Second payload' }, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ), }, createdAt: new Date(), @@ -1361,10 +1677,16 @@ describe('runWorkflow', () => { return [payload1.message, payload2.message]; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual(['First payload', 'Second payload']); }); @@ -1374,7 +1696,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1390,8 +1717,9 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { count: 1, status: 'active' }, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ), }, createdAt: new Date(), @@ -1404,8 +1732,9 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { count: 2, status: 'complete' }, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ), }, createdAt: new Date(), @@ -1426,10 +1755,16 @@ describe('runWorkflow', () => { return payloads; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual([ { count: 1, status: 'active' }, { count: 2, status: 'complete' }, @@ -1442,7 +1777,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1458,8 +1798,9 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { value: 100 }, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ), }, createdAt: new Date(), @@ -1472,8 +1813,9 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { value: 200 }, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ), }, createdAt: new Date(), @@ -1488,10 +1830,16 @@ describe('runWorkflow', () => { return payload.value; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual(100); }); @@ -1501,7 +1849,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1517,8 +1870,9 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { data: 'first' }, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ), }, createdAt: new Date('2024-01-01T00:00:01.000Z'), @@ -1531,8 +1885,9 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { data: 'second' }, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ), }, createdAt: new Date('2024-01-01T00:00:02.000Z'), @@ -1550,7 +1905,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X008RT6YEW43G8QX6JY', eventData: { - result: await dehydrateStepReturnValue(42, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 42, + 'wrun_test', + noEncryptionKey, + ops + ), }, createdAt: new Date('2024-01-01T00:00:04.000Z'), }, @@ -1571,10 +1931,16 @@ describe('runWorkflow', () => { }; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual({ data1: 'first', stepResult: 42, @@ -1588,7 +1954,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1604,8 +1975,9 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { iteration: 1 }, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ), }, createdAt: new Date('2024-01-01T00:00:01.000Z'), @@ -1623,7 +1995,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X008RT6YEW43G8QX6JY', eventData: { - result: await dehydrateStepReturnValue(10, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 10, + 'wrun_test', + noEncryptionKey, + ops + ), }, createdAt: new Date('2024-01-01T00:00:03.000Z'), }, @@ -1641,7 +2018,8 @@ describe('runWorkflow', () => { } }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -1659,7 +2037,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1675,8 +2058,9 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { result: 'success' }, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ), }, createdAt: new Date(), @@ -1691,10 +2075,16 @@ describe('runWorkflow', () => { return { token: hook.token, result: payload.result }; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual({ token: 'my-custom-token', result: 'success', @@ -1707,7 +2097,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1737,7 +2132,8 @@ describe('runWorkflow', () => { return payload; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -1754,7 +2150,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1788,7 +2189,8 @@ describe('runWorkflow', () => { return results; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -1806,7 +2208,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1820,12 +2227,14 @@ describe('runWorkflow', () => { return new Response('Hello, world!', { status: 201 }); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); const res = await hydrateWorkflowReturnValue( result as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); expect(res).toBeInstanceOf(Response); expect(res.status).toEqual(201); @@ -1842,7 +2251,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1856,12 +2270,14 @@ describe('runWorkflow', () => { return Response.json({ message: 'success', count: 42 }, { status: 201 }); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); const res = await hydrateWorkflowReturnValue( result as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); expect(res).toBeInstanceOf(Response); expect(res.status).toEqual(201); @@ -1878,7 +2294,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1895,12 +2316,14 @@ describe('runWorkflow', () => { }); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); const res = await hydrateWorkflowReturnValue( result as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); expect(res).toBeInstanceOf(Response); expect(res.status).toEqual(202); @@ -1914,7 +2337,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1928,12 +2356,14 @@ describe('runWorkflow', () => { return new Response(null, { status: 204 }); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); const res = await hydrateWorkflowReturnValue( result as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); expect(res).toBeInstanceOf(Response); expect(res.status).toEqual(204); @@ -1946,7 +2376,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1961,12 +2396,14 @@ describe('runWorkflow', () => { return new Response(data, { status: 200 }); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); const res = await hydrateWorkflowReturnValue( result as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); expect(res).toBeInstanceOf(Response); expect(res.status).toEqual(200); @@ -1983,7 +2420,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -1998,7 +2440,8 @@ describe('runWorkflow', () => { return new Response('hello', { status: 204 }); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ) ).rejects.toThrow( 'Response constructor: Invalid response status code 204' @@ -2012,7 +2455,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2026,12 +2474,14 @@ describe('runWorkflow', () => { return Response.redirect('https://example.com/redirect'); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); const res = await hydrateWorkflowReturnValue( result as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); expect(res).toBeInstanceOf(Response); expect(res.status).toEqual(302); @@ -2047,7 +2497,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2061,12 +2516,14 @@ describe('runWorkflow', () => { return Response.redirect('https://example.com/moved', 301); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); const res = await hydrateWorkflowReturnValue( result as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); expect(res).toBeInstanceOf(Response); expect(res.status).toEqual(301); @@ -2081,7 +2538,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2097,12 +2559,14 @@ describe('runWorkflow', () => { ); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); const statuses = await hydrateWorkflowReturnValue( result as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); expect(statuses).toEqual([301, 302, 303, 307, 308]); }); @@ -2113,7 +2577,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2128,7 +2597,8 @@ describe('runWorkflow', () => { return Response.redirect('https://example.com', 200); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ) ).rejects.toThrow( 'Invalid redirect status code: 200. Must be one of: 301, 302, 303, 307, 308' @@ -2144,7 +2614,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2158,12 +2633,14 @@ describe('runWorkflow', () => { return new Request('https://example.com/api'); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); const req = await hydrateWorkflowReturnValue( result as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); expect(req).toBeInstanceOf(Request); expect(req.method).toEqual('GET'); @@ -2177,7 +2654,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2194,12 +2676,14 @@ describe('runWorkflow', () => { }); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); const req = await hydrateWorkflowReturnValue( result as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); expect(req).toBeInstanceOf(Request); expect(req.method).toEqual('POST'); @@ -2217,7 +2701,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2238,12 +2727,14 @@ describe('runWorkflow', () => { }); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); const req = await hydrateWorkflowReturnValue( result as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); expect(req).toBeInstanceOf(Request); expect(req.headers.get('Content-Type')).toEqual('application/json'); @@ -2256,7 +2747,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2274,12 +2770,14 @@ describe('runWorkflow', () => { }); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); const req = await hydrateWorkflowReturnValue( result as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); expect(req).toBeInstanceOf(Request); expect(req.method).toEqual('PUT'); @@ -2296,7 +2794,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2314,7 +2817,8 @@ describe('runWorkflow', () => { }); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ) ).rejects.toThrow('Request with GET/HEAD method cannot have body.'); }); @@ -2325,7 +2829,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2343,7 +2852,8 @@ describe('runWorkflow', () => { }); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ) ).rejects.toThrow('Request with GET/HEAD method cannot have body.'); }); @@ -2354,7 +2864,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2369,7 +2884,8 @@ describe('runWorkflow', () => { return new Request('/'); }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ) ).rejects.toThrow('Failed to parse URL from /'); }); @@ -2380,7 +2896,12 @@ describe('runWorkflow', () => { runId: 'test-clone-bug-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2413,13 +2934,15 @@ describe('runWorkflow', () => { }; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); const result_obj = await hydrateWorkflowReturnValue( result as any, - ops, - 'wrun_test' + 'wrun_test', + noEncryptionKey, + ops ); // According to MDN, the req1 properties should be inherited @@ -2441,7 +2964,12 @@ describe('runWorkflow', () => { runId: workflowRunId, workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2476,10 +3004,16 @@ describe('runWorkflow', () => { return 'sleep completed'; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual('sleep completed'); }); @@ -2492,7 +3026,12 @@ describe('runWorkflow', () => { runId: workflowRunId, workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2508,7 +3047,8 @@ describe('runWorkflow', () => { return 'done'; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -2527,7 +3067,12 @@ describe('runWorkflow', () => { runId: workflowRunId, workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2578,10 +3123,16 @@ describe('runWorkflow', () => { return 'all sleeps completed'; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual('all sleeps completed'); }); @@ -2594,7 +3145,12 @@ describe('runWorkflow', () => { runId: workflowRunId, workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2638,7 +3194,8 @@ describe('runWorkflow', () => { return 'all sleeps completed'; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -2656,7 +3213,12 @@ describe('runWorkflow', () => { runId: workflowRunId, workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2677,7 +3239,12 @@ describe('runWorkflow', () => { eventType: 'step_completed', correlationId: 'step_01HK153X008RT6YEW43G8QX6JX', eventData: { - result: await dehydrateStepReturnValue(42, ops, 'wrun_test'), + result: await dehydrateStepReturnValue( + 42, + 'wrun_test', + noEncryptionKey, + ops + ), }, createdAt: new Date('2024-01-01T00:00:01.000Z'), }, @@ -2709,10 +3276,16 @@ describe('runWorkflow', () => { return { step: stepResult, slept: true }; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual({ step: 42, slept: true, @@ -2727,7 +3300,12 @@ describe('runWorkflow', () => { runId: workflowRunId, workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -2762,10 +3340,16 @@ describe('runWorkflow', () => { return 'sleep with date completed'; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); expect( - await hydrateWorkflowReturnValue(result as any, ops, 'wrun_test') + await hydrateWorkflowReturnValue( + result as any, + 'wrun_test', + noEncryptionKey, + ops + ) ).toEqual('sleep with date completed'); }); @@ -3040,7 +3624,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -3059,7 +3648,8 @@ describe('runWorkflow', () => { return result; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; @@ -3087,7 +3677,12 @@ describe('runWorkflow', () => { runId: 'test-run-123', workflowName: 'workflow', status: 'running', - input: await dehydrateWorkflowArguments([], ops, 'wrun_test'), + input: await dehydrateWorkflowArguments( + [], + 'wrun_test', + noEncryptionKey, + ops + ), createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), startedAt: new Date('2024-01-01T00:00:00.000Z'), @@ -3103,7 +3698,8 @@ describe('runWorkflow', () => { return result; }${getWorkflowTransformCode('workflow')}`, workflowRun, - events + events, + noEncryptionKey ); } catch (err) { error = err as Error; diff --git a/packages/core/src/workflow.ts b/packages/core/src/workflow.ts index 69d8d9811b..07b4b00f08 100644 --- a/packages/core/src/workflow.ts +++ b/packages/core/src/workflow.ts @@ -34,7 +34,8 @@ import { createSleep } from './workflow/sleep.js'; export async function runWorkflow( workflowCode: string, workflowRun: WorkflowRun, - events: Event[] + events: Event[], + encryptionKey: Uint8Array | undefined ): Promise { return trace(`workflow.run ${workflowRun.workflowName}`, async (span) => { span?.setAttributes({ @@ -83,6 +84,8 @@ export async function runWorkflow( }); const workflowContext: WorkflowOrchestratorContext = { + runId: workflowRun.runId, + encryptionKey, globalThis: vmGlobalThis, onWorkflowError: workflowDiscontinuation.reject, eventsConsumer, @@ -637,6 +640,8 @@ export async function runWorkflow( const args = await hydrateWorkflowArguments( workflowRun.input, + workflowRun.runId, + encryptionKey, vmGlobalThis ); @@ -650,7 +655,12 @@ export async function runWorkflow( workflowDiscontinuation.promise, ]); - const dehydrated = await dehydrateWorkflowReturnValue(result, vmGlobalThis); + const dehydrated = await dehydrateWorkflowReturnValue( + result, + workflowRun.runId, + encryptionKey, + vmGlobalThis + ); span?.setAttributes({ ...Attribute.WorkflowResultType(typeof result), diff --git a/packages/core/src/workflow/hook.test.ts b/packages/core/src/workflow/hook.test.ts index 778a544e82..0cae5c6b71 100644 --- a/packages/core/src/workflow/hook.test.ts +++ b/packages/core/src/workflow/hook.test.ts @@ -19,6 +19,8 @@ function setupWorkflowContext(events: Event[]): WorkflowOrchestratorContext { const ulid = monotonicFactory(() => context.globalThis.Math.random()); const workflowStartedAt = context.globalThis.Date.now(); return { + runId: 'wrun_test', + encryptionKey: undefined, globalThis: context.globalThis, eventsConsumer: new EventsConsumer(events, { onUnconsumedEvent: () => {}, @@ -44,8 +46,9 @@ describe('createCreateHook', () => { eventData: { payload: await dehydrateStepReturnValue( { message: 'hello' }, - ops, - 'wrun_test' + 'wrun_test', + undefined, + ops ), }, createdAt: new Date(), @@ -133,8 +136,9 @@ describe('createCreateHook', () => { eventData: { payload: await dehydrateStepReturnValue( { data: 'test' }, - ops, - 'wrun_test' + 'wrun_test', + undefined, + ops ), }, createdAt: new Date(), @@ -201,8 +205,9 @@ describe('createCreateHook', () => { eventData: { payload: await dehydrateStepReturnValue( { message: 'first' }, - ops, - 'wrun_test' + 'wrun_test', + undefined, + ops ), }, createdAt: new Date(), @@ -215,8 +220,9 @@ describe('createCreateHook', () => { eventData: { payload: await dehydrateStepReturnValue( { message: 'second' }, - ops, - 'wrun_test' + 'wrun_test', + undefined, + ops ), }, createdAt: new Date(), diff --git a/packages/core/src/workflow/hook.ts b/packages/core/src/workflow/hook.ts index 7178a96f52..f4e7a30369 100644 --- a/packages/core/src/workflow/hook.ts +++ b/packages/core/src/workflow/hook.ts @@ -94,7 +94,12 @@ export function createCreateHook(ctx: WorkflowOrchestratorContext) { const next = promises.shift(); if (next) { // Reconstruct the payload from the event data - hydrateStepReturnValue(event.eventData.payload, ctx.globalThis) + hydrateStepReturnValue( + event.eventData.payload, + ctx.runId, + ctx.encryptionKey, + ctx.globalThis + ) .then((payload) => { next.resolve(payload); }) @@ -140,7 +145,12 @@ export function createCreateHook(ctx: WorkflowOrchestratorContext) { if (payloadsQueue.length > 0) { const nextPayload = payloadsQueue.shift(); if (nextPayload) { - hydrateStepReturnValue(nextPayload.eventData.payload, ctx.globalThis) + hydrateStepReturnValue( + nextPayload.eventData.payload, + ctx.runId, + ctx.encryptionKey, + ctx.globalThis + ) .then((payload) => { resolvers.resolve(payload); }) diff --git a/packages/world-testing/src/addition.mts b/packages/world-testing/src/addition.mts index 4998888de0..875ecfb2df 100644 --- a/packages/world-testing/src/addition.mts +++ b/packages/world-testing/src/addition.mts @@ -23,7 +23,11 @@ export function addition(world: string) { timeout: 10_000, } ); - const output = await hydrateWorkflowReturnValue(run.output!, [], run.runId); + const output = await hydrateWorkflowReturnValue( + run.output!, + run.runId, + undefined + ); expect(output).toEqual(3); }); } diff --git a/packages/world-testing/src/errors.mts b/packages/world-testing/src/errors.mts index 1ba66cbb76..ec6b221c09 100644 --- a/packages/world-testing/src/errors.mts +++ b/packages/world-testing/src/errors.mts @@ -24,7 +24,11 @@ export function errors(world: string) { timeout: 50_000, } ); - const output = await hydrateWorkflowReturnValue(run.output!, [], run.runId); + const output = (await hydrateWorkflowReturnValue( + run.output!, + run.runId, + undefined + )) as any; expect(output).toEqual({ gotFatalError: true, retryableResult: { diff --git a/packages/world-testing/src/hooks.mts b/packages/world-testing/src/hooks.mts index 9b257dc528..f0d6b11ee0 100644 --- a/packages/world-testing/src/hooks.mts +++ b/packages/world-testing/src/hooks.mts @@ -67,7 +67,11 @@ export function hooks(world: string) { } ); - const output = await hydrateWorkflowReturnValue(run.output!, [], run.runId); + const output = await hydrateWorkflowReturnValue( + run.output!, + run.runId, + undefined + ); expect(output).toEqual({ collected: [ { diff --git a/packages/world-testing/src/idempotency.mts b/packages/world-testing/src/idempotency.mts index 5e5f11b3e4..ac6d71cf71 100644 --- a/packages/world-testing/src/idempotency.mts +++ b/packages/world-testing/src/idempotency.mts @@ -21,7 +21,11 @@ export function idempotency(world: string) { } ); - const output = await hydrateWorkflowReturnValue(run.output!, [], run.runId); + const output = await hydrateWorkflowReturnValue( + run.output!, + run.runId, + undefined + ); expect(output).toEqual({ numbers: Array.from({ length: 20 }, () => expect.any(Number)), diff --git a/packages/world-testing/src/null-byte.mts b/packages/world-testing/src/null-byte.mts index 776eaed22c..da25378aeb 100644 --- a/packages/world-testing/src/null-byte.mts +++ b/packages/world-testing/src/null-byte.mts @@ -23,7 +23,11 @@ export function nullByte(world: string) { timeout: 10_000, } ); - const output = await hydrateWorkflowReturnValue(run.output!, [], run.runId); + const output = await hydrateWorkflowReturnValue( + run.output!, + run.runId, + undefined + ); expect(output).toEqual('null byte \0'); }); } diff --git a/packages/world/src/interfaces.ts b/packages/world/src/interfaces.ts index 7523cd3080..07661f4c1b 100644 --- a/packages/world/src/interfaces.ts +++ b/packages/world/src/interfaces.ts @@ -186,4 +186,25 @@ export interface World extends Queue, Storage, Streamer { * without relying on `process.exit()`. */ close?(): Promise; + + /** + * Retrieve the AES-256 encryption key for a specific workflow run. + * + * The returned key is a ready-to-use 32-byte AES-256 key. The World + * implementation handles all key retrieval and derivation internally + * (e.g., HKDF from a deployment key). The core encryption module uses + * this key directly for AES-GCM encrypt/decrypt operations. + * + * Accepts either a full `WorkflowRun` (preferred, avoids redundant + * lookups) or a plain `runId` string (used by `start()` before the + * run entity exists — the World assumes the current deployment). + * + * When not implemented, encryption is disabled — data is stored unencrypted. + * + * @param run - A WorkflowRun entity or a runId string + * @returns The per-run AES-256 key, or undefined if encryption is not configured + */ + getEncryptionKeyForRun?( + run: WorkflowRun | string + ): Promise; } From 7691b492e963faae606e69a573520c6c6ea7ad77 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Sat, 14 Feb 2026 02:38:05 -0800 Subject: [PATCH 2/4] update changeset --- .changeset/encryptor-interface.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.changeset/encryptor-interface.md b/.changeset/encryptor-interface.md index 64dc0fa0c3..a79ce12af7 100644 --- a/.changeset/encryptor-interface.md +++ b/.changeset/encryptor-interface.md @@ -2,12 +2,7 @@ "@workflow/core": patch "@workflow/world": patch "@workflow/cli": patch -"@workflow/web": patch "@workflow/world-testing": patch --- -Add encryption key interface and thread through serialization layer - -Adds `World.getEncryptionKeyForRun()` to `@workflow/world` — the World retrieves and derives the per-run AES-256 encryption key. The 8 dehydrate/hydrate serialization functions now accept an optional `key: Uint8Array | undefined` parameter for future encryption support. Restructures `resumeHook` to resolve the encryption key once and reuse for both metadata decryption and payload encryption. - -**Breaking change (internal API):** The dehydrate/hydrate function parameter order has changed. For example, `dehydrateWorkflowArguments(value, ops, runId, ...)` is now `dehydrateWorkflowArguments(value, runId, key, ops, ...)`. The `runId` and `key` parameters are now grouped together as the 2nd and 3rd arguments across all 8 functions. These are internal APIs not intended for external consumption. +Add `World.getEncryptionKeyForRun()` and thread encryption key through serialization layer From 728453549faae27ea66f20abad46885d6d787a22 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Sun, 15 Feb 2026 02:27:30 -0800 Subject: [PATCH 3/4] Fix runId mismatch in workflow tests: use workflowRun.runId consistently --- packages/core/src/workflow.test.ts | 248 ++++++++++++++--------------- 1 file changed, 124 insertions(+), 124 deletions(-) diff --git a/packages/core/src/workflow.test.ts b/packages/core/src/workflow.test.ts index 158f70f1f0..ee68ce6e67 100644 --- a/packages/core/src/workflow.test.ts +++ b/packages/core/src/workflow.test.ts @@ -36,7 +36,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -57,7 +57,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -74,7 +74,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [1, 2], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -95,7 +95,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -118,7 +118,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -137,7 +137,7 @@ describe('runWorkflow', () => { events, noEncryptionKey )) as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -156,7 +156,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -182,7 +182,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 3, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -205,7 +205,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -222,7 +222,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -268,7 +268,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 3, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -297,7 +297,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 3, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -326,7 +326,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 3, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -360,7 +360,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -384,7 +384,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -459,13 +459,13 @@ describe('runWorkflow', () => { // The date should be the same const date1 = await hydrateWorkflowReturnValue( result1 as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); const date2 = await hydrateWorkflowReturnValue( result2 as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -483,7 +483,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -516,7 +516,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 3, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -531,7 +531,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 7, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -553,7 +553,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -569,7 +569,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -602,7 +602,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 3, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -617,7 +617,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 7, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -639,7 +639,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -655,7 +655,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -688,7 +688,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 7, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -703,7 +703,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 3, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -725,7 +725,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -740,7 +740,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -792,7 +792,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 4, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -807,7 +807,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 3, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -822,7 +822,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 2, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -837,7 +837,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 1, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -852,7 +852,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 0, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -889,7 +889,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -908,7 +908,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -946,7 +946,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -982,7 +982,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1021,7 +1021,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1071,7 +1071,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1120,7 +1120,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1171,7 +1171,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1225,7 +1225,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1270,7 +1270,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1305,7 +1305,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1340,7 +1340,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1375,7 +1375,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1410,7 +1410,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1445,7 +1445,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1482,7 +1482,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1527,7 +1527,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1568,7 +1568,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1587,7 +1587,7 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { message: 'Hello from hook' }, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1610,7 +1610,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -1625,7 +1625,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1644,7 +1644,7 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { message: 'First payload' }, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1659,7 +1659,7 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { message: 'Second payload' }, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1683,7 +1683,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -1698,7 +1698,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1717,7 +1717,7 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { count: 1, status: 'active' }, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1732,7 +1732,7 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { count: 2, status: 'complete' }, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1761,7 +1761,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -1779,7 +1779,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1798,7 +1798,7 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { value: 100 }, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1813,7 +1813,7 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { value: 200 }, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1836,7 +1836,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -1851,7 +1851,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1870,7 +1870,7 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { data: 'first' }, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1885,7 +1885,7 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { data: 'second' }, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1907,7 +1907,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 42, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1937,7 +1937,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -1956,7 +1956,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1975,7 +1975,7 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { iteration: 1 }, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -1997,7 +1997,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 10, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2039,7 +2039,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2058,7 +2058,7 @@ describe('runWorkflow', () => { eventData: { payload: await dehydrateStepReturnValue( { result: 'success' }, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2081,7 +2081,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -2099,7 +2099,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2152,7 +2152,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2210,7 +2210,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2232,7 +2232,7 @@ describe('runWorkflow', () => { ); const res = await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -2253,7 +2253,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2275,7 +2275,7 @@ describe('runWorkflow', () => { ); const res = await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -2296,7 +2296,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2321,7 +2321,7 @@ describe('runWorkflow', () => { ); const res = await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -2339,7 +2339,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2361,7 +2361,7 @@ describe('runWorkflow', () => { ); const res = await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -2378,7 +2378,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2401,7 +2401,7 @@ describe('runWorkflow', () => { ); const res = await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -2422,7 +2422,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2457,7 +2457,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2479,7 +2479,7 @@ describe('runWorkflow', () => { ); const res = await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -2499,7 +2499,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2521,7 +2521,7 @@ describe('runWorkflow', () => { ); const res = await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -2540,7 +2540,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2564,7 +2564,7 @@ describe('runWorkflow', () => { ); const statuses = await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -2579,7 +2579,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2616,7 +2616,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2638,7 +2638,7 @@ describe('runWorkflow', () => { ); const req = await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -2656,7 +2656,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2681,7 +2681,7 @@ describe('runWorkflow', () => { ); const req = await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -2703,7 +2703,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2732,7 +2732,7 @@ describe('runWorkflow', () => { ); const req = await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -2749,7 +2749,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2775,7 +2775,7 @@ describe('runWorkflow', () => { ); const req = await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -2796,7 +2796,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2831,7 +2831,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2866,7 +2866,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2898,7 +2898,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -2940,7 +2940,7 @@ describe('runWorkflow', () => { const result_obj = await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ); @@ -2966,7 +2966,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -3010,7 +3010,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -3028,7 +3028,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -3069,7 +3069,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -3129,7 +3129,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -3147,7 +3147,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -3215,7 +3215,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -3241,7 +3241,7 @@ describe('runWorkflow', () => { eventData: { result: await dehydrateStepReturnValue( 42, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -3282,7 +3282,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -3302,7 +3302,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -3346,7 +3346,7 @@ describe('runWorkflow', () => { expect( await hydrateWorkflowReturnValue( result as any, - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ) @@ -3626,7 +3626,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), @@ -3679,7 +3679,7 @@ describe('runWorkflow', () => { status: 'running', input: await dehydrateWorkflowArguments( [], - 'wrun_test', + 'wrun_123', noEncryptionKey, ops ), From f28311e18e05c64ca54bd0df26ad6529668690b8 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 17 Feb 2026 23:52:14 -0800 Subject: [PATCH 4/4] Address review comments: inline resolveEncryptionKeyForRun, fix double key resolution, update JSDoc params --- packages/cli/src/lib/inspect/hydration.ts | 9 +++- packages/core/src/runtime/resume-hook.ts | 20 ++----- packages/core/src/runtime/start.ts | 7 +-- packages/core/src/runtime/step-handler.ts | 8 ++- packages/core/src/serialization.ts | 63 ++++++++++++++--------- packages/world/src/interfaces.ts | 10 ++-- 6 files changed, 66 insertions(+), 51 deletions(-) diff --git a/packages/cli/src/lib/inspect/hydration.ts b/packages/cli/src/lib/inspect/hydration.ts index c2d6dbea11..8aa168a241 100644 --- a/packages/cli/src/lib/inspect/hydration.ts +++ b/packages/cli/src/lib/inspect/hydration.ts @@ -128,15 +128,20 @@ function getRevivers(): Revivers { // Public API // --------------------------------------------------------------------------- +/** Resolver function that retrieves the encryption key for a given run ID. */ +export type EncryptionKeyResolver = + | ((runId: string) => Promise) + | null; + /** * Hydrate the serialized data fields of a resource for CLI display. * - * The optional `_encryptorResolver` parameter is accepted for forward + * The optional `_encryptionKeyResolver` parameter is accepted for forward * compatibility with encryption support but is not yet used. */ export function hydrateResourceIO( resource: T, - _encryptorResolver?: unknown + _encryptionKeyResolver?: EncryptionKeyResolver ): T { return hydrateResourceIOGeneric(resource as any, getRevivers()) as T; } diff --git a/packages/core/src/runtime/resume-hook.ts b/packages/core/src/runtime/resume-hook.ts index dfe28071dd..8f8c26c4be 100644 --- a/packages/core/src/runtime/resume-hook.ts +++ b/packages/core/src/runtime/resume-hook.ts @@ -17,17 +17,6 @@ import { waitedUntil } from '../util.js'; import { getWorkflowQueueName } from './helpers.js'; import { getWorld } from './world.js'; -/** - * Resolve the encryption key for a given run ID. - * Returns undefined if the World doesn't support encryption. - */ -async function resolveEncryptionKeyForRun( - runId: string -): Promise { - const world = getWorld(); - return world.getEncryptionKeyForRun?.(runId); -} - /** * Internal helper that returns both the hook and the resolved encryption key. */ @@ -36,7 +25,7 @@ async function getHookByTokenWithKey( ): Promise<{ hook: Hook; encryptionKey: Uint8Array | undefined }> { const world = getWorld(); const hook = await world.hooks.getByToken(token); - const encryptionKey = await resolveEncryptionKeyForRun(hook.runId); + const encryptionKey = await world.getEncryptionKeyForRun?.(hook.runId); if (typeof hook.metadata !== 'undefined') { hook.metadata = await hydrateStepArguments( hook.metadata as any, @@ -91,7 +80,7 @@ export async function getHookByToken(token: string): Promise { export async function resumeHook( tokenOrHook: string | Hook, payload: T, - _encryptionKey?: Uint8Array | undefined + encryptionKeyOverride?: Uint8Array | undefined ): Promise { return await waitedUntil(() => { return trace('hook.resume', async (span) => { @@ -103,11 +92,12 @@ export async function resumeHook( if (typeof tokenOrHook === 'string') { const result = await getHookByTokenWithKey(tokenOrHook); hook = result.hook; - encryptionKey = _encryptionKey ?? result.encryptionKey; + encryptionKey = encryptionKeyOverride ?? result.encryptionKey; } else { hook = tokenOrHook; encryptionKey = - _encryptionKey ?? (await resolveEncryptionKeyForRun(hook.runId)); + encryptionKeyOverride ?? + (await world.getEncryptionKeyForRun?.(hook.runId)); } span?.setAttributes({ diff --git a/packages/core/src/runtime/start.ts b/packages/core/src/runtime/start.ts index 99b41ae2bb..18422aefe2 100644 --- a/packages/core/src/runtime/start.ts +++ b/packages/core/src/runtime/start.ts @@ -117,9 +117,10 @@ export async function start( const specVersion = opts.specVersion ?? SPEC_VERSION_CURRENT; const v1Compat = isLegacySpecVersion(specVersion); - // Resolve encryption key for the new run. Since start() always runs on the - // current deployment, we can use a placeholder runId — the implementation - // will resolve to the local deployment's key regardless. + // Resolve encryption key for the new run. The runId has already been + // generated above (client-generated ULID) and will be used for both + // key derivation and the run_created event. The World implementation + // uses the runId for per-run HKDF key derivation. const encryptionKey = await world.getEncryptionKeyForRun?.(runId); // Create run via run_created event (event-sourced architecture) diff --git a/packages/core/src/runtime/step-handler.ts b/packages/core/src/runtime/step-handler.ts index 7d83f5eb18..64aa9f8c1a 100644 --- a/packages/core/src/runtime/step-handler.ts +++ b/packages/core/src/runtime/step-handler.ts @@ -293,13 +293,13 @@ const stepHandler = getWorldHandlers().createQueueHandler( // operations (e.g., stream loading) are added to `ops` and executed later // via Promise.all(ops) - their timing is not included in this measurement. const ops: Promise[] = []; + const encryptionKey = + await world.getEncryptionKeyForRun?.(workflowRunId); const hydratedInput = await trace( 'step.hydrate', {}, async (hydrateSpan) => { const startTime = Date.now(); - const encryptionKey = - await world.getEncryptionKeyForRun?.(workflowRunId); const result = await hydrateStepArguments( step.input, workflowRunId, @@ -357,12 +357,10 @@ const stepHandler = getWorldHandlers().createQueueHandler( {}, async (dehydrateSpan) => { const startTime = Date.now(); - const stepEncryptionKey = - await world.getEncryptionKeyForRun?.(workflowRunId); const dehydrated = await dehydrateStepReturnValue( result, workflowRunId, - stepEncryptionKey, + encryptionKey, ops ); const durationMs = Date.now() - startTime; diff --git a/packages/core/src/serialization.ts b/packages/core/src/serialization.ts index d5282490bb..8c13eb2131 100644 --- a/packages/core/src/serialization.ts +++ b/packages/core/src/serialization.ts @@ -1371,9 +1371,12 @@ function getStepRevivers( * into a format that can be saved to the database and then hydrated from * within the workflow execution environment. * - * @param value - * @param global - * @param runId + * @param value - The workflow arguments to serialize + * @param runId - The workflow run ID (used for stream serialization) + * @param _key - Per-run AES-256 encryption key, or undefined to skip encryption + * @param ops - Array to collect pending async operations (e.g., stream uploads) + * @param global - The global object for custom type serialization + * @param v1Compat - Whether to use legacy v1 serialization format * @returns The dehydrated value as binary data (Uint8Array) with format prefix */ export async function dehydrateWorkflowArguments( @@ -1404,8 +1407,10 @@ export async function dehydrateWorkflowArguments( * arguments from the database at the start of workflow execution. * * @param value - Binary serialized data (Uint8Array) with format prefix - * @param global - * @param extraRevivers + * @param _runId - The workflow run ID (reserved for future encryption use) + * @param _key - Per-run AES-256 encryption key, or undefined to skip decryption + * @param global - The global object for custom type deserialization + * @param extraRevivers - Additional revivers for custom types * @returns The hydrated value */ export async function hydrateWorkflowArguments( @@ -1440,8 +1445,11 @@ export async function hydrateWorkflowArguments( * Called at the end of a completed workflow execution to serialize the * return value into a format that can be saved to the database. * - * @param value - * @param global + * @param value - The workflow return value to serialize + * @param _runId - The workflow run ID (reserved for future encryption use) + * @param _key - Per-run AES-256 encryption key, or undefined to skip encryption + * @param global - The global object for custom type serialization + * @param v1Compat - Whether to use legacy v1 serialization format * @returns The dehydrated value as binary data (Uint8Array) with format prefix */ export async function dehydrateWorkflowReturnValue( @@ -1472,10 +1480,11 @@ export async function dehydrateWorkflowReturnValue( * return value of a completed workflow run. * * @param value - Binary serialized data (Uint8Array) with format prefix - * @param ops - * @param global - * @param extraRevivers - * @param runId + * @param runId - The workflow run ID (used for stream deserialization) + * @param _key - Per-run AES-256 encryption key, or undefined to skip decryption + * @param ops - Array to collect pending async operations (e.g., stream downloads) + * @param global - The global object for custom type deserialization + * @param extraRevivers - Additional revivers for custom types * @returns The hydrated return value, ready to be consumed by the client */ export async function hydrateWorkflowReturnValue( @@ -1512,8 +1521,11 @@ export async function hydrateWorkflowReturnValue( * Dehydrates values from within the workflow execution environment * into a format that can be saved to the database. * - * @param value - * @param global + * @param value - The step arguments to serialize + * @param _runId - The workflow run ID (reserved for future encryption use) + * @param _key - Per-run AES-256 encryption key, or undefined to skip encryption + * @param global - The global object for custom type serialization + * @param v1Compat - Whether to use legacy v1 serialization format * @returns The dehydrated value as binary data (Uint8Array) with format prefix */ export async function dehydrateStepArguments( @@ -1543,10 +1555,11 @@ export async function dehydrateStepArguments( * from the database at the start of the step execution. * * @param value - Binary serialized data (Uint8Array) with format prefix - * @param ops - * @param global - * @param extraRevivers - * @param runId + * @param runId - The workflow run ID (used for stream deserialization) + * @param _key - Per-run AES-256 encryption key, or undefined to skip decryption + * @param ops - Array to collect pending async operations (e.g., stream downloads) + * @param global - The global object for custom type deserialization + * @param extraRevivers - Additional revivers for custom types * @returns The hydrated value, ready to be consumed by the step user-code function */ export async function hydrateStepArguments( @@ -1583,10 +1596,12 @@ export async function hydrateStepArguments( * Dehydrates values from within the step execution environment * into a format that can be saved to the database. * - * @param value - * @param ops - * @param global - * @param runId + * @param value - The step return value to serialize + * @param runId - The workflow run ID (used for stream serialization) + * @param _key - Per-run AES-256 encryption key, or undefined to skip encryption + * @param ops - Array to collect pending async operations (e.g., stream uploads) + * @param global - The global object for custom type serialization + * @param v1Compat - Whether to use legacy v1 serialization format * @returns The dehydrated value as binary data (Uint8Array) with format prefix */ export async function dehydrateStepReturnValue( @@ -1617,8 +1632,10 @@ export async function dehydrateStepReturnValue( * Hydrates the return value of a step from the database. * * @param value - Binary serialized data (Uint8Array) with format prefix - * @param global - * @param extraRevivers + * @param _runId - The workflow run ID (reserved for future encryption use) + * @param _key - Per-run AES-256 encryption key, or undefined to skip decryption + * @param global - The global object for custom type deserialization + * @param extraRevivers - Additional revivers for custom types * @returns The hydrated return value of a step, ready to be consumed by the workflow handler */ export async function hydrateStepReturnValue( diff --git a/packages/world/src/interfaces.ts b/packages/world/src/interfaces.ts index 07661f4c1b..4a940e2977 100644 --- a/packages/world/src/interfaces.ts +++ b/packages/world/src/interfaces.ts @@ -195,9 +195,13 @@ export interface World extends Queue, Storage, Streamer { * (e.g., HKDF from a deployment key). The core encryption module uses * this key directly for AES-GCM encrypt/decrypt operations. * - * Accepts either a full `WorkflowRun` (preferred, avoids redundant - * lookups) or a plain `runId` string (used by `start()` before the - * run entity exists — the World assumes the current deployment). + * Accepts either a full `WorkflowRun` object or a plain `runId` string: + * - `WorkflowRun` — Used by the o11y/CLI path when the run entity is + * already available. Provides `deploymentId` for cross-deployment key + * resolution without a redundant lookup. + * - `string` (runId) — Used by `start()` and `step-handler` in the + * runtime path where the run entity may not exist yet or isn't needed. + * The World assumes the current deployment for key resolution. * * When not implemented, encryption is disabled — data is stored unencrypted. *