first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
# memory-adapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides an in-memory implementation of `AuditStorageAdapter` backed by a `Map<string, AuditEvent>`. Designed for unit tests, integration tests, local development, and single-node scenarios where persistence across process restarts is not required. All filtering, sorting, and pagination are performed in-process without any external dependencies.
## Features
- Full `AuditStorageAdapter` implementation — save, query, getLatestEvent, deleteExpired, archive, count
- Rich query filtering: event types, userId, sessionId, severity (range-based), targetType, targetId, time range, tenantId
- Stable ASC/DESC ordering by timestamp with offset/limit pagination
- Separate archive store for events moved by `archive()`
- `snapshot()` and `archivedSnapshot()` helpers for test assertions
- `clear()` for test teardown
## Usage: Basic Example
```ts
import { MemoryAuditAdapter } from "@suaveplan/audit-logging/adapters/memory/memory-adapter";
import { AuditLogger } from "@suaveplan/audit-logging/core/audit-logger/audit-logger";
const adapter = new MemoryAuditAdapter();
const logger = new AuditLogger({
storage: adapter,
secretKey: "test-secret",
});
await logger.log({
type: "auth_login",
severity: "info",
actor: { userId: "user-1" },
hash: "",
});
const events = adapter.snapshot();
console.log(events.length); // 1
```
## Usage: Advanced Example
```ts
import { MemoryAuditAdapter } from "@suaveplan/audit-logging/adapters/memory/memory-adapter";
import { AuditEventType, AuditSeverity } from "@suaveplan/audit-logging";
const adapter = new MemoryAuditAdapter();
// Seed events
for (let i = 0; i < 20; i++) {
await adapter.save({
id: `evt-${i}`,
type: i % 2 === 0 ? AuditEventType.AUTH_LOGIN : AuditEventType.DATA_READ,
severity: AuditSeverity.INFO,
actor: { userId: `user-${i % 3}` },
timestamp: new Date(Date.now() - i * 60_000),
hash: "placeholder",
});
}
// Paginated query
const page1 = await adapter.query({ limit: 5, offset: 0, order: "desc" });
const page2 = await adapter.query({ limit: 5, offset: 5, order: "desc" });
// Count by user
const userCount = await adapter.count({ userId: "user-0" });
// Archive old events
const cutoff = new Date(Date.now() - 10 * 60_000);
const archived = await adapter.archive(cutoff, "s3://audit-archive/2024");
console.log(`Archived ${archived} events`);
// Delete remaining old events
const deleted = await adapter.deleteExpired(cutoff);
console.log(`Deleted ${deleted} expired events`);
```
## API Reference
### `MemoryAuditAdapter`
**`save(event: AuditEvent): Promise<void>`**
Stores the event by its `id`. Overwrites if `id` already exists.
**`query(options: AuditQueryOptions): Promise<AuditEvent[]>`**
Filters, sorts, and paginates the in-memory event set. All filter fields are ANDed together.
- `severity` accepts a single value or array; filters to events matching any of the provided severities
- `order` defaults to `"asc"`
- `offset` defaults to 0; `limit` defaults to unlimited
**`getLatestEvent(tenantId?: string): Promise<AuditEvent | null>`**
Returns the event with the most recent timestamp, optionally scoped to a tenant. Returns `null` if no events exist.
**`deleteExpired(before: Date): Promise<number>`**
Removes all events where `timestamp < before`. Returns the count deleted.
**`archive(before: Date, location: string): Promise<number>`**
Moves events where `timestamp < before` into the internal archive map (keyed by `location:id`). Returns the count archived.
**`count(options?: Partial<AuditQueryOptions>): Promise<number>`**
Returns the total count of events matching the filter, or total size if no options provided.
**`snapshot(): AuditEvent[]`**
Returns a copy of all currently stored (non-archived) events. For test assertions.
**`archivedSnapshot(): AuditEvent[]`**
Returns all archived events. For test assertions.
**`clear(): void`**
Empties both the main store and the archive. Use in `afterEach` test teardown.
## Implementation Notes
The severity filter applies range semantics: when multiple severities are provided it matches events whose severity order falls between the minimum and maximum provided ordinals. This is intentional — it allows callers to request `[WARNING, ERROR, CRITICAL]` without listing every intermediate level. Events are sorted using `Array.sort()` which is stable in V8 / Bun, so tie-breaking by insertion order is reliable. The internal `archived` map is keyed by `location:id` to allow the same event to be archived to multiple locations in tests without collision.

