From ed73125168fa05ab663bc5dfac4eb2c50929bce7 Mon Sep 17 00:00:00 2001 From: thephez Date: Wed, 29 Oct 2025 15:27:45 -0400 Subject: [PATCH 1/3] feat(sdk): add automatic entropy generation for document creation - Make entropyHex parameter optional in documents.create() - Add generateEntropy() utility function that works in Node.js and browsers - Auto-generate entropy when not provided - Add comprehensive tests for entropy generation and auto-generation behavior --- packages/js-evo-sdk/src/documents/facade.ts | 8 +- packages/js-evo-sdk/src/util.ts | 31 ++++++++ .../tests/unit/facades/documents.spec.mjs | 30 +++++++- packages/js-evo-sdk/tests/unit/util.spec.mjs | 77 +++++++++++++++++++ 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 packages/js-evo-sdk/tests/unit/util.spec.mjs diff --git a/packages/js-evo-sdk/src/documents/facade.ts b/packages/js-evo-sdk/src/documents/facade.ts index 0c7d9e370e0..c1c71ff5141 100644 --- a/packages/js-evo-sdk/src/documents/facade.ts +++ b/packages/js-evo-sdk/src/documents/facade.ts @@ -1,5 +1,5 @@ import * as wasm from '../wasm.js'; -import { asJsonString } from '../util.js'; +import { asJsonString, generateEntropy } from '../util.js'; import type { EvoSDK } from '../sdk.js'; export class DocumentsFacade { @@ -35,10 +35,12 @@ export class DocumentsFacade { type: string; ownerId: wasm.IdentifierLike; data: unknown; - entropyHex: string; + entropyHex?: string; // Now optional - will auto-generate if not provided privateKeyWif: string; }): Promise { - const { contractId, type, ownerId, data, entropyHex, privateKeyWif } = args; + const { contractId, type, ownerId, data, privateKeyWif } = args; + // Auto-generate entropy if not provided + const entropyHex = args.entropyHex ?? generateEntropy(); const w = await this.sdk.getWasmSdkConnected(); return w.documentCreate( contractId, diff --git a/packages/js-evo-sdk/src/util.ts b/packages/js-evo-sdk/src/util.ts index 36d5f27d2fb..bfec38161c9 100644 --- a/packages/js-evo-sdk/src/util.ts +++ b/packages/js-evo-sdk/src/util.ts @@ -3,3 +3,34 @@ export function asJsonString(value: unknown): string | undefined { if (typeof value === 'string') return value; return JSON.stringify(value); } + +/** + * Generate 32 bytes of cryptographically secure random entropy as a hex string. + * Works in both Node.js and browser environments. + * + * @returns A 64-character hex string representing 32 bytes of entropy + * @throws Error if no secure random source is available + */ +export function generateEntropy(): string { + // Node.js environment + if (typeof globalThis !== 'undefined' && globalThis.crypto && 'randomBytes' in globalThis.crypto) { + // @ts-ignore - Node.js crypto.randomBytes exists but may not be in types + return globalThis.crypto.randomBytes(32).toString('hex'); + } + + // Browser environment or Node.js with Web Crypto API + if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.getRandomValues) { + const buffer = new Uint8Array(32); + globalThis.crypto.getRandomValues(buffer); + return Array.from(buffer).map(b => b.toString(16).padStart(2, '0')).join(''); + } + + // Fallback for older environments + if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) { + const buffer = new Uint8Array(32); + window.crypto.getRandomValues(buffer); + return Array.from(buffer).map(b => b.toString(16).padStart(2, '0')).join(''); + } + + throw new Error('No secure random source available. This environment does not support crypto.randomBytes or crypto.getRandomValues.'); +} diff --git a/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs b/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs index ce40ee1918c..c627103a214 100644 --- a/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs +++ b/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs @@ -55,7 +55,7 @@ describe('DocumentsFacade', () => { expect(wasmSdk.getDocumentWithProofInfo).to.be.calledOnceWithExactly('c', 't', 'id'); }); - it('create() calls wasmSdk.documentCreate with JSON data', async () => { + it('create() calls wasmSdk.documentCreate with JSON data and provided entropy', async () => { const data = { foo: 'bar' }; await client.documents.create({ contractId: 'c', @@ -68,6 +68,34 @@ describe('DocumentsFacade', () => { expect(wasmSdk.documentCreate).to.be.calledOnceWithExactly('c', 't', 'o', JSON.stringify(data), 'ee', 'wif'); }); + it('create() auto-generates entropy when not provided', async () => { + const data = { foo: 'bar' }; + await client.documents.create({ + contractId: 'c', + type: 't', + ownerId: 'o', + data, + // No entropyHex provided - should auto-generate + privateKeyWif: 'wif', + }); + + // Check that documentCreate was called + expect(wasmSdk.documentCreate).to.be.calledOnce(); + const [contractId, type, ownerId, jsonData, entropy, wif] = wasmSdk.documentCreate.firstCall.args; + + // Verify all params except entropy + expect(contractId).to.equal('c'); + expect(type).to.equal('t'); + expect(ownerId).to.equal('o'); + expect(jsonData).to.equal(JSON.stringify(data)); + expect(wif).to.equal('wif'); + + // Verify that entropy was auto-generated (should be 64 hex chars = 32 bytes) + expect(entropy).to.be.a('string'); + expect(entropy).to.match(/^[0-9a-f]{64}$/i); + expect(entropy.length).to.equal(64); + }); + it('replace() calls wasmSdk.documentReplace with BigInt revision', async () => { await client.documents.replace({ contractId: 'c', diff --git a/packages/js-evo-sdk/tests/unit/util.spec.mjs b/packages/js-evo-sdk/tests/unit/util.spec.mjs new file mode 100644 index 00000000000..c98665886bf --- /dev/null +++ b/packages/js-evo-sdk/tests/unit/util.spec.mjs @@ -0,0 +1,77 @@ +import { asJsonString, generateEntropy } from '../../dist/util.js'; + +describe('Util Functions', () => { + describe('asJsonString', () => { + it('returns undefined for null', () => { + expect(asJsonString(null)).to.be.undefined; + }); + + it('returns undefined for undefined', () => { + expect(asJsonString(undefined)).to.be.undefined; + }); + + it('returns string as-is', () => { + expect(asJsonString('hello')).to.equal('hello'); + }); + + it('converts objects to JSON string', () => { + const obj = { foo: 'bar', num: 42 }; + expect(asJsonString(obj)).to.equal(JSON.stringify(obj)); + }); + + it('converts arrays to JSON string', () => { + const arr = [1, 2, 'three']; + expect(asJsonString(arr)).to.equal(JSON.stringify(arr)); + }); + }); + + describe('generateEntropy', () => { + it('generates a 64-character hex string', () => { + const entropy = generateEntropy(); + expect(entropy).to.be.a('string'); + expect(entropy.length).to.equal(64); + }); + + it('generates valid hexadecimal', () => { + const entropy = generateEntropy(); + expect(entropy).to.match(/^[0-9a-f]{64}$/i); + }); + + it('generates different values each time', () => { + const entropy1 = generateEntropy(); + const entropy2 = generateEntropy(); + const entropy3 = generateEntropy(); + + // Should be different (extremely unlikely to be the same) + expect(entropy1).to.not.equal(entropy2); + expect(entropy2).to.not.equal(entropy3); + expect(entropy1).to.not.equal(entropy3); + }); + + it('returns exactly 32 bytes when decoded', () => { + const entropy = generateEntropy(); + // Convert hex string to bytes + const bytes = []; + for (let i = 0; i < entropy.length; i += 2) { + bytes.push(parseInt(entropy.substr(i, 2), 16)); + } + expect(bytes.length).to.equal(32); + }); + + it('generates values with good distribution', () => { + // Generate multiple samples and check that we get a variety of hex digits + const samples = []; + for (let i = 0; i < 10; i++) { + samples.push(generateEntropy()); + } + + // Check that we see various hex digits (not all zeros or all ones) + const allChars = samples.join(''); + const uniqueChars = new Set(allChars).size; + + // We should see most of the 16 possible hex digits (0-9, a-f) + // With 640 characters (10 * 64), we expect to see all 16 + expect(uniqueChars).to.be.at.least(10); + }); + }); +}); \ No newline at end of file From 55ae1f4acac20f18542bbc06b41f52e34fedcf70 Mon Sep 17 00:00:00 2001 From: thephez Date: Wed, 29 Oct 2025 16:22:19 -0400 Subject: [PATCH 2/3] chore: lint fixes --- packages/js-evo-sdk/src/util.ts | 4 ++-- .../js-evo-sdk/tests/unit/facades/documents.spec.mjs | 4 +++- packages/js-evo-sdk/tests/unit/util.spec.mjs | 10 +++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/js-evo-sdk/src/util.ts b/packages/js-evo-sdk/src/util.ts index bfec38161c9..9bd7552afc8 100644 --- a/packages/js-evo-sdk/src/util.ts +++ b/packages/js-evo-sdk/src/util.ts @@ -22,14 +22,14 @@ export function generateEntropy(): string { if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.getRandomValues) { const buffer = new Uint8Array(32); globalThis.crypto.getRandomValues(buffer); - return Array.from(buffer).map(b => b.toString(16).padStart(2, '0')).join(''); + return Array.from(buffer).map((b) => b.toString(16).padStart(2, '0')).join(''); } // Fallback for older environments if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) { const buffer = new Uint8Array(32); window.crypto.getRandomValues(buffer); - return Array.from(buffer).map(b => b.toString(16).padStart(2, '0')).join(''); + return Array.from(buffer).map((b) => b.toString(16).padStart(2, '0')).join(''); } throw new Error('No secure random source available. This environment does not support crypto.randomBytes or crypto.getRandomValues.'); diff --git a/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs b/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs index c627103a214..28786e22f84 100644 --- a/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs +++ b/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs @@ -81,7 +81,9 @@ describe('DocumentsFacade', () => { // Check that documentCreate was called expect(wasmSdk.documentCreate).to.be.calledOnce(); - const [contractId, type, ownerId, jsonData, entropy, wif] = wasmSdk.documentCreate.firstCall.args; + const [ + contractId, type, ownerId, jsonData, entropy, wif, + ] = wasmSdk.documentCreate.firstCall.args; // Verify all params except entropy expect(contractId).to.equal('c'); diff --git a/packages/js-evo-sdk/tests/unit/util.spec.mjs b/packages/js-evo-sdk/tests/unit/util.spec.mjs index c98665886bf..a366cee7701 100644 --- a/packages/js-evo-sdk/tests/unit/util.spec.mjs +++ b/packages/js-evo-sdk/tests/unit/util.spec.mjs @@ -3,11 +3,11 @@ import { asJsonString, generateEntropy } from '../../dist/util.js'; describe('Util Functions', () => { describe('asJsonString', () => { it('returns undefined for null', () => { - expect(asJsonString(null)).to.be.undefined; + expect(asJsonString(null)).to.be.undefined(); }); it('returns undefined for undefined', () => { - expect(asJsonString(undefined)).to.be.undefined; + expect(asJsonString(undefined)).to.be.undefined(); }); it('returns string as-is', () => { @@ -53,7 +53,7 @@ describe('Util Functions', () => { // Convert hex string to bytes const bytes = []; for (let i = 0; i < entropy.length; i += 2) { - bytes.push(parseInt(entropy.substr(i, 2), 16)); + bytes.push(parseInt(entropy.substring(i, 2), 16)); } expect(bytes.length).to.equal(32); }); @@ -61,7 +61,7 @@ describe('Util Functions', () => { it('generates values with good distribution', () => { // Generate multiple samples and check that we get a variety of hex digits const samples = []; - for (let i = 0; i < 10; i++) { + for (let i = 0; i < 10; i += 1) { samples.push(generateEntropy()); } @@ -74,4 +74,4 @@ describe('Util Functions', () => { expect(uniqueChars).to.be.at.least(10); }); }); -}); \ No newline at end of file +}); From 99d39394c5dd388465ac5e284a1888ebec55a160 Mon Sep 17 00:00:00 2001 From: thephez Date: Mon, 29 Dec 2025 12:24:34 -0500 Subject: [PATCH 3/3] refactor(sdk): remove dead Node.js randomBytes branch from generateEntropy The check for globalThis.crypto.randomBytes was always false because globalThis.crypto is the Web Crypto API, not the Node.js crypto module. The Web Crypto API (getRandomValues) works in both Node.js v15+ and browsers, making this branch unnecessary dead code. --- packages/js-evo-sdk/src/util.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/js-evo-sdk/src/util.ts b/packages/js-evo-sdk/src/util.ts index 9bd7552afc8..2e26645fc9c 100644 --- a/packages/js-evo-sdk/src/util.ts +++ b/packages/js-evo-sdk/src/util.ts @@ -12,13 +12,7 @@ export function asJsonString(value: unknown): string | undefined { * @throws Error if no secure random source is available */ export function generateEntropy(): string { - // Node.js environment - if (typeof globalThis !== 'undefined' && globalThis.crypto && 'randomBytes' in globalThis.crypto) { - // @ts-ignore - Node.js crypto.randomBytes exists but may not be in types - return globalThis.crypto.randomBytes(32).toString('hex'); - } - - // Browser environment or Node.js with Web Crypto API + // Web Crypto API - works in both Node.js (v15+) and browsers if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.getRandomValues) { const buffer = new Uint8Array(32); globalThis.crypto.getRandomValues(buffer); @@ -32,5 +26,5 @@ export function generateEntropy(): string { return Array.from(buffer).map((b) => b.toString(16).padStart(2, '0')).join(''); } - throw new Error('No secure random source available. This environment does not support crypto.randomBytes or crypto.getRandomValues.'); + throw new Error('No secure random source available. This environment does not support crypto.getRandomValues.'); }