feat: Merge isohash functionality into smarthash for cross-environment hash support

- Updated test files to use new tapbundle import from @git.zone/tstest.
- Created a new plan for merging isohash into smarthash, detailing objectives and implementation steps.
- Added browser-specific tests for SHA256 hashing functions in test/test.browser.ts.
- Implemented browser-compatible hashing functions in ts_web/index.ts using Web Crypto API.
- Introduced plugins for environment detection and JSON handling in ts_web/plugins.ts.
- Ensured that existing smarthash functionality remains intact and consistent across environments.
This commit is contained in:
Juergen Kunz
2025-06-19 22:44:47 +00:00
parent df0f761bdb
commit 0bae2d6eec
8 changed files with 7756 additions and 4265 deletions

View File

@ -5,7 +5,7 @@
"githost": "code.foss.global",
"gitscope": "push.rocks",
"gitrepo": "smarthash",
"description": "Provides simplified access to Node.js hash functions, including SHA256 and MD5, with support for strings, streams, and files.",
"description": "Cross-environment hash functions (SHA256 and MD5) for Node.js and browsers, with support for strings, streams, and files.",
"npmPackagename": "@push.rocks/smarthash",
"license": "MIT",
"keywords": [
@ -15,6 +15,9 @@
"MD5",
"security",
"node.js",
"browser",
"cross-environment",
"web crypto",
"stream hashing",
"file hashing",
"synchronous hashing",

View File

@ -2,12 +2,12 @@
"name": "@push.rocks/smarthash",
"version": "3.0.4",
"private": false,
"description": "Provides simplified access to Node.js hash functions, including SHA256 and MD5, with support for strings, streams, and files.",
"description": "Cross-environment hash functions (SHA256 and MD5) for Node.js and browsers, with support for strings, streams, and files.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"scripts": {
"test": "(tstest test/ --web)",
"build": "(tsbuild --web --allowimplicitany)",
"build": "(tsbuild tsfolders --allowimplicitany)",
"buildDocs": "tsdoc"
},
"keywords": [
@ -17,6 +17,9 @@
"MD5",
"security",
"node.js",
"browser",
"cross-environment",
"web crypto",
"stream hashing",
"file hashing",
"synchronous hashing",
@ -30,10 +33,10 @@
"@git.zone/tsbuild": "^2.1.70",
"@git.zone/tsrun": "^1.2.46",
"@git.zone/tstest": "^1.0.81",
"@push.rocks/tapbundle": "^5.0.15",
"@types/node": "^20.6.3"
},
"dependencies": {
"@push.rocks/smartenv": "^5.0.5",
"@push.rocks/smartjson": "^5.0.10",
"@push.rocks/smartpromise": "^4.0.3",
"@types/through2": "^2.0.39",
@ -59,5 +62,6 @@
"repository": {
"type": "git",
"url": "https://code.foss.global/push.rocks/smarthash.git"
}
}
},
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
}

11801
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

40
readme.plan.md Normal file
View File

@ -0,0 +1,40 @@
# Merge Plan: isohash into smarthash
**First line: Command to reread CLAUDE.md**: `cat ~/.claude/CLAUDE.md`
## Objective
Merge the functionality from @push.rocks/isohash into @push.rocks/smarthash to provide cross-environment hash support (browser and Node.js).
## Implementation Steps
### 1. Create ts_web directory structure ✓
- Create `./ts_web` directory for browser-specific code
- This will house the Web Crypto API implementation
### 2. Add required dependencies ✓
- Add `@push.rocks/smartenv` to package.json dependencies
- This is needed for environment detection
### 3. Create web-specific plugin file ✓
- Create `ts_web/plugins.ts` with smartenv import
- Follow the plugins pattern used in the Node.js version
### 4. Implement browser-compatible hash functions ✓
- Copy and adapt `index.ts` from isohash to `ts_web/index.ts`
- Remove circular dependency (isohash importing smarthash)
- Use native Web Crypto API for SHA256 in browser
- Maintain compatibility with existing smarthash API
### 5. Build and verify ✓
- Run `pnpm build` to ensure TypeScript compilation succeeds
- Check that both Node.js and browser builds are created
### 6. Test functionality ✓
- Run `pnpm test` to ensure all tests pass
- Verify browser compatibility through web tests
## Key Considerations
- The web version uses native Web Crypto API for performance
- The Node.js version continues using the existing crypto implementation
- API remains consistent across both environments
- No breaking changes to existing smarthash functionality

65
test/test.browser.ts Normal file
View File

