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
35 changes: 35 additions & 0 deletions contract/tests/concurrent_submit.rs
Original file line number Diff line number Diff line change
@@ -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<HashSet<String>>, 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");
}
32 changes: 32 additions & 0 deletions docs/cors-and-cache-invalidation.md
Original file line number Diff line number Diff line change
@@ -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`.
30 changes: 30 additions & 0 deletions frontend/lib/page-messages.ts
Original file line number Diff line number Diff line change
@@ -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'];