ocpp-ws-io 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugins.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { O as OCPPPlugin } from './types-BunMs45p.js';
1
+ import { O as OCPPPlugin } from './types-BHIHsj__.js';
2
2
  import 'ws';
3
3
  import 'node:https';
4
4
  import 'node:http';
@@ -8,6 +8,176 @@ import 'node:tls';
8
8
  import 'voltlog-io';
9
9
  import 'ajv';
10
10
 
11
+ /**
12
+ * Options for the async worker plugin.
13
+ */
14
+ interface AsyncWorkerOptions {
15
+ /**
16
+ * Maximum number of concurrent tasks.
17
+ * @default 10
18
+ */
19
+ concurrency?: number;
20
+ /**
21
+ * Maximum queue depth. When exceeded, tasks are handled per `overflowStrategy`.
22
+ * @default 1000
23
+ */
24
+ maxQueueSize?: number;
25
+ /**
26
+ * What to do when queue is full.
27
+ * - `"drop-oldest"`: Remove oldest queued task (default)
28
+ * - `"drop-newest"`: Reject the incoming task
29
+ * @default "drop-oldest"
30
+ */
31
+ overflowStrategy?: "drop-oldest" | "drop-newest";
32
+ /**
33
+ * Error handler for failed tasks.
34
+ */
35
+ onError?: (error: Error, taskName: string) => void;
36
+ /**
37
+ * Logger for queue warnings (overflow, drain).
38
+ */
39
+ logger?: {
40
+ warn: (...args: unknown[]) => void;
41
+ };
42
+ /**
43
+ * Drain timeout during shutdown (ms).
44
+ * The worker will attempt to complete in-flight tasks before force-closing.
45
+ * @default 5000
46
+ */
47
+ drainTimeoutMs?: number;
48
+ }
49
+ /**
50
+ * Extended OCPPPlugin exposing `enqueue()`, `queueSize()`, and `activeCount()`.
51
+ */
52
+ interface AsyncWorkerPlugin extends OCPPPlugin {
53
+ /**
54
+ * Enqueue a task for non-blocking background execution.
55
+ * Returns immediately — the task runs asynchronously.
56
+ * @returns `true` if enqueued, `false` if dropped due to overflow.
57
+ */
58
+ enqueue(taskName: string, fn: () => Promise<void>): boolean;
59
+ /** Current number of tasks waiting in the queue. */
60
+ queueSize(): number;
61
+ /** Number of currently executing tasks. */
62
+ activeCount(): number;
63
+ /** Total number of tasks dropped due to overflow since init. */
64
+ droppedCount(): number;
65
+ }
66
+ /**
67
+ * Background task queue for non-blocking plugin I/O.
68
+ *
69
+ * Provides a bounded, concurrent work queue that plugins can use to offload
70
+ * slow operations (MQTT publish, DB write, HTTP call) without blocking the
71
+ * OCPP message processing loop.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * import { asyncWorkerPlugin } from 'ocpp-ws-io/plugins';
76
+ *
77
+ * const worker = asyncWorkerPlugin({ concurrency: 20, maxQueueSize: 5000 });
78
+ * server.plugin(worker);
79
+ *
80
+ * // From any hook:
81
+ * worker.enqueue("db-write", async () => {
82
+ * await db.insert({ ... });
83
+ * });
84
+ * ```
85
+ */
86
+ declare function asyncWorkerPlugin(options?: AsyncWorkerOptions): AsyncWorkerPlugin;
87
+
88
+ /**
89
+ * Minimal AMQP channel contract — compatible with `amqplib`.
90
+ * Users bring their own AMQP dependency; this plugin does not bundle one.
91
+ */
92
+ interface AmqpChannelLike {
93
+ /** Publish a message to an exchange with a routing key. */
94
+ publish(exchange: string, routingKey: string, content: Buffer, options?: Record<string, unknown>): boolean;
95
+ /** Close the channel. */
96
+ close(): Promise<void> | void;
97
+ }
98
+ type AmqpEvent = "connect" | "disconnect" | "message" | "security" | "auth_failed" | "eviction";
99
+ /**
100
+ * Options for the AMQP plugin.
101
+ */
102
+ interface AmqpPluginOptions {
103
+ /**
104
+ * User-provided AMQP channel (from `amqplib`).
105
+ * The plugin does NOT manage connections — users provide a ready channel.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const conn = await amqp.connect('amqp://localhost');
110
+ * const channel = await conn.createChannel();
111
+ * await channel.assertExchange('ocpp.events', 'topic', { durable: true });
112
+ * ```
113
+ */
114
+ channel: AmqpChannelLike;
115
+ /**
116
+ * Exchange to publish to.
117
+ * @default "ocpp.events"
118
+ */
119
+ exchange?: string;
120
+ /**
121
+ * Routing key pattern. `{event}` and `{identity}` are interpolated.
122
+ * @default "ocpp.{event}.{identity}"
123
+ */
124
+ routingKey?: string;
125
+ /**
126
+ * Which events to publish.
127
+ * @default ["connect", "disconnect", "message", "security"]
128
+ */
129
+ events?: AmqpEvent[];
130
+ /**
131
+ * AMQP publish options.
132
+ */
133
+ publishOptions?: {
134
+ /** Mark messages as persistent (survives broker restart). @default true */
135
+ persistent?: boolean;
136
+ /** Message priority (0-9). */
137
+ priority?: number;
138
+ /** Content type header. @default "application/json" */
139
+ contentType?: string;
140
+ };
141
+ /**
142
+ * Include full message payloads.
143
+ * @default false
144
+ */
145
+ includePayload?: boolean;
146
+ /**
147
+ * Optional async worker for non-blocking publishes.
148
+ */
149
+ worker?: AsyncWorkerPlugin;
150
+ }
151
+ /**
152
+ * Publishes OCPP events to an AMQP exchange (RabbitMQ, Azure Service Bus, etc).
153
+ *
154
+ * For enterprise integrations where guaranteed delivery and topic-based
155
+ * routing are critical. Uses AMQP `topic` exchange by default so consumers
156
+ * can subscribe to specific event patterns.
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * import amqp from 'amqplib';
161
+ * import { amqpPlugin } from 'ocpp-ws-io/plugins';
162
+ *
163
+ * const conn = await amqp.connect('amqp://localhost');
164
+ * const channel = await conn.createChannel();
165
+ * await channel.assertExchange('ocpp.events', 'topic', { durable: true });
166
+ *
167
+ * server.plugin(amqpPlugin({
168
+ * channel,
169
+ * exchange: 'ocpp.events',
170
+ * events: ['connect', 'disconnect', 'message', 'security'],
171
+ * }));
172
+ *
173
+ * // Consumer binds:
174
+ * // ocpp.connect.* — all connection events
175
+ * // ocpp.message.CP-101 — messages from CP-101
176
+ * // ocpp.security.* — all security events
177
+ * ```
178
+ */
179
+ declare function amqpPlugin(options: AmqpPluginOptions): OCPPPlugin;
180
+
11
181
  /**
12
182
  * Options for the anomaly detection plugin.
13
183
  */
