diff --git a/.gitignore b/.gitignore index 8abbdbc..fafe138 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules +/benchmark/node_modules .TODO .DS_Store \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..49ce751 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,6 @@ +npm test +npm run build +npm run denoify +git add deno_dist +git add dist +git add cjs diff --git a/README.md b/README.md index 49b18a0..0e87aaf 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ Parameters: - `salt?: string`: Optional salt string. If not provided, a random salt will be generated. - `saltLength?: number` Optional maximum lenght of the random salt (in bytes, defaults to 12). +Returns: `Promise` + ### `verifySolution(payload, hmacKey)` Verifies an ALTCHA solution. The payload can be a Base64-encoded JSON payload (as submitted by the widget) or an object. @@ -51,6 +53,69 @@ Parameters: - `payload: string | Payload` - `hmacKey: string` +Returns: `Promise` + +### `solveChallenge(challenge, salt, algorithm?, max?, start?)` + +Finds a solution to the given challenge. + +Parameters: + +- `challenge: string` (required): The challenge hash. +- `salt: string` (required): The challenge salt. +- `algorithm?: string`: Optional algorithm (default: `SHA-256`). +- `max?: string`: Optional `maxnumber` to iterate to (default: 1e6). +- `start?: string`: Optional starting number (default: 0). + +Returns: `{ controller: AbortController, promise: Promise }` + +### `solveChallengeWorkers(workerScript, concurrency, challenge, salt, algorithm?, max?, start?)` + +Finds a solution to the given challenge with [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker) running concurrently. + +Parameters: + +- `workerScript: string` (required): The path or URL of the worker script. +- `concurrency: number` (required): The concurrency (number of workers). +- `challenge: string` (required): The challenge hash. +- `salt: string` (required): The challenge salt. +- `algorithm?: string`: Optional algorithm (default: `SHA-256`). +- `max?: string`: Optional `maxnumber` to iterate to (default: 1e6). +- `start?: string`: Optional starting number (default: 0). + +Returns: `Promise` + +Usage with `altcha-lib/worker`: + +```ts +import { solveChallengeWorkers } from 'altcha-lib'; + +const solution = await solveChallengeWorkers( + 'altcha-lib/worker', // URL to + 8, // spawn 8 workers + challenge, + salt, +); +``` + +## Benchmarks + +``` +> solveChallenge() +- n = 1,000............................... 317 ops/s ±2.63% +- n = 10,000.............................. 32 ops/s ±1.88% +- n = 100,000............................. 3 ops/s ±0.34% +- n = 500,000............................. 0 ops/s ±0.32% + +> solveChallengeWorkers() (8 workers) +- n = 1,000............................... 66 ops/s ±3.44% +- n = 10,000.............................. 31 ops/s ±4.28% +- n = 100,000............................. 7 ops/s ±4.40% +- n = 500,000............................. 1 ops/s ±2.49% +``` + +Run with Bun on MacBook Pro M3-Pro. See [/benchmark](/benchmark/) folder for more details. + ## License MIT \ No newline at end of file diff --git a/benchmark/bench.ts b/benchmark/bench.ts new file mode 100644 index 0000000..ce44378 --- /dev/null +++ b/benchmark/bench.ts @@ -0,0 +1,94 @@ +import { benchmark } from './helpers.js'; +import { + createChallenge, + solveChallenge, + solveChallengeWorkers, +} from '../lib/index.js'; + +const hmacKey = 'test'; +const workers = 8; +const workerScript = await import.meta.resolve!('../lib/worker.ts'); + +const challenge1 = await createChallenge({ + hmacKey, + maxNumber: 1000, + number: 1000, +}); + +const challenge2 = await createChallenge({ + hmacKey, + maxNumber: 10000, + number: 10000, +}); + +const challenge3 = await createChallenge({ + hmacKey, + maxNumber: 100000, + number: 100000, +}); + +const challenge4 = await createChallenge({ + hmacKey, + maxNumber: 500000, + number: 500000, +}); + +await benchmark('solveChallenge()', (bench) => { + bench + .add('n = 1,000', async () => { + await solveChallenge(challenge1.challenge, challenge1.salt).promise; + }) + .add('n = 10,000', async () => { + await solveChallenge(challenge2.challenge, challenge2.salt).promise; + }) + .add('n = 100,000', async () => { + await solveChallenge(challenge3.challenge, challenge3.salt).promise; + }) + .add('n = 500,000', async () => { + await solveChallenge(challenge4.challenge, challenge4.salt).promise; + }) +}); + +await benchmark(`solveChallengeWorkers() (${workers} workers)`, (bench) => { + bench + .add('n = 1,000', async () => { + await solveChallengeWorkers( + workerScript, + workers, + challenge1.challenge, + challenge1.salt, + challenge1.algorithm, + challenge1.max, + ); + }) + .add('n = 10,000', async () => { + await solveChallengeWorkers( + workerScript, + workers, + challenge2.challenge, + challenge2.salt, + challenge2.algorithm, + challenge2.max, + ); + }) + .add('n = 100,000', async () => { + await solveChallengeWorkers( + workerScript, + workers, + challenge3.challenge, + challenge3.salt, + challenge3.algorithm, + challenge3.max, + ); + }) + .add('n = 500,000', async () => { + await solveChallengeWorkers( + workerScript, + workers, + challenge4.challenge, + challenge4.salt, + challenge4.algorithm, + challenge4.max, + ); + }); +}); diff --git a/benchmark/helpers.ts b/benchmark/helpers.ts new file mode 100644 index 0000000..5f21624 --- /dev/null +++ b/benchmark/helpers.ts @@ -0,0 +1,27 @@ +import { Bench } from 'tinybench'; + +const NAME_MAX_LEN = 40; + +export async function benchmark(name: string, initFn: (bench: Bench) => void, duration: number = 500) { + const bench = new Bench({ + time: duration, + throws: true, + warmupTime: 2000, + warmupIterations: 100, + }); + initFn(bench); + await bench.run(); + console.log('>', name); + for (let row of bench.table()) { + if (row) { + console.log( + '-', + row['Task Name'].slice(0, NAME_MAX_LEN).padEnd(NAME_MAX_LEN, '.'), + row['ops/sec'].padStart(10, ' '), + 'ops/s', + row['Margin'], + ); + } + } + console.log(''); +} \ No newline at end of file diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json new file mode 100644 index 0000000..fd6f7ae --- /dev/null +++ b/benchmark/package-lock.json @@ -0,0 +1,18 @@ +{ + "name": "benchmark", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "tinybench": "^2.6.0" + } + }, + "node_modules/tinybench": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", + "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", + "dev": true + } + } +} diff --git a/benchmark/package.json b/benchmark/package.json new file mode 100644 index 0000000..0eb41fd --- /dev/null +++ b/benchmark/package.json @@ -0,0 +1,9 @@ +{ + "type": "module", + "scripts": { + "bench": "npx tsx bench.ts" + }, + "devDependencies": { + "tinybench": "^2.6.0" + } +} diff --git a/cjs/dist/crypto.d.ts b/cjs/dist/crypto.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/cjs/dist/crypto.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/cjs/dist/crypto.js b/cjs/dist/crypto.js new file mode 100644 index 0000000..71bf7c4 --- /dev/null +++ b/cjs/dist/crypto.js @@ -0,0 +1,6 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +if (!('crypto' in globalThis)) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + globalThis.crypto = require('node:crypto').webcrypto; +} diff --git a/cjs/dist/helpers.d.ts b/cjs/dist/helpers.d.ts index 9bea651..58941f6 100644 --- a/cjs/dist/helpers.d.ts +++ b/cjs/dist/helpers.d.ts @@ -1,4 +1,6 @@ +import './crypto.js'; import type { Algorithm } from './types.js'; +export declare const encoder: TextEncoder; export declare function ab2hex(ab: ArrayBuffer | Uint8Array): string; export declare function hash(algorithm: Algorithm, str: string): Promise; export declare function hmac(algorithm: Algorithm, str: string, secret: string): Promise; diff --git a/cjs/dist/helpers.js b/cjs/dist/helpers.js index 3ce18e1..ea41865 100644 --- a/cjs/dist/helpers.js +++ b/cjs/dist/helpers.js @@ -1,11 +1,8 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.randomInt = exports.randomBytes = exports.hmac = exports.hash = exports.ab2hex = void 0; -const encoder = new TextEncoder(); -if (!('crypto' in globalThis)) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - globalThis.crypto = require('node:crypto').webcrypto; -} +exports.randomInt = exports.randomBytes = exports.hmac = exports.hash = exports.ab2hex = exports.encoder = void 0; +require("./crypto.js"); +exports.encoder = new TextEncoder(); function ab2hex(ab) { return [...new Uint8Array(ab)] .map((x) => x.toString(16).padStart(2, '0')) @@ -13,15 +10,15 @@ function ab2hex(ab) { } exports.ab2hex = ab2hex; async function hash(algorithm, str) { - return ab2hex(await crypto.subtle.digest(algorithm.toUpperCase(), encoder.encode(str))); + return ab2hex(await crypto.subtle.digest(algorithm.toUpperCase(), exports.encoder.encode(str))); } exports.hash = hash; async function hmac(algorithm, str, secret) { - const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { + const key = await crypto.subtle.importKey('raw', exports.encoder.encode(secret), { name: 'HMAC', hash: algorithm, }, false, ['sign', 'verify']); - return ab2hex(await crypto.subtle.sign('HMAC', key, encoder.encode(str))); + return ab2hex(await crypto.subtle.sign('HMAC', key, exports.encoder.encode(str))); } exports.hmac = hmac; function randomBytes(length) { diff --git a/cjs/dist/index.d.ts b/cjs/dist/index.d.ts index d78ee6f..e95ce80 100644 --- a/cjs/dist/index.d.ts +++ b/cjs/dist/index.d.ts @@ -1,3 +1,8 @@ -import type { Challenge, ChallengeOptions, Payload } from './types.js'; +import type { Challenge, ChallengeOptions, Payload, Solution } from './types.js'; export declare function createChallenge(options: ChallengeOptions): Promise; export declare function verifySolution(payload: string | Payload, hmacKey: string): Promise; +export declare function solveChallenge(challenge: string, salt: string, algorithm?: string, max?: number, start?: number): { + promise: Promise; + controller: AbortController; +}; +export declare function solveChallengeWorkers(workerScript: string | URL, concurrency: number, challenge: string, salt: string, algorithm?: string, max?: number, startNumber?: number): Promise; diff --git a/cjs/dist/index.js b/cjs/dist/index.js index ce71e47..a61eeb2 100644 --- a/cjs/dist/index.js +++ b/cjs/dist/index.js @@ -1,20 +1,21 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.verifySolution = exports.createChallenge = void 0; +exports.solveChallengeWorkers = exports.solveChallenge = exports.verifySolution = exports.createChallenge = void 0; const helpers_js_1 = require("./helpers.js"); const DEFAULT_MAX_NUMBER = 1e6; const DEFAULT_SALT_LEN = 12; const DEFAULT_ALG = 'SHA-256'; async function createChallenge(options) { const algorithm = options.algorithm || DEFAULT_ALG; - const maxNumber = options.maxNumber || DEFAULT_MAX_NUMBER; + const max = options.maxNumber || DEFAULT_MAX_NUMBER; const saltLength = options.saltLength || DEFAULT_SALT_LEN; const salt = options.salt || (0, helpers_js_1.ab2hex)((0, helpers_js_1.randomBytes)(saltLength)); - const number = options.number === void 0 ? (0, helpers_js_1.randomInt)(maxNumber) : options.number; + const number = options.number === void 0 ? (0, helpers_js_1.randomInt)(max) : options.number; const challenge = await (0, helpers_js_1.hash)(algorithm, salt + number); return { algorithm, challenge, + max, salt, signature: await (0, helpers_js_1.hmac)(algorithm, challenge, options.hmacKey), }; @@ -34,3 +35,83 @@ async function verifySolution(payload, hmacKey) { check.signature === payload.signature); } exports.verifySolution = verifySolution; +function solveChallenge(challenge, salt, algorithm = 'SHA-256', max = 1e6, start = 0) { + const controller = new AbortController(); + const promise = new Promise((resolve, reject) => { + const startTime = Date.now(); + const next = (n) => { + if (controller.signal.aborted || n > max) { + resolve(null); + } + else { + hashChallenge(salt, n, algorithm) + .then((t) => { + if (t === challenge) { + resolve({ + number: n, + took: Date.now() - startTime, + }); + } + else { + next(n + 1); + } + }) + .catch(reject); + } + }; + next(start); + }); + return { + promise, + controller, + }; +} +exports.solveChallenge = solveChallenge; +async function solveChallengeWorkers(workerScript, concurrency, challenge, salt, algorithm = 'SHA-256', max = 1e6, startNumber = 0) { + const workers = []; + if (concurrency < 1) { + throw new Error('Wrong number of workers configured.'); + } + if (concurrency > 16) { + throw new Error('Too many workers. Max. 16 allowed workers.'); + } + for (let i = 0; i < concurrency; i++) { + workers.push(new Worker(workerScript, { + type: 'module', + })); + } + const step = Math.ceil(max / concurrency); + const solutions = await Promise.all(workers.map((worker, i) => { + const start = startNumber + i * step; + return new Promise((resolve) => { + worker.addEventListener('message', (message) => { + if (message.data) { + for (const w of workers) { + if (w !== worker) { + w.postMessage({ type: 'abort' }); + } + } + } + resolve(message.data); + }); + worker.postMessage({ + payload: { + algorithm, + challenge, + max: start + step, + salt, + start, + }, + type: 'work', + }); + }); + })); + for (const worker of workers) { + worker.terminate(); + } + return solutions.find((solution) => !!solution) || null; +} +exports.solveChallengeWorkers = solveChallengeWorkers; +async function hashChallenge(salt, num, algorithm) { + return (0, helpers_js_1.ab2hex)(await crypto.subtle.digest(algorithm.toUpperCase(), helpers_js_1.encoder.encode(salt + num))); +} diff --git a/cjs/dist/types.d.ts b/cjs/dist/types.d.ts index 3c9cba4..6603a73 100644 --- a/cjs/dist/types.d.ts +++ b/cjs/dist/types.d.ts @@ -2,6 +2,7 @@ export type Algorithm = 'SHA-1' | 'SHA-256' | 'SHA-512'; export interface Challenge { algorithm: Algorithm; challenge: string; + max?: number; salt: string; signature: string; } @@ -20,3 +21,8 @@ export interface Payload { salt: string; signature: string; } +export interface Solution { + number: number; + took: number; + worker?: boolean; +} diff --git a/cjs/dist/worker.d.ts b/cjs/dist/worker.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/cjs/dist/worker.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/cjs/dist/worker.js b/cjs/dist/worker.js new file mode 100644 index 0000000..b6715b3 --- /dev/null +++ b/cjs/dist/worker.js @@ -0,0 +1,19 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_js_1 = require("./index.js"); +let controller = undefined; +onmessage = async (message) => { + const { type, payload } = message.data; + if (type === 'abort') { + controller?.abort(); + controller = undefined; + } + else if (type === 'work') { + const { alg, challenge, max, salt, start } = payload || {}; + const result = (0, index_js_1.solveChallenge)(challenge, salt, alg, max, start); + controller = result.controller; + result.promise.then((solution) => { + self.postMessage(solution ? { ...solution, worker: true } : solution); + }); + } +}; diff --git a/deno_dist/README.md b/deno_dist/README.md index 2cffaae..0e87aaf 100644 --- a/deno_dist/README.md +++ b/deno_dist/README.md @@ -37,8 +37,12 @@ Parameters: - `options: ChallengeOptions`: - `algorithm?: string`: Algorithm to use (`SHA-1`, `SHA-256`, `SHA-512`, default: `SHA-256`). - `hmacKey: string` (required): Signature HMAC key. + - `maxNumber?: number` Optional maximum number for the random number generator (defaults to 1,000,000). - `number?: number`: Optional number to use. If not provided, a random number will be generated. - `salt?: string`: Optional salt string. If not provided, a random salt will be generated. + - `saltLength?: number` Optional maximum lenght of the random salt (in bytes, defaults to 12). + +Returns: `Promise` ### `verifySolution(payload, hmacKey)` @@ -49,6 +53,69 @@ Parameters: - `payload: string | Payload` - `hmacKey: string` +Returns: `Promise` + +### `solveChallenge(challenge, salt, algorithm?, max?, start?)` + +Finds a solution to the given challenge. + +Parameters: + +- `challenge: string` (required): The challenge hash. +- `salt: string` (required): The challenge salt. +- `algorithm?: string`: Optional algorithm (default: `SHA-256`). +- `max?: string`: Optional `maxnumber` to iterate to (default: 1e6). +- `start?: string`: Optional starting number (default: 0). + +Returns: `{ controller: AbortController, promise: Promise }` + +### `solveChallengeWorkers(workerScript, concurrency, challenge, salt, algorithm?, max?, start?)` + +Finds a solution to the given challenge with [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker) running concurrently. + +Parameters: + +- `workerScript: string` (required): The path or URL of the worker script. +- `concurrency: number` (required): The concurrency (number of workers). +- `challenge: string` (required): The challenge hash. +- `salt: string` (required): The challenge salt. +- `algorithm?: string`: Optional algorithm (default: `SHA-256`). +- `max?: string`: Optional `maxnumber` to iterate to (default: 1e6). +- `start?: string`: Optional starting number (default: 0). + +Returns: `Promise` + +Usage with `altcha-lib/worker`: + +```ts +import { solveChallengeWorkers } from 'altcha-lib'; + +const solution = await solveChallengeWorkers( + 'altcha-lib/worker', // URL to + 8, // spawn 8 workers + challenge, + salt, +); +``` + +## Benchmarks + +``` +> solveChallenge() +- n = 1,000............................... 317 ops/s ±2.63% +- n = 10,000.............................. 32 ops/s ±1.88% +- n = 100,000............................. 3 ops/s ±0.34% +- n = 500,000............................. 0 ops/s ±0.32% + +> solveChallengeWorkers() (8 workers) +- n = 1,000............................... 66 ops/s ±3.44% +- n = 10,000.............................. 31 ops/s ±4.28% +- n = 100,000............................. 7 ops/s ±4.40% +- n = 500,000............................. 1 ops/s ±2.49% +``` + +Run with Bun on MacBook Pro M3-Pro. See [/benchmark](/benchmark/) folder for more details. + ## License MIT \ No newline at end of file diff --git a/deno_dist/crypto.ts b/deno_dist/crypto.ts new file mode 100644 index 0000000..b35a7f9 --- /dev/null +++ b/deno_dist/crypto.ts @@ -0,0 +1,4 @@ +if (!('crypto' in globalThis)) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + globalThis.crypto = require('node:crypto').webcrypto; +} diff --git a/deno_dist/helpers.ts b/deno_dist/helpers.ts index 04c609c..9eaf36b 100644 --- a/deno_dist/helpers.ts +++ b/deno_dist/helpers.ts @@ -1,11 +1,7 @@ +import './crypto.ts'; import type { Algorithm } from './types.ts'; -const encoder = new TextEncoder(); - -if (!('crypto' in globalThis)) { - // @ts-ignore - globalThis.crypto = (await import('node:crypto')).webcrypto; -} +export const encoder = new TextEncoder(); export function ab2hex(ab: ArrayBuffer | Uint8Array) { return [...new Uint8Array(ab)] diff --git a/deno_dist/index.ts b/deno_dist/index.ts index 0075414..f6d0ab9 100644 --- a/deno_dist/index.ts +++ b/deno_dist/index.ts @@ -1,25 +1,36 @@ -import { ab2hex, hash, hmac, randomBytes, randomInt } from './helpers.ts'; +import { + ab2hex, + encoder, + hash, + hmac, + randomBytes, + randomInt, +} from './helpers.ts'; import type { Algorithm, Challenge, ChallengeOptions, Payload, + Solution, } from './types.ts'; -const DEFAULT_MAX_NUMBER = 1e7; +const DEFAULT_MAX_NUMBER = 1e6; +const DEFAULT_SALT_LEN = 12; const DEFAULT_ALG: Algorithm = 'SHA-256'; export async function createChallenge( options: ChallengeOptions ): Promise { const algorithm = options.algorithm || DEFAULT_ALG; - const salt = options.salt || ab2hex(randomBytes(12)); - const number = - options.number === void 0 ? randomInt(DEFAULT_MAX_NUMBER) : options.number; + const max = options.maxNumber || DEFAULT_MAX_NUMBER; + const saltLength = options.saltLength || DEFAULT_SALT_LEN; + const salt = options.salt || ab2hex(randomBytes(saltLength)); + const number = options.number === void 0 ? randomInt(max) : options.number; const challenge = await hash(algorithm, salt + number); return { algorithm, challenge, + max, salt, signature: await hmac(algorithm, challenge, options.hmacKey), }; @@ -43,3 +54,105 @@ export async function verifySolution( check.signature === payload.signature ); } + +export function solveChallenge( + challenge: string, + salt: string, + algorithm: string = 'SHA-256', + max: number = 1e6, + start: number = 0 +): { promise: Promise; controller: AbortController } { + const controller = new AbortController(); + const promise = new Promise((resolve, reject) => { + const startTime = Date.now(); + const next = (n: number) => { + if (controller.signal.aborted || n > max) { + resolve(null); + } else { + hashChallenge(salt, n, algorithm) + .then((t) => { + if (t === challenge) { + resolve({ + number: n, + took: Date.now() - startTime, + }); + } else { + next(n + 1); + } + }) + .catch(reject); + } + }; + next(start); + }) as Promise; + return { + promise, + controller, + }; +} + +export async function solveChallengeWorkers( + workerScript: string | URL, + concurrency: number, + challenge: string, + salt: string, + algorithm: string = 'SHA-256', + max: number = 1e6, + startNumber: number = 0 +) { + const workers: Worker[] = []; + if (concurrency < 1) { + throw new Error('Wrong number of workers configured.'); + } + if (concurrency > 16) { + throw new Error('Too many workers. Max. 16 allowed workers.'); + } + for (let i = 0; i < concurrency; i++) { + workers.push( + new Worker(workerScript, { + type: 'module', + }) + ); + } + const step = Math.ceil(max / concurrency); + const solutions = await Promise.all( + workers.map((worker, i) => { + const start = startNumber + i * step; + return new Promise((resolve) => { + worker.addEventListener('message', (message: MessageEvent) => { + if (message.data) { + for (const w of workers) { + if (w !== worker) { + w.postMessage({ type: 'abort' }); + } + } + } + resolve(message.data); + }); + worker.postMessage({ + payload: { + algorithm, + challenge, + max: start + step, + salt, + start, + }, + type: 'work', + }); + }) as Promise; + }) + ); + for (const worker of workers) { + worker.terminate(); + } + return solutions.find((solution) => !!solution) || null; +} + +async function hashChallenge(salt: string, num: number, algorithm: string) { + return ab2hex( + await crypto.subtle.digest( + algorithm.toUpperCase(), + encoder.encode(salt + num) + ) + ); +} diff --git a/deno_dist/types.ts b/deno_dist/types.ts index 7fed753..909e84e 100644 --- a/deno_dist/types.ts +++ b/deno_dist/types.ts @@ -3,6 +3,7 @@ export type Algorithm = 'SHA-1' | 'SHA-256' | 'SHA-512'; export interface Challenge { algorithm: Algorithm; challenge: string; + max?: number; salt: string; signature: string; } @@ -10,8 +11,10 @@ export interface Challenge { export interface ChallengeOptions { algorithm?: Algorithm; hmacKey: string; + maxNumber?: number; number?: number; salt?: string; + saltLength?: number; } export interface Payload { @@ -21,3 +24,9 @@ export interface Payload { salt: string; signature: string; } + +export interface Solution { + number: number; + took: number; + worker?: boolean; +} diff --git a/deno_dist/worker.ts b/deno_dist/worker.ts new file mode 100644 index 0000000..043be7e --- /dev/null +++ b/deno_dist/worker.ts @@ -0,0 +1,20 @@ +import { solveChallenge } from './index.ts'; + +let controller: AbortController | undefined = undefined; + +onmessage = async (message) => { + const { type, payload } = message.data; + if (type === 'abort') { + controller?.abort(); + controller = undefined; + } else if (type === 'work') { + const { alg, challenge, max, salt, start } = payload || {}; + const result = solveChallenge(challenge, salt, alg, max, start); + controller = result.controller; + result.promise.then((solution) => { + self.postMessage(solution ? { ...solution, worker: true } : solution); + }); + } +}; + +export {}; diff --git a/dist/crypto.d.ts b/dist/crypto.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/crypto.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/crypto.js b/dist/crypto.js new file mode 100644 index 0000000..9a3ed67 --- /dev/null +++ b/dist/crypto.js @@ -0,0 +1,5 @@ +if (!('crypto' in globalThis)) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + globalThis.crypto = require('node:crypto').webcrypto; +} +export {}; diff --git a/dist/helpers.d.ts b/dist/helpers.d.ts index 9bea651..58941f6 100644 --- a/dist/helpers.d.ts +++ b/dist/helpers.d.ts @@ -1,4 +1,6 @@ +import './crypto.js'; import type { Algorithm } from './types.js'; +export declare const encoder: TextEncoder; export declare function ab2hex(ab: ArrayBuffer | Uint8Array): string; export declare function hash(algorithm: Algorithm, str: string): Promise; export declare function hmac(algorithm: Algorithm, str: string, secret: string): Promise; diff --git a/dist/helpers.js b/dist/helpers.js index bd2f842..10ff6df 100644 --- a/dist/helpers.js +++ b/dist/helpers.js @@ -1,8 +1,5 @@ -const encoder = new TextEncoder(); -if (!('crypto' in globalThis)) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - globalThis.crypto = require('node:crypto').webcrypto; -} +import './crypto.js'; +export const encoder = new TextEncoder(); export function ab2hex(ab) { return [...new Uint8Array(ab)] .map((x) => x.toString(16).padStart(2, '0')) diff --git a/dist/index.d.ts b/dist/index.d.ts index d78ee6f..e95ce80 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,3 +1,8 @@ -import type { Challenge, ChallengeOptions, Payload } from './types.js'; +import type { Challenge, ChallengeOptions, Payload, Solution } from './types.js'; export declare function createChallenge(options: ChallengeOptions): Promise; export declare function verifySolution(payload: string | Payload, hmacKey: string): Promise; +export declare function solveChallenge(challenge: string, salt: string, algorithm?: string, max?: number, start?: number): { + promise: Promise; + controller: AbortController; +}; +export declare function solveChallengeWorkers(workerScript: string | URL, concurrency: number, challenge: string, salt: string, algorithm?: string, max?: number, startNumber?: number): Promise; diff --git a/dist/index.js b/dist/index.js index cbf485c..2680d60 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,17 +1,18 @@ -import { ab2hex, hash, hmac, randomBytes, randomInt } from './helpers.js'; +import { ab2hex, encoder, hash, hmac, randomBytes, randomInt, } from './helpers.js'; const DEFAULT_MAX_NUMBER = 1e6; const DEFAULT_SALT_LEN = 12; const DEFAULT_ALG = 'SHA-256'; export async function createChallenge(options) { const algorithm = options.algorithm || DEFAULT_ALG; - const maxNumber = options.maxNumber || DEFAULT_MAX_NUMBER; + const max = options.maxNumber || DEFAULT_MAX_NUMBER; const saltLength = options.saltLength || DEFAULT_SALT_LEN; const salt = options.salt || ab2hex(randomBytes(saltLength)); - const number = options.number === void 0 ? randomInt(maxNumber) : options.number; + const number = options.number === void 0 ? randomInt(max) : options.number; const challenge = await hash(algorithm, salt + number); return { algorithm, challenge, + max, salt, signature: await hmac(algorithm, challenge, options.hmacKey), }; @@ -29,3 +30,81 @@ export async function verifySolution(payload, hmacKey) { return (check.challenge === payload.challenge && check.signature === payload.signature); } +export function solveChallenge(challenge, salt, algorithm = 'SHA-256', max = 1e6, start = 0) { + const controller = new AbortController(); + const promise = new Promise((resolve, reject) => { + const startTime = Date.now(); + const next = (n) => { + if (controller.signal.aborted || n > max) { + resolve(null); + } + else { + hashChallenge(salt, n, algorithm) + .then((t) => { + if (t === challenge) { + resolve({ + number: n, + took: Date.now() - startTime, + }); + } + else { + next(n + 1); + } + }) + .catch(reject); + } + }; + next(start); + }); + return { + promise, + controller, + }; +} +export async function solveChallengeWorkers(workerScript, concurrency, challenge, salt, algorithm = 'SHA-256', max = 1e6, startNumber = 0) { + const workers = []; + if (concurrency < 1) { + throw new Error('Wrong number of workers configured.'); + } + if (concurrency > 16) { + throw new Error('Too many workers. Max. 16 allowed workers.'); + } + for (let i = 0; i < concurrency; i++) { + workers.push(new Worker(workerScript, { + type: 'module', + })); + } + const step = Math.ceil(max / concurrency); + const solutions = await Promise.all(workers.map((worker, i) => { + const start = startNumber + i * step; + return new Promise((resolve) => { + worker.addEventListener('message', (message) => { + if (message.data) { + for (const w of workers) { + if (w !== worker) { + w.postMessage({ type: 'abort' }); + } + } + } + resolve(message.data); + }); + worker.postMessage({ + payload: { + algorithm, + challenge, + max: start + step, + salt, + start, + }, + type: 'work', + }); + }); + })); + for (const worker of workers) { + worker.terminate(); + } + return solutions.find((solution) => !!solution) || null; +} +async function hashChallenge(salt, num, algorithm) { + return ab2hex(await crypto.subtle.digest(algorithm.toUpperCase(), encoder.encode(salt + num))); +} diff --git a/dist/types.d.ts b/dist/types.d.ts index 3c9cba4..6603a73 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -2,6 +2,7 @@ export type Algorithm = 'SHA-1' | 'SHA-256' | 'SHA-512'; export interface Challenge { algorithm: Algorithm; challenge: string; + max?: number; salt: string; signature: string; } @@ -20,3 +21,8 @@ export interface Payload { salt: string; signature: string; } +export interface Solution { + number: number; + took: number; + worker?: boolean; +} diff --git a/dist/worker.d.ts b/dist/worker.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/worker.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/worker.js b/dist/worker.js new file mode 100644 index 0000000..72124ae --- /dev/null +++ b/dist/worker.js @@ -0,0 +1,17 @@ +import { solveChallenge } from './index.js'; +let controller = undefined; +onmessage = async (message) => { + const { type, payload } = message.data; + if (type === 'abort') { + controller?.abort(); + controller = undefined; + } + else if (type === 'work') { + const { alg, challenge, max, salt, start } = payload || {}; + const result = solveChallenge(challenge, salt, alg, max, start); + controller = result.controller; + result.promise.then((solution) => { + self.postMessage(solution ? { ...solution, worker: true } : solution); + }); + } +}; diff --git a/lib/crypto.ts b/lib/crypto.ts new file mode 100644 index 0000000..b35a7f9 --- /dev/null +++ b/lib/crypto.ts @@ -0,0 +1,4 @@ +if (!('crypto' in globalThis)) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + globalThis.crypto = require('node:crypto').webcrypto; +} diff --git a/lib/helpers.ts b/lib/helpers.ts index 2807099..a494620 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -1,11 +1,7 @@ +import './crypto.js'; import type { Algorithm } from './types.js'; -const encoder = new TextEncoder(); - -if (!('crypto' in globalThis)) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - globalThis.crypto = require('node:crypto').webcrypto; -} +export const encoder = new TextEncoder(); export function ab2hex(ab: ArrayBuffer | Uint8Array) { return [...new Uint8Array(ab)] diff --git a/lib/index.ts b/lib/index.ts index e2bb47f..a70ead6 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,9 +1,17 @@ -import { ab2hex, hash, hmac, randomBytes, randomInt } from './helpers.js'; +import { + ab2hex, + encoder, + hash, + hmac, + randomBytes, + randomInt, +} from './helpers.js'; import type { Algorithm, Challenge, ChallengeOptions, Payload, + Solution, } from './types.js'; const DEFAULT_MAX_NUMBER = 1e6; @@ -14,15 +22,15 @@ export async function createChallenge( options: ChallengeOptions ): Promise { const algorithm = options.algorithm || DEFAULT_ALG; - const maxNumber = options.maxNumber || DEFAULT_MAX_NUMBER; + const max = options.maxNumber || DEFAULT_MAX_NUMBER; const saltLength = options.saltLength || DEFAULT_SALT_LEN; const salt = options.salt || ab2hex(randomBytes(saltLength)); - const number = - options.number === void 0 ? randomInt(maxNumber) : options.number; + const number = options.number === void 0 ? randomInt(max) : options.number; const challenge = await hash(algorithm, salt + number); return { algorithm, challenge, + max, salt, signature: await hmac(algorithm, challenge, options.hmacKey), }; @@ -46,3 +54,105 @@ export async function verifySolution( check.signature === payload.signature ); } + +export function solveChallenge( + challenge: string, + salt: string, + algorithm: string = 'SHA-256', + max: number = 1e6, + start: number = 0 +): { promise: Promise; controller: AbortController } { + const controller = new AbortController(); + const promise = new Promise((resolve, reject) => { + const startTime = Date.now(); + const next = (n: number) => { + if (controller.signal.aborted || n > max) { + resolve(null); + } else { + hashChallenge(salt, n, algorithm) + .then((t) => { + if (t === challenge) { + resolve({ + number: n, + took: Date.now() - startTime, + }); + } else { + next(n + 1); + } + }) + .catch(reject); + } + }; + next(start); + }) as Promise; + return { + promise, + controller, + }; +} + +export async function solveChallengeWorkers( + workerScript: string | URL, + concurrency: number, + challenge: string, + salt: string, + algorithm: string = 'SHA-256', + max: number = 1e6, + startNumber: number = 0 +) { + const workers: Worker[] = []; + if (concurrency < 1) { + throw new Error('Wrong number of workers configured.'); + } + if (concurrency > 16) { + throw new Error('Too many workers. Max. 16 allowed workers.'); + } + for (let i = 0; i < concurrency; i++) { + workers.push( + new Worker(workerScript, { + type: 'module', + }) + ); + } + const step = Math.ceil(max / concurrency); + const solutions = await Promise.all( + workers.map((worker, i) => { + const start = startNumber + i * step; + return new Promise((resolve) => { + worker.addEventListener('message', (message: MessageEvent) => { + if (message.data) { + for (const w of workers) { + if (w !== worker) { + w.postMessage({ type: 'abort' }); + } + } + } + resolve(message.data); + }); + worker.postMessage({ + payload: { + algorithm, + challenge, + max: start + step, + salt, + start, + }, + type: 'work', + }); + }) as Promise; + }) + ); + for (const worker of workers) { + worker.terminate(); + } + return solutions.find((solution) => !!solution) || null; +} + +async function hashChallenge(salt: string, num: number, algorithm: string) { + return ab2hex( + await crypto.subtle.digest( + algorithm.toUpperCase(), + encoder.encode(salt + num) + ) + ); +} diff --git a/lib/types.ts b/lib/types.ts index 48ba028..909e84e 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -3,6 +3,7 @@ export type Algorithm = 'SHA-1' | 'SHA-256' | 'SHA-512'; export interface Challenge { algorithm: Algorithm; challenge: string; + max?: number; salt: string; signature: string; } @@ -23,3 +24,9 @@ export interface Payload { salt: string; signature: string; } + +export interface Solution { + number: number; + took: number; + worker?: boolean; +} diff --git a/lib/worker.ts b/lib/worker.ts new file mode 100644 index 0000000..0e01325 --- /dev/null +++ b/lib/worker.ts @@ -0,0 +1,20 @@ +import { solveChallenge } from './index.js'; + +let controller: AbortController | undefined = undefined; + +onmessage = async (message) => { + const { type, payload } = message.data; + if (type === 'abort') { + controller?.abort(); + controller = undefined; + } else if (type === 'work') { + const { alg, challenge, max, salt, start } = payload || {}; + const result = solveChallenge(challenge, salt, alg, max, start); + controller = result.controller; + result.promise.then((solution) => { + self.postMessage(solution ? { ...solution, worker: true } : solution); + }); + } +}; + +export {}; diff --git a/package-lock.json b/package-lock.json index 64dfc13..b8d7639 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { "name": "altcha-lib", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "altcha-lib", - "version": "0.1.1", + "version": "0.1.2", "license": "MIT", "devDependencies": { "@types/node": "^20.9.0", "@typescript-eslint/eslint-plugin": "^6.21.0", "denoify": "^1.6.9", "eslint": "^8.56.0", + "husky": "^9.0.11", "prettier": "^3.2.5", "rimraf": "^5.0.5", "ts-node": "^10.9.1", @@ -953,9 +954,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.10.0.tgz", - "integrity": "sha512-/MeDQmcD96nVoRumKUljsYOLqfv1YFJps+0pTrb2Z9Nl/w5qNUysMaWQsrd1mvAlNT4yza1iVyIu4Q4AgF6V3A==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.0.tgz", + "integrity": "sha512-jwXtxYbRt1V+CdQSy6Z+uZti7JF5irRKF8hlKfEnF/xJpcNGuuiZMBvuoYM+x9sr9iWGnzrlM0+9hvQ1kgkf1w==", "cpu": [ "arm" ], @@ -966,9 +967,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.10.0.tgz", - "integrity": "sha512-lvu0jK97mZDJdpZKDnZI93I0Om8lSDaiPx3OiCk0RXn3E8CMPJNS/wxjAvSJJzhhZpfjXsjLWL8LnS6qET4VNQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.0.tgz", + "integrity": "sha512-fI9nduZhCccjzlsA/OuAwtFGWocxA4gqXGTLvOyiF8d+8o0fZUeSztixkYjcGq1fGZY3Tkq4yRvHPFxU+jdZ9Q==", "cpu": [ "arm64" ], @@ -979,9 +980,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.10.0.tgz", - "integrity": "sha512-uFpayx8I8tyOvDkD7X6n0PriDRWxcqEjqgtlxnUA/G9oS93ur9aZ8c8BEpzFmsed1TH5WZNG5IONB8IiW90TQg==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.0.tgz", + "integrity": "sha512-BcnSPRM76/cD2gQC+rQNGBN6GStBs2pl/FpweW8JYuz5J/IEa0Fr4AtrPv766DB/6b2MZ/AfSIOSGw3nEIP8SA==", "cpu": [ "arm64" ], @@ -992,9 +993,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.10.0.tgz", - "integrity": "sha512-nIdCX03qFKoR/MwQegQBK+qZoSpO3LESurVAC6s6jazLA1Mpmgzo3Nj3H1vydXp/JM29bkCiuF7tDuToj4+U9Q==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.0.tgz", + "integrity": "sha512-LDyFB9GRolGN7XI6955aFeI3wCdCUszFWumWU0deHA8VpR3nWRrjG6GtGjBrQxQKFevnUTHKCfPR4IvrW3kCgQ==", "cpu": [ "x64" ], @@ -1005,9 +1006,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.10.0.tgz", - "integrity": "sha512-Fz7a+y5sYhYZMQFRkOyCs4PLhICAnxRX/GnWYReaAoruUzuRtcf+Qnw+T0CoAWbHCuz2gBUwmWnUgQ67fb3FYw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.0.tgz", + "integrity": "sha512-ygrGVhQP47mRh0AAD0zl6QqCbNsf0eTo+vgwkY6LunBcg0f2Jv365GXlDUECIyoXp1kKwL5WW6rsO429DBY/bA==", "cpu": [ "arm" ], @@ -1018,9 +1019,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.10.0.tgz", - "integrity": "sha512-yPtF9jIix88orwfTi0lJiqINnlWo6p93MtZEoaehZnmCzEmLL0eqjA3eGVeyQhMtxdV+Mlsgfwhh0+M/k1/V7Q==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.0.tgz", + "integrity": "sha512-x+uJ6MAYRlHGe9wi4HQjxpaKHPM3d3JjqqCkeC5gpnnI6OWovLdXTpfa8trjxPLnWKyBsSi5kne+146GAxFt4A==", "cpu": [ "arm64" ], @@ -1031,9 +1032,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.10.0.tgz", - "integrity": "sha512-9GW9yA30ib+vfFiwjX+N7PnjTnCMiUffhWj4vkG4ukYv1kJ4T9gHNg8zw+ChsOccM27G9yXrEtMScf1LaCuoWQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.0.tgz", + "integrity": "sha512-nrRw8ZTQKg6+Lttwqo6a2VxR9tOroa2m91XbdQ2sUUzHoedXlsyvY1fN4xWdqz8PKmf4orDwejxXHjh7YBGUCA==", "cpu": [ "arm64" ], @@ -1043,10 +1044,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.0.tgz", + "integrity": "sha512-xV0d5jDb4aFu84XKr+lcUJ9y3qpIWhttO3Qev97z8DKLXR62LC3cXT/bMZXrjLF9X+P5oSmJTzAhqwUbY96PnA==", + "cpu": [ + "ppc64le" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.10.0.tgz", - "integrity": "sha512-X1ES+V4bMq2ws5fF4zHornxebNxMXye0ZZjUrzOrf7UMx1d6wMQtfcchZ8SqUnQPPHdOyOLW6fTcUiFgHFadRA==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.0.tgz", + "integrity": "sha512-SDDhBQwZX6LPRoPYjAZWyL27LbcBo7WdBFWJi5PI9RPCzU8ijzkQn7tt8NXiXRiFMJCVpkuMkBf4OxSxVMizAw==", "cpu": [ "riscv64" ], @@ -1056,10 +1070,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.0.tgz", + "integrity": "sha512-RxB/qez8zIDshNJDufYlTT0ZTVut5eCpAZ3bdXDU9yTxBzui3KhbGjROK2OYTTor7alM7XBhssgoO3CZ0XD3qA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.10.0.tgz", - "integrity": "sha512-w/5OpT2EnI/Xvypw4FIhV34jmNqU5PZjZue2l2Y3ty1Ootm3SqhI+AmfhlUYGBTd9JnpneZCDnt3uNOiOBkMyw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.0.tgz", + "integrity": "sha512-C6y6z2eCNCfhZxT9u+jAM2Fup89ZjiG5pIzZIDycs1IwESviLxwkQcFRGLjnDrP+PT+v5i4YFvlcfAs+LnreXg==", "cpu": [ "x64" ], @@ -1070,9 +1097,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.10.0.tgz", - "integrity": "sha512-q/meftEe3QlwQiGYxD9rWwB21DoKQ9Q8wA40of/of6yGHhZuGfZO0c3WYkN9dNlopHlNT3mf5BPsUSxoPuVQaw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.0.tgz", + "integrity": "sha512-i0QwbHYfnOMYsBEyjxcwGu5SMIi9sImDVjDg087hpzXqhBSosxkE7gyIYFHgfFl4mr7RrXksIBZ4DoLoP4FhJg==", "cpu": [ "x64" ], @@ -1083,9 +1110,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.10.0.tgz", - "integrity": "sha512-NrR6667wlUfP0BHaEIKgYM/2va+Oj+RjZSASbBMnszM9k+1AmliRjHc3lJIiOehtSSjqYiO7R6KLNrWOX+YNSQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.0.tgz", + "integrity": "sha512-Fq52EYb0riNHLBTAcL0cun+rRwyZ10S9vKzhGKKgeD+XbwunszSY0rVMco5KbOsTlwovP2rTOkiII/fQ4ih/zQ==", "cpu": [ "arm64" ], @@ -1096,9 +1123,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.10.0.tgz", - "integrity": "sha512-FV0Tpt84LPYDduIDcXvEC7HKtyXxdvhdAOvOeWMWbQNulxViH2O07QXkT/FffX4FqEI02jEbCJbr+YcuKdyyMg==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.0.tgz", + "integrity": "sha512-e/PBHxPdJ00O9p5Ui43+vixSgVf4NlLsmV6QneGERJ3lnjIua/kim6PRFe3iDueT1rQcgSkYP8ZBBXa/h4iPvw==", "cpu": [ "ia32" ], @@ -1109,9 +1136,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.10.0.tgz", - "integrity": "sha512-OZoJd+o5TaTSQeFFQ6WjFCiltiYVjIdsXxwu/XZ8qRpsvMQr4UsVrE5UyT9RIvsnuF47DqkJKhhVZ2Q9YW9IpQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.0.tgz", + "integrity": "sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag==", "cpu": [ "x64" ], @@ -2585,6 +2612,21 @@ "node": ">=16.17.0" } }, + "node_modules/husky": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", + "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "dev": true, + "bin": { + "husky": "bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -3276,9 +3318,9 @@ } }, "node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "dev": true, "funding": [ { @@ -3297,7 +3339,7 @@ "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -3432,9 +3474,9 @@ } }, "node_modules/rollup": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.10.0.tgz", - "integrity": "sha512-t2v9G2AKxcQ8yrG+WGxctBes1AomT0M4ND7jTFBCVPXQ/WFTvNSefIrNSmLKhIKBrvN8SG+CZslimJcT3W2u2g==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.0.tgz", + "integrity": "sha512-Qe7w62TyawbDzB4yt32R0+AbIo6m1/sqO7UPzFS8Z/ksL5mrfhA0v4CavfdmFav3D+ub4QeAgsGEe84DoWe/nQ==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -3447,19 +3489,21 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.10.0", - "@rollup/rollup-android-arm64": "4.10.0", - "@rollup/rollup-darwin-arm64": "4.10.0", - "@rollup/rollup-darwin-x64": "4.10.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.10.0", - "@rollup/rollup-linux-arm64-gnu": "4.10.0", - "@rollup/rollup-linux-arm64-musl": "4.10.0", - "@rollup/rollup-linux-riscv64-gnu": "4.10.0", - "@rollup/rollup-linux-x64-gnu": "4.10.0", - "@rollup/rollup-linux-x64-musl": "4.10.0", - "@rollup/rollup-win32-arm64-msvc": "4.10.0", - "@rollup/rollup-win32-ia32-msvc": "4.10.0", - "@rollup/rollup-win32-x64-msvc": "4.10.0", + "@rollup/rollup-android-arm-eabi": "4.14.0", + "@rollup/rollup-android-arm64": "4.14.0", + "@rollup/rollup-darwin-arm64": "4.14.0", + "@rollup/rollup-darwin-x64": "4.14.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.14.0", + "@rollup/rollup-linux-arm64-gnu": "4.14.0", + "@rollup/rollup-linux-arm64-musl": "4.14.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.14.0", + "@rollup/rollup-linux-riscv64-gnu": "4.14.0", + "@rollup/rollup-linux-s390x-gnu": "4.14.0", + "@rollup/rollup-linux-x64-gnu": "4.14.0", + "@rollup/rollup-linux-x64-musl": "4.14.0", + "@rollup/rollup-win32-arm64-msvc": "4.14.0", + "@rollup/rollup-win32-ia32-msvc": "4.14.0", + "@rollup/rollup-win32-x64-msvc": "4.14.0", "fsevents": "~2.3.2" } }, @@ -3577,9 +3621,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -3955,14 +3999,14 @@ "dev": true }, "node_modules/vite": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.1.tgz", - "integrity": "sha512-wclpAgY3F1tR7t9LL5CcHC41YPkQIpKUGeIuT8MdNwNZr6OqOTLs7JX5vIHAtzqLWXts0T+GDrh9pN2arneKqg==", + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz", + "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==", "dev": true, "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.35", - "rollup": "^4.2.0" + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" }, "bin": { "vite": "bin/vite.js" @@ -4031,6 +4075,412 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, "node_modules/vitest": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz", diff --git a/package.json b/package.json index 143f26c..4edebe1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "altcha-lib", - "version": "0.1.2", + "version": "0.1.3", "description": "A library for creating and verifying ALTCHA challenges for Node.js, Bun and Deno.", "author": "Daniel Regeci", "license": "MIT", @@ -19,8 +19,9 @@ "denoify": "rimraf deno_dist && denoify && find deno_dist/. -type f -exec sed -i '' -e 's/node:node:/node:/g' {} +", "eslint": "eslint ./lib/**/*", "format": "prettier --write './(lib|tests)/**/*'", - "test": "vitest", - "test:deno": "deno test tests/deno.ts" + "test": "vitest --run", + "test:deno": "deno test --allow-read tests/deno.ts", + "prepare": "husky" }, "files": [ "cjs", @@ -36,6 +37,12 @@ "./types": { "types": "./dist/types.d.ts", "import": "./dist/types.js" + }, + "./worker": { + "types": "./dist/worker.d.ts", + "import": "./dist/worker.js", + "require": "./cjs/dist/worker.js", + "default": "./dist/worker.js" } }, "typesVersions": { @@ -50,6 +57,7 @@ "@typescript-eslint/eslint-plugin": "^6.21.0", "denoify": "^1.6.9", "eslint": "^8.56.0", + "husky": "^9.0.11", "prettier": "^3.2.5", "rimraf": "^5.0.5", "ts-node": "^10.9.1", diff --git a/tests/challenge.test.ts b/tests/challenge.test.ts index 578d8a8..c72a4bf 100644 --- a/tests/challenge.test.ts +++ b/tests/challenge.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { createChallenge, verifySolution } from '../lib/index.js'; +import { + createChallenge, + solveChallenge, + solveChallengeWorkers, + verifySolution, +} from '../lib/index.js'; import { Challenge } from '../lib/types.js'; describe('challenge', () => { @@ -13,6 +18,7 @@ describe('challenge', () => { expect(challenge).toEqual({ algorithm: 'SHA-256', challenge: expect.any(String), + max: expect.any(Number), salt: expect.any(String), signature: expect.any(String), } satisfies Challenge); @@ -29,6 +35,7 @@ describe('challenge', () => { expect(challenge).toEqual({ algorithm: 'SHA-1', challenge: expect.any(String), + max: expect.any(Number), salt: expect.any(String), signature: expect.any(String), } satisfies Challenge); @@ -45,6 +52,7 @@ describe('challenge', () => { expect(challenge).toEqual({ algorithm: 'SHA-512', challenge: expect.any(String), + max: expect.any(Number), salt: expect.any(String), signature: expect.any(String), } satisfies Challenge); @@ -168,4 +176,39 @@ describe('challenge', () => { expect(ok).toEqual(false); }); }); + + describe('solveChallenge', () => { + it('should solve challenge', async () => { + const number = 100; + const challenge = await createChallenge({ + number, + hmacKey, + }); + const { promise } = solveChallenge( + challenge.challenge, + challenge.salt, + challenge.algorithm + ); + const result = await promise; + expect(result?.number).toEqual(number); + }); + }); + + describe.skipIf(!('Worker' in globalThis))('solveChallengeWorkers', () => { + it('should solve challenge using workers', async () => { + const number = 100; + const challenge = await createChallenge({ + number, + hmacKey, + }); + const result = await solveChallengeWorkers( + './lib/worker.ts', + 4, + challenge.challenge, + challenge.salt, + challenge.algorithm + ); + expect(result?.number).toEqual(number); + }); + }); }); diff --git a/tests/deno.ts b/tests/deno.ts index c54cfdb..20247fa 100644 --- a/tests/deno.ts +++ b/tests/deno.ts @@ -1,5 +1,10 @@ import { assertEquals } from 'https://deno.land/std@0.213.0/assert/mod.ts'; -import { createChallenge, verifySolution } from '../deno_dist/index.ts'; +import { + createChallenge, + verifySolution, + solveChallenge, + solveChallengeWorkers, +} from '../deno_dist/index.ts'; const hmacKey = 'test'; @@ -35,3 +40,33 @@ Deno.test('verifySolution()', async (t) => { assertEquals(ok, true); }); }); + +Deno.test('solveChallenge()', async (t) => { + await t.step('should solve challenge', async () => { + const number = 100; + const challenge = await createChallenge({ + hmacKey, + number, + }); + const result = await solveChallenge(challenge.challenge, challenge.salt) + .promise; + assertEquals(result?.number, number); + }); +}); + +Deno.test('solveChallengeWorkers()', async (t) => { + await t.step('should solve challenge', async () => { + const number = 100; + const challenge = await createChallenge({ + hmacKey, + number, + }); + const result = await solveChallengeWorkers( + new URL('../deno_dist/worker.ts', import.meta.url), + 8, + challenge.challenge, + challenge.salt + ); + assertEquals(result?.number, number); + }); +});