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,32 @@
# @suaveplan/network-plugin-middleware - Priority-ordered middleware chain for the Suaveplan mesh: manager + logging, validation, transformation, metrics, throttling, security, i18n
## Summary
Priority-ordered middleware chain for the Suaveplan mesh. Inbound and outbound message flow each pass through the same five-band pipeline (CRITICAL → HIGH → NORMAL → LOW → BACKGROUND) with runtime add/remove/enable/disable. Ships seven concrete middlewares — `LoggingMiddleware`, `ValidationMiddleware` (Zod, strict / lenient), `TransformationMiddleware`, `MetricsMiddleware` (size + processing time), `ThrottlingMiddleware` (`@suaveplan/rate-limiter` token-bucket; global / per-peer / per-type), `SecurityMiddleware` (injectable sign / verify hooks per tier policy), and `I18nMiddleware` (locale headers, RTL marker, UTC timestamp normalisation) — wrapped in a single `MiddlewarePlugin` that satisfies the `NetworkPlugin` lifecycle.
<!-- begin:auto-docs -->
## Documentation tree
- [i18n](./src/i18n/i18n-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [i18n-middleware](./src/i18n/i18n-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [logging](./src/logging/logging-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [logging-middleware](./src/logging/logging-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [manager](./src/manager/middleware-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [middleware-manager](./src/manager/middleware-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [metrics](./src/metrics/metrics-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [metrics-middleware](./src/metrics/metrics-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [plugin](./src/plugin/middleware-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [middleware-plugin](./src/plugin/middleware-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [security](./src/security/security-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [security-middleware](./src/security/security-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [throttling](./src/throttling/throttling-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [throttling-middleware](./src/throttling/throttling-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [transformation](./src/transformation/transformation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [transformation-middleware](./src/transformation/transformation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [validation](./src/validation/validation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [validation-middleware](./src/validation/validation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the middleware chain. Each code maps to one failure class so consumers ca…
<!-- end:auto-docs --> - [i18n-middleware](./src/i18n/i18n-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [logging](./src/logging/logging-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [logging-middleware](./src/logging/logging-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [manager](./src/manager/middleware-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [middleware-manager](./src/manager/middleware-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/metrics/metrics-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics-middleware](./src/metrics/metrics-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/middleware-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [middleware-plugin](./src/plugin/middleware-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [security](./src/security/security-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [security-middleware](./src/security/security-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [throttling](./src/throttling/throttling-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [throttling-middleware](./src/throttling/throttling-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [transformation](./src/transformation/transformation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [transformation-middleware](./src/transformation/transformation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation-middleware](./src/validation/validation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the middleware chain. Each code maps to one failure class so consumers ca…
- [i18n-middleware](./src/i18n/i18n-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [logging](./src/logging/logging-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [logging-middleware](./src/logging/logging-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [manager](./src/manager/middleware-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [middleware-manager](./src/manager/middleware-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/metrics/metrics-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics-middleware](./src/metrics/metrics-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/middleware-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [middleware-plugin](./src/plugin/middleware-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [security](./src/security/security-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [security-middleware](./src/security/security-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [throttling](./src/throttling/throttling-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [throttling-middleware](./src/throttling/throttling-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [transformation](./src/transformation/transformation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [transformation-middleware](./src/transformation/transformation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation-middleware](./src/validation/validation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the middleware chain. Each code maps to one failure class so consumers ca…
- [i18n-middleware](./src/i18n/i18n-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [logging](./src/logging/logging-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [logging-middleware](./src/logging/logging-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [manager](./src/manager/middleware-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [middleware-manager](./src/manager/middleware-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/metrics/metrics-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics-middleware](./src/metrics/metrics-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/middleware-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [middleware-plugin](./src/plugin/middleware-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [security](./src/security/security-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [security-middleware](./src/security/security-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [throttling](./src/throttling/throttling-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [throttling-middleware](./src/throttling/throttling-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [transformation](./src/transformation/transformation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [transformation-middleware](./src/transformation/transformation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation-middleware](./src/validation/validation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the middleware chain. Each code maps to one failure class so consumers ca…
- [i18n-middleware](./src/i18n/i18n-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [logging](./src/logging/logging-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ - [logging-middleware](./src/logging/logging-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [manager](./src/manager/middleware-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ - [middleware-manager](./src/manager/middleware-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/metrics/metrics-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ - [metrics-middleware](./src/metrics/metrics-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/middleware-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ - [middleware-plugin](./src/plugin/middleware-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [security](./src/security/security-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ - [security-middleware](./src/security/security-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [throttling](./src/throttling/throttling-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ - [throttling-middleware](./src/throttling/throttling-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [transformation](./src/transformation/transformation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ - [transformation-middleware](./src/transformation/transformation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_ - [validation-middleware](./src/validation/validation-middleware.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of every error code raised by the middleware chain. Each code maps to one failure class so consumers ca…

View File

@@ -0,0 +1,56 @@
# Error codes — `@suaveplan/network-plugin-middleware`
## Purpose
Frozen registry of every error code raised by the middleware chain. Each code maps to one failure class so consumers can branch on `error.code` without parsing message text. Codes are namespaced under `NET_MW` and registered into the global error registry on first import via the side-effect import in `src/index.ts`.
## Features
- Six codes covering registration, validation, throttling, security, and generic chain errors.
- `NetworkMwError` extends `SuaveplanError<NetworkMwErrorCode>` so cause-chain traversal, redaction, and OTel adapters work without per-package wiring.
- Typed union `NetworkMwErrorCode` exhaustively covers the closed registry so callers using `switch` get exhaustiveness checking.
- Frozen registry — runtime mutation throws.
## Usage: Basic Example
```ts
import {
NETWORK_MW_CODES,
NetworkMwError,
} from "@suaveplan/network-plugin-middleware";
throw new NetworkMwError(
NETWORK_MW_CODES.VALIDATION_FAILED,
"validation.bad-payload",
{ context: { messageType: "registry:add", reason: "missing peerId" } }
);
```
## Usage: Advanced Example
```ts
try {
await throttler.apply(envelope, context);
} catch (err) {
if (
err instanceof NetworkMwError &&
err.code === NETWORK_MW_CODES.THROTTLED
) {
return { dropped: true, reason: "rate-limit", retryAfter: 1_000 };
}
throw err;
}
```
## API Reference
- `NETWORK_MW_CODES` — frozen object of `KEY -> "NET_MW.KEY"`. Throws on mutation attempts.
- `NetworkMwErrorCode` — TypeScript union of every code value.
- `NetworkMwError` — `SuaveplanError<NetworkMwErrorCode>` subclass; `name === "NetworkMwError"`.
## Implementation Notes
- The registry is constructed via `defineCodes("NET_MW", […] as const)` so the type system enumerates the value union. Adding a code requires bumping the `as const` tuple — TypeScript will then propagate the new key everywhere `NetworkMwErrorCode` is referenced.
- Codes are deliberately coarse (one per failure class) rather than per-call-site so consumers can build stable retry / observability policies without churn when implementations change.
- `MIDDLEWARE_ERROR` is the catch-all the manager raises when a middleware throws under the `error-policy: "stop"` strategy. Specific middlewares (validation, throttling, security) prefer their own dedicated codes for richer downstream logic.
- The side-effect import in `src/index.ts` registers the codes even when consumers tree-shake the named exports.

View File

@@ -0,0 +1,59 @@
# `I18nMiddleware`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Normalises locale-related metadata across the chain. Injects an `x-locale` header, sets `x-direction: rtl` for right-to-left scripts, and (when configured with a clock) rewrites the envelope's timestamp to a single reference frame so peers in different timezones do not produce comparison-resistant timestamps.
## Features
- BCP-47 locale tag carried in the `x-locale` header so receivers can switch translations / formatters without inspecting the payload.
- RTL detection via configurable locale-prefix list (defaults to `ar`, `he`, `fa`, `ur` — covers the bulk of right-to-left scripts).
- Optional UTC normalisation: callers pass a `clock` (typically `Date.now`) and the middleware rewrites `envelope.timestamp` so cross-host comparisons stay reliable.
- Lives in the NORMAL band by default (`priority: 280`) so locale headers are present before transformation / metrics observe them.
## Usage: Basic Example
```ts
import {
I18nMiddleware,
MiddlewareManager,
} from "@suaveplan/network-plugin-middleware";
const manager = new MiddlewareManager();
manager.register(new I18nMiddleware({ locale: "ar-EG" }));
const result = await manager.dispatch(envelope, "outgoing");
result.envelope?.headers["x-locale"]; // "ar-EG"
result.envelope?.headers["x-direction"]; // "rtl"
```
## Usage: Advanced Example
```ts
import { I18nMiddleware } from "@suaveplan/network-plugin-middleware";
let now = 1_700_000_000_000;
const i18n = new I18nMiddleware({
id: "i18n-utc",
priority: 290,
locale: "en-US",
rtlLocales: ["ar", "he", "fa", "ur", "yi"],
clock: () => now,
});
```
## API Reference
- `class I18nMiddleware implements Middleware`
- `constructor(config?: I18nMiddlewareConfig)`.
- `apply(envelope, context): MessageEnvelope` — never drops.
- `interface I18nMiddlewareConfig` — `{ id?, priority?, direction?, locale?, rtlLocales?, clock? }`.
## Implementation Notes
- RTL detection compares the lower-cased locale tag against each prefix. A locale matches the prefix exactly (`"ar"`) or hyphen-bounded (`"ar-EG"`); `"argonaut"` does NOT match the `ar` prefix because the comparator only allows the empty suffix or a `-` separator.
- The middleware always returns a new envelope object; even when it leaves the headers untouched, callers can rely on identity-equality being a useful "did i18n run?" signal.
- The clock rewrite is opt-in: applications that already enforce monotonic timestamps elsewhere (e.g. via `network-plugin-protocol`) should leave it unset to avoid double-normalisation.
- `x-locale` is the conventional WebSocket extension header, deliberately reused so subscribers (logger, metrics) do not need to learn a new key per protocol surface.

View File

@@ -0,0 +1,77 @@
# `LoggingMiddleware`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Records every envelope that traverses the chain as a structured log entry. Pluggable formatter, optional payload preview with size cap, and optional bus publish (`middleware:logged`) so a single registered logger feeds whatever observability stack the deployment runs (console, JSON sink, OTel adapter).
## Features
- Configurable id and priority (default `"logger"`, `300` so it lives in the LOW band — observability runs after the substantive middleware).
- `includeBody` toggle: off by default so logs do not accidentally leak sensitive payloads; when on, payloads are clipped to `maxBodyChars` (default 1024) and JSON-serialised when possible.
- `formatter` knob lets callers shape the entry (newline-delimited JSON, OTel `LogRecord`, …) without subclassing.
- `bus` injection is optional — leave it unset if the consumer wants to call the formatter manually or attach via OTel.
## Usage: Basic Example
```ts
import {
LoggingMiddleware,
MiddlewareManager,
} from "@suaveplan/network-plugin-middleware";
const manager = new MiddlewareManager();
manager.register(new LoggingMiddleware({ includeBody: true }));
await manager.dispatch(envelope, "outgoing");
```
## Usage: Advanced Example
```ts
import {
LoggingMiddleware,
MiddlewareManager,
type LoggingBus,
} from "@suaveplan/network-plugin-middleware";
const events: unknown[] = [];
const bus: LoggingBus = {
emit: (_event, payload) => events.push(payload.entry),
};
const manager = new MiddlewareManager();
manager.register(
new LoggingMiddleware({
id: "wire-tap",
priority: 410, // BACKGROUND
includeBody: true,
maxBodyChars: 256,
bus,
formatter: ({ envelope, context, bodyPreview }) => ({
ts: envelope.timestamp,
t: envelope.type,
d: context.direction,
preview: bodyPreview,
}),
})
);
await manager.dispatch(envelope, "incoming");
```
## API Reference
- `class LoggingMiddleware implements Middleware`
- `constructor(config?: LoggingMiddlewareConfig)`.
- `apply(envelope, context): MessageEnvelope` — never drops.
- `interface LoggingMiddlewareConfig` — `{ id?, priority?, direction?, includeBody?, maxBodyChars?, bus?, formatter? }`.
- `interface LoggingBus` — `{ emit("middleware:logged", payload): void }`.
- `type LoggingFormatter` — `(input) => unknown`.
## Implementation Notes
- The default formatter returns a frozen object so consumers cannot accidentally mutate the entry between emit and observation.
- Body serialisation uses `JSON.stringify` with a `try`/`catch` fallback to `String(...)` so circular references or `BigInt` payloads do not blow up the chain. Callers that care about exact serialisation should pass a custom formatter and skip the built-in preview.
- The body cap is character-based, not byte-based, because the entry travels through structured logs that consume characters; any byte-budget conversion belongs in the sink.
- Payload preview is suppressed when `includeBody === false` even if a custom formatter requests it — the formatter receives `bodyPreview: null` so callers cannot accidentally bypass the toggle through formatter composition.

View File

@@ -0,0 +1,94 @@
# `MiddlewareManager`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Owns the registered middleware list and runs both inbound and outbound chains in priority order for every dispatch. Pins the priority bands defined in `NET-41` (CRITICAL `[0,99]`, HIGH `[100,199]`, NORMAL `[200,299]`, LOW `[300,399]`, BACKGROUND `[400,∞)`) and handles the three error policies (`continue`, `stop`, `retry`) so individual middlewares stay narrow.
## Features
- `register` / `unregister` / `enable` / `disable` for runtime mutation without restart.
- Per-direction chain: `direction: undefined` means "run on both"; otherwise the middleware only runs for the matching direction.
- `chain(direction)` returns the ordered, enabled list a dispatch would run; useful for debugging and tests.
- `dispatch(envelope, direction)` returns a `ChainResult` so callers branch on `dropped: true` instead of inspecting envelope state.
- `priorityForBand(priority)` exported as a free function so callers asking "what band does this priority belong to?" do not need a manager instance.
## Usage: Basic Example
```ts
import {
MiddlewareManager,
type Middleware,
} from "@suaveplan/network-plugin-middleware";
const manager = new MiddlewareManager();
manager.register({
id: "tag",
priority: 250,
apply: (env) => ({
...env,
headers: { ...env.headers, "x-tagged": "yes" },
}),
} satisfies Middleware);
const result = await manager.dispatch(envelope, "outgoing");
result.dropped; // false
result.envelope?.headers["x-tagged"]; // "yes"
```
## Usage: Advanced Example
```ts
import {
MiddlewareManager,
priorityForBand,
} from "@suaveplan/network-plugin-middleware";
const manager = new MiddlewareManager({
errorPolicy: { policy: "retry", maxRetries: 3 },
});
manager.register({
id: "validate",
priority: 50, // CRITICAL band
direction: "incoming",
apply: async (env) => (env.payload === undefined ? null : env),
});
manager.register({
id: "metrics",
priority: 410, // BACKGROUND band
apply: async (env) => env,
});
priorityForBand(50); // "CRITICAL"
priorityForBand(410); // "BACKGROUND"
manager.disable("metrics");
const result = await manager.dispatch(envelope, "incoming");
if (result.dropped) {
console.warn("dropped by", result.droppedBy);
}
```
## API Reference
- `class MiddlewareManager(config?)`
- `register(middleware: Middleware): void` — throws `MIDDLEWARE_ALREADY_REGISTERED` on duplicate id.
- `unregister(id: string): void` — throws `MIDDLEWARE_NOT_REGISTERED` if missing.
- `enable(id) / disable(id): void` — throws `MIDDLEWARE_NOT_REGISTERED` if missing.
- `has(id): boolean`.
- `list(): ReadonlyArray<Middleware>`.
- `chain(direction): ReadonlyArray<Middleware>` — ordered, enabled, direction-filtered list.
- `dispatch(envelope, direction): Promise<ChainResult>`.
- `function priorityForBand(priority: number): MiddlewarePriority`.
## Implementation Notes
- Middlewares are stored in a `Map` keyed by id. Re-`register` is rejected so callers cannot accidentally clobber an existing middleware silently — `unregister` first if you mean to replace.
- The chain runs synchronously in priority order. Async middlewares add one micro-task per call but preserve ordering across awaits.
- Error handling goes through `runOne`: `continue` swallows and forwards the un-mutated envelope, `stop` rethrows as `MIDDLEWARE_ERROR` with the original as `cause`, `retry` re-invokes up to `maxRetries` before falling back to `stop`. The chain itself never silently drops because of an exception — callers always learn what happened.
- `ChainResult.executed` is frozen so consumers cannot mutate the audit trail. `envelope` is `null` exactly when `dropped: true`, so the type forces explicit handling.
- Outbound and inbound chains share state (the `state` bag is per-dispatch, not per-direction) but execute independently so a slow `incoming` dispatch never blocks an `outgoing` send.

View File

@@ -0,0 +1,78 @@
# `MetricsMiddleware`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Records per-message metrics — size, processing time, and chain queue wait — and publishes them to a `middleware:metrics` bus topic for `network-plugin-monitor` to aggregate. Sits in the BACKGROUND band by default (`priority: 410`) so it observes the chain after every other middleware has run.
## Features
- Approximate UTF-8 size derived from a JSON serialisation of the envelope; falls back to a `String(payload)` form for non-serialisable payloads.
- Processing-time / queue-wait deltas derived from a per-dispatch state key (`metrics.enqueuedAt`) so registering the middleware twice — once early, once late — yields `enter` and `exit` measurements that share a baseline.
- Optional bus injection so the middleware can be used purely for its side-effects (custom subscriber attached elsewhere).
- Injectable clock for deterministic tests.
## Usage: Basic Example
```ts
import {
MetricsMiddleware,
MiddlewareManager,
} from "@suaveplan/network-plugin-middleware";
const samples: unknown[] = [];
const manager = new MiddlewareManager();
manager.register(
new MetricsMiddleware({
bus: { emit: (_event, m) => samples.push(m) },
})
);
await manager.dispatch(envelope, "outgoing");
```
## Usage: Advanced Example
```ts
import {
MetricsMiddleware,
type MessageMetrics,
} from "@suaveplan/network-plugin-middleware";
let now = 0;
const events: MessageMetrics[] = [];
const bus = { emit: (_event: string, m: MessageMetrics) => events.push(m) };
// Two instances: one at the start of the chain (enter), one at the
// end (exit). The late instance reports a non-zero `processingTimeMs`
// because the early instance seeded `metrics.enqueuedAt`.
const enter = new MetricsMiddleware({
id: "metrics-enter",
priority: 1,
bus,
clock: () => now,
});
const exit = new MetricsMiddleware({
id: "metrics-exit",
priority: 999,
bus,
clock: () => now,
});
```
## API Reference
- `class MetricsMiddleware implements Middleware`
- `constructor(config?: MetricsMiddlewareConfig)`.
- `apply(envelope, context): MessageEnvelope` — never drops, always emits one metrics event when `bus` is set.
- `interface MetricsMiddlewareConfig` — `{ id?, priority?, direction?, bus?, clock? }`.
- `interface MessageMetrics` — `{ direction, messageId, messageType, sizeBytes, processingTimeMs, queueWaitMs }`.
- `interface MetricsBus` — `{ emit("middleware:metrics", metrics): void }`.
## Implementation Notes
- Size estimation uses a hand-rolled UTF-8 byte counter rather than `TextEncoder` so the middleware works on constrained runtimes (Bun browser, edge workers) without an extra allocation per message. The estimate is exact for the BMP and within a small percentage for surrogate-paired scripts.
- The state key `metrics.enqueuedAt` is intentionally namespaced so it does not collide with caller state. Stage-specific keys (e.g. `metrics.enqueuedAt.encryption`) can be layered by configuring `id` accordingly and reading state in a custom subscriber.
- The middleware never drops — telemetry should never be the cause of a message vanishing. Failures inside the bus emit propagate as exceptions and are governed by the manager's `error-policy`.
- `processingTimeMs` and `queueWaitMs` are equal in the single-instance case because the chain treats the first sample as both the enqueue and the snapshot. Two-instance setups (early enter / late exit) populate them separately.

View File

@@ -0,0 +1,85 @@
# `MiddlewarePlugin`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Single `NetworkPlugin` that owns a `MiddlewareManager` and exposes it as a public field so consumers register, dispatch, and inspect middleware via the manager directly. Keeps the `NetworkPlugin` facade narrow — the manager is the substantive surface; the plugin just gives it a lifecycle hook into a `NetworkNode`.
## Features
- `install(node)` records the host node and registers any middlewares passed at construction time (idempotent: pre-existing ids are skipped).
- `start` / `stop` / `dispose` lifecycle that satisfies `NetworkPlugin`.
- `dispose` unregisters every middleware so a re-installed plugin starts clean.
- Public `manager` field gives consumers full access to runtime register / unregister / enable / disable / dispatch.
## Usage: Basic Example
```ts
import { NetworkBus, NetworkNode } from "@suaveplan/network";
import {
LoggingMiddleware,
MiddlewarePlugin,
} from "@suaveplan/network-plugin-middleware";
const bus = new NetworkBus();
const node = new NetworkNode({ id: "node-1", networkBus: bus });
const plugin = new MiddlewarePlugin({
middlewares: [new LoggingMiddleware({ includeBody: true })],
});
node.use(plugin);
await node.start();
await plugin.manager.dispatch(envelope, "outgoing");
```
## Usage: Advanced Example
```ts
import { NetworkBus, NetworkNode } from "@suaveplan/network";
import {
MiddlewarePlugin,
ValidationMiddleware,
SecurityMiddleware,
ThrottlingMiddleware,
} from "@suaveplan/network-plugin-middleware";
const plugin = new MiddlewarePlugin({
errorPolicy: { policy: "stop" },
middlewares: [
new ValidationMiddleware({ schemas: { /* … */ } }),
new SecurityMiddleware({ /* … */ }),
new ThrottlingMiddleware({
global: { capacity: 100, refillRate: 50 },
}),
],
});
const node = new NetworkNode({
id: "node-1",
networkBus: new NetworkBus(),
});
node.use(plugin);
await node.start();
```
## API Reference
- `class MiddlewarePlugin implements NetworkPlugin`
- `name: "middleware"`.
- `manager: MiddlewareManager` — public field; primary surface.
- `constructor(config?: MiddlewarePluginConfig)`.
- `install(node: NetworkNode): void`.
- `start(): void` / `stop(): void` — idempotent.
- `dispose(): void` — unregisters every middleware and detaches the node.
- `host(): NetworkNode | null` — node currently bound (or `null` after dispose).
- `isStarted(): boolean`.
- `interface MiddlewarePluginConfig` — `{ errorPolicy?, middlewares? }`.
## Implementation Notes
- The plugin holds a _reference_ to the manager rather than re-exporting its methods so the public surface stays explicit. Callers that prefer a thin facade can write one over the public field — keeping the plugin minimal makes refactors cheaper.
- Middlewares registered at construction time are only applied on `install` so a plugin built before its host node still satisfies the same contract once attached.
- `dispose` walks `manager.list()` and unregisters each middleware. The `has(id)` guard absorbs the (rare) case of an id being removed mid-iteration by an external listener.
- `start` / `stop` only flip a boolean: the manager is fully usable without the plugin being started — `start` exists to satisfy `NetworkPlugin` and to give consumers a single hook for "OK, now you may begin processing".

View File

@@ -0,0 +1,86 @@
# `SecurityMiddleware`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Applies sign / encrypt on outbound messages and verifies signatures on inbound messages, gated by a per-`MessageType` or per-tier policy. Crypto-implementation-agnostic: callers supply `sign`, `encrypt`, and `verify` callbacks so the middleware works with any crypto module today (and will plug into `@suaveplan/network-plugin-crypto-messages` once that package ships).
## Features
- Two-key lookup: policy is checked first by `MessageType`, then by tier name resolved from a caller-supplied `tierFor(peerId)` lookup. The first match wins.
- `requireSignature: true` rejects unsigned/malformed inbound messages with `SECURITY_REJECTED`.
- Outbound `sign` runs before `encrypt` so the verifier sees the signature inside the encrypted envelope.
- Misconfiguration (policy demands signing without supplying a `sign` callback) is detected at dispatch time, not runtime, so deployments fail loudly.
- Lives in CRITICAL band by default (`priority: 80`) so security runs immediately after validation.
## Usage: Basic Example
```ts
import {
SecurityMiddleware,
MiddlewareManager,
} from "@suaveplan/network-plugin-middleware";
const manager = new MiddlewareManager();
manager.register(
new SecurityMiddleware({
policy: {
"registry:add": { sign: true, requireSignature: true },
},
sign: async (env) => ({
...env,
headers: { ...env.headers, "x-signature": "sig-stub" },
}),
verify: async (env) => env.headers["x-signature"] === "sig-stub",
})
);
```
## Usage: Advanced Example
```ts
import {
SecurityMiddleware,
type SecurityPolicy,
type TierName,
} from "@suaveplan/network-plugin-middleware";
const policy: SecurityPolicy = {
"tier-0": { sign: true, encrypt: true, requireSignature: true },
"tier-1": { sign: true, requireSignature: true },
"tier-2": { sign: true },
"registry:add": { sign: true, requireSignature: true },
};
const security = new SecurityMiddleware({
id: "tier-aware-security",
priority: 90,
policy,
tierFor: (peerId): TierName | undefined =>
peerId.startsWith("hub-") ? "tier-0" : "tier-2",
sign: signWithEd25519,
encrypt: encryptWithChacha,
verify: verifyEd25519,
});
```
## API Reference
- `class SecurityMiddleware implements Middleware`
- `constructor(config?: SecurityMiddlewareConfig)`.
- `apply(envelope, context): Promise<MessageEnvelope | null>`.
- `interface SecurityMiddlewareConfig` — `{ id?, priority?, direction?, tierFor?, policy?, sign?, encrypt?, verify? }`.
- `interface SecurityPolicyEntry` — `{ sign?, encrypt?, requireSignature? }`.
- `type SecurityPolicy` — `Readonly<Record<string, SecurityPolicyEntry | undefined>>` keyed by `MessageType` or `TierName`.
- `type TierName` — `"tier-0" | "tier-1" | "tier-2" | "tier-3"`.
- `type SignFn` / `EncryptFn` / `VerifyFn` — callback signatures.
## Implementation Notes
- The `MessageType` lookup wins over the tier lookup so deployments can opt specific message types out of (or into) more restrictive policies without rewriting the tier table.
- `tierFor` resolves against `envelope.targetId ?? envelope.senderId` so outbound messages key off the recipient and inbound messages key off the originator. Callers that need different lookup semantics can compose two SecurityMiddleware instances under different ids.
- Crypto callbacks MAY be sync or async; the middleware always awaits, so adding async-only flows (e.g. WebCrypto) does not require recompiling sync paths.
- The error code is deliberately generic (`SECURITY_REJECTED`) — distinguishing missing-callback vs invalid-signature is left to the `messageKey` so callers can branch on the key string when richer feedback is required.
- Policy entries default to "no action" — `sign: false` and `requireSignature: false` are equivalent to omitting the keys, matching the proposal's "additive only" stance.

View File

@@ -0,0 +1,68 @@
# `ThrottlingMiddleware`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Token-bucket rate limiting backed by `@suaveplan/rate-limiter` for the network chain. Three independent layers — global, per-`senderId`, per-`MessageType` — combine conservatively: a message is allowed only when every applicable bucket has tokens. When any bucket refuses, the middleware either throws `THROTTLED` (default) so the manager's error policy decides, or returns `null` for a clean chain drop.
## Features
- Three configurable layers, each with its own `capacity` (burst) and `refillRate` (sustained tokens / sec).
- Per-peer buckets are created lazily on first sight so the cost is `O(active peers)` rather than `O(known peers)`.
- `onExceed: "throw"` / `"drop"` lets callers choose between loud-failure semantics (informational headers, retry policy) and silent suppression (audit-only deployments).
- Injectable clock — every internal `TokenBucket` shares the manager's clock so deterministic tests can pin the schedule exactly.
- Lives in HIGH band by default (`priority: 150`) so throttling runs after security/validation but before application-level transformation.
## Usage: Basic Example
```ts
import {
ThrottlingMiddleware,
MiddlewareManager,
} from "@suaveplan/network-plugin-middleware";
const manager = new MiddlewareManager();
manager.register(
new ThrottlingMiddleware({
global: { capacity: 10, refillRate: 5 },
})
);
await manager.dispatch(envelope, "outgoing");
```
## Usage: Advanced Example
```ts
import { ThrottlingMiddleware } from "@suaveplan/network-plugin-middleware";
let now = 0;
const throttler = new ThrottlingMiddleware({
id: "tiered-throttle",
priority: 160,
onExceed: "drop",
clock: () => now,
global: { capacity: 1000, refillRate: 500 },
perPeer: { capacity: 100, refillRate: 50 },
perType: {
"chunk:upload": { capacity: 20, refillRate: 5 },
"registry:add": { capacity: 5, refillRate: 1 },
},
});
```
## API Reference
- `class ThrottlingMiddleware implements Middleware`
- `constructor(config?: ThrottlingMiddlewareConfig)`.
- `apply(envelope, context): MessageEnvelope | null` — `null` only when `onExceed: "drop"`.
- `interface ThrottleLimit` — `{ refillRate, capacity }`.
- `interface ThrottlingMiddlewareConfig` — `{ id?, priority?, direction?, global?, perPeer?, perType?, onExceed?, clock? }`.
## Implementation Notes
- All three layers must allow the message — this is intentional: the most-restrictive bucket wins. Combining limits any other way (e.g. "any bucket allows") would let a misbehaving peer chew through the global budget unchecked.
- Per-peer buckets are not garbage-collected. For long-running processes with high peer churn, attach an external `peer:disconnected` listener and call `forget(senderId)` from a custom subclass that exposes the peer-bucket map; the default behaviour optimises for the common case where the peer set is bounded.
- `THROTTLED` carries `retryAfterSec` so callers (the dispatcher, a custom error responder) can communicate `Retry-After` semantics back to the sender.
- `onExceed: "drop"` returns `null` from `apply`, which the manager surfaces as `ChainResult.dropped: true` with `droppedBy: "throttler"` — observability gets the throttle event even when the failure is intentionally silent.

View File

@@ -0,0 +1,83 @@
# `TransformationMiddleware`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Applies per-`MessageType` transforms to the envelope. Use it for the routine, predictable mutations the chain owns: compressing large payloads, stripping debug fields in production, injecting OTel trace context, redacting fields by policy. Sits in the NORMAL band by default (`priority: 220`) so it runs after security/validation but before metrics and logging.
## Features
- Per-`MessageType` transform map, plus optional `defaultTransform` for catch-all behaviour.
- Transforms MAY be async; the middleware always awaits.
- A transform may return `null` to drop the message (the manager surfaces this via `ChainResult.dropped`), `undefined` to leave the envelope unchanged, or a new envelope.
- Direction filter respected — pair with `direction: "outgoing"` for a "compress on send" rule.
## Usage: Basic Example
```ts
import {
TransformationMiddleware,
MiddlewareManager,
} from "@suaveplan/network-plugin-middleware";
const manager = new MiddlewareManager();
manager.register(
new TransformationMiddleware({
transforms: {
"telemetry:report": (env) => ({
...env,
payload: redactDebug(env.payload),
}),
},
})
);
```
## Usage: Advanced Example
```ts
import {
TransformationMiddleware,
type TransformFn,
} from "@suaveplan/network-plugin-middleware";
const compressIfLarge: TransformFn = async (env) => {
if (typeof env.payload !== "string") return env;
if (env.payload.length < 4096) return env;
return {
...env,
headers: { ...env.headers, "x-encoding": "gzip" },
payload: await gzip(env.payload),
};
};
const dropDebug: TransformFn = (env) =>
env.type.startsWith("debug:") && process.env.NODE_ENV === "production"
? null
: undefined;
const transformer = new TransformationMiddleware({
id: "shape-and-drop",
priority: 230,
direction: "outgoing",
transforms: { "chunk:upload": compressIfLarge },
defaultTransform: dropDebug,
});
```
## API Reference
- `class TransformationMiddleware implements Middleware`
- `constructor(config?: TransformationMiddlewareConfig)`.
- `apply(envelope, context): Promise<MessageEnvelope | null>`.
- `interface TransformationMiddlewareConfig` — `{ id?, priority?, direction?, transforms?, defaultTransform? }`.
- `type TransformFn` — `(envelope, context) => MessageEnvelope | null | undefined | Promise<…>`.
## Implementation Notes
- Returning `undefined` from a transform is treated as "no change". This keeps trivial pass-throughs short (`(env) => env.x === ok ? undefined : edited(env)`) without forcing callers to spread the envelope on every match.
- The middleware always awaits the transform so async transforms (compression, asynchronous redaction, OTel context injection) preserve chain order.
- Transforms run for messages whose type matches; messages without an explicit entry fall through unless `defaultTransform` is set. The split lets callers narrow expensive transforms to specific types without hand-coding type checks inside the transform body.
- The middleware does NOT validate the result of a transform — that is `ValidationMiddleware`'s job. Compose them in priority order (validate first → transform → re-validate later if needed).

View File

@@ -0,0 +1,56 @@
# Chain types — `@suaveplan/network-plugin-middleware`
## Purpose
Shapes the manager uses to report what happened during a chain run and to configure how it reacts when a middleware throws. Both shapes live here (rather than in the manager module) so consumers — bus subscribers, telemetry adapters, custom dispatchers — can import the types without pulling in the manager runtime.
## Features
- `ErrorPolicy` closed union (`continue`, `stop`, `retry`) covering the three meaningful strategies; `ErrorPolicyConfig` carries the optional `maxRetries` knob.
- `ChainResult` includes `dropped`, the resulting envelope (or `null`), the ordered list of middleware ids that ran, and an optional `droppedBy` id so callers can report which middleware refused the message.
## Usage: Basic Example
```ts
import type {
ChainResult,
ErrorPolicyConfig,
} from "@suaveplan/network-plugin-middleware";
const policy: ErrorPolicyConfig = { policy: "retry", maxRetries: 3 };
declare const result: ChainResult;
if (result.dropped) {
console.warn("message dropped by", result.droppedBy);
}
```
## Usage: Advanced Example
```ts
import type {
ChainResult,
ErrorPolicy,
} from "@suaveplan/network-plugin-middleware";
const fallback: ErrorPolicy = "continue";
declare const result: ChainResult;
console.table({
dropped: result.dropped,
droppedBy: result.droppedBy ?? "—",
executed: result.executed.join(" → "),
});
```
## API Reference
- `type ErrorPolicy` — `"continue" | "stop" | "retry"`.
- `interface ErrorPolicyConfig` — `{ policy, maxRetries? }`.
- `interface ChainResult` — `{ dropped, envelope, executed, droppedBy? }`.
## Implementation Notes
- `ChainResult.envelope` is typed `MessageEnvelope | null` rather than `MessageEnvelope?` so consumers cannot accidentally dispatch a dropped envelope by treating `undefined` as "no change".
- `executed` is `ReadonlyArray<string>` so consumers cannot mutate the audit trail. The manager builds the array per dispatch and freezes the result before returning.
- `ErrorPolicyConfig.maxRetries` is only honoured under `policy: "retry"` so the policy / knobs stay grouped — callers that misconfigure another policy with a `maxRetries` value get no surprises.
- These types do not include retry attempt counts: the goal of `ChainResult` is *what happened*, not *how it got there*. Adding finer-grained instrumentation belongs in `metrics-middleware` so callers do not pay the cost when they do not opt in.

View File

@@ -0,0 +1,69 @@
# Middleware types — `@suaveplan/network-plugin-middleware`
## Purpose
Shared structural types for the middleware chain (`NET-41`). The contract a middleware author satisfies is a single function (`apply(envelope, context)`) returning the next envelope, `null` to drop, or a Promise of either; all of the bookkeeping (priority bands, direction filtering, enable/disable) lives in the manager so middleware classes stay narrow.
## Features
- `Middleware` — minimal callable interface every concrete middleware satisfies.
- `MiddlewareContext` — carries direction + per-dispatch `state` scratch so middlewares can pass derived values downstream without mutating envelope headers.
- `ChainDirection` — closed `"incoming" | "outgoing"` union so dual-direction logic stays exhaustively type-checked.
- `MiddlewarePriority` — closed band union (`CRITICAL`, `HIGH`, `NORMAL`, `LOW`, `BACKGROUND`) for callers that prefer the enum to a raw number.
## Usage: Basic Example
```ts
import type {
Middleware,
MiddlewareContext,
} from "@suaveplan/network-plugin-middleware";
import type { MessageEnvelope } from "@suaveplan/network";
const tagger: Middleware = {
id: "header-tagger",
priority: 250,
apply(envelope: MessageEnvelope, _ctx: MiddlewareContext) {
return {
...envelope,
headers: { ...envelope.headers, "x-tagged": "yes" },
};
},
};
```
## Usage: Advanced Example
```ts
import type {
Middleware,
MiddlewareContext,
} from "@suaveplan/network-plugin-middleware";
import type { MessageEnvelope } from "@suaveplan/network";
const inboundOnlyDropper: Middleware = {
id: "drop-debug",
priority: 350,
direction: "incoming",
enabled: process.env.NODE_ENV === "production",
async apply(env: MessageEnvelope, ctx: MiddlewareContext) {
ctx.state["sawDebug"] = env.type.startsWith("debug:");
return ctx.state["sawDebug"] === true ? null : env;
},
};
```
## API Reference
- `interface Middleware` — `{ id, priority, direction?, enabled?, apply(envelope, context) }` — `apply` MAY be sync or async.
- `interface MiddlewareContext` — `{ direction, state }`.
- `type ChainDirection` — `"incoming" | "outgoing"`.
- `type MiddlewarePriority` — `"CRITICAL" | "HIGH" | "NORMAL" | "LOW" | "BACKGROUND"`.
## Implementation Notes
- `priority` is a raw `number` rather than the band enum so callers can position middleware *within* a band (e.g. `120` vs `180` both inside HIGH). The manager exposes `priorityForBand` to convert when callers prefer the enum.
- `direction` is optional: middleware that sets neither runs in both directions. Filtering happens once at registration time so the per-dispatch loop stays branchless.
- `enabled` defaults to `true`. The manager respects this on every dispatch so middlewares can flip on/off at runtime without re-registering.
- `apply` MAY return a Promise; the manager always awaits, even for sync middlewares, so the chain order is preserved across async boundaries. Synchronous middlewares pay one micro-task per call, which is negligible compared to network RTT.
- `state` is a plain object rather than a `Map` so JSON-friendly observability adapters can serialise it for tracing.

View File

@@ -0,0 +1,81 @@
# `ValidationMiddleware`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Schema-based payload validation for inbound and outbound messages. Per-`MessageType` Zod schemas describe the expected shape; messages whose type has no entry pass through (or hit the optional `defaultSchema`). Two modes — `strict` (default) raises `VALIDATION_FAILED` so the manager's `error-policy: "stop"` rejects the envelope; `lenient` logs a structured warning and forwards the envelope unchanged.
## Features
- Per-`MessageType` schema map, plus optional `defaultSchema` for catch-all enforcement.
- Strict / lenient modes covering both "block bad messages" and "observe-only deployment" stories.
- Lenient mode emits structured warnings (`{ messageId, messageType, issues[] }`) to a pluggable logger so deployments wire whatever sink they prefer.
- Lives in CRITICAL band by default (`priority: 50`) so validation runs before any transformation, security, or metrics middleware.
## Usage: Basic Example
```ts
import z from "zod";
import {
ValidationMiddleware,
MiddlewareManager,
} from "@suaveplan/network-plugin-middleware";
const manager = new MiddlewareManager();
manager.register(
new ValidationMiddleware({
schemas: {
"registry:add": z.object({ peerId: z.string().min(1) }),
},
})
);
await manager.dispatch(envelope, "incoming");
```
## Usage: Advanced Example
```ts
import z from "zod";
import {
ValidationMiddleware,
type ValidationLogger,
} from "@suaveplan/network-plugin-middleware";
const warnings: unknown[] = [];
const logger: ValidationLogger = {
warn: (w) => warnings.push(w),
};
const validator = new ValidationMiddleware({
id: "lenient-validator",
priority: 60,
mode: "lenient",
logger,
schemas: {
"registry:add": z.object({ peerId: z.string().uuid() }),
},
defaultSchema: z
.object({})
.passthrough()
.refine((p) => Object.keys(p).length > 0, "non-empty payload required"),
});
```
## API Reference
- `class ValidationMiddleware implements Middleware`
- `constructor(config?: ValidationMiddlewareConfig)`.
- `apply(envelope, context): MessageEnvelope` — throws `VALIDATION_FAILED` (strict) or warns (lenient).
- `interface ValidationMiddlewareConfig` — `{ id?, priority?, direction?, mode?, logger?, schemas?, defaultSchema? }`.
- `type ValidationMode` — `"strict" | "lenient"`.
- `interface ValidationWarning` — `{ messageId, messageType, issues }`.
- `interface ValidationLogger` — `{ warn(warning): void }`.
## Implementation Notes
- Validation uses `safeParse` so a thrown Zod error never escapes the middleware. Strict mode wraps the issues into `NetworkMwError(VALIDATION_FAILED)` so the manager's error policy controls what happens next; lenient mode forwards unchanged so callers see _all_ validation issues over time, not just the first.
- Messages without a schema pass through silently. The `defaultSchema` knob lets callers reverse this default for "deny by default" deployments without registering a schema per message type.
- The middleware does NOT modify the envelope. Schemas that _transform_ payloads should compose with `TransformationMiddleware` downstream — splitting concerns keeps the validator's behaviour predictable for audit reviews.
- The default priority (`50`) places the validator inside the CRITICAL band so it runs before any other middleware. Override via the `priority` knob when validation is informational rather than gating.