package captcha import ( "sync" "time" ) // NonceStore tracks used challenge nonces for single-use enforcement. The // default MemoryNonceStore is per-process (no DB) — sufficient for the 5a // model. A durable cross-process store (5b) can implement this interface later. type NonceStore interface { // MarkUsed records nonce; returns true on first use, false if already seen. MarkUsed(nonce string, ttl time.Duration) bool } type MemoryNonceStore struct { mu sync.Mutex used map[string]time.Time } func NewMemoryNonceStore() *MemoryNonceStore { return &MemoryNonceStore{used: make(map[string]time.Time)} } func (m *MemoryNonceStore) MarkUsed(nonce string, ttl time.Duration) bool { now := time.Now() m.mu.Lock() defer m.mu.Unlock() for k, exp := range m.used { if !exp.After(now) { delete(m.used, k) } } if _, seen := m.used[nonce]; seen { return false } m.used[nonce] = now.Add(ttl) return true }