mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
8d0928b2a2
* fix(core,errors): classify SDK encryption failures as RUNTIME_ERROR SDK-level AES-GCM encrypt/decrypt failures are never the user's fault, but the run-failure classifier was tagging them as USER_ERROR because the native Web Crypto OperationError (most commonly raised by AESCipherJob.onDone on GCM auth-tag mismatch) does not match any RUNTIME_ERROR_CHECKS entry. Introduce a new RuntimeDecryptionError (subclass of WorkflowRuntimeError) that the encryption module throws when subtle.encrypt/subtle.decrypt fails, with the original DOMException as cause plus diagnostic context (operation, byteLength, printable/hex format prefix of the input header). classifyRunError now picks it up via RUNTIME_ERROR_CHECKS, so these failures surface as RUNTIME_ERROR with a proper named class for dashboards and triage. * Trim changeset description to one sentence * Trim historical-context comments * docs: add runtime-decryption-failed troubleshooting page (v4 + v5) * fix(core): round-trip RuntimeDecryptionError context, fix formatPrefix, propagate through serialization wrappers Addresses review feedback on #2145: - Add a RuntimeDecryptionError reducer/reviver (+ SerializableSpecial entry + globalThis registration) so its `context` (operation, byteLength, formatPrefix) survives the dehydrate/hydrate run-error round trip instead of being dropped by the generic Error reducer. - Stop capturing `formatPrefix` in the low-level encryption layer, which only sees the stripped AES payload (nonce bytes), not the outer `encr` marker. The serialization layer now attaches the real envelope prefix. - Rethrow RuntimeDecryptionError unchanged from the serialize/dehydrate catch blocks instead of reframing it as a SerializationError, so an encryption failure during dehydration stays a RUNTIME_ERROR rather than being misclassified as USER_ERROR. * fix(core): enrich stream decrypt errors with envelope prefix + fix lint - Mirror the catch/enrich/rethrow block from serialization/encryption.ts around the stream-path aesGcmDecrypt() call so auth-tag failures on encrypted stream frames also carry context.formatPrefix = 'encr' (addresses review feedback). Add a tampered-frame test. - Fix all auto-fixable Biome lint findings in the touched files (template literals, useless try/catch wrappers, optional chaining, non-null assertions).
63 lines
2.2 KiB
TypeScript
63 lines
2.2 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
import {
|
|
RuntimeDecryptionError,
|
|
WorkflowError,
|
|
WorkflowRuntimeError,
|
|
} from './index.js';
|
|
|
|
describe('RuntimeDecryptionError', () => {
|
|
test('sets the name and extends WorkflowRuntimeError', () => {
|
|
const err = new RuntimeDecryptionError('decrypt failed');
|
|
expect(err.name).toBe('RuntimeDecryptionError');
|
|
expect(err).toBeInstanceOf(WorkflowError);
|
|
expect(err).toBeInstanceOf(WorkflowRuntimeError);
|
|
expect(err).toBeInstanceOf(RuntimeDecryptionError);
|
|
});
|
|
|
|
test('adds the runtime-decryption-failed docs link', () => {
|
|
const err = new RuntimeDecryptionError('decrypt failed');
|
|
expect(err.message).toContain(
|
|
'https://workflow-sdk.dev/err/runtime-decryption-failed'
|
|
);
|
|
});
|
|
|
|
test('preserves cause for debugging', () => {
|
|
const cause = new Error('underlying OperationError');
|
|
const err = new RuntimeDecryptionError('decrypt failed', { cause });
|
|
expect(err.cause).toBe(cause);
|
|
});
|
|
|
|
test('records optional diagnostic context', () => {
|
|
const err = new RuntimeDecryptionError('decrypt failed', {
|
|
context: {
|
|
operation: 'decrypt',
|
|
byteLength: 42,
|
|
formatPrefix: 'encr',
|
|
},
|
|
});
|
|
expect(err.context).toEqual({
|
|
operation: 'decrypt',
|
|
byteLength: 42,
|
|
formatPrefix: 'encr',
|
|
});
|
|
});
|
|
|
|
test('omits the context property when not provided', () => {
|
|
const err = new RuntimeDecryptionError('decrypt failed');
|
|
expect('context' in err).toBe(false);
|
|
});
|
|
|
|
test('RuntimeDecryptionError.is discriminates by name', () => {
|
|
const err = new RuntimeDecryptionError('decrypt failed');
|
|
const other = new Error('decrypt failed');
|
|
const runtimeOnly = new WorkflowRuntimeError('decrypt failed');
|
|
expect(RuntimeDecryptionError.is(err)).toBe(true);
|
|
// .is() is a name-based duck check, so a plain WorkflowRuntimeError
|
|
// does NOT pass — subclassing alone is not enough.
|
|
expect(RuntimeDecryptionError.is(runtimeOnly)).toBe(false);
|
|
expect(RuntimeDecryptionError.is(other)).toBe(false);
|
|
expect(RuntimeDecryptionError.is(null)).toBe(false);
|
|
expect(RuntimeDecryptionError.is(undefined)).toBe(false);
|
|
});
|
|
});
|