ecd2bc9348
KeyStore + Cache TS interfaces with WebCrypto non-extractable Ed25519 keys persisted via IndexedDB (idb), plus thin api/session.ts that loads or creates the device session at app startup. Vitest unit tests under fake-indexeddb cover both adapters; Playwright e2e verifies the keypair survives reload and produces signatures still verifiable under the persisted public key (gateway round-trip moves to Phase 7's existing acceptance bullet). Browser baseline: WebCrypto Ed25519 — Chrome >=137, Firefox >=130, Safari >=17.4. No JS fallback; ui/docs/storage.md documents the matrix and the WebKit non-determinism quirk. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
70 lines
2.5 KiB
TypeScript
70 lines
2.5 KiB
TypeScript
// `session.ts` is the thin layer that loads or creates the device
|
|
// session at app startup. The keypair half is always materialised
|
|
// (loaded from the [KeyStore], or freshly generated when the slot is
|
|
// empty); the device-session-id half is read out of the [Cache] and
|
|
// is `null` until Phase 7's confirm-email-code handler stores it via
|
|
// [setDeviceSessionId]. A `null` deviceSessionId is the marker that
|
|
// the user must run the email-code login flow.
|
|
//
|
|
// Phase 7 wires the production caller (a layout-level loader that
|
|
// runs `loadDeviceSession` on first render and forwards the result
|
|
// into the Svelte session store). Phase 6 ships the persistence
|
|
// primitives this loader needs.
|
|
|
|
import type { Cache, DeviceKeypair, KeyStore } from "../platform/store/index";
|
|
|
|
export const SESSION_NAMESPACE = "session";
|
|
export const SESSION_ID_KEY = "device-session-id";
|
|
|
|
export interface DeviceSession {
|
|
keypair: DeviceKeypair;
|
|
deviceSessionId: string | null;
|
|
}
|
|
|
|
/**
|
|
* loadDeviceSession returns the device session for the current
|
|
* device. The keypair is loaded from `keyStore`; if the slot is
|
|
* empty, a fresh non-exportable Ed25519 keypair is generated and
|
|
* persisted. The returned `deviceSessionId` is `null` until Phase 7
|
|
* registers the public key with the gateway and persists the
|
|
* resulting id via [setDeviceSessionId].
|
|
*/
|
|
export async function loadDeviceSession(
|
|
keyStore: KeyStore,
|
|
cache: Cache,
|
|
): Promise<DeviceSession> {
|
|
const existing = await keyStore.load();
|
|
const keypair = existing ?? (await keyStore.generate());
|
|
const stored = await cache.get<string>(SESSION_NAMESPACE, SESSION_ID_KEY);
|
|
return { keypair, deviceSessionId: stored ?? null };
|
|
}
|
|
|
|
/**
|
|
* setDeviceSessionId persists the device-session id returned by the
|
|
* gateway's confirm-email-code response so subsequent app starts can
|
|
* resume the session without re-login.
|
|
*/
|
|
export async function setDeviceSessionId(
|
|
cache: Cache,
|
|
deviceSessionId: string,
|
|
): Promise<void> {
|
|
await cache.put(SESSION_NAMESPACE, SESSION_ID_KEY, deviceSessionId);
|
|
}
|
|
|
|
/**
|
|
* clearDeviceSession wipes both the device keypair and the stored
|
|
* device-session id. The next [loadDeviceSession] call will generate
|
|
* a fresh keypair and report `deviceSessionId: null`, forcing
|
|
* re-login. Used by user-driven logout and by gateway-driven
|
|
* revocation paths.
|
|
*/
|
|
export async function clearDeviceSession(
|
|
keyStore: KeyStore,
|
|
cache: Cache,
|
|
): Promise<void> {
|
|
await Promise.all([
|
|
keyStore.clear(),
|
|
cache.delete(SESSION_NAMESPACE, SESSION_ID_KEY),
|
|
]);
|
|
}
|