chore(packages): post-audit test + packaging polish

- Add safeCompare unit tests (identity, length mismatch, hex-nibble diff).
- Add Buffer-secret cases to hmac tests to lock in Svix/MS-Teams contract.
- Declare `reactflow` as a peerDependency on @sim/workflow-types — only used for type imports.
- Add a barrel export to @sim/workflow-persistence for consumers that prefer package-level imports; subpath exports retained.
- Document the data-field invariant in load.ts for loop/parallel subflow patching.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Waleed Latif
2026-04-22 22:27:21 -07:00
parent 3d4be6dce5
commit ad9855dff5
7 changed files with 88 additions and 4 deletions

View File

@@ -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": [

View File

@@ -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)
})
})

View File

@@ -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)
})
})

View File

@@ -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"

View File

@@ -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'

View File

@@ -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

View File

@@ -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"
}
}