/** * T8 (v0.10) — `HostPinStore`: a CACHE / AUDIT TRAIL of the API-sourced host fingerprint per * `host_id`, NOT an independent trust root. * * It never establishes trust from a relay-forwarded handshake — `record` only ever stores a value * the caller already verified against the authenticated `HostRecord.enrollFpr` (§4.2, fetched * out-of-band of the relay over the P5-authenticated HTTPS API). Drift between a freshly-sourced * API fingerprint and the cached one is surfaced for review, but the API value is authoritative. * * Only the PUBLIC fingerprint (`enroll_fpr`) is persisted — never a key, secret, or plaintext * (INV5/INV9): a fingerprint is safe at rest and is exactly what enables first-connect MITM defense. */ const KEY_PREFIX = 'relay-web:hostpin:' export interface HostPinStore { /** last cached API-sourced enroll_fpr for this host, or null if none cached yet */ get(hostId: string): string | null /** cache an API-VERIFIED fingerprint (audit/drift detection) — caller must have verified it */ record(hostId: string, apiSourcedFpr: string): void } /** * In-memory fallback for non-DOM/test contexts (keeps the store usable everywhere). The store only * ever calls `getItem`/`setItem`, so only those are implemented (cast to the Storage interface). */ function memoryStorage(): Storage { const map = new Map() return { getItem: (k: string) => map.get(k) ?? null, setItem: (k: string, v: string) => { map.set(k, v) }, } as unknown as Storage } function defaultStorage(): Storage { try { if (typeof localStorage !== 'undefined') return localStorage } catch { // localStorage can throw in sandboxed contexts; fall back to memory. } return memoryStorage() } export function createHostPinStore(storage: Storage = defaultStorage()): HostPinStore { const keyFor = (hostId: string): string => `${KEY_PREFIX}${hostId}` return { get(hostId: string): string | null { return storage.getItem(keyFor(hostId)) }, record(hostId: string, apiSourcedFpr: string): void { // Store only the API-verified value; this method never derives trust from a handshake. storage.setItem(keyFor(hostId), apiSourcedFpr) }, } }