import { hmac } from '@noble/hashes/hmac'; import { sha256 } from '@noble/hashes/sha256'; import { bytesToHex } from '@noble/hashes/utils'; import { hexToBase36 } from '@/lib/nsiteSubdomain'; import { getStorageKey } from '@/lib/storageKey'; /** * Get or create a device-local random seed persisted in localStorage. * This is a general-purpose secret used to derive private identifiers * (e.g. sandbox frame subdomains) that must not be predictable by third parties. */ function getSeed(appId: string): string { const key = getStorageKey(appId, 'seed'); const stored = localStorage.getItem(key); if (stored) return stored; const seed = crypto.randomUUID(); localStorage.setItem(key, seed); return seed; } /** * Derive a stable, private subdomain label for a sandbox frame. * * Uses HMAC-SHA256 with the device-local seed as the key and * `prefix|identifier` as the message. Because the seed is secret to * this device, a third party cannot predict or collide with another * app's subdomain, preventing cross-app localStorage/IndexedDB access * on the sandbox domain. * * The `prefix` acts as a domain separator so that different use-cases * (e.g. "webxdc", "nsite") produce distinct subdomains even for the * same identifier. * * The result is a 50-character base36 string (256 bits of entropy) that * fits within the 63-character subdomain label limit. * * @param appId The app's configured `appId` — used to namespace the device seed in localStorage. */ export function deriveIframeSubdomain(appId: string, prefix: string, identifier: string): string { const seed = getSeed(appId); const enc = new TextEncoder(); const mac = hmac(sha256, enc.encode(seed), enc.encode(`${prefix}|${identifier}`)); return hexToBase36(bytesToHex(mac)); }