All files / lib/imports url-normalize.ts

100% Statements 14/14
100% Branches 4/4
100% Functions 2/2
100% Lines 13/13

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46                                    22x 22x   2x   20x   20x     22x               20x 20x 20x 20x 20x 640x   20x    
/**
 * Shared URL normalization + idempotency hashing for pro imports.
 *
 * Used by admin import enqueue routes. The idempotency key is
 * `SHA-256(proId | normalized_url)` and lives on `pro_imports.idempotencyKey`
 * with a unique constraint, so the same Pro cannot queue the same URL twice
 * concurrently.
 */
 
import { ValidationError } from "../errors";
 
/**
 * Strip fragment, drop trailing slash, lowercase origin. Keeps query string
 * verbatim because Houzz includes user-scoped query params that distinguish
 * profiles (e.g. `?irs=…`).
 */
export function normalizeUrl(raw: string): string {
	let url: URL;
	try {
		url = new URL(raw);
	} catch {
		throw new ValidationError("sourceUrl is not a valid URL");
	}
	url.hash = "";
	const cleanedPath =
		url.pathname.endsWith("/") && url.pathname.length > 1
			? url.pathname.slice(0, -1)
			: url.pathname;
	return `${url.origin.toLowerCase()}${cleanedPath}${url.search}`;
}
 
/** SHA-256 hex digest of `${proId}|${normalizedUrl}`. */
export async function hashIdempotencyKey(
	proId: string,
	normalizedUrl: string,
): Promise<string> {
	const data = new TextEncoder().encode(`${proId}|${normalizedUrl}`);
	const digest = await crypto.subtle.digest("SHA-256", data);
	const bytes = new Uint8Array(digest);
	let hex = "";
	for (let i = 0; i < bytes.length; i++) {
		hex += bytes[i].toString(16).padStart(2, "0");
	}
	return hex;
}