Skip to content

feat(domain): add configuration audit trail with diff detection and rollback#796

Merged
diegosouzapw merged 1 commit intodiegosouzapw:mainfrom
igormorais123:feat/config-audit-trail
Mar 30, 2026
Merged

feat(domain): add configuration audit trail with diff detection and rollback#796
diegosouzapw merged 1 commit intodiegosouzapw:mainfrom
igormorais123:feat/config-audit-trail

Conversation

@igormorais123
Copy link
Copy Markdown

Summary

  • Add configAudit.ts to src/domain/
  • Record every change to providers, combos, policies, and connections
  • Compute structured diffs (added/removed/changed keys)
  • Track change source (dashboard, API, sync, auto-healing, CLI, MCP)
  • Enable rollback via getRollbackState(entryId)
  • Support configuration snapshots for export/import

Motivation

When a routing config change causes failures, there's currently no way to:

  1. See what changed and when
  2. Compare current config vs. a known-good state
  3. Rollback to a previous working configuration

This is critical after provider cleanups (disabling dead connections) or combo reordering — operations that can silently break routing.

API

// Record a change
recordChange('update', 'combo', 'coding-power', 'Coding Power', 
  { models: ['codex', 'opus'] },        // before
  { models: ['codex', 'sonnet', 'opus'] }, // after
  'dashboard',
  'Added sonnet as second fallback'
);

// Query audit log
const { entries, total } = getAuditLog({ target: 'combo', limit: 20 });

// Rollback: get state before a change
const previousState = getRollbackState(entry.id);

// Export snapshot
const snapshot = createSnapshot('v4.1', 'Before cleanup', fullConfig);

// Summary
const summary = getAuditSummary();
// → { totalEntries: 47, byTarget: { combo: 12, provider: 35 }, ... }

Test plan

  • Diff computation: added, removed, changed keys
  • CRUD audit entries
  • Filtered retrieval with pagination
  • Rollback state extraction
  • Snapshot creation
  • Summary statistics
  • Log bounded to 1000 entries

Relates to #791

🤖 Generated with Claude Code

…ollback

Add configAudit module that records every change to provider connections,
combos, and routing policies with:

- Before/after state snapshots
- Structured diff (added, removed, changed keys)
- Source tracking (dashboard, API, sync, auto-healing)
- Filtered retrieval with pagination
- Rollback state extraction
- Configuration snapshot export for backup

Enables traceability and quick rollback when config changes cause issues.

Closes diegosouzapw#791
@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot bot commented Mar 30, 2026

Code Review Summary

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 3
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
src/domain/configAudit.ts 80 ID Collision Risk: The generateId() function uses idCounter++ which will eventually overflow. While the counter uses base-36 and padStart(4, "0"), there's no protection against the counter wrapping or multiple server instances generating the same IDs. Consider using crypto.randomUUID() or include a server-specific prefix.

WARNING

File Line Issue
src/domain/configAudit.ts 73-74 Incorrect oldest/newest timestamps in getAuditSummary(): The code returns auditLog[0] and auditLog[auditLog.length - 1] as oldest/newest, but the log is sorted by newest first when entries are added. Since entries are pushed and then sliced, the oldest entry is actually at the END of the array, not the beginning. The function should iterate to find min/max timestamps instead.

| src/domain/configAudit.ts | 161 | Missing input validation: The targetId parameter in recordChange() has no length or format validation. Consider adding bounds checking to prevent extremely long IDs from being stored.

| src/domain/configAudit.ts | 73 | In-memory storage limitation: The audit log is stored in-memory and will be lost on server restart. While this is documented in a comment, it poses a risk in production environments where audit trail persistence is critical for troubleshooting. Consider integrating with the SQLite database layer used elsewhere in the codebase.

Files Reviewed (1 file)
  • src/domain/configAudit.ts - 4 issues

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a configuration audit trail system in src/domain/configAudit.ts to track changes to entities like providers and policies with before/after snapshots. Key feedback includes replacing the counter-based ID generation with UUIDs for better reliability, using a more robust deep comparison method instead of JSON.stringify for computing diffs, and optimizing the in-memory log bounding logic by using shift() instead of slice().

Comment on lines +79 to +86
let idCounter = 0;