@@ -15,19 +185,38 @@ interface AnomalyPluginOptions {
15
185
  /**
16
186
  * Maximum number of connections from the same identity
17
187
  * within the sliding window before triggering an anomaly.
18
- * Default: 5
188
+ * @default 5
19
189
  */
20
190
  reconnectThreshold?: number;
21
191
  /**
22
192
  * Sliding window duration in milliseconds.
23
- * Default: 60_000 (1 minute)
193
+ * @default 60_000 (1 minute)
24
194
  */
25
195
  windowMs?: number;
196
+ /**
197
+ * Maximum auth failures from the same IP within the window
198
+ * before triggering a brute-force anomaly.
199
+ * @default 5
200
+ */
201
+ authFailureThreshold?: number;
202
+ /**
203
+ * Maximum bad messages from the same identity within the window
204
+ * before triggering a fuzzing anomaly.
205
+ * @default 10
206
+ */
207
+ badMessageThreshold?: number;
208
+ /**
209
+ * Maximum evictions for the same identity within the window
210
+ * before triggering an identity-collision anomaly.
211
+ * @default 3
212
+ */
213
+ evictionThreshold?: number;
26
214
  }
27
215
  /**
28
- * Detects anomalous connection patterns such as rapid reconnections
29
- * from the same identity. Emits `securityEvent` on the server with
30
- * type `ANOMALY_RAPID_RECONNECT`.
216
+ * Detects anomalous connection patterns: rapid reconnections, brute-force
217
+ * auth attempts, message fuzzing, and identity-stealing races.
218
+ *
219
+ * Emits `securityEvent` on the server with typed anomaly identifiers.
31
220
  *
32
221
  * @example
33
222
  * ```ts
@@ -35,6 +224,8 @@ interface AnomalyPluginOptions {
35
224
  *
36
225
  * server.plugin(anomalyPlugin({
37
226
  * reconnectThreshold: 10,
227
+ * authFailureThreshold: 5,
228
+ * badMessageThreshold: 20,
38
229
  * windowMs: 60_000,
39
230
  * }));
40
231
  *
@@ -47,22 +238,122 @@ interface AnomalyPluginOptions {
47
238
  */
48
239
  declare function anomalyPlugin(options?: AnomalyPluginOptions): OCPPPlugin;
49
240
 
241
+ /**
242
+ * Options for the circuit-breaker plugin.
243
+ */
244
+ interface CircuitBreakerOptions {
245
+ /**
246
+ * Number of consecutive failures before the circuit opens.
247
+ * @default 5
248
+ */
249
+ failureThreshold?: number;
250
+ /**
251
+ * Duration in ms the circuit stays OPEN before attempting a HALF_OPEN probe.
252
+ * @default 30000 (30 seconds)
253
+ */
254
+ resetTimeoutMs?: number;
255
+ /**
256
+ * Maximum number of concurrent outgoing calls per client.
257
+ * When exceeded, calls are rejected immediately (fail-fast).
258
+ * @default 20
259
+ */
260
+ maxConcurrent?: number;
261
+ /**
262
+ * Optional callback when a circuit state changes.
263
+ * Useful for alerting/metrics integrations.
264
+ */
265
+ onStateChange?: (identity: string, from: CircuitState, to: CircuitState) => void;
266
+ /**
267
+ * Optional logger. Falls back to silent no-op.
268
+ */
269
+ logger?: {
270
+ warn: (...args: unknown[]) => void;
271
+ error: (...args: unknown[]) => void;
272
+ };
273
+ }
274
+ type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN";
275
+ /**
276
+ * Circuit Breaker Plugin (Level 2: Lifecycle Controller)
277
+ *
278
+ * Implements per-client circuit breaker pattern for flapping or unreliable
279
+ * charging stations. Prevents a misbehaving client from degrading system
280
+ * performance by fast-failing calls to clients exhibiting repeated errors.
281
+ *
282
+ * **State Machine:**
283
+ * ```
284
+ * CLOSED ─(failures ≥ threshold)─→ OPEN
285
+ * OPEN ─(resetTimeout expires)─→ HALF_OPEN
286
+ * HALF_OPEN ─(success)─→ CLOSED
287
+ * HALF_OPEN ─(failure)─→ OPEN
288
+ * ```
289
+ *
290
+ * @example
291
+ * ```ts
292
+ * import { circuitBreakerPlugin } from 'ocpp-ws-io/plugins';
293
+ *
294
+ * server.plugin(circuitBreakerPlugin({
295
+ * failureThreshold: 3,
296
+ * resetTimeoutMs: 15_000,
297
+ * onStateChange: (id, from, to) => {
298
+ * console.log(`Circuit ${id}: ${from} → ${to}`);
299
+ * },
300
+ * }));
301
+ * ```
302
+ */
303
+ declare function circuitBreakerPlugin(options?: CircuitBreakerOptions): OCPPPlugin;
304
+
50
305
  /**
51
306
  * Options for the connection guard plugin.
52
307
  */
53
308
  interface ConnectionGuardOptions {
54
- /** Maximum allowed concurrent connections. */
309
+ /**
310
+ * Maximum number of concurrent WebSocket connections.
311
+ * New connections beyond this limit are immediately closed.
312
+ */
55
313
  maxConnections: number;
314
+ /**
315
+ * Close code sent when the limit is exceeded.
316
+ * @default 4029
317
+ */
318
+ closeCode?: number;
319
+ /**
320
+ * Close reason sent when the limit is exceeded.
321
+ * @default "Connection limit reached"
322
+ */
323
+ closeReason?: string;
324
+ /**
325
+ * Force-close clients that exceed pong timeout (dead peers).
326
+ * Reclaims connection slots held by unresponsive clients.
327
+ * @default true
328
+ */
329
+ forceCloseOnPongTimeout?: boolean;
330
+ /**
331
+ * Force-close clients experiencing backpressure (slow consumers).
332
+ * Useful for freeing slots when a client can't keep up.
333
+ * @default false
334
+ */
335
+ forceCloseOnBackpressure?: boolean;
336
+ /**
337
+ * Logger for guard events.
338
+ */
339
+ logger?: {
340
+ warn: (...args: unknown[]) => void;
341
+ };
56
342
  }
