Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 1 addition & 22 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>

<!-- Apply the saved/system theme before first paint to avoid a light flash. -->
<script src="/theme-init.js"></script>
<!-- Apply the saved/system theme and language before first paint to avoid a flash. -->
<script>
(function () {
try {
var stored = localStorage.getItem("synapsvault-theme");
var dark = stored ? stored === "dark" : window.matchMedia("(prefers-color-scheme: dark)").matches;
if (dark) document.documentElement.classList.add("dark");
} catch (e) {}
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) {}
})();
</script>
<script src="/theme-init.js"></script>
</head>
<body>
<div id="root"></div>
Expand Down
16 changes: 16 additions & 0 deletions public/theme-init.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
// Apply the saved theme before first paint to avoid a flash.
(function () {
try {
var stored = localStorage.getItem("synapsvault-theme");
var dark = stored ? stored === "dark" : window.matchMedia("(prefers-color-scheme: dark)").matches;
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) {}
})();
33 changes: 22 additions & 11 deletions src/api/requestSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -105,15 +118,13 @@ export async function signedPublisherFetch(
): Promise<Response> {
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<string, string> | 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,
Expand Down
4 changes: 4 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -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=()"
}
]
},
Expand Down
Loading