View File

@@ -0,0 +1,101 @@
# storage-adapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the `AuditStorageAdapter` interface contract that all storage backends must implement. This adapter pattern decouples the `AuditLogger` from any specific persistence technology — in-memory, SQLite, SurrealDB, PostgreSQL, or S3 — allowing implementations to be swapped without changing logger code.
## Features
- `AuditStorageAdapter` interface with six methods covering the full CRUD + lifecycle surface
- `BaseAuditStorageAdapter` abstract class with typed abstract method signatures for concrete adapters
- Supports pagination, time-range filtering, tenant scoping, and soft-archive semantics
## Usage: Basic Example
```ts
import type { AuditStorageAdapter } from "@suaveplan/audit-logging/adapters/storage";
class MyAdapter implements AuditStorageAdapter {
async save(event) { /* ... */ }
async query(options) { return []; }
async getLatestEvent(tenantId?) { return null; }
async deleteExpired(before) { return 0; }
async archive(before, location) { return 0; }
async count(options?) { return 0; }
}
```
## Usage: Advanced Example
```ts
import { BaseAuditStorageAdapter } from "@suaveplan/audit-logging/adapters/storage";
import type { AuditEvent, AuditQueryOptions } from "@suaveplan/audit-logging";
class SqliteAuditAdapter extends BaseAuditStorageAdapter {
constructor(private readonly db: DatabaseAdapter) {
super();
}
async save(event: AuditEvent): Promise<void> {
await this.db.query("INSERT INTO audit_events VALUES (?)", [JSON.stringify(event)]);
}
async query(options: AuditQueryOptions): Promise<AuditEvent[]> {
// Build WHERE clause from options...
return [];
}
async getLatestEvent(tenantId?: string): Promise<AuditEvent | null> {
// SELECT ... ORDER BY timestamp DESC LIMIT 1
return null;
}
async deleteExpired(before: Date): Promise<number> {
const result = await this.db.query("DELETE FROM audit_events WHERE timestamp < ?", [before]);
return result.rowsAffected ?? 0;
}
async archive(before: Date, location: string): Promise<number> {
// Copy to archive, then mark deleted
return 0;
}
async count(options?: Partial<AuditQueryOptions>): Promise<number> {
const result = await this.db.query("SELECT COUNT(*) FROM audit_events");
return Number(result.rows[0][0]);
}
}
```
## API Reference
### `AuditStorageAdapter` (interface)
**`save(event: AuditEvent): Promise<void>`**
Persist a single audit event. Throws `AuditError` with `STORAGE_ERROR` on failure.
**`query(options: AuditQueryOptions): Promise<AuditEvent[]>`**
Filter and paginate events. Applies all provided filters; returns an empty array when no events match.
**`getLatestEvent(tenantId?: string): Promise<AuditEvent | null>`**
Returns the most recent event (by timestamp) for the given tenant, or globally if no tenant is specified. Used by `AuditLogger` to link HMAC chains.
**`deleteExpired(before: Date): Promise<number>`**
Hard-deletes all events with `timestamp < before`. Returns the count of deleted records.
**`archive(before: Date, location: string): Promise<number>`**
Copies events with `timestamp < before` to `location`, then marks them. Returns the count of archived records.
**`count(options?: Partial<AuditQueryOptions>): Promise<number>`**
Returns the total number of events matching the optional filter criteria.
### `BaseAuditStorageAdapter` (abstract class)
Abstract base that implements `AuditStorageAdapter` with abstract method signatures. Extend this class when building new adapters to get compile-time enforcement that all required methods are implemented.
## Implementation Notes
The interface is intentionally minimal — it does not prescribe transaction semantics, connection pooling, or retry logic, which are concerns of specific adapter implementations. The `archive` method is a two-phase operation; the exact archival format (JSON Lines, Parquet, CSV) is left to the implementation. The `getLatestEvent` method is called once per `log()` invocation when chain integrity is enabled, so implementations should ensure it is fast (indexed by timestamp).

