@altrepo/hash 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # altrepo-hash
2
+
3
+ AltRepo Hash is a lightweight Node.js utility library for generating hashes and verifying checksums for text, bytes, and files.
4
+
5
+ ## Features
6
+
7
+ - **Straightforward API:** Easily generate hashes or verify checksums with a clean, intuitive interface.
8
+ - **Zero Dependencies:** Built entirely on top of Node.js's standard `crypto` and `fs` modules, ensuring a lightweight footprint with no external requirements.
9
+ - **Secure Verifications:** Utilizes constant-time comparisons (`crypto.timingSafeEqual`) to securely verify file and text hashes.
10
+ - **Broad Algorithm Support:** Includes built-in support for all major hashing algorithms including `sha256`, `sha512`, `blake2b512`, `blake2s256`, `md5`, `sha1`, and more.
11
+ - **Memory Efficient:** Safely processes large files in chunks via `hashFile` without loading the entire file into memory using standard readable streams.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install altrepo-hash
17
+ ```
18
+
19
+ ## Example API
20
+
21
+ ```typescript
22
+ import { hashText, hashFile, verifyFileHash } from "altrepo-hash";
23
+
24
+ async function run() {
25
+ console.log(hashText("hello", "sha256"));
26
+
27
+ console.log(await hashFile("download.zip", "sha256"));
28
+
29
+ console.log(
30
+ await verifyFileHash("download.zip", "abc123...", "sha256")
31
+ );
32
+ }
33
+
34
+ run();
35
+ ```
36
+
37
+ ## Browser Version
38
+
39
+ AltRepo Hash is the JS companion to the browser-based hash tools on AltRepo.
40
+
41
+ More hash tools:
42
+ https://altrepo.net/hash/
@@ -0,0 +1,3 @@
1
+ export declare function hashBlob(blob: Blob, algorithm: string): Promise<string>;
2
+ export declare function hashFile(file: File, algorithm: string): Promise<string>;
3
+ export declare function verifyBlobHash(blob: Blob, expectedHash: string, algorithm: string): Promise<boolean>;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hashBlob = hashBlob;
4
+ exports.hashFile = hashFile;
5
+ exports.verifyBlobHash = verifyBlobHash;
6
+ const shared_1 = require("./shared");
7
+ async function hashBlob(blob, algorithm) {
8
+ const arrayBuffer = await blob.arrayBuffer();
9
+ return (0, shared_1.hashBytes)(arrayBuffer, algorithm);
10
+ }
11
+ async function hashFile(file, algorithm) {
12
+ return hashBlob(file, algorithm);
13
+ }
14
+ // Simple constant-time comparison for browser
15
+ function secureCompare(a, b) {
16
+ if (a.length !== b.length) {
17
+ return false;
18
+ }
19
+ let mismatch = 0;
20
+ for (let i = 0; i < a.length; ++i) {
21
+ mismatch |= (a.charCodeAt(i) ^ b.charCodeAt(i));
22
+ }
23
+ return mismatch === 0;
24
+ }
25
+ async function verifyBlobHash(blob, expectedHash, algorithm) {
26
+ const actualHash = await hashBlob(blob, algorithm);
27
+ return secureCompare(actualHash, expectedHash.toLowerCase());
28
+ }
package/dist/node.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function hashFile(filePath: string, algorithm: string): Promise<string>;
2
+ export declare function verifyFileHash(filePath: string, expectedHash: string, algorithm: string): Promise<boolean>;
package/dist/node.js ADDED
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.hashFile = hashFile;
37
+ exports.verifyFileHash = verifyFileHash;
38
+ const crypto = __importStar(require("crypto"));
39
+ const fs = __importStar(require("fs"));
40
+ async function hashFile(filePath, algorithm) {
41
+ return new Promise((resolve, reject) => {
42
+ try {
43
+ // Allow node formats like 'sha256'
44
+ const alg = algorithm.toLowerCase().replace("_", "-").replace("sha-", "sha");
45
+ const hasher = crypto.createHash(alg);
46
+ const stream = fs.createReadStream(filePath);
47
+ stream.on("error", (err) => reject(err));
48
+ stream.on("data", (chunk) => hasher.update(chunk));
49
+ stream.on("end", () => resolve(hasher.digest("hex")));
50
+ }
51
+ catch (err) {
52
+ reject(err);
53
+ }
54
+ });
55
+ }
56
+ function secureCompare(actualHex, expectedHex) {
57
+ const actualBuffer = Buffer.from(actualHex, "hex");
58
+ const expectedBuffer = Buffer.from(expectedHex.toLowerCase(), "hex");
59
+ if (actualBuffer.length !== expectedBuffer.length) {
60
+ return false;
61
+ }
62
+ return crypto.timingSafeEqual(actualBuffer, expectedBuffer);
63
+ }
64
+ async function verifyFileHash(filePath, expectedHash, algorithm) {
65
+ const actualHash = await hashFile(filePath, algorithm);
66
+ return secureCompare(actualHash, expectedHash);
67
+ }
@@ -0,0 +1,10 @@
1
+ export declare const SUPPORTED_ALGORITHMS: string[];
2
+ export declare function supportedAlgorithms(): string[];
3
+ /**
4
+ * Generate a hash string from a Uint8Array or ArrayBuffer.
5
+ */
6
+ export declare function hashBytes(data: BufferSource, algorithm: string): Promise<string>;
7
+ /**
8
+ * Generate a hash string from text.
9
+ */
10
+ export declare function hashText(text: string, algorithm: string): Promise<string>;
package/dist/shared.js ADDED
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SUPPORTED_ALGORITHMS = void 0;
4
+ exports.supportedAlgorithms = supportedAlgorithms;
5
+ exports.hashBytes = hashBytes;
6
+ exports.hashText = hashText;
7
+ exports.SUPPORTED_ALGORITHMS = [
8
+ "sha-1",
9
+ "sha-256",
10
+ "sha-384",
11
+ "sha-512",
12
+ ];
13
+ function supportedAlgorithms() {
14
+ return [...exports.SUPPORTED_ALGORITHMS];
15
+ }
16
+ function normalizeAlgorithm(algorithm) {
17
+ const alg = algorithm.toLowerCase().replace(/_/g, "-");
18
+ if (!alg.startsWith("sha-")) {
19
+ const algWithDash = alg.replace("sha", "sha-");
20
+ if (exports.SUPPORTED_ALGORITHMS.includes(algWithDash)) {
21
+ return algWithDash;
22
+ }
23
+ }
24
+ return alg;
25
+ }
26
+ /**
27
+ * Generate a hash string from a Uint8Array or ArrayBuffer.
28
+ */
29
+ async function hashBytes(data, algorithm) {
30
+ const normalizedAlg = normalizeAlgorithm(algorithm);
31
+ if (!exports.SUPPORTED_ALGORITHMS.includes(normalizedAlg)) {
32
+ throw new Error(`Unsupported algorithm in browser/shared context: ${algorithm}. Supported: ${exports.SUPPORTED_ALGORITHMS.join(", ")}`);
33
+ }
34
+ const hashBuffer = await globalThis.crypto.subtle.digest(normalizedAlg, data);
35
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
36
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
37
+ }
38
+ /**
39
+ * Generate a hash string from text.
40
+ */
41
+ async function hashText(text, algorithm) {
42
+ const encoder = new TextEncoder();
43
+ const data = encoder.encode(text);
44
+ return hashBytes(data, algorithm);
45
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@altrepo/hash",
3
+ "version": "0.1.0",
4
+ "description": "A lightweight multi-environment utility library for generating hashes and verifying checksums for text, bytes, and files.",
5
+ "exports": {
6
+ ".": "./dist/shared.js",
7
+ "./node": "./dist/node.js",
8
+ "./browser": "./dist/browser.js"
9
+ },
10
+ "typesVersions": {
11
+ "*": {
12
+ "node": [
13
+ "dist/node.d.ts"
14
+ ],
15
+ "browser": [
16
+ "dist/browser.d.ts"
17
+ ],
18
+ "*": [
19
+ "dist/shared.d.ts"
20
+ ]
21
+ }
22
+ },
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "test": "vitest run",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "keywords": [
29
+ "hash",
30
+ "checksum",
31
+ "sha256",
32
+ "sha512",
33
+ "sha3",
34
+ "blake2",
35
+ "browser",
36
+ "node",
37
+ "webcrypto",
38
+ "altrepo"
39
+ ],
40
+ "homepage": "https://altrepo.net/hash/text-hash-generator.html",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/alterpoYT/altrepo-hash.git"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/alterpoYT/altrepo-hash/issues"
47
+ },
48
+ "author": "AltRepo <void@altrepo.net>",
49
+ "license": "MIT",
50
+ "devDependencies": {
51
+ "@types/node": "^20.0.0",
52
+ "jsdom": "^29.1.1",
53
+ "typescript": "^5.0.0",
54
+ "vitest": "^1.0.0"
55
+ },
56
+ "engines": {
57
+ "node": ">=18.0.0"
58
+ }
59
+ }
package/src/browser.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { hashBytes } from "./shared";
2
+
3
+ export async function hashBlob(blob: Blob, algorithm: string): Promise<string> {
4
+ const arrayBuffer = await blob.arrayBuffer();
5
+ return hashBytes(arrayBuffer, algorithm);
6
+ }
7
+
8
+ export async function hashFile(file: File, algorithm: string): Promise<string> {
9
+ return hashBlob(file, algorithm);
10
+ }
11
+
12
+ // Simple constant-time comparison for browser
13
+ function secureCompare(a: string, b: string): boolean {
14
+ if (a.length !== b.length) {
15
+ return false;
16
+ }
17
+ let mismatch = 0;
18
+ for (let i = 0; i < a.length; ++i) {
19
+ mismatch |= (a.charCodeAt(i) ^ b.charCodeAt(i));
20
+ }
21
+ return mismatch === 0;
22
+ }
23
+
24
+ export async function verifyBlobHash(blob: Blob, expectedHash: string, algorithm: string): Promise<boolean> {
25
+ const actualHash = await hashBlob(blob, algorithm);
26
+ return secureCompare(actualHash, expectedHash.toLowerCase());
27
+ }
package/src/node.ts ADDED
@@ -0,0 +1,33 @@
1
+ import * as crypto from "crypto";
2
+ import * as fs from "fs";
3
+
4
+ export async function hashFile(filePath: string, algorithm: string): Promise<string> {
5
+ return new Promise((resolve, reject) => {
6
+ try {
7
+ // Allow node formats like 'sha256'
8
+ const alg = algorithm.toLowerCase().replace("_", "-").replace("sha-", "sha");
9
+ const hasher = crypto.createHash(alg);
10
+ const stream = fs.createReadStream(filePath);
11
+
12
+ stream.on("error", (err) => reject(err));
13
+ stream.on("data", (chunk) => hasher.update(chunk));
14
+ stream.on("end", () => resolve(hasher.digest("hex")));
15
+ } catch (err) {
16
+ reject(err);
17
+ }
18
+ });
19
+ }
20
+
21
+ function secureCompare(actualHex: string, expectedHex: string): boolean {
22
+ const actualBuffer = Buffer.from(actualHex, "hex");
23
+ const expectedBuffer = Buffer.from(expectedHex.toLowerCase(), "hex");
24
+ if (actualBuffer.length !== expectedBuffer.length) {
25
+ return false;
26
+ }
27
+ return crypto.timingSafeEqual(actualBuffer, expectedBuffer);
28
+ }
29
+
30
+ export async function verifyFileHash(filePath: string, expectedHash: string, algorithm: string): Promise<boolean> {
31
+ const actualHash = await hashFile(filePath, algorithm);
32
+ return secureCompare(actualHash, expectedHash);
33
+ }
package/src/shared.ts ADDED
@@ -0,0 +1,44 @@
1
+ export const SUPPORTED_ALGORITHMS = [
2
+ "sha-1",
3
+ "sha-256",
4
+ "sha-384",
5
+ "sha-512",
6
+ ];
7
+
8
+ export function supportedAlgorithms(): string[] {
9
+ return [...SUPPORTED_ALGORITHMS];
10
+ }
11
+
12
+ function normalizeAlgorithm(algorithm: string): string {
13
+ const alg = algorithm.toLowerCase().replace(/_/g, "-");
14
+ if (!alg.startsWith("sha-")) {
15
+ const algWithDash = alg.replace("sha", "sha-");
16
+ if (SUPPORTED_ALGORITHMS.includes(algWithDash)) {
17
+ return algWithDash;
18
+ }
19
+ }
20
+ return alg;
21
+ }
22
+
23
+ /**
24
+ * Generate a hash string from a Uint8Array or ArrayBuffer.
25
+ */
26
+ export async function hashBytes(data: BufferSource, algorithm: string): Promise<string> {
27
+ const normalizedAlg = normalizeAlgorithm(algorithm);
28
+ if (!SUPPORTED_ALGORITHMS.includes(normalizedAlg)) {
29
+ throw new Error(`Unsupported algorithm in browser/shared context: ${algorithm}. Supported: ${SUPPORTED_ALGORITHMS.join(", ")}`);
30
+ }
31
+
32
+ const hashBuffer = await globalThis.crypto.subtle.digest(normalizedAlg, data);
33
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
34
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
35
+ }
36
+
37
+ /**
38
+ * Generate a hash string from text.
39
+ */
40
+ export async function hashText(text: string, algorithm: string): Promise<string> {
41
+ const encoder = new TextEncoder();
42
+ const data = encoder.encode(text);
43
+ return hashBytes(data, algorithm);
44
+ }
@@ -0,0 +1,29 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect, beforeAll } from "vitest";
3
+ import { hashBlob, hashFile, verifyBlobHash } from "../src/browser";
4
+
5
+ describe("browser", () => {
6
+ beforeAll(() => {
7
+ // In jsdom WebCrypto isn't perfectly mapped to globalThis.crypto.subtle out of the box in some Node versions,
8
+ // but in modern Node environments globalThis.crypto works directly. We'll map Node's crypto to the global.
9
+ if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
10
+ const crypto = require("crypto");
11
+ Object.defineProperty(globalThis, "crypto", {
12
+ value: crypto.webcrypto
13
+ });
14
+ }
15
+ });
16
+
17
+ it("should hash blob and verify securely", async () => {
18
+ const blob = new Blob(["hello browser"]);
19
+ const expected = "ec687a304db07950a92649222c77ef61efae6e968e62dfb6065a4f242246a760";
20
+ expect(await hashBlob(blob, "sha256")).toBe(expected);
21
+ expect(await verifyBlobHash(blob, expected, "sha256")).toBe(true);
22
+ });
23
+
24
+ it("should hash File object", async () => {
25
+ const file = new File(["hello browser"], "test.txt");
26
+ const expected = "ec687a304db07950a92649222c77ef61efae6e968e62dfb6065a4f242246a760";
27
+ expect(await hashFile(file, "sha256")).toBe(expected);
28
+ });
29
+ });
@@ -0,0 +1,20 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { hashFile, verifyFileHash } from "../src/node";
5
+
6
+ describe("node", () => {
7
+ it("should hash file and verify securely", async () => {
8
+ const tmpPath = path.join(__dirname, "temp-test-file.txt");
9
+ fs.writeFileSync(tmpPath, "hello file");
10
+
11
+ try {
12
+ const expected = "f3877e8a3d98f809d9f844060fbea2864a4b66980a22ff22297014d0c168db2e";
13
+ expect(await hashFile(tmpPath, "sha256")).toBe(expected);
14
+ expect(await verifyFileHash(tmpPath, expected, "sha256")).toBe(true);
15
+ expect(await verifyFileHash(tmpPath, "badhash000000000000000000000000000000000000000000000000000000000", "sha256")).toBe(false);
16
+ } finally {
17
+ fs.unlinkSync(tmpPath);
18
+ }
19
+ });
20
+ });
@@ -0,0 +1,14 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { hashText, hashBytes, supportedAlgorithms } from "../src/shared";
3
+
4
+ describe("shared (WebCrypto)", () => {
5
+ it("should have expected algorithms", () => {
6
+ const algs = supportedAlgorithms();
7
+ expect(algs).toContain("sha-256");
8
+ });
9
+
10
+ it("should hash text via WebCrypto", async () => {
11
+ const expected = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
12
+ expect(await hashText("hello", "sha256")).toBe(expected);
13
+ });
14
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "declaration": true,
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "lib": ["es2022", "dom"]
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "tests", "dist"]
16
+ }