From 39ea00fff3d6faee6c9ba647bf5cb6917c60c8c6 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Mon, 3 Aug 2026 08:39:54 +0200 Subject: [PATCH] test(opaque): fix playwright scenarios --- frontend/src/lib/api/endpoints/opaque.ts | 31 +++++++++++++++++++++--- frontend/svelte.config.js | 8 +++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/api/endpoints/opaque.ts b/frontend/src/lib/api/endpoints/opaque.ts index dae9c7a2..5687bebe 100644 --- a/frontend/src/lib/api/endpoints/opaque.ts +++ b/frontend/src/lib/api/endpoints/opaque.ts @@ -34,11 +34,36 @@ * carries. In Phase 1 the config is fetched from `/api/health` (or a * dedicated `/api/auth/opaque/params` endpoint) at page load and cached. */ -import { client, ready } from '@serenity-kit/opaque'; import { ApiError, apiFetch } from '$lib/api/client'; import { getCsrfHeaders } from '$lib/api/csrf'; import type { AuthResponse } from '$lib/api/types'; +/** + * Lazily load the `@serenity-kit/opaque` WASM module. A top-level + * static import here would trigger `WebAssembly.compile()` the moment + * ANYONE imports this file — including the read-only helpers + * (`fetchOpaqueParams`, `checkOpaqueAvailable`) that don't need + * crypto at all. Under a strict CSP without `'wasm-unsafe-eval'` + * (which is the default posture and what Playwright's SPA suite + * exercises), that fails at import time and crashes the login page + * even in OPAQUE-off deployments where the substrate is disabled. + * + * Hoisting the import into this async helper defers WASM + * compilation to the first crypto call site. The read-only paths + * never fire it; only `opaqueRegister` / `opaqueLogin` do — and + * those callers already know they need the WASM (they've already + * confirmed `params.enabled === true` upstream). + * + * Awaits `ready` before returning so callers get a fully-initialized + * client. `import()` is memoized by the module loader so subsequent + * calls hit the same instance. + */ +async function opaqueWasm(): Promise { + const mod = await import('@serenity-kit/opaque'); + await mod.ready; + return mod.client; +} + /** * Client-side Argon2id parameters — must match the server's config * ([`OpaqueConfig::ksf_*`] in Rust). Fetched from the server at page load @@ -232,7 +257,7 @@ export async function opaqueRegister( ksf: OpaqueKsfConfig, ciphersuiteVersion: number ): Promise { - await ready; + const client = await opaqueWasm(); // ── Round 1 ───────────────────────────────────────────────────────── const { clientRegistrationState, registrationRequest } = client.startRegistration({ password }); @@ -297,7 +322,7 @@ export async function opaqueLogin( password: string, ksf: OpaqueKsfConfig ): Promise { - await ready; + const client = await opaqueWasm(); // ── KE1 ───────────────────────────────────────────────────────────── const { clientLoginState, startLoginRequest } = client.startLogin({ password }); diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js index 15d73992..70a4758f 100644 --- a/frontend/svelte.config.js +++ b/frontend/svelte.config.js @@ -90,7 +90,13 @@ const config = { mode: 'hash', directives: { 'default-src': ['self'], - 'script-src': ['self', themeInitHash], + // `'wasm-unsafe-eval'` (CSP Level 3) permits WebAssembly.compile() + // without allowing eval() for JavaScript. Required for the OPAQUE + // aPAKE client (`@serenity-kit/opaque`), lazy-loaded by the login + // path only when `OXICLOUD_OPAQUE_MODE != off`. Mirrors the Rust + // server's `content_security_policy` in `src/interfaces/web/mod.rs` + // so headers + this meta tag agree on the same posture. + 'script-src': ['self', 'wasm-unsafe-eval', themeInitHash], 'worker-src': ['self'], 'style-src': ['self', 'unsafe-inline'], 'img-src': ['self', 'data:', 'blob:', 'https:'],