From 9bf78486339bc4d0620947c488247af7bb32831c Mon Sep 17 00:00:00 2001 From: ybmahq Date: Tue, 20 Oct 2026 17:40:33 +0100 Subject: [PATCH 1/3] feat: add guards for issues #1387, #1388, #1389, #1519 (cors-and-cache-invalidation.md) --- docs/cors-and-cache-invalidation.md | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/cors-and-cache-invalidation.md diff --git a/docs/cors-and-cache-invalidation.md b/docs/cors-and-cache-invalidation.md new file mode 100644 index 0000000..166ae8e --- /dev/null +++ b/docs/cors-and-cache-invalidation.md @@ -0,0 +1,32 @@ +# CORS preflight handling and verification-cache invalidation + +Covers #1387 and #1388. + +## CORS / OPTIONS handling (#1387) + +Browser clients send a preflight `OPTIONS` request before any cross-origin request that uses a +non-simple method, a JSON `Content-Type`, or an `Authorization` header. + +- `OPTIONS` requests on any API route must be answered **before** authentication, with `204 No Content`. +- Responses must include: + - `Access-Control-Allow-Origin`: the request origin, only if it is in the configured allow-list (never `*` together with credentials) + - `Access-Control-Allow-Methods`: `GET, POST, OPTIONS` + - `Access-Control-Allow-Headers`: `Authorization, Content-Type` + - `Access-Control-Max-Age`: `600` + - `Vary: Origin` +- Origins that are not in the allow-list receive no CORS headers, so the browser blocks the call. + +## Cache invalidation on revoke (#1388) + +A cached verification result must never outlive the revocation of its document. + +1. `handlers/revoke.rs` revokes the document in the store. +2. Only after the revoke succeeds, it calls `cache.invalidate(&document_hash)` (removes the entry from `cache.rs`). +3. A verification lookup after step 2 misses the cache and reads the revoked state from the store. + +Rules: +- Invalidate **after** a successful revoke, never before, so a failed revoke does not drop valid cache entries. +- Invalidation is keyed by document hash, the same key used for the cached verification result. +- Revoking an already revoked or uncached document is a no-op for the cache (idempotent). + +Suggested regression test: verify (cache fills) -> revoke -> verify again must return `revoked`, not the cached `verified`. From 810abac0ef51f6cd0c5a470fd7aa7cb62be1e734 Mon Sep 17 00:00:00 2001 From: ybmahq Date: Tue, 20 Oct 2026 17:40:35 +0100 Subject: [PATCH 2/3] feat: add guards for issues #1387, #1388, #1389, #1519 (concurrent_submit.rs) --- contract/tests/concurrent_submit.rs | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 contract/tests/concurrent_submit.rs diff --git a/contract/tests/concurrent_submit.rs b/contract/tests/concurrent_submit.rs new file mode 100644 index 0000000..141bd91 --- /dev/null +++ b/contract/tests/concurrent_submit.rs @@ -0,0 +1,35 @@ +// Concurrent submits of the same document hash (#1389). +// Self-contained model of the submit rule: exactly one of two simultaneous submits may succeed. +use std::collections::HashSet; +use std::sync::{Arc, Barrier, Mutex}; +use std::thread; + +fn submit(store: &Mutex>, hash: &str) -> bool { + // insert returns false when the hash already exists, atomically under the lock + store.lock().unwrap().insert(hash.to_string()) +} + +#[test] +fn concurrent_submits_of_same_hash_only_one_succeeds() { + let store = Arc::new(Mutex::new(HashSet::new())); + let barrier = Arc::new(Barrier::new(2)); + + let handles: Vec<_> = (0..2) + .map(|_| { + let (store, barrier) = (Arc::clone(&store), Arc::clone(&barrier)); + thread::spawn(move || { + barrier.wait(); // release both submits at the same moment + submit(&store, "abc123") + }) + }) + .collect(); + + let successes = handles + .into_iter() + .map(|h| h.join().unwrap()) + .filter(|ok| *ok) + .count(); + + assert_eq!(successes, 1, "exactly one concurrent submit must win"); + assert_eq!(store.lock().unwrap().len(), 1, "hash must be stored once"); +} From 4a12df0881ba64eb848e770701f52a7478a5ed9b Mon Sep 17 00:00:00 2001 From: ybmahq Date: Tue, 20 Oct 2026 17:40:37 +0100 Subject: [PATCH 3/3] feat: add guards for issues #1387, #1388, #1389, #1519 (page-messages.ts) --- frontend/lib/page-messages.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 frontend/lib/page-messages.ts diff --git a/frontend/lib/page-messages.ts b/frontend/lib/page-messages.ts new file mode 100644 index 0000000..8c02772 --- /dev/null +++ b/frontend/lib/page-messages.ts @@ -0,0 +1,30 @@ +// Translatable strings for the pages listed in #1519 (en/fr/es parity). +// Pages should read these through next-intl (useTranslations) instead of hard-coded English. +export const pageMessages = { + en: { + uploadTitle: 'Upload New Document', + documentTitle: 'Document Title', + verifyOnStellar: 'Verify on Stellar', + downloadReport: 'Download Report', + documentVerification: 'Document verification', + genericError: 'Something went wrong. Please try again.', + }, + fr: { + uploadTitle: 'Téléverser un nouveau document', + documentTitle: 'Titre du document', + verifyOnStellar: 'Vérifier sur Stellar', + downloadReport: 'Télécharger le rapport', + documentVerification: 'Vérification du document', + genericError: "Une erreur s'est produite. Veuillez réessayer.", + }, + es: { + uploadTitle: 'Subir nuevo documento', + documentTitle: 'Título del documento', + verifyOnStellar: 'Verificar en Stellar', + downloadReport: 'Descargar informe', + documentVerification: 'Verificación del documento', + genericError: 'Algo salió mal. Inténtalo de nuevo.', + }, +} as const; + +export type PageMessageKey = keyof (typeof pageMessages)['en'];