Files
suaveplan/openspec/archive/2026-04-24-enterprise-audit-logging/proposal.md
2026-05-27 23:29:53 +01:00

332 lines
11 KiB
Markdown

# 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
```ts
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
```ts
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
```ts
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
```ts
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
```ts
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 `zeroize`d 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
```text
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.