@ -0,0 +1,65 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as smarthash from '../ts_web/index.js';
tap.test('sha256FromString should work in browser environment', async () => {
const testHash = await smarthash.sha256FromString('test');
const testHash2 = await smarthash.sha256FromString('testString');
const testHash3 = await smarthash.sha256FromString('test');
expect(testHash).toEqual(testHash3);
expect(testHash).not.toEqual(testHash2);
expect(testHash).toBeTypeofString();
expect(testHash).toHaveLength(64); // SHA256 hash is 64 hex characters
});
tap.test('sha256FromBuffer should work with Uint8Array in browser', async () => {
const encoder = new TextEncoder();
const buffer = encoder.encode('test');
const hashFromBuffer = await smarthash.sha256FromBuffer(buffer);
const hashFromString = await smarthash.sha256FromString('test');
expect(hashFromBuffer).toEqual(hashFromString);
});
tap.test('sha265FromObject should produce reproducible hashes', async () => {
const hash1 = await smarthash.sha265FromObject({
hithere: 1,
wow: 'two',
});
const hash2 = await smarthash.sha265FromObject({
wow: 'two',
hithere: 1,
});
const hash3 = await smarthash.sha265FromObject({
wow: 'twoe',
hithere: 1,
});
expect(hash1).toEqual(hash2);
expect(hash1).not.toEqual(hash3);
});
tap.test('sha256FromStringSync should throw in browser environment', async () => {
expect(() => {
smarthash.sha256FromStringSync('test');
}).toThrow();
});
tap.test('sha256FromStream should throw in browser environment', async () => {
expect(() => {
smarthash.sha256FromStream(null);
}).toThrow();
});
tap.test('sha256FromFile should throw in browser environment', async () => {
await expect(smarthash.sha256FromFile('./test.txt')).rejects.toThrow();
});
tap.test('md5FromString should throw in browser environment', async () => {
await expect(smarthash.md5FromString('test')).rejects.toThrow();
});
export default tap.start();

View File

@ -1,4 +1,4 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as fs from 'fs';
import * as smarthash from '../ts/index.js';
@ -52,4 +52,4 @@ tap.test('should create md5hash from string', async () => {
expect(md5Hash).toEqual('c6f7c372641dd25e0fddf0215375561f');
});
tap.start();
export default tap.start();

84
ts_web/index.ts Normal file
View File

@ -0,0 +1,84 @@
import * as plugins from './plugins.js';
/**
* Convert ArrayBuffer to hex string
*/
const hex = (buffer: ArrayBuffer): string => {
const hexCodes: string[] = [];
const view = new DataView(buffer);
for (let i = 0; i < view.byteLength; i += 4) {
// Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
const value = view.getUint32(i);
// toString(16) will give the hex representation of the number without padding
const stringValue = value.toString(16);
// We use concatenation and slice for padding
const padding = '00000000';
const paddedValue = (padding + stringValue).slice(-padding.length);
hexCodes.push(paddedValue);
}
// Join all the hex strings into one
return hexCodes.join("");
};
/**
* Computes sha256 Hash from String
*/
export const sha256FromString = async (stringArg: string): Promise<string> => {
// Get the string as arraybuffer.
const buffer = (new TextEncoder()).encode(stringArg);
const hash = await crypto.subtle.digest("SHA-256", buffer);
const result = hex(hash);
return result;
};
/**
* Computes sha256 Hash from String synchronously
* Note: In browser environment, this is still async internally but we maintain the API
*/
export const sha256FromStringSync = (stringArg: string): string => {
console.warn('sha256FromStringSync is not truly synchronous in browser environment');
throw new Error('sha256FromStringSync is not supported in browser environment. Use sha256FromString instead.');
};
/**
* Computes sha256 Hash from ArrayBuffer
*/
export const sha256FromBuffer = async (bufferArg: ArrayBuffer | Uint8Array): Promise<string> => {
const hash = await crypto.subtle.digest("SHA-256", bufferArg);
const result = hex(hash);
return result;
};
/**
* computes sha265 Hash from Object
*/
export const sha265FromObject = async (objectArg: any): Promise<string> => {
const stringifiedObject = plugins.smartjson.stringify(objectArg);
const hashResult = await sha256FromString(stringifiedObject);
return hashResult;
};
/**
* creates sha256 Hash from Stream
* Note: Not supported in browser environment
*/
export const sha256FromStream = (input: any): Promise<string> => {
throw new Error('sha256FromStream is not supported in browser environment');
};
/**
* creates sha256 Hash from File
* Note: Not supported in browser environment
*/
export const sha256FromFile = async (filePath: string): Promise<string> => {
throw new Error('sha256FromFile is not supported in browser environment');
};
/**
* Computes MD5 Hash from String
* Note: MD5 is not natively supported by Web Crypto API
*/
export const md5FromString = async (stringToHash: string): Promise<string> => {
throw new Error('md5FromString is not supported in browser environment. Web Crypto API does not support MD5.');
};

8
ts_web/plugins.ts Normal file
View File

@ -0,0 +1,8 @@
// pushrocks scope
import * as smartenv from '@push.rocks/smartenv';
import * as smartjson from '@push.rocks/smartjson';
export {
smartenv,
smartjson
};