View File

@@ -0,0 +1,127 @@
# audit-logger
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `AuditLogger` class is the primary entry point for the `@suaveplan/audit-logging` package. It orchestrates event creation (ID generation, timestamping, HMAC signing), storage persistence, GDPR data export, and retention policy enforcement. All dependencies — storage adapter, clock, ID generator, logger, and event bus — are injected through the constructor, enabling full testability without mocks.
## Features
- `log()` — generates ID and timestamp, computes HMAC hash (with optional chain linking), saves event, emits OTel counter and bus event
- `query()` — delegates to the storage adapter with full filter support
- `verifyChainIntegrity()` — delegates to `verifyChain()` in chain-integrity module
- `exportGDPRData()` — collects all events for a user ID, builds summary statistics, respects AbortSignal
- `applyRetentionPolicy()` — archives then hard-deletes events beyond the retention window
- Injectable `Clock` and `IdGenerator` from `@suaveplan/types`
- OTel counter `audit.events_logged_total` with `event_type` and `severity` labels
- Bus event `audit:event:logged` emitted after each successful log
## Usage: Basic Example
```ts
import { AuditLogger } from "@suaveplan/audit-logging/core/audit-logger/audit-logger";
import { MemoryAuditAdapter } from "@suaveplan/audit-logging/adapters/memory/memory-adapter";
import { AuditEventType, AuditSeverity } from "@suaveplan/audit-logging";
const logger = new AuditLogger({
storage: new MemoryAuditAdapter(),
secretKey: process.env.AUDIT_SECRET_KEY!,
});
const event = await logger.log({
type: AuditEventType.AUTH_LOGIN,
severity: AuditSeverity.INFO,
actor: { userId: "user-123", ipAddress: "203.0.113.1" },
target: { type: "session", id: "sess-456" },
});
console.log(event.id); // generated UUID
console.log(event.hash); // hex HMAC-SHA256
```
## Usage: Advanced Example
```ts
import { AuditLogger } from "@suaveplan/audit-logging/core/audit-logger/audit-logger";
import { MemoryAuditAdapter } from "@suaveplan/audit-logging/adapters/memory/memory-adapter";
import { AuditEventType, AuditSeverity } from "@suaveplan/audit-logging";
const adapter = new MemoryAuditAdapter();
let seq = 0;
const logger = new AuditLogger({
storage: adapter,
secretKey: "my-secret",
enableChainIntegrity: true,
retentionPolicy: {
retentionDays: 90,
archiveBeforeDelete: true,
archiveLocation: "s3://my-bucket/audit",
},
clock: () => Date.now(),
idGenerator: () => `evt-${++seq}`,
});
// Log a chain of events
await logger.log({ type: AuditEventType.AUTH_LOGIN, severity: AuditSeverity.INFO, actor: { userId: "u1" } });
await logger.log({ type: AuditEventType.DATA_READ, severity: AuditSeverity.INFO, actor: { userId: "u1" }, target: { type: "report", id: "r-1" } });
// Verify chain
const events = await logger.query({ order: "asc" });
const result = await logger.verifyChainIntegrity(events);
console.log(result.valid); // true
// GDPR export
const gdprExport = await logger.exportGDPRData("u1");
console.log(gdprExport.summary.totalEvents); // 2
// Apply retention
const retention = await logger.applyRetentionPolicy();
console.log(retention.eventsDeleted, retention.eventsArchived);
```
## API Reference
### `new AuditLogger(options: AuditLoggerOptions)`
Throws `AuditError` with `SECRET_KEY_MISSING` if `options.secretKey` is empty.
### `log(event): Promise<AuditEvent>`
Input type: `Omit<AuditEvent, 'id' | 'timestamp' | 'hash' | 'previousHash'>`.
- Generates `id` via `idGenerator()`
- Sets `timestamp` via `new Date(clock())`
- If `enableChainIntegrity`: fetches latest event hash as `previousHash`
- Computes `hash = computeEventHash(event, secretKey, previousHash)`
- Saves to storage; throws `AuditError(STORAGE_ERROR)` on failure
- Increments OTel counter `audit.events_logged_total`
- Emits `audit:event:logged` on bus
### `query(options): Promise<AuditEvent[]>`
Delegates to `storage.query(options)`. Throws `AuditError(STORAGE_ERROR)` on storage failure.
### `verifyChainIntegrity(events): Promise<ChainVerificationResult>`
Events must be in ascending chronological order. Delegates to `verifyChain`.
### `exportGDPRData(userId, signal?): Promise<GDPRDataExport>`
- Queries all events with `{ userId }`
- Builds `eventTypes` frequency map and `dateRange`
- Returns `GDPRDataExport` with summary
- Throws `AuditError(EXPORT_FAILED)` on aborted signal or storage failure
### `applyRetentionPolicy(signal?): Promise<RetentionResult>`
- Requires `retentionPolicy` option to be set (returns empty result otherwise)
- Archives events older than `retentionDays` if `archiveBeforeDelete && archiveLocation`
- Hard-deletes expired events via `storage.deleteExpired(cutoff)`
- Non-fatal errors are collected in `result.errors`
## Implementation Notes
The counter and meter are created once at module scope to avoid per-instance SDK lookups and respect OTel's singleton meter provider model. The `bus.emit` call is fire-and-forget (no await) to keep the hot path synchronous. The `previousHash` lookup uses tenant scoping when `event.tenantId` is set, so multi-tenant deployments maintain separate chains per tenant. All optional properties on `AuditEvent` are included via spread with conditional key inclusion to satisfy `exactOptionalPropertyTypes`.

