mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
0b1ca15dec
* feat(core): seal hook payloads to the target run's public key This is the payoff for the sealed-box work: cross-deployment `resumeHook()` no longer calls `getEncryptionKeyForRun`, which on Vercel means a ~350ms `run-key` API round trip (Cosmos reads, a 50–75KB `encrypted_env.json` fetch from S3, up to three KMS decrypts) to recover 32 bytes. When the target run publishes a public key, the resumer seals to it using only the run entity it already fetched. More than half of `resumeHook()` calls miss the same-deployment fast path, so this is the dominant term in hook-resumption latency — and for Eve, hook resumption is what an agent turn waits on. It also reduces privilege. Fetching the symmetric key grants read access to everything in the run; sealing grants only the ability to write one payload to it. A resumer is now cryptographically unable to read the run it resumes. Sealing is gated on the presence of `encryptionPublicKey`, deliberately *not* on the capability version table the way `encr` and `gzip` are. A run only carries a public key if the runtime that created it could also open a sealed payload, and runs are pinned to their creating deployment — so presence is a stronger attestation than comparing versions, and it stays correct when `@workflow/core` and `@workflow/world-vercel` versions drift independently (each single-axis version gate wedges a run under one drift direction; presence wedges under neither). `resumeWebhook` keeps using the symmetric key it already had to fetch in order to hydrate hook metadata — sealing there would add an ECDH without saving a round trip. **Read path.** Sealing is useless if the run cannot open the result, and every reader previously resolved a bare symmetric `CryptoKey`, which by construction cannot open `encp`. Key resolution now yields the full capability (symmetric key + X25519 keypair) at every read site: `memoizeEncryptionKey`, the `Run` class, `runs.ts`, and `getHookByTokenWithKey`. Without this the first sealed hook payload would have wedged its run with `RuntimeDecryptionError`. Holder types widen from `CryptoKey` to `PayloadKey`, which is a pure widening. Falls back to the symmetric path when the run has no public key (older SDKs), when the stored value is malformed, or when encryption is off — all covered by tests, along with an end-to-end assertion that a sealed payload actually hydrates with the keys the owning deployment re-derives. * review: correct stale docs on the key-resolution path - `memoizeEncryptionKey`'s JSDoc still described importing an AES-256 `CryptoKey`. It now resolves a run's full capability (symmetric key plus X25519 keypair), and the reason matters: a run reading its own event log can meet sealed payloads another run wrote to it, and resolving only the symmetric key would leave those unopenable. Documented explicitly so the next reader does not "simplify" it back. - A comment in `resumeHook` pointed at the `encr` capability gate as being "above" when it is in the fallback branch below. Reworded. - Added the `decodeRunPublicKey` boundary test that belongs with the strict base64 work but needed this branch's code to exist.
283 B
283 B
@workflow/core
| @workflow/core |
|---|
| minor |
resumeHook() now seals its payload to the target run's published public key when one is present, removing the cross-deployment run-key API round trip from hook resumption. Readers resolve the full key capability so they can open sealed payloads.