57
343
  /**
58
- * Enforces a hard limit on concurrent connections.
59
- * New connections exceeding the limit are force-closed with code 4001.
344
+ * Enforces a hard cap on concurrent WebSocket connections.
345
+ * Optionally reclaims slots from dead peers (pong timeout) and
346
+ * slow consumers (backpressure).
60
347
  *
61
348
  * @example
62
349
  * ```ts
63
350
  * import { connectionGuardPlugin } from 'ocpp-ws-io/plugins';
64
351
  *
65
- * server.plugin(connectionGuardPlugin({ maxConnections: 5000 }));
352
+ * server.plugin(connectionGuardPlugin({
353
+ * maxConnections: 1000,
354
+ * forceCloseOnPongTimeout: true,
355
+ * forceCloseOnBackpressure: false, // only enable if you want aggressive slot reclaim
356
+ * }));
66
357
  * ```
67
358
  */
68
359
  declare function connectionGuardPlugin(options: ConnectionGuardOptions): OCPPPlugin;
@@ -81,6 +372,168 @@ declare function connectionGuardPlugin(options: ConnectionGuardOptions): OCPPPlu
81
372
  */
82
373
  declare function heartbeatPlugin(): OCPPPlugin;
83
374
 
375
+ /**
376
+ * Minimal Kafka Producer contract — compatible with `kafkajs`.
377
+ * Users bring their own Kafka dependency; this plugin does not bundle one.
378
+ */
379
+ interface KafkaProducerLike {
380
+ send(record: {
381
+ topic: string;
382
+ messages: Array<{
383
+ key?: string | Buffer;
384
+ value: string | Buffer;
385
+ headers?: Record<string, string | Buffer>;
386
+ }>;
387
+ acks?: number;
388
+ timeout?: number;
389
+ compression?: number;
390
+ }): Promise<unknown>;
391
+ }
392
+ type KafkaEvent = "connect" | "disconnect" | "message" | "security" | "auth_failed" | "eviction";
393
+ /**
394
+ * Options for the Kafka plugin.
395
+ */
396
+ interface KafkaPluginOptions {
397
+ /**
398
+ * User-provided Kafka Producer (e.g. from `kafkajs`).
399
+ * The plugin does NOT manage connections — users provide a ready producer.
400
+ */
401
+ producer: KafkaProducerLike;
402
+ /**
403
+ * Base topic to publish to.
404
+ * If `topicRouting` is true, this is a prefix.
405
+ * @default "ocpp.events"
406
+ */
407
+ topic?: string;
408
+ /**
409
+ * If true, route events to specific topics instead of all to `topic`.
410
+ * e.g., `ocpp.events.message`, `ocpp.events.security`
411
+ * @default false
412
+ */
413
+ topicRouting?: boolean;
414
+ /**
415
+ * Which events to publish.
416
+ * @default ["connect", "disconnect", "message", "security"]
417
+ */
418
+ events?: KafkaEvent[];
419
+ /**
420
+ * Include full message payloads for message events.
421
+ * @default false
422
+ */
423
+ includePayload?: boolean;
424
+ /**
425
+ * Optional async worker for non-blocking publishes.
426
+ * Highly recommended for Kafka.
427
+ */
428
+ worker?: AsyncWorkerPlugin;
429
+ }
430
+ /**
431
+ * Publishes OCPP events to an Apache Kafka topic.
432
+ *
433
+ * Designed for high-throughput edge environments where raw OCPP telemetry
434
+ * needs to be streamed into big data architectures (Data Lakes, ClickHouse)
435
+ * for analysis.
436
+ *
437
+ * @example
438
+ * ```ts
439
+ * import { Kafka } from 'kafkajs';
440
+ *
441
+ * const kafka = new Kafka({ clientId: 'ocpp', brokers: ['localhost:9092'] });
442
+ * const producer = kafka.producer();
443
+ * await producer.connect();
444
+ *
445
+ * server.plugin(kafkaPlugin({
446
+ * producer,
447
+ * topic: 'ocpp.telemetry',
448
+ * includePayload: true
449
+ * }));
450
+ * ```
451
+ */
452
+ declare function kafkaPlugin(options: KafkaPluginOptions): OCPPPlugin;
453
+
454
+ /**
455
+ * Minimal Redis interface for the Deduplication plugin.
456
+ * Supports **both** ioredis positional-arg style and node-redis v4 options-object style.
457
+ *
458
+ * Users bring their own Redis client (e.g., ioredis, node-redis).
459
+ */
460
+ interface DedupRedisLike {
461
+ /**
462
+ * Sets a key with NX + PX semantics.
463
+ *
464
+ * **ioredis style:** `set(key, value, "PX", ms, "NX")` → `Promise<"OK" | null>`
465
+ * **node-redis v4 style:** `set(key, value, { PX: ms, NX: true })` → `Promise<string | null>`
466
+ */
467
+ set(key: string, value: string, ...args: unknown[]): Promise<"OK" | string | null> | ("OK" | string | null);
468
+ }
469
+ interface MessageDedupOptions {
470
+ /**
471
+ * User-provided Redis instance.
472
+ * Compatible with both `ioredis` and `node-redis` (v4+).
473
+ */
474
+ redis: DedupRedisLike;
475
+ /**
476
+ * Time-to-Live for the deduplication cache in milliseconds.
477
+ * @default 300000 (5 minutes)
478
+ */
479
+ ttlMs?: number;
480
+ /**
481
+ * Prefix for Redis keys.
482
+ * @default "ocpp:dedup:"
483
+ */
484
+ prefix?: string;
485
+ /**
486
+ * Which Redis calling convention to use:
487
+ * - `"positional"` (ioredis): `set(key, val, "PX", ms, "NX")`
488
+ * - `"options"` (node-redis v4): `set(key, val, { PX: ms, NX: true })`
489
+ * @default "positional"
490
+ */
491
+ redisStyle?: "positional" | "options";
492
+ /**
493
+ * Optional logger. Falls back to silent no-op if not provided.
494
+ */
495
+ logger?: {
496
+ warn: (...args: unknown[]) => void;
497
+ error: (...args: unknown[]) => void;
498
+ };
499
+ }
500
+ /**
501
+ * Message Deduplication Plugin (Level 3: Interceptor)
502
+ *
503
+ * Prevents processing of duplicate messages from edge devices. When a bad
504
+ * mobile network connection causes a charging station to send the SAME
505
+ * message multiple times, this plugin catches and drops the duplicates
506
+ * before the application logic processes them.
507
+ *
508
+ * It hooks into `onBeforeReceive` and returns `false` to block execution
509
+ * if a message with the identical unique MessageID is found in Redis.
510
+ *
511
+ * @example ioredis (default)
512
+ * ```ts
513
+ * import Redis from 'ioredis';
514
+ * const redis = new Redis();
515
+ *
516
+ * server.plugin(messageDedupPlugin({
517
+ * redis,
518
+ * ttlMs: 60 * 1000 // Remember messages for 1 minute
519
+ * }));
520
+ * ```
521
+ *
522
+ * @example node-redis v4
523
+ * ```ts
524
+ * import { createClient } from 'redis';
525
+ * const redis = createClient();
526
+ * await redis.connect();
527
+ *
528
+ * server.plugin(messageDedupPlugin({
529
+ * redis,
530
+ * redisStyle: 'options',
531
+ * ttlMs: 60 * 1000,
532
+ * }));
533
+ * ```
534
+ */
535
+ declare function messageDedupPlugin(options: MessageDedupOptions): OCPPPlugin;
536
+
84
537
  /**
85
538
  * Snapshot of tracked server metrics at a point in time.
86
539
  */
