phase 6: web storage layer (KeyStore, Cache, session)
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>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
clearDeviceSession,
|
||||
loadDeviceSession,
|
||||
setDeviceSessionId,
|
||||
} from "../../../api/session";
|
||||
import { loadStore } from "../../../platform/store/index";
|
||||
|
||||
interface DebugSnapshot {
|
||||
publicKey: number[];
|
||||
deviceSessionId: string | null;
|
||||
}
|
||||
|
||||
interface DebugSurface {
|
||||
ready: true;
|
||||
loadSession(): Promise<DebugSnapshot>;
|
||||
clearSession(): Promise<void>;
|
||||
signWithDevice(message: number[]): Promise<number[]>;
|
||||
setDeviceSessionId(id: string): Promise<void>;
|
||||
verifyWithStoredPublicKey(
|
||||
message: number[],
|
||||
signature: number[],
|
||||
): Promise<boolean>;
|
||||
}
|
||||
|
||||
type DebugWindow = typeof globalThis & { __galaxyDebug?: DebugSurface };
|
||||
|
||||
let ready = $state(false);
|
||||
|
||||
function describe(err: unknown): string {
|
||||
if (err instanceof Error) {
|
||||
return `${err.name}: ${err.message}`;
|
||||
}
|
||||
return String(err);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (!import.meta.env.DEV) {
|
||||
return;
|
||||
}
|
||||
const { keyStore, cache } = await loadStore();
|
||||
const surface: DebugSurface = {
|
||||
ready: true,
|
||||
async loadSession() {
|
||||
try {
|
||||
const session = await loadDeviceSession(keyStore, cache);
|
||||
return {
|
||||
publicKey: Array.from(session.keypair.publicKey),
|
||||
deviceSessionId: session.deviceSessionId,
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`loadSession: ${describe(err)}`);
|
||||
}
|
||||
},
|
||||
async clearSession() {
|
||||
try {
|
||||
await clearDeviceSession(keyStore, cache);
|
||||
} catch (err) {
|
||||
throw new Error(`clearSession: ${describe(err)}`);
|
||||
}
|
||||
},
|
||||
async signWithDevice(message: number[]) {
|
||||
try {
|
||||
const session = await loadDeviceSession(keyStore, cache);
|
||||
const sig = await session.keypair.sign(new Uint8Array(message));
|
||||
return Array.from(sig);
|
||||
} catch (err) {
|
||||
throw new Error(`signWithDevice: ${describe(err)}`);
|
||||
}
|
||||
},
|
||||
async setDeviceSessionId(id: string) {
|
||||
try {
|
||||
await setDeviceSessionId(cache, id);
|
||||
} catch (err) {
|
||||
throw new Error(`setDeviceSessionId: ${describe(err)}`);
|
||||
}
|
||||
},
|
||||
async verifyWithStoredPublicKey(message, signature) {
|
||||
try {
|
||||
const session = await loadDeviceSession(keyStore, cache);
|
||||
const importedPublic = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
session.keypair.publicKey as BufferSource,
|
||||
{ name: "Ed25519" },
|
||||
false,
|
||||
["verify"],
|
||||
);
|
||||
return await crypto.subtle.verify(
|
||||
{ name: "Ed25519" },
|
||||
importedPublic,
|
||||
new Uint8Array(signature) as BufferSource,
|
||||
new Uint8Array(message) as BufferSource,
|
||||
);
|
||||
} catch (err) {
|
||||
throw new Error(`verifyWithStoredPublicKey: ${describe(err)}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
(window as DebugWindow).__galaxyDebug = surface;
|
||||
ready = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<h1>store debug</h1>
|
||||
{#if ready}
|
||||
<p data-testid="debug-store-ready">debug store ready</p>
|
||||
{:else}
|
||||
<p>booting…</p>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
padding: 2rem;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
// Debug-only route used by Playwright e2e tests in Phase 6 to drive
|
||||
// the [KeyStore]/[Cache] surface from the browser. SSR is disabled so
|
||||
// the keystore code only runs in the browser, and prerender is
|
||||
// disabled so the static-adapter build never freezes a debug page
|
||||
// into the production bundle.
|
||||
//
|
||||
// The route itself is gated at runtime by `import.meta.env.DEV`
|
||||
// inside `+page.svelte` — a production build still emits an empty
|
||||
// shell here, but the debug entry point never attaches.
|
||||
|
||||
export const prerender = false;
|
||||
export const ssr = false;
|
||||
Reference in New Issue
Block a user