Ctrl K
ComunidadGuías…

src/helpers.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import type { HmacAlgorithm } from './types';

/** Throws unless `algorithm` is exactly one of `allowed`. */
export function assertAlgorithm<T extends string>(
	algorithm: unknown,
	allowed: readonly T[]
): asserts algorithm is T {
	if (!allowed.includes(algorithm as T)) {
		throw new Error(
			`Unsupported algorithm: ${String(algorithm)}. Expected one of: ${allowed.join(', ')}.`
		);
	}
}

/** Throws unless `value` is an integer of at least 1. */
export function assertPositiveInteger(name: string, value: unknown): asserts value is number {
	if (!Number.isInteger(value) || (value as number) < 1) {
		throw new Error(`${name} must be a positive integer. Got: ${String(value)}`);
	}
}

/** Throws unless `secret` is a non-empty string. */
export function assertSecret(name: string, secret: unknown): asserts secret is string {
	if (typeof secret !== 'string' || secret === '') {
		throw new Error(`${name} must be a non-empty string.`);
	}
}

/** Checks if a buffer starts with the given prefix bytes. */
export function bufferStartsWith(buffer: Uint8Array, prefix: Uint8Array) {
	if (prefix.length > buffer.length) {
		return false;
	}
	for (let i = 0; i < prefix.length; i++) {
		if (buffer[i] !== prefix[i]) {
			return false;
		}
	}
	return true;
}

/** Converts a byte buffer to a lowercase hex string. */
export function bufferToHex(buffer: Uint8Array | ArrayBuffer) {
	return Array.from(new Uint8Array(buffer))
		.map((b) => b.toString(16).padStart(2, '0'))
		.join('');
}

/** Returns a canonical (sorted-key) JSON string for consistent hashing/signing. */
export function canonicalJSON(obj: unknown): string {
	return JSON.stringify(sortKeys(obj));
}

/** Concatenates two Uint8Arrays into a new buffer. */
export function concatBuffers(a: Uint8Array, b: Uint8Array) {
	const out = new Uint8Array(a.length + b.length);
	out.set(a, 0);
	out.set(b, a.length);
	return out;
}

/** Converts a hex string to a Uint8Array. Throws on odd length or non-hex characters. */
export function hexToBuffer(hex: string): Uint8Array {
	if (hex.length % 2 !== 0) {
		throw new Error(`Hex string must have an even length. Got: ${hex}`);
	}
	if (!/^[0-9a-fA-F]*$/.test(hex)) {
		throw new Error('Hex string contains non-hex characters.');
	}
	const buffer = new Uint8Array(hex.length / 2);
	for (let i = 0; i < buffer.length; i++) {
		buffer[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
	}
	return buffer;
}

/** Checks if two strings are equal in constant time. */
export function constantTimeEqual(a: string, b: string): boolean {
	if (a.length !== b.length) {
		return false;
	}
	let result = 0;
	for (let i = 0; i < a.length; i++) {
		result |= a.charCodeAt(i) ^ b.charCodeAt(i);
	}
	return result === 0;
}

/** Yields to the event loop after the given milliseconds. */
export async function delay(ms: number) {
	await new Promise((resolve) => setTimeout(resolve, ms));
}

/** Generate a SHA hash */
export async function hash(algorithm: string, data: ArrayBuffer | string): Promise<Uint8Array> {
	return new Uint8Array(
		await crypto.subtle.digest(
			algorithm.toUpperCase(),
			typeof data === 'string' ? new TextEncoder().encode(data) : new Uint8Array(data)
		)
	);
}

/** Computes an HMAC signature using the Web Crypto API. */
export async function hmac(
	algorithm: HmacAlgorithm,
	data: string | Uint8Array,
	keyStr: string
): Promise<Uint8Array> {
	const key = await crypto.subtle.importKey(
		'raw',
		new TextEncoder().encode(keyStr),
		{
			name: 'HMAC',
			hash: { name: algorithm }
		},
		false,
		['sign', 'verify']
	);
	const signature = await crypto.subtle.sign(
		'HMAC',
		key,
		typeof data === 'string' ? new TextEncoder().encode(data) : (data as BufferSource)
	);
	return new Uint8Array(signature);
}

/** Generates a random integer between the specified minimum and maximum values (inclusive). */
export function randomInt(max: number, min: number = 1): number {
	const ab = new Uint32Array(1);
	crypto.getRandomValues(ab);
	const randomNumber = ab[0] / (0xffffffff + 1);
	return Math.floor(randomNumber * (max - min + 1) + min);
}

/**
 * Recursively sorts object keys alphabetically for deterministic serialization.
 * Uses `Object.fromEntries` so that a `__proto__` key stays an own property (and is
 * serialized and signed) instead of invoking the prototype setter.
 */
export function sortKeys<T = unknown>(obj: T): T {
	if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
		return obj;
	}
	return Object.fromEntries(
		Object.keys(obj)
			.sort()
			.flatMap((key) => {
				const value = (obj as Record<string, unknown>)[key];
				return value === undefined ? [] : [[key, sortKeys(value)]];
			})
	) as T;
}

/** Returns elapsed time in milliseconds since `start`, rounded to one decimal. */
export function timeDuration(start: number) {
	return Math.floor((performance.now() - start) * 10) / 10;
}

/** Inject CSS tag into the document */
export function injectCss(css: string, id: string = 'altcha-css', nonce?: string) {
	if (typeof document !== 'undefined' && document && !document.getElementById(id)) {
		const style = document.createElement('style');
		style.id = id;
		style.textContent = css;
		const resolvedNonce =
			nonce ??
			(document.currentScript as HTMLElement | null)?.nonce ??
			document.querySelector<HTMLMetaElement>('meta[name="csp-nonce"]')?.content;
		if (resolvedNonce) {
			style.nonce = resolvedNonce;
		}
		document.head.appendChild(style);
	}
}