diff --git a/bun.lock b/bun.lock index 99b9dad3c7..916bc86fb3 100644 --- a/bun.lock +++ b/bun.lock @@ -459,13 +459,14 @@ "packages/workflow-types": { "name": "@sim/workflow-types", "version": "0.1.0", - "dependencies": { - "reactflow": "^11.11.4", - }, "devDependencies": { "@sim/tsconfig": "workspace:*", + "reactflow": "^11.11.4", "typescript": "^5.7.3", }, + "peerDependencies": { + "reactflow": "^11.11.4", + }, }, }, "trustedDependencies": [ diff --git a/packages/security/src/compare.test.ts b/packages/security/src/compare.test.ts new file mode 100644 index 0000000000..265235e65b --- /dev/null +++ b/packages/security/src/compare.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest' +import { safeCompare } from './compare' + +describe('safeCompare', () => { + it('returns true for identical strings', () => { + expect(safeCompare('abc', 'abc')).toBe(true) + }) + + it('returns false for equal-length different strings', () => { + expect(safeCompare('abc', 'abd')).toBe(false) + }) + + it('returns false for different-length strings without throwing', () => { + expect(safeCompare('short', 'longer-value')).toBe(false) + expect(safeCompare('', 'a')).toBe(false) + expect(safeCompare('a', '')).toBe(false) + }) + + it('returns true for two empty strings', () => { + expect(safeCompare('', '')).toBe(true) + }) + + it('handles long inputs', () => { + const a = 'x'.repeat(10_000) + const b = 'x'.repeat(10_000) + expect(safeCompare(a, b)).toBe(true) + expect(safeCompare(a, `${b.slice(0, -1)}y`)).toBe(false) + }) + + it('is case-sensitive', () => { + expect(safeCompare('ABC', 'abc')).toBe(false) + }) + + it('distinguishes hex digests that differ in one nibble', () => { + const a = 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7' + const b = 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff8' + expect(safeCompare(a, b)).toBe(false) + expect(safeCompare(a, a)).toBe(true) + }) +}) diff --git a/packages/security/src/hmac.test.ts b/packages/security/src/hmac.test.ts index f0f84d29f8..8daecafa0d 100644 --- a/packages/security/src/hmac.test.ts +++ b/packages/security/src/hmac.test.ts @@ -24,6 +24,11 @@ describe('hmacSha256Hex', () => { it('differs when secret changes', () => { expect(hmacSha256Hex('body', 'k1')).not.toBe(hmacSha256Hex('body', 'k2')) }) + + it('accepts a Buffer secret and matches the equivalent binary-string secret', () => { + const raw = Buffer.from('0b'.repeat(20), 'hex') + expect(hmacSha256Hex('Hi There', raw)).toBe(hmacSha256Hex('Hi There', raw.toString('binary'))) + }) }) describe('hmacSha256Base64', () => { @@ -40,4 +45,11 @@ describe('hmacSha256Base64', () => { const b64 = hmacSha256Base64('body', 'secret') expect(Buffer.from(b64, 'base64').toString('hex')).toBe(hex) }) + + it('accepts a Buffer secret (Svix / MS-Teams scheme)', () => { + const secret = Buffer.from('whsec-decoded-bytes') + const hex = hmacSha256Hex('body', secret) + const b64 = hmacSha256Base64('body', secret) + expect(Buffer.from(b64, 'base64').toString('hex')).toBe(hex) + }) }) diff --git a/packages/workflow-persistence/package.json b/packages/workflow-persistence/package.json index 88b1be60d4..00a99f1dff 100644 --- a/packages/workflow-persistence/package.json +++ b/packages/workflow-persistence/package.json @@ -10,6 +10,10 @@ "node": ">=20.0.0" }, "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + }, "./load": { "types": "./src/load.ts", "default": "./src/load.ts" diff --git a/packages/workflow-persistence/src/index.ts b/packages/workflow-persistence/src/index.ts new file mode 100644 index 0000000000..ad0b35248c --- /dev/null +++ b/packages/workflow-persistence/src/index.ts @@ -0,0 +1,19 @@ +export { + loadWorkflowFromNormalizedTablesRaw, + persistMigratedBlocks, + type RawNormalizedWorkflow, +} from './load' +export { saveWorkflowToNormalizedTables } from './save' +export { + DEFAULT_SUBBLOCK_TYPE, + mergeSubBlockValues, + mergeSubblockStateWithValues, +} from './subblocks' +export { + convertLoopBlockToLoop, + convertParallelBlockToParallel, + findChildNodes, + generateLoopBlocks, + generateParallelBlocks, +} from './subflow-helpers' +export type { DbOrTx, NormalizedWorkflowData } from './types' diff --git a/packages/workflow-persistence/src/load.ts b/packages/workflow-persistence/src/load.ts index 7142f9ae7c..8f19375c81 100644 --- a/packages/workflow-persistence/src/load.ts +++ b/packages/workflow-persistence/src/load.ts @@ -18,6 +18,13 @@ export interface RawNormalizedWorkflow extends NormalizedWorkflowData { * backfill, tool sanitization) depend on the block/tool registry that lives in * the Next app and should not be pulled into leaf services. Callers that want * migrated state should wrap this with their own migration pipeline. + * + * Invariant: downstream migrations must not mutate `block.data.collection`, + * `block.data.whileCondition`, or `block.data.doWhileCondition`. Those fields + * are patched here from the subflow config on the pre-migration block, and + * callers re-sync only `loop.enabled`/`parallel.enabled` from the migrated + * block. If a future migration rewrites these data fields, the loop/parallel + * config on the returned object will silently diverge from the migrated block. */ export async function loadWorkflowFromNormalizedTablesRaw( workflowId: string diff --git a/packages/workflow-types/package.json b/packages/workflow-types/package.json index dd8f6832c2..cc041b0caa 100644 --- a/packages/workflow-types/package.json +++ b/packages/workflow-types/package.json @@ -26,11 +26,12 @@ "format": "biome format --write .", "format:check": "biome format ." }, - "dependencies": { + "peerDependencies": { "reactflow": "^11.11.4" }, "devDependencies": { "@sim/tsconfig": "workspace:*", + "reactflow": "^11.11.4", "typescript": "^5.7.3" } }