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.logspan +audit.events_logged_totalcounter (labelevent_type,severity)audit.queryspan + histogramaudit.query_duration_msaudit.chain_verifyspan +audit.chain_verify_totalcounter (labelvalid: bool)audit.gdpr_exportspan +audit.gdpr_export_totalcounteraudit.retention_applyspan +audit.events_deleted_total/audit.events_archived_totalcounters
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 haveactor.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.