View File

@@ -0,0 +1,97 @@
# chain-integrity
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the cryptographic chain-integrity subsystem for `@suaveplan/audit-logging`. Each audit event is signed with an HMAC-SHA256 digest that covers both the event's canonical JSON form and the hash of the preceding event, forming a tamper-evident linked chain. Any post-hoc modification to an event — or insertion/deletion of events — will cause `verifyChain` to detect the broken link at the exact index.
## Features
- `canonicalise` — deterministic JSON serialisation with sorted keys for cross-runtime stability
- `computeEventHash` — HMAC-SHA256 over `previousHash:canonicalJSON` using `@suaveplan/crypto-hash`
- `verifyChain` — iterates an ordered event sequence, recomputes hashes, and returns pass/fail with broken-at index
- No `node:crypto` usage — all cryptography via `@suaveplan/crypto-hash`
## Usage: Basic Example
```ts
import { computeEventHash, verifyChain } from "@suaveplan/audit-logging/core/chain-integrity/chain-integrity";
const secretKey = "super-secret-key";
const eventPayload = {
id: "evt-1",
type: "auth_login",
severity: "info",
actor: { userId: "user-1" },
timestamp: new Date("2024-01-01T00:00:00Z"),
};
const hash = computeEventHash(eventPayload, secretKey);
const event = { ...eventPayload, hash };
const result = verifyChain([event], secretKey);
console.log(result.valid); // true
```
## Usage: Advanced Example
```ts
import { computeEventHash, verifyChain } from "@suaveplan/audit-logging/core/chain-integrity/chain-integrity";
const secretKey = "my-secret";
const events = [];
// Build chain
let previousHash: string | undefined;
for (const payload of rawPayloads) {
const hash = computeEventHash(payload, secretKey, previousHash);
const event = { ...payload, hash, ...(previousHash ? { previousHash } : {}) };
events.push(event);
previousHash = hash;
}
// Verify chain
const result = verifyChain(events, secretKey);
if (!result.valid) {
console.error(`Chain broken at index ${result.brokenAt}: ${result.reason}`);
}
// Tamper detection example
const tampered = [...events];
tampered[2] = { ...tampered[2], actor: { userId: "attacker" } };
const tamperResult = verifyChain(tampered, secretKey);
console.log(tamperResult.brokenAt); // 2
```
## API Reference
### `canonicalise(event): string`
Serialises an audit event to deterministic JSON with sorted keys, excluding `hash` and `previousHash`.
- `event` — event minus hash fields (all other fields serialised)
- Returns: canonical JSON string
### `computeEventHash(event, secretKey, previousHash?): string`
Computes HMAC-SHA256 over the canonical representation.
- `event` — event without hash/previousHash
- `secretKey` — UTF-8 signing key; must be non-empty
- `previousHash?` — hex hash of preceding event; prepended as `${previousHash}:` before the canonical JSON
- Returns: lowercase hex string (64 characters for SHA-256)
### `verifyChain(events, secretKey): ChainVerificationResult`
Verifies an ordered sequence of events.
- `events` — events in ascending chronological order
- `secretKey` — the secret used when originally signing
- Returns: `{ valid: true }` or `{ valid: false, brokenAt: number, reason: string }`
## Implementation Notes
Key sort in `canonicalise` is recursive via `sortReplacer` to handle nested objects like `target.metadata`. The actor object is sorted separately before the main sort pass to avoid double-sorting overhead. HMAC uses SHA-256 (not SHA-512) for a good balance of security and performance given the expected volume of audit events. The chain model does not enforce sequential ordering by `previousHash` linkage — it trusts the caller to provide events in order and simply recomputes each event's hash to detect any tampering. This design keeps verification O(n) without requiring a full DAG traversal.