function generateId(): string {
idCounter++;
const ts = Date.now().toString(36);
const seq = idCounter.toString(36).padStart(4, "0");
return `audit-${ts}-${seq}`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current generateId implementation using a simple idCounter is not robust. It's not guaranteed to be unique across application restarts or in a concurrent environment. A better approach is to use a universally unique identifier.

I suggest using crypto.randomUUID(), which is built into modern Node.js versions (v14.17.0+). This removes the need for idCounter and is much more reliable.

Suggested change
let idCounter = 0;
function generateId(): string {
idCounter++;
const ts = Date.now().toString(36);
const seq = idCounter.toString(36).padStart(4, "0");
return `audit-${ts}-${seq}`;
}
// idCounter is no longer needed when using UUIDs.
function generateId(): string {
// Using crypto.randomUUID() for globally unique identifiers.
// This is more robust than a timestamp-based counter.
// Add `import { randomUUID } from 'node:crypto';` to file imports.
return `audit-${randomUUID()}`;
}

if (afterKeys.has(key)) {
const beforeVal = before![key];
const afterVal = after![key];
if (JSON.stringify(beforeVal) !== JSON.stringify(afterVal)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using JSON.stringify for object comparison can be unreliable. The order of object keys is not guaranteed in JavaScript, which can lead to different JSON strings for semantically identical objects (e.g., {a:1, b:2} vs {b:2, a:1}).

For more robust deep object comparison, consider using a library like lodash.isEqual.

Comment on lines +165 to +167
if (auditLog.length > 1000) {
auditLog = auditLog.slice(-1000);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To keep the log bounded, the current code uses auditLog.slice(-1000). This creates a new array of up to 1000 elements every time an entry is added beyond the limit, which can be inefficient. Since entries are added one by one, a more performant approach is to simply remove the oldest element using auditLog.shift().

Suggested change
if (auditLog.length > 1000) {
auditLog = auditLog.slice(-1000);
}
if (auditLog.length > 1000) {
auditLog.shift();
}

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 94a5e43e5d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +155 to +156
before,
after,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clone before/after snapshots when recording changes

recordChange stores before and after by reference, so later mutations to the original config objects rewrite historical audit entries and invalidate rollback data. This breaks the core audit-trail guarantee in common flows where callers reuse mutable config objects (e.g., update object then pass it through additional transformations). Store deep-cloned snapshots at write time so history remains immutable.

Useful? React with 👍 / 👎.

export function getRollbackState(entryId: string): Record<string, unknown> | null {
const entry = getAuditEntry(entryId);
if (!entry) return null;
return entry.before;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Return a copy from rollback state access

getRollbackState returns the internal entry.before object directly, which lets callers mutate the audit log by editing the returned value. After one rollback attempt that modifies this object, future rollbacks and log inspection can be silently corrupted. Return a cloned snapshot instead of the in-memory reference.

Useful? React with 👍 / 👎.

if (afterKeys.has(key)) {
const beforeVal = before![key];
const afterVal = after![key];
if (JSON.stringify(beforeVal) !== JSON.stringify(afterVal)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use structural compare instead of JSON stringify diff

Diff detection currently uses JSON.stringify equality, which is order-sensitive for object keys. Two semantically identical configs with different key insertion order will be marked as changed, producing noisy or misleading audit entries and potentially triggering unnecessary rollback decisions. Use a stable deep-equality check that ignores key ordering for plain objects.

Useful? React with 👍 / 👎.

@diegosouzapw
Copy link
Copy Markdown
Owner

Olá, Professor Igor! Muito obrigado pela iniciativa fenomenal! 🕵️‍♂️📝

Manter um log de auditoria (Audit Trail) contra mudanças que possam quebrar as políticas e o fluxo de proxy é uma feature muito aguardada, especialmente pensando em rollbacks de configurações falhas!

Para transformarmos este conceito genial em uma feature de produção pronta para o OmniRoute, analise as sugestões abaixo para ajustarmos este Pull Request:

Pontos de Adaptação (Padrão de Arquitetura)

  1. O Log de Auditoria precisa sobreviver a reinícios (SQLite)
    Atualmente a variável let auditLog: ConfigAuditEntry[] = []; mora unicamente em memória volátil. Devido ao ciclo de vida do proxy, configurações em runtime são perdidas quando o container/processo reinicia.
    O que fazer: Crie um modulo de banco de dados (src/lib/db/audit.ts) ou integre ao banco existente gerenciado em src/lib/db/core.ts. Todas as gravações recordChange devem fazer um INSERT seguro. Isso protegerá nosso log de auditoria persistentemente.

  2. Integração Real com o Core App
    Assim como um bom livro na prateleira precisa ser lido, o seu código precisa ser usado pelo sistema. Atualmente as funções recordChange(), getAuditLog() e getRollbackState() estão perfeitamente definidas... mas não possuem nenhum apelador.
    O que fazer: Integre a função recordChange nos endpoints de Update de Providers/Combos reais. (Para fins didáticos, você pode adicioná-lo dentro de src/app/api/settings/route.ts ou funções que salvam provedores no banco).

  3. Cobertura de Testes (Faltante)
    Seu checklist aponta os testes como desenvolvidos, porém nenhum arquivo .test.ts foi adicionado à remessa ou ao diff. Certifique-se de realizar o commit do seu arquivo de teste unitário.

Por favor, faça essas adições arquiteturais e atualize sua branch feat/config-audit-trail. A sua lógica de diff computation (keys modified/added/removed) é fantástica, estamos ansiosos para aplicar esse código na infraestrutura quando as integrações passarem! Um super abraço! 🤝

@diegosouzapw diegosouzapw merged commit b502a93 into diegosouzapw:main Mar 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants