src/helpers.ts
Rama main · réplica de lectura del repositorio original.
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);
}
}