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,31 @@
# @suaveplan/events-schema - Runtime schema-validation companion for @suaveplan/events: validates event payloads at dispatch boundaries via Zod schemas with strict/warn/sample modes and sensitive field redaction.
## Summary
Runtime schema-validation companion for `@suaveplan/events`. Validates event payloads at dispatch boundaries via Zod schemas with strict/warn/sample modes and sensitive field redaction.
<!-- begin:auto-docs -->
## Documentation tree
- [attach-event-schemas](./src/core/attach-event-schemas/attach-event-schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [attach-event-schemas](./src/core/attach-event-schemas/attach-event-schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [attach-event-schemas](./src/core/attach-event-schemas/attach-event-schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [attach-event-schemas](./src/core/attach-event-schemas/attach-event-schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [attach-event-schemas](./src/core/attach-event-schemas/attach-event-schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [attach-event-schemas](./src/core/attach-event-schemas/attach-event-schemas.md) — <!-- begin:auto-docs --> _(no submodules)_
## Quick Start
```ts
import { EventBus } from "@suaveplan/events";
import { attachEventSchemas } from "@suaveplan/events-schema";
import z from "zod";
const bus = new EventBus();
const handle = attachEventSchemas(bus, { mode: "strict" });
handle.registerSchema("user.created", z.object({ id: z.string() }));
await bus.emitAsync("user.created", { id: "u-1" }); // passes
await bus.emitAsync("user.created", { id: 123 }); // throws EventsSchemaError
handle.detach();
```
## Modules
- [`src/core/attach-event-schemas/attach-event-schemas`](src/core/attach-event-schemas/attach-event-schemas.md) — `attachEventSchemas`, `registerEventSchema`, `EventSchemasHandle`

View File

@@ -0,0 +1,97 @@
# attach-event-schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Attaches runtime schema-validation instrumentation to an `EventBus` via the public interceptor API from `@suaveplan/events`. On each dispatch, it validates the event payload against a Zod schema registered for that event name. Three validation modes provide flexibility across different deployment stages: `"strict"` rejects the dispatch with a structured error (suitable for CI and development), `"warn"` logs the rejection but lets the event through (suitable for canary rollout), and `"sample"` validates a configurable fraction of events (suitable for high-throughput production paths).
Validation statistics are tracked in-process and available without an OTel exporter via `EventSchemasHandle.stats()`. OTel counters and histograms are also emitted for dashboard visibility.
## Features
- `attachEventSchemas(bus, options?)` — installs a schema-validation interceptor and returns `EventSchemasHandle`
- Three validation modes: `strict`, `warn`, `sample`
- `EventSchemasHandle.registerSchema(eventName, schema, version?)` — registers/updates a Zod schema
- `EventSchemasHandle.stats()` — returns in-process validation statistics
- Sensitive field redaction in rejection output (opt-in via `redactSensitive: true`)
- OTel counters: `events_schema.validations_total`, `events_schema.validations_failed_total`, `events_schema.unknown_schema_total`
- OTel histogram: `events_schema.validation_duration_ms`
- `registerEventSchema(registry, eventName, schema, options?)` — standalone registration helper
- Injectable `registry`, `clock`, `rng`, `logger` for testability
## Usage: Basic Example
```ts
import { EventBus } from "@suaveplan/events";
import { attachEventSchemas } from "@suaveplan/events-schema";
import z from "zod";
const bus = new EventBus<{ "user.created": { id: string } }>();
const handle = attachEventSchemas(bus);
handle.registerSchema("user.created", z.object({ id: z.string() }));
bus.on("user.created", (p) => console.log("created", p.id));
await bus.emitAsync("user.created", { id: "u-1" }); // passes
handle.detach();
```
## Usage: Advanced Example
```ts
import { EventBus } from "@suaveplan/events";
import { attachEventSchemas, registerEventSchema } from "@suaveplan/events-schema";
import { SchemaRegistry } from "@suaveplan/schema-registry";
import z from "zod";
const registry = new SchemaRegistry();
const bus = new EventBus();
const handle = attachEventSchemas(bus, {
registry,
mode: "warn",
onRejection: (r) => console.warn("REJECTED", r.eventName, r.errors),
redactSensitive: true,
});
handle.registerSchema("order.placed", z.object({
orderId: z.string(),
amount: z.number().positive(),
}));
await bus.emitAsync("order.placed", { orderId: "o-1", amount: -5 });
// Logs a warning but does NOT throw (warn mode)
console.log(handle.stats().rejectionsTotal["order.placed"]); // 1
handle.detach();
```
## API Reference
### `attachEventSchemas<TMap>(bus, options?): EventSchemasHandle`
| Param | Type | Description |
|---|---|---|
| `bus` | `EventBus<TMap>` | The event bus to instrument |
| `options` | `EventSchemasOptions?` | Registry, mode, sampleRate, onRejection, redact, clock, rng, logger |
**Returns**: `EventSchemasHandle`
### `registerEventSchema(registry, eventName, schema, options?): void`
Standalone helper for registering schemas in a `SchemaRegistry` before attaching.
### `EventSchemasHandle`
| Method | Returns | Description |
|---|---|---|
| `detach()` | `void` | Remove the interceptor |
| `registerSchema(eventName, schema, version?)` | `void` | Register a Zod schema |
| `stats()` | `SchemaValidationStats` | In-process statistics |
## Implementation Notes
Schemas registered via `handle.registerSchema` are stored in an in-memory `Map` in addition to the registry. This avoids the registry's compatibility enforcement on the first registration (which would fail with `SchemaRegistryError` for new subjects). Subsequent registrations via `registerSchema` go through the registry's compatibility check. The `sample` mode evaluates the random gate before schema lookup to avoid the lookup cost for unsampled dispatches. The `rng.next()` call uses the injectable `Rng` interface from `@suaveplan/types` for deterministic testing.