# 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`. ### 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, secretKey: string): Promise`: - 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`: - 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`.