diff --git a/index.html b/index.html index 4330cb0..823f990 100644 --- a/index.html +++ b/index.html @@ -39,29 +39,8 @@ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Sora:wght@600;700&display=swap" /> - - - +
diff --git a/public/theme-init.js b/public/theme-init.js index 891e198..1f5fbb6 100644 --- a/public/theme-init.js +++ b/public/theme-init.js @@ -1,3 +1,4 @@ +// Apply the saved theme before first paint to avoid a flash. (function () { try { var stored = localStorage.getItem("synapsvault-theme"); @@ -5,3 +6,18 @@ if (dark) document.documentElement.classList.add("dark"); } catch (e) {} })(); + +// Apply the saved language before first paint to avoid a flash. +(function () { + try { + var storedLang = localStorage.getItem("synapsvault-lang"); + var navLang = (navigator.language || "").split("-")[0].toLowerCase(); + var lang = + storedLang === "en" || storedLang === "es" + ? storedLang + : navLang === "es" + ? "es" + : "en"; + document.documentElement.lang = lang; + } catch (e) {} +})(); diff --git a/src/api/requestSignature.ts b/src/api/requestSignature.ts index 213a1da..e7de419 100644 --- a/src/api/requestSignature.ts +++ b/src/api/requestSignature.ts @@ -2,10 +2,23 @@ * HMAC-SHA256 request signing for publisher mutations. * Mirrors server/src/utils/requestSignature.ts — keep in sync when changing the scheme. * - * Replay protection: the signature covers a Unix-seconds timestamp but no nonce, so a - * captured signed request can be replayed for as long as the server accepts that timestamp. - * Replay defence must be enforced server-side: bound the accepted timestamp window and reject - * repeated Idempotency-Keys / signatures within it. Mutations should send a unique Idempotency-Key. + * Replay protection: the signature covers a Unix-seconds timestamp but no nonce. A captured + * signed request can be replayed as long as the server accepts that timestamp. The server + * MUST implement replay defense: + * + * 1. **Timestamp window**: Enforce a reasonable window (e.g., ±5 minutes from server time). + * Requests outside this window are rejected, limiting the replay attack window. + * + * 2. **Idempotency-Key deduplication**: Track recent Idempotency-Keys per API key and reject + * replayed signatures within the timestamp window. Combined with the timestamp check, this + * prevents the same signed request from being submitted twice. + * + * 3. **Nonce (optional)**: For higher security, the server may require a server-provided nonce + * in the signature and invalidate used nonces. This prevents even timestamp-replayed requests + * outside the window. Nonces are not yet implemented in this client but are compatible with + * this signing scheme (add nonce to the canonical string before signing). + * + * Client responsibility: Always provide a unique Idempotency-Key for each mutation. */ import { networkFetch } from "./http.js"; @@ -105,15 +118,13 @@ export async function signedPublisherFetch( ): Promise { const method = (init.method ?? "GET").toUpperCase(); const body = typeof init.body === "string" ? init.body : undefined; - const path = new URL(url).pathname + new URL(url).search; - const requestHeaders = new Headers(init.headers); - const idempotencyKey = requestHeaders.get("Idempotency-Key") ?? undefined; const parsed = new URL(url, typeof window !== "undefined" ? window.location.origin : "http://localhost"); const path = parsed.pathname + parsed.search; - const idempotencyKey = - init.headers instanceof Headers - ? (init.headers.get("Idempotency-Key") ?? undefined) - : (init.headers as Record | undefined)?.["Idempotency-Key"]; + + // Normalize headers to ensure array-form headers are properly read. + // Without normalization, Idempotency-Key from array-form headers would be missed. + const requestHeaders = new Headers(init.headers); + const idempotencyKey = requestHeaders.get("Idempotency-Key") ?? undefined; const signatureHeaders = await signPublisherRequestHeaders({ apiKey, diff --git a/vercel.json b/vercel.json index 3bafb2b..b02ffb7 100644 --- a/vercel.json +++ b/vercel.json @@ -28,6 +28,10 @@ { "key": "Content-Security-Policy", "value": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self' https://horizon.stellar.org https://horizon-testnet.stellar.org https://soroban.stellar.org https://soroban-testnet.stellar.org https://*.ingest.sentry.io; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'" + }, + { + "key": "Permissions-Policy", + "value": "camera=(), microphone=(), geolocation=(), usb=()" } ] },