Files
eranos/src/lib/iframeSubdomain.ts
T
Alex Gleason 52e42fcd6e Replace hardcoded 'Ditto' with appConfig.appName and appConfig.appId
User-facing display strings now read from config.appName so forks can
rebrand without code changes, and localStorage keys are namespaced by
config.appId so forks running on the same origin don't clobber each
other's preferences. Module-level cache-key constants that previously
hardcoded 'ditto:' have been refactored into hook-scoped reads from
config.appId (via a new getStorageKey() helper). The helpContent FAQ
template now uses {appName} placeholders substituted at read-time
through getFAQCategories(appName)/getFAQItem(appName, id).
2026-04-17 11:01:04 -05:00

47 lines
1.7 KiB
TypeScript

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));
}