View File

@@ -0,0 +1,91 @@
# audit-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides a stable error-code registry and typed error class for `@suaveplan/audit-logging`. All package-level failures are represented as `AuditError` instances carrying a stable `AUDIT.*` code, enabling programmatic error handling and downstream telemetry without relying on message string parsing.
## Features
- `AUDIT_CODES` — frozen registry of six stable error codes via `defineCodes`
- `AuditErrorCode` — union type of all valid AUDIT code strings
- `AuditError` — extends `SuaveplanError<AuditErrorCode>` for full cause-chain support, OTel span events, and redaction
## Usage: Basic Example
```ts
import { AuditError, AUDIT_CODES } from "@suaveplan/audit-logging/errors";
throw new AuditError(
AUDIT_CODES.STORAGE_ERROR,
"audit.storage.save.failed",
{ context: { table: "audit_events", operation: "insert" } }
);
```
## Usage: Advanced Example
```ts
import { AuditError, AUDIT_CODES } from "@suaveplan/audit-logging/errors";
import { walkCauses } from "@suaveplan/error";
try {
await adapter.save(event);
} catch (err) {
throw new AuditError(
AUDIT_CODES.STORAGE_ERROR,
"audit.storage.save.failed",
{
...(err instanceof Error ? { cause: err } : {}),
context: { eventId: event.id, eventType: event.type },
}
);
}
// Handle downstream:
try {
await logger.log(auditEvent);
} catch (err) {
if (err instanceof AuditError && err.code === AUDIT_CODES.CHAIN_BROKEN) {
console.error("Tamper detected:", err.context);
}
for (const cause of walkCauses(err)) {
console.error(" caused by:", cause.message);
}
}
```
## API Reference
### `AUDIT_CODES`
Frozen object with the following keys:
- `STORAGE_ERROR` → `"AUDIT.STORAGE_ERROR"` — storage backend operation failed
- `CHAIN_BROKEN` → `"AUDIT.CHAIN_BROKEN"` — HMAC chain verification failed
- `INVALID_EVENT` → `"AUDIT.INVALID_EVENT"` — event data failed schema validation
- `EXPORT_FAILED` → `"AUDIT.EXPORT_FAILED"` — GDPR data export failed
- `RETENTION_FAILED` → `"AUDIT.RETENTION_FAILED"` — retention policy application failed
- `SECRET_KEY_MISSING` → `"AUDIT.SECRET_KEY_MISSING"` — no secret key provided for HMAC
### `AuditErrorCode`
Type alias: `"AUDIT.STORAGE_ERROR" | "AUDIT.CHAIN_BROKEN" | "AUDIT.INVALID_EVENT" | "AUDIT.EXPORT_FAILED" | "AUDIT.RETENTION_FAILED" | "AUDIT.SECRET_KEY_MISSING"`
### `AuditError`
`class AuditError extends SuaveplanError<AuditErrorCode>`
**Constructor:**
- `code: AuditErrorCode` — stable error code from `AUDIT_CODES`
- `messageKey: string` — i18n message key (resolved by the global message resolver)
- `options?: SuaveplanErrorOptions` — optional `cause`, `context`, `clock`, `traceId`
**Inherited properties:** `code`, `context`, `timestamp`, `traceId`, `cause` (from `SuaveplanError`)
## Implementation Notes
`defineCodes` is called once at module load time with a `const` array, so TypeScript infers the exact literal union for `AuditErrorCode`. Using `as const` on the reasons array is essential — without it the inference falls back to `string[]` and the code type widens to `string`. The `AuditError` constructor delegates entirely to `SuaveplanError`; its only addition is setting `this.name = "AuditError"` for cleaner stack traces and `instanceof` checks.

