export interface StoredKey { scope: string; /** Non-extractable. Never leaves this object as bytes. */ privateKey: CryptoKey; publicKeyRaw: Uint8Array; /** * `Date.now()` when the key was minted. Absent on keys written by an earlier * version, which are by definition the oldest ones there. */ createdAt?: number; } export interface KeyStore { get(scope: string): Promise; put(key: StoredKey): Promise; remove(scope: string): Promise; /** Every scope currently holding a key, so a device can purge the rest. */ scopes(): Promise; } /** * The recipient's keypair, in IndexedDB. * * IndexedDB rather than `localStorage`, and the reason is not capacity or * ergonomics: IndexedDB can structured-clone a `CryptoKey` object directly, so * the non-extractable private key is stored *as a key handle*. `localStorage` * holds strings only, which would force `extractable: true` and put the raw * private key within reach of any script on the page. * * ## How long this actually survives — read before shipping * * Longer than a session, but it is **not** permanent storage, and the failure * is silent. A device whose key is gone still works — a fresh keypair is * generated on demand and new uploads seal to it — but anything already in the * queue under the old key is undecryptable for good. * * Survives: page reload, tab close, browser restart, device reboot, and any * amount of server-side redeployment. None of this is on the server. * * Lost to: * * - **Clearing site data.** The realistic one, and the worst: it can happen * mid-shift with items still queued. * - **A replaced, reset or reimaged device**, and any other browser profile. * - **Private / incognito browsing**, where the store dies with the window. * - **Safari's ITP eviction.** All script-writable storage for an origin is * erased after *seven days without user interaction with that site*. It is a * tracking countermeasure, not a storage policy, and Chrome and Firefox do * not do it. Note what resets the clock: someone opening the page, not the * browser being used. A kiosk in daily use never approaches it, and a device * idle long enough to trigger it has an empty queue anyway — so this is the * least costly of the four, despite sounding like the most alarming. * - **Storage pressure**, where a browser evicts whole origins under disk * pressure. `navigator.storage.persist()` asks for exemption from this one * and is usually granted to a site with real interaction history — see * `requestPersistence`. It does **not** exempt anything from ITP above. * * The mitigation is not to prevent loss, which is not possible from here, but * to make it legible: `decrypt` reports `wrong-key` and `no-key` as distinct * reasons so a dashboard can say which happened instead of showing a broken * image. * * ## The opposite risk: keys outlive their scope * * Nothing here expires anything. A device that has served ten rooms holds ten * private keys, each still able to open whatever was queued under it, and the * one that matters is only ever the current scope's. On shared, rented or * resold hardware the other nine are keys to someone else's uploads. * * Purging is the app's job, because only the app knows which scopes are stale — * a device legitimately serving two rooms at once must not lose one. `scopes()` * is the mechanism: read them, `remove` the ones that no longer apply. Do it * when the scope changes, when it drops, and on sign-out. Where the current * scope is unknown — an unassigned device sitting in stock — `createdAt` is the * only handle left: drop everything past some age. * * Deletion is final in the same way loss is. Purge a scope with items still * queued under it and those items are unreadable for good. */ export declare function indexedDbKeyStore(dbName?: string, storeName?: string): KeyStore; /** * A key store in a `Map`. For tests, and for Node, which has WebCrypto but no * IndexedDB. Everything in it dies with the process. */ export declare function memoryKeyStore(): KeyStore; /** * Asks the browser not to evict this origin under storage pressure. * * Worth calling once on the requester's side. Returns whether it was granted — * browsers decide by interaction history and installed-app status, so a first * visit is usually refused and a daily-use kiosk usually is not. * * This says nothing about Safari's seven-day ITP eviction, which no API opts * out of. */ export declare function requestPersistence(): Promise; //# sourceMappingURL=keystore.d.ts.map