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:
Ilia Denisov
2026-05-07 14:08:09 +02:00
parent 87a6694e2d
commit ecd2bc9348
18 changed files with 1133 additions and 29 deletions
+59 -18
View File
@@ -607,24 +607,55 @@ provide a generic local cache for game state. Defines the
TypeScript-side `KeyStore` and `Cache` interfaces that desktop and
mobile adapters will satisfy in later phases.
Decisions taken with the project owner before implementation:
1. **Phase 6 stops at the storage boundary.** The PLAN previously
listed a Playwright check that the gateway accepts a signed
request. Public-key registration happens through the email-code
confirm endpoint, which Phase 7 wires; building a temporary
test-only registration path was rejected as throw-away
scaffolding. The live-gateway round-trip is therefore covered by
Phase 7's existing acceptance bullet "the first authenticated
Connect call after login … succeeds end-to-end" instead, which
cannot pass unless the Phase 6 keystore persists and signs
correctly.
2. **Modern-browser baseline, no JS Ed25519 fallback.** WebCrypto
Ed25519 lands in Chrome ≥137, Firefox ≥130, Safari ≥17.4. Phase 7
surfaces a clear "browser not supported" message for older
engines instead of carrying a parallel `@noble/ed25519` code
path. The full matrix and rationale live in
`ui/docs/storage.md`.
Artifacts:
- `ui/frontend/src/platform/store/index.ts` defining `KeyStore` and
`Cache` interfaces
- `ui/frontend/src/platform/store/idb-cache.ts` IndexedDB-backed
`Cache` using the `idb` library
- `ui/frontend/src/platform/store/webcrypto-keystore.ts` WebCrypto
non-exportable Ed25519 key generation and IndexedDB handle
persistence
- `ui/frontend/src/api/session.ts` thin layer that loads or creates the
device session at app startup
- `ui/frontend/src/platform/store/index.ts` — public `KeyStore`,
`Cache`, `DeviceKeypair` interfaces and the `loadStore()`
resolver, with no web-specific imports in any public signature
- `ui/frontend/src/platform/store/idb.ts` — shared `galaxy-ui`
IndexedDB connection (typed via `idb`'s `DBSchema`) used by both
the keystore and the cache
- `ui/frontend/src/platform/store/idb-cache.ts` — IndexedDB-backed
`Cache` keyed by compound `[namespace, key]`
- `ui/frontend/src/platform/store/webcrypto-keystore.ts` — WebCrypto
non-exportable Ed25519 key generation, structured-cloned through
IDB
- `ui/frontend/src/platform/store/web.ts` — the `loadWebStore`
factory wired into `loadStore`
- `ui/frontend/src/api/session.ts` thin layer with
`loadDeviceSession`, `setDeviceSessionId`, `clearDeviceSession`
- `ui/frontend/src/routes/__debug/store/+page.svelte` (+ `+page.ts`
with `prerender = false; ssr = false;`) — dev-only debug surface
the Phase 6 Playwright spec drives through `window.__galaxyDebug`
- topic doc `ui/docs/storage.md` describing the browser baseline,
IDB schema, keystore lifecycle, and cache contract
Dependencies: Phase 5.
Acceptance criteria:
- a freshly generated keypair survives page reloads and produces
signatures that the gateway accepts;
- a freshly generated keypair survives page reloads (the loaded
handle still produces signatures verifiable under the persisted
public key); the live-gateway round-trip is covered by Phase 7;
- clearing site data removes the keypair, and the next request
triggers a re-login flow;
- `KeyStore` and `Cache` interfaces have full TypeScript types and
@@ -632,12 +663,19 @@ Acceptance criteria:
Targeted tests:
- Vitest unit tests for `IDBCache` with `fake-indexeddb`;
- Vitest unit tests for `WebCryptoKeyStore` exercising generate, load,
sign, clear;
- Playwright integration test: generate keypair, sign a request
through `WasmCore`, send through Connect, verify gateway accepts,
reload the page, sign another request, verify accepted.
- Vitest unit tests for `IDBCache` with `fake-indexeddb`
(round-trip, namespace isolation, delete, clear-with-namespace,
full clear);
- Vitest unit tests for `WebCryptoKeyStore` (generate, load,
signature determinism under Node WebCrypto, signature
verifiability after a simulated reload, third-party verify of the
public key, clear, fresh-keypair-after-clear);
- Playwright e2e (`storage-keypair-persistence.spec.ts`, all four
projects): generate keypair, sign canonical bytes, capture the
signature, reload, assert the previous signature still verifies
under the public key (works on every engine in the baseline
including non-deterministic WebKit), and that
`clearDeviceSession` forces a fresh keypair on next load.
## Phase 7. Auth Flow UI
@@ -670,7 +708,10 @@ Acceptance criteria:
- the first authenticated Connect call after login (e.g.
`user.account.read`) succeeds end-to-end through `WasmCore` →
`GalaxyClient` → ConnectRPC → gateway, with the response signature
verified and the payload decoded back to JSON;
verified and the payload decoded back to JSON. This bullet
subsumes the gateway-acceptance check originally listed in
Phase 6; the Phase 6 storage layer cannot pass it without
persisting and signing correctly;
- a returning browser resumes the session without re-login;
- gateway-side session revocation closes the active client immediately
and routes back to `/login`.