import type { HmacAlgorithm } from './types'; /** Throws unless `algorithm` is exactly one of `allowed`. */ export function assertAlgorithm( 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 { 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 { 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(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)[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('meta[name="csp-nonce"]')?.content; if (resolvedNonce) { style.nonce = resolvedNonce; } document.head.appendChild(style); } }