@@ -99,6 +552,36 @@ interface MetricsSnapshot {
99
552
  uptimeMs: number;
100
553
  /** ISO timestamp of this snapshot */
101
554
  timestamp: string;
555
+ /** Total inbound messages received */
556
+ totalMessagesIn: number;
557
+ /** Total outbound messages sent */
558
+ totalMessagesOut: number;
559
+ /** Total CALL messages */
560
+ totalCalls: number;
561
+ /** Total CALLRESULT messages */
562
+ totalCallResults: number;
563
+ /** Total CALLERROR messages */
564
+ totalCallErrors: number;
565
+ /** Total WebSocket/protocol errors */
566
+ totalErrors: number;
567
+ /** Total malformed/unparseable messages */
568
+ totalBadMessages: number;
569
+ /** Total user handler errors */
570
+ totalHandlerErrors: number;
571
+ /** Total rate limit hits */
572
+ totalRateLimitHits: number;
573
+ /** Total auth failures */
574
+ totalAuthFailures: number;
575
+ /** Total client evictions */
576
+ totalEvictions: number;
577
+ /** Total backpressure events */
578
+ totalBackpressureEvents: number;
579
+ /** Total pong timeouts (dead peers) */
580
+ totalPongTimeouts: number;
581
+ /** Total schema validation failures */
582
+ totalValidationFailures: number;
583
+ /** Total security events (from anomaly detector etc.) */
584
+ totalSecurityEvents: number;
102
585
  }
103
586
  /**
104
587
  * Options for the metrics plugin.
@@ -117,8 +600,12 @@ interface MetricsPlugin extends OCPPPlugin {
117
600
  getMetrics(): MetricsSnapshot;
118
601
  }
119
602
  /**
120
- * Tracks real-time server metrics: connection counters, peak, average duration.
603
+ * Tracks comprehensive real-time server metrics: connection counters, message
604
+ * throughput, error rates, security events, and peak values.
605
+ *
121
606
  * Access metrics anytime via `.getMetrics()` on the returned plugin instance.
607
+ * Automatically exports all counters to the Prometheus `/metrics` endpoint
608
+ * via `getCustomMetrics()`.
122
609
  *
123
610
  * @example
124
611
  * ```ts
@@ -126,7 +613,7 @@ interface MetricsPlugin extends OCPPPlugin {
126
613
  *
127
614
  * const metrics = metricsPlugin({
128
615
  * intervalMs: 10_000,
129
- * onSnapshot: (snap) => console.log(`Active: ${snap.activeConnections}`),
616
+ * onSnapshot: (snap) => console.log(`Active: ${snap.activeConnections}, Msgs: ${snap.totalMessagesIn}`),
130
617
  * });
131
618
  * server.plugin(metrics);
132
619
  *
@@ -136,6 +623,107 @@ interface MetricsPlugin extends OCPPPlugin {
136
623
  */
137
624
  declare function metricsPlugin(options?: MetricsPluginOptions): MetricsPlugin;
138
625
 
