package store import ( "context" "sync" "time" ) // Memory is the Phase-1 in-process Store. Thread-safe via a single RWMutex; // each call is cheap enough that per-table locks would be over-engineering. type Memory struct { mu sync.RWMutex sessions map[string]Session previews map[string]Preview approvals map[string]Approval } // NewMemory returns an empty Memory store. func NewMemory() *Memory { return &Memory{ sessions: map[string]Session{}, previews: map[string]Preview{}, approvals: map[string]Approval{}, } } func (m *Memory) CreateSession(_ context.Context, s Session) error { m.mu.Lock() defer m.mu.Unlock() m.sessions[s.ID] = s return nil } func (m *Memory) GetSession(_ context.Context, id string) (Session, error) { m.mu.RLock() defer m.mu.RUnlock() s, ok := m.sessions[id] if !ok { return Session{}, ErrNotFound } return s, nil } func (m *Memory) SavePreview(_ context.Context, p Preview) error { m.mu.Lock() defer m.mu.Unlock() m.previews[p.ID] = p return nil } func (m *Memory) GetPreview(_ context.Context, id string) (Preview, error) { m.mu.RLock() defer m.mu.RUnlock() p, ok := m.previews[id] if !ok { return Preview{}, ErrNotFound } // Return a copy so callers cannot mutate our byte slices. return clonePreview(p), nil } // ConsumePreview marks the preview as consumed if it is still valid and // unconsumed. The returned Preview is the pre-mutation copy. func (m *Memory) ConsumePreview(_ context.Context, id string, now time.Time) (Preview, error) { m.mu.Lock() defer m.mu.Unlock() p, ok := m.previews[id] if !ok { return Preview{}, ErrNotFound } if p.ConsumedAt != nil { return Preview{}, ErrAlreadyConsumed } snapshot := clonePreview(p) ts := now p.ConsumedAt = &ts m.previews[id] = p return snapshot, nil } func (m *Memory) SaveApproval(_ context.Context, a Approval) error { m.mu.Lock() defer m.mu.Unlock() m.approvals[a.ID] = a return nil } func (m *Memory) MarkApprovalLogged(_ context.Context, id string, logIndex int64, loggedAt time.Time) error { m.mu.Lock() defer m.mu.Unlock() a, ok := m.approvals[id] if !ok { return ErrNotFound } a.TLogStatus = TLogLogged a.LogIndex = &logIndex a.LoggedAt = &loggedAt a.TLogError = "" m.approvals[id] = a return nil } func (m *Memory) MarkApprovalTLogError(_ context.Context, id string, errorMsg string) error { m.mu.Lock() defer m.mu.Unlock() a, ok := m.approvals[id] if !ok { return ErrNotFound } // Keep status at attestation-pending — a reconciler will retry. We // only record the last error message for diagnostics. a.TLogError = errorMsg m.approvals[id] = a return nil } // GetApproval is a test-side accessor; not part of the Store interface // because the HTTP handlers never need it. func (m *Memory) GetApproval(id string) (Approval, bool) { m.mu.RLock() defer m.mu.RUnlock() a, ok := m.approvals[id] return a, ok } func clonePreview(p Preview) Preview { out := p out.ContentCanonical = append([]byte(nil), p.ContentCanonical...) out.Salt = append([]byte(nil), p.Salt...) out.Nonce = append([]byte(nil), p.Nonce...) if p.ConsumedAt != nil { t := *p.ConsumedAt out.ConsumedAt = &t } return out }