Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,331 @@
# 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.

View File

@@ -0,0 +1,191 @@
# Tasks: enterprise-audit-logging
**Change ID**: enterprise-audit-logging
**Package**: `@suaveplan/audit-logging`
**Target path**: `universal/auth/audit-logging`
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/crypto`, `@suaveplan/logger`, `@suaveplan/serialize`
---
## 1. Scaffold
- [x] Create directory `universal/auth/audit-logging/` with `src/`, `tests/`, `src/__benchmarks__/`, and all feature subdirectories per the layout in `proposal.md`.
- [x] Write `package.json` at version `0.1.0` with:
- `"type": "module"`, `"sideEffects": false`
- `exports` map with top-level `"."` plus subpaths: `"./adapters/storage-adapter/storage-adapter"`, `"./adapters/memory-adapter/memory-adapter"`, `"./core/audit-logger/audit-logger"`, `"./core/chain-integrity/chain-integrity"`, `"./core/gdpr/gdpr-export"`, `"./core/retention/retention-policy"`, `"./schemas/audit-logging.schemas"`, `"./types/audit-logging.types"`
- `dependencies`: `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@suaveplan/events workspace:^`, `@suaveplan/crypto workspace:^`, `@suaveplan/logger workspace:^`, `@suaveplan/serialize workspace:^`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`
- `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `fast-check catalog:testing`, `mitata catalog:testing`, `@suaveplan/testing workspace:^`
- [x] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`.
- [x] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config` (pool: `forks`).
- [x] Add `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check`, `bench` scripts to `package.json`.
---
## 2. Types and errors
### 2a. Types (`src/types/audit-logging.types.ts` + `.md`)
- [x] Define all public interfaces from `proposal.md`: `AuditActor`, `AuditTarget`, `AuditEvent`, `AuditLoggerOptions`, `AuditQueryOptions`, `AuditRetentionPolicy`, `GDPRDataExport`, `ChainVerificationResult`, `RetentionResult`.
- [x] Export `AuditEventType` and `AuditSeverity` enums (move to `constants/index.ts` and re-export from types for backward compat).
- [x] Write `audit-logging.types.md` (≥200 words, API reference for every type and enum member).
### 2b. Error codes (`src/error-codes.ts`)
- [x] Define `AuditErrorCode` union — six codes: `AUDIT_STORAGE_ERROR`, `AUDIT_CHAIN_BROKEN`, `AUDIT_INVALID_EVENT`, `AUDIT_EXPORT_FAILED`, `AUDIT_RETENTION_FAILED`, `AUDIT_SECRET_KEY_MISSING`.
- [x] Define `AUDIT_ERROR_METADATA` with `httpStatus`, `severity`, `retryable`, `userMessageKey`, `category`, `description` per code.
- [x] Export `registerAuditErrorCodes()` as idempotent eager side-effect.
- [x] Export `AuditError extends SuaveplanError<AuditErrorCode>`.
### 2c. Zod schemas (`src/schemas/audit-logging.schemas.ts` + `.md`)
- [x] Write Zod schemas: `AuditEventSchema`, `AuditActorSchema`, `AuditTargetSchema`, `AuditQueryOptionsSchema`, `AuditLoggerOptionsSchema`, `AuditRetentionPolicySchema`, `GDPRDataExportSchema`.
- [x] Write `audit-logging.schemas.md`.
- [x] Write `audit-logging.schemas.test.ts` — parse valid/invalid for each schema; verify enum members.
---
## 3. Storage adapter interface (`src/adapters/storage-adapter/`)
- [x] Define `AuditStorageAdapter` interface: `save`, `query`, `getLatestEvent`, `deleteExpired`, `archive`, `count`.
- [x] Write `storage-adapter.types.ts`.
- [x] Write `storage-adapter.md` (≥200 words — interface contract, expected error semantics, query filter semantics).
---
## 4. MemoryAuditAdapter (`src/adapters/memory-adapter/`)
- [x] Implement `MemoryAuditAdapter` using an in-memory array of `AuditEvent`:
- `save(event)` — appends to array.
- `query(options)` — applies all filters from `AuditQueryOptions`, sorts by `timestamp` per `order`, applies `limit` + `offset`.
- `getLatestEvent(tenantId?)` — returns the most recent event for tenantId (or globally).
- `deleteExpired(before)` — removes events with `timestamp < before`; returns count deleted.
- `archive(before, location)` — for in-memory adapter, marks events as archived (or removes); returns count.
- `count(options?)` — returns filtered count without full query overhead.
- [x] Write `memory-adapter.test.ts` — save, query with each filter type, pagination, sort order, getLatestEvent, deleteExpired, count. Each `it` has `{ timeout: 5000 }`.
- [x] Write `memory-adapter.md`.
---
## 5. Chain integrity (`src/core/chain-integrity/`)
- [x] Implement `computeEventHash(event: Omit<AuditEvent, 'hash'>, secretKey: string): Promise<string>`:
- Use `@suaveplan/serialize`'s deterministic JSON encoder for the canonical form of the event.
- Compute HMAC-SHA256 via `@suaveplan/crypto`'s `hmac`.
- Return hex-encoded digest.
- [x] Implement `verifyChain(events: AuditEvent[], secretKey: string): Promise<ChainVerificationResult>`:
- For each event at index `i`, recompute expected hash; compare with `event.hash` using constant-time equality.
- Verify `event.previousHash === events[i-1].hash` (except the first event).
- Return `{ valid: true }` or `{ valid: false, brokenAt: i, reason: string }` on first failure.
- [x] Write `chain-integrity.test.ts` — valid chain, tampered event hash, tampered previousHash, single-event chain, empty chain. Each `it` has `{ timeout: 5000 }`.
- [x] Write `chain-integrity.md`.
---
## 6. AuditLogger (`src/core/audit-logger/`)
- [x] Implement `AuditLogger(options)`:
- Validate `options` with `AuditLoggerOptionsSchema`; throw `AuditError` with `AUDIT_SECRET_KEY_MISSING` if `secretKey` is empty.
- `log(partialEvent)`:
- Assign `id` via `idGenerator.generate()` (default: `crypto.randomUUID()`).
- Assign `timestamp` via `clock.now()`.
- If `enableChainIntegrity`, fetch previous event via `storage.getLatestEvent(tenantId)` and set `previousHash`.
- Compute `hash` via `computeEventHash`.
- Save via `storage.save(event)`.
- Emit `audit:event:logged` on `bus`.
- Instrument with `audit.log` span and `audit.events_logged_total` counter.
- Return the complete `AuditEvent`.
- `query(options)` — delegates to `storage.query`; instruments with `audit.query` span.
- `verifyChainIntegrity(events)` — delegates to `verifyChain`; emits `audit:chain:broken` on bus if invalid; instruments with `audit.chain_verify` span.
- [x] Write `audit-logger.test.ts` (≤500 lines) — log returns complete event with hash, chain linking (previousHash), query delegation, verifyChainIntegrity valid + broken chain, secret key missing error, FakeClock for deterministic timestamps, IdGenerator for deterministic IDs, event emission. `{ timeout: 5000 }` / `{ timeout: 30000 }` as appropriate.
- [x] Write `audit-logger.md`.
---
## 7. GDPR export (`src/core/gdpr/`)
- [x] Implement `exportGDPRData(userId, storage, options?, signal?)`:
- Query all events for `userId` via `storage.query({ userId })` in pages; respect `signal` between pages.
- Build `GDPRDataExport` with `summary.totalEvents`, `summary.eventTypes` histogram, `summary.dateRange`.
- Instrument with `audit.gdpr_export` span and counter.
- On abort, throw `AuditError` with `AUDIT_EXPORT_FAILED` (cause: `AbortError`).
- [x] Write `gdpr-export.test.ts` — export with multiple event types, FakeClock, AbortSignal mid-export, empty user history. Each `it` has `{ timeout: 30000 }`.
- [x] Write `gdpr-export.md`.
---
## 8. Retention policy (`src/core/retention/`)
- [x] Implement `applyRetentionPolicy(storage, policy, clock, signal?)`:
- Compute cutoff = `clock.now() - policy.retentionDays * 86_400_000`.
- If `policy.archiveBeforeDelete`, call `storage.archive(cutoff, policy.archiveLocation)` first.
- Call `storage.deleteExpired(cutoff)`.
- Emit `audit:retention:applied` on bus.
- Instrument with `audit.retention_apply` span and `audit.events_deleted_total` / `audit.events_archived_total` counters.
- On abort, throw `AuditError` with `AUDIT_RETENTION_FAILED`.
- Returns `RetentionResult`.
- [x] Write `retention-policy.test.ts` — archive+delete, delete-only, FakeClock for cutoff computation, AbortSignal cancellation, zero events case. Each `it` has `{ timeout: 5000 }`.
- [x] Write `retention-policy.md`.
---
## 9. OTel instrumentation (cross-cutting review)
- [x] Confirm all spans and counters from `proposal.md §5` are wired in each module.
- [x] Add `tests/otel.test.ts` — in-memory exporter; assert `audit.events_logged_total` increments, `audit.query_duration_ms` records, `audit.chain_verify_total` records valid/invalid labels.
---
## 10. Property-based tests (`tests/property.test.ts`)
Use `fast-check`. All test timeouts `{ timeout: 30000 }`.
- [x] **HMAC stability**: same event data (property-order independent via deterministic serialize) always produces the same hash.
- [x] **Chain integrity breakage**: mutating any field of any event in an N-event chain causes `verifyChain` to return `brokenAt` at the mutated index.
- [x] **Query filter correctness**: all events returned by `query({ userId: X })` have `actor.userId === X`; no events with other userIds are returned.
- [x] **Retention idempotency**: applying retention policy twice with the same cutoff (via FakeClock) reports zero deletes and zero archives on the second run.
---
## 11. Benchmarks (`src/__benchmarks__/audit-logging.bench.ts`)
Use `mitata`.
- [x] `log_with_chain_integrity`: 10 000 log calls, chain-integrity enabled.
- [x] `log_without_chain_integrity`: 10 000 log calls, chain-integrity disabled.
- [x] `query_10k_events`: 10 000 query calls on a MemoryAuditAdapter with 10 000 pre-loaded events.
- [x] `verify_chain_100` / `verify_chain_1k` / `verify_chain_10k`: chain verification at three scales.
- [x] Document placeholder numbers in `README.md` under `## Benchmarks`.
---
## 12. Docs
Per-file `.md` (all ≥200 words):
- [x] `src/types/audit-logging.types.md`
- [x] `src/adapters/storage-adapter/storage-adapter.md`
- [x] `src/adapters/memory-adapter/memory-adapter.md`
- [x] `src/core/audit-logger/audit-logger.md`
- [x] `src/core/chain-integrity/chain-integrity.md`
- [x] `src/core/gdpr/gdpr-export.md`
- [x] `src/core/retention/retention-policy.md`
- [x] `src/schemas/audit-logging.schemas.md`
Root README:
- [x] Write `universal/auth/audit-logging/README.md`: purpose, install, quick-start (log + query + verify), chain-integrity guide, GDPR export guide, modules table, benchmarks placeholder.
---
## 13. Verify and publish
- [x] Run `bunx turbo lint --filter=@suaveplan/audit-logging` — zero errors.
- [x] Run `bunx turbo typecheck --filter=@suaveplan/audit-logging` — zero errors.
- [x] Run `bunx turbo test --filter=@suaveplan/audit-logging` — all tests pass.
- [x] Run `bunx turbo test:coverage --filter=@suaveplan/audit-logging` — 100/100/100/100.
- [x] Run `bun run docs:check --filter=@suaveplan/audit-logging` — zero errors.
- [x] Confirm no `TODO`, `FIXME`, `@deprecated`, untyped `any`, `ts-ignore`, or `.skip` in `src/` or `tests/`.
- [x] Confirm `"sideEffects": false` in `package.json`.
- [x] Run `bun scripts/publish.ts --package @suaveplan/audit-logging`.
- [x] Update `CLAUDE.md` package listing to include `@suaveplan/audit-logging`.
- [x] Archive: move `openspec/changes/enterprise-audit-logging/` to `openspec/archive/2026-04-24-enterprise-audit-logging/` and fold spec delta into `openspec/specs/audit-logging/spec.md`.