package audit import ( "bufio" "encoding/json" "fmt" "os" "path/filepath" "sync" "github.com/netkingdom/flex-auth/pkg/api" ) // JSONLDecisionLog persists compact decision envelopes for local development. type JSONLDecisionLog struct { path string mu sync.Mutex } // NewJSONLDecisionLog returns a JSONL-backed decision log. func NewJSONLDecisionLog(path string) *JSONLDecisionLog { return &JSONLDecisionLog{path: path} } // Append writes one decision envelope as one JSON line. func (l *JSONLDecisionLog) Append(decision api.DecisionEnvelope) error { l.mu.Lock() defer l.mu.Unlock() if err := os.MkdirAll(filepath.Dir(l.path), 0o755); err != nil { return fmt.Errorf("create decision log directory: %w", err) } file, err := os.OpenFile(l.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { return fmt.Errorf("open decision log: %w", err) } defer file.Close() data, err := json.Marshal(decision) if err != nil { return fmt.Errorf("marshal decision envelope: %w", err) } if _, err := file.Write(append(data, '\n')); err != nil { return fmt.Errorf("write decision log: %w", err) } return nil } // ReadAll returns every decision envelope from the log in file order. func (l *JSONLDecisionLog) ReadAll() ([]api.DecisionEnvelope, error) { l.mu.Lock() defer l.mu.Unlock() file, err := os.Open(l.path) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, fmt.Errorf("open decision log: %w", err) } defer file.Close() var decisions []api.DecisionEnvelope scanner := bufio.NewScanner(file) for scanner.Scan() { if scanner.Text() == "" { continue } var decision api.DecisionEnvelope if err := json.Unmarshal(scanner.Bytes(), &decision); err != nil { return nil, fmt.Errorf("unmarshal decision log line %d: %w", len(decisions)+1, err) } decisions = append(decisions, decision) } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("read decision log: %w", err) } return decisions, nil } // Find returns one decision envelope by id. func (l *JSONLDecisionLog) Find(id string) (api.DecisionEnvelope, bool, error) { decisions, err := l.ReadAll() if err != nil { return api.DecisionEnvelope{}, false, err } for _, decision := range decisions { if decision.ID == id { return decision, true, nil } } return api.DecisionEnvelope{}, false, nil }