From ab1c957742f0d47161650e07d84601098c267555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcelo=20Gon=C3=A7alves?= Date: Fri, 10 Apr 2026 17:18:11 +0100 Subject: [PATCH 1/4] Add hashDeterministicObject --- package-lock.json | 16 ++-------------- package.json | 1 + packages/server/shared/src/lib/hash.ts | 7 +++++++ 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index e0aa2c931c..fe90df2d93 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", @@ -20511,19 +20512,6 @@ "freebsd" ] }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", @@ -65751,4 +65739,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index dce6ce569a..b11f6e6506 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/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, }; From 04fb8ec90c7918fb6df21b4288c3ce5f594723a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcelo=20Gon=C3=A7alves?= Date: Fri, 10 Apr 2026 17:33:07 +0100 Subject: [PATCH 2/4] Add unit tests --- packages/server/shared/test/hash.test.ts | 73 +++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) 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(); + }); +}); From ed489635caeb885056589850616fca0440f07eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcelo=20Gon=C3=A7alves?= Date: Fri, 10 Apr 2026 17:34:53 +0100 Subject: [PATCH 3/4] WIP --- package-lock.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/package-lock.json b/package-lock.json index fe90df2d93..f270caa914 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20512,6 +20512,19 @@ "freebsd" ] }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", From 499c27ab4ba58404b6e004599bd2978d7b3a3a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcelo=20Gon=C3=A7alves?= Date: Fri, 10 Apr 2026 21:18:41 +0100 Subject: [PATCH 4/4] Remove console log --- packages/engine/src/lib/variables/props-resolver.ts | 1 - 1 file changed, 1 deletion(-) 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 ''; } }