View File

@@ -0,0 +1,110 @@
# audit-logging.schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides Zod v4 schema definitions for all public types in `@suaveplan/audit-logging`. These schemas enable runtime validation at API boundaries, wire deserialization, and property-based testing. Every schema mirrors its corresponding TypeScript interface exactly, ensuring type inference from `z.infer<>` produces the same shape as the hand-written types in `audit-logging.types.ts`.
## Features
- `AuditSeveritySchema` — enum validator for the five severity levels
- `AuditEventTypeSchema` — enum validator for all 29 built-in event types
- `AuditActorSchema` — optional-field object schema for actors
- `AuditTargetSchema` — required type/id with optional metadata record
- `AuditEventSchema` — complete event record with all fields
- `AuditQueryOptionsSchema` — query filter schema with union severity support
- `AuditRetentionPolicySchema` — policy with positive retentionDays validation
- `ChainVerificationResultSchema` — pass/fail result with optional broken-at index
- `RetentionResultSchema` — counts and errors array
## Usage: Basic Example
```ts
import { AuditEventSchema } from "@suaveplan/audit-logging/schemas/audit-logging.schemas";
const raw = JSON.parse(apiResponse);
const event = AuditEventSchema.parse({
...raw,
timestamp: new Date(raw.timestamp),
});
```
## Usage: Advanced Example
```ts
import {
AuditQueryOptionsSchema,
AuditRetentionPolicySchema,
} from "@suaveplan/audit-logging/schemas/audit-logging.schemas";
import { AuditLogger } from "@suaveplan/audit-logging/core/audit-logger/audit-logger";
import { MemoryAuditAdapter } from "@suaveplan/audit-logging/adapters/memory/memory-adapter";
// Validate query options from user input
function parseQueryOptions(input: unknown) {
return AuditQueryOptionsSchema.parse(input);
}
// Validate policy from config file
function parsePolicy(input: unknown) {
const policy = AuditRetentionPolicySchema.parse(input);
return new AuditLogger({
storage: new MemoryAuditAdapter(),
secretKey: "key",
retentionPolicy: policy,
});
}
```
## API Reference
### `AuditSeveritySchema`
`z.enum(["debug", "info", "warning", "error", "critical"])`
### `AuditEventTypeSchema`
`z.enum([...all 29 values...])`
### `AuditActorSchema`
All fields optional: `userId`, `sessionId`, `apiKeyId`, `ipAddress`, `userAgent`, `tenantId`.
### `AuditTargetSchema`
- `type: string` (required)
- `id: string` (required)
- `metadata?: Record<string, unknown>`
### `AuditEventSchema`
- All AuditEvent fields with proper Zod types
- `timestamp: z.date()` — expects a `Date` object (not a string)
- `type: z.union([AuditEventTypeSchema, z.string()])` — accepts custom types
### `AuditQueryOptionsSchema`
All fields optional. `severity` accepts a single value or array.
### `AuditRetentionPolicySchema`
- `retentionDays: z.number().int().positive()` — must be ≥ 1
- `archiveBeforeDelete: z.boolean()`
- `archiveLocation?: z.string()`
### `ChainVerificationResultSchema`
- `valid: z.boolean()`
- `brokenAt?: z.number().int().nonnegative()`
- `reason?: z.string()`
### `RetentionResultSchema`
- `eventsDeleted: z.number().int().nonnegative()`
- `eventsArchived: z.number().int().nonnegative()`
- `errors: z.array(z.string())`
## Implementation Notes
Schemas use `z.record(z.string(), z.unknown())` (two-argument form) per Zod v4 requirements. The `AuditEventTypeSchema` enumerates all values from the `AuditEventType` enum directly rather than using `z.nativeEnum()`, which is deprecated in Zod v4 in favour of `z.enum()`. The `timestamp` field expects an actual `Date` object — callers parsing JSON should convert the ISO string first with `new Date(raw.timestamp)`.

