diff --git a/.changeset/features-encryption-metadata.md b/.changeset/features-encryption-metadata.md new file mode 100644 index 0000000000..c70fdbc0d8 --- /dev/null +++ b/.changeset/features-encryption-metadata.md @@ -0,0 +1,5 @@ +--- +"@workflow/core": minor +--- + +Add `features.encryption` to `WorkflowMetadata` returned by `getWorkflowMetadata()` diff --git a/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx b/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx index 23b3573a2d..7fc920997b 100644 --- a/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx +++ b/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx @@ -13,6 +13,7 @@ You may want to use this function when you need to: * Log workflow run IDs * Access timing information of a workflow +* Detect whether encryption is enabled for the current run If you need to access step context, take a look at [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata). @@ -29,6 +30,32 @@ async function testWorkflow() { } ``` +### Detecting Encryption + +The `features` object indicates which capabilities are active for the current run. Library authors can use `features.encryption` to control whether sensitive data is included in step return values, which are serialized to the event log: + +```typescript lineNumbers +import { getWorkflowMetadata } from "workflow" + +declare function getUserProfile(userId: string): Promise<{ name: string; ssn: string }>; // @setup + +async function fetchUserProfile(userId: string) { + "use step" + + const { features } = getWorkflowMetadata() // [!code highlight] + const profile = await getUserProfile(userId) + + if (!features.encryption) { // [!code highlight] + // Omit sensitive fields from the return value, + // since it will be stored unencrypted in the event log + const { ssn, ...safe } = profile + return safe + } + + return profile +} +``` + ## API Signature ### Parameters diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index 5e921f8be8..897995b1f1 100644 --- a/packages/core/e2e/e2e.test.ts +++ b/packages/core/e2e/e2e.test.ts @@ -596,6 +596,16 @@ describe('e2e', () => { ); expect(returnValue.stepMetadata.url).toBeUndefined(); + // workflow context should have features and stepMetadata shouldn't + expect(returnValue.workflowMetadata.features).toBeDefined(); + expect(typeof returnValue.workflowMetadata.features.encryption).toBe( + 'boolean' + ); + expect(returnValue.innerWorkflowMetadata.features).toStrictEqual( + returnValue.workflowMetadata.features + ); + expect(returnValue.stepMetadata.features).toBeUndefined(); + // workflow context shouldn't have stepId, stepStartedAt, or attempt expect(returnValue.workflowMetadata.stepId).toBeUndefined(); expect(returnValue.workflowMetadata.stepStartedAt).toBeUndefined(); diff --git a/packages/core/src/runtime/step-handler.ts b/packages/core/src/runtime/step-handler.ts index 6bf0057a60..65872545a8 100644 --- a/packages/core/src/runtime/step-handler.ts +++ b/packages/core/src/runtime/step-handler.ts @@ -512,6 +512,7 @@ const stepHandler = createQueueHandler( url: isVercel ? `https://${process.env.VERCEL_URL}` : `http://localhost:${port ?? 3000}`, + features: { encryption: !!encryptionKey }, }, ops, closureVars: hydratedInput.closureVars, diff --git a/packages/core/src/serialization.test.ts b/packages/core/src/serialization.test.ts index 376312c4d9..a0333dc379 100644 --- a/packages/core/src/serialization.test.ts +++ b/packages/core/src/serialization.test.ts @@ -2266,6 +2266,7 @@ describe('step function serialization', () => { workflowRunId: 'test-run', workflowStartedAt: new Date(), url: 'http://localhost:3000', + features: { encryption: false }, }, ops: [], }, diff --git a/packages/core/src/step/writable-stream.test.ts b/packages/core/src/step/writable-stream.test.ts index b49e6cff52..8df7341a11 100644 --- a/packages/core/src/step/writable-stream.test.ts +++ b/packages/core/src/step/writable-stream.test.ts @@ -36,6 +36,8 @@ describe('step-level getWritable', () => { workflowName: 'test-workflow', workflowRunId: 'wrun_test123', workflowStartedAt: new Date(), + url: 'http://localhost:3000', + features: { encryption: false }, }, ops, encryptionKey: undefined, @@ -82,6 +84,8 @@ describe('step-level getWritable', () => { workflowName: 'test-workflow', workflowRunId: 'wrun_test123', workflowStartedAt: new Date(), + url: 'http://localhost:3000', + features: { encryption: false }, }, ops, encryptionKey: undefined, diff --git a/packages/core/src/workflow.ts b/packages/core/src/workflow.ts index fdbe341b29..1a16ecf7a0 100644 --- a/packages/core/src/workflow.ts +++ b/packages/core/src/workflow.ts @@ -212,6 +212,7 @@ export async function runWorkflow( workflowRunId: workflowRun.runId, workflowStartedAt: new vmGlobalThis.Date(+startedAt), url, + features: { encryption: !!encryptionKey }, }; // @ts-expect-error - `@types/node` says symbol is not valid, but it does work diff --git a/packages/core/src/workflow/get-workflow-metadata.ts b/packages/core/src/workflow/get-workflow-metadata.ts index 50ef0bca47..c89bb06cd2 100644 --- a/packages/core/src/workflow/get-workflow-metadata.ts +++ b/packages/core/src/workflow/get-workflow-metadata.ts @@ -18,6 +18,18 @@ export interface WorkflowMetadata { * The URL where the workflow can be triggered. */ url: string; + + /** + * Feature flags indicating which capabilities are active for this workflow run. + */ + features: { + /** + * Whether encryption is enabled for this workflow run. + * When `true`, step inputs, outputs, and other serialized data + * are encrypted at rest. + */ + encryption: boolean; + }; } export const WORKFLOW_CONTEXT_SYMBOL = diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index cb86ae83a1..36811e8eed 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -246,6 +246,7 @@ export async function workflowAndStepMetadataWorkflow() { workflowRunId: workflowMetadata.workflowRunId, workflowStartedAt: workflowMetadata.workflowStartedAt, url: workflowMetadata.url, + features: workflowMetadata.features, }, stepMetadata, innerWorkflowMetadata,