Files
2026-05-27 23:29:53 +01:00

11 KiB

OpenSpec Change: enterprise-audit-logging

  • Change ID: enterprise-audit-logging
  • Package: @suaveplan/audit-logging
  • Category: universal
  • Target path: universal/auth/audit-logging
  • Status: PENDING
  • Date: 2026-04-24
  • Reference spec: /mnt/s/web/suaveplan/enterprise/audit-logging.md

Summary

Deliver @suaveplan/audit-logging on the genesis branch from scratch at enterprise quality. The package provides a tamper-proof, GDPR-compliant audit trail: AuditLogger with cryptographic chain-integrity signing, pluggable storage adapters, retention policy enforcement, GDPR data export, and a rich event taxonomy spanning auth, authorization, data access, and system events. The main-branch implementation was tightly coupled to SurrealDB and Node's node:crypto; the genesis rewrite is database-agnostic with a StorageAdapter interface, uses @suaveplan/crypto for HMAC signing, adds structured errors via @suaveplan/error, Zod schemas, an injectable clock and ID generator, OTel instrumentation, property-based tests, and mitata benchmarks.


Current-state audit (genesis)

The package does not exist on genesis yet — this is a fresh build.


Available workspace deps on genesis

Package Role
@suaveplan/error (workspace:^) Base class for AuditError; defineCodes for AUDIT.* error codes
@suaveplan/types (workspace:^) Clock (injectable timestamps), IdGenerator (event IDs), LoggerLike, StorageLike
@suaveplan/events (workspace:^) EventBusLike for audit lifecycle events
@suaveplan/crypto (workspace:^) HMAC-SHA256 for chain-integrity signing, zeroize for secret clearing
@suaveplan/logger (workspace:^) Structured logging with trace correlation
@suaveplan/serialize (workspace:^) Deterministic JSON serialization for HMAC signing stability
zod (catalog:forms) Zod schemas for all public option types and event shapes
@opentelemetry/api (catalog:observability) Spans, counters, histograms
tslib (catalog:build) TypeScript runtime helpers

Dev deps: vitest, @vitest/coverage-istanbul, fast-check, mitata (all catalog:testing), @suaveplan/testing (workspace:^).


Preserved public API surface (from main branch)

Event taxonomy enums

enum AuditEventType {
  // Authentication
  AUTH_LOGIN, AUTH_LOGOUT, AUTH_REGISTER, AUTH_PASSWORD_CHANGE,
  AUTH_PASSWORD_RESET, AUTH_MFA_ENABLE, AUTH_MFA_DISABLE,
  AUTH_SESSION_CREATE, AUTH_SESSION_REVOKE,
  AUTH_API_KEY_CREATE, AUTH_API_KEY_REVOKE,
  // Authorization
  AUTHZ_PERMISSION_CHECK, AUTHZ_PERMISSION_GRANT, AUTHZ_PERMISSION_REVOKE,
  AUTHZ_ROLE_ASSIGN, AUTHZ_ROLE_UNASSIGN,
  // Data access
  DATA_CREATE, DATA_READ, DATA_UPDATE, DATA_DELETE, DATA_EXPORT, DATA_IMPORT,
  // System
  SYSTEM_CONFIG_CHANGE, SYSTEM_SECURITY_ALERT, SYSTEM_RATE_LIMIT, SYSTEM_ERROR,
  // GDPR
  GDPR_DATA_EXPORT, GDPR_DATA_DELETE, GDPR_CONSENT_CHANGE
}

enum AuditSeverity {
  DEBUG = "debug",
  INFO = "info",
  WARNING = "warning",
  ERROR = "error",
  CRITICAL = "critical"
}

Core types

interface AuditActor {
  userId?: string
  sessionId?: string
  apiKeyId?: string
  ipAddress?: string
  userAgent?: string
  tenantId?: string
}

interface AuditTarget {
  type: string
  id: string
  metadata?: Record<string, unknown>
}

