diff --git a/package-lock.json b/package-lock.json index 7a817f7193..1a8e95ca00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -162,6 +162,7 @@ "dotenv": "16.4.5", "embla-carousel-react": "8.1.8", "fast-deep-equal": "3.1.3", + "fast-json-stable-stringify": "2.1.0", "fastify": "5.8.3", "fastify-favicon": "5.0.0", "fastify-plugin": "5.0.1", diff --git a/package.json b/package.json index f1befccf12..2d3af16acb 100644 --- a/package.json +++ b/package.json @@ -187,6 +187,7 @@ "dotenv": "16.4.5", "embla-carousel-react": "8.1.8", "fast-deep-equal": "3.1.3", + "fast-json-stable-stringify": "2.1.0", "fastify": "5.8.3", "fastify-favicon": "5.0.0", "fastify-plugin": "5.0.1", diff --git a/packages/engine/src/lib/variables/props-resolver.ts b/packages/engine/src/lib/variables/props-resolver.ts index ec1d6ae97d..32719db66b 100644 --- a/packages/engine/src/lib/variables/props-resolver.ts +++ b/packages/engine/src/lib/variables/props-resolver.ts @@ -246,7 +246,6 @@ async function evalInScope( }); return result ?? ''; } catch (exception) { - console.warn('[evalInScope] Error evaluating variable', exception); return ''; } } diff --git a/packages/server/shared/src/lib/hash.ts b/packages/server/shared/src/lib/hash.ts index 43391c7d2e..6e4c344706 100644 --- a/packages/server/shared/src/lib/hash.ts +++ b/packages/server/shared/src/lib/hash.ts @@ -1,4 +1,5 @@ import * as crypto from 'crypto'; +import stringify from 'fast-json-stable-stringify'; function hashObject( object: object, @@ -8,6 +9,12 @@ function hashObject( return crypto.createHash('sha256').update(jsonString).digest('hex'); } +function hashDeterministicObject(object: object): string { + const jsonString = stringify(object); + return crypto.createHash('sha256').update(jsonString).digest('hex'); +} + export const hashUtils = { hashObject, + hashDeterministicObject, }; diff --git a/packages/server/shared/test/hash.test.ts b/packages/server/shared/test/hash.test.ts index e1d60ff2b3..5da21365e0 100644 --- a/packages/server/shared/test/hash.test.ts +++ b/packages/server/shared/test/hash.test.ts @@ -40,7 +40,7 @@ describe('Hash Object', () => { it('should allow custom replacers to modify the hash', () => { const object = { key: 'value', anotherKey: 42 }; - const replacer = (key: string, value: unknown) => + const replacer = (key: string, value: unknown): unknown => key === 'key' ? 'modifiedValue' : value; const hashWithReplacer = hashUtils.hashObject(object, replacer); @@ -71,3 +71,74 @@ describe('Hash Object', () => { expect(() => hashUtils.hashObject(object)).toThrow(); }); }); + +describe('Hash Deterministic Object', () => { + it('should return a consistent hash for the same object', () => { + const object = { key: 'value', anotherKey: 42 }; + const hash1 = hashUtils.hashDeterministicObject(object); + const hash2 = hashUtils.hashDeterministicObject(object); + + expect(hash1).toEqual(hash2); + expect(hash1).toEqual( + 'bcfbe7147cc6988bf4987c322ca615a900ed0bce28c358e8404c1b5c14ac389f', + ); + }); + + it('should return the same hash for objects with keys in different orders', () => { + const object1 = { key: 'value', anotherKey: 42 }; + const object2 = { anotherKey: 42, key: 'value' }; + + const hash1 = hashUtils.hashDeterministicObject(object1); + const hash2 = hashUtils.hashDeterministicObject(object2); + + expect(hash1).toEqual(hash2); + expect(hash1).toEqual( + 'bcfbe7147cc6988bf4987c322ca615a900ed0bce28c358e8404c1b5c14ac389f', + ); + }); + + it('should return different hashes for different objects', () => { + const object1 = { key: 'value1' }; + const object2 = { key: 'value2' }; + + const hash1 = hashUtils.hashDeterministicObject(object1); + const hash2 = hashUtils.hashDeterministicObject(object2); + + expect(hash1).not.toEqual(hash2); + expect(hash1).toEqual( + 'dfada72ccc2244e8c7aef8f0dbe7c026a6553bc5bda3f7654f3d0b94dd51a23b', + ); + expect(hash2).toEqual( + '711db6965d4867a7c0f6f20864ae49896b97ba3616a9aa53b536a773468f662e', + ); + }); + + it('should handle nested objects correctly and deterministically', () => { + const nestedObject1 = { key: { b: 2, a: 1 } }; + const nestedObject2 = { key: { a: 1, b: 2 } }; + + const hash1 = hashUtils.hashDeterministicObject(nestedObject1); + const hash2 = hashUtils.hashDeterministicObject(nestedObject2); + + expect(hash1).toEqual(hash2); + expect(hash1).toEqual( + '44a71c92a8d07443b2539b0d82a137c3a620aa81be56de2f29a9e2fe45fefbc4', + ); + }); + + it('should handle empty objects', () => { + const object = {}; + + const hash = hashUtils.hashDeterministicObject(object); + + expect(hash).toEqual( + '44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a', + ); + }); + + it('should handle edge case with undefined object', () => { + const object = undefined as unknown as object; + + expect(() => hashUtils.hashDeterministicObject(object)).toThrow(); + }); +});