626
+ /**
627
+ * Minimal MQTT client contract — compatible with `mqtt.js`, `aedes`, etc.
628
+ * Users bring their own MQTT dependency; this plugin does not bundle one.
629
+ */
630
+ interface MqttClientLike {
631
+ publish(topic: string, message: string | Buffer, opts?: {
632
+ qos?: number;
633
+ retain?: boolean;
634
+ }, callback?: (err?: Error) => void): unknown;
635
+ end(force?: boolean, callback?: () => void): void;
636
+ connected: boolean;
637
+ }
638
+ type MqttEvent = "connect" | "disconnect" | "message" | "security" | "error" | "auth_failed" | "eviction";
639
+ /**
640
+ * Options for the MQTT plugin.
641
+ */
642
+ interface MqttPluginOptions {
643
+ /**
644
+ * User-provided MQTT client instance (e.g., `mqtt.connect(...)`).
645
+ * The plugin does NOT install `mqtt` — users bring their own.
646
+ */
647
+ client: MqttClientLike;
648
+ /**
649
+ * Topic prefix for all published messages.
650
+ * Identity is appended: `{prefix}/{identity}/{event}`
651
+ * @default "ocpp"
652
+ */
653
+ topicPrefix?: string;
654
+ /**
655
+ * Which events to publish.
656
+ * @default ["connect", "disconnect", "message", "security"]
657
+ */
658
+ events?: MqttEvent[];
659
+ /**
660
+ * QoS level for published messages.
661
+ * - 0: at most once (fire-and-forget, fastest)
662
+ * - 1: at least once (with ack)
663
+ * - 2: exactly once (slowest)
664
+ * @default 0
665
+ */
666
+ qos?: 0 | 1 | 2;
667
+ /**
668
+ * Whether to publish full message payloads or just metadata.
669
+ * @default false
670
+ */
671
+ includePayload?: boolean;
672
+ /**
673
+ * Custom topic builder for full control over topic structure.
674
+ * If set, overrides `topicPrefix`.
675
+ */
676
+ topicBuilder?: (event: string, identity?: string) => string;
677
+ /**
678
+ * Transform the payload before publishing.
679
+ * Useful for filtering sensitive data or reformatting.
680
+ */
681
+ transform?: (payload: Record<string, unknown>) => Record<string, unknown>;
682
+ /**
683
+ * Optional async worker for non-blocking publishes.
684
+ * When provided, all publish calls are enqueued to the worker.
685
+ */
686
+ worker?: AsyncWorkerPlugin;
687
+ }
688
+ /**
689
+ * Publishes OCPP events to an MQTT broker.
690
+ *
691
+ * Essential for IoT-style architectures where charging stations, fleet
692
+ * management systems, and monitoring dashboards subscribe to live OCPP feeds.
693
+ *
694
+ * @example
695
+ * ```ts
696
+ * import mqtt from 'mqtt';
697
+ * import { mqttPlugin } from 'ocpp-ws-io/plugins';
698
+ *
699
+ * const client = mqtt.connect('mqtt://broker:1883');
700
+ *
701
+ * server.plugin(mqttPlugin({
702
+ * client,
703
+ * topicPrefix: 'ocpp/v1',
704
+ * events: ['connect', 'disconnect', 'message'],
705
+ * qos: 1,
706
+ * }));
707
+ * ```
708
+ *
709
+ * @example With async worker for non-blocking
710
+ * ```ts
711
+ * import { asyncWorkerPlugin, mqttPlugin } from 'ocpp-ws-io/plugins';
712
+ *
713
+ * const worker = asyncWorkerPlugin({ concurrency: 20 });
714
+ * server.plugin(worker, mqttPlugin({ client, worker }));
715
+ * ```
716
+ *
717
+ * @example Topic structure
718
+ * ```
719
+ * ocpp/CP-101/connect → { identity, ip, protocol, timestamp }
720
+ * ocpp/CP-101/message/IN → { method, messageType, timestamp }
721
+ * ocpp/CP-101/disconnect → { code, reason, durationSec }
722
+ * ocpp/security → { type, identity, ip, details }
723
+ * ```
724
+ */
725
+ declare function mqttPlugin(options: MqttPluginOptions): OCPPPlugin;
726
+
139
727
  /**
140
728
  * Options for the OpenTelemetry plugin.
141
729
  */
@@ -149,17 +737,20 @@ interface OtelPluginOptions {
149
737
  */
150
738
  tracer?: {
151
739
  startSpan: (name: string, options?: Record<string, unknown>) => {
152
- setAttribute: (key: string, value: string | number) => void;
740
+ setAttribute: (key: string, value: string | number | boolean) => void;
153
741
  setStatus: (status: {
154
742
  code: number;
155
743
  message?: string;
156
744
  }) => void;
745
+ addEvent: (name: string, attributes?: Record<string, unknown>) => void;
746
+ recordException: (error: Error) => void;
157
747
  end: () => void;
158
748
  };
159
749
  };
160
750
  }
161
751
  /**
162
- * OpenTelemetry integration — creates spans for connection lifecycle events.
752
+ * OpenTelemetry integration — creates spans for connection lifecycle,
753
+ * individual OCPP messages, errors, and security events.
163
754
  *
164
755
  * Requires `@opentelemetry/api` as an **optional peer dependency**.
165
756
  * If not installed or no tracer is provided, the plugin becomes a silent no-op.
@@ -182,40 +773,440 @@ interface OtelPluginOptions {
182
773
  */
183
774
  declare function otelPlugin(options?: OtelPluginOptions): OCPPPlugin;
184
775
 
