ocpp-ws-io 2.2.4 → 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 +3 -3
- 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-B_y56LLS.d.ts → context-B4-L-O1-.d.ts} +1 -1
- package/dist/{context-BVYqFvfw.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-DrWsObRI.d.ts → index-CEMhGOxh.d.ts} +16 -6
- package/dist/{index-Dr8uXTWu.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 +7 -7
- package/dist/index.mjs +7 -7
- package/dist/nestjs.d.mts +1 -1
- package/dist/nestjs.d.ts +1 -1
- package/dist/nestjs.js +8 -8
- package/dist/nestjs.mjs +8 -8
- package/dist/parse-worker.cjs +91 -0
- package/dist/plugins.d.mts +12 -2
- package/dist/plugins.d.ts +12 -2
- package/dist/plugins.js +1 -1
- package/dist/plugins.mjs +1 -1
- package/dist/{types-BsoUgWtt.d.mts → types-tTYOr5Gm.d.mts} +110 -22
- package/dist/{types-BsoUgWtt.d.ts → types-tTYOr5Gm.d.ts} +110 -22
- package/package.json +1 -1
|
@@ -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';
|
|
@@ -351,6 +351,11 @@ interface ConnectionGuardOptions {
|
|
|
351
351
|
* Optionally reclaims slots from dead peers (pong timeout) and
|
|
352
352
|
* slow consumers (backpressure).
|
|
353
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
|
+
*
|
|
354
359
|
* @example
|
|
355
360
|
* ```ts
|
|
356
361
|
* import { connectionGuardPlugin } from 'ocpp-ws-io/plugins';
|
|
@@ -471,6 +476,11 @@ interface DedupRedisLike {
|
|
|
471
476
|
* **node-redis v4 style:** `set(key, value, { PX: ms, NX: true })` → `Promise<string | null>`
|
|
472
477
|
*/
|
|
473
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);
|
|
474
484
|
}
|
|
475
485
|
interface MessageDedupOptions {
|
|
476
486
|
/**
|
|
@@ -940,7 +950,7 @@ interface RedisClientLike {
|
|
|
940
950
|
quit?(): Promise<unknown> | unknown;
|
|
941
951
|
disconnect?(): void;
|
|
942
952
|
}
|
|
943
|
-
type RedisPubSubEvent = "connect" | "disconnect" | "message" | "security" | "auth_failed" | "eviction";
|
|
953
|
+
type RedisPubSubEvent = "connect" | "disconnect" | "message" | "security" | "auth_failed" | "eviction" | "closing";
|
|
944
954
|
/**
|
|
945
955
|
* Options for the Redis Pub/Sub plugin.
|
|
946
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';
|
|
@@ -351,6 +351,11 @@ interface ConnectionGuardOptions {
|
|
|
351
351
|
* Optionally reclaims slots from dead peers (pong timeout) and
|
|
352
352
|
* slow consumers (backpressure).
|
|
353
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
|
+
*
|
|
354
359
|
* @example
|
|
355
360
|
* ```ts
|
|
356
361
|
* import { connectionGuardPlugin } from 'ocpp-ws-io/plugins';
|
|
@@ -471,6 +476,11 @@ interface DedupRedisLike {
|
|
|
471
476
|
* **node-redis v4 style:** `set(key, value, { PX: ms, NX: true })` → `Promise<string | null>`
|
|
472
477
|
*/
|
|
473
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);
|
|
474
484
|
}
|
|
475
485
|
interface MessageDedupOptions {
|
|
476
486
|
/**
|
|
@@ -940,7 +950,7 @@ interface RedisClientLike {
|
|
|
940
950
|
quit?(): Promise<unknown> | unknown;
|
|
941
951
|
disconnect?(): void;
|
|
942
952
|
}
|
|
943
|
-
type RedisPubSubEvent = "connect" | "disconnect" | "message" | "security" | "auth_failed" | "eviction";
|
|
953
|
+
type RedisPubSubEvent = "connect" | "disconnect" | "message" | "security" | "auth_failed" | "eviction" | "closing";
|
|
944
954
|
/**
|
|
945
955
|
* Options for the Redis Pub/Sub plugin.
|
|
946
956
|
*/
|
package/dist/plugins.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
'use strict';var crypto=require('crypto');function x(n){let d=n.exchange??"ocpp.events",l=n.routingKey??"ocpp.{event}.{identity}",a=new Set(n.events??["connect","disconnect","message","security"]),s={persistent:n.publishOptions?.persistent??true,contentType:n.publishOptions?.contentType??"application/json",...n.publishOptions?.priority!==void 0&&{priority:n.publishOptions.priority}},o=new Map;function r(t,i){return l.replace("{event}",t).replace("{identity}",i??"server")}function e(t,i,c){if(!a.has(t))return;let u=r(t,i),m=Buffer.from(JSON.stringify(c));if(n.worker)n.worker.enqueue("amqp-publish",async()=>{n.channel.publish(d,u,m,s);});else try{n.channel.publish(d,u,m,s);}catch{}}return {name:"amqp",onConnection(t){o.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,c){let u=o.get(t.identity),m=u?Math.round((Date.now()-u)/1e3):0;o.delete(t.identity),e("disconnect",t.identity,{identity:t.identity,code:i,reason:c,durationSec:m,timestamp:new Date().toISOString()});},onMessage(t,i){let c={identity:t.identity,direction:i.direction,messageType:i.message[0],timestamp:i.ctx.timestamp};i.message[0]===2&&i.message[2]&&(c.method=i.message[2]),i.ctx.latencyMs!==void 0&&(c.latencyMs=i.ctx.latencyMs),n.includePayload&&(c.payload=i.message),e(`message.${i.direction}`,t.identity,c);},onSecurityEvent(t){e("security",t.identity,{type:t.type,identity:t.identity,ip:t.ip,timestamp:t.timestamp,details:t.details});},onAuthFailed(t,i,c){e("auth_failed",t.identity,{identity:t.identity,ip:t.remoteAddress,code:i,reason:c,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(){o.clear();try{n.channel.close();}catch{}}}}function R(n){let d=n?.reconnectThreshold??5,l=n?.authFailureThreshold??5,a=n?.badMessageThreshold??10,s=n?.evictionThreshold??3,o=n?.windowMs??6e4,r=new Map,e=new Map,t=new Map,i=new Map,c=null,u=null;function m(p,f){let w=f-o,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&&c){let S={type:P,identity:_.identity,ip:_.ip??_.evictedIp,timestamp:new Date().toISOString(),details:{..._,countInWindow:b.length,threshold:w,windowMs:o}};c.emit("securityEvent",S);}}return {name:"anomaly",onInit(p){c=p,u=setInterval(()=>{let f=Date.now();g(r,f),g(e,f),g(t,f),g(i,f);},o).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,a,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress});},onValidationFailure(p){y(t,p.identity,a,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress,source:"validation_failure"});},onEviction(p,f){y(i,p.identity,s,"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(),c=null;}}}function L(n){let d=n?.concurrency??10,l=n?.maxQueueSize??1e3,a=n?.overflowStrategy??"drop-oldest",s=n?.drainTimeoutMs??5e3,o=[],r=0,e=0,t=true,i=null;function c(){for(;r<d&&o.length>0;){let g=o.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&&o.length===0&&i&&(i(),i=null),c();});}}function u(g,y){if(!t)return false;if(o.length>=l){if(a==="drop-newest")return e++,n?.logger?.warn?.(`[async-worker] Queue full (${l}), dropping task: ${g}`),false;let p=o.shift();e++,n?.logger?.warn?.(`[async-worker] Queue full (${l}), dropping oldest task: ${p?.name??"unknown"}`);}return o.push({name:g,fn:y}),c(),true}return {name:"async-worker",enqueue:u,queueSize:()=>o.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 ${o.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&&o.length===0?Promise.resolve():new Promise(g=>{i=g;let y=setTimeout(()=>{n?.logger?.warn?.(`[async-worker] Drain timeout (${s}ms), ${r} tasks still active, ${o.length} queued`),o.length=0,i=null,g();},s);y&&typeof y=="object"&&"unref"in y&&y.unref();})},onClose(){t=false,o.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 a=this.keys().next().value;a!==void 0&&this.delete(a);}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,a=n?.maxConcurrent??20,s=n?.maxTrackedClients??1e4,o=n?.logger,r=n?.onStateChange,e=new v(s);function t(c){let u=e.get(c);return u||(u={state:"CLOSED",failures:0,lastFailure:0,concurrentCalls:0},e.set(c,u)),u}function i(c,u){let m=t(c),g=m.state;g!==u&&(m.state=u,o?.warn?.(`[circuit-breaker] ${c}: ${g} \u2192 ${u}`),r?.(c,g,u));}return {name:"circuit-breaker",onConnection(c){let u=t(c.identity);c.use(async(m,g)=>{if(m.type!=="outgoing_call")return g();if(u.concurrentCalls>=a)throw o?.warn?.(`[circuit-breaker] ${c.identity}: concurrent limit (${a}) reached, rejecting ${m.method}`),new Error(`Circuit breaker: concurrent call limit exceeded for ${c.identity}`);let y=Date.now();if(u.state==="OPEN")if(y-u.lastFailure>=l)i(c.identity,"HALF_OPEN");else throw new Error(`Circuit breaker OPEN for ${c.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(c.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(c.identity,"OPEN"),p}});},onDisconnect(c){let u=e.get(c.identity);u&&(u.concurrentCalls=0);},onClose(){e.clear();}}}function $(n){let d=n.maxConnections,l=n.closeCode??4029,a=n.closeReason??"Connection limit reached",s=n.forceCloseOnPongTimeout??true,o=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:a}));},onDisconnect(){r=Math.max(0,r-1);},onPongTimeout(e){s&&(n.logger?.warn?.(`[connection-guard] Pong timeout \u2014 closing dead peer: ${e.identity}`),e.close({code:4e3,reason:"Pong timeout"}));},onBackpressure(e,t){o&&(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.handle("Heartbeat",()=>({currentTime:new Date().toISOString()}));}}}function N(n){let d=n.topic??"ocpp.events",l=n.topicRouting??false,a=new Set(n.events??["connect","disconnect","message","security"]),s=new Map;function o(e){return l?`${d}.${e}`:d}function r(e,t,i){if(!a.has(e.split(".")[0]))return;let c=o(e.split(".")[0]),u=JSON.stringify(i),m=t??"server";n.worker?n.worker.enqueue("kafka-publish",async()=>{await n.producer.send({topic:c,messages:[{key:m,value:u,headers:{event:e}}]});}):n.producer.send({topic:c,messages:[{key:m,value:u,headers:{event:e}}]}).catch(()=>{});}return {name:"kafka",onConnection(e){s.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 c=s.get(e.identity),u=c?Math.round((Date.now()-c)/1e3):0;s.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(){s.clear();}}}function q(n){let d=n.redis,l=n.ttlMs??3e5,a=n.prefix??"ocpp:dedup:",s=n.redisStyle??"positional",o=n.logger;async function r(e){return s==="options"?await d.set(e,"1",{PX:l,NX:true})!==null:await d.set(e,"1","PX",l,"NX")==="OK"}return {name:"message-dedup",async onBeforeReceive(e,t){let i;try{let u=typeof t=="string"?t:t?.toString()||"",m=JSON.parse(u);Array.isArray(m)&&m.length>1&&(i=String(m[1]));}catch{return}if(!i)return;let c=`${a}${e.identity}:${i}`;try{if(!await r(c))return o?.warn?.(`[message-dedup] Dropping duplicate message: ${c}`),!1}catch(u){o?.error?.("[message-dedup] Redis failure, falling through:",u);}}}}function B(n){let d=n?.intervalMs??3e4,l=0,a=0,s=0,o=0,r=0,e=Date.now(),t=null,i=0,c=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:a,activeConnections:s,peakConnections:o,connectionDurationAvgMs:a>0?Math.round(r/a):0,uptimeMs:Date.now()-e,timestamp:new Date().toISOString(),totalMessagesIn:i,totalMessagesOut:c,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++,s++,s>o&&(o=s),C.set(E.identity,Date.now());},onDisconnect(E){a++,s=Math.max(0,s-1);let O=C.get(E.identity);O&&(r+=Date.now()-O,C.delete(E.identity));},onMessage(E,O){O.direction==="IN"?i++:c++;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 ${a}`,"# HELP ocpp_connections_active Currently active connections","# TYPE ocpp_connections_active gauge",`ocpp_connections_active ${s}`,"# HELP ocpp_connections_peak Highest concurrent connections","# TYPE ocpp_connections_peak gauge",`ocpp_connections_peak ${o}`,"# 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 ${c}`,"# 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"]),a=n.qos??0,s=new Map;function o(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,c=JSON.stringify(i);n.worker?n.worker.enqueue("mqtt-publish",()=>new Promise((u,m)=>{n.client.publish(e,c,{qos:a},g=>g?m(g):u());})):n.client.publish(e,c,{qos:a});}return {name:"mqtt",onConnection(e){s.set(e.identity,Date.now()),l.has("connect")&&r(o("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")){s.delete(e.identity);return}let c=s.get(e.identity),u=c?Math.round((Date.now()-c)/1e3):0;s.delete(e.identity),r(o("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(o(`message/${t.direction}`,e.identity),i);},onSecurityEvent(e){l.has("security")&&r(o("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(o("error",e.identity),{identity:e.identity,error:t.message,timestamp:new Date().toISOString()});},onAuthFailed(e,t,i){l.has("auth_failed")&&r(o("auth_failed"),{identity:e.identity,ip:e.remoteAddress,code:t,reason:i,timestamp:new Date().toISOString()});},onEviction(e,t){l.has("eviction")&&r(o("eviction",e.identity),{identity:e.identity,evictedBy:t.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){r(o("closing"),{timestamp:new Date().toISOString()});},onClose(){s.clear();try{n.client.end(!1);}catch{}}}}function H(n){let d=n?.tracer??null,l=new Map;return {name:"otel",async onInit(a){if(!d)try{d=(await import('@opentelemetry/api')).trace.getTracer(n?.serviceName??"ocpp-server","1.0.0");}catch{a.log.warn?.("otelPlugin: @opentelemetry/api not found \u2014 plugin disabled. Install it as a peer dependency."),d=null;}},onConnection(a){if(!d)return;let s=d.startSpan("ocpp.connection",{kind:1});s.setAttribute("ocpp.identity",a.identity),s.setAttribute("ocpp.protocol",a.protocol??"unknown"),s.setAttribute("net.peer.ip",a.handshake.remoteAddress),l.set(a.identity,{span:s,startTime:Date.now()});},onDisconnect(a,s){let o=l.get(a.identity);if(!o)return;let r=Date.now()-o.startTime;o.span.setAttribute("ocpp.close_code",s),o.span.setAttribute("ocpp.duration_ms",r),o.span.setStatus({code:1}),o.span.end(),l.delete(a.identity);},onMessage(a,s){if(!d)return;let o=s.message[0];if(o!==2){let t=l.get(a.identity);t&&t.span.addEvent(o===3?"ocpp.call_result":"ocpp.call_error",{direction:s.direction,"ocpp.message_id":String(s.message[1]),...s.ctx.latencyMs!==void 0&&{"ocpp.latency_ms":s.ctx.latencyMs}});return}let r=String(s.message[2]??"unknown"),e=d.startSpan(`ocpp.call.${r}`,{kind:s.direction==="IN"?1:2});e.setAttribute("ocpp.identity",a.identity),e.setAttribute("ocpp.method",r),e.setAttribute("ocpp.direction",s.direction),e.setAttribute("ocpp.message_id",String(s.message[1])),s.ctx.latencyMs!==void 0&&e.setAttribute("ocpp.latency_ms",s.ctx.latencyMs),e.setStatus({code:1}),e.end();},onError(a,s){let o=l.get(a.identity);o&&(o.span.recordException(s),o.span.addEvent("ocpp.error",{"error.message":s.message}));},onHandlerError(a,s,o){let r=l.get(a.identity);r&&(r.span.recordException(o),r.span.addEvent("ocpp.handler_error",{"ocpp.method":s,"error.message":o.message}));},onBadMessage(a,s,o){let r=l.get(a.identity);r&&(r.span.recordException(o),r.span.addEvent("ocpp.bad_message",{"raw.preview":typeof s=="string"?s.slice(0,200):"<buffer>","error.message":o.message}));},onValidationFailure(a,s,o){let r=l.get(a.identity);r&&(r.span.recordException(o),r.span.addEvent("ocpp.validation_failure",{"error.message":o.message}));},onRateLimitExceeded(a){let s=l.get(a.identity);s&&s.span.addEvent("ocpp.rate_limit_exceeded");},onPongTimeout(a){let s=l.get(a.identity);s&&s.span.addEvent("ocpp.pong_timeout");},onBackpressure(a,s){let o=l.get(a.identity);o&&o.span.addEvent("ocpp.backpressure",{"ocpp.buffered_bytes":s});},onEviction(a,s){let o=l.get(a.identity);o&&o.span.addEvent("ocpp.evicted",{"net.peer.ip.new":s.handshake.remoteAddress});},onTelemetry(a){if(!d)return;let s=d.startSpan("ocpp.telemetry_push",{kind:0});s.setAttribute("ocpp.connected_clients",a.connectedClients),s.setAttribute("ocpp.active_sessions",a.activeSessions),s.setAttribute("ocpp.uptime_seconds",a.uptimeSeconds),s.setAttribute("ocpp.memory_rss",a.memoryUsage.rss),s.setAttribute("ocpp.memory_heap_used",a.memoryUsage.heapUsed),s.setAttribute("ocpp.pid",a.pid),a.webSockets&&(s.setAttribute("ocpp.ws_total",a.webSockets.total),s.setAttribute("ocpp.ws_buffered_amount",a.webSockets.bufferedAmount)),s.setStatus({code:1}),s.end();},onSecurityEvent(a){if(!d)return;let s=d.startSpan("ocpp.security_event",{kind:0});s.setAttribute("security.event_type",a.type),a.identity&&s.setAttribute("ocpp.identity",a.identity),a.ip&&s.setAttribute("net.peer.ip",a.ip),s.setStatus({code:2,message:a.type}),s.end();},onAuthFailed(a,s,o){if(!d)return;let r=d.startSpan("ocpp.auth_failed",{kind:1});r.setAttribute("ocpp.identity",a.identity),r.setAttribute("net.peer.ip",a.remoteAddress),r.setAttribute("ocpp.close_code",s),r.setAttribute("ocpp.close_reason",o),r.setStatus({code:2,message:"Auth failed"}),r.end();},onClosing(){for(let[,a]of l)a.span.addEvent("ocpp.server_closing");},onClose(){for(let[,a]of l)a.span.setStatus({code:2,message:"Server shutdown"}),a.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***",a=n.incoming??true,s=n.outgoing??true;function o(e){if(!e||typeof e!="object")return e;if(Array.isArray(e))return e.map(o);let t={};for(let[i,c]of Object.entries(e))d.has(i)?t[i]=l:c&&typeof c=="object"?t[i]=o(c):t[i]=c;return t}let r=async(e,t)=>{a&&(e.type==="incoming_call"&&e.params?e.params=o(e.params):e.type==="incoming_result"&&e.payload&&(e.payload=o(e.payload))),s&&(e.type==="outgoing_call"&&e.params?e.params=o(e.params):e.type==="outgoing_result"&&e.payload&&(e.payload=o(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,a=n.windowMs??3e5,s=n.maxTrackedKeys??1e4,o=n.logger,r=new v(s),e=new v(s);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<a);return r.set(u,y),y}function c(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:a};Promise.resolve(P.send(_)).catch(k=>{o?.error?.("[rate-limit-notifier] Alert delivery failed:",k);});}return {name:"rate-limit-notifier",onRateLimitExceeded(u,m){c(u.identity,u.handshake.remoteAddress,"RATE_LIMIT_EXCEEDED");},onSecurityEvent(u){(u.type==="RATE_LIMIT_EXCEEDED"||u.type==="CONNECTION_RATE_LIMIT")&&c(u.identity,u.ip,u.type);},onClose(){r.clear(),e.clear();}}}function j(n){let d=n.mode??"pubsub",l=n.prefix??"ocpp",a=new Set(n.events??["connect","disconnect","message","security"]),s=n.maxStreamLength??1e4,o=n.serialize??JSON.stringify,r=new Map;function e(i){return `${l}:${i}`}function t(i,c){if(!a.has(i))return;let u=e(i),m=o(c),g=async()=>{d==="stream"&&n.client.xadd?await n.client.xadd(u,"MAXLEN","~",s,"*","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,c,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:c,reason:u,durationSec:g,timestamp:new Date().toISOString()});},onMessage(i,c){let u={identity:i.identity,direction:c.direction,messageType:c.message[0],timestamp:c.ctx.timestamp};c.message[0]===2&&c.message[2]&&(u.method=c.message[2]),c.ctx.latencyMs!==void 0&&(u.latencyMs=c.ctx.latencyMs),n.includePayload&&(u.payload=c.message),t(`message:${c.direction}`,u);},onSecurityEvent(i){t("security",{type:i.type,identity:i.identity,ip:i.ip,timestamp:i.timestamp,details:i.details});},onAuthFailed(i,c,u){t("auth_failed",{identity:i.identity,ip:i.remoteAddress,code:c,reason:u,timestamp:new Date().toISOString()});},onEviction(i,c){t("eviction",{identity:i.identity,evictedBy:c.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:",a=n.syntheticResponse??true,s=n.flushConcurrency??5,o=n.flushDelayMs??200,r=n.logger,e=new Set;function t(i){return new Promise(c=>setTimeout(c,i))}return {name:"replay-buffer",onConnection(i){let c=`${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(c,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(a)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(c);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>=s&&(await t(o),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,a=new Map,s;for(let r of n.rules)r.method==="*"?s=r:a.set(r.method,r);function o(r){return a.get(r)??s}return {name:"schema-versioning",onConnection(r){if(n.applyWhen&&r.protocol!==n.applyWhen)return;let e=async(t,i)=>{let c=t.method,u=o(c);if(!u){if(d==="reject")throw l?.warn?.(`[schema-versioning] No transform rule for method "${c}", rejecting`),new Error(`Schema versioning: no transform rule for "${c}" (${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 ${c} UP: ${n.sourceVersion} \u2192 ${n.targetVersion}`);}catch(m){l?.warn?.(`[schema-versioning] Transform UP failed for ${c}:`,m);}else if(t.type==="outgoing_call")try{let m=u.transform(t.params,"down");t.params=m,l?.debug?.(`[schema-versioning] Transformed ${c} DOWN: ${n.targetVersion} \u2192 ${n.sourceVersion}`);}catch(m){l?.warn?.(`[schema-versioning] Transform DOWN failed for ${c}:`,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 ${c}:`,m);}return i()};r.use(e);}}}function z(n){let d=n?.logger??console,l=n?.logLevel??"standard",a=l==="standard"||l==="verbose",s=l==="verbose",o=new Map;return {name:"session-log",onConnection(r){o.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=o.get(r.identity),c=i?Math.round((Date.now()-i)/1e3):0;o.delete(r.identity),d.info("[session] disconnected",{identity:r.identity,code:e,reason:t,durationSec:c});},onError(r,e){a&&(d.error??d.warn)("[session] error",{identity:r.identity,error:e.message});},onAuthFailed(r,e,t){a&&d.warn("[session] auth failed",{identity:r.identity,ip:r.remoteAddress,code:e,reason:t});},onEviction(r,e){a&&d.warn("[session] evicted",{identity:r.identity,evictedIp:r.handshake.remoteAddress,newIp:e.handshake.remoteAddress});},onBadMessage(r,e){s&&d.warn("[session] bad message",{identity:r.identity,raw:typeof e=="string"?e.slice(0,200):"<buffer>"});},onSecurityEvent(r){s&&d.warn("[session] security event",{type:r.type,identity:r.identity,ip:r.ip,details:r.details});},onHandlerError(r,e,t){s&&(d.error??d.warn)("[session] handler error",{identity:r.identity,method:e,error:t.message});},onValidationFailure(r,e,t){s&&d.warn("[session] validation failure",{identity:r.identity,error:t.message});},onRateLimitExceeded(r){a&&d.warn("[session] rate limit exceeded",{identity:r.identity,ip:r.handshake.remoteAddress});},onPongTimeout(r){s&&d.warn("[session] pong timeout (dead peer)",{identity:r.identity});},onBackpressure(r,e){s&&d.warn("[session] backpressure",{identity:r.identity,bufferedBytes:e});},onClose(){o.clear();}}}function G(n){let d=new Set(n.events??["init","connect","disconnect","close"]),l=n.timeout??5e3,a=n.retries??1;async function s(o){if(!d.has(o.event))return;let r=JSON.stringify(o),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<=a;t++)try{let i=new AbortController,c=setTimeout(()=>i.abort(),l);await fetch(n.url,{method:"POST",headers:e,body:r,signal:i.signal}),clearTimeout(c);return}catch{}}return {name:"webhook",onInit(){s({event:"init",timestamp:new Date().toISOString()}).catch(()=>{});},onConnection(o){s({event:"connect",timestamp:new Date().toISOString(),data:{identity:o.identity,ip:o.handshake.remoteAddress,protocol:o.protocol}}).catch(()=>{});},onDisconnect(o,r,e){s({event:"disconnect",timestamp:new Date().toISOString(),data:{identity:o.identity,code:r,reason:e}}).catch(()=>{});},onSecurityEvent(o){s({event:"security",timestamp:o.timestamp,data:{type:o.type,identity:o.identity,ip:o.ip,details:o.details}}).catch(()=>{});},onAuthFailed(o,r,e){s({event:"auth_failed",timestamp:new Date().toISOString(),data:{identity:o.identity,ip:o.remoteAddress,code:r,reason:e}}).catch(()=>{});},onEviction(o,r){s({event:"eviction",timestamp:new Date().toISOString(),data:{identity:o.identity,evictedIp:o.handshake.remoteAddress,newIp:r.handshake.remoteAddress}}).catch(()=>{});},onClosing(){s({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=G;
|
|
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(n){let d=n.exchange??"ocpp.events",l=n.routingKey??"ocpp.{event}.{identity}",a=new Set(n.events??["connect","disconnect","message","security"]),s={persistent:n.publishOptions?.persistent??true,contentType:n.publishOptions?.contentType??"application/json",...n.publishOptions?.priority!==void 0&&{priority:n.publishOptions.priority}},o=new Map;function r(t,i){return l.replace("{event}",t).replace("{identity}",i??"server")}function e(t,i,c){if(!a.has(t))return;let u=r(t,i),m=Buffer.from(JSON.stringify(c));if(n.worker)n.worker.enqueue("amqp-publish",async()=>{n.channel.publish(d,u,m,s);});else try{n.channel.publish(d,u,m,s);}catch{}}return {name:"amqp",onConnection(t){o.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,c){let u=o.get(t.identity),m=u?Math.round((Date.now()-u)/1e3):0;o.delete(t.identity),e("disconnect",t.identity,{identity:t.identity,code:i,reason:c,durationSec:m,timestamp:new Date().toISOString()});},onMessage(t,i){let c={identity:t.identity,direction:i.direction,messageType:i.message[0],timestamp:i.ctx.timestamp};i.message[0]===2&&i.message[2]&&(c.method=i.message[2]),i.ctx.latencyMs!==void 0&&(c.latencyMs=i.ctx.latencyMs),n.includePayload&&(c.payload=i.message),e(`message.${i.direction}`,t.identity,c);},onSecurityEvent(t){e("security",t.identity,{type:t.type,identity:t.identity,ip:t.ip,timestamp:t.timestamp,details:t.details});},onAuthFailed(t,i,c){e("auth_failed",t.identity,{identity:t.identity,ip:t.remoteAddress,code:i,reason:c,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(){o.clear();try{n.channel.close();}catch{}}}}function L(n){let d=n?.reconnectThreshold??5,l=n?.authFailureThreshold??5,a=n?.badMessageThreshold??10,s=n?.evictionThreshold??3,o=n?.windowMs??6e4,r=new Map,e=new Map,t=new Map,i=new Map,c=null,u=null;function m(p,f){let _=f-o,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>_&&c){let E={type:w,identity:b.identity,ip:b.ip??b.evictedIp,timestamp:new Date().toISOString(),details:{...b,countInWindow:k.length,threshold:_,windowMs:o}};c.emit("securityEvent",E);}}return {name:"anomaly",onInit(p){c=p,u=setInterval(()=>{let f=Date.now();g(r,f),g(e,f),g(t,f),g(i,f);},o).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,a,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress});},onValidationFailure(p){y(t,p.identity,a,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress,source:"validation_failure"});},onEviction(p,f){y(i,p.identity,s,"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(),c=null;}}}function D(n){let d=n?.concurrency??10,l=n?.maxQueueSize??1e3,a=n?.overflowStrategy??"drop-oldest",s=n?.drainTimeoutMs??5e3,o=[],r=0,e=0,t=true,i=null;function c(){for(;r<d&&o.length>0;){let g=o.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&&o.length===0&&i&&(i(),i=null),c();});}}function u(g,y){if(!t)return false;if(o.length>=l){if(a==="drop-newest")return e++,n?.logger?.warn?.(`[async-worker] Queue full (${l}), dropping task: ${g}`),false;let p=o.shift();e++,n?.logger?.warn?.(`[async-worker] Queue full (${l}), dropping oldest task: ${p?.name??"unknown"}`);}return o.push({name:g,fn:y}),c(),true}return {name:"async-worker",enqueue:u,queueSize:()=>o.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 ${o.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&&o.length===0?Promise.resolve():new Promise(g=>{i=g;let y=setTimeout(()=>{n?.logger?.warn?.(`[async-worker] Drain timeout (${s}ms), ${r} tasks still active, ${o.length} queued`),o.length=0,i=null,g();},s);y&&typeof y=="object"&&"unref"in y&&y.unref();})},onClose(){t=false,o.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 a=this.keys().next().value;a!==void 0&&this.delete(a);}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,a=n?.maxConcurrent??20,s=n?.maxTrackedClients??1e4,o=n?.logger,r=n?.onStateChange,e=new S(s);function t(c){let u=e.get(c);return u||(u={state:"CLOSED",failures:0,lastFailure:0,concurrentCalls:0},e.set(c,u)),u}function i(c,u){let m=t(c),g=m.state;g!==u&&(m.state=u,o?.warn?.(`[circuit-breaker] ${c}: ${g} \u2192 ${u}`),r?.(c,g,u));}return {name:"circuit-breaker",onConnection(c){let u=t(c.identity);c.use(async(m,g)=>{if(m.type!=="outgoing_call")return g();if(u.concurrentCalls>=a)throw o?.warn?.(`[circuit-breaker] ${c.identity}: concurrent limit (${a}) reached, rejecting ${m.method}`),new Error(`Circuit breaker: concurrent call limit exceeded for ${c.identity}`);let y=Date.now();if(u.state==="OPEN")if(y-u.lastFailure>=l)i(c.identity,"HALF_OPEN");else throw new Error(`Circuit breaker OPEN for ${c.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(c.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(c.identity,"OPEN"),p}});},onDisconnect(c){let u=e.get(c.identity);u&&(u.concurrentCalls=0);},onClose(){e.clear();}}}function I(n){let d=n.maxConnections,l=n.closeCode??4029,a=n.closeReason??"Connection limit reached",s=n.forceCloseOnPongTimeout??true,o=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:a}));},onDisconnect(){r=Math.max(0,r-1);},onPongTimeout(e){s&&(n.logger?.warn?.(`[connection-guard] Pong timeout \u2014 closing dead peer: ${e.identity}`),e.close({code:4e3,reason:"Pong timeout"}));},onBackpressure(e,t){o&&(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.handle("Heartbeat",()=>({currentTime:new Date().toISOString()}));}}}function q(n){let d=n.topic??"ocpp.events",l=n.topicRouting??false,a=new Set(n.events??["connect","disconnect","message","security"]),s=new Map;function o(e){return l?`${d}.${e}`:d}function r(e,t,i){if(!a.has(e.split(".")[0]))return;let c=o(e.split(".")[0]),u=JSON.stringify(i),m=t??"server";n.worker?n.worker.enqueue("kafka-publish",async()=>{await n.producer.send({topic:c,messages:[{key:m,value:u,headers:{event:e}}]});}):n.producer.send({topic:c,messages:[{key:m,value:u,headers:{event:e}}]}).catch(()=>{});}return {name:"kafka",onConnection(e){s.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 c=s.get(e.identity),u=c?Math.round((Date.now()-c)/1e3):0;s.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(){s.clear();}}}function B(n){let d=n.redis,l=n.ttlMs??3e5,a=n.prefix??"ocpp:dedup:",s=n.redisStyle??"positional",o=n.logger;async function r(e){return s==="options"?await d.set(e,"1",{PX:l,NX:true})!==null:await d.set(e,"1","PX",l,"NX")==="OK"}return {name:"message-dedup",async onBeforeReceive(e,t){let i;try{let u=typeof t=="string"?t:t?.toString()||"",m=JSON.parse(u);Array.isArray(m)&&m.length>1&&(i=String(m[1]));}catch{return}if(!i)return;let c=`${a}${e.identity}:${i}`;try{if(!await r(c))return o?.warn?.(`[message-dedup] Dropping duplicate message: ${c}`),!1}catch(u){o?.error?.("[message-dedup] Redis failure, falling through:",u);}}}}function F(n){let d=n?.intervalMs??3e4,l=0,a=0,s=0,o=0,r=0,e=Date.now(),t=null,i=0,c=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:a,activeConnections:s,peakConnections:o,connectionDurationAvgMs:a>0?Math.round(r/a):0,uptimeMs:Date.now()-e,timestamp:new Date().toISOString(),totalMessagesIn:i,totalMessagesOut:c,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++,s++,s>o&&(o=s),A.set(O.identity,Date.now());},onDisconnect(O){a++,s=Math.max(0,s-1);let C=A.get(O.identity);C&&(r+=Date.now()-C,A.delete(O.identity));},onMessage(O,C){C.direction==="IN"?i++:c++;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 ${a}`,"# HELP ocpp_connections_active Currently active connections","# TYPE ocpp_connections_active gauge",`ocpp_connections_active ${s}`,"# HELP ocpp_connections_peak Highest concurrent connections","# TYPE ocpp_connections_peak gauge",`ocpp_connections_peak ${o}`,"# 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 ${c}`,"# 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"]),a=n.qos??0,s=new Map;function o(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,c=JSON.stringify(i);n.worker?n.worker.enqueue("mqtt-publish",()=>new Promise((u,m)=>{n.client.publish(e,c,{qos:a},g=>g?m(g):u());})):n.client.publish(e,c,{qos:a});}return {name:"mqtt",onConnection(e){s.set(e.identity,Date.now()),l.has("connect")&&r(o("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")){s.delete(e.identity);return}let c=s.get(e.identity),u=c?Math.round((Date.now()-c)/1e3):0;s.delete(e.identity),r(o("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(o(`message/${t.direction}`,e.identity),i);},onSecurityEvent(e){l.has("security")&&r(o("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(o("error",e.identity),{identity:e.identity,error:t.message,timestamp:new Date().toISOString()});},onAuthFailed(e,t,i){l.has("auth_failed")&&r(o("auth_failed"),{identity:e.identity,ip:e.remoteAddress,code:t,reason:i,timestamp:new Date().toISOString()});},onEviction(e,t){l.has("eviction")&&r(o("eviction",e.identity),{identity:e.identity,evictedBy:t.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){r(o("closing"),{timestamp:new Date().toISOString()});},onClose(){s.clear();try{n.client.end(!1);}catch{}}}}function W(n){let d=n?.tracer??null,l=new Map;return {name:"otel",async onInit(a){if(!d)try{d=(await import('@opentelemetry/api')).trace.getTracer(n?.serviceName??"ocpp-server","1.0.0");}catch{a.log.warn?.("otelPlugin: @opentelemetry/api not found \u2014 plugin disabled. Install it as a peer dependency."),d=null;}},onConnection(a){if(!d)return;let s=d.startSpan("ocpp.connection",{kind:1});s.setAttribute("ocpp.identity",a.identity),s.setAttribute("ocpp.protocol",a.protocol??"unknown"),s.setAttribute("net.peer.ip",a.handshake.remoteAddress),l.set(a.identity,{span:s,startTime:Date.now()});},onDisconnect(a,s){let o=l.get(a.identity);if(!o)return;let r=Date.now()-o.startTime;o.span.setAttribute("ocpp.close_code",s),o.span.setAttribute("ocpp.duration_ms",r),o.span.setStatus({code:1}),o.span.end(),l.delete(a.identity);},onMessage(a,s){if(!d)return;let o=s.message[0];if(o!==2){let t=l.get(a.identity);t&&t.span.addEvent(o===3?"ocpp.call_result":"ocpp.call_error",{direction:s.direction,"ocpp.message_id":String(s.message[1]),...s.ctx.latencyMs!==void 0&&{"ocpp.latency_ms":s.ctx.latencyMs}});return}let r=String(s.message[2]??"unknown"),e=d.startSpan(`ocpp.call.${r}`,{kind:s.direction==="IN"?1:2});e.setAttribute("ocpp.identity",a.identity),e.setAttribute("ocpp.method",r),e.setAttribute("ocpp.direction",s.direction),e.setAttribute("ocpp.message_id",String(s.message[1])),s.ctx.latencyMs!==void 0&&e.setAttribute("ocpp.latency_ms",s.ctx.latencyMs),e.setStatus({code:1}),e.end();},onError(a,s){let o=l.get(a.identity);o&&(o.span.recordException(s),o.span.addEvent("ocpp.error",{"error.message":s.message}));},onHandlerError(a,s,o){let r=l.get(a.identity);r&&(r.span.recordException(o),r.span.addEvent("ocpp.handler_error",{"ocpp.method":s,"error.message":o.message}));},onBadMessage(a,s,o){let r=l.get(a.identity);r&&(r.span.recordException(o),r.span.addEvent("ocpp.bad_message",{"raw.preview":typeof s=="string"?s.slice(0,200):"<buffer>","error.message":o.message}));},onValidationFailure(a,s,o){let r=l.get(a.identity);r&&(r.span.recordException(o),r.span.addEvent("ocpp.validation_failure",{"error.message":o.message}));},onRateLimitExceeded(a){let s=l.get(a.identity);s&&s.span.addEvent("ocpp.rate_limit_exceeded");},onPongTimeout(a){let s=l.get(a.identity);s&&s.span.addEvent("ocpp.pong_timeout");},onBackpressure(a,s){let o=l.get(a.identity);o&&o.span.addEvent("ocpp.backpressure",{"ocpp.buffered_bytes":s});},onEviction(a,s){let o=l.get(a.identity);o&&o.span.addEvent("ocpp.evicted",{"net.peer.ip.new":s.handshake.remoteAddress});},onTelemetry(a){if(!d)return;let s=d.startSpan("ocpp.telemetry_push",{kind:0});s.setAttribute("ocpp.connected_clients",a.connectedClients),s.setAttribute("ocpp.active_sessions",a.activeSessions),s.setAttribute("ocpp.uptime_seconds",a.uptimeSeconds),s.setAttribute("ocpp.memory_rss",a.memoryUsage.rss),s.setAttribute("ocpp.memory_heap_used",a.memoryUsage.heapUsed),s.setAttribute("ocpp.pid",a.pid),a.webSockets&&(s.setAttribute("ocpp.ws_total",a.webSockets.total),s.setAttribute("ocpp.ws_buffered_amount",a.webSockets.bufferedAmount)),s.setStatus({code:1}),s.end();},onSecurityEvent(a){if(!d)return;let s=d.startSpan("ocpp.security_event",{kind:0});s.setAttribute("security.event_type",a.type),a.identity&&s.setAttribute("ocpp.identity",a.identity),a.ip&&s.setAttribute("net.peer.ip",a.ip),s.setStatus({code:2,message:a.type}),s.end();},onAuthFailed(a,s,o){if(!d)return;let r=d.startSpan("ocpp.auth_failed",{kind:1});r.setAttribute("ocpp.identity",a.identity),r.setAttribute("net.peer.ip",a.remoteAddress),r.setAttribute("ocpp.close_code",s),r.setAttribute("ocpp.close_reason",o),r.setStatus({code:2,message:"Auth failed"}),r.end();},onClosing(){for(let[,a]of l)a.span.addEvent("ocpp.server_closing");},onClose(){for(let[,a]of l)a.span.setStatus({code:2,message:"Server shutdown"}),a.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***",a=n.incoming??true,s=n.outgoing??true;function o(e){if(!e||typeof e!="object")return e;if(Array.isArray(e))return e.map(o);let t={};for(let[i,c]of Object.entries(e))d.has(i)?t[i]=l:c&&typeof c=="object"?t[i]=o(c):t[i]=c;return t}let r=async(e,t)=>{a&&(e.type==="incoming_call"&&e.params?e.params=o(e.params):e.type==="incoming_result"&&e.payload&&(e.payload=o(e.payload))),s&&(e.type==="outgoing_call"&&e.params?e.params=o(e.params):e.type==="outgoing_result"&&e.payload&&(e.payload=o(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,a=n.windowMs??3e5,s=n.maxTrackedKeys??1e4,o=n.logger,r=new S(s),e=new S(s);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<a);return r.set(u,y),y}function c(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:a};Promise.resolve(w.send(b)).catch(v=>{o?.error?.("[rate-limit-notifier] Alert delivery failed:",v);});}return {name:"rate-limit-notifier",onRateLimitExceeded(u,m){c(u.identity,u.handshake.remoteAddress,"RATE_LIMIT_EXCEEDED");},onSecurityEvent(u){(u.type==="RATE_LIMIT_EXCEEDED"||u.type==="CONNECTION_RATE_LIMIT")&&c(u.identity,u.ip,u.type);},onClose(){r.clear(),e.clear();}}}function Y(n){let d=n.mode??"pubsub",l=n.prefix??"ocpp",a=new Set(n.events??["connect","disconnect","message","security"]),s=n.maxStreamLength??1e4,o=n.serialize??JSON.stringify,r=new Map;function e(i){return `${l}:${i}`}function t(i,c){if(!a.has(i))return;let u=e(i),m=o(c),g=async()=>{d==="stream"&&n.client.xadd?await n.client.xadd(u,"MAXLEN","~",s,"*","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,c,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:c,reason:u,durationSec:g,timestamp:new Date().toISOString()});},onMessage(i,c){let u={identity:i.identity,direction:c.direction,messageType:c.message[0],timestamp:c.ctx.timestamp};c.message[0]===2&&c.message[2]&&(u.method=c.message[2]),c.ctx.latencyMs!==void 0&&(u.latencyMs=c.ctx.latencyMs),n.includePayload&&(u.payload=c.message),t(`message:${c.direction}`,u);},onSecurityEvent(i){t("security",{type:i.type,identity:i.identity,ip:i.ip,timestamp:i.timestamp,details:i.details});},onAuthFailed(i,c,u){t("auth_failed",{identity:i.identity,ip:i.remoteAddress,code:c,reason:u,timestamp:new Date().toISOString()});},onEviction(i,c){t("eviction",{identity:i.identity,evictedBy:c.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:",a=n.syntheticResponse??true,s=n.flushConcurrency??5,o=n.flushDelayMs??200,r=n.logger,e=new Set;function t(i){return new Promise(c=>setTimeout(c,i))}return {name:"replay-buffer",onConnection(i){let c=`${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(c,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(a)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(c);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>=s&&(await t(o),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,a=new Map,s;for(let r of n.rules)r.method==="*"?s=r:a.set(r.method,r);function o(r){return a.get(r)??s}return {name:"schema-versioning",onConnection(r){if(n.applyWhen&&r.protocol!==n.applyWhen)return;let e=async(t,i)=>{let c=t.method,u=o(c);if(!u){if(d==="reject")throw l?.warn?.(`[schema-versioning] No transform rule for method "${c}", rejecting`),new Error(`Schema versioning: no transform rule for "${c}" (${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 ${c} UP: ${n.sourceVersion} \u2192 ${n.targetVersion}`);}catch(m){l?.warn?.(`[schema-versioning] Transform UP failed for ${c}:`,m);}else if(t.type==="outgoing_call")try{let m=u.transform(t.params,"down");t.params=m,l?.debug?.(`[schema-versioning] Transformed ${c} DOWN: ${n.targetVersion} \u2192 ${n.sourceVersion}`);}catch(m){l?.warn?.(`[schema-versioning] Transform DOWN failed for ${c}:`,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 ${c}:`,m);}return i()};r.use(e);}}}function U(n){let d=n?.logger??console,l=n?.logLevel??"standard",a=l==="standard"||l==="verbose",s=l==="verbose",o=new Map;return {name:"session-log",onConnection(r){o.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=o.get(r.identity),c=i?Math.round((Date.now()-i)/1e3):0;o.delete(r.identity),d.info("[session] disconnected",{identity:r.identity,code:e,reason:t,durationSec:c});},onError(r,e){a&&(d.error??d.warn)("[session] error",{identity:r.identity,error:e.message});},onAuthFailed(r,e,t){a&&d.warn("[session] auth failed",{identity:r.identity,ip:r.remoteAddress,code:e,reason:t});},onEviction(r,e){a&&d.warn("[session] evicted",{identity:r.identity,evictedIp:r.handshake.remoteAddress,newIp:e.handshake.remoteAddress});},onBadMessage(r,e){s&&d.warn("[session] bad message",{identity:r.identity,raw:typeof e=="string"?e.slice(0,200):"<buffer>"});},onSecurityEvent(r){s&&d.warn("[session] security event",{type:r.type,identity:r.identity,ip:r.ip,details:r.details});},onHandlerError(r,e,t){s&&(d.error??d.warn)("[session] handler error",{identity:r.identity,method:e,error:t.message});},onValidationFailure(r,e,t){s&&d.warn("[session] validation failure",{identity:r.identity,error:t.message});},onRateLimitExceeded(r){a&&d.warn("[session] rate limit exceeded",{identity:r.identity,ip:r.handshake.remoteAddress});},onPongTimeout(r){s&&d.warn("[session] pong timeout (dead peer)",{identity:r.identity});},onBackpressure(r,e){s&&d.warn("[session] backpressure",{identity:r.identity,bufferedBytes:e});},onClose(){o.clear();}}}function X(n){let d=new Set(n.events??["init","connect","disconnect","close"]),l=n.timeout??5e3,a=n.retries??1;async function s(o){if(!d.has(o.event))return;let r=JSON.stringify(o),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<=a;t++)try{let i=new AbortController,c=setTimeout(()=>i.abort(),l);await fetch(n.url,{method:"POST",headers:e,body:r,signal:i.signal}),clearTimeout(c);return}catch{}}return {name:"webhook",onInit(){s({event:"init",timestamp:new Date().toISOString()}).catch(()=>{});},onConnection(o){s({event:"connect",timestamp:new Date().toISOString(),data:{identity:o.identity,ip:o.handshake.remoteAddress,protocol:o.protocol}}).catch(()=>{});},onDisconnect(o,r,e){s({event:"disconnect",timestamp:new Date().toISOString(),data:{identity:o.identity,code:r,reason:e}}).catch(()=>{});},onSecurityEvent(o){s({event:"security",timestamp:o.timestamp,data:{type:o.type,identity:o.identity,ip:o.ip,details:o.details}}).catch(()=>{});},onAuthFailed(o,r,e){s({event:"auth_failed",timestamp:new Date().toISOString(),data:{identity:o.identity,ip:o.remoteAddress,code:r,reason:e}}).catch(()=>{});},onEviction(o,r){s({event:"eviction",timestamp:new Date().toISOString(),data:{identity:o.identity,evictedIp:o.handshake.remoteAddress,newIp:r.handshake.remoteAddress}}).catch(()=>{});},onClosing(){s({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,X 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};
|