crypto-hash
Tiny isomorphic hashing module that uses the native crypto API in Node.js and the browser.
Repository Health
Technical Analysis
crypto-hash gives Node.js and browser code the exact same SHA-1/256/384/512 hashing API by delegating to whatever native crypto implementation the runtime already ships — node:crypto on the server, SubtleCrypto (window.crypto.subtle) in the browser. There’s no bundled hashing implementation and no polyfill, so the browser build is only around 300 bytes minified and gzipped, and there are zero runtime dependencies to audit or update.
On Node.js, hashing is offloaded to a lazily-spawned, unref’d worker thread so CPU-bound digest computation never blocks the event loop, while still exposing a plain async function(input, options) call site. Input can be a string, ArrayBuffer, TypedArray, DataView, or Node Buffer (including offset views/subarrays), and the output is either a hex string or a raw ArrayBuffer, selected via an outputFormat option that TypeScript consumers get correctly typed through overloaded signatures.
What You Get
- Four named async exports — sha1, sha256, sha384, sha512 — with an identical
(input, options?) => Promise<string | ArrayBuffer>signature across Node.js and the browser - Automatic dual-entry resolution via package.json
exports(node vs default/browser conditions), so bundlers and Node itself pick the right implementation with no configuration - Node.js hashing runs off the main thread on a lazily-created, unref’d worker (thread.js), so it never blocks the event loop and doesn’t keep the process alive on its own
- Accepts strings, ArrayBuffer, any ArrayBufferView (TypedArray/DataView, including offset subarrays), and Node Buffer as input without requiring manual conversion
- TypeScript types (index.d.ts) with overloaded return types —
outputFormat: 'buffer'is statically typed to returnPromise<ArrayBuffer>, everything else toPromise<string> - Careful buffer-safety handling — every buffer handed to the worker thread or SubtleCrypto is first copied so postMessage’s zero-copy transfer never detaches memory the caller still owns
Common Use Cases
- Hashing request bodies, file contents, or arbitrary buffers for checksums/ETags in a Node.js server without writing separate browser-side hashing code
- Computing content hashes in browser-side JavaScript (e.g. client-side dedup or integrity checks) using the same function names and options as the server code
- Generating cache keys or content-addressable identifiers where a fast, well-known SHA digest is enough and adding a large hashing dependency isn’t worth it
- Offloading hashing work in high-throughput Node.js services so digest computation doesn’t compete with request handling on the main thread
- Cross-environment isomorphic codebases (shared modules used by both a Node backend and a bundled frontend) that need one hashing API instead of two
Under The Hood
Architecture
The package ships two parallel entry points selected automatically through package.json’s exports map (node vs default conditions): index.js for Node.js and browser.js for everything else, both re-exporting a create(algorithm) factory as sha1/sha256/sha384/sha512. A shared utilities.js normalizes every accepted input shape (string, ArrayBuffer, TypedArray, DataView, Node Buffer, including offset subarray views) into a clean buffer and converts digest output to hex via a precomputed lookup table. The Node path additionally offloads the actual digest computation to a lazily-spawned, unref’d worker thread (thread.js), dispatching each call as a {id, value} message and resolving pending promises from a Map keyed by an incrementing task counter, so hashing never blocks the event loop and an idle worker doesn’t keep the process alive. Because index.js and browser.js each define their own create closure independently, keeping their behavior identical is a manual discipline enforced only by the shared utilities and the dual (Node+Playwright) test suite, not by a single shared abstraction.
Tech Stack
Pure ESM JavaScript ("type": "module") with zero runtime dependencies, targeting Node.js 20+, and relying exclusively on the built-in node:crypto, node:worker_threads, and (in the browser) SubtleCrypto.digest. There is no bundled hash implementation, no polyfill for older browsers, and explicitly no Internet Explorer support; the browser path requires a secure (HTTPS) context. Dev tooling is xo (an ESLint preset) for linting, ava for the Node test runner, Playwright for a real-browser test pass, and hash.js plus @sindresorhus/is used only as independent test oracles. There is no build step — the package ships its raw ESM source files as listed in package.json’s files array.
Code Quality test.js (ava) exercises all four algorithms against known fixture digests and cross-checks them against an independent hash.js implementation, plus a comprehensive set of edge cases: ArrayBuffer/TypedArray/Buffer input equivalence, offset/subarray views, hex vs buffer output, non-detachment of buffers after being transferred to the worker, concurrent/parallel execution, and large-buffer transfer performance. A separate test-browser.js runs the same surface through Playwright against a real browser engine. CI (.github/workflows/main.yml) runs the full suite across a small Node.js version matrix. Explicit error handling is limited to the worker’s own ‘error’/‘exit’ listeners rejecting any in-flight tasks; the library otherwise assumes well-typed input. xo enforces consistent style, with a handful of inline eslint-disable comments explaining deliberate exceptions. TypeScript consumers get accurate, overloaded return types via index.d.ts.
API Design
The public surface is intentionally tiny: four named async functions, each (input, {outputFormat}?) => Promise<string | ArrayBuffer>, requiring zero configuration to start hashing. TypeScript overloads make the outputFormat: 'buffer' case statically typed to return ArrayBuffer rather than the default hex string. Every accepted input shape is handled transparently without requiring the caller to pre-convert, and the Node-side worker-thread offload is entirely invisible at the call site — callers get a synchronous-feeling one-liner backed by non-blocking execution. The trade-off is no streaming/incremental hashing API (the whole input must already be in memory), and SHA-1 is exposed with an explicit doc warning against using it for anything security-sensitive.