776
+ interface PiiRedactorOptions {
777
+ /**
778
+ * List of object keys that should be redacted.
779
+ * Matches ANY key in the payload recursively.
780
+ * @default ["idTag", "authorizationKey", "token", "password", "securityCode"]
781
+ */
782
+ sensitiveKeys?: string[];
783
+ /**
784
+ * The replacement string to use for redacted values.
785
+ * @default "***REDACTED***"
786
+ */
787
+ replacement?: string;
788
+ /**
789
+ * Enable/disable redaction for incoming messages.
790
+ * @default true
791
+ */
792
+ incoming?: boolean;
793
+ /**
794
+ * Enable/disable redaction for outgoing messages.
795
+ * @default true
796
+ */
797
+ outgoing?: boolean;
798
+ }
799
+ /**
800
+ * Redacts sensitive Personally Identifiable Information (PII) from message payloads.
801
+ *
802
+ * As a Level 4 (Middleware) plugin, this executes directly in the message processing chain.
803
+ * It recursively scans and masks sensitive fields (e.g., `idTag`, `password`) in both
804
+ * incoming and outgoing payloads. Because it mutates the payload inline, the redacted
805
+ * data will be what application handlers, downstream plugins, and observability tools see.
806
+ *
807
+ * @example
808
+ * ```ts
809
+ * server.plugin(piiRedactorPlugin({
810
+ * sensitiveKeys: ['idTag', 'password', 'authorizationKey'],
811
+ * replacement: '[HIDDEN]'
812
+ * }));
813
+ * ```
814
+ */
815
+ declare function piiRedactorPlugin(options?: PiiRedactorOptions): OCPPPlugin;
816
+
817
+ /**
818
+ * Destination for rate-limit alerts.
819
+ */
820
+ interface AlertSink {
821
+ /** Send an alert payload. Returns a promise that resolves on success. */
822
+ send(payload: RateLimitAlert): void | Promise<void>;
823
+ }
824
+ interface RateLimitAlert {
825
+ /** The event type that triggered the alert */
826
+ eventType: "RATE_LIMIT_EXCEEDED" | "CONNECTION_RATE_LIMIT";
827
+ /** Station identity (if known) */
828
+ identity?: string;
829
+ /** Remote IP address */
830
+ ip?: string;
831
+ /** ISO 8601 timestamp of the event */
832
+ timestamp: string;
833
+ /** Number of times the event occurred in the current window */
834
+ count: number;
835
+ /** Window duration in ms */
836
+ windowMs: number;
837
+ }
838
+ interface RateLimitNotifierOptions {
839
+ /**
840
+ * Where to send alerts. Can be a webhook URL (string), a Kafka-like sink
841
+ * object, or any object with a `send(payload)` method.
842
+ *
843
+ * @example Webhook URL
844
+ * ```ts
845
+ * rateLimitNotifierPlugin({ sink: 'https://alerts.example.com/hook' })
846
+ * ```
847
+ *
848
+ * @example Custom sink
849
+ * ```ts
850
+ * rateLimitNotifierPlugin({
851
+ * sink: {
852
+ * send: (alert) => kafka.send({ topic: 'rate-limits', messages: [{ value: JSON.stringify(alert) }] })
853
+ * }
854
+ * })
855
+ * ```
856
+ */
857
+ sink: string | AlertSink;
858
+ /**
859
+ * Minimum interval (ms) between alerts for the same identity.
860
+ * Prevents alert storms.
861
+ * @default 60000 (1 minute)
862
+ */
863
+ cooldownMs?: number;
864
+ /**
865
+ * Number of rate-limit events before an alert is sent.
866
+ * @default 1
867
+ */
868
+ threshold?: number;
869
+ /**
870
+ * Sliding window in ms to count rate limit events.
871
+ * @default 300000 (5 minutes)
872
+ */
873
+ windowMs?: number;
874
+ /**
875
+ * Custom HTTP headers for webhook sink.
876
+ */
877
+ headers?: Record<string, string>;
878
+ /**
879
+ * Optional logger.
880
+ */
881
+ logger?: {
882
+ warn: (...args: unknown[]) => void;
883
+ error: (...args: unknown[]) => void;
884
+ };
885
+ }
886
+ /**
887
+ * Rate Limit Notifier Plugin (Level 1: Passive Hook)
888
+ *
889
+ * Fires alerts to an external sink (webhook URL or custom sink) whenever a
890
+ * client exceeds the server's rate limit. Implements per-identity cooldown
891
+ * and threshold-based alerting to prevent alert storms.
892
+ *
893
+ * @example
894
+ * ```ts
895
+ * server.plugin(rateLimitNotifierPlugin({
896
+ * sink: 'https://slack.example.com/webhook',
897
+ * cooldownMs: 120_000,
898
+ * threshold: 3,
899
+ * }));
900
+ * ```
901
+ */
902
+ declare function rateLimitNotifierPlugin(options: RateLimitNotifierOptions): OCPPPlugin;
903
+
904
+ /**
905
+ * Minimal Redis client contract — compatible with `ioredis` and `node-redis`.
906
+ * Users bring their own Redis dependency; this plugin does not bundle one.
907
+ */
908
+ interface RedisClientLike {
909
+ /** Publish to a Pub/Sub channel. */
910
+ publish(channel: string, message: string): Promise<number> | unknown;
911
+ /** Append to a Redis Stream (optional — only needed for stream mode). */
912
+ xadd?(key: string, ...args: (string | number)[]): Promise<string | null> | unknown;
913
+ /** Graceful disconnect. */
914
+ quit?(): Promise<unknown> | unknown;
915
+ disconnect?(): void;
916
+ }
917
+ type RedisPubSubEvent = "connect" | "disconnect" | "message" | "security" | "auth_failed" | "eviction";
918
+ /**
919
+ * Options for the Redis Pub/Sub plugin.
920
+ */
921
+ interface RedisPubSubPluginOptions {
922
+ /**
923
+ * User-provided Redis client for publishing.
924
+ * Supports ioredis or node-redis compatible clients.
925
+ */
926
+ client: RedisClientLike;
927
+ /**
928
+ * Publishing mode:
929
+ * - `"pubsub"`: Uses PUBLISH to channels (real-time subscribers)
930
+ * - `"stream"`: Uses XADD to Redis Streams (persistent, consumer groups)
931
+ * @default "pubsub"
932
+ */
933
+ mode?: "pubsub" | "stream";
934
+ /**
935
+ * Channel/stream key prefix.
936
+ * @default "ocpp"
937
+ */
938
+ prefix?: string;
939
+ /**
940
+ * Which events to publish.
941
+ * @default ["connect", "disconnect", "message", "security"]
942
+ */
943
+ events?: RedisPubSubEvent[];
944
+ /**
945
+ * For stream mode: max stream length (MAXLEN ~approximate trimming).
946
+ * Older entries are trimmed automatically.
947
+ * @default 10000
948
+ */
949
+ maxStreamLength?: number;
950
+ /**
951
+ * Include full message payloads.
952
+ * @default false
953
+ */
954
+ includePayload?: boolean;
955
+ /**
956
+ * Custom serializer.
957
+ * @default JSON.stringify
958
+ */
959
+ serialize?: (data: Record<string, unknown>) => string;
960
+ /**
961
+ * Optional async worker for non-blocking publishes.
962
+ */
963
+ worker?: AsyncWorkerPlugin;
964
+ }
965
+ /**
966
+ * Publishes OCPP events to Redis Pub/Sub channels or Redis Streams.
967
+ *
968
+ * Ideal for microservice architectures where billing, analytics, and alerting
969
+ * services subscribe to OCPP feeds, or for durable event sourcing via Streams.
970
+ *
971
+ * @example Pub/Sub mode
972
+ * ```ts
973
+ * import Redis from 'ioredis';
974
+ * import { redisPubSubPlugin } from 'ocpp-ws-io/plugins';
975
+ *
976
+ * const redis = new Redis();
977
+ * server.plugin(redisPubSubPlugin({
978
+ * client: redis,
979
+ * mode: 'pubsub',
980
+ * prefix: 'ocpp',
981
+ * events: ['connect', 'disconnect', 'message'],
982
+ * }));
983
+ * // Subscribers: redis.subscribe('ocpp:connect')
984
+ * ```
985
+ *
986
+ * @example Stream mode (durable)
987
+ * ```ts
988
+ * server.plugin(redisPubSubPlugin({
989
+ * client: redis,
990
+ * mode: 'stream',
991
+ * maxStreamLength: 50000,
992
+ * }));
993
+ * // Consumers: redis.xreadgroup('GROUP', 'mygroup', 'consumer1', ...)
994
+ * ```
995
+ */
996
+ declare function redisPubSubPlugin(options: RedisPubSubPluginOptions): OCPPPlugin;
997
+
998
+ interface ReplayRedisLike {
999
+ rpush(key: string, ...values: string[]): Promise<number>;
1000
+ lpop(key: string): Promise<string | null>;
1001
+ }
1002
+ interface ReplayBufferOptions {
1003
+ /** User-provided Redis instance */
1004
+ redis: ReplayRedisLike;
1005
+ /**
1006
+ * Prefix for Redis keys
1007
+ * @default "ocpp:replay:"
1008
+ */
1009
+ prefix?: string;
1010
+ /**
1011
+ * If true, queued messages will return a synthetic response to the caller
1012
+ * immediately, rather than letting the caller timeout or fail.
1013
+ * @default true
1014
+ */
1015
+ syntheticResponse?: boolean;
1016
+ /**
1017
+ * Maximum number of messages to flush concurrently on reconnection.
1018
+ * Prevents overwhelming a freshly-connected client.
1019
+ * @default 5
1020
+ */
1021
+ flushConcurrency?: number;
1022
+ /**
1023
+ * Delay in ms between each flush batch to avoid overwhelming the client.
1024
+ * @default 200
1025
+ */
1026
+ flushDelayMs?: number;
1027
+ /**
1028
+ * Optional logger. Falls back to silent no-op if not provided.
1029
+ */
1030
+ logger?: {
1031
+ warn: (...args: unknown[]) => void;
1032
+ error: (...args: unknown[]) => void;
1033
+ };
1034
+ }
1035
+ /**
1036
+ * Replay Buffer Plugin (Level 3: Interceptor)
1037
+ *
1038
+ * Provides a persistent, distributed offline queue for backend-initiated
1039
+ * commands (like RemoteStopTransaction or UnlockConnector).
1040
+ *
1041
+ * If a call is made to an offline client, this plugin intercepts the error,
1042
+ * queues the message in Redis, and automatically flushes the queue when the
1043
+ * client reconnects (even if it reconnects to a different server node).
1044
+ *
1045
+ * @example
1046
+ * ```ts
1047
+ * server.plugin(replayBufferPlugin({
1048
+ * redis,
1049
+ * flushConcurrency: 3,
1050
+ * logger: pino(),
1051
+ * }));
1052
+ * ```
1053
+ */
1054
+ declare function replayBufferPlugin(options: ReplayBufferOptions): OCPPPlugin;
1055
+
1056
+ /**
1057
+ * A transformation rule for a specific OCPP method/action.
1058
+ */
1059
+ interface TransformRule {
1060
+ /**
1061
+ * The method name to match (e.g., "BootNotification", "StatusNotification").
1062
+ * Supports exact match or wildcard "*" for all methods.
1063
+ */
1064
+ method: string;
1065
+ /**
1066
+ * Transform function for the payload.
1067
+ * Receives the current payload and source/target versions.
1068
+ * Must return the transformed payload.
1069
+ */
1070
+ transform: (payload: Record<string, unknown>, direction: "up" | "down") => Record<string, unknown>;
1071
+ }
1072
+ interface SchemaVersioningOptions {
1073
+ /**
1074
+ * Source OCPP version identifier (e.g., "ocpp1.6" or "ocpp2.0.1").
1075
+ * This is the version used by the charging station (client).
1076
+ */
1077
+ sourceVersion: string;
1078
+ /**
1079
+ * Target OCPP version identifier (e.g., "ocpp2.0.1" or "ocpp1.6").
1080
+ * This is the version the application handlers expect.
1081
+ */
1082
+ targetVersion: string;
1083
+ /**
1084
+ * Array of transformation rules for specific OCPP actions.
1085
+ * Rules are checked in order; the first matching rule is applied.
1086
+ */
1087
+ rules: TransformRule[];
1088
+ /**
1089
+ * How to handle methods without a transform rule:
1090
+ * - "passthrough": Forward as-is
1091
+ * - "reject": Drop the message
1092
+ * @default "passthrough"
1093
+ */
1094
+ unmatchedBehavior?: "passthrough" | "reject";
1095
+ /**
1096
+ * Optional: Only apply transformations when client protocol matches this version.
1097
+ * If not set, applies to all clients.
1098
+ */
1099
+ applyWhen?: string;
1100
+ /**
1101
+ * Optional logger.
1102
+ */
1103
+ logger?: {
1104
+ warn: (...args: unknown[]) => void;
1105
+ debug?: (...args: unknown[]) => void;
1106
+ };
1107
+ }
1108
+ /**
1109
+ * Schema Versioning Plugin (Level 4: Middleware)
1110
+ *
1111
+ * Provides OCPP version transformation between 1.6 and 2.0.1 payloads.
1112
+ * This plugin sits in the middleware chain and transforms message payloads
1113
+ * between protocol versions, enabling a server to support mixed-version
1114
+ * charging station fleets with unified application handlers.
1115
+ *
1116
+ * **Architecture:**
1117
+ * ```
1118
+ * Station (1.6) ──→ [Transform to 2.0.1] ──→ Application Handler
1119
+ * Station (1.6) ←── [Transform to 1.6] ←── Application Handler
1120
+ * ```
1121
+ *
1122
+ * @example
1123
+ * ```ts
1124
+ * import { schemaVersioningPlugin } from 'ocpp-ws-io/plugins';
1125
+ *
1126
+ * server.plugin(schemaVersioningPlugin({
1127
+ * sourceVersion: 'ocpp1.6',
1128
+ * targetVersion: 'ocpp2.0.1',
1129
+ * rules: [
1130
+ * {
1131
+ * method: 'BootNotification',
1132
+ * transform: (payload, direction) => {
1133
+ * if (direction === 'up') {
1134
+ * // 1.6 → 2.0.1: Wrap in chargingStation object
1135
+ * return {
1136
+ * chargingStation: {
1137
+ * model: payload.chargePointModel,
1138
+ * vendorName: payload.chargePointVendor,
1139
+ * serialNumber: payload.chargePointSerialNumber,
1140
+ * firmwareVersion: payload.firmwareVersion,
1141
+ * },
1142
+ * reason: 'PowerUp',
1143
+ * };
1144
+ * }
1145
+ * // 2.0.1 → 1.6: Flatten chargingStation
1146
+ * const cs = payload.chargingStation as Record<string, unknown> ?? {};
1147
+ * return {
1148
+ * chargePointModel: cs.model,
1149
+ * chargePointVendor: cs.vendorName,
1150
+ * chargePointSerialNumber: cs.serialNumber,
1151
+ * firmwareVersion: cs.firmwareVersion,
1152
+ * };
1153
+ * },
1154
+ * },
1155
+ * ],
1156
+ * }));
1157
+ * ```
1158
+ */
1159
+ declare function schemaVersioningPlugin(options: SchemaVersioningOptions): OCPPPlugin;
1160
+
185
1161
  /**
186
1162
  * Options for the session log plugin.
187
1163
  */
