ocpp-ws-io 2.2.0 → 2.2.2-beta.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/README.md +29 -0
- package/dist/adapters/redis.d.mts +2 -2
- package/dist/adapters/redis.d.ts +2 -2
- package/dist/adapters/redis.js +1 -1
- package/dist/adapters/redis.mjs +1 -1
- package/dist/browser.js +1 -1
- package/dist/browser.mjs +1 -1
- package/dist/context-Cy7YIKyU.d.mts +21 -0
- package/dist/context-DcTIzhq-.d.ts +21 -0
- package/dist/express.d.mts +70 -0
- package/dist/express.d.ts +70 -0
- package/dist/express.js +2 -0
- package/dist/express.mjs +2 -0
- package/dist/fastify.d.mts +37 -0
- package/dist/fastify.d.ts +37 -0
- package/dist/fastify.js +2 -0
- package/dist/fastify.mjs +2 -0
- package/dist/hono.d.mts +51 -0
- package/dist/hono.d.ts +51 -0
- package/dist/hono.js +2 -0
- package/dist/hono.mjs +2 -0
- package/dist/{index-B98n5Et3.d.mts → index-B9rTwvbn.d.mts} +1 -1
- package/dist/{index-xx7uU8pY.d.ts → index-D5pJ3wS4.d.ts} +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +7 -7
- package/dist/index.mjs +7 -7
- package/dist/nestjs.d.mts +169 -0
- package/dist/nestjs.d.ts +169 -0
- package/dist/nestjs.js +4826 -0
- package/dist/nestjs.mjs +4826 -0
- package/dist/plugins.d.mts +1017 -26
- package/dist/plugins.d.ts +1017 -26
- package/dist/plugins.js +1 -1
- package/dist/plugins.mjs +1 -1
- package/dist/{types-BunMs45p.d.mts → types-xFfIgIuS.d.mts} +108 -3
- package/dist/{types-BunMs45p.d.ts → types-xFfIgIuS.d.ts} +108 -3
- package/package.json +174 -113
- package/dist/browser.d.mts +0 -4971
- package/dist/browser.d.ts +0 -4971
package/dist/plugins.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { j as OCPPPlugin } from './types-xFfIgIuS.mjs';
|
|
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
|
-
*
|
|
188
|
+
* @default 5
|
|
19
189
|
*/
|
|
20
190
|
reconnectThreshold?: number;
|
|
21
191
|
/**
|
|
22
192
|
* Sliding window duration in milliseconds.
|
|
23
|
-
*
|
|
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
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
59
|
-
*
|
|
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({
|
|
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,
|
|
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
|
|
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
|
-
*
|
|
191
|
-
*
|
|
1166
|
+
* Logger instance. Must have at least `info` and `warn`.
|
|
1167
|
+
* @default console
|
|
192
1168
|
*/
|
|
193
1169
|
logger?: {
|
|
194
|
-
info: (
|
|
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
|
-
*
|
|
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
|
-
*
|
|
206
|
-
*
|
|
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:
|
|
218
|
-
events?:
|
|
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 };
|