first commit
This commit is contained in:
32
docs/networking/network/network-plugin-middleware/README.md
Normal file
32
docs/networking/network/network-plugin-middleware/README.md
Normal 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…
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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".
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user