188
1164
  interface SessionLogOptions {
189
1165
  /**
190
- * Custom logger instance. Defaults to `console`.
191
- * Must have `info` method.
1166
+ * Logger instance. Must have at least `info` and `warn`.
1167
+ * @default console
192
1168
  */
193
1169
  logger?: {
194
- info: (msg: string, meta?: Record<string, unknown>) => void;
1170
+ info: (...args: unknown[]) => void;
1171
+ warn: (...args: unknown[]) => void;
1172
+ error?: (...args: unknown[]) => void;
195
1173
  };
1174
+ /**
1175
+ * Logging verbosity level:
1176
+ * - `"minimal"`: connect/disconnect only
1177
+ * - `"standard"`: + errors, auth failures, evictions
1178
+ * - `"verbose"`: + bad messages, security events
1179
+ * @default "standard"
1180
+ */
1181
+ logLevel?: "minimal" | "standard" | "verbose";
196
1182
  }
197
1183
  /**
198
- * Logs connect/disconnect events with identity, IP, protocol, and connection duration.
1184
+ * Structured session lifecycle logger.
1185
+ *
1186
+ * Logs connection events, errors, auth failures, and evictions at
1187
+ * configurable verbosity levels.
199
1188
  *
200
1189
  * @example
201
1190
  * ```ts
202
1191
  * import { sessionLogPlugin } from 'ocpp-ws-io/plugins';
203
1192
  *
204
- * server.plugin(sessionLogPlugin());
205
- * // => Connected: CP-101 from 192.168.1.1 via ocpp1.6
206
- * // => Disconnected: CP-101 after 3600s (code: 1000)
1193
+ * server.plugin(sessionLogPlugin({
1194
+ * logLevel: 'verbose',
1195
+ * logger: pino(),
1196
+ * }));
207
1197
  * ```
208
1198
  */
