From b7fa7fca7ff9f97b87a0c3bbe9a49b7cc2944f29 Mon Sep 17 00:00:00 2001 From: Deborah Amoni Date: Tue, 20 Oct 2026 11:17:30 +0100 Subject: [PATCH 1/4] #30: Normalize headers before reading Idempotency-Key When init.headers is passed as an array (e.g., [["key", "value"]]), it must be normalized before reading from it. The previous code had duplicate variable definitions that would skip normalization for non-Headers instances. Fix: Consistently use new Headers(init.headers) to normalize all header formats before reading Idempotency-Key. This ensures array-form headers are properly converted to the Headers API before lookup. --- src/api/requestSignature.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/api/requestSignature.ts b/src/api/requestSignature.ts index 213a1da..b6a8664 100644 --- a/src/api/requestSignature.ts +++ b/src/api/requestSignature.ts @@ -105,15 +105,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, From 543c1b5a78b85fa94f0f33e458480602a56f97b0 Mon Sep 17 00:00:00 2001 From: Deborah Amoni Date: Tue, 20 Oct 2026 11:17:42 +0100 Subject: [PATCH 2/4] #32: Document server-side replay protection requirements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The signature scheme includes a timestamp but no nonce, making requests vulnerable to replay attacks within the timestamp window. Replay protection must be enforced server-side using: 1. Timestamp window validation (e.g., ±5 minutes) 2. Idempotency-Key deduplication to track and reject replayed requests 3. Optional nonce requirement for higher security Update documentation to clearly specify these requirements and explain how they work together to prevent replays. Also document that nonces are compatible with the signing scheme if the server wants to add them in the future. --- src/api/requestSignature.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/api/requestSignature.ts b/src/api/requestSignature.ts index b6a8664..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"; From 2ebeeba4b748e4ca29f84a9ac1d5d5933fa3e73f Mon Sep 17 00:00:00 2001 From: Deborah Amoni Date: Tue, 20 Oct 2026 11:18:10 +0100 Subject: [PATCH 3/4] #35: Fix Content-Security-Policy violation by moving inline script The index.html had an inline - +
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) {} +})(); From beb476d38af1e2b497471da213b57776cc748b31 Mon Sep 17 00:00:00 2001 From: Deborah Amoni Date: Tue, 20 Oct 2026 11:18:22 +0100 Subject: [PATCH 4/4] #36: Add Permissions-Policy header to Vercel deployments The nginx.conf sets Permissions-Policy to restrict camera, microphone, geolocation, and USB access: camera=(), microphone=(), geolocation=(), usb=(). Vercel deployments were missing this header, creating an inconsistency between the two deployment targets. Now both nginx and Vercel enforce the same security headers. --- vercel.json | 4 ++++ 1 file changed, 4 insertions(+) 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=()" } ] },