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

11 KiB

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

  • Create directory universal/auth/audit-logging/ with src/, tests/, src/__benchmarks__/, and all feature subdirectories per the layout in proposal.md.
  • 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:^
  • Write tsconfig.json extending @suaveplan/typescript-config/library.json.
  • Write vitest.config.ts using createTestConfig() from @suaveplan/testing/config (pool: forks).
  • 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)

  • Define all public interfaces from proposal.md: AuditActor, AuditTarget, AuditEvent, AuditLoggerOptions, AuditQueryOptions, AuditRetentionPolicy, GDPRDataExport, ChainVerificationResult, RetentionResult.
  • Export AuditEventType and AuditSeverity enums (move to constants/index.ts and re-export from types for backward compat).
  • Write audit-logging.types.md (≥200 words, API reference for every type and enum member).

2b. Error codes (src/error-codes.ts)

  • Define AuditErrorCode union — six codes: AUDIT_STORAGE_ERROR, AUDIT_CHAIN_BROKEN, AUDIT_INVALID_EVENT, AUDIT_EXPORT_FAILED, AUDIT_RETENTION_FAILED, AUDIT_SECRET_KEY_MISSING.
  • Define AUDIT_ERROR_METADATA with httpStatus, severity, retryable, userMessageKey, category, description per code.
  • Export registerAuditErrorCodes() as idempotent eager side-effect.
  • Export AuditError extends SuaveplanError<AuditErrorCode>.

2c. Zod schemas (src/schemas/audit-logging.schemas.ts + .md)

  • Write Zod schemas: AuditEventSchema, AuditActorSchema, AuditTargetSchema, AuditQueryOptionsSchema, AuditLoggerOptionsSchema, AuditRetentionPolicySchema, GDPRDataExportSchema.
  • Write audit-logging.schemas.md.
  • Write audit-logging.schemas.test.ts — parse valid/invalid for each schema; verify enum members.

3. Storage adapter interface (src/adapters/storage-adapter/)

  • Define AuditStorageAdapter interface: save, query, getLatestEvent, deleteExpired, archive, count.
  • Write storage-adapter.types.ts.
  • Write storage-adapter.md (≥200 words — interface contract, expected error semantics, query filter semantics).

4. MemoryAuditAdapter (src/adapters/memory-adapter/)

  • 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.
  • Write memory-adapter.test.ts — save, query with each filter type, pagination, sort order, getLatestEvent, deleteExpired, count. Each it has { timeout: 5000 }.
  • Write memory-adapter.md.

5. Chain integrity (src/core/chain-integrity/)

  • 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.
  • 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.
  • Write chain-integrity.test.ts — valid chain, tampered event hash, tampered previousHash, single-event chain, empty chain. Each it has { timeout: 5000 }.
  • Write chain-integrity.md.

6. AuditLogger (src/core/audit-logger/)

  • 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.
  • 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.
  • Write audit-logger.md.

7. GDPR export (src/core/gdpr/)

  • 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).
  • Write gdpr-export.test.ts — export with multiple event types, FakeClock, AbortSignal mid-export, empty user history. Each it has { timeout: 30000 }.
  • Write gdpr-export.md.

8. Retention policy (src/core/retention/)

  • 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.
  • Write retention-policy.test.ts — archive+delete, delete-only, FakeClock for cutoff computation, AbortSignal cancellation, zero events case. Each it has { timeout: 5000 }.
  • Write retention-policy.md.

9. OTel instrumentation (cross-cutting review)

  • Confirm all spans and counters from proposal.md §5 are wired in each module.
  • 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 }.

  • HMAC stability: same event data (property-order independent via deterministic serialize) always produces the same hash.
  • Chain integrity breakage: mutating any field of any event in an N-event chain causes verifyChain to return brokenAt at the mutated index.
  • Query filter correctness: all events returned by query({ userId: X }) have actor.userId === X; no events with other userIds are returned.
  • 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.

  • log_with_chain_integrity: 10 000 log calls, chain-integrity enabled.
  • log_without_chain_integrity: 10 000 log calls, chain-integrity disabled.
  • query_10k_events: 10 000 query calls on a MemoryAuditAdapter with 10 000 pre-loaded events.
  • verify_chain_100 / verify_chain_1k / verify_chain_10k: chain verification at three scales.
  • Document placeholder numbers in README.md under ## Benchmarks.

12. Docs

Per-file .md (all ≥200 words):

  • src/types/audit-logging.types.md
  • src/adapters/storage-adapter/storage-adapter.md
  • src/adapters/memory-adapter/memory-adapter.md
  • src/core/audit-logger/audit-logger.md
  • src/core/chain-integrity/chain-integrity.md
  • src/core/gdpr/gdpr-export.md
  • src/core/retention/retention-policy.md
  • src/schemas/audit-logging.schemas.md

Root README:

  • 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

  • Run bunx turbo lint --filter=@suaveplan/audit-logging — zero errors.
  • Run bunx turbo typecheck --filter=@suaveplan/audit-logging — zero errors.
  • Run bunx turbo test --filter=@suaveplan/audit-logging — all tests pass.
  • Run bunx turbo test:coverage --filter=@suaveplan/audit-logging — 100/100/100/100.
  • Run bun run docs:check --filter=@suaveplan/audit-logging — zero errors.
  • Confirm no TODO, FIXME, @deprecated, untyped any, ts-ignore, or .skip in src/ or tests/.
  • Confirm "sideEffects": false in package.json.
  • Run bun scripts/publish.ts --package @suaveplan/audit-logging.
  • Update CLAUDE.md package listing to include @suaveplan/audit-logging.
  • 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.