interface AuditEvent {
  id: string
  type: AuditEventType | string
  severity: AuditSeverity
  actor: AuditActor
  target?: AuditTarget
  details?: Record<string, unknown>
  timestamp: Date
  previousHash?: string          // chain-integrity: hash of the previous event
  hash: string                   // HMAC-SHA256 of this event's canonical form
  tenantId?: string
  correlationId?: string
}

interface AuditLoggerOptions {
  storage: AuditStorageAdapter
  secretKey: string
  tableName?: string
  retentionPolicy?: AuditRetentionPolicy
  enableChainIntegrity?: boolean
  clock?: Clock
  idGenerator?: IdGenerator
  logger?: LoggerLike
  bus?: EventBusLike
}

interface AuditQueryOptions {
  eventTypes?: AuditEventType[]
  userId?: string
  sessionId?: string
  severity?: AuditSeverity | AuditSeverity[]
  targetType?: string
  targetId?: string
  startTime?: Date
  endTime?: Date
  limit?: number
  offset?: number
  order?: "asc" | "desc"
  tenantId?: string
}

interface AuditRetentionPolicy {
  retentionDays: number
  archiveBeforeDelete: boolean
  archiveLocation?: string
}

interface GDPRDataExport {
  userId: string
  exportedAt: Date
  auditEvents: AuditEvent[]
  summary: {
    totalEvents: number
    eventTypes: Record<string, number>
    dateRange: { first: Date; last: Date }
  }
}

AuditLogger class

class AuditLogger {
  constructor(options: AuditLoggerOptions)
  log(event: Omit<AuditEvent, 'id' | 'timestamp' | 'hash' | 'previousHash'>): Promise<AuditEvent>
  query(options: AuditQueryOptions): Promise<AuditEvent[]>
  verifyChainIntegrity(events: AuditEvent[]): Promise<ChainVerificationResult>
  exportGDPRData(userId: string, signal?: AbortSignal): Promise<GDPRDataExport>
  applyRetentionPolicy(signal?: AbortSignal): Promise<RetentionResult>
}

interface ChainVerificationResult {
  valid: boolean
  brokenAt?: number       // index in the events array where chain breaks
  reason?: string
}

interface RetentionResult {
  eventsDeleted: number
  eventsArchived: number
  errors: string[]
}

Storage adapter interface

interface AuditStorageAdapter {
  save(event: AuditEvent): Promise<void>
  query(options: AuditQueryOptions): Promise<AuditEvent[]>
  getLatestEvent(tenantId?: string): Promise<AuditEvent | null>
  deleteExpired(before: Date): Promise<number>
  archive(before: Date, location: string): Promise<number>
  count(options?: Partial<AuditQueryOptions>): Promise<number>
}

// In-memory adapter for tests and single-node use
class MemoryAuditAdapter implements AuditStorageAdapter

Error codes

type AuditErrorCode =
  | "AUDIT_STORAGE_ERROR"
  | "AUDIT_CHAIN_BROKEN"
  | "AUDIT_INVALID_EVENT"
  | "AUDIT_EXPORT_FAILED"
  | "AUDIT_RETENTION_FAILED"
  | "AUDIT_SECRET_KEY_MISSING"

class AuditError extends SuaveplanError<AuditErrorCode>
function registerAuditErrorCodes(): void

New additions for genesis

1. Database-agnostic AuditStorageAdapter

Replaces the hard SurrealDB coupling. MemoryAuditAdapter ships in-package. Database-specific adapters (SurrealDB, PostgreSQL, SQLite) live in separate packages.

2. Chain integrity via @suaveplan/crypto

HMAC-SHA256 signing uses @suaveplan/crypto's hmac (delegates to @suaveplan/crypto-hash) instead of node:crypto. The secret key is zeroized on AuditLogger disposal. Canonical JSON serialization for HMAC input uses @suaveplan/serialize's deterministic encoder.

3. Clock and IdGenerator injection