View File

@@ -0,0 +1,148 @@
# audit-logging.types
## Purpose
Defines the complete TypeScript type system for `@suaveplan/audit-logging`. These types form the public contract for all audit event data, query filtering, storage adapters, GDPR exports, and chain-integrity verification. Keeping them in a dedicated types file ensures zero runtime overhead — types are erased during compilation, and consumers can import only what they need.
## Features
- `AuditActor` — who triggered an event (user, session, API key, IP, tenant)
- `AuditTarget` — the resource acted upon (type, id, optional metadata)
- `AuditEvent` — complete immutable event record including HMAC chain fields
- `AuditQueryOptions` — rich filtering: event types, user/session, severity, time range, pagination
- `AuditRetentionPolicy` — configurable TTL with archive-before-delete support
- `AuditLoggerOptions` — injectable clock, ID generator, logger, event bus, and storage adapter
- `AuditStorageAdapter` — pluggable backend interface (save, query, count, archive, deleteExpired)
- `GDPRDataExport` — structured export with event list and summary statistics
- `ChainVerificationResult` — pass/fail with broken-at index and reason string
- `RetentionResult` — counts of deleted/archived events plus any errors
## Usage: Basic Example
```ts
import type { AuditEvent, AuditActor, AuditTarget } from "@suaveplan/audit-logging";
import { AuditEventType, AuditSeverity } from "@suaveplan/audit-logging";
const actor: AuditActor = {
userId: "user-123",
ipAddress: "203.0.113.1",
};
const target: AuditTarget = {
type: "document",
id: "doc-456",
};
const event: AuditEvent = {
id: "evt-789",
type: AuditEventType.DATA_READ,
severity: AuditSeverity.INFO,
actor,
target,
timestamp: new Date(),
hash: "computed-hmac",
};
```
## Usage: Advanced Example
```ts
import type { AuditQueryOptions, AuditRetentionPolicy, AuditLoggerOptions } from "@suaveplan/audit-logging";
import { AuditEventType, AuditSeverity } from "@suaveplan/audit-logging";
import { MemoryAuditAdapter } from "@suaveplan/audit-logging/adapters/memory/memory-adapter";
const query: AuditQueryOptions = {
eventTypes: [AuditEventType.AUTH_LOGIN, AuditEventType.AUTH_LOGOUT],
severity: [AuditSeverity.WARNING, AuditSeverity.ERROR, AuditSeverity.CRITICAL],
startTime: new Date("2024-01-01"),
endTime: new Date("2024-12-31"),
tenantId: "tenant-abc",
limit: 100,
offset: 0,
order: "desc",
};
const retention: AuditRetentionPolicy = {
retentionDays: 90,
archiveBeforeDelete: true,
archiveLocation: "s3://my-bucket/audit-archive",
};
const options: AuditLoggerOptions = {
storage: new MemoryAuditAdapter(),
secretKey: "my-secret-key",
enableChainIntegrity: true,
retentionPolicy: retention,
clock: () => Date.now(),
idGenerator: () => crypto.randomUUID(),
};
```
## API Reference
### `AuditActor`
All fields optional. Represents the entity that performed the action.
- `userId?: string` — authenticated user identifier
- `sessionId?: string` — active session identifier
- `apiKeyId?: string` — API key used for the request
- `ipAddress?: string` — source IP address
- `userAgent?: string` — HTTP User-Agent header
- `tenantId?: string` — tenant in multi-tenant deployments
### `AuditTarget`
- `type: string` — resource type (e.g. `"document"`, `"user"`)
- `id: string` — resource identifier
- `metadata?: Record<string, unknown>` — additional context
### `AuditEvent`
- `id: string` — unique event identifier
- `type: AuditEventType | string` — event type from the taxonomy or custom string
- `severity: AuditSeverity` — debug/info/warning/error/critical
- `actor: AuditActor` — who triggered the event
- `target?: AuditTarget` — optional target resource
- `details?: Record<string, unknown>` — arbitrary event-specific data
- `timestamp: Date` — when the event occurred
- `previousHash?: string` — HMAC of the preceding event (chain integrity)
- `hash: string` — HMAC-SHA256 of this event's canonical form
- `tenantId?: string` — tenant scoping
- `correlationId?: string` — request/trace correlation
### `AuditQueryOptions`
Filtering fields for `AuditStorageAdapter.query()`. All optional.
### `AuditStorageAdapter`
- `save(event)` — persist a single event
- `query(options)` — filter and paginate events
- `getLatestEvent(tenantId?)` — most recent event for chain linking
- `deleteExpired(before)` — remove old events; returns count deleted
- `archive(before, location)` — copy then mark events; returns count archived
- `count(options?)` — count matching events
### `GDPRDataExport`
- `userId: string` — the data subject
- `exportedAt: Date` — export timestamp
- `auditEvents: AuditEvent[]` — all events involving the user
- `summary` — `totalEvents`, `eventTypes` map, `dateRange`
### `ChainVerificationResult`
- `valid: boolean` — whether chain is intact
- `brokenAt?: number` — zero-based index of first broken link
- `reason?: string` — human-readable explanation
### `RetentionResult`
- `eventsDeleted: number` — count of hard-deleted events
- `eventsArchived: number` — count of archived events
- `errors: string[]` — non-fatal errors encountered
## Implementation Notes
Types are defined as interfaces rather than classes to allow multiple implementations of `AuditStorageAdapter` without prototype overhead. The `exactOptionalPropertyTypes` constraint means all optional fields use `?:` and callers must use spread syntax to conditionally include them. The `AuditEvent.hash` field is always a hex-encoded HMAC-SHA256 string, computed by `computeEventHash` in `chain-integrity.ts`.