209
1199
  declare function sessionLogPlugin(options?: SessionLogOptions): OCPPPlugin;
210
1200
 
1201
+ type WebhookEvent = "init" | "connect" | "disconnect" | "close" | "security" | "auth_failed" | "eviction" | "closing";
211
1202
  /**
212
1203
  * Options for the webhook plugin.
213
1204
  */
214
1205
  interface WebhookPluginOptions {
215
1206
  /** Webhook HTTP endpoint URL. */
216
1207
  url: string;
217
- /** Which lifecycle events to send (default: all). */
218
- events?: Array<"init" | "connect" | "disconnect" | "close">;
1208
+ /** Which lifecycle events to send (default: lifecycle events only). */
1209
+ events?: WebhookEvent[];
219
1210
  /** Custom HTTP headers to include (e.g. Authorization). */
220
1211
  headers?: Record<string, string>;
221
1212
  /** HMAC-SHA256 secret for signing payloads (sent as `X-Signature` header). */
@@ -226,7 +1217,7 @@ interface WebhookPluginOptions {
226
1217
  retries?: number;
227
1218
  }
228
1219
  /**
229
- * Sends HTTP POST webhooks on server lifecycle events.
1220
+ * Sends HTTP POST webhooks on server lifecycle and security events.
230
1221
  * Uses Node.js built-in `fetch` (Node 18+).
231
1222
  *
232
1223
  * @example
@@ -236,11 +1227,11 @@ interface WebhookPluginOptions {
236
1227
  * server.plugin(webhookPlugin({
237
1228
  * url: 'https://api.example.com/ocpp-events',
238
1229
  * secret: process.env.WEBHOOK_SECRET,
239
- * events: ['connect', 'disconnect'],
1230
+ * events: ['connect', 'disconnect', 'security', 'auth_failed'],
240
1231
  * headers: { Authorization: 'Bearer token123' },
241
1232
  * }));
242
1233
  * ```
243
1234
  */
244
1235
  declare function webhookPlugin(options: WebhookPluginOptions): OCPPPlugin;
245
1236
 
246
- export { type AnomalyPluginOptions, type ConnectionGuardOptions, type MetricsPlugin, type MetricsPluginOptions, type MetricsSnapshot, type OtelPluginOptions, type SessionLogOptions, type WebhookPluginOptions, anomalyPlugin, connectionGuardPlugin, heartbeatPlugin, metricsPlugin, otelPlugin, sessionLogPlugin, webhookPlugin };
1237
+ export { type AlertSink, type AmqpChannelLike, type AmqpPluginOptions, type AnomalyPluginOptions, type AsyncWorkerOptions, type AsyncWorkerPlugin, type CircuitBreakerOptions, type CircuitState, type ConnectionGuardOptions, type DedupRedisLike, type KafkaPluginOptions, type KafkaProducerLike, type MessageDedupOptions, type MetricsPlugin, type MetricsPluginOptions, type MetricsSnapshot, type MqttClientLike, type MqttPluginOptions, type OtelPluginOptions, type PiiRedactorOptions, type RateLimitAlert, type RateLimitNotifierOptions, type RedisClientLike, type RedisPubSubPluginOptions, type ReplayBufferOptions, type ReplayRedisLike, type SchemaVersioningOptions, type SessionLogOptions, type TransformRule, type WebhookPluginOptions, amqpPlugin, anomalyPlugin, asyncWorkerPlugin, circuitBreakerPlugin, connectionGuardPlugin, heartbeatPlugin, kafkaPlugin, messageDedupPlugin, metricsPlugin, mqttPlugin, otelPlugin, piiRedactorPlugin, rateLimitNotifierPlugin, redisPubSubPlugin, replayBufferPlugin, schemaVersioningPlugin, sessionLogPlugin, webhookPlugin };