AuditLogger accepts clock?: Clock and idGenerator?: IdGenerator. All Date.now() and randomUUID() calls delegated to injected instances so audit events have deterministic timestamps and IDs in tests.

4. AbortSignal on long-running operations

exportGDPRData and applyRetentionPolicy accept an optional AbortSignal; abort mid-iteration throws AuditError with AUDIT_EXPORT_FAILED or AUDIT_RETENTION_FAILED.

5. OTel instrumentation

  • audit.log span + audit.events_logged_total counter (label event_type, severity)
  • audit.query span + histogram audit.query_duration_ms
  • audit.chain_verify span + audit.chain_verify_total counter (label valid: bool)
  • audit.gdpr_export span + audit.gdpr_export_total counter
  • audit.retention_apply span + audit.events_deleted_total / audit.events_archived_total counters

6. Zod schemas

AuditEventSchema, AuditActorSchema, AuditTargetSchema, AuditQueryOptionsSchema, AuditLoggerOptionsSchema, AuditRetentionPolicySchema, GDPRDataExportSchema.

7. EventBus integration

Optional bus?: EventBusLike on AuditLoggerOptions. Emits:

Event Payload
audit:event:logged { id, type, severity, tenantId }
audit:chain:broken { brokenAt, reason }
audit:retention:applied { deleted, archived }

8. Property-based tests (fast-check)

  • HMAC stability: same event data always produces the same hash regardless of object property order.
  • Chain integrity: any mutation of any event in a sequence breaks verification at the mutated index.
  • Query filter correctness: events returned by query({ userId: X }) all have actor.userId === X.
  • Retention idempotency: applying retention policy twice with the same cutoff date deletes zero additional events on the second run.

9. Benchmark suite

  • AuditLogger.log — 10 000 iterations with chain-integrity enabled vs disabled.
  • AuditLogger.query — 10 000 queries on a MemoryAuditAdapter with 10 000 pre-loaded events.
  • verifyChainIntegrity — on 100, 1 000, 10 000 event chains.

File layout

universal/auth/audit-logging/
  package.json
  tsconfig.json
  vitest.config.ts
  src/
    index.ts
    error-codes.ts
    constants/
      index.ts                            # AuditEventType, AuditSeverity
    types/
      audit-logging.types.ts
      audit-logging.types.md
    adapters/
      storage-adapter/
        storage-adapter.ts                # AuditStorageAdapter interface
        storage-adapter.md
        storage-adapter.types.ts
        index.ts
      memory-adapter/
        memory-adapter.ts
        memory-adapter.md
        memory-adapter.test.ts
        index.ts
    core/
      audit-logger/
        audit-logger.ts                   # AuditLogger class
        audit-logger.md
        audit-logger.types.ts
        audit-logger.test.ts
        index.ts
      chain-integrity/
        chain-integrity.ts                # computeHash, verifyChain
        chain-integrity.md
        chain-integrity.test.ts
        index.ts
      gdpr/
        gdpr-export.ts                    # exportGDPRData logic
        gdpr-export.md
        gdpr-export.test.ts
        index.ts
      retention/
        retention-policy.ts               # applyRetentionPolicy logic
        retention-policy.md
        retention-policy.test.ts
        index.ts
    schemas/
      audit-logging.schemas.ts
      audit-logging.schemas.md
      audit-logging.schemas.test.ts
    __benchmarks__/
      audit-logging.bench.ts
  tests/
    integration.test.ts
    property.test.ts

Impact

Downstream packages: auth-related packages and API gateways will write audit events. The storage adapter pattern allows a SurrealDB adapter to be shipped separately.

Breaking changes from main-branch API: AuditLoggerOptions.database (SurrealDB) is replaced by AuditLoggerOptions.storage (adapter interface). All other exported symbols are preserved. No node:crypto import in the public interface.

Workspace dep prerequisites: @suaveplan/error, @suaveplan/types, @suaveplan/events, @suaveplan/crypto, @suaveplan/logger, @suaveplan/serialize must be published to Verdaccio before this package can be built or tested.