-
Notifications
You must be signed in to change notification settings - Fork 263
Add World.getEncryptionKeyForRun and thread encryption key through serialization
#979
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2d8e9fb
7691b49
7284535
f28311e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| "@workflow/core": patch | ||
| "@workflow/world": patch | ||
| "@workflow/cli": patch | ||
| "@workflow/world-testing": patch | ||
| --- | ||
|
|
||
| Add `World.getEncryptionKeyForRun()` and thread encryption key through serialization layer | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -128,9 +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<Uint8Array | undefined>) | ||
| | null; | ||
|
|
||
| /** | ||
| * Hydrate the serialized data fields of a resource for CLI display. | ||
| * | ||
| * The optional `_encryptionKeyResolver` parameter is accepted for forward | ||
| * compatibility with encryption support but is not yet used. | ||
| */ | ||
| export function hydrateResourceIO<T>(resource: T): T { | ||
| export function hydrateResourceIO<T>( | ||
| resource: T, | ||
| _encryptionKeyResolver?: EncryptionKeyResolver | ||
| ): T { | ||
| return hydrateResourceIOGeneric(resource as any, getRevivers()) as T; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: The type of
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a proper |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,22 +18,33 @@ 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 | ||
| * Internal helper that returns both the hook and the resolved encryption key. | ||
| */ | ||
| export async function getHookByToken(token: string): Promise<Hook> { | ||
| async function getHookByTokenWithKey( | ||
| token: string | ||
| ): Promise<{ hook: Hook; encryptionKey: Uint8Array | undefined }> { | ||
| const world = getWorld(); | ||
| const hook = await world.hooks.getByToken(token); | ||
| const encryptionKey = await world.getEncryptionKeyForRun?.(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<Hook> { | ||
| const { hook } = await getHookByTokenWithKey(token); | ||
| return hook; | ||
| } | ||
|
|
||
|
|
@@ -68,17 +79,26 @@ export async function getHookByToken(token: string): Promise<Hook> { | |
| */ | ||
| export async function resumeHook<T = any>( | ||
| tokenOrHook: string | Hook, | ||
| payload: T | ||
| payload: T, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: (I see @VaguelySerious flagged this too — just confirming it's still present in the latest revision.)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — renamed to |
||
| encryptionKeyOverride?: Uint8Array | undefined | ||
| ): Promise<Hook> { | ||
|
TooTallNate marked this conversation as resolved.
|
||
| 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 = encryptionKeyOverride ?? result.encryptionKey; | ||
| } else { | ||
| hook = tokenOrHook; | ||
| encryptionKey = | ||
| encryptionKeyOverride ?? | ||
| (await world.getEncryptionKeyForRun?.(hook.runId)); | ||
| } | ||
|
|
||
| span?.setAttributes({ | ||
| ...Attribute.HookToken(hook.token), | ||
|
|
@@ -91,8 +111,9 @@ export async function resumeHook<T = any>( | |
| const v1Compat = isLegacySpecVersion(hook.specVersion); | ||
| const dehydratedPayload = await dehydrateStepReturnValue( | ||
| payload, | ||
| ops, | ||
| hook.runId, | ||
| encryptionKey, | ||
| ops, | ||
| globalThis, | ||
| v1Compat | ||
| ); | ||
|
|
@@ -200,7 +221,7 @@ export async function resumeWebhook( | |
| token: string, | ||
| request: Request | ||
| ): Promise<Response> { | ||
| const hook = await getHookByToken(token); | ||
| const { hook, encryptionKey } = await getHookByTokenWithKey(token); | ||
|
|
||
| let response: Response | undefined; | ||
| let responseReadable: ReadableStream<Response> | undefined; | ||
|
|
@@ -229,7 +250,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, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -117,12 +117,19 @@ export async function start<TArgs extends unknown[], TResult>( | |
| const specVersion = opts.specVersion ?? SPEC_VERSION_CURRENT; | ||
| const v1Compat = isLegacySpecVersion(specVersion); | ||
|
|
||
| // 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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like this isn't adequately documented in the world interface . If a placeholder "works", then
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated the comment. The runId is a client-generated ULID that has already been created at that point — it is the actual runId that will be used for the run_created event and key derivation. Clarified the comment to explain this. |
||
|
|
||
| // 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 | ||
| ); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.