1/** Browser capabilities that determine whether lofi can provide its runtime guarantees. */2export type DeviceCapabilityReport = {3 // Package-owned device capability checks.4 secureContext: boolean;5 opfs: boolean;6 sharedWorker: boolean;7 webLocks: boolean;8 messageChannel: boolean;9 durableDriverSupported: boolean;10 webAuthn: boolean;11 prf: "available" | "not-reported" | "unknown" | "unavailable";12 persistentPermission: "granted" | "not-granted" | "unavailable" | "error";13 displayMode: "standalone" | "browser";14};1516/** Capability report that excludes the separately requested persistence permission. */17export type DurableCapabilityReport = Omit<DeviceCapabilityReport, "persistentPermission">;1819type StorageManagerWithOpfs = StorageManager & {20 getDirectory?: () => Promise<FileSystemDirectoryHandle>;21};2223function browserStorage(): StorageManagerWithOpfs | undefined {24 return typeof navigator === "undefined"25 ? undefined26 : navigator.storage as StorageManagerWithOpfs | undefined;27}2829type PublicKeyCredentialCapabilities = typeof PublicKeyCredential & {30 getClientCapabilities?: () => Promise<Record<string, boolean>>;31};3233async function readPrfCapability(34 webAuthn: boolean,35): Promise<DeviceCapabilityReport["prf"]> {36 if (!webAuthn) return "unavailable";37 const publicKeyCredential = PublicKeyCredential as PublicKeyCredentialCapabilities;38 if (typeof publicKeyCredential.getClientCapabilities !== "function") return "unknown";39 try {40 const capabilities = await publicKeyCredential.getClientCapabilities();41 return capabilities["extension:prf"] === true ? "available" : "not-reported";42 } catch {43 return "unknown";44 }45}4647/** Reads synchronous browser capabilities needed by the durable Jazz driver. */48export function durableCapabilityReport(): DurableCapabilityReport {49 const storage = browserStorage();50 const locks = typeof navigator === "undefined" ? undefined : navigator.locks;51 const secureContext = globalThis.isSecureContext === true;52 const opfs = typeof storage?.getDirectory === "function";53 const sharedWorker = typeof globalThis.SharedWorker === "function";54 const webLocks = typeof locks?.request === "function";55 const messageChannel = typeof globalThis.MessageChannel === "function";56 return {57 secureContext,58 opfs,59 sharedWorker,60 webLocks,61 messageChannel,62 durableDriverSupported: secureContext && opfs && sharedWorker && webLocks && messageChannel,63 webAuthn: typeof PublicKeyCredential === "function",64 prf: "unknown",65 displayMode: typeof globalThis.matchMedia === "function" &&66 globalThis.matchMedia("(display-mode: standalone)").matches67 ? "standalone"68 : "browser",69 };70}7172/** Reads the complete capability report without requesting new browser permission. */73export async function readDeviceCapabilityReport(): Promise<DeviceCapabilityReport> {74 const base = durableCapabilityReport();75 const storage = browserStorage();76 const prf = await readPrfCapability(base.webAuthn);77 if (typeof storage?.persisted !== "function") {78 return { ...base, prf, persistentPermission: "unavailable" };79 }80 try {81 return {82 ...base,83 prf,84 persistentPermission: await storage.persisted() ? "granted" : "not-granted",85 };86 } catch {87 return { ...base, prf, persistentPermission: "error" };88 }89}9091/** Requests eviction protection, then returns the browser's authoritative capability report. */92export async function requestPersistentStorage(): Promise<DeviceCapabilityReport> {93 const storage = browserStorage();94 if (typeof storage?.persist !== "function") return await readDeviceCapabilityReport();95 try {96 await storage.persist();97 } catch {98 // The subsequent read reports the browser's authoritative persisted state.99 }100 return await readDeviceCapabilityReport();101}102103/** Raised when the browser cannot provide the durable local-storage contract. */104export class DurableStorageUnsupportedError extends Error {105 /** Stable error class name for diagnostics and error boundaries. */106 override name = "DurableStorageUnsupportedError";107 /** Capabilities observed when the runtime rejected startup. */108 readonly report: DurableCapabilityReport;109110 /** Creates an error that identifies every missing durable-driver capability. */111 constructor(report: DurableCapabilityReport = durableCapabilityReport()) {112 const missing = Object.entries({113 secureContext: report.secureContext,114 opfs: report.opfs,115 sharedWorker: report.sharedWorker,116 webLocks: report.webLocks,117 messageChannel: report.messageChannel,118 }).filter(([, available]) => !available).map(([name]) => name);119 super(120 `Durable local storage is unsupported; missing ${121 missing.join(", ")122 }. Use the stable HTTPS device URL in a supported browser.`,123 );124 this.report = report;125 }126}127128/** Throws unless the current browser can open lofi's persistent local driver. */129export function assertDurableBrowser(): void {130 const report = durableCapabilityReport();131 if (!report.durableDriverSupported) throw new DurableStorageUnsupportedError(report);132}