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:
+59
-18
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user