events: verify, normalize, append-only store with provider-id dedup

Signature verification (gitea hex HMAC-SHA256 over the raw body via
constant-time hmac.Equal; redmine/discourse constant-time shared-secret
headers) with one generic ErrUnverified so rejects give attackers no
oracle. Tolerant normalization of the known Redmine/Discourse/Gitea
payload variants into one Event record (canonical subject ids, actor,
title, repo, sha256 payload digest) plus the DESIGN action mapping
(dispatch_turn / respond_turn / pipeline_step / ignore). Store: JSONL
under state dir, 0600, dedup keyed on provider event id (delivery
header, payload-digest fallback), index rebuilt at startup so replays
across restarts still dedup; torn tail lines skipped, not fatal.
This commit is contained in:
2026-08-28 21:38:16 -05:00
parent 164b14592b
commit 05ec1a4142
7 changed files with 948 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
package events
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
)
// Store is the append-only event log: one Event per JSON line under
// stateDir/events.jsonl, deduped by provider event id (source:providerID).
// The dedup index is rebuilt from the file at open, so restarts keep the
// at-least-once semantics (a torn tail line is skipped, not fatal).
type Store struct {
mu sync.Mutex
path string
f *os.File
seen map[string]bool
}
// OpenStore creates/opens the state dir and the event log.
func OpenStore(stateDir string) (*Store, error) {
if err := os.MkdirAll(stateDir, 0o700); err != nil {
return nil, fmt.Errorf("events state dir: %w", err)
}
path := filepath.Join(stateDir, "events.jsonl")
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
if err != nil {
return nil, fmt.Errorf("open event log: %w", err)
}
s := &Store{path: path, f: f, seen: make(map[string]bool)}
if err := s.loadIndex(); err != nil {
f.Close()
return nil, err
}
return s, nil
}
// loadIndex populates the dedup set from the existing log.
func (s *Store) loadIndex() error {
rf, err := os.Open(s.path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("read event log: %w", err)
}
defer rf.Close()
sc := bufio.NewScanner(rf)
sc.Buffer(make([]byte, 0, 64*1024), 1<<20)
for sc.Scan() {
var ev Event
if err := json.Unmarshal(sc.Bytes(), &ev); err != nil {
continue // torn/malformed tail line; dedup stays conservative
}
if ev.ProviderID != "" {
s.seen[ev.DedupKey()] = true
}
}
return sc.Err()
}
// Append records ev unless its provider event id is already logged.
// stored=false means duplicate (replay); err means the write failed and the
// event is NOT deduped (safe to retry).
func (s *Store) Append(ev Event) (stored bool, err error) {
s.mu.Lock()
defer s.mu.Unlock()
key := ev.DedupKey()
if s.seen[key] {
return false, nil
}
line, err := json.Marshal(ev)
if err != nil {
return false, fmt.Errorf("encode event: %w", err)
}
if _, err := s.f.Write(append(line, '\n')); err != nil {
return false, fmt.Errorf("append event log: %w", err)
}
s.seen[key] = true
return true, nil
}
// Len reports how many distinct events the index holds (ops/tests).
func (s *Store) Len() int {
s.mu.Lock()
defer s.mu.Unlock()
return len(s.seen)
}
// Path exposes the log location (ops messages only; contents stay put).
func (s *Store) Path() string { return s.path }
// Close closes the underlying file.
func (s *Store) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
return s.f.Close()
}