ocpp-ws-io 2.2.2 → 2.3.0
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 +4 -4
- 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-DcTIzhq-.d.ts → context-B4-L-O1-.d.ts} +1 -1
- package/dist/{context-Cy7YIKyU.d.mts → context-BNELI5Ux.d.mts} +1 -1
- package/dist/express.d.mts +1 -1
- package/dist/express.d.ts +1 -1
- package/dist/fastify.d.mts +2 -2
- package/dist/fastify.d.ts +2 -2
- package/dist/hono.d.mts +2 -2
- package/dist/hono.d.ts +2 -2
- package/dist/{index-D5pJ3wS4.d.ts → index-CEMhGOxh.d.ts} +16 -6
- package/dist/{index-B9rTwvbn.d.mts → index-Gz98XqQ7.d.mts} +16 -6
- package/dist/index.d.mts +9 -11
- package/dist/index.d.ts +9 -11
- package/dist/index.js +6 -6
- package/dist/index.mjs +6 -6
- package/dist/nestjs.d.mts +1 -1
- package/dist/nestjs.d.ts +1 -1
- package/dist/nestjs.js +6 -6
- package/dist/nestjs.mjs +6 -6
- package/dist/parse-worker.cjs +91 -0
- package/dist/plugins.d.mts +43 -7
- package/dist/plugins.d.ts +43 -7
- package/dist/plugins.js +1 -1
- package/dist/plugins.mjs +1 -1
- package/dist/{types-xFfIgIuS.d.mts → types-tTYOr5Gm.d.mts} +139 -24
- package/dist/{types-xFfIgIuS.d.ts → types-tTYOr5Gm.d.ts} +139 -24
- package/package.json +2 -2
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Worker Entry Point for JSON Parse + Optional AJV ───────────
|
|
3
|
+
// Runs in a worker_threads context. Receives raw message data
|
|
4
|
+
// (string or Uint8Array — Buffers arrive as Uint8Array after the
|
|
5
|
+
// structured clone), parses it, and optionally validates with AJV.
|
|
6
|
+
|
|
7
|
+
const { parentPort } = require("node:worker_threads");
|
|
8
|
+
|
|
9
|
+
if (!parentPort) {
|
|
10
|
+
throw new Error("parse-worker must be run inside a worker thread");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Mirror src/validator.ts: rewrite OCPP 2.1 `urn:<Method>Request/Response`
|
|
14
|
+
// ids to the `.req`/`.conf` convention, then `urn:` -> `urn/` because
|
|
15
|
+
// AJV's fast-uri resolver rejects single-colon URNs.
|
|
16
|
+
function normalizeSchemaId(id) {
|
|
17
|
+
const m = /^urn:(.+?)(Request|Response)$/.exec(id);
|
|
18
|
+
let out = m ? `urn:${m[1]}.${m[2] === "Request" ? "req" : "conf"}` : id;
|
|
19
|
+
if (out.startsWith("urn:")) out = out.replace("urn:", "urn/");
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Lazy-loaded AJV instance for validation in the worker
|
|
24
|
+
let ajv = null;
|
|
25
|
+
const compiledSchemas = new Map();
|
|
26
|
+
|
|
27
|
+
function getOrCompileSchema(schemaId, schemas) {
|
|
28
|
+
const normalizedId = normalizeSchemaId(schemaId);
|
|
29
|
+
const cached = compiledSchemas.get(normalizedId);
|
|
30
|
+
if (cached) return cached;
|
|
31
|
+
|
|
32
|
+
if (!ajv) {
|
|
33
|
+
try {
|
|
34
|
+
const Ajv = require("ajv").default;
|
|
35
|
+
const addFormats = require("ajv-formats").default;
|
|
36
|
+
ajv = new Ajv({ allErrors: true, strict: false });
|
|
37
|
+
addFormats(ajv);
|
|
38
|
+
for (const [id, schema] of Object.entries(schemas)) {
|
|
39
|
+
try {
|
|
40
|
+
ajv.addSchema({ ...schema, $id: undefined }, normalizeSchemaId(id));
|
|
41
|
+
} catch {
|
|
42
|
+
// Ignore duplicate schema errors
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
return null; // AJV not available
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const validate = ajv.getSchema(normalizedId);
|
|
52
|
+
if (validate) {
|
|
53
|
+
compiledSchemas.set(normalizedId, validate);
|
|
54
|
+
return validate;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Schema not found
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
parentPort.on("message", (request) => {
|
|
63
|
+
const { id, buffer, schemaInfo } = request;
|
|
64
|
+
try {
|
|
65
|
+
// Buffers are cloned as Uint8Array across postMessage — decode to utf8
|
|
66
|
+
// text before parsing (JSON.parse on a Uint8Array would throw).
|
|
67
|
+
const text =
|
|
68
|
+
typeof buffer === "string" ? buffer : Buffer.from(buffer).toString("utf8");
|
|
69
|
+
const message = JSON.parse(text);
|
|
70
|
+
|
|
71
|
+
let validationError;
|
|
72
|
+
if (schemaInfo && Array.isArray(message) && message[0] === 2) {
|
|
73
|
+
const method = message[2];
|
|
74
|
+
const schemaId = `urn:${method}.req`;
|
|
75
|
+
const validate = getOrCompileSchema(schemaId, schemaInfo.schemas);
|
|
76
|
+
if (validate) {
|
|
77
|
+
const valid = validate(message[3]);
|
|
78
|
+
if (!valid) {
|
|
79
|
+
validationError = {
|
|
80
|
+
schemaId,
|
|
81
|
+
errors: JSON.stringify(validate.errors),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
parentPort.postMessage({ id, message, validationError });
|
|
88
|
+
} catch (err) {
|
|
89
|
+
parentPort.postMessage({ id, error: err.message });
|
|
90
|
+
}
|
|
91
|
+
});
|
package/dist/plugins.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { j as OCPPPlugin } from './types-
|
|
1
|
+
import { j as OCPPPlugin } from './types-tTYOr5Gm.mjs';
|
|
2
2
|
import 'ws';
|
|
3
3
|
import 'node:https';
|
|
4
4
|
import 'node:http';
|
|
@@ -242,6 +242,12 @@ declare function anomalyPlugin(options?: AnomalyPluginOptions): OCPPPlugin;
|
|
|
242
242
|
* Options for the circuit-breaker plugin.
|
|
243
243
|
*/
|
|
244
244
|
interface CircuitBreakerOptions {
|
|
245
|
+
/**
|
|
246
|
+
* Maximum number of distinct client identities to track circuit state for.
|
|
247
|
+
* Bounds memory under identity churn / random-identity floods (LRU eviction).
|
|
248
|
+
* @default 10000
|
|
249
|
+
*/
|
|
250
|
+
maxTrackedClients?: number;
|
|
245
251
|
/**
|
|
246
252
|
* Number of consecutive failures before the circuit opens.
|
|
247
253
|
* @default 5
|
|
@@ -345,6 +351,11 @@ interface ConnectionGuardOptions {
|
|
|
345
351
|
* Optionally reclaims slots from dead peers (pong timeout) and
|
|
346
352
|
* slow consumers (backpressure).
|
|
347
353
|
*
|
|
354
|
+
* NOTE: prefer `new OCPPServer({ maxConnections })` for the hard cap — it
|
|
355
|
+
* rejects at upgrade time, before TLS/auth work. This plugin's cap closes
|
|
356
|
+
* connections only after they complete the handshake; its main value is the
|
|
357
|
+
* pong-timeout / backpressure slot-reclaim options.
|
|
358
|
+
*
|
|
348
359
|
* @example
|
|
349
360
|
* ```ts
|
|
350
361
|
* import { connectionGuardPlugin } from 'ocpp-ws-io/plugins';
|
|
@@ -465,6 +476,11 @@ interface DedupRedisLike {
|
|
|
465
476
|
* **node-redis v4 style:** `set(key, value, { PX: ms, NX: true })` → `Promise<string | null>`
|
|
466
477
|
*/
|
|
467
478
|
set(key: string, value: string, ...args: unknown[]): Promise<"OK" | string | null> | ("OK" | string | null);
|
|
479
|
+
/**
|
|
480
|
+
* Fetch a cached value (used to replay responses for duplicate CALLs).
|
|
481
|
+
* Optional — without it duplicates are silently dropped.
|
|
482
|
+
*/
|
|
483
|
+
get?(key: string): Promise<string | null> | (string | null);
|
|
468
484
|
}
|
|
469
485
|
interface MessageDedupOptions {
|
|
470
486
|
/**
|
|
@@ -775,11 +791,19 @@ declare function otelPlugin(options?: OtelPluginOptions): OCPPPlugin;
|
|
|
775
791
|
|
|
776
792
|
interface PiiRedactorOptions {
|
|
777
793
|
/**
|
|
778
|
-
* List of object keys
|
|
779
|
-
*
|
|
780
|
-
*
|
|
794
|
+
* **Required.** List of object keys to redact (matched recursively, at any
|
|
795
|
+
* depth). There is no default — you must explicitly list every key to redact,
|
|
796
|
+
* so nothing is ever scrubbed by accident.
|
|
797
|
+
*
|
|
798
|
+
* ⚠️ **Redaction mutates the live payload, not just logs.** If you list a key
|
|
799
|
+
* here that your handlers need on an **incoming** message (e.g. `idTag` for
|
|
800
|
+
* `Authorize` / `StartTransaction`), the handler will receive the redacted
|
|
801
|
+
* placeholder and cannot use the real value. Either set `incoming: false`, or
|
|
802
|
+
* don't include such keys, when you need the value at the handler.
|
|
803
|
+
*
|
|
804
|
+
* @example ["password", "authorizationKey", "token"]
|
|
781
805
|
*/
|
|
782
|
-
sensitiveKeys
|
|
806
|
+
sensitiveKeys: string[];
|
|
783
807
|
/**
|
|
784
808
|
* The replacement string to use for redacted values.
|
|
785
809
|
* @default "***REDACTED***"
|
|
@@ -804,6 +828,12 @@ interface PiiRedactorOptions {
|
|
|
804
828
|
* incoming and outgoing payloads. Because it mutates the payload inline, the redacted
|
|
805
829
|
* data will be what application handlers, downstream plugins, and observability tools see.
|
|
806
830
|
*
|
|
831
|
+
* ⚠️ **Caveat:** because it mutates the live payload (not a logging copy), redacting a
|
|
832
|
+
* key on **incoming** messages also hides it from your handlers. The default
|
|
833
|
+
* `sensitiveKeys` includes `idTag` — if your handlers authorize by `idTag`
|
|
834
|
+
* (`Authorize`, `StartTransaction`), either pass `incoming: false` or drop `idTag`
|
|
835
|
+
* from `sensitiveKeys` so the handler still receives the real value.
|
|
836
|
+
*
|
|
807
837
|
* @example
|
|
808
838
|
* ```ts
|
|
809
839
|
* server.plugin(piiRedactorPlugin({
|
|
@@ -812,7 +842,7 @@ interface PiiRedactorOptions {
|
|
|
812
842
|
* }));
|
|
813
843
|
* ```
|
|
814
844
|
*/
|
|
815
|
-
declare function piiRedactorPlugin(options
|
|
845
|
+
declare function piiRedactorPlugin(options: PiiRedactorOptions): OCPPPlugin;
|
|
816
846
|
|
|
817
847
|
/**
|
|
818
848
|
* Destination for rate-limit alerts.
|
|
@@ -871,6 +901,12 @@ interface RateLimitNotifierOptions {
|
|
|
871
901
|
* @default 300000 (5 minutes)
|
|
872
902
|
*/
|
|
873
903
|
windowMs?: number;
|
|
904
|
+
/**
|
|
905
|
+
* Maximum number of distinct identities/IPs to track alert state for.
|
|
906
|
+
* Bounds memory under identity/IP churn (LRU eviction of the oldest keys).
|
|
907
|
+
* @default 10000
|
|
908
|
+
*/
|
|
909
|
+
maxTrackedKeys?: number;
|
|
874
910
|
/**
|
|
875
911
|
* Custom HTTP headers for webhook sink.
|
|
876
912
|
*/
|
|
@@ -914,7 +950,7 @@ interface RedisClientLike {
|
|
|
914
950
|
quit?(): Promise<unknown> | unknown;
|
|
915
951
|
disconnect?(): void;
|
|
916
952
|
}
|
|
917
|
-
type RedisPubSubEvent = "connect" | "disconnect" | "message" | "security" | "auth_failed" | "eviction";
|
|
953
|
+
type RedisPubSubEvent = "connect" | "disconnect" | "message" | "security" | "auth_failed" | "eviction" | "closing";
|
|
918
954
|
/**
|
|
919
955
|
* Options for the Redis Pub/Sub plugin.
|
|
920
956
|
*/
|
package/dist/plugins.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { j as OCPPPlugin } from './types-
|
|
1
|
+
import { j as OCPPPlugin } from './types-tTYOr5Gm.js';
|
|
2
2
|
import 'ws';
|
|
3
3
|
import 'node:https';
|
|
4
4
|
import 'node:http';
|
|
@@ -242,6 +242,12 @@ declare function anomalyPlugin(options?: AnomalyPluginOptions): OCPPPlugin;
|
|
|
242
242
|
* Options for the circuit-breaker plugin.
|
|
243
243
|
*/
|
|
244
244
|
interface CircuitBreakerOptions {
|
|
245
|
+
/**
|
|
246
|
+
* Maximum number of distinct client identities to track circuit state for.
|
|
247
|
+
* Bounds memory under identity churn / random-identity floods (LRU eviction).
|
|
248
|
+
* @default 10000
|
|
249
|
+
*/
|
|
250
|
+
maxTrackedClients?: number;
|
|
245
251
|
/**
|
|
246
252
|
* Number of consecutive failures before the circuit opens.
|
|
247
253
|
* @default 5
|
|
@@ -345,6 +351,11 @@ interface ConnectionGuardOptions {
|
|
|
345
351
|
* Optionally reclaims slots from dead peers (pong timeout) and
|
|
346
352
|
* slow consumers (backpressure).
|
|
347
353
|
*
|
|
354
|
+
* NOTE: prefer `new OCPPServer({ maxConnections })` for the hard cap — it
|
|
355
|
+
* rejects at upgrade time, before TLS/auth work. This plugin's cap closes
|
|
356
|
+
* connections only after they complete the handshake; its main value is the
|
|
357
|
+
* pong-timeout / backpressure slot-reclaim options.
|
|
358
|
+
*
|
|
348
359
|
* @example
|
|
349
360
|
* ```ts
|
|
350
361
|
* import { connectionGuardPlugin } from 'ocpp-ws-io/plugins';
|
|
@@ -465,6 +476,11 @@ interface DedupRedisLike {
|
|
|
465
476
|
* **node-redis v4 style:** `set(key, value, { PX: ms, NX: true })` → `Promise<string | null>`
|
|
466
477
|
*/
|
|
467
478
|
set(key: string, value: string, ...args: unknown[]): Promise<"OK" | string | null> | ("OK" | string | null);
|
|
479
|
+
/**
|
|
480
|
+
* Fetch a cached value (used to replay responses for duplicate CALLs).
|
|
481
|
+
* Optional — without it duplicates are silently dropped.
|
|
482
|
+
*/
|
|
483
|
+
get?(key: string): Promise<string | null> | (string | null);
|
|
468
484
|
}
|
|
469
485
|
interface MessageDedupOptions {
|
|
470
486
|
/**
|
|
@@ -775,11 +791,19 @@ declare function otelPlugin(options?: OtelPluginOptions): OCPPPlugin;
|
|
|
775
791
|
|
|
776
792
|
interface PiiRedactorOptions {
|
|
777
793
|
/**
|
|
778
|
-
* List of object keys
|
|
779
|
-
*
|
|
780
|
-
*
|
|
794
|
+
* **Required.** List of object keys to redact (matched recursively, at any
|
|
795
|
+
* depth). There is no default — you must explicitly list every key to redact,
|
|
796
|
+
* so nothing is ever scrubbed by accident.
|
|
797
|
+
*
|
|
798
|
+
* ⚠️ **Redaction mutates the live payload, not just logs.** If you list a key
|
|
799
|
+
* here that your handlers need on an **incoming** message (e.g. `idTag` for
|
|
800
|
+
* `Authorize` / `StartTransaction`), the handler will receive the redacted
|
|
801
|
+
* placeholder and cannot use the real value. Either set `incoming: false`, or
|
|
802
|
+
* don't include such keys, when you need the value at the handler.
|
|
803
|
+
*
|
|
804
|
+
* @example ["password", "authorizationKey", "token"]
|
|
781
805
|
*/
|
|
782
|
-
sensitiveKeys
|
|
806
|
+
sensitiveKeys: string[];
|
|
783
807
|
/**
|
|
784
808
|
* The replacement string to use for redacted values.
|
|
785
809
|
* @default "***REDACTED***"
|
|
@@ -804,6 +828,12 @@ interface PiiRedactorOptions {
|
|
|
804
828
|
* incoming and outgoing payloads. Because it mutates the payload inline, the redacted
|
|
805
829
|
* data will be what application handlers, downstream plugins, and observability tools see.
|
|
806
830
|
*
|
|
831
|
+
* ⚠️ **Caveat:** because it mutates the live payload (not a logging copy), redacting a
|
|
832
|
+
* key on **incoming** messages also hides it from your handlers. The default
|
|
833
|
+
* `sensitiveKeys` includes `idTag` — if your handlers authorize by `idTag`
|
|
834
|
+
* (`Authorize`, `StartTransaction`), either pass `incoming: false` or drop `idTag`
|
|
835
|
+
* from `sensitiveKeys` so the handler still receives the real value.
|
|
836
|
+
*
|
|
807
837
|
* @example
|
|
808
838
|
* ```ts
|
|
809
839
|
* server.plugin(piiRedactorPlugin({
|
|
@@ -812,7 +842,7 @@ interface PiiRedactorOptions {
|
|
|
812
842
|
* }));
|
|
813
843
|
* ```
|
|
814
844
|
*/
|
|
815
|
-
declare function piiRedactorPlugin(options
|
|
845
|
+
declare function piiRedactorPlugin(options: PiiRedactorOptions): OCPPPlugin;
|
|
816
846
|
|
|
817
847
|
/**
|
|
818
848
|
* Destination for rate-limit alerts.
|
|
@@ -871,6 +901,12 @@ interface RateLimitNotifierOptions {
|
|
|
871
901
|
* @default 300000 (5 minutes)
|
|
872
902
|
*/
|
|
873
903
|
windowMs?: number;
|
|
904
|
+
/**
|
|
905
|
+
* Maximum number of distinct identities/IPs to track alert state for.
|
|
906
|
+
* Bounds memory under identity/IP churn (LRU eviction of the oldest keys).
|
|
907
|
+
* @default 10000
|
|
908
|
+
*/
|
|
909
|
+
maxTrackedKeys?: number;
|
|
874
910
|
/**
|
|
875
911
|
* Custom HTTP headers for webhook sink.
|
|
876
912
|
*/
|
|
@@ -914,7 +950,7 @@ interface RedisClientLike {
|
|
|
914
950
|
quit?(): Promise<unknown> | unknown;
|
|
915
951
|
disconnect?(): void;
|
|
916
952
|
}
|
|
917
|
-
type RedisPubSubEvent = "connect" | "disconnect" | "message" | "security" | "auth_failed" | "eviction";
|
|
953
|
+
type RedisPubSubEvent = "connect" | "disconnect" | "message" | "security" | "auth_failed" | "eviction" | "closing";
|
|
918
954
|
/**
|
|
919
955
|
* Options for the Redis Pub/Sub plugin.
|
|
920
956
|
*/
|
package/dist/plugins.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
'use strict';var crypto=require('crypto');function T(r){let u=r.exchange??"ocpp.events",d=r.routingKey??"ocpp.{event}.{identity}",c=new Set(r.events??["connect","disconnect","message","security"]),a={persistent:r.publishOptions?.persistent??true,contentType:r.publishOptions?.contentType??"application/json",...r.publishOptions?.priority!==void 0&&{priority:r.publishOptions.priority}},s=new Map;function n(t,i){return d.replace("{event}",t).replace("{identity}",i??"server")}function e(t,i,o){if(!c.has(t))return;let l=n(t,i),m=Buffer.from(JSON.stringify(o));if(r.worker)r.worker.enqueue("amqp-publish",async()=>{r.channel.publish(u,l,m,a);});else try{r.channel.publish(u,l,m,a);}catch{}}return {name:"amqp",onConnection(t){s.set(t.identity,Date.now()),e("connect",t.identity,{identity:t.identity,ip:t.handshake.remoteAddress,protocol:t.protocol,timestamp:new Date().toISOString()});},onDisconnect(t,i,o){let l=s.get(t.identity),m=l?Math.round((Date.now()-l)/1e3):0;s.delete(t.identity),e("disconnect",t.identity,{identity:t.identity,code:i,reason:o,durationSec:m,timestamp:new Date().toISOString()});},onMessage(t,i){let o={identity:t.identity,direction:i.direction,messageType:i.message[0],timestamp:i.ctx.timestamp};i.message[0]===2&&i.message[2]&&(o.method=i.message[2]),i.ctx.latencyMs!==void 0&&(o.latencyMs=i.ctx.latencyMs),r.includePayload&&(o.payload=i.message),e(`message.${i.direction}`,t.identity,o);},onSecurityEvent(t){e("security",t.identity,{type:t.type,identity:t.identity,ip:t.ip,timestamp:t.timestamp,details:t.details});},onAuthFailed(t,i,o){e("auth_failed",t.identity,{identity:t.identity,ip:t.remoteAddress,code:i,reason:o,timestamp:new Date().toISOString()});},onEviction(t,i){e("eviction",t.identity,{identity:t.identity,evictedBy:i.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){e("closing",void 0,{timestamp:new Date().toISOString()});},onClose(){s.clear();try{r.channel.close();}catch{}}}}function R(r){let u=r?.reconnectThreshold??5,d=r?.authFailureThreshold??5,c=r?.badMessageThreshold??10,a=r?.evictionThreshold??3,s=r?.windowMs??6e4,n=new Map,e=new Map,t=new Map,i=new Map,o=null,l=null;function m(p,f){let w=f-s,P=0;for(;P<p.length&&p[P]<w;)P++;return P>0?p.slice(P):p}function g(p,f){for(let[w,P]of p){let _=m(P,f);_.length===0?p.delete(w):p.set(w,_);}}function y(p,f,w,P,_){let k=Date.now(),b=p.get(f)??[];if(b=m(b,k),b.push(k),p.set(f,b),b.length>w&&o){let v={type:P,identity:_.identity,ip:_.ip??_.evictedIp,timestamp:new Date().toISOString(),details:{..._,countInWindow:b.length,threshold:w,windowMs:s}};o.emit("securityEvent",v);}}return {name:"anomaly",onInit(p){o=p,l=setInterval(()=>{let f=Date.now();g(n,f),g(e,f),g(t,f),g(i,f);},s).unref();},onConnection(p){y(n,p.identity,u,"ANOMALY_RAPID_RECONNECT",{identity:p.identity,ip:p.handshake.remoteAddress});},onAuthFailed(p,f,w){y(e,p.remoteAddress,d,"ANOMALY_AUTH_BRUTE_FORCE",{ip:p.remoteAddress,identity:p.identity,code:f,reason:w});},onBadMessage(p){y(t,p.identity,c,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress});},onValidationFailure(p){y(t,p.identity,c,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress,source:"validation_failure"});},onEviction(p,f){y(i,p.identity,a,"ANOMALY_IDENTITY_COLLISION",{identity:p.identity,evictedIp:p.handshake.remoteAddress,newIp:f.handshake.remoteAddress});},onClose(){l&&(clearInterval(l),l=null),n.clear(),e.clear(),t.clear(),i.clear(),o=null;}}}function x(r){let u=r?.concurrency??10,d=r?.maxQueueSize??1e3,c=r?.overflowStrategy??"drop-oldest",a=r?.drainTimeoutMs??5e3,s=[],n=0,e=0,t=true,i=null;function o(){for(;n<u&&s.length>0;){let g=s.shift();n++,g.fn().catch(y=>{if(r?.onError)try{r.onError(y instanceof Error?y:new Error(String(y)),g.name);}catch{}}).finally(()=>{n--,!t&&n===0&&s.length===0&&i&&(i(),i=null),o();});}}function l(g,y){if(!t)return false;if(s.length>=d){if(c==="drop-newest")return e++,r?.logger?.warn?.(`[async-worker] Queue full (${d}), dropping task: ${g}`),false;let p=s.shift();e++,r?.logger?.warn?.(`[async-worker] Queue full (${d}), dropping oldest task: ${p?.name??"unknown"}`);}return s.push({name:g,fn:y}),o(),true}return {name:"async-worker",enqueue:l,queueSize:()=>s.length,activeCount:()=>n,droppedCount:()=>e,getCustomMetrics(){return ["# HELP ocpp_async_worker_queue_size Current tasks waiting in the background queue","# TYPE ocpp_async_worker_queue_size gauge",`ocpp_async_worker_queue_size ${s.length}`,"# HELP ocpp_async_worker_active_tasks Currently executing background tasks","# TYPE ocpp_async_worker_active_tasks gauge",`ocpp_async_worker_active_tasks ${n}`,"# HELP ocpp_async_worker_dropped_total Tasks dropped due to queue overflow","# TYPE ocpp_async_worker_dropped_total counter",`ocpp_async_worker_dropped_total ${e}`]},onClosing(){return t=false,n===0&&s.length===0?Promise.resolve():new Promise(g=>{i=g;let y=setTimeout(()=>{r?.logger?.warn?.(`[async-worker] Drain timeout (${a}ms), ${n} tasks still active, ${s.length} queued`),s.length=0,i=null,g();},a);y&&typeof y=="object"&&"unref"in y&&y.unref();})},onClose(){t=false,s.length=0,i=null;}}}function L(r){let u=r?.failureThreshold??5,d=r?.resetTimeoutMs??3e4,c=r?.maxConcurrent??20,a=r?.logger,s=r?.onStateChange,n=new Map;function e(i){let o=n.get(i);return o||(o={state:"CLOSED",failures:0,lastFailure:0,concurrentCalls:0},n.set(i,o)),o}function t(i,o){let l=e(i),m=l.state;m!==o&&(l.state=o,a?.warn?.(`[circuit-breaker] ${i}: ${m} \u2192 ${o}`),s?.(i,m,o));}return {name:"circuit-breaker",onConnection(i){let o=e(i.identity);i.use(async(l,m)=>{if(l.type!=="outgoing_call")return m();if(o.concurrentCalls>=c)throw a?.warn?.(`[circuit-breaker] ${i.identity}: concurrent limit (${c}) reached, rejecting ${l.method}`),new Error(`Circuit breaker: concurrent call limit exceeded for ${i.identity}`);let g=Date.now();if(o.state==="OPEN")if(g-o.lastFailure>=d)t(i.identity,"HALF_OPEN");else throw new Error(`Circuit breaker OPEN for ${i.identity}: ${o.failures} consecutive failures`);o.concurrentCalls++;try{let y=await m();return o.concurrentCalls--,o.state==="HALF_OPEN"?(t(i.identity,"CLOSED"),o.failures=0):o.failures=Math.max(0,o.failures-1),y}catch(y){throw o.concurrentCalls--,o.failures++,o.lastFailure=Date.now(),(o.state==="HALF_OPEN"||o.failures>=u)&&t(i.identity,"OPEN"),y}});},onDisconnect(i){let o=n.get(i.identity);o&&(o.concurrentCalls=0);},onClose(){n.clear();}}}function D(r){let u=r.maxConnections,d=r.closeCode??4029,c=r.closeReason??"Connection limit reached",a=r.forceCloseOnPongTimeout??true,s=r.forceCloseOnBackpressure??false,n=0;return {name:"connection-guard",onConnection(e){n++,n>u&&(r.logger?.warn?.(`[connection-guard] Limit exceeded (${n}/${u}), closing: ${e.identity}`),e.close({code:d,reason:c}));},onDisconnect(){n=Math.max(0,n-1);},onPongTimeout(e){a&&(r.logger?.warn?.(`[connection-guard] Pong timeout \u2014 closing dead peer: ${e.identity}`),e.close({code:4e3,reason:"Pong timeout"}));},onBackpressure(e,t){s&&(r.logger?.warn?.(`[connection-guard] Backpressure (${t} bytes) \u2014 closing slow client: ${e.identity}`),e.close({code:4001,reason:"Backpressure exceeded"}));},onClose(){n=0;}}}function $(){return {name:"heartbeat",onConnection(r){r.handle("Heartbeat",()=>({currentTime:new Date().toISOString()}));}}}function I(r){let u=r.topic??"ocpp.events",d=r.topicRouting??false,c=new Set(r.events??["connect","disconnect","message","security"]),a=new Map;function s(e){return d?`${u}.${e}`:u}function n(e,t,i){if(!c.has(e.split(".")[0]))return;let o=s(e.split(".")[0]),l=JSON.stringify(i),m=t??"server";r.worker?r.worker.enqueue("kafka-publish",async()=>{await r.producer.send({topic:o,messages:[{key:m,value:l,headers:{event:e}}]});}):r.producer.send({topic:o,messages:[{key:m,value:l,headers:{event:e}}]}).catch(()=>{});}return {name:"kafka",onConnection(e){a.set(e.identity,Date.now()),n("connect",e.identity,{identity:e.identity,ip:e.handshake.remoteAddress,protocol:e.protocol,timestamp:new Date().toISOString()});},onDisconnect(e,t,i){let o=a.get(e.identity),l=o?Math.round((Date.now()-o)/1e3):0;a.delete(e.identity),n("disconnect",e.identity,{identity:e.identity,code:t,reason:i,durationSec:l,timestamp:new Date().toISOString()});},onMessage(e,t){let i={identity:e.identity,direction:t.direction,messageType:t.message[0],timestamp:t.ctx.timestamp};t.message[0]===2&&t.message[2]&&(i.method=t.message[2]),t.ctx.latencyMs!==void 0&&(i.latencyMs=t.ctx.latencyMs),r.includePayload&&(i.payload=t.message),n(`message.${t.direction}`,e.identity,i);},onSecurityEvent(e){n("security",e.identity,{type:e.type,identity:e.identity,ip:e.ip,timestamp:e.timestamp,details:e.details});},onAuthFailed(e,t,i){n("auth_failed",e.identity,{identity:e.identity,ip:e.remoteAddress,code:t,reason:i,timestamp:new Date().toISOString()});},onEviction(e,t){n("eviction",e.identity,{identity:e.identity,evictedBy:t.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){n("closing",void 0,{timestamp:new Date().toISOString()});},onClose(){a.clear();}}}function N(r){let u=r.redis,d=r.ttlMs??3e5,c=r.prefix??"ocpp:dedup:",a=r.redisStyle??"positional",s=r.logger;async function n(e){return a==="options"?await u.set(e,"1",{PX:d,NX:true})!==null:await u.set(e,"1","PX",d,"NX")==="OK"}return {name:"message-dedup",async onBeforeReceive(e,t){let i;try{let l=typeof t=="string"?t:t?.toString()||"",m=JSON.parse(l);Array.isArray(m)&&m.length>1&&(i=String(m[1]));}catch{return}if(!i)return;let o=`${c}${e.identity}:${i}`;try{if(!await n(o))return s?.warn?.(`[message-dedup] Dropping duplicate message: ${o}`),!1}catch(l){s?.error?.("[message-dedup] Redis failure, falling through:",l);}}}}function q(r){let u=r?.intervalMs??3e4,d=0,c=0,a=0,s=0,n=0,e=Date.now(),t=null,i=0,o=0,l=0,m=0,g=0,y=0,p=0,f=0,w=0,P=0,_=0,k=0,b=0,v=0,C=0,O=new Map;function A(){return {totalConnections:d,totalDisconnections:c,activeConnections:a,peakConnections:s,connectionDurationAvgMs:c>0?Math.round(n/c):0,uptimeMs:Date.now()-e,timestamp:new Date().toISOString(),totalMessagesIn:i,totalMessagesOut:o,totalCalls:l,totalCallResults:m,totalCallErrors:g,totalErrors:y,totalBadMessages:p,totalHandlerErrors:f,totalRateLimitHits:w,totalAuthFailures:P,totalEvictions:_,totalBackpressureEvents:k,totalPongTimeouts:b,totalValidationFailures:v,totalSecurityEvents:C}}return {name:"metrics",getMetrics:A,onInit(){e=Date.now(),u>0&&r?.onSnapshot&&(t=setInterval(()=>{r.onSnapshot(A());},u),t&&typeof t=="object"&&"unref"in t&&t.unref());},onConnection(S){d++,a++,a>s&&(s=a),O.set(S.identity,Date.now());},onDisconnect(S){c++,a=Math.max(0,a-1);let E=O.get(S.identity);E&&(n+=Date.now()-E,O.delete(S.identity));},onMessage(S,E){E.direction==="IN"?i++:o++;let M=E.message[0];M===2?l++:M===3?m++:M===4&&g++;},onError(){y++;},onBadMessage(){p++;},onHandlerError(){f++;},onRateLimitExceeded(){w++;},onAuthFailed(){P++;},onEviction(){_++;},onBackpressure(){k++;},onPongTimeout(){b++;},onValidationFailure(){v++;},onSecurityEvent(){C++;},getCustomMetrics(){return ["# HELP ocpp_connections_total Total connections since server start","# TYPE ocpp_connections_total counter",`ocpp_connections_total ${d}`,"# HELP ocpp_disconnections_total Total disconnections since server start","# TYPE ocpp_disconnections_total counter",`ocpp_disconnections_total ${c}`,"# HELP ocpp_connections_active Currently active connections","# TYPE ocpp_connections_active gauge",`ocpp_connections_active ${a}`,"# HELP ocpp_connections_peak Highest concurrent connections","# TYPE ocpp_connections_peak gauge",`ocpp_connections_peak ${s}`,"# HELP ocpp_connection_duration_avg_ms Average connection duration","# TYPE ocpp_connection_duration_avg_ms gauge",`ocpp_connection_duration_avg_ms ${A().connectionDurationAvgMs}`,"# HELP ocpp_messages_in_total Total inbound messages","# TYPE ocpp_messages_in_total counter",`ocpp_messages_in_total ${i}`,"# HELP ocpp_messages_out_total Total outbound messages","# TYPE ocpp_messages_out_total counter",`ocpp_messages_out_total ${o}`,"# HELP ocpp_calls_total Total CALL messages","# TYPE ocpp_calls_total counter",`ocpp_calls_total ${l}`,"# HELP ocpp_call_results_total Total CALLRESULT messages","# TYPE ocpp_call_results_total counter",`ocpp_call_results_total ${m}`,"# HELP ocpp_call_errors_total Total CALLERROR messages","# TYPE ocpp_call_errors_total counter",`ocpp_call_errors_total ${g}`,"# HELP ocpp_errors_total WebSocket/protocol errors","# TYPE ocpp_errors_total counter",`ocpp_errors_total ${y}`,"# HELP ocpp_bad_messages_total Malformed messages received","# TYPE ocpp_bad_messages_total counter",`ocpp_bad_messages_total ${p}`,"# HELP ocpp_handler_errors_total User handler errors","# TYPE ocpp_handler_errors_total counter",`ocpp_handler_errors_total ${f}`,"# HELP ocpp_rate_limit_hits_total Rate limit violations","# TYPE ocpp_rate_limit_hits_total counter",`ocpp_rate_limit_hits_total ${w}`,"# HELP ocpp_auth_failures_total Authentication failures","# TYPE ocpp_auth_failures_total counter",`ocpp_auth_failures_total ${P}`,"# HELP ocpp_evictions_total Client evictions","# TYPE ocpp_evictions_total counter",`ocpp_evictions_total ${_}`,"# HELP ocpp_backpressure_events_total Slow client backpressure events","# TYPE ocpp_backpressure_events_total counter",`ocpp_backpressure_events_total ${k}`,"# HELP ocpp_pong_timeouts_total Dead peer timeouts","# TYPE ocpp_pong_timeouts_total counter",`ocpp_pong_timeouts_total ${b}`,"# HELP ocpp_validation_failures_total Schema validation failures","# TYPE ocpp_validation_failures_total counter",`ocpp_validation_failures_total ${v}`,"# HELP ocpp_security_events_total Security events from anomaly detection","# TYPE ocpp_security_events_total counter",`ocpp_security_events_total ${C}`]},onClose(){t&&(clearInterval(t),t=null),O.clear();}}}function B(r){let u=r.topicPrefix??"ocpp",d=new Set(r.events??["connect","disconnect","message","security"]),c=r.qos??0,a=new Map;function s(e,t){return r.topicBuilder?r.topicBuilder(e,t):t?`${u}/${t}/${e}`:`${u}/${e}`}function n(e,t){let i=r.transform?r.transform(t):t,o=JSON.stringify(i);r.worker?r.worker.enqueue("mqtt-publish",()=>new Promise((l,m)=>{r.client.publish(e,o,{qos:c},g=>g?m(g):l());})):r.client.publish(e,o,{qos:c});}return {name:"mqtt",onConnection(e){a.set(e.identity,Date.now()),d.has("connect")&&n(s("connect",e.identity),{identity:e.identity,ip:e.handshake.remoteAddress,protocol:e.protocol,timestamp:new Date().toISOString()});},onDisconnect(e,t,i){if(!d.has("disconnect")){a.delete(e.identity);return}let o=a.get(e.identity),l=o?Math.round((Date.now()-o)/1e3):0;a.delete(e.identity),n(s("disconnect",e.identity),{identity:e.identity,code:t,reason:i,durationSec:l,timestamp:new Date().toISOString()});},onMessage(e,t){if(!d.has("message"))return;let i={identity:e.identity,direction:t.direction,messageType:t.message[0],timestamp:t.ctx.timestamp};t.message[0]===2&&t.message[2]&&(i.method=t.message[2]),t.ctx.latencyMs!==void 0&&(i.latencyMs=t.ctx.latencyMs),r.includePayload&&(i.payload=t.message),n(s(`message/${t.direction}`,e.identity),i);},onSecurityEvent(e){d.has("security")&&n(s("security",e.identity),{type:e.type,identity:e.identity,ip:e.ip,timestamp:e.timestamp,details:e.details});},onError(e,t){d.has("error")&&n(s("error",e.identity),{identity:e.identity,error:t.message,timestamp:new Date().toISOString()});},onAuthFailed(e,t,i){d.has("auth_failed")&&n(s("auth_failed"),{identity:e.identity,ip:e.remoteAddress,code:t,reason:i,timestamp:new Date().toISOString()});},onEviction(e,t){d.has("eviction")&&n(s("eviction",e.identity),{identity:e.identity,evictedBy:t.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){n(s("closing"),{timestamp:new Date().toISOString()});},onClose(){a.clear();try{r.client.end(!1);}catch{}}}}function F(r){let u=r?.tracer??null,d=new Map;return {name:"otel",async onInit(c){if(!u)try{u=(await import('@opentelemetry/api')).trace.getTracer(r?.serviceName??"ocpp-server","1.0.0");}catch{c.log.warn?.("otelPlugin: @opentelemetry/api not found \u2014 plugin disabled. Install it as a peer dependency."),u=null;}},onConnection(c){if(!u)return;let a=u.startSpan("ocpp.connection",{kind:1});a.setAttribute("ocpp.identity",c.identity),a.setAttribute("ocpp.protocol",c.protocol??"unknown"),a.setAttribute("net.peer.ip",c.handshake.remoteAddress),d.set(c.identity,{span:a,startTime:Date.now()});},onDisconnect(c,a){let s=d.get(c.identity);if(!s)return;let n=Date.now()-s.startTime;s.span.setAttribute("ocpp.close_code",a),s.span.setAttribute("ocpp.duration_ms",n),s.span.setStatus({code:1}),s.span.end(),d.delete(c.identity);},onMessage(c,a){if(!u)return;let s=a.message[0];if(s!==2){let t=d.get(c.identity);t&&t.span.addEvent(s===3?"ocpp.call_result":"ocpp.call_error",{direction:a.direction,"ocpp.message_id":String(a.message[1]),...a.ctx.latencyMs!==void 0&&{"ocpp.latency_ms":a.ctx.latencyMs}});return}let n=String(a.message[2]??"unknown"),e=u.startSpan(`ocpp.call.${n}`,{kind:a.direction==="IN"?1:2});e.setAttribute("ocpp.identity",c.identity),e.setAttribute("ocpp.method",n),e.setAttribute("ocpp.direction",a.direction),e.setAttribute("ocpp.message_id",String(a.message[1])),a.ctx.latencyMs!==void 0&&e.setAttribute("ocpp.latency_ms",a.ctx.latencyMs),e.setStatus({code:1}),e.end();},onError(c,a){let s=d.get(c.identity);s&&(s.span.recordException(a),s.span.addEvent("ocpp.error",{"error.message":a.message}));},onHandlerError(c,a,s){let n=d.get(c.identity);n&&(n.span.recordException(s),n.span.addEvent("ocpp.handler_error",{"ocpp.method":a,"error.message":s.message}));},onBadMessage(c,a,s){let n=d.get(c.identity);n&&(n.span.recordException(s),n.span.addEvent("ocpp.bad_message",{"raw.preview":typeof a=="string"?a.slice(0,200):"<buffer>","error.message":s.message}));},onValidationFailure(c,a,s){let n=d.get(c.identity);n&&(n.span.recordException(s),n.span.addEvent("ocpp.validation_failure",{"error.message":s.message}));},onRateLimitExceeded(c){let a=d.get(c.identity);a&&a.span.addEvent("ocpp.rate_limit_exceeded");},onPongTimeout(c){let a=d.get(c.identity);a&&a.span.addEvent("ocpp.pong_timeout");},onBackpressure(c,a){let s=d.get(c.identity);s&&s.span.addEvent("ocpp.backpressure",{"ocpp.buffered_bytes":a});},onEviction(c,a){let s=d.get(c.identity);s&&s.span.addEvent("ocpp.evicted",{"net.peer.ip.new":a.handshake.remoteAddress});},onTelemetry(c){if(!u)return;let a=u.startSpan("ocpp.telemetry_push",{kind:0});a.setAttribute("ocpp.connected_clients",c.connectedClients),a.setAttribute("ocpp.active_sessions",c.activeSessions),a.setAttribute("ocpp.uptime_seconds",c.uptimeSeconds),a.setAttribute("ocpp.memory_rss",c.memoryUsage.rss),a.setAttribute("ocpp.memory_heap_used",c.memoryUsage.heapUsed),a.setAttribute("ocpp.pid",c.pid),c.webSockets&&(a.setAttribute("ocpp.ws_total",c.webSockets.total),a.setAttribute("ocpp.ws_buffered_amount",c.webSockets.bufferedAmount)),a.setStatus({code:1}),a.end();},onSecurityEvent(c){if(!u)return;let a=u.startSpan("ocpp.security_event",{kind:0});a.setAttribute("security.event_type",c.type),c.identity&&a.setAttribute("ocpp.identity",c.identity),c.ip&&a.setAttribute("net.peer.ip",c.ip),a.setStatus({code:2,message:c.type}),a.end();},onAuthFailed(c,a,s){if(!u)return;let n=u.startSpan("ocpp.auth_failed",{kind:1});n.setAttribute("ocpp.identity",c.identity),n.setAttribute("net.peer.ip",c.remoteAddress),n.setAttribute("ocpp.close_code",a),n.setAttribute("ocpp.close_reason",s),n.setStatus({code:2,message:"Auth failed"}),n.end();},onClosing(){for(let[,c]of d)c.span.addEvent("ocpp.server_closing");},onClose(){for(let[,c]of d)c.span.setStatus({code:2,message:"Server shutdown"}),c.span.end();d.clear();}}}function H(r={}){let u=new Set(r.sensitiveKeys??["idTag","authorizationKey","token","password","securityCode"]),d=r.replacement??"***REDACTED***",c=r.incoming??true,a=r.outgoing??true;function s(e){if(!e||typeof e!="object")return e;if(Array.isArray(e))return e.map(s);let t={};for(let[i,o]of Object.entries(e))u.has(i)?t[i]=d:o&&typeof o=="object"?t[i]=s(o):t[i]=o;return t}let n=async(e,t)=>{c&&(e.type==="incoming_call"&&e.params?e.params=s(e.params):e.type==="incoming_result"&&e.payload&&(e.payload=s(e.payload))),a&&(e.type==="outgoing_call"&&e.params?e.params=s(e.params):e.type==="outgoing_result"&&e.payload&&(e.payload=s(e.payload))),await t();};return {name:"pii-redactor",onConnection(e){e.use(n);}}}function W(r){let u=r.cooldownMs??6e4,d=r.threshold??1,c=r.windowMs??3e5,a=r.logger,s=new Map,n=new Map;function e(){return typeof r.sink=="string"?{async send(o){await fetch(r.sink,{method:"POST",headers:{"Content-Type":"application/json",...r.headers},body:JSON.stringify(o)});}}:r.sink}function t(o){let l=Date.now(),g=(s.get(o)??[]).filter(y=>l-y<c);return s.set(o,g),g}function i(o,l,m){let g=o??l??"unknown",y=Date.now(),p=t(g);if(p.push(y),p.length<d)return;let f=n.get(g)??0;if(y-f<u)return;n.set(g,y);let w=e(),P={eventType:m,identity:o,ip:l,timestamp:new Date().toISOString(),count:p.length,windowMs:c};Promise.resolve(w.send(P)).catch(_=>{a?.error?.("[rate-limit-notifier] Alert delivery failed:",_);});}return {name:"rate-limit-notifier",onRateLimitExceeded(o,l){i(o.identity,o.handshake.remoteAddress,"RATE_LIMIT_EXCEEDED");},onSecurityEvent(o){(o.type==="RATE_LIMIT_EXCEEDED"||o.type==="CONNECTION_RATE_LIMIT")&&i(o.identity,o.ip,o.type);},onClose(){s.clear(),n.clear();}}}function j(r){let u=r.mode??"pubsub",d=r.prefix??"ocpp",c=new Set(r.events??["connect","disconnect","message","security"]),a=r.maxStreamLength??1e4,s=r.serialize??JSON.stringify,n=new Map;function e(i){return `${d}:${i}`}function t(i,o){if(!c.has(i))return;let l=e(i),m=s(o),g=async()=>{u==="stream"&&r.client.xadd?await r.client.xadd(l,"MAXLEN","~",a,"*","data",m):await r.client.publish(l,m);};if(r.worker)r.worker.enqueue(`redis-${u}`,()=>g().catch(()=>{}));else try{g().catch?.(()=>{});}catch{}}return {name:"redis-pubsub",onConnection(i){n.set(i.identity,Date.now()),t("connect",{identity:i.identity,ip:i.handshake.remoteAddress,protocol:i.protocol,timestamp:new Date().toISOString()});},onDisconnect(i,o,l){let m=n.get(i.identity),g=m?Math.round((Date.now()-m)/1e3):0;n.delete(i.identity),t("disconnect",{identity:i.identity,code:o,reason:l,durationSec:g,timestamp:new Date().toISOString()});},onMessage(i,o){let l={identity:i.identity,direction:o.direction,messageType:o.message[0],timestamp:o.ctx.timestamp};o.message[0]===2&&o.message[2]&&(l.method=o.message[2]),o.ctx.latencyMs!==void 0&&(l.latencyMs=o.ctx.latencyMs),r.includePayload&&(l.payload=o.message),t(`message:${o.direction}`,l);},onSecurityEvent(i){t("security",{type:i.type,identity:i.identity,ip:i.ip,timestamp:i.timestamp,details:i.details});},onAuthFailed(i,o,l){t("auth_failed",{identity:i.identity,ip:i.remoteAddress,code:o,reason:l,timestamp:new Date().toISOString()});},onEviction(i,o){t("eviction",{identity:i.identity,evictedBy:o.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){t("closing",{timestamp:new Date().toISOString()});},onClose(){n.clear();try{r.client.quit?r.client.quit():r.client.disconnect&&r.client.disconnect();}catch{}}}}function Y(r){let u=r.redis,d=r.prefix??"ocpp:replay:",c=r.syntheticResponse??true,a=r.flushConcurrency??5,s=r.flushDelayMs??200,n=r.logger,e=new Set;function t(i){return new Promise(o=>setTimeout(o,i))}return {name:"replay-buffer",onConnection(i){let o=`${d}${i.identity}`,l=async(g,y)=>{if(g.type!=="outgoing_call")return y();try{return await y()}catch(p){let f=p instanceof Error?p.message:String(p);if(!(f.includes("WebSocket is not open")||f.includes("offline")||f.includes("CLOSED")||f.includes("CLOSING")))throw p;let P=JSON.stringify([2,g.messageId,g.method,g.params]);try{await u.rpush(o,P),n?.warn?.(`[replay-buffer] Queued offline command: ${g.method} for ${i.identity}`);}catch(_){throw n?.error?.(`[replay-buffer] Redis rpush failed for ${i.identity}:`,_),p}if(c)return {status:"Accepted",note:"Queued offline (ReplayBuffer)"};throw p}};i.use(l);let m=(async()=>{try{let g=0;for(;;){let y=await u.lpop(o);if(!y)break;let p;try{p=JSON.parse(y);}catch{n?.warn?.(`[replay-buffer] Skipping unparseable queued message for ${i.identity}`);continue}!Array.isArray(p)||p[0]!==2||(i.call(p[2],p[3]).catch(f=>{n?.warn?.(`[replay-buffer] Flush call failed for ${i.identity}/${p[2]}:`,f);}),g++,g>=a&&(await t(s),g=0));}}catch(g){n?.error?.(`[replay-buffer] Error flushing queue for ${i.identity}:`,g);}})();e.add(m),m.finally(()=>e.delete(m));},async onClosing(){e.size>0&&await Promise.allSettled([...e]);},onClose(){e.clear();}}}function K(r){let u=r.unmatchedBehavior??"passthrough",d=r.logger,c=new Map,a;for(let n of r.rules)n.method==="*"?a=n:c.set(n.method,n);function s(n){return c.get(n)??a}return {name:"schema-versioning",onConnection(n){if(r.applyWhen&&n.protocol!==r.applyWhen)return;let e=async(t,i)=>{let o=t.method,l=s(o);if(!l){if(u==="reject")throw d?.warn?.(`[schema-versioning] No transform rule for method "${o}", rejecting`),new Error(`Schema versioning: no transform rule for "${o}" (${r.sourceVersion} \u2192 ${r.targetVersion})`);return i()}if(t.type==="incoming_call")try{let m=l.transform(t.params,"up");t.params=m,d?.debug?.(`[schema-versioning] Transformed ${o} UP: ${r.sourceVersion} \u2192 ${r.targetVersion}`);}catch(m){d?.warn?.(`[schema-versioning] Transform UP failed for ${o}:`,m);}else if(t.type==="outgoing_call")try{let m=l.transform(t.params,"down");t.params=m,d?.debug?.(`[schema-versioning] Transformed ${o} DOWN: ${r.targetVersion} \u2192 ${r.sourceVersion}`);}catch(m){d?.warn?.(`[schema-versioning] Transform DOWN failed for ${o}:`,m);}else if(t.type==="outgoing_result")try{let m=l.transform(t.payload,"down");t.payload=m;}catch(m){d?.warn?.(`[schema-versioning] Transform DOWN (result) failed for ${o}:`,m);}return i()};n.use(e);}}}function V(r){let u=r?.logger??console,d=r?.logLevel??"standard",c=d==="standard"||d==="verbose",a=d==="verbose",s=new Map;return {name:"session-log",onConnection(n){s.set(n.identity,Date.now()),u.info("[session] connected",{identity:n.identity,ip:n.handshake.remoteAddress,protocol:n.protocol});},onDisconnect(n,e,t){let i=s.get(n.identity),o=i?Math.round((Date.now()-i)/1e3):0;s.delete(n.identity),u.info("[session] disconnected",{identity:n.identity,code:e,reason:t,durationSec:o});},onError(n,e){c&&(u.error??u.warn)("[session] error",{identity:n.identity,error:e.message});},onAuthFailed(n,e,t){c&&u.warn("[session] auth failed",{identity:n.identity,ip:n.remoteAddress,code:e,reason:t});},onEviction(n,e){c&&u.warn("[session] evicted",{identity:n.identity,evictedIp:n.handshake.remoteAddress,newIp:e.handshake.remoteAddress});},onBadMessage(n,e){a&&u.warn("[session] bad message",{identity:n.identity,raw:typeof e=="string"?e.slice(0,200):"<buffer>"});},onSecurityEvent(n){a&&u.warn("[session] security event",{type:n.type,identity:n.identity,ip:n.ip,details:n.details});},onHandlerError(n,e,t){a&&(u.error??u.warn)("[session] handler error",{identity:n.identity,method:e,error:t.message});},onValidationFailure(n,e,t){a&&u.warn("[session] validation failure",{identity:n.identity,error:t.message});},onRateLimitExceeded(n){c&&u.warn("[session] rate limit exceeded",{identity:n.identity,ip:n.handshake.remoteAddress});},onPongTimeout(n){a&&u.warn("[session] pong timeout (dead peer)",{identity:n.identity});},onBackpressure(n,e){a&&u.warn("[session] backpressure",{identity:n.identity,bufferedBytes:e});},onClose(){s.clear();}}}function U(r){let u=new Set(r.events??["init","connect","disconnect","close"]),d=r.timeout??5e3,c=r.retries??1;async function a(s){if(!u.has(s.event))return;let n=JSON.stringify(s),e={"Content-Type":"application/json",...r.headers};if(r.secret){let t=crypto.createHmac("sha256",r.secret).update(n).digest("hex");e["X-Signature"]=t;}for(let t=0;t<=c;t++)try{let i=new AbortController,o=setTimeout(()=>i.abort(),d);await fetch(r.url,{method:"POST",headers:e,body:n,signal:i.signal}),clearTimeout(o);return}catch{}}return {name:"webhook",onInit(){a({event:"init",timestamp:new Date().toISOString()}).catch(()=>{});},onConnection(s){a({event:"connect",timestamp:new Date().toISOString(),data:{identity:s.identity,ip:s.handshake.remoteAddress,protocol:s.protocol}}).catch(()=>{});},onDisconnect(s,n,e){a({event:"disconnect",timestamp:new Date().toISOString(),data:{identity:s.identity,code:n,reason:e}}).catch(()=>{});},onSecurityEvent(s){a({event:"security",timestamp:s.timestamp,data:{type:s.type,identity:s.identity,ip:s.ip,details:s.details}}).catch(()=>{});},onAuthFailed(s,n,e){a({event:"auth_failed",timestamp:new Date().toISOString(),data:{identity:s.identity,ip:s.remoteAddress,code:n,reason:e}}).catch(()=>{});},onEviction(s,n){a({event:"eviction",timestamp:new Date().toISOString(),data:{identity:s.identity,evictedIp:s.handshake.remoteAddress,newIp:n.handshake.remoteAddress}}).catch(()=>{});},onClosing(){a({event:"closing",timestamp:new Date().toISOString()}).catch(()=>{});},onClose(){}}}exports.amqpPlugin=T;exports.anomalyPlugin=R;exports.asyncWorkerPlugin=x;exports.circuitBreakerPlugin=L;exports.connectionGuardPlugin=D;exports.heartbeatPlugin=$;exports.kafkaPlugin=I;exports.messageDedupPlugin=N;exports.metricsPlugin=q;exports.mqttPlugin=B;exports.otelPlugin=F;exports.piiRedactorPlugin=H;exports.rateLimitNotifierPlugin=W;exports.redisPubSubPlugin=j;exports.replayBufferPlugin=Y;exports.schemaVersioningPlugin=K;exports.sessionLogPlugin=V;exports.webhookPlugin=U;
|
|
1
|
+
'use strict';var crypto=require('crypto');function x(n){let d=n.exchange??"ocpp.events",l=n.routingKey??"ocpp.{event}.{identity}",c=new Set(n.events??["connect","disconnect","message","security"]),o={persistent:n.publishOptions?.persistent??true,contentType:n.publishOptions?.contentType??"application/json",...n.publishOptions?.priority!==void 0&&{priority:n.publishOptions.priority}},s=new Map;function r(t,i){return l.replace("{event}",t).replace("{identity}",i??"server")}function e(t,i,a){if(!c.has(t))return;let u=r(t,i),m=Buffer.from(JSON.stringify(a));if(n.worker)n.worker.enqueue("amqp-publish",async()=>{n.channel.publish(d,u,m,o);});else try{n.channel.publish(d,u,m,o);}catch{}}return {name:"amqp",onConnection(t){s.set(t.identity,Date.now()),e("connect",t.identity,{identity:t.identity,ip:t.handshake.remoteAddress,protocol:t.protocol,timestamp:new Date().toISOString()});},onDisconnect(t,i,a){let u=s.get(t.identity),m=u?Math.round((Date.now()-u)/1e3):0;s.delete(t.identity),e("disconnect",t.identity,{identity:t.identity,code:i,reason:a,durationSec:m,timestamp:new Date().toISOString()});},onMessage(t,i){let a={identity:t.identity,direction:i.direction,messageType:i.message[0],timestamp:i.ctx.timestamp};i.message[0]===2&&i.message[2]&&(a.method=i.message[2]),i.ctx.latencyMs!==void 0&&(a.latencyMs=i.ctx.latencyMs),n.includePayload&&(a.payload=i.message),e(`message.${i.direction}`,t.identity,a);},onSecurityEvent(t){e("security",t.identity,{type:t.type,identity:t.identity,ip:t.ip,timestamp:t.timestamp,details:t.details});},onAuthFailed(t,i,a){e("auth_failed",t.identity,{identity:t.identity,ip:t.remoteAddress,code:i,reason:a,timestamp:new Date().toISOString()});},onEviction(t,i){e("eviction",t.identity,{identity:t.identity,evictedBy:i.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){e("closing",void 0,{timestamp:new Date().toISOString()});},onClose(){s.clear();try{n.channel.close();}catch{}}}}function R(n){let d=n?.reconnectThreshold??5,l=n?.authFailureThreshold??5,c=n?.badMessageThreshold??10,o=n?.evictionThreshold??3,s=n?.windowMs??6e4,r=new Map,e=new Map,t=new Map,i=new Map,a=null,u=null;function m(p,f){let w=f-s,P=0;for(;P<p.length&&p[P]<w;)P++;return P>0?p.slice(P):p}function g(p,f){for(let[w,P]of p){let _=m(P,f);_.length===0?p.delete(w):p.set(w,_);}}function y(p,f,w,P,_){let k=Date.now(),b=p.get(f)??[];if(b=m(b,k),b.push(k),p.set(f,b),b.length>w&&a){let S={type:P,identity:_.identity,ip:_.ip??_.evictedIp,timestamp:new Date().toISOString(),details:{..._,countInWindow:b.length,threshold:w,windowMs:s}};a.emit("securityEvent",S);}}return {name:"anomaly",onInit(p){a=p,u=setInterval(()=>{let f=Date.now();g(r,f),g(e,f),g(t,f),g(i,f);},s).unref();},onConnection(p){y(r,p.identity,d,"ANOMALY_RAPID_RECONNECT",{identity:p.identity,ip:p.handshake.remoteAddress});},onAuthFailed(p,f,w){y(e,p.remoteAddress,l,"ANOMALY_AUTH_BRUTE_FORCE",{ip:p.remoteAddress,identity:p.identity,code:f,reason:w});},onBadMessage(p){y(t,p.identity,c,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress});},onValidationFailure(p){y(t,p.identity,c,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress,source:"validation_failure"});},onEviction(p,f){y(i,p.identity,o,"ANOMALY_IDENTITY_COLLISION",{identity:p.identity,evictedIp:p.handshake.remoteAddress,newIp:f.handshake.remoteAddress});},onClose(){u&&(clearInterval(u),u=null),r.clear(),e.clear(),t.clear(),i.clear(),a=null;}}}function L(n){let d=n?.concurrency??10,l=n?.maxQueueSize??1e3,c=n?.overflowStrategy??"drop-oldest",o=n?.drainTimeoutMs??5e3,s=[],r=0,e=0,t=true,i=null;function a(){for(;r<d&&s.length>0;){let g=s.shift();r++,g.fn().catch(y=>{if(n?.onError)try{n.onError(y instanceof Error?y:new Error(String(y)),g.name);}catch{}}).finally(()=>{r--,!t&&r===0&&s.length===0&&i&&(i(),i=null),a();});}}function u(g,y){if(!t)return false;if(s.length>=l){if(c==="drop-newest")return e++,n?.logger?.warn?.(`[async-worker] Queue full (${l}), dropping task: ${g}`),false;let p=s.shift();e++,n?.logger?.warn?.(`[async-worker] Queue full (${l}), dropping oldest task: ${p?.name??"unknown"}`);}return s.push({name:g,fn:y}),a(),true}return {name:"async-worker",enqueue:u,queueSize:()=>s.length,activeCount:()=>r,droppedCount:()=>e,getCustomMetrics(){return ["# HELP ocpp_async_worker_queue_size Current tasks waiting in the background queue","# TYPE ocpp_async_worker_queue_size gauge",`ocpp_async_worker_queue_size ${s.length}`,"# HELP ocpp_async_worker_active_tasks Currently executing background tasks","# TYPE ocpp_async_worker_active_tasks gauge",`ocpp_async_worker_active_tasks ${r}`,"# HELP ocpp_async_worker_dropped_total Tasks dropped due to queue overflow","# TYPE ocpp_async_worker_dropped_total counter",`ocpp_async_worker_dropped_total ${e}`]},onClosing(){return t=false,r===0&&s.length===0?Promise.resolve():new Promise(g=>{i=g;let y=setTimeout(()=>{n?.logger?.warn?.(`[async-worker] Drain timeout (${o}ms), ${r} tasks still active, ${s.length} queued`),s.length=0,i=null,g();},o);y&&typeof y=="object"&&"unref"in y&&y.unref();})},onClose(){t=false,s.length=0,i=null;}}}var v=class extends Map{_maxSize;constructor(d){if(super(),d<1)throw new RangeError("LRUMap maxSize must be >= 1");this._maxSize=d;}get maxSize(){return this._maxSize}set(d,l){if(this.has(d)&&this.delete(d),super.set(d,l),this.size>this._maxSize){let c=this.keys().next().value;c!==void 0&&this.delete(c);}return this}get(d){if(!this.has(d))return;let l=super.get(d);return this.delete(d),super.set(d,l),l}};function D(n){let d=n?.failureThreshold??5,l=n?.resetTimeoutMs??3e4,c=n?.maxConcurrent??20,o=n?.maxTrackedClients??1e4,s=n?.logger,r=n?.onStateChange,e=new v(o);function t(a){let u=e.get(a);return u||(u={state:"CLOSED",failures:0,lastFailure:0,concurrentCalls:0},e.set(a,u)),u}function i(a,u){let m=t(a),g=m.state;g!==u&&(m.state=u,s?.warn?.(`[circuit-breaker] ${a}: ${g} \u2192 ${u}`),r?.(a,g,u));}return {name:"circuit-breaker",onConnection(a){let u=t(a.identity);a.use(async(m,g)=>{if(m.type!=="outgoing_call")return g();if(u.concurrentCalls>=c)throw s?.warn?.(`[circuit-breaker] ${a.identity}: concurrent limit (${c}) reached, rejecting ${m.method}`),new Error(`Circuit breaker: concurrent call limit exceeded for ${a.identity}`);let y=Date.now();if(u.state==="OPEN")if(y-u.lastFailure>=l)i(a.identity,"HALF_OPEN");else throw new Error(`Circuit breaker OPEN for ${a.identity}: ${u.failures} consecutive failures`);u.concurrentCalls++;try{let p=await g();return u.concurrentCalls=Math.max(0,u.concurrentCalls-1),u.state==="HALF_OPEN"?(i(a.identity,"CLOSED"),u.failures=0):u.failures=Math.max(0,u.failures-1),p}catch(p){throw u.concurrentCalls=Math.max(0,u.concurrentCalls-1),u.failures++,u.lastFailure=Date.now(),(u.state==="HALF_OPEN"||u.failures>=d)&&i(a.identity,"OPEN"),p}});},onDisconnect(a){let u=e.get(a.identity);u&&(u.concurrentCalls=0);},onClose(){e.clear();}}}function $(n){let d=n.maxConnections,l=n.closeCode??4029,c=n.closeReason??"Connection limit reached",o=n.forceCloseOnPongTimeout??true,s=n.forceCloseOnBackpressure??false,r=0;return {name:"connection-guard",onConnection(e){r++,r>d&&(n.logger?.warn?.(`[connection-guard] Limit exceeded (${r}/${d}), closing: ${e.identity}`),e.close({code:l,reason:c}));},onDisconnect(){r=Math.max(0,r-1);},onPongTimeout(e){o&&(n.logger?.warn?.(`[connection-guard] Pong timeout \u2014 closing dead peer: ${e.identity}`),e.close({code:4e3,reason:"Pong timeout"}));},onBackpressure(e,t){s&&(n.logger?.warn?.(`[connection-guard] Backpressure (${t} bytes) \u2014 closing slow client: ${e.identity}`),e.close({code:4001,reason:"Backpressure exceeded"}));},onClose(){r=0;}}}function I(){return {name:"heartbeat",onConnection(n){n.hasHandler("Heartbeat")||n.handle("Heartbeat",()=>({currentTime:new Date().toISOString()}));}}}function N(n){let d=n.topic??"ocpp.events",l=n.topicRouting??false,c=new Set(n.events??["connect","disconnect","message","security"]),o=new Map;function s(e){return l?`${d}.${e}`:d}function r(e,t,i){if(!c.has(e.split(".")[0]))return;let a=s(e.split(".")[0]),u=JSON.stringify(i),m=t??"server";n.worker?n.worker.enqueue("kafka-publish",async()=>{await n.producer.send({topic:a,messages:[{key:m,value:u,headers:{event:e}}]});}):n.producer.send({topic:a,messages:[{key:m,value:u,headers:{event:e}}]}).catch(()=>{});}return {name:"kafka",onConnection(e){o.set(e.identity,Date.now()),r("connect",e.identity,{identity:e.identity,ip:e.handshake.remoteAddress,protocol:e.protocol,timestamp:new Date().toISOString()});},onDisconnect(e,t,i){let a=o.get(e.identity),u=a?Math.round((Date.now()-a)/1e3):0;o.delete(e.identity),r("disconnect",e.identity,{identity:e.identity,code:t,reason:i,durationSec:u,timestamp:new Date().toISOString()});},onMessage(e,t){let i={identity:e.identity,direction:t.direction,messageType:t.message[0],timestamp:t.ctx.timestamp};t.message[0]===2&&t.message[2]&&(i.method=t.message[2]),t.ctx.latencyMs!==void 0&&(i.latencyMs=t.ctx.latencyMs),n.includePayload&&(i.payload=t.message),r(`message.${t.direction}`,e.identity,i);},onSecurityEvent(e){r("security",e.identity,{type:e.type,identity:e.identity,ip:e.ip,timestamp:e.timestamp,details:e.details});},onAuthFailed(e,t,i){r("auth_failed",e.identity,{identity:e.identity,ip:e.remoteAddress,code:t,reason:i,timestamp:new Date().toISOString()});},onEviction(e,t){r("eviction",e.identity,{identity:e.identity,evictedBy:t.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){r("closing",void 0,{timestamp:new Date().toISOString()});},onClose(){o.clear();}}}function q(n){let d=n.redis,l=n.ttlMs??3e5,c=n.prefix??"ocpp:dedup:",o=n.redisStyle??"positional",s=n.logger;async function r(t){return o==="options"?await d.set(t,"1",{PX:l,NX:true})!==null:await d.set(t,"1","PX",l,"NX")==="OK"}async function e(t,i){o==="options"?await d.set(t,i,{PX:l}):await d.set(t,i,"PX",l);}return {name:"message-dedup",async onBeforeReceive(t,i){let a;try{let g=typeof i=="string"?i:i?.toString()||"";a=JSON.parse(g);}catch{return}if(!Array.isArray(a)||a[0]!==2||typeof a[1]!="string")return;let u=a[1],m=`${c}${t.identity}:${u}`;try{if(!await r(m)){if(d.get){let y=await d.get(`${c}resp:${t.identity}:${u}`);if(y)try{t.sendRaw(y);}catch{}}return s?.warn?.(`[message-dedup] Dropping duplicate message: ${m}`),!1}}catch(g){s?.error?.("[message-dedup] Redis failure, falling through:",g);}},onBeforeSend(t,i){if(Array.isArray(i)&&(i[0]===3||i[0]===4)&&typeof i[1]=="string"&&d.get){let a=`${c}resp:${t.identity}:${i[1]}`;Promise.resolve(e(a,JSON.stringify(i))).catch(()=>{});}return true}}}function B(n){let d=n?.intervalMs??3e4,l=0,c=0,o=0,s=0,r=0,e=Date.now(),t=null,i=0,a=0,u=0,m=0,g=0,y=0,p=0,f=0,w=0,P=0,_=0,k=0,b=0,S=0,A=0,C=new Map;function M(){return {totalConnections:l,totalDisconnections:c,activeConnections:o,peakConnections:s,connectionDurationAvgMs:c>0?Math.round(r/c):0,uptimeMs:Date.now()-e,timestamp:new Date().toISOString(),totalMessagesIn:i,totalMessagesOut:a,totalCalls:u,totalCallResults:m,totalCallErrors:g,totalErrors:y,totalBadMessages:p,totalHandlerErrors:f,totalRateLimitHits:w,totalAuthFailures:P,totalEvictions:_,totalBackpressureEvents:k,totalPongTimeouts:b,totalValidationFailures:S,totalSecurityEvents:A}}return {name:"metrics",getMetrics:M,onInit(){e=Date.now(),d>0&&n?.onSnapshot&&(t=setInterval(()=>{n.onSnapshot(M());},d),t&&typeof t=="object"&&"unref"in t&&t.unref());},onConnection(E){l++,o++,o>s&&(s=o),C.set(E.identity,Date.now());},onDisconnect(E){c++,o=Math.max(0,o-1);let O=C.get(E.identity);O&&(r+=Date.now()-O,C.delete(E.identity));},onMessage(E,O){O.direction==="IN"?i++:a++;let T=O.message[0];T===2?u++:T===3?m++:T===4&&g++;},onError(){y++;},onBadMessage(){p++;},onHandlerError(){f++;},onRateLimitExceeded(){w++;},onAuthFailed(){P++;},onEviction(){_++;},onBackpressure(){k++;},onPongTimeout(){b++;},onValidationFailure(){S++;},onSecurityEvent(){A++;},getCustomMetrics(){return ["# HELP ocpp_connections_total Total connections since server start","# TYPE ocpp_connections_total counter",`ocpp_connections_total ${l}`,"# HELP ocpp_disconnections_total Total disconnections since server start","# TYPE ocpp_disconnections_total counter",`ocpp_disconnections_total ${c}`,"# HELP ocpp_connections_active Currently active connections","# TYPE ocpp_connections_active gauge",`ocpp_connections_active ${o}`,"# HELP ocpp_connections_peak Highest concurrent connections","# TYPE ocpp_connections_peak gauge",`ocpp_connections_peak ${s}`,"# HELP ocpp_connection_duration_avg_ms Average connection duration","# TYPE ocpp_connection_duration_avg_ms gauge",`ocpp_connection_duration_avg_ms ${M().connectionDurationAvgMs}`,"# HELP ocpp_messages_in_total Total inbound messages","# TYPE ocpp_messages_in_total counter",`ocpp_messages_in_total ${i}`,"# HELP ocpp_messages_out_total Total outbound messages","# TYPE ocpp_messages_out_total counter",`ocpp_messages_out_total ${a}`,"# HELP ocpp_calls_total Total CALL messages","# TYPE ocpp_calls_total counter",`ocpp_calls_total ${u}`,"# HELP ocpp_call_results_total Total CALLRESULT messages","# TYPE ocpp_call_results_total counter",`ocpp_call_results_total ${m}`,"# HELP ocpp_call_errors_total Total CALLERROR messages","# TYPE ocpp_call_errors_total counter",`ocpp_call_errors_total ${g}`,"# HELP ocpp_errors_total WebSocket/protocol errors","# TYPE ocpp_errors_total counter",`ocpp_errors_total ${y}`,"# HELP ocpp_bad_messages_total Malformed messages received","# TYPE ocpp_bad_messages_total counter",`ocpp_bad_messages_total ${p}`,"# HELP ocpp_handler_errors_total User handler errors","# TYPE ocpp_handler_errors_total counter",`ocpp_handler_errors_total ${f}`,"# HELP ocpp_rate_limit_hits_total Rate limit violations","# TYPE ocpp_rate_limit_hits_total counter",`ocpp_rate_limit_hits_total ${w}`,"# HELP ocpp_auth_failures_total Authentication failures","# TYPE ocpp_auth_failures_total counter",`ocpp_auth_failures_total ${P}`,"# HELP ocpp_evictions_total Client evictions","# TYPE ocpp_evictions_total counter",`ocpp_evictions_total ${_}`,"# HELP ocpp_backpressure_events_total Slow client backpressure events","# TYPE ocpp_backpressure_events_total counter",`ocpp_backpressure_events_total ${k}`,"# HELP ocpp_pong_timeouts_total Dead peer timeouts","# TYPE ocpp_pong_timeouts_total counter",`ocpp_pong_timeouts_total ${b}`,"# HELP ocpp_validation_failures_total Schema validation failures","# TYPE ocpp_validation_failures_total counter",`ocpp_validation_failures_total ${S}`,"# HELP ocpp_security_events_total Security events from anomaly detection","# TYPE ocpp_security_events_total counter",`ocpp_security_events_total ${A}`]},onClose(){t&&(clearInterval(t),t=null),C.clear();}}}function F(n){let d=n.topicPrefix??"ocpp",l=new Set(n.events??["connect","disconnect","message","security"]),c=n.qos??0,o=new Map;function s(e,t){return n.topicBuilder?n.topicBuilder(e,t):t?`${d}/${t}/${e}`:`${d}/${e}`}function r(e,t){let i=n.transform?n.transform(t):t,a=JSON.stringify(i);n.worker?n.worker.enqueue("mqtt-publish",()=>new Promise((u,m)=>{n.client.publish(e,a,{qos:c},g=>g?m(g):u());})):n.client.publish(e,a,{qos:c});}return {name:"mqtt",onConnection(e){o.set(e.identity,Date.now()),l.has("connect")&&r(s("connect",e.identity),{identity:e.identity,ip:e.handshake.remoteAddress,protocol:e.protocol,timestamp:new Date().toISOString()});},onDisconnect(e,t,i){if(!l.has("disconnect")){o.delete(e.identity);return}let a=o.get(e.identity),u=a?Math.round((Date.now()-a)/1e3):0;o.delete(e.identity),r(s("disconnect",e.identity),{identity:e.identity,code:t,reason:i,durationSec:u,timestamp:new Date().toISOString()});},onMessage(e,t){if(!l.has("message"))return;let i={identity:e.identity,direction:t.direction,messageType:t.message[0],timestamp:t.ctx.timestamp};t.message[0]===2&&t.message[2]&&(i.method=t.message[2]),t.ctx.latencyMs!==void 0&&(i.latencyMs=t.ctx.latencyMs),n.includePayload&&(i.payload=t.message),r(s(`message/${t.direction}`,e.identity),i);},onSecurityEvent(e){l.has("security")&&r(s("security",e.identity),{type:e.type,identity:e.identity,ip:e.ip,timestamp:e.timestamp,details:e.details});},onError(e,t){l.has("error")&&r(s("error",e.identity),{identity:e.identity,error:t.message,timestamp:new Date().toISOString()});},onAuthFailed(e,t,i){l.has("auth_failed")&&r(s("auth_failed"),{identity:e.identity,ip:e.remoteAddress,code:t,reason:i,timestamp:new Date().toISOString()});},onEviction(e,t){l.has("eviction")&&r(s("eviction",e.identity),{identity:e.identity,evictedBy:t.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){r(s("closing"),{timestamp:new Date().toISOString()});},onClose(){o.clear();try{n.client.end(!1);}catch{}}}}function H(n){let d=n?.tracer??null,l=new Map;return {name:"otel",async onInit(c){if(!d)try{d=(await import('@opentelemetry/api')).trace.getTracer(n?.serviceName??"ocpp-server","1.0.0");}catch{c.log.warn?.("otelPlugin: @opentelemetry/api not found \u2014 plugin disabled. Install it as a peer dependency."),d=null;}},onConnection(c){if(!d)return;let o=d.startSpan("ocpp.connection",{kind:1});o.setAttribute("ocpp.identity",c.identity),o.setAttribute("ocpp.protocol",c.protocol??"unknown"),o.setAttribute("net.peer.ip",c.handshake.remoteAddress),l.set(c.identity,{span:o,startTime:Date.now()});},onDisconnect(c,o){let s=l.get(c.identity);if(!s)return;let r=Date.now()-s.startTime;s.span.setAttribute("ocpp.close_code",o),s.span.setAttribute("ocpp.duration_ms",r),s.span.setStatus({code:1}),s.span.end(),l.delete(c.identity);},onMessage(c,o){if(!d)return;let s=o.message[0];if(s!==2){let t=l.get(c.identity);t&&t.span.addEvent(s===3?"ocpp.call_result":"ocpp.call_error",{direction:o.direction,"ocpp.message_id":String(o.message[1]),...o.ctx.latencyMs!==void 0&&{"ocpp.latency_ms":o.ctx.latencyMs}});return}let r=String(o.message[2]??"unknown"),e=d.startSpan(`ocpp.call.${r}`,{kind:o.direction==="IN"?1:2});e.setAttribute("ocpp.identity",c.identity),e.setAttribute("ocpp.method",r),e.setAttribute("ocpp.direction",o.direction),e.setAttribute("ocpp.message_id",String(o.message[1])),o.ctx.latencyMs!==void 0&&e.setAttribute("ocpp.latency_ms",o.ctx.latencyMs),e.setStatus({code:1}),e.end();},onError(c,o){let s=l.get(c.identity);s&&(s.span.recordException(o),s.span.addEvent("ocpp.error",{"error.message":o.message}));},onHandlerError(c,o,s){let r=l.get(c.identity);r&&(r.span.recordException(s),r.span.addEvent("ocpp.handler_error",{"ocpp.method":o,"error.message":s.message}));},onBadMessage(c,o,s){let r=l.get(c.identity);r&&(r.span.recordException(s),r.span.addEvent("ocpp.bad_message",{"raw.preview":typeof o=="string"?o.slice(0,200):"<buffer>","error.message":s.message}));},onValidationFailure(c,o,s){let r=l.get(c.identity);r&&(r.span.recordException(s),r.span.addEvent("ocpp.validation_failure",{"error.message":s.message}));},onRateLimitExceeded(c){let o=l.get(c.identity);o&&o.span.addEvent("ocpp.rate_limit_exceeded");},onPongTimeout(c){let o=l.get(c.identity);o&&o.span.addEvent("ocpp.pong_timeout");},onBackpressure(c,o){let s=l.get(c.identity);s&&s.span.addEvent("ocpp.backpressure",{"ocpp.buffered_bytes":o});},onEviction(c,o){let s=l.get(c.identity);s&&s.span.addEvent("ocpp.evicted",{"net.peer.ip.new":o.handshake.remoteAddress});},onTelemetry(c){if(!d)return;let o=d.startSpan("ocpp.telemetry_push",{kind:0});o.setAttribute("ocpp.connected_clients",c.connectedClients),o.setAttribute("ocpp.active_sessions",c.activeSessions),o.setAttribute("ocpp.uptime_seconds",c.uptimeSeconds),o.setAttribute("ocpp.memory_rss",c.memoryUsage.rss),o.setAttribute("ocpp.memory_heap_used",c.memoryUsage.heapUsed),o.setAttribute("ocpp.pid",c.pid),c.webSockets&&(o.setAttribute("ocpp.ws_total",c.webSockets.total),o.setAttribute("ocpp.ws_buffered_amount",c.webSockets.bufferedAmount)),o.setStatus({code:1}),o.end();},onSecurityEvent(c){if(!d)return;let o=d.startSpan("ocpp.security_event",{kind:0});o.setAttribute("security.event_type",c.type),c.identity&&o.setAttribute("ocpp.identity",c.identity),c.ip&&o.setAttribute("net.peer.ip",c.ip),o.setStatus({code:2,message:c.type}),o.end();},onAuthFailed(c,o,s){if(!d)return;let r=d.startSpan("ocpp.auth_failed",{kind:1});r.setAttribute("ocpp.identity",c.identity),r.setAttribute("net.peer.ip",c.remoteAddress),r.setAttribute("ocpp.close_code",o),r.setAttribute("ocpp.close_reason",s),r.setStatus({code:2,message:"Auth failed"}),r.end();},onClosing(){for(let[,c]of l)c.span.addEvent("ocpp.server_closing");},onClose(){for(let[,c]of l)c.span.setStatus({code:2,message:"Server shutdown"}),c.span.end();l.clear();}}}function W(n){if(!n||!Array.isArray(n.sensitiveKeys)||n.sensitiveKeys.length===0)throw new Error("piiRedactorPlugin requires a non-empty 'sensitiveKeys' array \u2014 explicitly list the keys to redact, e.g. piiRedactorPlugin({ sensitiveKeys: ['password', 'authorizationKey'] }).");let d=new Set(n.sensitiveKeys),l=n.replacement??"***REDACTED***",c=n.incoming??true,o=n.outgoing??true;function s(e){if(!e||typeof e!="object")return e;if(Array.isArray(e))return e.map(s);let t={};for(let[i,a]of Object.entries(e))d.has(i)?t[i]=l:a&&typeof a=="object"?t[i]=s(a):t[i]=a;return t}let r=async(e,t)=>{c&&(e.type==="incoming_call"&&e.params?e.params=s(e.params):e.type==="incoming_result"&&e.payload&&(e.payload=s(e.payload))),o&&(e.type==="outgoing_call"&&e.params?e.params=s(e.params):e.type==="outgoing_result"&&e.payload&&(e.payload=s(e.payload))),await t();};return {name:"pii-redactor",onConnection(e){e.use(r);}}}function K(n){let d=n.cooldownMs??6e4,l=n.threshold??1,c=n.windowMs??3e5,o=n.maxTrackedKeys??1e4,s=n.logger,r=new v(o),e=new v(o);function t(){return typeof n.sink=="string"?{async send(u){await fetch(n.sink,{method:"POST",headers:{"Content-Type":"application/json",...n.headers},body:JSON.stringify(u)});}}:n.sink}function i(u){let m=Date.now(),y=(r.get(u)??[]).filter(p=>m-p<c);return r.set(u,y),y}function a(u,m,g){let y=u??m??"unknown",p=Date.now(),f=i(y);if(f.push(p),f.length<l)return;let w=e.get(y)??0;if(p-w<d)return;e.set(y,p);let P=t(),_={eventType:g,identity:u,ip:m,timestamp:new Date().toISOString(),count:f.length,windowMs:c};Promise.resolve(P.send(_)).catch(k=>{s?.error?.("[rate-limit-notifier] Alert delivery failed:",k);});}return {name:"rate-limit-notifier",onRateLimitExceeded(u,m){a(u.identity,u.handshake.remoteAddress,"RATE_LIMIT_EXCEEDED");},onSecurityEvent(u){(u.type==="RATE_LIMIT_EXCEEDED"||u.type==="CONNECTION_RATE_LIMIT")&&a(u.identity,u.ip,u.type);},onClose(){r.clear(),e.clear();}}}function j(n){let d=n.mode??"pubsub",l=n.prefix??"ocpp",c=new Set(n.events??["connect","disconnect","message","security"]),o=n.maxStreamLength??1e4,s=n.serialize??JSON.stringify,r=new Map;function e(i){return `${l}:${i}`}function t(i,a){if(!c.has(i))return;let u=e(i),m=s(a),g=async()=>{d==="stream"&&n.client.xadd?await n.client.xadd(u,"MAXLEN","~",o,"*","data",m):await n.client.publish(u,m);};if(n.worker)n.worker.enqueue(`redis-${d}`,()=>g().catch(()=>{}));else try{g().catch?.(()=>{});}catch{}}return {name:"redis-pubsub",onConnection(i){r.set(i.identity,Date.now()),t("connect",{identity:i.identity,ip:i.handshake.remoteAddress,protocol:i.protocol,timestamp:new Date().toISOString()});},onDisconnect(i,a,u){let m=r.get(i.identity),g=m?Math.round((Date.now()-m)/1e3):0;r.delete(i.identity),t("disconnect",{identity:i.identity,code:a,reason:u,durationSec:g,timestamp:new Date().toISOString()});},onMessage(i,a){let u={identity:i.identity,direction:a.direction,messageType:a.message[0],timestamp:a.ctx.timestamp};a.message[0]===2&&a.message[2]&&(u.method=a.message[2]),a.ctx.latencyMs!==void 0&&(u.latencyMs=a.ctx.latencyMs),n.includePayload&&(u.payload=a.message),t(`message:${a.direction}`,u);},onSecurityEvent(i){t("security",{type:i.type,identity:i.identity,ip:i.ip,timestamp:i.timestamp,details:i.details});},onAuthFailed(i,a,u){t("auth_failed",{identity:i.identity,ip:i.remoteAddress,code:a,reason:u,timestamp:new Date().toISOString()});},onEviction(i,a){t("eviction",{identity:i.identity,evictedBy:a.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){t("closing",{timestamp:new Date().toISOString()});},onClose(){r.clear();try{n.client.quit?n.client.quit():n.client.disconnect&&n.client.disconnect();}catch{}}}}function Y(n){let d=n.redis,l=n.prefix??"ocpp:replay:",c=n.syntheticResponse??true,o=n.flushConcurrency??5,s=n.flushDelayMs??200,r=n.logger,e=new Set;function t(i){return new Promise(a=>setTimeout(a,i))}return {name:"replay-buffer",onConnection(i){let a=`${l}${i.identity}`,u=async(g,y)=>{if(g.type!=="outgoing_call")return y();try{return await y()}catch(p){let f=p instanceof Error?p.message:String(p);if(!(f.includes("WebSocket is not open")||f.includes("offline")||f.includes("CLOSED")||f.includes("CLOSING")))throw p;let P=JSON.stringify([2,g.messageId,g.method,g.params]);try{await d.rpush(a,P),r?.warn?.(`[replay-buffer] Queued offline command: ${g.method} for ${i.identity}`);}catch(_){throw r?.error?.(`[replay-buffer] Redis rpush failed for ${i.identity}:`,_),p}if(c)return {status:"Accepted",note:"Queued offline (ReplayBuffer)"};throw p}};i.use(u);let m=(async()=>{try{let g=0;for(;;){let y=await d.lpop(a);if(!y)break;let p;try{p=JSON.parse(y);}catch{r?.warn?.(`[replay-buffer] Skipping unparseable queued message for ${i.identity}`);continue}!Array.isArray(p)||p[0]!==2||(i.call(p[2],p[3]).catch(f=>{r?.warn?.(`[replay-buffer] Flush call failed for ${i.identity}/${p[2]}:`,f);}),g++,g>=o&&(await t(s),g=0));}}catch(g){r?.error?.(`[replay-buffer] Error flushing queue for ${i.identity}:`,g);}})();e.add(m),m.finally(()=>e.delete(m));},async onClosing(){e.size>0&&await Promise.allSettled([...e]);},onClose(){e.clear();}}}function V(n){let d=n.unmatchedBehavior??"passthrough",l=n.logger,c=new Map,o;for(let r of n.rules)r.method==="*"?o=r:c.set(r.method,r);function s(r){return c.get(r)??o}return {name:"schema-versioning",onConnection(r){if(n.applyWhen&&r.protocol!==n.applyWhen)return;let e=async(t,i)=>{let a=t.method,u=s(a);if(!u){if(d==="reject")throw l?.warn?.(`[schema-versioning] No transform rule for method "${a}", rejecting`),new Error(`Schema versioning: no transform rule for "${a}" (${n.sourceVersion} \u2192 ${n.targetVersion})`);return i()}if(t.type==="incoming_call")try{let m=u.transform(t.params,"up");t.params=m,l?.debug?.(`[schema-versioning] Transformed ${a} UP: ${n.sourceVersion} \u2192 ${n.targetVersion}`);}catch(m){l?.warn?.(`[schema-versioning] Transform UP failed for ${a}:`,m);}else if(t.type==="outgoing_call")try{let m=u.transform(t.params,"down");t.params=m,l?.debug?.(`[schema-versioning] Transformed ${a} DOWN: ${n.targetVersion} \u2192 ${n.sourceVersion}`);}catch(m){l?.warn?.(`[schema-versioning] Transform DOWN failed for ${a}:`,m);}else if(t.type==="outgoing_result")try{let m=u.transform(t.payload,"down");t.payload=m;}catch(m){l?.warn?.(`[schema-versioning] Transform DOWN (result) failed for ${a}:`,m);}return i()};r.use(e);}}}function z(n){let d=n?.logger??console,l=n?.logLevel??"standard",c=l==="standard"||l==="verbose",o=l==="verbose",s=new Map;return {name:"session-log",onConnection(r){s.set(r.identity,Date.now()),d.info("[session] connected",{identity:r.identity,ip:r.handshake.remoteAddress,protocol:r.protocol});},onDisconnect(r,e,t){let i=s.get(r.identity),a=i?Math.round((Date.now()-i)/1e3):0;s.delete(r.identity),d.info("[session] disconnected",{identity:r.identity,code:e,reason:t,durationSec:a});},onError(r,e){c&&(d.error??d.warn)("[session] error",{identity:r.identity,error:e.message});},onAuthFailed(r,e,t){c&&d.warn("[session] auth failed",{identity:r.identity,ip:r.remoteAddress,code:e,reason:t});},onEviction(r,e){c&&d.warn("[session] evicted",{identity:r.identity,evictedIp:r.handshake.remoteAddress,newIp:e.handshake.remoteAddress});},onBadMessage(r,e){o&&d.warn("[session] bad message",{identity:r.identity,raw:typeof e=="string"?e.slice(0,200):"<buffer>"});},onSecurityEvent(r){o&&d.warn("[session] security event",{type:r.type,identity:r.identity,ip:r.ip,details:r.details});},onHandlerError(r,e,t){o&&(d.error??d.warn)("[session] handler error",{identity:r.identity,method:e,error:t.message});},onValidationFailure(r,e,t){o&&d.warn("[session] validation failure",{identity:r.identity,error:t.message});},onRateLimitExceeded(r){c&&d.warn("[session] rate limit exceeded",{identity:r.identity,ip:r.handshake.remoteAddress});},onPongTimeout(r){o&&d.warn("[session] pong timeout (dead peer)",{identity:r.identity});},onBackpressure(r,e){o&&d.warn("[session] backpressure",{identity:r.identity,bufferedBytes:e});},onClose(){s.clear();}}}function X(n){let d=new Set(n.events??["init","connect","disconnect","close"]),l=n.timeout??5e3,c=n.retries??1;async function o(s){if(!d.has(s.event))return;let r=JSON.stringify(s),e={"Content-Type":"application/json",...n.headers};if(n.secret){let t=crypto.createHmac("sha256",n.secret).update(r).digest("hex");e["X-Signature"]=t;}for(let t=0;t<=c;t++){let i=new AbortController,a=setTimeout(()=>i.abort(),l);try{let u=await fetch(n.url,{method:"POST",headers:e,body:r,signal:i.signal});if(!u.ok)throw new Error(`Webhook responded with HTTP ${u.status}`);return}catch{t<c&&await new Promise(u=>setTimeout(u,250*2**t));}finally{clearTimeout(a);}}}return {name:"webhook",onInit(){o({event:"init",timestamp:new Date().toISOString()}).catch(()=>{});},onConnection(s){o({event:"connect",timestamp:new Date().toISOString(),data:{identity:s.identity,ip:s.handshake.remoteAddress,protocol:s.protocol}}).catch(()=>{});},onDisconnect(s,r,e){o({event:"disconnect",timestamp:new Date().toISOString(),data:{identity:s.identity,code:r,reason:e}}).catch(()=>{});},onSecurityEvent(s){o({event:"security",timestamp:s.timestamp,data:{type:s.type,identity:s.identity,ip:s.ip,details:s.details}}).catch(()=>{});},onAuthFailed(s,r,e){o({event:"auth_failed",timestamp:new Date().toISOString(),data:{identity:s.identity,ip:s.remoteAddress,code:r,reason:e}}).catch(()=>{});},onEviction(s,r){o({event:"eviction",timestamp:new Date().toISOString(),data:{identity:s.identity,evictedIp:s.handshake.remoteAddress,newIp:r.handshake.remoteAddress}}).catch(()=>{});},onClosing(){o({event:"closing",timestamp:new Date().toISOString()}).catch(()=>{});},onClose(){}}}exports.amqpPlugin=x;exports.anomalyPlugin=R;exports.asyncWorkerPlugin=L;exports.circuitBreakerPlugin=D;exports.connectionGuardPlugin=$;exports.heartbeatPlugin=I;exports.kafkaPlugin=N;exports.messageDedupPlugin=q;exports.metricsPlugin=B;exports.mqttPlugin=F;exports.otelPlugin=H;exports.piiRedactorPlugin=W;exports.rateLimitNotifierPlugin=K;exports.redisPubSubPlugin=j;exports.replayBufferPlugin=Y;exports.schemaVersioningPlugin=V;exports.sessionLogPlugin=z;exports.webhookPlugin=X;
|
package/dist/plugins.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import {createHmac}from'crypto';function R(r){let u=r.exchange??"ocpp.events",d=r.routingKey??"ocpp.{event}.{identity}",c=new Set(r.events??["connect","disconnect","message","security"]),a={persistent:r.publishOptions?.persistent??true,contentType:r.publishOptions?.contentType??"application/json",...r.publishOptions?.priority!==void 0&&{priority:r.publishOptions.priority}},s=new Map;function n(t,i){return d.replace("{event}",t).replace("{identity}",i??"server")}function e(t,i,o){if(!c.has(t))return;let l=n(t,i),m=Buffer.from(JSON.stringify(o));if(r.worker)r.worker.enqueue("amqp-publish",async()=>{r.channel.publish(u,l,m,a);});else try{r.channel.publish(u,l,m,a);}catch{}}return {name:"amqp",onConnection(t){s.set(t.identity,Date.now()),e("connect",t.identity,{identity:t.identity,ip:t.handshake.remoteAddress,protocol:t.protocol,timestamp:new Date().toISOString()});},onDisconnect(t,i,o){let l=s.get(t.identity),m=l?Math.round((Date.now()-l)/1e3):0;s.delete(t.identity),e("disconnect",t.identity,{identity:t.identity,code:i,reason:o,durationSec:m,timestamp:new Date().toISOString()});},onMessage(t,i){let o={identity:t.identity,direction:i.direction,messageType:i.message[0],timestamp:i.ctx.timestamp};i.message[0]===2&&i.message[2]&&(o.method=i.message[2]),i.ctx.latencyMs!==void 0&&(o.latencyMs=i.ctx.latencyMs),r.includePayload&&(o.payload=i.message),e(`message.${i.direction}`,t.identity,o);},onSecurityEvent(t){e("security",t.identity,{type:t.type,identity:t.identity,ip:t.ip,timestamp:t.timestamp,details:t.details});},onAuthFailed(t,i,o){e("auth_failed",t.identity,{identity:t.identity,ip:t.remoteAddress,code:i,reason:o,timestamp:new Date().toISOString()});},onEviction(t,i){e("eviction",t.identity,{identity:t.identity,evictedBy:i.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){e("closing",void 0,{timestamp:new Date().toISOString()});},onClose(){s.clear();try{r.channel.close();}catch{}}}}function x(r){let u=r?.reconnectThreshold??5,d=r?.authFailureThreshold??5,c=r?.badMessageThreshold??10,a=r?.evictionThreshold??3,s=r?.windowMs??6e4,n=new Map,e=new Map,t=new Map,i=new Map,o=null,l=null;function m(p,f){let _=f-s,w=0;for(;w<p.length&&p[w]<_;)w++;return w>0?p.slice(w):p}function g(p,f){for(let[_,w]of p){let b=m(w,f);b.length===0?p.delete(_):p.set(_,b);}}function y(p,f,_,w,b){let v=Date.now(),k=p.get(f)??[];if(k=m(k,v),k.push(v),p.set(f,k),k.length>_&&o){let S={type:w,identity:b.identity,ip:b.ip??b.evictedIp,timestamp:new Date().toISOString(),details:{...b,countInWindow:k.length,threshold:_,windowMs:s}};o.emit("securityEvent",S);}}return {name:"anomaly",onInit(p){o=p,l=setInterval(()=>{let f=Date.now();g(n,f),g(e,f),g(t,f),g(i,f);},s).unref();},onConnection(p){y(n,p.identity,u,"ANOMALY_RAPID_RECONNECT",{identity:p.identity,ip:p.handshake.remoteAddress});},onAuthFailed(p,f,_){y(e,p.remoteAddress,d,"ANOMALY_AUTH_BRUTE_FORCE",{ip:p.remoteAddress,identity:p.identity,code:f,reason:_});},onBadMessage(p){y(t,p.identity,c,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress});},onValidationFailure(p){y(t,p.identity,c,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress,source:"validation_failure"});},onEviction(p,f){y(i,p.identity,a,"ANOMALY_IDENTITY_COLLISION",{identity:p.identity,evictedIp:p.handshake.remoteAddress,newIp:f.handshake.remoteAddress});},onClose(){l&&(clearInterval(l),l=null),n.clear(),e.clear(),t.clear(),i.clear(),o=null;}}}function L(r){let u=r?.concurrency??10,d=r?.maxQueueSize??1e3,c=r?.overflowStrategy??"drop-oldest",a=r?.drainTimeoutMs??5e3,s=[],n=0,e=0,t=true,i=null;function o(){for(;n<u&&s.length>0;){let g=s.shift();n++,g.fn().catch(y=>{if(r?.onError)try{r.onError(y instanceof Error?y:new Error(String(y)),g.name);}catch{}}).finally(()=>{n--,!t&&n===0&&s.length===0&&i&&(i(),i=null),o();});}}function l(g,y){if(!t)return false;if(s.length>=d){if(c==="drop-newest")return e++,r?.logger?.warn?.(`[async-worker] Queue full (${d}), dropping task: ${g}`),false;let p=s.shift();e++,r?.logger?.warn?.(`[async-worker] Queue full (${d}), dropping oldest task: ${p?.name??"unknown"}`);}return s.push({name:g,fn:y}),o(),true}return {name:"async-worker",enqueue:l,queueSize:()=>s.length,activeCount:()=>n,droppedCount:()=>e,getCustomMetrics(){return ["# HELP ocpp_async_worker_queue_size Current tasks waiting in the background queue","# TYPE ocpp_async_worker_queue_size gauge",`ocpp_async_worker_queue_size ${s.length}`,"# HELP ocpp_async_worker_active_tasks Currently executing background tasks","# TYPE ocpp_async_worker_active_tasks gauge",`ocpp_async_worker_active_tasks ${n}`,"# HELP ocpp_async_worker_dropped_total Tasks dropped due to queue overflow","# TYPE ocpp_async_worker_dropped_total counter",`ocpp_async_worker_dropped_total ${e}`]},onClosing(){return t=false,n===0&&s.length===0?Promise.resolve():new Promise(g=>{i=g;let y=setTimeout(()=>{r?.logger?.warn?.(`[async-worker] Drain timeout (${a}ms), ${n} tasks still active, ${s.length} queued`),s.length=0,i=null,g();},a);y&&typeof y=="object"&&"unref"in y&&y.unref();})},onClose(){t=false,s.length=0,i=null;}}}function D(r){let u=r?.failureThreshold??5,d=r?.resetTimeoutMs??3e4,c=r?.maxConcurrent??20,a=r?.logger,s=r?.onStateChange,n=new Map;function e(i){let o=n.get(i);return o||(o={state:"CLOSED",failures:0,lastFailure:0,concurrentCalls:0},n.set(i,o)),o}function t(i,o){let l=e(i),m=l.state;m!==o&&(l.state=o,a?.warn?.(`[circuit-breaker] ${i}: ${m} \u2192 ${o}`),s?.(i,m,o));}return {name:"circuit-breaker",onConnection(i){let o=e(i.identity);i.use(async(l,m)=>{if(l.type!=="outgoing_call")return m();if(o.concurrentCalls>=c)throw a?.warn?.(`[circuit-breaker] ${i.identity}: concurrent limit (${c}) reached, rejecting ${l.method}`),new Error(`Circuit breaker: concurrent call limit exceeded for ${i.identity}`);let g=Date.now();if(o.state==="OPEN")if(g-o.lastFailure>=d)t(i.identity,"HALF_OPEN");else throw new Error(`Circuit breaker OPEN for ${i.identity}: ${o.failures} consecutive failures`);o.concurrentCalls++;try{let y=await m();return o.concurrentCalls--,o.state==="HALF_OPEN"?(t(i.identity,"CLOSED"),o.failures=0):o.failures=Math.max(0,o.failures-1),y}catch(y){throw o.concurrentCalls--,o.failures++,o.lastFailure=Date.now(),(o.state==="HALF_OPEN"||o.failures>=u)&&t(i.identity,"OPEN"),y}});},onDisconnect(i){let o=n.get(i.identity);o&&(o.concurrentCalls=0);},onClose(){n.clear();}}}function $(r){let u=r.maxConnections,d=r.closeCode??4029,c=r.closeReason??"Connection limit reached",a=r.forceCloseOnPongTimeout??true,s=r.forceCloseOnBackpressure??false,n=0;return {name:"connection-guard",onConnection(e){n++,n>u&&(r.logger?.warn?.(`[connection-guard] Limit exceeded (${n}/${u}), closing: ${e.identity}`),e.close({code:d,reason:c}));},onDisconnect(){n=Math.max(0,n-1);},onPongTimeout(e){a&&(r.logger?.warn?.(`[connection-guard] Pong timeout \u2014 closing dead peer: ${e.identity}`),e.close({code:4e3,reason:"Pong timeout"}));},onBackpressure(e,t){s&&(r.logger?.warn?.(`[connection-guard] Backpressure (${t} bytes) \u2014 closing slow client: ${e.identity}`),e.close({code:4001,reason:"Backpressure exceeded"}));},onClose(){n=0;}}}function I(){return {name:"heartbeat",onConnection(r){r.handle("Heartbeat",()=>({currentTime:new Date().toISOString()}));}}}function N(r){let u=r.topic??"ocpp.events",d=r.topicRouting??false,c=new Set(r.events??["connect","disconnect","message","security"]),a=new Map;function s(e){return d?`${u}.${e}`:u}function n(e,t,i){if(!c.has(e.split(".")[0]))return;let o=s(e.split(".")[0]),l=JSON.stringify(i),m=t??"server";r.worker?r.worker.enqueue("kafka-publish",async()=>{await r.producer.send({topic:o,messages:[{key:m,value:l,headers:{event:e}}]});}):r.producer.send({topic:o,messages:[{key:m,value:l,headers:{event:e}}]}).catch(()=>{});}return {name:"kafka",onConnection(e){a.set(e.identity,Date.now()),n("connect",e.identity,{identity:e.identity,ip:e.handshake.remoteAddress,protocol:e.protocol,timestamp:new Date().toISOString()});},onDisconnect(e,t,i){let o=a.get(e.identity),l=o?Math.round((Date.now()-o)/1e3):0;a.delete(e.identity),n("disconnect",e.identity,{identity:e.identity,code:t,reason:i,durationSec:l,timestamp:new Date().toISOString()});},onMessage(e,t){let i={identity:e.identity,direction:t.direction,messageType:t.message[0],timestamp:t.ctx.timestamp};t.message[0]===2&&t.message[2]&&(i.method=t.message[2]),t.ctx.latencyMs!==void 0&&(i.latencyMs=t.ctx.latencyMs),r.includePayload&&(i.payload=t.message),n(`message.${t.direction}`,e.identity,i);},onSecurityEvent(e){n("security",e.identity,{type:e.type,identity:e.identity,ip:e.ip,timestamp:e.timestamp,details:e.details});},onAuthFailed(e,t,i){n("auth_failed",e.identity,{identity:e.identity,ip:e.remoteAddress,code:t,reason:i,timestamp:new Date().toISOString()});},onEviction(e,t){n("eviction",e.identity,{identity:e.identity,evictedBy:t.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){n("closing",void 0,{timestamp:new Date().toISOString()});},onClose(){a.clear();}}}function q(r){let u=r.redis,d=r.ttlMs??3e5,c=r.prefix??"ocpp:dedup:",a=r.redisStyle??"positional",s=r.logger;async function n(e){return a==="options"?await u.set(e,"1",{PX:d,NX:true})!==null:await u.set(e,"1","PX",d,"NX")==="OK"}return {name:"message-dedup",async onBeforeReceive(e,t){let i;try{let l=typeof t=="string"?t:t?.toString()||"",m=JSON.parse(l);Array.isArray(m)&&m.length>1&&(i=String(m[1]));}catch{return}if(!i)return;let o=`${c}${e.identity}:${i}`;try{if(!await n(o))return s?.warn?.(`[message-dedup] Dropping duplicate message: ${o}`),!1}catch(l){s?.error?.("[message-dedup] Redis failure, falling through:",l);}}}}function B(r){let u=r?.intervalMs??3e4,d=0,c=0,a=0,s=0,n=0,e=Date.now(),t=null,i=0,o=0,l=0,m=0,g=0,y=0,p=0,f=0,_=0,w=0,b=0,v=0,k=0,S=0,A=0,C=new Map;function M(){return {totalConnections:d,totalDisconnections:c,activeConnections:a,peakConnections:s,connectionDurationAvgMs:c>0?Math.round(n/c):0,uptimeMs:Date.now()-e,timestamp:new Date().toISOString(),totalMessagesIn:i,totalMessagesOut:o,totalCalls:l,totalCallResults:m,totalCallErrors:g,totalErrors:y,totalBadMessages:p,totalHandlerErrors:f,totalRateLimitHits:_,totalAuthFailures:w,totalEvictions:b,totalBackpressureEvents:v,totalPongTimeouts:k,totalValidationFailures:S,totalSecurityEvents:A}}return {name:"metrics",getMetrics:M,onInit(){e=Date.now(),u>0&&r?.onSnapshot&&(t=setInterval(()=>{r.onSnapshot(M());},u),t&&typeof t=="object"&&"unref"in t&&t.unref());},onConnection(E){d++,a++,a>s&&(s=a),C.set(E.identity,Date.now());},onDisconnect(E){c++,a=Math.max(0,a-1);let O=C.get(E.identity);O&&(n+=Date.now()-O,C.delete(E.identity));},onMessage(E,O){O.direction==="IN"?i++:o++;let T=O.message[0];T===2?l++:T===3?m++:T===4&&g++;},onError(){y++;},onBadMessage(){p++;},onHandlerError(){f++;},onRateLimitExceeded(){_++;},onAuthFailed(){w++;},onEviction(){b++;},onBackpressure(){v++;},onPongTimeout(){k++;},onValidationFailure(){S++;},onSecurityEvent(){A++;},getCustomMetrics(){return ["# HELP ocpp_connections_total Total connections since server start","# TYPE ocpp_connections_total counter",`ocpp_connections_total ${d}`,"# HELP ocpp_disconnections_total Total disconnections since server start","# TYPE ocpp_disconnections_total counter",`ocpp_disconnections_total ${c}`,"# HELP ocpp_connections_active Currently active connections","# TYPE ocpp_connections_active gauge",`ocpp_connections_active ${a}`,"# HELP ocpp_connections_peak Highest concurrent connections","# TYPE ocpp_connections_peak gauge",`ocpp_connections_peak ${s}`,"# HELP ocpp_connection_duration_avg_ms Average connection duration","# TYPE ocpp_connection_duration_avg_ms gauge",`ocpp_connection_duration_avg_ms ${M().connectionDurationAvgMs}`,"# HELP ocpp_messages_in_total Total inbound messages","# TYPE ocpp_messages_in_total counter",`ocpp_messages_in_total ${i}`,"# HELP ocpp_messages_out_total Total outbound messages","# TYPE ocpp_messages_out_total counter",`ocpp_messages_out_total ${o}`,"# HELP ocpp_calls_total Total CALL messages","# TYPE ocpp_calls_total counter",`ocpp_calls_total ${l}`,"# HELP ocpp_call_results_total Total CALLRESULT messages","# TYPE ocpp_call_results_total counter",`ocpp_call_results_total ${m}`,"# HELP ocpp_call_errors_total Total CALLERROR messages","# TYPE ocpp_call_errors_total counter",`ocpp_call_errors_total ${g}`,"# HELP ocpp_errors_total WebSocket/protocol errors","# TYPE ocpp_errors_total counter",`ocpp_errors_total ${y}`,"# HELP ocpp_bad_messages_total Malformed messages received","# TYPE ocpp_bad_messages_total counter",`ocpp_bad_messages_total ${p}`,"# HELP ocpp_handler_errors_total User handler errors","# TYPE ocpp_handler_errors_total counter",`ocpp_handler_errors_total ${f}`,"# HELP ocpp_rate_limit_hits_total Rate limit violations","# TYPE ocpp_rate_limit_hits_total counter",`ocpp_rate_limit_hits_total ${_}`,"# HELP ocpp_auth_failures_total Authentication failures","# TYPE ocpp_auth_failures_total counter",`ocpp_auth_failures_total ${w}`,"# HELP ocpp_evictions_total Client evictions","# TYPE ocpp_evictions_total counter",`ocpp_evictions_total ${b}`,"# HELP ocpp_backpressure_events_total Slow client backpressure events","# TYPE ocpp_backpressure_events_total counter",`ocpp_backpressure_events_total ${v}`,"# HELP ocpp_pong_timeouts_total Dead peer timeouts","# TYPE ocpp_pong_timeouts_total counter",`ocpp_pong_timeouts_total ${k}`,"# HELP ocpp_validation_failures_total Schema validation failures","# TYPE ocpp_validation_failures_total counter",`ocpp_validation_failures_total ${S}`,"# HELP ocpp_security_events_total Security events from anomaly detection","# TYPE ocpp_security_events_total counter",`ocpp_security_events_total ${A}`]},onClose(){t&&(clearInterval(t),t=null),C.clear();}}}function F(r){let u=r.topicPrefix??"ocpp",d=new Set(r.events??["connect","disconnect","message","security"]),c=r.qos??0,a=new Map;function s(e,t){return r.topicBuilder?r.topicBuilder(e,t):t?`${u}/${t}/${e}`:`${u}/${e}`}function n(e,t){let i=r.transform?r.transform(t):t,o=JSON.stringify(i);r.worker?r.worker.enqueue("mqtt-publish",()=>new Promise((l,m)=>{r.client.publish(e,o,{qos:c},g=>g?m(g):l());})):r.client.publish(e,o,{qos:c});}return {name:"mqtt",onConnection(e){a.set(e.identity,Date.now()),d.has("connect")&&n(s("connect",e.identity),{identity:e.identity,ip:e.handshake.remoteAddress,protocol:e.protocol,timestamp:new Date().toISOString()});},onDisconnect(e,t,i){if(!d.has("disconnect")){a.delete(e.identity);return}let o=a.get(e.identity),l=o?Math.round((Date.now()-o)/1e3):0;a.delete(e.identity),n(s("disconnect",e.identity),{identity:e.identity,code:t,reason:i,durationSec:l,timestamp:new Date().toISOString()});},onMessage(e,t){if(!d.has("message"))return;let i={identity:e.identity,direction:t.direction,messageType:t.message[0],timestamp:t.ctx.timestamp};t.message[0]===2&&t.message[2]&&(i.method=t.message[2]),t.ctx.latencyMs!==void 0&&(i.latencyMs=t.ctx.latencyMs),r.includePayload&&(i.payload=t.message),n(s(`message/${t.direction}`,e.identity),i);},onSecurityEvent(e){d.has("security")&&n(s("security",e.identity),{type:e.type,identity:e.identity,ip:e.ip,timestamp:e.timestamp,details:e.details});},onError(e,t){d.has("error")&&n(s("error",e.identity),{identity:e.identity,error:t.message,timestamp:new Date().toISOString()});},onAuthFailed(e,t,i){d.has("auth_failed")&&n(s("auth_failed"),{identity:e.identity,ip:e.remoteAddress,code:t,reason:i,timestamp:new Date().toISOString()});},onEviction(e,t){d.has("eviction")&&n(s("eviction",e.identity),{identity:e.identity,evictedBy:t.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){n(s("closing"),{timestamp:new Date().toISOString()});},onClose(){a.clear();try{r.client.end(!1);}catch{}}}}function H(r){let u=r?.tracer??null,d=new Map;return {name:"otel",async onInit(c){if(!u)try{u=(await import('@opentelemetry/api')).trace.getTracer(r?.serviceName??"ocpp-server","1.0.0");}catch{c.log.warn?.("otelPlugin: @opentelemetry/api not found \u2014 plugin disabled. Install it as a peer dependency."),u=null;}},onConnection(c){if(!u)return;let a=u.startSpan("ocpp.connection",{kind:1});a.setAttribute("ocpp.identity",c.identity),a.setAttribute("ocpp.protocol",c.protocol??"unknown"),a.setAttribute("net.peer.ip",c.handshake.remoteAddress),d.set(c.identity,{span:a,startTime:Date.now()});},onDisconnect(c,a){let s=d.get(c.identity);if(!s)return;let n=Date.now()-s.startTime;s.span.setAttribute("ocpp.close_code",a),s.span.setAttribute("ocpp.duration_ms",n),s.span.setStatus({code:1}),s.span.end(),d.delete(c.identity);},onMessage(c,a){if(!u)return;let s=a.message[0];if(s!==2){let t=d.get(c.identity);t&&t.span.addEvent(s===3?"ocpp.call_result":"ocpp.call_error",{direction:a.direction,"ocpp.message_id":String(a.message[1]),...a.ctx.latencyMs!==void 0&&{"ocpp.latency_ms":a.ctx.latencyMs}});return}let n=String(a.message[2]??"unknown"),e=u.startSpan(`ocpp.call.${n}`,{kind:a.direction==="IN"?1:2});e.setAttribute("ocpp.identity",c.identity),e.setAttribute("ocpp.method",n),e.setAttribute("ocpp.direction",a.direction),e.setAttribute("ocpp.message_id",String(a.message[1])),a.ctx.latencyMs!==void 0&&e.setAttribute("ocpp.latency_ms",a.ctx.latencyMs),e.setStatus({code:1}),e.end();},onError(c,a){let s=d.get(c.identity);s&&(s.span.recordException(a),s.span.addEvent("ocpp.error",{"error.message":a.message}));},onHandlerError(c,a,s){let n=d.get(c.identity);n&&(n.span.recordException(s),n.span.addEvent("ocpp.handler_error",{"ocpp.method":a,"error.message":s.message}));},onBadMessage(c,a,s){let n=d.get(c.identity);n&&(n.span.recordException(s),n.span.addEvent("ocpp.bad_message",{"raw.preview":typeof a=="string"?a.slice(0,200):"<buffer>","error.message":s.message}));},onValidationFailure(c,a,s){let n=d.get(c.identity);n&&(n.span.recordException(s),n.span.addEvent("ocpp.validation_failure",{"error.message":s.message}));},onRateLimitExceeded(c){let a=d.get(c.identity);a&&a.span.addEvent("ocpp.rate_limit_exceeded");},onPongTimeout(c){let a=d.get(c.identity);a&&a.span.addEvent("ocpp.pong_timeout");},onBackpressure(c,a){let s=d.get(c.identity);s&&s.span.addEvent("ocpp.backpressure",{"ocpp.buffered_bytes":a});},onEviction(c,a){let s=d.get(c.identity);s&&s.span.addEvent("ocpp.evicted",{"net.peer.ip.new":a.handshake.remoteAddress});},onTelemetry(c){if(!u)return;let a=u.startSpan("ocpp.telemetry_push",{kind:0});a.setAttribute("ocpp.connected_clients",c.connectedClients),a.setAttribute("ocpp.active_sessions",c.activeSessions),a.setAttribute("ocpp.uptime_seconds",c.uptimeSeconds),a.setAttribute("ocpp.memory_rss",c.memoryUsage.rss),a.setAttribute("ocpp.memory_heap_used",c.memoryUsage.heapUsed),a.setAttribute("ocpp.pid",c.pid),c.webSockets&&(a.setAttribute("ocpp.ws_total",c.webSockets.total),a.setAttribute("ocpp.ws_buffered_amount",c.webSockets.bufferedAmount)),a.setStatus({code:1}),a.end();},onSecurityEvent(c){if(!u)return;let a=u.startSpan("ocpp.security_event",{kind:0});a.setAttribute("security.event_type",c.type),c.identity&&a.setAttribute("ocpp.identity",c.identity),c.ip&&a.setAttribute("net.peer.ip",c.ip),a.setStatus({code:2,message:c.type}),a.end();},onAuthFailed(c,a,s){if(!u)return;let n=u.startSpan("ocpp.auth_failed",{kind:1});n.setAttribute("ocpp.identity",c.identity),n.setAttribute("net.peer.ip",c.remoteAddress),n.setAttribute("ocpp.close_code",a),n.setAttribute("ocpp.close_reason",s),n.setStatus({code:2,message:"Auth failed"}),n.end();},onClosing(){for(let[,c]of d)c.span.addEvent("ocpp.server_closing");},onClose(){for(let[,c]of d)c.span.setStatus({code:2,message:"Server shutdown"}),c.span.end();d.clear();}}}function W(r={}){let u=new Set(r.sensitiveKeys??["idTag","authorizationKey","token","password","securityCode"]),d=r.replacement??"***REDACTED***",c=r.incoming??true,a=r.outgoing??true;function s(e){if(!e||typeof e!="object")return e;if(Array.isArray(e))return e.map(s);let t={};for(let[i,o]of Object.entries(e))u.has(i)?t[i]=d:o&&typeof o=="object"?t[i]=s(o):t[i]=o;return t}let n=async(e,t)=>{c&&(e.type==="incoming_call"&&e.params?e.params=s(e.params):e.type==="incoming_result"&&e.payload&&(e.payload=s(e.payload))),a&&(e.type==="outgoing_call"&&e.params?e.params=s(e.params):e.type==="outgoing_result"&&e.payload&&(e.payload=s(e.payload))),await t();};return {name:"pii-redactor",onConnection(e){e.use(n);}}}function j(r){let u=r.cooldownMs??6e4,d=r.threshold??1,c=r.windowMs??3e5,a=r.logger,s=new Map,n=new Map;function e(){return typeof r.sink=="string"?{async send(o){await fetch(r.sink,{method:"POST",headers:{"Content-Type":"application/json",...r.headers},body:JSON.stringify(o)});}}:r.sink}function t(o){let l=Date.now(),g=(s.get(o)??[]).filter(y=>l-y<c);return s.set(o,g),g}function i(o,l,m){let g=o??l??"unknown",y=Date.now(),p=t(g);if(p.push(y),p.length<d)return;let f=n.get(g)??0;if(y-f<u)return;n.set(g,y);let _=e(),w={eventType:m,identity:o,ip:l,timestamp:new Date().toISOString(),count:p.length,windowMs:c};Promise.resolve(_.send(w)).catch(b=>{a?.error?.("[rate-limit-notifier] Alert delivery failed:",b);});}return {name:"rate-limit-notifier",onRateLimitExceeded(o,l){i(o.identity,o.handshake.remoteAddress,"RATE_LIMIT_EXCEEDED");},onSecurityEvent(o){(o.type==="RATE_LIMIT_EXCEEDED"||o.type==="CONNECTION_RATE_LIMIT")&&i(o.identity,o.ip,o.type);},onClose(){s.clear(),n.clear();}}}function Y(r){let u=r.mode??"pubsub",d=r.prefix??"ocpp",c=new Set(r.events??["connect","disconnect","message","security"]),a=r.maxStreamLength??1e4,s=r.serialize??JSON.stringify,n=new Map;function e(i){return `${d}:${i}`}function t(i,o){if(!c.has(i))return;let l=e(i),m=s(o),g=async()=>{u==="stream"&&r.client.xadd?await r.client.xadd(l,"MAXLEN","~",a,"*","data",m):await r.client.publish(l,m);};if(r.worker)r.worker.enqueue(`redis-${u}`,()=>g().catch(()=>{}));else try{g().catch?.(()=>{});}catch{}}return {name:"redis-pubsub",onConnection(i){n.set(i.identity,Date.now()),t("connect",{identity:i.identity,ip:i.handshake.remoteAddress,protocol:i.protocol,timestamp:new Date().toISOString()});},onDisconnect(i,o,l){let m=n.get(i.identity),g=m?Math.round((Date.now()-m)/1e3):0;n.delete(i.identity),t("disconnect",{identity:i.identity,code:o,reason:l,durationSec:g,timestamp:new Date().toISOString()});},onMessage(i,o){let l={identity:i.identity,direction:o.direction,messageType:o.message[0],timestamp:o.ctx.timestamp};o.message[0]===2&&o.message[2]&&(l.method=o.message[2]),o.ctx.latencyMs!==void 0&&(l.latencyMs=o.ctx.latencyMs),r.includePayload&&(l.payload=o.message),t(`message:${o.direction}`,l);},onSecurityEvent(i){t("security",{type:i.type,identity:i.identity,ip:i.ip,timestamp:i.timestamp,details:i.details});},onAuthFailed(i,o,l){t("auth_failed",{identity:i.identity,ip:i.remoteAddress,code:o,reason:l,timestamp:new Date().toISOString()});},onEviction(i,o){t("eviction",{identity:i.identity,evictedBy:o.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){t("closing",{timestamp:new Date().toISOString()});},onClose(){n.clear();try{r.client.quit?r.client.quit():r.client.disconnect&&r.client.disconnect();}catch{}}}}function K(r){let u=r.redis,d=r.prefix??"ocpp:replay:",c=r.syntheticResponse??true,a=r.flushConcurrency??5,s=r.flushDelayMs??200,n=r.logger,e=new Set;function t(i){return new Promise(o=>setTimeout(o,i))}return {name:"replay-buffer",onConnection(i){let o=`${d}${i.identity}`,l=async(g,y)=>{if(g.type!=="outgoing_call")return y();try{return await y()}catch(p){let f=p instanceof Error?p.message:String(p);if(!(f.includes("WebSocket is not open")||f.includes("offline")||f.includes("CLOSED")||f.includes("CLOSING")))throw p;let w=JSON.stringify([2,g.messageId,g.method,g.params]);try{await u.rpush(o,w),n?.warn?.(`[replay-buffer] Queued offline command: ${g.method} for ${i.identity}`);}catch(b){throw n?.error?.(`[replay-buffer] Redis rpush failed for ${i.identity}:`,b),p}if(c)return {status:"Accepted",note:"Queued offline (ReplayBuffer)"};throw p}};i.use(l);let m=(async()=>{try{let g=0;for(;;){let y=await u.lpop(o);if(!y)break;let p;try{p=JSON.parse(y);}catch{n?.warn?.(`[replay-buffer] Skipping unparseable queued message for ${i.identity}`);continue}!Array.isArray(p)||p[0]!==2||(i.call(p[2],p[3]).catch(f=>{n?.warn?.(`[replay-buffer] Flush call failed for ${i.identity}/${p[2]}:`,f);}),g++,g>=a&&(await t(s),g=0));}}catch(g){n?.error?.(`[replay-buffer] Error flushing queue for ${i.identity}:`,g);}})();e.add(m),m.finally(()=>e.delete(m));},async onClosing(){e.size>0&&await Promise.allSettled([...e]);},onClose(){e.clear();}}}function V(r){let u=r.unmatchedBehavior??"passthrough",d=r.logger,c=new Map,a;for(let n of r.rules)n.method==="*"?a=n:c.set(n.method,n);function s(n){return c.get(n)??a}return {name:"schema-versioning",onConnection(n){if(r.applyWhen&&n.protocol!==r.applyWhen)return;let e=async(t,i)=>{let o=t.method,l=s(o);if(!l){if(u==="reject")throw d?.warn?.(`[schema-versioning] No transform rule for method "${o}", rejecting`),new Error(`Schema versioning: no transform rule for "${o}" (${r.sourceVersion} \u2192 ${r.targetVersion})`);return i()}if(t.type==="incoming_call")try{let m=l.transform(t.params,"up");t.params=m,d?.debug?.(`[schema-versioning] Transformed ${o} UP: ${r.sourceVersion} \u2192 ${r.targetVersion}`);}catch(m){d?.warn?.(`[schema-versioning] Transform UP failed for ${o}:`,m);}else if(t.type==="outgoing_call")try{let m=l.transform(t.params,"down");t.params=m,d?.debug?.(`[schema-versioning] Transformed ${o} DOWN: ${r.targetVersion} \u2192 ${r.sourceVersion}`);}catch(m){d?.warn?.(`[schema-versioning] Transform DOWN failed for ${o}:`,m);}else if(t.type==="outgoing_result")try{let m=l.transform(t.payload,"down");t.payload=m;}catch(m){d?.warn?.(`[schema-versioning] Transform DOWN (result) failed for ${o}:`,m);}return i()};n.use(e);}}}function z(r){let u=r?.logger??console,d=r?.logLevel??"standard",c=d==="standard"||d==="verbose",a=d==="verbose",s=new Map;return {name:"session-log",onConnection(n){s.set(n.identity,Date.now()),u.info("[session] connected",{identity:n.identity,ip:n.handshake.remoteAddress,protocol:n.protocol});},onDisconnect(n,e,t){let i=s.get(n.identity),o=i?Math.round((Date.now()-i)/1e3):0;s.delete(n.identity),u.info("[session] disconnected",{identity:n.identity,code:e,reason:t,durationSec:o});},onError(n,e){c&&(u.error??u.warn)("[session] error",{identity:n.identity,error:e.message});},onAuthFailed(n,e,t){c&&u.warn("[session] auth failed",{identity:n.identity,ip:n.remoteAddress,code:e,reason:t});},onEviction(n,e){c&&u.warn("[session] evicted",{identity:n.identity,evictedIp:n.handshake.remoteAddress,newIp:e.handshake.remoteAddress});},onBadMessage(n,e){a&&u.warn("[session] bad message",{identity:n.identity,raw:typeof e=="string"?e.slice(0,200):"<buffer>"});},onSecurityEvent(n){a&&u.warn("[session] security event",{type:n.type,identity:n.identity,ip:n.ip,details:n.details});},onHandlerError(n,e,t){a&&(u.error??u.warn)("[session] handler error",{identity:n.identity,method:e,error:t.message});},onValidationFailure(n,e,t){a&&u.warn("[session] validation failure",{identity:n.identity,error:t.message});},onRateLimitExceeded(n){c&&u.warn("[session] rate limit exceeded",{identity:n.identity,ip:n.handshake.remoteAddress});},onPongTimeout(n){a&&u.warn("[session] pong timeout (dead peer)",{identity:n.identity});},onBackpressure(n,e){a&&u.warn("[session] backpressure",{identity:n.identity,bufferedBytes:e});},onClose(){s.clear();}}}function G(r){let u=new Set(r.events??["init","connect","disconnect","close"]),d=r.timeout??5e3,c=r.retries??1;async function a(s){if(!u.has(s.event))return;let n=JSON.stringify(s),e={"Content-Type":"application/json",...r.headers};if(r.secret){let t=createHmac("sha256",r.secret).update(n).digest("hex");e["X-Signature"]=t;}for(let t=0;t<=c;t++)try{let i=new AbortController,o=setTimeout(()=>i.abort(),d);await fetch(r.url,{method:"POST",headers:e,body:n,signal:i.signal}),clearTimeout(o);return}catch{}}return {name:"webhook",onInit(){a({event:"init",timestamp:new Date().toISOString()}).catch(()=>{});},onConnection(s){a({event:"connect",timestamp:new Date().toISOString(),data:{identity:s.identity,ip:s.handshake.remoteAddress,protocol:s.protocol}}).catch(()=>{});},onDisconnect(s,n,e){a({event:"disconnect",timestamp:new Date().toISOString(),data:{identity:s.identity,code:n,reason:e}}).catch(()=>{});},onSecurityEvent(s){a({event:"security",timestamp:s.timestamp,data:{type:s.type,identity:s.identity,ip:s.ip,details:s.details}}).catch(()=>{});},onAuthFailed(s,n,e){a({event:"auth_failed",timestamp:new Date().toISOString(),data:{identity:s.identity,ip:s.remoteAddress,code:n,reason:e}}).catch(()=>{});},onEviction(s,n){a({event:"eviction",timestamp:new Date().toISOString(),data:{identity:s.identity,evictedIp:s.handshake.remoteAddress,newIp:n.handshake.remoteAddress}}).catch(()=>{});},onClosing(){a({event:"closing",timestamp:new Date().toISOString()}).catch(()=>{});},onClose(){}}}export{R as amqpPlugin,x as anomalyPlugin,L as asyncWorkerPlugin,D as circuitBreakerPlugin,$ as connectionGuardPlugin,I as heartbeatPlugin,N as kafkaPlugin,q as messageDedupPlugin,B as metricsPlugin,F as mqttPlugin,H as otelPlugin,W as piiRedactorPlugin,j as rateLimitNotifierPlugin,Y as redisPubSubPlugin,K as replayBufferPlugin,V as schemaVersioningPlugin,z as sessionLogPlugin,G as webhookPlugin};
|
|
1
|
+
import {createHmac}from'crypto';function R(n){let d=n.exchange??"ocpp.events",l=n.routingKey??"ocpp.{event}.{identity}",c=new Set(n.events??["connect","disconnect","message","security"]),o={persistent:n.publishOptions?.persistent??true,contentType:n.publishOptions?.contentType??"application/json",...n.publishOptions?.priority!==void 0&&{priority:n.publishOptions.priority}},s=new Map;function r(t,i){return l.replace("{event}",t).replace("{identity}",i??"server")}function e(t,i,a){if(!c.has(t))return;let u=r(t,i),m=Buffer.from(JSON.stringify(a));if(n.worker)n.worker.enqueue("amqp-publish",async()=>{n.channel.publish(d,u,m,o);});else try{n.channel.publish(d,u,m,o);}catch{}}return {name:"amqp",onConnection(t){s.set(t.identity,Date.now()),e("connect",t.identity,{identity:t.identity,ip:t.handshake.remoteAddress,protocol:t.protocol,timestamp:new Date().toISOString()});},onDisconnect(t,i,a){let u=s.get(t.identity),m=u?Math.round((Date.now()-u)/1e3):0;s.delete(t.identity),e("disconnect",t.identity,{identity:t.identity,code:i,reason:a,durationSec:m,timestamp:new Date().toISOString()});},onMessage(t,i){let a={identity:t.identity,direction:i.direction,messageType:i.message[0],timestamp:i.ctx.timestamp};i.message[0]===2&&i.message[2]&&(a.method=i.message[2]),i.ctx.latencyMs!==void 0&&(a.latencyMs=i.ctx.latencyMs),n.includePayload&&(a.payload=i.message),e(`message.${i.direction}`,t.identity,a);},onSecurityEvent(t){e("security",t.identity,{type:t.type,identity:t.identity,ip:t.ip,timestamp:t.timestamp,details:t.details});},onAuthFailed(t,i,a){e("auth_failed",t.identity,{identity:t.identity,ip:t.remoteAddress,code:i,reason:a,timestamp:new Date().toISOString()});},onEviction(t,i){e("eviction",t.identity,{identity:t.identity,evictedBy:i.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){e("closing",void 0,{timestamp:new Date().toISOString()});},onClose(){s.clear();try{n.channel.close();}catch{}}}}function L(n){let d=n?.reconnectThreshold??5,l=n?.authFailureThreshold??5,c=n?.badMessageThreshold??10,o=n?.evictionThreshold??3,s=n?.windowMs??6e4,r=new Map,e=new Map,t=new Map,i=new Map,a=null,u=null;function m(p,f){let _=f-s,w=0;for(;w<p.length&&p[w]<_;)w++;return w>0?p.slice(w):p}function g(p,f){for(let[_,w]of p){let b=m(w,f);b.length===0?p.delete(_):p.set(_,b);}}function y(p,f,_,w,b){let v=Date.now(),k=p.get(f)??[];if(k=m(k,v),k.push(v),p.set(f,k),k.length>_&&a){let E={type:w,identity:b.identity,ip:b.ip??b.evictedIp,timestamp:new Date().toISOString(),details:{...b,countInWindow:k.length,threshold:_,windowMs:s}};a.emit("securityEvent",E);}}return {name:"anomaly",onInit(p){a=p,u=setInterval(()=>{let f=Date.now();g(r,f),g(e,f),g(t,f),g(i,f);},s).unref();},onConnection(p){y(r,p.identity,d,"ANOMALY_RAPID_RECONNECT",{identity:p.identity,ip:p.handshake.remoteAddress});},onAuthFailed(p,f,_){y(e,p.remoteAddress,l,"ANOMALY_AUTH_BRUTE_FORCE",{ip:p.remoteAddress,identity:p.identity,code:f,reason:_});},onBadMessage(p){y(t,p.identity,c,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress});},onValidationFailure(p){y(t,p.identity,c,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress,source:"validation_failure"});},onEviction(p,f){y(i,p.identity,o,"ANOMALY_IDENTITY_COLLISION",{identity:p.identity,evictedIp:p.handshake.remoteAddress,newIp:f.handshake.remoteAddress});},onClose(){u&&(clearInterval(u),u=null),r.clear(),e.clear(),t.clear(),i.clear(),a=null;}}}function D(n){let d=n?.concurrency??10,l=n?.maxQueueSize??1e3,c=n?.overflowStrategy??"drop-oldest",o=n?.drainTimeoutMs??5e3,s=[],r=0,e=0,t=true,i=null;function a(){for(;r<d&&s.length>0;){let g=s.shift();r++,g.fn().catch(y=>{if(n?.onError)try{n.onError(y instanceof Error?y:new Error(String(y)),g.name);}catch{}}).finally(()=>{r--,!t&&r===0&&s.length===0&&i&&(i(),i=null),a();});}}function u(g,y){if(!t)return false;if(s.length>=l){if(c==="drop-newest")return e++,n?.logger?.warn?.(`[async-worker] Queue full (${l}), dropping task: ${g}`),false;let p=s.shift();e++,n?.logger?.warn?.(`[async-worker] Queue full (${l}), dropping oldest task: ${p?.name??"unknown"}`);}return s.push({name:g,fn:y}),a(),true}return {name:"async-worker",enqueue:u,queueSize:()=>s.length,activeCount:()=>r,droppedCount:()=>e,getCustomMetrics(){return ["# HELP ocpp_async_worker_queue_size Current tasks waiting in the background queue","# TYPE ocpp_async_worker_queue_size gauge",`ocpp_async_worker_queue_size ${s.length}`,"# HELP ocpp_async_worker_active_tasks Currently executing background tasks","# TYPE ocpp_async_worker_active_tasks gauge",`ocpp_async_worker_active_tasks ${r}`,"# HELP ocpp_async_worker_dropped_total Tasks dropped due to queue overflow","# TYPE ocpp_async_worker_dropped_total counter",`ocpp_async_worker_dropped_total ${e}`]},onClosing(){return t=false,r===0&&s.length===0?Promise.resolve():new Promise(g=>{i=g;let y=setTimeout(()=>{n?.logger?.warn?.(`[async-worker] Drain timeout (${o}ms), ${r} tasks still active, ${s.length} queued`),s.length=0,i=null,g();},o);y&&typeof y=="object"&&"unref"in y&&y.unref();})},onClose(){t=false,s.length=0,i=null;}}}var S=class extends Map{_maxSize;constructor(d){if(super(),d<1)throw new RangeError("LRUMap maxSize must be >= 1");this._maxSize=d;}get maxSize(){return this._maxSize}set(d,l){if(this.has(d)&&this.delete(d),super.set(d,l),this.size>this._maxSize){let c=this.keys().next().value;c!==void 0&&this.delete(c);}return this}get(d){if(!this.has(d))return;let l=super.get(d);return this.delete(d),super.set(d,l),l}};function $(n){let d=n?.failureThreshold??5,l=n?.resetTimeoutMs??3e4,c=n?.maxConcurrent??20,o=n?.maxTrackedClients??1e4,s=n?.logger,r=n?.onStateChange,e=new S(o);function t(a){let u=e.get(a);return u||(u={state:"CLOSED",failures:0,lastFailure:0,concurrentCalls:0},e.set(a,u)),u}function i(a,u){let m=t(a),g=m.state;g!==u&&(m.state=u,s?.warn?.(`[circuit-breaker] ${a}: ${g} \u2192 ${u}`),r?.(a,g,u));}return {name:"circuit-breaker",onConnection(a){let u=t(a.identity);a.use(async(m,g)=>{if(m.type!=="outgoing_call")return g();if(u.concurrentCalls>=c)throw s?.warn?.(`[circuit-breaker] ${a.identity}: concurrent limit (${c}) reached, rejecting ${m.method}`),new Error(`Circuit breaker: concurrent call limit exceeded for ${a.identity}`);let y=Date.now();if(u.state==="OPEN")if(y-u.lastFailure>=l)i(a.identity,"HALF_OPEN");else throw new Error(`Circuit breaker OPEN for ${a.identity}: ${u.failures} consecutive failures`);u.concurrentCalls++;try{let p=await g();return u.concurrentCalls=Math.max(0,u.concurrentCalls-1),u.state==="HALF_OPEN"?(i(a.identity,"CLOSED"),u.failures=0):u.failures=Math.max(0,u.failures-1),p}catch(p){throw u.concurrentCalls=Math.max(0,u.concurrentCalls-1),u.failures++,u.lastFailure=Date.now(),(u.state==="HALF_OPEN"||u.failures>=d)&&i(a.identity,"OPEN"),p}});},onDisconnect(a){let u=e.get(a.identity);u&&(u.concurrentCalls=0);},onClose(){e.clear();}}}function I(n){let d=n.maxConnections,l=n.closeCode??4029,c=n.closeReason??"Connection limit reached",o=n.forceCloseOnPongTimeout??true,s=n.forceCloseOnBackpressure??false,r=0;return {name:"connection-guard",onConnection(e){r++,r>d&&(n.logger?.warn?.(`[connection-guard] Limit exceeded (${r}/${d}), closing: ${e.identity}`),e.close({code:l,reason:c}));},onDisconnect(){r=Math.max(0,r-1);},onPongTimeout(e){o&&(n.logger?.warn?.(`[connection-guard] Pong timeout \u2014 closing dead peer: ${e.identity}`),e.close({code:4e3,reason:"Pong timeout"}));},onBackpressure(e,t){s&&(n.logger?.warn?.(`[connection-guard] Backpressure (${t} bytes) \u2014 closing slow client: ${e.identity}`),e.close({code:4001,reason:"Backpressure exceeded"}));},onClose(){r=0;}}}function N(){return {name:"heartbeat",onConnection(n){n.hasHandler("Heartbeat")||n.handle("Heartbeat",()=>({currentTime:new Date().toISOString()}));}}}function q(n){let d=n.topic??"ocpp.events",l=n.topicRouting??false,c=new Set(n.events??["connect","disconnect","message","security"]),o=new Map;function s(e){return l?`${d}.${e}`:d}function r(e,t,i){if(!c.has(e.split(".")[0]))return;let a=s(e.split(".")[0]),u=JSON.stringify(i),m=t??"server";n.worker?n.worker.enqueue("kafka-publish",async()=>{await n.producer.send({topic:a,messages:[{key:m,value:u,headers:{event:e}}]});}):n.producer.send({topic:a,messages:[{key:m,value:u,headers:{event:e}}]}).catch(()=>{});}return {name:"kafka",onConnection(e){o.set(e.identity,Date.now()),r("connect",e.identity,{identity:e.identity,ip:e.handshake.remoteAddress,protocol:e.protocol,timestamp:new Date().toISOString()});},onDisconnect(e,t,i){let a=o.get(e.identity),u=a?Math.round((Date.now()-a)/1e3):0;o.delete(e.identity),r("disconnect",e.identity,{identity:e.identity,code:t,reason:i,durationSec:u,timestamp:new Date().toISOString()});},onMessage(e,t){let i={identity:e.identity,direction:t.direction,messageType:t.message[0],timestamp:t.ctx.timestamp};t.message[0]===2&&t.message[2]&&(i.method=t.message[2]),t.ctx.latencyMs!==void 0&&(i.latencyMs=t.ctx.latencyMs),n.includePayload&&(i.payload=t.message),r(`message.${t.direction}`,e.identity,i);},onSecurityEvent(e){r("security",e.identity,{type:e.type,identity:e.identity,ip:e.ip,timestamp:e.timestamp,details:e.details});},onAuthFailed(e,t,i){r("auth_failed",e.identity,{identity:e.identity,ip:e.remoteAddress,code:t,reason:i,timestamp:new Date().toISOString()});},onEviction(e,t){r("eviction",e.identity,{identity:e.identity,evictedBy:t.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){r("closing",void 0,{timestamp:new Date().toISOString()});},onClose(){o.clear();}}}function B(n){let d=n.redis,l=n.ttlMs??3e5,c=n.prefix??"ocpp:dedup:",o=n.redisStyle??"positional",s=n.logger;async function r(t){return o==="options"?await d.set(t,"1",{PX:l,NX:true})!==null:await d.set(t,"1","PX",l,"NX")==="OK"}async function e(t,i){o==="options"?await d.set(t,i,{PX:l}):await d.set(t,i,"PX",l);}return {name:"message-dedup",async onBeforeReceive(t,i){let a;try{let g=typeof i=="string"?i:i?.toString()||"";a=JSON.parse(g);}catch{return}if(!Array.isArray(a)||a[0]!==2||typeof a[1]!="string")return;let u=a[1],m=`${c}${t.identity}:${u}`;try{if(!await r(m)){if(d.get){let y=await d.get(`${c}resp:${t.identity}:${u}`);if(y)try{t.sendRaw(y);}catch{}}return s?.warn?.(`[message-dedup] Dropping duplicate message: ${m}`),!1}}catch(g){s?.error?.("[message-dedup] Redis failure, falling through:",g);}},onBeforeSend(t,i){if(Array.isArray(i)&&(i[0]===3||i[0]===4)&&typeof i[1]=="string"&&d.get){let a=`${c}resp:${t.identity}:${i[1]}`;Promise.resolve(e(a,JSON.stringify(i))).catch(()=>{});}return true}}}function F(n){let d=n?.intervalMs??3e4,l=0,c=0,o=0,s=0,r=0,e=Date.now(),t=null,i=0,a=0,u=0,m=0,g=0,y=0,p=0,f=0,_=0,w=0,b=0,v=0,k=0,E=0,M=0,A=new Map;function T(){return {totalConnections:l,totalDisconnections:c,activeConnections:o,peakConnections:s,connectionDurationAvgMs:c>0?Math.round(r/c):0,uptimeMs:Date.now()-e,timestamp:new Date().toISOString(),totalMessagesIn:i,totalMessagesOut:a,totalCalls:u,totalCallResults:m,totalCallErrors:g,totalErrors:y,totalBadMessages:p,totalHandlerErrors:f,totalRateLimitHits:_,totalAuthFailures:w,totalEvictions:b,totalBackpressureEvents:v,totalPongTimeouts:k,totalValidationFailures:E,totalSecurityEvents:M}}return {name:"metrics",getMetrics:T,onInit(){e=Date.now(),d>0&&n?.onSnapshot&&(t=setInterval(()=>{n.onSnapshot(T());},d),t&&typeof t=="object"&&"unref"in t&&t.unref());},onConnection(O){l++,o++,o>s&&(s=o),A.set(O.identity,Date.now());},onDisconnect(O){c++,o=Math.max(0,o-1);let C=A.get(O.identity);C&&(r+=Date.now()-C,A.delete(O.identity));},onMessage(O,C){C.direction==="IN"?i++:a++;let x=C.message[0];x===2?u++:x===3?m++:x===4&&g++;},onError(){y++;},onBadMessage(){p++;},onHandlerError(){f++;},onRateLimitExceeded(){_++;},onAuthFailed(){w++;},onEviction(){b++;},onBackpressure(){v++;},onPongTimeout(){k++;},onValidationFailure(){E++;},onSecurityEvent(){M++;},getCustomMetrics(){return ["# HELP ocpp_connections_total Total connections since server start","# TYPE ocpp_connections_total counter",`ocpp_connections_total ${l}`,"# HELP ocpp_disconnections_total Total disconnections since server start","# TYPE ocpp_disconnections_total counter",`ocpp_disconnections_total ${c}`,"# HELP ocpp_connections_active Currently active connections","# TYPE ocpp_connections_active gauge",`ocpp_connections_active ${o}`,"# HELP ocpp_connections_peak Highest concurrent connections","# TYPE ocpp_connections_peak gauge",`ocpp_connections_peak ${s}`,"# HELP ocpp_connection_duration_avg_ms Average connection duration","# TYPE ocpp_connection_duration_avg_ms gauge",`ocpp_connection_duration_avg_ms ${T().connectionDurationAvgMs}`,"# HELP ocpp_messages_in_total Total inbound messages","# TYPE ocpp_messages_in_total counter",`ocpp_messages_in_total ${i}`,"# HELP ocpp_messages_out_total Total outbound messages","# TYPE ocpp_messages_out_total counter",`ocpp_messages_out_total ${a}`,"# HELP ocpp_calls_total Total CALL messages","# TYPE ocpp_calls_total counter",`ocpp_calls_total ${u}`,"# HELP ocpp_call_results_total Total CALLRESULT messages","# TYPE ocpp_call_results_total counter",`ocpp_call_results_total ${m}`,"# HELP ocpp_call_errors_total Total CALLERROR messages","# TYPE ocpp_call_errors_total counter",`ocpp_call_errors_total ${g}`,"# HELP ocpp_errors_total WebSocket/protocol errors","# TYPE ocpp_errors_total counter",`ocpp_errors_total ${y}`,"# HELP ocpp_bad_messages_total Malformed messages received","# TYPE ocpp_bad_messages_total counter",`ocpp_bad_messages_total ${p}`,"# HELP ocpp_handler_errors_total User handler errors","# TYPE ocpp_handler_errors_total counter",`ocpp_handler_errors_total ${f}`,"# HELP ocpp_rate_limit_hits_total Rate limit violations","# TYPE ocpp_rate_limit_hits_total counter",`ocpp_rate_limit_hits_total ${_}`,"# HELP ocpp_auth_failures_total Authentication failures","# TYPE ocpp_auth_failures_total counter",`ocpp_auth_failures_total ${w}`,"# HELP ocpp_evictions_total Client evictions","# TYPE ocpp_evictions_total counter",`ocpp_evictions_total ${b}`,"# HELP ocpp_backpressure_events_total Slow client backpressure events","# TYPE ocpp_backpressure_events_total counter",`ocpp_backpressure_events_total ${v}`,"# HELP ocpp_pong_timeouts_total Dead peer timeouts","# TYPE ocpp_pong_timeouts_total counter",`ocpp_pong_timeouts_total ${k}`,"# HELP ocpp_validation_failures_total Schema validation failures","# TYPE ocpp_validation_failures_total counter",`ocpp_validation_failures_total ${E}`,"# HELP ocpp_security_events_total Security events from anomaly detection","# TYPE ocpp_security_events_total counter",`ocpp_security_events_total ${M}`]},onClose(){t&&(clearInterval(t),t=null),A.clear();}}}function H(n){let d=n.topicPrefix??"ocpp",l=new Set(n.events??["connect","disconnect","message","security"]),c=n.qos??0,o=new Map;function s(e,t){return n.topicBuilder?n.topicBuilder(e,t):t?`${d}/${t}/${e}`:`${d}/${e}`}function r(e,t){let i=n.transform?n.transform(t):t,a=JSON.stringify(i);n.worker?n.worker.enqueue("mqtt-publish",()=>new Promise((u,m)=>{n.client.publish(e,a,{qos:c},g=>g?m(g):u());})):n.client.publish(e,a,{qos:c});}return {name:"mqtt",onConnection(e){o.set(e.identity,Date.now()),l.has("connect")&&r(s("connect",e.identity),{identity:e.identity,ip:e.handshake.remoteAddress,protocol:e.protocol,timestamp:new Date().toISOString()});},onDisconnect(e,t,i){if(!l.has("disconnect")){o.delete(e.identity);return}let a=o.get(e.identity),u=a?Math.round((Date.now()-a)/1e3):0;o.delete(e.identity),r(s("disconnect",e.identity),{identity:e.identity,code:t,reason:i,durationSec:u,timestamp:new Date().toISOString()});},onMessage(e,t){if(!l.has("message"))return;let i={identity:e.identity,direction:t.direction,messageType:t.message[0],timestamp:t.ctx.timestamp};t.message[0]===2&&t.message[2]&&(i.method=t.message[2]),t.ctx.latencyMs!==void 0&&(i.latencyMs=t.ctx.latencyMs),n.includePayload&&(i.payload=t.message),r(s(`message/${t.direction}`,e.identity),i);},onSecurityEvent(e){l.has("security")&&r(s("security",e.identity),{type:e.type,identity:e.identity,ip:e.ip,timestamp:e.timestamp,details:e.details});},onError(e,t){l.has("error")&&r(s("error",e.identity),{identity:e.identity,error:t.message,timestamp:new Date().toISOString()});},onAuthFailed(e,t,i){l.has("auth_failed")&&r(s("auth_failed"),{identity:e.identity,ip:e.remoteAddress,code:t,reason:i,timestamp:new Date().toISOString()});},onEviction(e,t){l.has("eviction")&&r(s("eviction",e.identity),{identity:e.identity,evictedBy:t.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){r(s("closing"),{timestamp:new Date().toISOString()});},onClose(){o.clear();try{n.client.end(!1);}catch{}}}}function W(n){let d=n?.tracer??null,l=new Map;return {name:"otel",async onInit(c){if(!d)try{d=(await import('@opentelemetry/api')).trace.getTracer(n?.serviceName??"ocpp-server","1.0.0");}catch{c.log.warn?.("otelPlugin: @opentelemetry/api not found \u2014 plugin disabled. Install it as a peer dependency."),d=null;}},onConnection(c){if(!d)return;let o=d.startSpan("ocpp.connection",{kind:1});o.setAttribute("ocpp.identity",c.identity),o.setAttribute("ocpp.protocol",c.protocol??"unknown"),o.setAttribute("net.peer.ip",c.handshake.remoteAddress),l.set(c.identity,{span:o,startTime:Date.now()});},onDisconnect(c,o){let s=l.get(c.identity);if(!s)return;let r=Date.now()-s.startTime;s.span.setAttribute("ocpp.close_code",o),s.span.setAttribute("ocpp.duration_ms",r),s.span.setStatus({code:1}),s.span.end(),l.delete(c.identity);},onMessage(c,o){if(!d)return;let s=o.message[0];if(s!==2){let t=l.get(c.identity);t&&t.span.addEvent(s===3?"ocpp.call_result":"ocpp.call_error",{direction:o.direction,"ocpp.message_id":String(o.message[1]),...o.ctx.latencyMs!==void 0&&{"ocpp.latency_ms":o.ctx.latencyMs}});return}let r=String(o.message[2]??"unknown"),e=d.startSpan(`ocpp.call.${r}`,{kind:o.direction==="IN"?1:2});e.setAttribute("ocpp.identity",c.identity),e.setAttribute("ocpp.method",r),e.setAttribute("ocpp.direction",o.direction),e.setAttribute("ocpp.message_id",String(o.message[1])),o.ctx.latencyMs!==void 0&&e.setAttribute("ocpp.latency_ms",o.ctx.latencyMs),e.setStatus({code:1}),e.end();},onError(c,o){let s=l.get(c.identity);s&&(s.span.recordException(o),s.span.addEvent("ocpp.error",{"error.message":o.message}));},onHandlerError(c,o,s){let r=l.get(c.identity);r&&(r.span.recordException(s),r.span.addEvent("ocpp.handler_error",{"ocpp.method":o,"error.message":s.message}));},onBadMessage(c,o,s){let r=l.get(c.identity);r&&(r.span.recordException(s),r.span.addEvent("ocpp.bad_message",{"raw.preview":typeof o=="string"?o.slice(0,200):"<buffer>","error.message":s.message}));},onValidationFailure(c,o,s){let r=l.get(c.identity);r&&(r.span.recordException(s),r.span.addEvent("ocpp.validation_failure",{"error.message":s.message}));},onRateLimitExceeded(c){let o=l.get(c.identity);o&&o.span.addEvent("ocpp.rate_limit_exceeded");},onPongTimeout(c){let o=l.get(c.identity);o&&o.span.addEvent("ocpp.pong_timeout");},onBackpressure(c,o){let s=l.get(c.identity);s&&s.span.addEvent("ocpp.backpressure",{"ocpp.buffered_bytes":o});},onEviction(c,o){let s=l.get(c.identity);s&&s.span.addEvent("ocpp.evicted",{"net.peer.ip.new":o.handshake.remoteAddress});},onTelemetry(c){if(!d)return;let o=d.startSpan("ocpp.telemetry_push",{kind:0});o.setAttribute("ocpp.connected_clients",c.connectedClients),o.setAttribute("ocpp.active_sessions",c.activeSessions),o.setAttribute("ocpp.uptime_seconds",c.uptimeSeconds),o.setAttribute("ocpp.memory_rss",c.memoryUsage.rss),o.setAttribute("ocpp.memory_heap_used",c.memoryUsage.heapUsed),o.setAttribute("ocpp.pid",c.pid),c.webSockets&&(o.setAttribute("ocpp.ws_total",c.webSockets.total),o.setAttribute("ocpp.ws_buffered_amount",c.webSockets.bufferedAmount)),o.setStatus({code:1}),o.end();},onSecurityEvent(c){if(!d)return;let o=d.startSpan("ocpp.security_event",{kind:0});o.setAttribute("security.event_type",c.type),c.identity&&o.setAttribute("ocpp.identity",c.identity),c.ip&&o.setAttribute("net.peer.ip",c.ip),o.setStatus({code:2,message:c.type}),o.end();},onAuthFailed(c,o,s){if(!d)return;let r=d.startSpan("ocpp.auth_failed",{kind:1});r.setAttribute("ocpp.identity",c.identity),r.setAttribute("net.peer.ip",c.remoteAddress),r.setAttribute("ocpp.close_code",o),r.setAttribute("ocpp.close_reason",s),r.setStatus({code:2,message:"Auth failed"}),r.end();},onClosing(){for(let[,c]of l)c.span.addEvent("ocpp.server_closing");},onClose(){for(let[,c]of l)c.span.setStatus({code:2,message:"Server shutdown"}),c.span.end();l.clear();}}}function K(n){if(!n||!Array.isArray(n.sensitiveKeys)||n.sensitiveKeys.length===0)throw new Error("piiRedactorPlugin requires a non-empty 'sensitiveKeys' array \u2014 explicitly list the keys to redact, e.g. piiRedactorPlugin({ sensitiveKeys: ['password', 'authorizationKey'] }).");let d=new Set(n.sensitiveKeys),l=n.replacement??"***REDACTED***",c=n.incoming??true,o=n.outgoing??true;function s(e){if(!e||typeof e!="object")return e;if(Array.isArray(e))return e.map(s);let t={};for(let[i,a]of Object.entries(e))d.has(i)?t[i]=l:a&&typeof a=="object"?t[i]=s(a):t[i]=a;return t}let r=async(e,t)=>{c&&(e.type==="incoming_call"&&e.params?e.params=s(e.params):e.type==="incoming_result"&&e.payload&&(e.payload=s(e.payload))),o&&(e.type==="outgoing_call"&&e.params?e.params=s(e.params):e.type==="outgoing_result"&&e.payload&&(e.payload=s(e.payload))),await t();};return {name:"pii-redactor",onConnection(e){e.use(r);}}}function j(n){let d=n.cooldownMs??6e4,l=n.threshold??1,c=n.windowMs??3e5,o=n.maxTrackedKeys??1e4,s=n.logger,r=new S(o),e=new S(o);function t(){return typeof n.sink=="string"?{async send(u){await fetch(n.sink,{method:"POST",headers:{"Content-Type":"application/json",...n.headers},body:JSON.stringify(u)});}}:n.sink}function i(u){let m=Date.now(),y=(r.get(u)??[]).filter(p=>m-p<c);return r.set(u,y),y}function a(u,m,g){let y=u??m??"unknown",p=Date.now(),f=i(y);if(f.push(p),f.length<l)return;let _=e.get(y)??0;if(p-_<d)return;e.set(y,p);let w=t(),b={eventType:g,identity:u,ip:m,timestamp:new Date().toISOString(),count:f.length,windowMs:c};Promise.resolve(w.send(b)).catch(v=>{s?.error?.("[rate-limit-notifier] Alert delivery failed:",v);});}return {name:"rate-limit-notifier",onRateLimitExceeded(u,m){a(u.identity,u.handshake.remoteAddress,"RATE_LIMIT_EXCEEDED");},onSecurityEvent(u){(u.type==="RATE_LIMIT_EXCEEDED"||u.type==="CONNECTION_RATE_LIMIT")&&a(u.identity,u.ip,u.type);},onClose(){r.clear(),e.clear();}}}function Y(n){let d=n.mode??"pubsub",l=n.prefix??"ocpp",c=new Set(n.events??["connect","disconnect","message","security"]),o=n.maxStreamLength??1e4,s=n.serialize??JSON.stringify,r=new Map;function e(i){return `${l}:${i}`}function t(i,a){if(!c.has(i))return;let u=e(i),m=s(a),g=async()=>{d==="stream"&&n.client.xadd?await n.client.xadd(u,"MAXLEN","~",o,"*","data",m):await n.client.publish(u,m);};if(n.worker)n.worker.enqueue(`redis-${d}`,()=>g().catch(()=>{}));else try{g().catch?.(()=>{});}catch{}}return {name:"redis-pubsub",onConnection(i){r.set(i.identity,Date.now()),t("connect",{identity:i.identity,ip:i.handshake.remoteAddress,protocol:i.protocol,timestamp:new Date().toISOString()});},onDisconnect(i,a,u){let m=r.get(i.identity),g=m?Math.round((Date.now()-m)/1e3):0;r.delete(i.identity),t("disconnect",{identity:i.identity,code:a,reason:u,durationSec:g,timestamp:new Date().toISOString()});},onMessage(i,a){let u={identity:i.identity,direction:a.direction,messageType:a.message[0],timestamp:a.ctx.timestamp};a.message[0]===2&&a.message[2]&&(u.method=a.message[2]),a.ctx.latencyMs!==void 0&&(u.latencyMs=a.ctx.latencyMs),n.includePayload&&(u.payload=a.message),t(`message:${a.direction}`,u);},onSecurityEvent(i){t("security",{type:i.type,identity:i.identity,ip:i.ip,timestamp:i.timestamp,details:i.details});},onAuthFailed(i,a,u){t("auth_failed",{identity:i.identity,ip:i.remoteAddress,code:a,reason:u,timestamp:new Date().toISOString()});},onEviction(i,a){t("eviction",{identity:i.identity,evictedBy:a.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){t("closing",{timestamp:new Date().toISOString()});},onClose(){r.clear();try{n.client.quit?n.client.quit():n.client.disconnect&&n.client.disconnect();}catch{}}}}function V(n){let d=n.redis,l=n.prefix??"ocpp:replay:",c=n.syntheticResponse??true,o=n.flushConcurrency??5,s=n.flushDelayMs??200,r=n.logger,e=new Set;function t(i){return new Promise(a=>setTimeout(a,i))}return {name:"replay-buffer",onConnection(i){let a=`${l}${i.identity}`,u=async(g,y)=>{if(g.type!=="outgoing_call")return y();try{return await y()}catch(p){let f=p instanceof Error?p.message:String(p);if(!(f.includes("WebSocket is not open")||f.includes("offline")||f.includes("CLOSED")||f.includes("CLOSING")))throw p;let w=JSON.stringify([2,g.messageId,g.method,g.params]);try{await d.rpush(a,w),r?.warn?.(`[replay-buffer] Queued offline command: ${g.method} for ${i.identity}`);}catch(b){throw r?.error?.(`[replay-buffer] Redis rpush failed for ${i.identity}:`,b),p}if(c)return {status:"Accepted",note:"Queued offline (ReplayBuffer)"};throw p}};i.use(u);let m=(async()=>{try{let g=0;for(;;){let y=await d.lpop(a);if(!y)break;let p;try{p=JSON.parse(y);}catch{r?.warn?.(`[replay-buffer] Skipping unparseable queued message for ${i.identity}`);continue}!Array.isArray(p)||p[0]!==2||(i.call(p[2],p[3]).catch(f=>{r?.warn?.(`[replay-buffer] Flush call failed for ${i.identity}/${p[2]}:`,f);}),g++,g>=o&&(await t(s),g=0));}}catch(g){r?.error?.(`[replay-buffer] Error flushing queue for ${i.identity}:`,g);}})();e.add(m),m.finally(()=>e.delete(m));},async onClosing(){e.size>0&&await Promise.allSettled([...e]);},onClose(){e.clear();}}}function z(n){let d=n.unmatchedBehavior??"passthrough",l=n.logger,c=new Map,o;for(let r of n.rules)r.method==="*"?o=r:c.set(r.method,r);function s(r){return c.get(r)??o}return {name:"schema-versioning",onConnection(r){if(n.applyWhen&&r.protocol!==n.applyWhen)return;let e=async(t,i)=>{let a=t.method,u=s(a);if(!u){if(d==="reject")throw l?.warn?.(`[schema-versioning] No transform rule for method "${a}", rejecting`),new Error(`Schema versioning: no transform rule for "${a}" (${n.sourceVersion} \u2192 ${n.targetVersion})`);return i()}if(t.type==="incoming_call")try{let m=u.transform(t.params,"up");t.params=m,l?.debug?.(`[schema-versioning] Transformed ${a} UP: ${n.sourceVersion} \u2192 ${n.targetVersion}`);}catch(m){l?.warn?.(`[schema-versioning] Transform UP failed for ${a}:`,m);}else if(t.type==="outgoing_call")try{let m=u.transform(t.params,"down");t.params=m,l?.debug?.(`[schema-versioning] Transformed ${a} DOWN: ${n.targetVersion} \u2192 ${n.sourceVersion}`);}catch(m){l?.warn?.(`[schema-versioning] Transform DOWN failed for ${a}:`,m);}else if(t.type==="outgoing_result")try{let m=u.transform(t.payload,"down");t.payload=m;}catch(m){l?.warn?.(`[schema-versioning] Transform DOWN (result) failed for ${a}:`,m);}return i()};r.use(e);}}}function U(n){let d=n?.logger??console,l=n?.logLevel??"standard",c=l==="standard"||l==="verbose",o=l==="verbose",s=new Map;return {name:"session-log",onConnection(r){s.set(r.identity,Date.now()),d.info("[session] connected",{identity:r.identity,ip:r.handshake.remoteAddress,protocol:r.protocol});},onDisconnect(r,e,t){let i=s.get(r.identity),a=i?Math.round((Date.now()-i)/1e3):0;s.delete(r.identity),d.info("[session] disconnected",{identity:r.identity,code:e,reason:t,durationSec:a});},onError(r,e){c&&(d.error??d.warn)("[session] error",{identity:r.identity,error:e.message});},onAuthFailed(r,e,t){c&&d.warn("[session] auth failed",{identity:r.identity,ip:r.remoteAddress,code:e,reason:t});},onEviction(r,e){c&&d.warn("[session] evicted",{identity:r.identity,evictedIp:r.handshake.remoteAddress,newIp:e.handshake.remoteAddress});},onBadMessage(r,e){o&&d.warn("[session] bad message",{identity:r.identity,raw:typeof e=="string"?e.slice(0,200):"<buffer>"});},onSecurityEvent(r){o&&d.warn("[session] security event",{type:r.type,identity:r.identity,ip:r.ip,details:r.details});},onHandlerError(r,e,t){o&&(d.error??d.warn)("[session] handler error",{identity:r.identity,method:e,error:t.message});},onValidationFailure(r,e,t){o&&d.warn("[session] validation failure",{identity:r.identity,error:t.message});},onRateLimitExceeded(r){c&&d.warn("[session] rate limit exceeded",{identity:r.identity,ip:r.handshake.remoteAddress});},onPongTimeout(r){o&&d.warn("[session] pong timeout (dead peer)",{identity:r.identity});},onBackpressure(r,e){o&&d.warn("[session] backpressure",{identity:r.identity,bufferedBytes:e});},onClose(){s.clear();}}}function G(n){let d=new Set(n.events??["init","connect","disconnect","close"]),l=n.timeout??5e3,c=n.retries??1;async function o(s){if(!d.has(s.event))return;let r=JSON.stringify(s),e={"Content-Type":"application/json",...n.headers};if(n.secret){let t=createHmac("sha256",n.secret).update(r).digest("hex");e["X-Signature"]=t;}for(let t=0;t<=c;t++){let i=new AbortController,a=setTimeout(()=>i.abort(),l);try{let u=await fetch(n.url,{method:"POST",headers:e,body:r,signal:i.signal});if(!u.ok)throw new Error(`Webhook responded with HTTP ${u.status}`);return}catch{t<c&&await new Promise(u=>setTimeout(u,250*2**t));}finally{clearTimeout(a);}}}return {name:"webhook",onInit(){o({event:"init",timestamp:new Date().toISOString()}).catch(()=>{});},onConnection(s){o({event:"connect",timestamp:new Date().toISOString(),data:{identity:s.identity,ip:s.handshake.remoteAddress,protocol:s.protocol}}).catch(()=>{});},onDisconnect(s,r,e){o({event:"disconnect",timestamp:new Date().toISOString(),data:{identity:s.identity,code:r,reason:e}}).catch(()=>{});},onSecurityEvent(s){o({event:"security",timestamp:s.timestamp,data:{type:s.type,identity:s.identity,ip:s.ip,details:s.details}}).catch(()=>{});},onAuthFailed(s,r,e){o({event:"auth_failed",timestamp:new Date().toISOString(),data:{identity:s.identity,ip:s.remoteAddress,code:r,reason:e}}).catch(()=>{});},onEviction(s,r){o({event:"eviction",timestamp:new Date().toISOString(),data:{identity:s.identity,evictedIp:s.handshake.remoteAddress,newIp:r.handshake.remoteAddress}}).catch(()=>{});},onClosing(){o({event:"closing",timestamp:new Date().toISOString()}).catch(()=>{});},onClose(){}}}export{R as amqpPlugin,L as anomalyPlugin,D as asyncWorkerPlugin,$ as circuitBreakerPlugin,I as connectionGuardPlugin,N as heartbeatPlugin,q as kafkaPlugin,B as messageDedupPlugin,F as metricsPlugin,H as mqttPlugin,W as otelPlugin,K as piiRedactorPlugin,j as rateLimitNotifierPlugin,Y as redisPubSubPlugin,V as replayBufferPlugin,z as schemaVersioningPlugin,U as sessionLogPlugin,G as webhookPlugin};
|