11 KiB
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/withsrc/,tests/,src/__benchmarks__/, and all feature subdirectories per the layout inproposal.md. - Write
package.jsonat version0.1.0with:"type": "module","sideEffects": falseexportsmap 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:builddevDependencies:vitest catalog:testing,@vitest/coverage-istanbul catalog:testing,fast-check catalog:testing,mitata catalog:testing,@suaveplan/testing workspace:^
- Write
tsconfig.jsonextending@suaveplan/typescript-config/library.json. - Write
vitest.config.tsusingcreateTestConfig()from@suaveplan/testing/config(pool:forks). - Add
lint,typecheck,test,test:coverage,build,docs:check,benchscripts topackage.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
AuditEventTypeandAuditSeverityenums (move toconstants/index.tsand 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
AuditErrorCodeunion — six codes:AUDIT_STORAGE_ERROR,AUDIT_CHAIN_BROKEN,AUDIT_INVALID_EVENT,AUDIT_EXPORT_FAILED,AUDIT_RETENTION_FAILED,AUDIT_SECRET_KEY_MISSING. - Define
AUDIT_ERROR_METADATAwithhttpStatus,severity,retryable,userMessageKey,category,descriptionper 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
AuditStorageAdapterinterface: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
MemoryAuditAdapterusing an in-memory array ofAuditEvent:save(event)— appends to array.query(options)— applies all filters fromAuditQueryOptions, sorts bytimestampperorder, applieslimit+offset.getLatestEvent(tenantId?)— returns the most recent event for tenantId (or globally).deleteExpired(before)— removes events withtimestamp < 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. Eachithas{ 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'shmac. - Return hex-encoded digest.
- Use
- Implement
verifyChain(events: AuditEvent[], secretKey: string): Promise<ChainVerificationResult>:- For each event at index
i, recompute expected hash; compare withevent.hashusing 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.
- For each event at index
- Write
chain-integrity.test.ts— valid chain, tampered event hash, tampered previousHash, single-event chain, empty chain. Eachithas{ timeout: 5000 }. - Write
chain-integrity.md.
6. AuditLogger (src/core/audit-logger/)
- Implement
AuditLogger(options):- Validate
optionswithAuditLoggerOptionsSchema; throwAuditErrorwithAUDIT_SECRET_KEY_MISSINGifsecretKeyis empty. log(partialEvent):- Assign
idviaidGenerator.generate()(default:crypto.randomUUID()). - Assign
timestampviaclock.now(). - If
enableChainIntegrity, fetch previous event viastorage.getLatestEvent(tenantId)and setpreviousHash. - Compute
hashviacomputeEventHash. - Save via
storage.save(event). - Emit
audit:event:loggedonbus. - Instrument with
audit.logspan andaudit.events_logged_totalcounter. - Return the complete
AuditEvent.
- Assign
query(options)— delegates tostorage.query; instruments withaudit.queryspan.verifyChainIntegrity(events)— delegates toverifyChain; emitsaudit:chain:brokenon bus if invalid; instruments withaudit.chain_verifyspan.
- Validate
- 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
userIdviastorage.query({ userId })in pages; respectsignalbetween pages. - Build
GDPRDataExportwithsummary.totalEvents,summary.eventTypeshistogram,summary.dateRange. - Instrument with
audit.gdpr_exportspan and counter. - On abort, throw
AuditErrorwithAUDIT_EXPORT_FAILED(cause:AbortError).
- Query all events for
- Write
gdpr-export.test.ts— export with multiple event types, FakeClock, AbortSignal mid-export, empty user history. Eachithas{ 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, callstorage.archive(cutoff, policy.archiveLocation)first. - Call
storage.deleteExpired(cutoff). - Emit
audit:retention:appliedon bus. - Instrument with
audit.retention_applyspan andaudit.events_deleted_total/audit.events_archived_totalcounters. - On abort, throw
AuditErrorwithAUDIT_RETENTION_FAILED. - Returns
RetentionResult.
- Compute cutoff =
- Write
retention-policy.test.ts— archive+delete, delete-only, FakeClock for cutoff computation, AbortSignal cancellation, zero events case. Eachithas{ timeout: 5000 }. - Write
retention-policy.md.
9. OTel instrumentation (cross-cutting review)
- Confirm all spans and counters from
proposal.md §5are wired in each module. - Add
tests/otel.test.ts— in-memory exporter; assertaudit.events_logged_totalincrements,audit.query_duration_msrecords,audit.chain_verify_totalrecords 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
verifyChainto returnbrokenAtat the mutated index. - Query filter correctness: all events returned by
query({ userId: X })haveactor.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.mdunder## Benchmarks.
12. Docs
Per-file .md (all ≥200 words):
src/types/audit-logging.types.mdsrc/adapters/storage-adapter/storage-adapter.mdsrc/adapters/memory-adapter/memory-adapter.mdsrc/core/audit-logger/audit-logger.mdsrc/core/chain-integrity/chain-integrity.mdsrc/core/gdpr/gdpr-export.mdsrc/core/retention/retention-policy.mdsrc/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, untypedany,ts-ignore, or.skipinsrc/ortests/. - Confirm
"sideEffects": falseinpackage.json. - Run
bun scripts/publish.ts --package @suaveplan/audit-logging. - Update
CLAUDE.mdpackage listing to include@suaveplan/audit-logging. - Archive: move
openspec/changes/enterprise-audit-logging/toopenspec/archive/2026-04-24-enterprise-audit-logging/and fold spec delta intoopenspec/specs/audit-logging/spec.md.