ocpp-ws-io 2.2.2 → 2.2.4

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.
@@ -1,4 +1,4 @@
1
- import { j as OCPPPlugin } from './types-xFfIgIuS.mjs';
1
+ import { j as OCPPPlugin } from './types-BsoUgWtt.mjs';
2
2
  import 'ws';
3
3
  import 'node:https';
4
4
  import 'node:http';
@@ -242,6 +242,12 @@ declare function anomalyPlugin(options?: AnomalyPluginOptions): OCPPPlugin;
242
242
  * Options for the circuit-breaker plugin.
243
243
  */
244
244
  interface CircuitBreakerOptions {
245
+ /**
246
+ * Maximum number of distinct client identities to track circuit state for.
247
+ * Bounds memory under identity churn / random-identity floods (LRU eviction).
248
+ * @default 10000
249
+ */
250
+ maxTrackedClients?: number;
245
251
  /**
246
252
  * Number of consecutive failures before the circuit opens.
247
253
  * @default 5
@@ -775,11 +781,19 @@ declare function otelPlugin(options?: OtelPluginOptions): OCPPPlugin;
775
781
 
776
782
  interface PiiRedactorOptions {
777
783
  /**
778
- * List of object keys that should be redacted.
779
- * Matches ANY key in the payload recursively.
780
- * @default ["idTag", "authorizationKey", "token", "password", "securityCode"]
784
+ * **Required.** List of object keys to redact (matched recursively, at any
785
+ * depth). There is no default you must explicitly list every key to redact,
786
+ * so nothing is ever scrubbed by accident.
787
+ *
788
+ * ⚠️ **Redaction mutates the live payload, not just logs.** If you list a key
789
+ * here that your handlers need on an **incoming** message (e.g. `idTag` for
790
+ * `Authorize` / `StartTransaction`), the handler will receive the redacted
791
+ * placeholder and cannot use the real value. Either set `incoming: false`, or
792
+ * don't include such keys, when you need the value at the handler.
793
+ *
794
+ * @example ["password", "authorizationKey", "token"]
781
795
  */
782
- sensitiveKeys?: string[];
796
+ sensitiveKeys: string[];
783
797
  /**
784
798
  * The replacement string to use for redacted values.
785
799
  * @default "***REDACTED***"
@@ -804,6 +818,12 @@ interface PiiRedactorOptions {
804
818
  * incoming and outgoing payloads. Because it mutates the payload inline, the redacted
805
819
  * data will be what application handlers, downstream plugins, and observability tools see.
806
820
  *
821
+ * ⚠️ **Caveat:** because it mutates the live payload (not a logging copy), redacting a
822
+ * key on **incoming** messages also hides it from your handlers. The default
823
+ * `sensitiveKeys` includes `idTag` — if your handlers authorize by `idTag`
824
+ * (`Authorize`, `StartTransaction`), either pass `incoming: false` or drop `idTag`
825
+ * from `sensitiveKeys` so the handler still receives the real value.
826
+ *
807
827
  * @example
808
828
  * ```ts
809
829
  * server.plugin(piiRedactorPlugin({
@@ -812,7 +832,7 @@ interface PiiRedactorOptions {
812
832
  * }));
813
833
  * ```
814
834
  */
815
- declare function piiRedactorPlugin(options?: PiiRedactorOptions): OCPPPlugin;
835
+ declare function piiRedactorPlugin(options: PiiRedactorOptions): OCPPPlugin;
816
836
 
817
837
  /**
818
838
  * Destination for rate-limit alerts.
@@ -871,6 +891,12 @@ interface RateLimitNotifierOptions {
871
891
  * @default 300000 (5 minutes)
872
892
  */
873
893
  windowMs?: number;
894
+ /**
895
+ * Maximum number of distinct identities/IPs to track alert state for.
896
+ * Bounds memory under identity/IP churn (LRU eviction of the oldest keys).
897
+ * @default 10000
898
+ */
899
+ maxTrackedKeys?: number;
874
900
  /**
875
901
  * Custom HTTP headers for webhook sink.
876
902
  */
package/dist/plugins.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { j as OCPPPlugin } from './types-xFfIgIuS.js';
1
+ import { j as OCPPPlugin } from './types-BsoUgWtt.js';
2
2
  import 'ws';
3
3
  import 'node:https';
4
4
  import 'node:http';
@@ -242,6 +242,12 @@ declare function anomalyPlugin(options?: AnomalyPluginOptions): OCPPPlugin;
242
242
  * Options for the circuit-breaker plugin.
243
243
  */
244
244
  interface CircuitBreakerOptions {
245
+ /**
246
+ * Maximum number of distinct client identities to track circuit state for.
247
+ * Bounds memory under identity churn / random-identity floods (LRU eviction).
248
+ * @default 10000
249
+ */
250
+ maxTrackedClients?: number;
245
251
  /**
246
252
  * Number of consecutive failures before the circuit opens.
247
253
  * @default 5
@@ -775,11 +781,19 @@ declare function otelPlugin(options?: OtelPluginOptions): OCPPPlugin;
775
781
 
776
782
  interface PiiRedactorOptions {
777
783
  /**
778
- * List of object keys that should be redacted.
779
- * Matches ANY key in the payload recursively.
780
- * @default ["idTag", "authorizationKey", "token", "password", "securityCode"]
784
+ * **Required.** List of object keys to redact (matched recursively, at any
785
+ * depth). There is no default you must explicitly list every key to redact,
786
+ * so nothing is ever scrubbed by accident.
787
+ *
788
+ * ⚠️ **Redaction mutates the live payload, not just logs.** If you list a key
789
+ * here that your handlers need on an **incoming** message (e.g. `idTag` for
790
+ * `Authorize` / `StartTransaction`), the handler will receive the redacted
791
+ * placeholder and cannot use the real value. Either set `incoming: false`, or
792
+ * don't include such keys, when you need the value at the handler.
793
+ *
794
+ * @example ["password", "authorizationKey", "token"]
781
795
  */
782
- sensitiveKeys?: string[];
796
+ sensitiveKeys: string[];
783
797
  /**
784
798
  * The replacement string to use for redacted values.
785
799
  * @default "***REDACTED***"
@@ -804,6 +818,12 @@ interface PiiRedactorOptions {
804
818
  * incoming and outgoing payloads. Because it mutates the payload inline, the redacted
805
819
  * data will be what application handlers, downstream plugins, and observability tools see.
806
820
  *
821
+ * ⚠️ **Caveat:** because it mutates the live payload (not a logging copy), redacting a
822
+ * key on **incoming** messages also hides it from your handlers. The default
823
+ * `sensitiveKeys` includes `idTag` — if your handlers authorize by `idTag`
824
+ * (`Authorize`, `StartTransaction`), either pass `incoming: false` or drop `idTag`
825
+ * from `sensitiveKeys` so the handler still receives the real value.
826
+ *
807
827
  * @example
808
828
  * ```ts
809
829
  * server.plugin(piiRedactorPlugin({
@@ -812,7 +832,7 @@ interface PiiRedactorOptions {
812
832
  * }));
813
833
  * ```
814
834
  */
815
- declare function piiRedactorPlugin(options?: PiiRedactorOptions): OCPPPlugin;
835
+ declare function piiRedactorPlugin(options: PiiRedactorOptions): OCPPPlugin;
816
836
 
817
837
  /**
818
838
  * Destination for rate-limit alerts.
@@ -871,6 +891,12 @@ interface RateLimitNotifierOptions {
871
891
  * @default 300000 (5 minutes)
872
892
  */
873
893
  windowMs?: number;
894
+ /**
895
+ * Maximum number of distinct identities/IPs to track alert state for.
896
+ * Bounds memory under identity/IP churn (LRU eviction of the oldest keys).
897
+ * @default 10000
898
+ */
899
+ maxTrackedKeys?: number;
874
900
  /**
875
901
  * Custom HTTP headers for webhook sink.
876
902
  */
package/dist/plugins.js CHANGED
@@ -1 +1 @@
1
- 'use strict';var crypto=require('crypto');function T(r){let u=r.exchange??"ocpp.events",d=r.routingKey??"ocpp.{event}.{identity}",c=new Set(r.events??["connect","disconnect","message","security"]),a={persistent:r.publishOptions?.persistent??true,contentType:r.publishOptions?.contentType??"application/json",...r.publishOptions?.priority!==void 0&&{priority:r.publishOptions.priority}},s=new Map;function n(t,i){return d.replace("{event}",t).replace("{identity}",i??"server")}function e(t,i,o){if(!c.has(t))return;let l=n(t,i),m=Buffer.from(JSON.stringify(o));if(r.worker)r.worker.enqueue("amqp-publish",async()=>{r.channel.publish(u,l,m,a);});else try{r.channel.publish(u,l,m,a);}catch{}}return {name:"amqp",onConnection(t){s.set(t.identity,Date.now()),e("connect",t.identity,{identity:t.identity,ip:t.handshake.remoteAddress,protocol:t.protocol,timestamp:new Date().toISOString()});},onDisconnect(t,i,o){let l=s.get(t.identity),m=l?Math.round((Date.now()-l)/1e3):0;s.delete(t.identity),e("disconnect",t.identity,{identity:t.identity,code:i,reason:o,durationSec:m,timestamp:new Date().toISOString()});},onMessage(t,i){let o={identity:t.identity,direction:i.direction,messageType:i.message[0],timestamp:i.ctx.timestamp};i.message[0]===2&&i.message[2]&&(o.method=i.message[2]),i.ctx.latencyMs!==void 0&&(o.latencyMs=i.ctx.latencyMs),r.includePayload&&(o.payload=i.message),e(`message.${i.direction}`,t.identity,o);},onSecurityEvent(t){e("security",t.identity,{type:t.type,identity:t.identity,ip:t.ip,timestamp:t.timestamp,details:t.details});},onAuthFailed(t,i,o){e("auth_failed",t.identity,{identity:t.identity,ip:t.remoteAddress,code:i,reason:o,timestamp:new Date().toISOString()});},onEviction(t,i){e("eviction",t.identity,{identity:t.identity,evictedBy:i.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){e("closing",void 0,{timestamp:new Date().toISOString()});},onClose(){s.clear();try{r.channel.close();}catch{}}}}function R(r){let u=r?.reconnectThreshold??5,d=r?.authFailureThreshold??5,c=r?.badMessageThreshold??10,a=r?.evictionThreshold??3,s=r?.windowMs??6e4,n=new Map,e=new Map,t=new Map,i=new Map,o=null,l=null;function m(p,f){let w=f-s,P=0;for(;P<p.length&&p[P]<w;)P++;return P>0?p.slice(P):p}function g(p,f){for(let[w,P]of p){let _=m(P,f);_.length===0?p.delete(w):p.set(w,_);}}function y(p,f,w,P,_){let k=Date.now(),b=p.get(f)??[];if(b=m(b,k),b.push(k),p.set(f,b),b.length>w&&o){let v={type:P,identity:_.identity,ip:_.ip??_.evictedIp,timestamp:new Date().toISOString(),details:{..._,countInWindow:b.length,threshold:w,windowMs:s}};o.emit("securityEvent",v);}}return {name:"anomaly",onInit(p){o=p,l=setInterval(()=>{let f=Date.now();g(n,f),g(e,f),g(t,f),g(i,f);},s).unref();},onConnection(p){y(n,p.identity,u,"ANOMALY_RAPID_RECONNECT",{identity:p.identity,ip:p.handshake.remoteAddress});},onAuthFailed(p,f,w){y(e,p.remoteAddress,d,"ANOMALY_AUTH_BRUTE_FORCE",{ip:p.remoteAddress,identity:p.identity,code:f,reason:w});},onBadMessage(p){y(t,p.identity,c,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress});},onValidationFailure(p){y(t,p.identity,c,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress,source:"validation_failure"});},onEviction(p,f){y(i,p.identity,a,"ANOMALY_IDENTITY_COLLISION",{identity:p.identity,evictedIp:p.handshake.remoteAddress,newIp:f.handshake.remoteAddress});},onClose(){l&&(clearInterval(l),l=null),n.clear(),e.clear(),t.clear(),i.clear(),o=null;}}}function x(r){let u=r?.concurrency??10,d=r?.maxQueueSize??1e3,c=r?.overflowStrategy??"drop-oldest",a=r?.drainTimeoutMs??5e3,s=[],n=0,e=0,t=true,i=null;function o(){for(;n<u&&s.length>0;){let g=s.shift();n++,g.fn().catch(y=>{if(r?.onError)try{r.onError(y instanceof Error?y:new Error(String(y)),g.name);}catch{}}).finally(()=>{n--,!t&&n===0&&s.length===0&&i&&(i(),i=null),o();});}}function l(g,y){if(!t)return false;if(s.length>=d){if(c==="drop-newest")return e++,r?.logger?.warn?.(`[async-worker] Queue full (${d}), dropping task: ${g}`),false;let p=s.shift();e++,r?.logger?.warn?.(`[async-worker] Queue full (${d}), dropping oldest task: ${p?.name??"unknown"}`);}return s.push({name:g,fn:y}),o(),true}return {name:"async-worker",enqueue:l,queueSize:()=>s.length,activeCount:()=>n,droppedCount:()=>e,getCustomMetrics(){return ["# HELP ocpp_async_worker_queue_size Current tasks waiting in the background queue","# TYPE ocpp_async_worker_queue_size gauge",`ocpp_async_worker_queue_size ${s.length}`,"# HELP ocpp_async_worker_active_tasks Currently executing background tasks","# TYPE ocpp_async_worker_active_tasks gauge",`ocpp_async_worker_active_tasks ${n}`,"# HELP ocpp_async_worker_dropped_total Tasks dropped due to queue overflow","# TYPE ocpp_async_worker_dropped_total counter",`ocpp_async_worker_dropped_total ${e}`]},onClosing(){return t=false,n===0&&s.length===0?Promise.resolve():new Promise(g=>{i=g;let y=setTimeout(()=>{r?.logger?.warn?.(`[async-worker] Drain timeout (${a}ms), ${n} tasks still active, ${s.length} queued`),s.length=0,i=null,g();},a);y&&typeof y=="object"&&"unref"in y&&y.unref();})},onClose(){t=false,s.length=0,i=null;}}}function L(r){let u=r?.failureThreshold??5,d=r?.resetTimeoutMs??3e4,c=r?.maxConcurrent??20,a=r?.logger,s=r?.onStateChange,n=new Map;function e(i){let o=n.get(i);return o||(o={state:"CLOSED",failures:0,lastFailure:0,concurrentCalls:0},n.set(i,o)),o}function t(i,o){let l=e(i),m=l.state;m!==o&&(l.state=o,a?.warn?.(`[circuit-breaker] ${i}: ${m} \u2192 ${o}`),s?.(i,m,o));}return {name:"circuit-breaker",onConnection(i){let o=e(i.identity);i.use(async(l,m)=>{if(l.type!=="outgoing_call")return m();if(o.concurrentCalls>=c)throw a?.warn?.(`[circuit-breaker] ${i.identity}: concurrent limit (${c}) reached, rejecting ${l.method}`),new Error(`Circuit breaker: concurrent call limit exceeded for ${i.identity}`);let g=Date.now();if(o.state==="OPEN")if(g-o.lastFailure>=d)t(i.identity,"HALF_OPEN");else throw new Error(`Circuit breaker OPEN for ${i.identity}: ${o.failures} consecutive failures`);o.concurrentCalls++;try{let y=await m();return o.concurrentCalls--,o.state==="HALF_OPEN"?(t(i.identity,"CLOSED"),o.failures=0):o.failures=Math.max(0,o.failures-1),y}catch(y){throw o.concurrentCalls--,o.failures++,o.lastFailure=Date.now(),(o.state==="HALF_OPEN"||o.failures>=u)&&t(i.identity,"OPEN"),y}});},onDisconnect(i){let o=n.get(i.identity);o&&(o.concurrentCalls=0);},onClose(){n.clear();}}}function D(r){let u=r.maxConnections,d=r.closeCode??4029,c=r.closeReason??"Connection limit reached",a=r.forceCloseOnPongTimeout??true,s=r.forceCloseOnBackpressure??false,n=0;return {name:"connection-guard",onConnection(e){n++,n>u&&(r.logger?.warn?.(`[connection-guard] Limit exceeded (${n}/${u}), closing: ${e.identity}`),e.close({code:d,reason:c}));},onDisconnect(){n=Math.max(0,n-1);},onPongTimeout(e){a&&(r.logger?.warn?.(`[connection-guard] Pong timeout \u2014 closing dead peer: ${e.identity}`),e.close({code:4e3,reason:"Pong timeout"}));},onBackpressure(e,t){s&&(r.logger?.warn?.(`[connection-guard] Backpressure (${t} bytes) \u2014 closing slow client: ${e.identity}`),e.close({code:4001,reason:"Backpressure exceeded"}));},onClose(){n=0;}}}function $(){return {name:"heartbeat",onConnection(r){r.handle("Heartbeat",()=>({currentTime:new Date().toISOString()}));}}}function I(r){let u=r.topic??"ocpp.events",d=r.topicRouting??false,c=new Set(r.events??["connect","disconnect","message","security"]),a=new Map;function s(e){return d?`${u}.${e}`:u}function n(e,t,i){if(!c.has(e.split(".")[0]))return;let o=s(e.split(".")[0]),l=JSON.stringify(i),m=t??"server";r.worker?r.worker.enqueue("kafka-publish",async()=>{await r.producer.send({topic:o,messages:[{key:m,value:l,headers:{event:e}}]});}):r.producer.send({topic:o,messages:[{key:m,value:l,headers:{event:e}}]}).catch(()=>{});}return {name:"kafka",onConnection(e){a.set(e.identity,Date.now()),n("connect",e.identity,{identity:e.identity,ip:e.handshake.remoteAddress,protocol:e.protocol,timestamp:new Date().toISOString()});},onDisconnect(e,t,i){let o=a.get(e.identity),l=o?Math.round((Date.now()-o)/1e3):0;a.delete(e.identity),n("disconnect",e.identity,{identity:e.identity,code:t,reason:i,durationSec:l,timestamp:new Date().toISOString()});},onMessage(e,t){let i={identity:e.identity,direction:t.direction,messageType:t.message[0],timestamp:t.ctx.timestamp};t.message[0]===2&&t.message[2]&&(i.method=t.message[2]),t.ctx.latencyMs!==void 0&&(i.latencyMs=t.ctx.latencyMs),r.includePayload&&(i.payload=t.message),n(`message.${t.direction}`,e.identity,i);},onSecurityEvent(e){n("security",e.identity,{type:e.type,identity:e.identity,ip:e.ip,timestamp:e.timestamp,details:e.details});},onAuthFailed(e,t,i){n("auth_failed",e.identity,{identity:e.identity,ip:e.remoteAddress,code:t,reason:i,timestamp:new Date().toISOString()});},onEviction(e,t){n("eviction",e.identity,{identity:e.identity,evictedBy:t.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){n("closing",void 0,{timestamp:new Date().toISOString()});},onClose(){a.clear();}}}function N(r){let u=r.redis,d=r.ttlMs??3e5,c=r.prefix??"ocpp:dedup:",a=r.redisStyle??"positional",s=r.logger;async function n(e){return a==="options"?await u.set(e,"1",{PX:d,NX:true})!==null:await u.set(e,"1","PX",d,"NX")==="OK"}return {name:"message-dedup",async onBeforeReceive(e,t){let i;try{let l=typeof t=="string"?t:t?.toString()||"",m=JSON.parse(l);Array.isArray(m)&&m.length>1&&(i=String(m[1]));}catch{return}if(!i)return;let o=`${c}${e.identity}:${i}`;try{if(!await n(o))return s?.warn?.(`[message-dedup] Dropping duplicate message: ${o}`),!1}catch(l){s?.error?.("[message-dedup] Redis failure, falling through:",l);}}}}function q(r){let u=r?.intervalMs??3e4,d=0,c=0,a=0,s=0,n=0,e=Date.now(),t=null,i=0,o=0,l=0,m=0,g=0,y=0,p=0,f=0,w=0,P=0,_=0,k=0,b=0,v=0,C=0,O=new Map;function A(){return {totalConnections:d,totalDisconnections:c,activeConnections:a,peakConnections:s,connectionDurationAvgMs:c>0?Math.round(n/c):0,uptimeMs:Date.now()-e,timestamp:new Date().toISOString(),totalMessagesIn:i,totalMessagesOut:o,totalCalls:l,totalCallResults:m,totalCallErrors:g,totalErrors:y,totalBadMessages:p,totalHandlerErrors:f,totalRateLimitHits:w,totalAuthFailures:P,totalEvictions:_,totalBackpressureEvents:k,totalPongTimeouts:b,totalValidationFailures:v,totalSecurityEvents:C}}return {name:"metrics",getMetrics:A,onInit(){e=Date.now(),u>0&&r?.onSnapshot&&(t=setInterval(()=>{r.onSnapshot(A());},u),t&&typeof t=="object"&&"unref"in t&&t.unref());},onConnection(S){d++,a++,a>s&&(s=a),O.set(S.identity,Date.now());},onDisconnect(S){c++,a=Math.max(0,a-1);let E=O.get(S.identity);E&&(n+=Date.now()-E,O.delete(S.identity));},onMessage(S,E){E.direction==="IN"?i++:o++;let M=E.message[0];M===2?l++:M===3?m++:M===4&&g++;},onError(){y++;},onBadMessage(){p++;},onHandlerError(){f++;},onRateLimitExceeded(){w++;},onAuthFailed(){P++;},onEviction(){_++;},onBackpressure(){k++;},onPongTimeout(){b++;},onValidationFailure(){v++;},onSecurityEvent(){C++;},getCustomMetrics(){return ["# HELP ocpp_connections_total Total connections since server start","# TYPE ocpp_connections_total counter",`ocpp_connections_total ${d}`,"# HELP ocpp_disconnections_total Total disconnections since server start","# TYPE ocpp_disconnections_total counter",`ocpp_disconnections_total ${c}`,"# HELP ocpp_connections_active Currently active connections","# TYPE ocpp_connections_active gauge",`ocpp_connections_active ${a}`,"# HELP ocpp_connections_peak Highest concurrent connections","# TYPE ocpp_connections_peak gauge",`ocpp_connections_peak ${s}`,"# HELP ocpp_connection_duration_avg_ms Average connection duration","# TYPE ocpp_connection_duration_avg_ms gauge",`ocpp_connection_duration_avg_ms ${A().connectionDurationAvgMs}`,"# HELP ocpp_messages_in_total Total inbound messages","# TYPE ocpp_messages_in_total counter",`ocpp_messages_in_total ${i}`,"# HELP ocpp_messages_out_total Total outbound messages","# TYPE ocpp_messages_out_total counter",`ocpp_messages_out_total ${o}`,"# HELP ocpp_calls_total Total CALL messages","# TYPE ocpp_calls_total counter",`ocpp_calls_total ${l}`,"# HELP ocpp_call_results_total Total CALLRESULT messages","# TYPE ocpp_call_results_total counter",`ocpp_call_results_total ${m}`,"# HELP ocpp_call_errors_total Total CALLERROR messages","# TYPE ocpp_call_errors_total counter",`ocpp_call_errors_total ${g}`,"# HELP ocpp_errors_total WebSocket/protocol errors","# TYPE ocpp_errors_total counter",`ocpp_errors_total ${y}`,"# HELP ocpp_bad_messages_total Malformed messages received","# TYPE ocpp_bad_messages_total counter",`ocpp_bad_messages_total ${p}`,"# HELP ocpp_handler_errors_total User handler errors","# TYPE ocpp_handler_errors_total counter",`ocpp_handler_errors_total ${f}`,"# HELP ocpp_rate_limit_hits_total Rate limit violations","# TYPE ocpp_rate_limit_hits_total counter",`ocpp_rate_limit_hits_total ${w}`,"# HELP ocpp_auth_failures_total Authentication failures","# TYPE ocpp_auth_failures_total counter",`ocpp_auth_failures_total ${P}`,"# HELP ocpp_evictions_total Client evictions","# TYPE ocpp_evictions_total counter",`ocpp_evictions_total ${_}`,"# HELP ocpp_backpressure_events_total Slow client backpressure events","# TYPE ocpp_backpressure_events_total counter",`ocpp_backpressure_events_total ${k}`,"# HELP ocpp_pong_timeouts_total Dead peer timeouts","# TYPE ocpp_pong_timeouts_total counter",`ocpp_pong_timeouts_total ${b}`,"# HELP ocpp_validation_failures_total Schema validation failures","# TYPE ocpp_validation_failures_total counter",`ocpp_validation_failures_total ${v}`,"# HELP ocpp_security_events_total Security events from anomaly detection","# TYPE ocpp_security_events_total counter",`ocpp_security_events_total ${C}`]},onClose(){t&&(clearInterval(t),t=null),O.clear();}}}function B(r){let u=r.topicPrefix??"ocpp",d=new Set(r.events??["connect","disconnect","message","security"]),c=r.qos??0,a=new Map;function s(e,t){return r.topicBuilder?r.topicBuilder(e,t):t?`${u}/${t}/${e}`:`${u}/${e}`}function n(e,t){let i=r.transform?r.transform(t):t,o=JSON.stringify(i);r.worker?r.worker.enqueue("mqtt-publish",()=>new Promise((l,m)=>{r.client.publish(e,o,{qos:c},g=>g?m(g):l());})):r.client.publish(e,o,{qos:c});}return {name:"mqtt",onConnection(e){a.set(e.identity,Date.now()),d.has("connect")&&n(s("connect",e.identity),{identity:e.identity,ip:e.handshake.remoteAddress,protocol:e.protocol,timestamp:new Date().toISOString()});},onDisconnect(e,t,i){if(!d.has("disconnect")){a.delete(e.identity);return}let o=a.get(e.identity),l=o?Math.round((Date.now()-o)/1e3):0;a.delete(e.identity),n(s("disconnect",e.identity),{identity:e.identity,code:t,reason:i,durationSec:l,timestamp:new Date().toISOString()});},onMessage(e,t){if(!d.has("message"))return;let i={identity:e.identity,direction:t.direction,messageType:t.message[0],timestamp:t.ctx.timestamp};t.message[0]===2&&t.message[2]&&(i.method=t.message[2]),t.ctx.latencyMs!==void 0&&(i.latencyMs=t.ctx.latencyMs),r.includePayload&&(i.payload=t.message),n(s(`message/${t.direction}`,e.identity),i);},onSecurityEvent(e){d.has("security")&&n(s("security",e.identity),{type:e.type,identity:e.identity,ip:e.ip,timestamp:e.timestamp,details:e.details});},onError(e,t){d.has("error")&&n(s("error",e.identity),{identity:e.identity,error:t.message,timestamp:new Date().toISOString()});},onAuthFailed(e,t,i){d.has("auth_failed")&&n(s("auth_failed"),{identity:e.identity,ip:e.remoteAddress,code:t,reason:i,timestamp:new Date().toISOString()});},onEviction(e,t){d.has("eviction")&&n(s("eviction",e.identity),{identity:e.identity,evictedBy:t.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){n(s("closing"),{timestamp:new Date().toISOString()});},onClose(){a.clear();try{r.client.end(!1);}catch{}}}}function F(r){let u=r?.tracer??null,d=new Map;return {name:"otel",async onInit(c){if(!u)try{u=(await import('@opentelemetry/api')).trace.getTracer(r?.serviceName??"ocpp-server","1.0.0");}catch{c.log.warn?.("otelPlugin: @opentelemetry/api not found \u2014 plugin disabled. Install it as a peer dependency."),u=null;}},onConnection(c){if(!u)return;let a=u.startSpan("ocpp.connection",{kind:1});a.setAttribute("ocpp.identity",c.identity),a.setAttribute("ocpp.protocol",c.protocol??"unknown"),a.setAttribute("net.peer.ip",c.handshake.remoteAddress),d.set(c.identity,{span:a,startTime:Date.now()});},onDisconnect(c,a){let s=d.get(c.identity);if(!s)return;let n=Date.now()-s.startTime;s.span.setAttribute("ocpp.close_code",a),s.span.setAttribute("ocpp.duration_ms",n),s.span.setStatus({code:1}),s.span.end(),d.delete(c.identity);},onMessage(c,a){if(!u)return;let s=a.message[0];if(s!==2){let t=d.get(c.identity);t&&t.span.addEvent(s===3?"ocpp.call_result":"ocpp.call_error",{direction:a.direction,"ocpp.message_id":String(a.message[1]),...a.ctx.latencyMs!==void 0&&{"ocpp.latency_ms":a.ctx.latencyMs}});return}let n=String(a.message[2]??"unknown"),e=u.startSpan(`ocpp.call.${n}`,{kind:a.direction==="IN"?1:2});e.setAttribute("ocpp.identity",c.identity),e.setAttribute("ocpp.method",n),e.setAttribute("ocpp.direction",a.direction),e.setAttribute("ocpp.message_id",String(a.message[1])),a.ctx.latencyMs!==void 0&&e.setAttribute("ocpp.latency_ms",a.ctx.latencyMs),e.setStatus({code:1}),e.end();},onError(c,a){let s=d.get(c.identity);s&&(s.span.recordException(a),s.span.addEvent("ocpp.error",{"error.message":a.message}));},onHandlerError(c,a,s){let n=d.get(c.identity);n&&(n.span.recordException(s),n.span.addEvent("ocpp.handler_error",{"ocpp.method":a,"error.message":s.message}));},onBadMessage(c,a,s){let n=d.get(c.identity);n&&(n.span.recordException(s),n.span.addEvent("ocpp.bad_message",{"raw.preview":typeof a=="string"?a.slice(0,200):"<buffer>","error.message":s.message}));},onValidationFailure(c,a,s){let n=d.get(c.identity);n&&(n.span.recordException(s),n.span.addEvent("ocpp.validation_failure",{"error.message":s.message}));},onRateLimitExceeded(c){let a=d.get(c.identity);a&&a.span.addEvent("ocpp.rate_limit_exceeded");},onPongTimeout(c){let a=d.get(c.identity);a&&a.span.addEvent("ocpp.pong_timeout");},onBackpressure(c,a){let s=d.get(c.identity);s&&s.span.addEvent("ocpp.backpressure",{"ocpp.buffered_bytes":a});},onEviction(c,a){let s=d.get(c.identity);s&&s.span.addEvent("ocpp.evicted",{"net.peer.ip.new":a.handshake.remoteAddress});},onTelemetry(c){if(!u)return;let a=u.startSpan("ocpp.telemetry_push",{kind:0});a.setAttribute("ocpp.connected_clients",c.connectedClients),a.setAttribute("ocpp.active_sessions",c.activeSessions),a.setAttribute("ocpp.uptime_seconds",c.uptimeSeconds),a.setAttribute("ocpp.memory_rss",c.memoryUsage.rss),a.setAttribute("ocpp.memory_heap_used",c.memoryUsage.heapUsed),a.setAttribute("ocpp.pid",c.pid),c.webSockets&&(a.setAttribute("ocpp.ws_total",c.webSockets.total),a.setAttribute("ocpp.ws_buffered_amount",c.webSockets.bufferedAmount)),a.setStatus({code:1}),a.end();},onSecurityEvent(c){if(!u)return;let a=u.startSpan("ocpp.security_event",{kind:0});a.setAttribute("security.event_type",c.type),c.identity&&a.setAttribute("ocpp.identity",c.identity),c.ip&&a.setAttribute("net.peer.ip",c.ip),a.setStatus({code:2,message:c.type}),a.end();},onAuthFailed(c,a,s){if(!u)return;let n=u.startSpan("ocpp.auth_failed",{kind:1});n.setAttribute("ocpp.identity",c.identity),n.setAttribute("net.peer.ip",c.remoteAddress),n.setAttribute("ocpp.close_code",a),n.setAttribute("ocpp.close_reason",s),n.setStatus({code:2,message:"Auth failed"}),n.end();},onClosing(){for(let[,c]of d)c.span.addEvent("ocpp.server_closing");},onClose(){for(let[,c]of d)c.span.setStatus({code:2,message:"Server shutdown"}),c.span.end();d.clear();}}}function H(r={}){let u=new Set(r.sensitiveKeys??["idTag","authorizationKey","token","password","securityCode"]),d=r.replacement??"***REDACTED***",c=r.incoming??true,a=r.outgoing??true;function s(e){if(!e||typeof e!="object")return e;if(Array.isArray(e))return e.map(s);let t={};for(let[i,o]of Object.entries(e))u.has(i)?t[i]=d:o&&typeof o=="object"?t[i]=s(o):t[i]=o;return t}let n=async(e,t)=>{c&&(e.type==="incoming_call"&&e.params?e.params=s(e.params):e.type==="incoming_result"&&e.payload&&(e.payload=s(e.payload))),a&&(e.type==="outgoing_call"&&e.params?e.params=s(e.params):e.type==="outgoing_result"&&e.payload&&(e.payload=s(e.payload))),await t();};return {name:"pii-redactor",onConnection(e){e.use(n);}}}function W(r){let u=r.cooldownMs??6e4,d=r.threshold??1,c=r.windowMs??3e5,a=r.logger,s=new Map,n=new Map;function e(){return typeof r.sink=="string"?{async send(o){await fetch(r.sink,{method:"POST",headers:{"Content-Type":"application/json",...r.headers},body:JSON.stringify(o)});}}:r.sink}function t(o){let l=Date.now(),g=(s.get(o)??[]).filter(y=>l-y<c);return s.set(o,g),g}function i(o,l,m){let g=o??l??"unknown",y=Date.now(),p=t(g);if(p.push(y),p.length<d)return;let f=n.get(g)??0;if(y-f<u)return;n.set(g,y);let w=e(),P={eventType:m,identity:o,ip:l,timestamp:new Date().toISOString(),count:p.length,windowMs:c};Promise.resolve(w.send(P)).catch(_=>{a?.error?.("[rate-limit-notifier] Alert delivery failed:",_);});}return {name:"rate-limit-notifier",onRateLimitExceeded(o,l){i(o.identity,o.handshake.remoteAddress,"RATE_LIMIT_EXCEEDED");},onSecurityEvent(o){(o.type==="RATE_LIMIT_EXCEEDED"||o.type==="CONNECTION_RATE_LIMIT")&&i(o.identity,o.ip,o.type);},onClose(){s.clear(),n.clear();}}}function j(r){let u=r.mode??"pubsub",d=r.prefix??"ocpp",c=new Set(r.events??["connect","disconnect","message","security"]),a=r.maxStreamLength??1e4,s=r.serialize??JSON.stringify,n=new Map;function e(i){return `${d}:${i}`}function t(i,o){if(!c.has(i))return;let l=e(i),m=s(o),g=async()=>{u==="stream"&&r.client.xadd?await r.client.xadd(l,"MAXLEN","~",a,"*","data",m):await r.client.publish(l,m);};if(r.worker)r.worker.enqueue(`redis-${u}`,()=>g().catch(()=>{}));else try{g().catch?.(()=>{});}catch{}}return {name:"redis-pubsub",onConnection(i){n.set(i.identity,Date.now()),t("connect",{identity:i.identity,ip:i.handshake.remoteAddress,protocol:i.protocol,timestamp:new Date().toISOString()});},onDisconnect(i,o,l){let m=n.get(i.identity),g=m?Math.round((Date.now()-m)/1e3):0;n.delete(i.identity),t("disconnect",{identity:i.identity,code:o,reason:l,durationSec:g,timestamp:new Date().toISOString()});},onMessage(i,o){let l={identity:i.identity,direction:o.direction,messageType:o.message[0],timestamp:o.ctx.timestamp};o.message[0]===2&&o.message[2]&&(l.method=o.message[2]),o.ctx.latencyMs!==void 0&&(l.latencyMs=o.ctx.latencyMs),r.includePayload&&(l.payload=o.message),t(`message:${o.direction}`,l);},onSecurityEvent(i){t("security",{type:i.type,identity:i.identity,ip:i.ip,timestamp:i.timestamp,details:i.details});},onAuthFailed(i,o,l){t("auth_failed",{identity:i.identity,ip:i.remoteAddress,code:o,reason:l,timestamp:new Date().toISOString()});},onEviction(i,o){t("eviction",{identity:i.identity,evictedBy:o.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){t("closing",{timestamp:new Date().toISOString()});},onClose(){n.clear();try{r.client.quit?r.client.quit():r.client.disconnect&&r.client.disconnect();}catch{}}}}function Y(r){let u=r.redis,d=r.prefix??"ocpp:replay:",c=r.syntheticResponse??true,a=r.flushConcurrency??5,s=r.flushDelayMs??200,n=r.logger,e=new Set;function t(i){return new Promise(o=>setTimeout(o,i))}return {name:"replay-buffer",onConnection(i){let o=`${d}${i.identity}`,l=async(g,y)=>{if(g.type!=="outgoing_call")return y();try{return await y()}catch(p){let f=p instanceof Error?p.message:String(p);if(!(f.includes("WebSocket is not open")||f.includes("offline")||f.includes("CLOSED")||f.includes("CLOSING")))throw p;let P=JSON.stringify([2,g.messageId,g.method,g.params]);try{await u.rpush(o,P),n?.warn?.(`[replay-buffer] Queued offline command: ${g.method} for ${i.identity}`);}catch(_){throw n?.error?.(`[replay-buffer] Redis rpush failed for ${i.identity}:`,_),p}if(c)return {status:"Accepted",note:"Queued offline (ReplayBuffer)"};throw p}};i.use(l);let m=(async()=>{try{let g=0;for(;;){let y=await u.lpop(o);if(!y)break;let p;try{p=JSON.parse(y);}catch{n?.warn?.(`[replay-buffer] Skipping unparseable queued message for ${i.identity}`);continue}!Array.isArray(p)||p[0]!==2||(i.call(p[2],p[3]).catch(f=>{n?.warn?.(`[replay-buffer] Flush call failed for ${i.identity}/${p[2]}:`,f);}),g++,g>=a&&(await t(s),g=0));}}catch(g){n?.error?.(`[replay-buffer] Error flushing queue for ${i.identity}:`,g);}})();e.add(m),m.finally(()=>e.delete(m));},async onClosing(){e.size>0&&await Promise.allSettled([...e]);},onClose(){e.clear();}}}function K(r){let u=r.unmatchedBehavior??"passthrough",d=r.logger,c=new Map,a;for(let n of r.rules)n.method==="*"?a=n:c.set(n.method,n);function s(n){return c.get(n)??a}return {name:"schema-versioning",onConnection(n){if(r.applyWhen&&n.protocol!==r.applyWhen)return;let e=async(t,i)=>{let o=t.method,l=s(o);if(!l){if(u==="reject")throw d?.warn?.(`[schema-versioning] No transform rule for method "${o}", rejecting`),new Error(`Schema versioning: no transform rule for "${o}" (${r.sourceVersion} \u2192 ${r.targetVersion})`);return i()}if(t.type==="incoming_call")try{let m=l.transform(t.params,"up");t.params=m,d?.debug?.(`[schema-versioning] Transformed ${o} UP: ${r.sourceVersion} \u2192 ${r.targetVersion}`);}catch(m){d?.warn?.(`[schema-versioning] Transform UP failed for ${o}:`,m);}else if(t.type==="outgoing_call")try{let m=l.transform(t.params,"down");t.params=m,d?.debug?.(`[schema-versioning] Transformed ${o} DOWN: ${r.targetVersion} \u2192 ${r.sourceVersion}`);}catch(m){d?.warn?.(`[schema-versioning] Transform DOWN failed for ${o}:`,m);}else if(t.type==="outgoing_result")try{let m=l.transform(t.payload,"down");t.payload=m;}catch(m){d?.warn?.(`[schema-versioning] Transform DOWN (result) failed for ${o}:`,m);}return i()};n.use(e);}}}function V(r){let u=r?.logger??console,d=r?.logLevel??"standard",c=d==="standard"||d==="verbose",a=d==="verbose",s=new Map;return {name:"session-log",onConnection(n){s.set(n.identity,Date.now()),u.info("[session] connected",{identity:n.identity,ip:n.handshake.remoteAddress,protocol:n.protocol});},onDisconnect(n,e,t){let i=s.get(n.identity),o=i?Math.round((Date.now()-i)/1e3):0;s.delete(n.identity),u.info("[session] disconnected",{identity:n.identity,code:e,reason:t,durationSec:o});},onError(n,e){c&&(u.error??u.warn)("[session] error",{identity:n.identity,error:e.message});},onAuthFailed(n,e,t){c&&u.warn("[session] auth failed",{identity:n.identity,ip:n.remoteAddress,code:e,reason:t});},onEviction(n,e){c&&u.warn("[session] evicted",{identity:n.identity,evictedIp:n.handshake.remoteAddress,newIp:e.handshake.remoteAddress});},onBadMessage(n,e){a&&u.warn("[session] bad message",{identity:n.identity,raw:typeof e=="string"?e.slice(0,200):"<buffer>"});},onSecurityEvent(n){a&&u.warn("[session] security event",{type:n.type,identity:n.identity,ip:n.ip,details:n.details});},onHandlerError(n,e,t){a&&(u.error??u.warn)("[session] handler error",{identity:n.identity,method:e,error:t.message});},onValidationFailure(n,e,t){a&&u.warn("[session] validation failure",{identity:n.identity,error:t.message});},onRateLimitExceeded(n){c&&u.warn("[session] rate limit exceeded",{identity:n.identity,ip:n.handshake.remoteAddress});},onPongTimeout(n){a&&u.warn("[session] pong timeout (dead peer)",{identity:n.identity});},onBackpressure(n,e){a&&u.warn("[session] backpressure",{identity:n.identity,bufferedBytes:e});},onClose(){s.clear();}}}function U(r){let u=new Set(r.events??["init","connect","disconnect","close"]),d=r.timeout??5e3,c=r.retries??1;async function a(s){if(!u.has(s.event))return;let n=JSON.stringify(s),e={"Content-Type":"application/json",...r.headers};if(r.secret){let t=crypto.createHmac("sha256",r.secret).update(n).digest("hex");e["X-Signature"]=t;}for(let t=0;t<=c;t++)try{let i=new AbortController,o=setTimeout(()=>i.abort(),d);await fetch(r.url,{method:"POST",headers:e,body:n,signal:i.signal}),clearTimeout(o);return}catch{}}return {name:"webhook",onInit(){a({event:"init",timestamp:new Date().toISOString()}).catch(()=>{});},onConnection(s){a({event:"connect",timestamp:new Date().toISOString(),data:{identity:s.identity,ip:s.handshake.remoteAddress,protocol:s.protocol}}).catch(()=>{});},onDisconnect(s,n,e){a({event:"disconnect",timestamp:new Date().toISOString(),data:{identity:s.identity,code:n,reason:e}}).catch(()=>{});},onSecurityEvent(s){a({event:"security",timestamp:s.timestamp,data:{type:s.type,identity:s.identity,ip:s.ip,details:s.details}}).catch(()=>{});},onAuthFailed(s,n,e){a({event:"auth_failed",timestamp:new Date().toISOString(),data:{identity:s.identity,ip:s.remoteAddress,code:n,reason:e}}).catch(()=>{});},onEviction(s,n){a({event:"eviction",timestamp:new Date().toISOString(),data:{identity:s.identity,evictedIp:s.handshake.remoteAddress,newIp:n.handshake.remoteAddress}}).catch(()=>{});},onClosing(){a({event:"closing",timestamp:new Date().toISOString()}).catch(()=>{});},onClose(){}}}exports.amqpPlugin=T;exports.anomalyPlugin=R;exports.asyncWorkerPlugin=x;exports.circuitBreakerPlugin=L;exports.connectionGuardPlugin=D;exports.heartbeatPlugin=$;exports.kafkaPlugin=I;exports.messageDedupPlugin=N;exports.metricsPlugin=q;exports.mqttPlugin=B;exports.otelPlugin=F;exports.piiRedactorPlugin=H;exports.rateLimitNotifierPlugin=W;exports.redisPubSubPlugin=j;exports.replayBufferPlugin=Y;exports.schemaVersioningPlugin=K;exports.sessionLogPlugin=V;exports.webhookPlugin=U;
1
+ 'use strict';var crypto=require('crypto');function x(n){let d=n.exchange??"ocpp.events",l=n.routingKey??"ocpp.{event}.{identity}",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;
package/dist/plugins.mjs CHANGED
@@ -1 +1 @@
1
- import {createHmac}from'crypto';function R(r){let u=r.exchange??"ocpp.events",d=r.routingKey??"ocpp.{event}.{identity}",c=new Set(r.events??["connect","disconnect","message","security"]),a={persistent:r.publishOptions?.persistent??true,contentType:r.publishOptions?.contentType??"application/json",...r.publishOptions?.priority!==void 0&&{priority:r.publishOptions.priority}},s=new Map;function n(t,i){return d.replace("{event}",t).replace("{identity}",i??"server")}function e(t,i,o){if(!c.has(t))return;let l=n(t,i),m=Buffer.from(JSON.stringify(o));if(r.worker)r.worker.enqueue("amqp-publish",async()=>{r.channel.publish(u,l,m,a);});else try{r.channel.publish(u,l,m,a);}catch{}}return {name:"amqp",onConnection(t){s.set(t.identity,Date.now()),e("connect",t.identity,{identity:t.identity,ip:t.handshake.remoteAddress,protocol:t.protocol,timestamp:new Date().toISOString()});},onDisconnect(t,i,o){let l=s.get(t.identity),m=l?Math.round((Date.now()-l)/1e3):0;s.delete(t.identity),e("disconnect",t.identity,{identity:t.identity,code:i,reason:o,durationSec:m,timestamp:new Date().toISOString()});},onMessage(t,i){let o={identity:t.identity,direction:i.direction,messageType:i.message[0],timestamp:i.ctx.timestamp};i.message[0]===2&&i.message[2]&&(o.method=i.message[2]),i.ctx.latencyMs!==void 0&&(o.latencyMs=i.ctx.latencyMs),r.includePayload&&(o.payload=i.message),e(`message.${i.direction}`,t.identity,o);},onSecurityEvent(t){e("security",t.identity,{type:t.type,identity:t.identity,ip:t.ip,timestamp:t.timestamp,details:t.details});},onAuthFailed(t,i,o){e("auth_failed",t.identity,{identity:t.identity,ip:t.remoteAddress,code:i,reason:o,timestamp:new Date().toISOString()});},onEviction(t,i){e("eviction",t.identity,{identity:t.identity,evictedBy:i.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){e("closing",void 0,{timestamp:new Date().toISOString()});},onClose(){s.clear();try{r.channel.close();}catch{}}}}function x(r){let u=r?.reconnectThreshold??5,d=r?.authFailureThreshold??5,c=r?.badMessageThreshold??10,a=r?.evictionThreshold??3,s=r?.windowMs??6e4,n=new Map,e=new Map,t=new Map,i=new Map,o=null,l=null;function m(p,f){let _=f-s,w=0;for(;w<p.length&&p[w]<_;)w++;return w>0?p.slice(w):p}function g(p,f){for(let[_,w]of p){let b=m(w,f);b.length===0?p.delete(_):p.set(_,b);}}function y(p,f,_,w,b){let v=Date.now(),k=p.get(f)??[];if(k=m(k,v),k.push(v),p.set(f,k),k.length>_&&o){let S={type:w,identity:b.identity,ip:b.ip??b.evictedIp,timestamp:new Date().toISOString(),details:{...b,countInWindow:k.length,threshold:_,windowMs:s}};o.emit("securityEvent",S);}}return {name:"anomaly",onInit(p){o=p,l=setInterval(()=>{let f=Date.now();g(n,f),g(e,f),g(t,f),g(i,f);},s).unref();},onConnection(p){y(n,p.identity,u,"ANOMALY_RAPID_RECONNECT",{identity:p.identity,ip:p.handshake.remoteAddress});},onAuthFailed(p,f,_){y(e,p.remoteAddress,d,"ANOMALY_AUTH_BRUTE_FORCE",{ip:p.remoteAddress,identity:p.identity,code:f,reason:_});},onBadMessage(p){y(t,p.identity,c,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress});},onValidationFailure(p){y(t,p.identity,c,"ANOMALY_MESSAGE_FUZZING",{identity:p.identity,ip:p.handshake.remoteAddress,source:"validation_failure"});},onEviction(p,f){y(i,p.identity,a,"ANOMALY_IDENTITY_COLLISION",{identity:p.identity,evictedIp:p.handshake.remoteAddress,newIp:f.handshake.remoteAddress});},onClose(){l&&(clearInterval(l),l=null),n.clear(),e.clear(),t.clear(),i.clear(),o=null;}}}function L(r){let u=r?.concurrency??10,d=r?.maxQueueSize??1e3,c=r?.overflowStrategy??"drop-oldest",a=r?.drainTimeoutMs??5e3,s=[],n=0,e=0,t=true,i=null;function o(){for(;n<u&&s.length>0;){let g=s.shift();n++,g.fn().catch(y=>{if(r?.onError)try{r.onError(y instanceof Error?y:new Error(String(y)),g.name);}catch{}}).finally(()=>{n--,!t&&n===0&&s.length===0&&i&&(i(),i=null),o();});}}function l(g,y){if(!t)return false;if(s.length>=d){if(c==="drop-newest")return e++,r?.logger?.warn?.(`[async-worker] Queue full (${d}), dropping task: ${g}`),false;let p=s.shift();e++,r?.logger?.warn?.(`[async-worker] Queue full (${d}), dropping oldest task: ${p?.name??"unknown"}`);}return s.push({name:g,fn:y}),o(),true}return {name:"async-worker",enqueue:l,queueSize:()=>s.length,activeCount:()=>n,droppedCount:()=>e,getCustomMetrics(){return ["# HELP ocpp_async_worker_queue_size Current tasks waiting in the background queue","# TYPE ocpp_async_worker_queue_size gauge",`ocpp_async_worker_queue_size ${s.length}`,"# HELP ocpp_async_worker_active_tasks Currently executing background tasks","# TYPE ocpp_async_worker_active_tasks gauge",`ocpp_async_worker_active_tasks ${n}`,"# HELP ocpp_async_worker_dropped_total Tasks dropped due to queue overflow","# TYPE ocpp_async_worker_dropped_total counter",`ocpp_async_worker_dropped_total ${e}`]},onClosing(){return t=false,n===0&&s.length===0?Promise.resolve():new Promise(g=>{i=g;let y=setTimeout(()=>{r?.logger?.warn?.(`[async-worker] Drain timeout (${a}ms), ${n} tasks still active, ${s.length} queued`),s.length=0,i=null,g();},a);y&&typeof y=="object"&&"unref"in y&&y.unref();})},onClose(){t=false,s.length=0,i=null;}}}function D(r){let u=r?.failureThreshold??5,d=r?.resetTimeoutMs??3e4,c=r?.maxConcurrent??20,a=r?.logger,s=r?.onStateChange,n=new Map;function e(i){let o=n.get(i);return o||(o={state:"CLOSED",failures:0,lastFailure:0,concurrentCalls:0},n.set(i,o)),o}function t(i,o){let l=e(i),m=l.state;m!==o&&(l.state=o,a?.warn?.(`[circuit-breaker] ${i}: ${m} \u2192 ${o}`),s?.(i,m,o));}return {name:"circuit-breaker",onConnection(i){let o=e(i.identity);i.use(async(l,m)=>{if(l.type!=="outgoing_call")return m();if(o.concurrentCalls>=c)throw a?.warn?.(`[circuit-breaker] ${i.identity}: concurrent limit (${c}) reached, rejecting ${l.method}`),new Error(`Circuit breaker: concurrent call limit exceeded for ${i.identity}`);let g=Date.now();if(o.state==="OPEN")if(g-o.lastFailure>=d)t(i.identity,"HALF_OPEN");else throw new Error(`Circuit breaker OPEN for ${i.identity}: ${o.failures} consecutive failures`);o.concurrentCalls++;try{let y=await m();return o.concurrentCalls--,o.state==="HALF_OPEN"?(t(i.identity,"CLOSED"),o.failures=0):o.failures=Math.max(0,o.failures-1),y}catch(y){throw o.concurrentCalls--,o.failures++,o.lastFailure=Date.now(),(o.state==="HALF_OPEN"||o.failures>=u)&&t(i.identity,"OPEN"),y}});},onDisconnect(i){let o=n.get(i.identity);o&&(o.concurrentCalls=0);},onClose(){n.clear();}}}function $(r){let u=r.maxConnections,d=r.closeCode??4029,c=r.closeReason??"Connection limit reached",a=r.forceCloseOnPongTimeout??true,s=r.forceCloseOnBackpressure??false,n=0;return {name:"connection-guard",onConnection(e){n++,n>u&&(r.logger?.warn?.(`[connection-guard] Limit exceeded (${n}/${u}), closing: ${e.identity}`),e.close({code:d,reason:c}));},onDisconnect(){n=Math.max(0,n-1);},onPongTimeout(e){a&&(r.logger?.warn?.(`[connection-guard] Pong timeout \u2014 closing dead peer: ${e.identity}`),e.close({code:4e3,reason:"Pong timeout"}));},onBackpressure(e,t){s&&(r.logger?.warn?.(`[connection-guard] Backpressure (${t} bytes) \u2014 closing slow client: ${e.identity}`),e.close({code:4001,reason:"Backpressure exceeded"}));},onClose(){n=0;}}}function I(){return {name:"heartbeat",onConnection(r){r.handle("Heartbeat",()=>({currentTime:new Date().toISOString()}));}}}function N(r){let u=r.topic??"ocpp.events",d=r.topicRouting??false,c=new Set(r.events??["connect","disconnect","message","security"]),a=new Map;function s(e){return d?`${u}.${e}`:u}function n(e,t,i){if(!c.has(e.split(".")[0]))return;let o=s(e.split(".")[0]),l=JSON.stringify(i),m=t??"server";r.worker?r.worker.enqueue("kafka-publish",async()=>{await r.producer.send({topic:o,messages:[{key:m,value:l,headers:{event:e}}]});}):r.producer.send({topic:o,messages:[{key:m,value:l,headers:{event:e}}]}).catch(()=>{});}return {name:"kafka",onConnection(e){a.set(e.identity,Date.now()),n("connect",e.identity,{identity:e.identity,ip:e.handshake.remoteAddress,protocol:e.protocol,timestamp:new Date().toISOString()});},onDisconnect(e,t,i){let o=a.get(e.identity),l=o?Math.round((Date.now()-o)/1e3):0;a.delete(e.identity),n("disconnect",e.identity,{identity:e.identity,code:t,reason:i,durationSec:l,timestamp:new Date().toISOString()});},onMessage(e,t){let i={identity:e.identity,direction:t.direction,messageType:t.message[0],timestamp:t.ctx.timestamp};t.message[0]===2&&t.message[2]&&(i.method=t.message[2]),t.ctx.latencyMs!==void 0&&(i.latencyMs=t.ctx.latencyMs),r.includePayload&&(i.payload=t.message),n(`message.${t.direction}`,e.identity,i);},onSecurityEvent(e){n("security",e.identity,{type:e.type,identity:e.identity,ip:e.ip,timestamp:e.timestamp,details:e.details});},onAuthFailed(e,t,i){n("auth_failed",e.identity,{identity:e.identity,ip:e.remoteAddress,code:t,reason:i,timestamp:new Date().toISOString()});},onEviction(e,t){n("eviction",e.identity,{identity:e.identity,evictedBy:t.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){n("closing",void 0,{timestamp:new Date().toISOString()});},onClose(){a.clear();}}}function q(r){let u=r.redis,d=r.ttlMs??3e5,c=r.prefix??"ocpp:dedup:",a=r.redisStyle??"positional",s=r.logger;async function n(e){return a==="options"?await u.set(e,"1",{PX:d,NX:true})!==null:await u.set(e,"1","PX",d,"NX")==="OK"}return {name:"message-dedup",async onBeforeReceive(e,t){let i;try{let l=typeof t=="string"?t:t?.toString()||"",m=JSON.parse(l);Array.isArray(m)&&m.length>1&&(i=String(m[1]));}catch{return}if(!i)return;let o=`${c}${e.identity}:${i}`;try{if(!await n(o))return s?.warn?.(`[message-dedup] Dropping duplicate message: ${o}`),!1}catch(l){s?.error?.("[message-dedup] Redis failure, falling through:",l);}}}}function B(r){let u=r?.intervalMs??3e4,d=0,c=0,a=0,s=0,n=0,e=Date.now(),t=null,i=0,o=0,l=0,m=0,g=0,y=0,p=0,f=0,_=0,w=0,b=0,v=0,k=0,S=0,A=0,C=new Map;function M(){return {totalConnections:d,totalDisconnections:c,activeConnections:a,peakConnections:s,connectionDurationAvgMs:c>0?Math.round(n/c):0,uptimeMs:Date.now()-e,timestamp:new Date().toISOString(),totalMessagesIn:i,totalMessagesOut:o,totalCalls:l,totalCallResults:m,totalCallErrors:g,totalErrors:y,totalBadMessages:p,totalHandlerErrors:f,totalRateLimitHits:_,totalAuthFailures:w,totalEvictions:b,totalBackpressureEvents:v,totalPongTimeouts:k,totalValidationFailures:S,totalSecurityEvents:A}}return {name:"metrics",getMetrics:M,onInit(){e=Date.now(),u>0&&r?.onSnapshot&&(t=setInterval(()=>{r.onSnapshot(M());},u),t&&typeof t=="object"&&"unref"in t&&t.unref());},onConnection(E){d++,a++,a>s&&(s=a),C.set(E.identity,Date.now());},onDisconnect(E){c++,a=Math.max(0,a-1);let O=C.get(E.identity);O&&(n+=Date.now()-O,C.delete(E.identity));},onMessage(E,O){O.direction==="IN"?i++:o++;let T=O.message[0];T===2?l++:T===3?m++:T===4&&g++;},onError(){y++;},onBadMessage(){p++;},onHandlerError(){f++;},onRateLimitExceeded(){_++;},onAuthFailed(){w++;},onEviction(){b++;},onBackpressure(){v++;},onPongTimeout(){k++;},onValidationFailure(){S++;},onSecurityEvent(){A++;},getCustomMetrics(){return ["# HELP ocpp_connections_total Total connections since server start","# TYPE ocpp_connections_total counter",`ocpp_connections_total ${d}`,"# HELP ocpp_disconnections_total Total disconnections since server start","# TYPE ocpp_disconnections_total counter",`ocpp_disconnections_total ${c}`,"# HELP ocpp_connections_active Currently active connections","# TYPE ocpp_connections_active gauge",`ocpp_connections_active ${a}`,"# HELP ocpp_connections_peak Highest concurrent connections","# TYPE ocpp_connections_peak gauge",`ocpp_connections_peak ${s}`,"# HELP ocpp_connection_duration_avg_ms Average connection duration","# TYPE ocpp_connection_duration_avg_ms gauge",`ocpp_connection_duration_avg_ms ${M().connectionDurationAvgMs}`,"# HELP ocpp_messages_in_total Total inbound messages","# TYPE ocpp_messages_in_total counter",`ocpp_messages_in_total ${i}`,"# HELP ocpp_messages_out_total Total outbound messages","# TYPE ocpp_messages_out_total counter",`ocpp_messages_out_total ${o}`,"# HELP ocpp_calls_total Total CALL messages","# TYPE ocpp_calls_total counter",`ocpp_calls_total ${l}`,"# HELP ocpp_call_results_total Total CALLRESULT messages","# TYPE ocpp_call_results_total counter",`ocpp_call_results_total ${m}`,"# HELP ocpp_call_errors_total Total CALLERROR messages","# TYPE ocpp_call_errors_total counter",`ocpp_call_errors_total ${g}`,"# HELP ocpp_errors_total WebSocket/protocol errors","# TYPE ocpp_errors_total counter",`ocpp_errors_total ${y}`,"# HELP ocpp_bad_messages_total Malformed messages received","# TYPE ocpp_bad_messages_total counter",`ocpp_bad_messages_total ${p}`,"# HELP ocpp_handler_errors_total User handler errors","# TYPE ocpp_handler_errors_total counter",`ocpp_handler_errors_total ${f}`,"# HELP ocpp_rate_limit_hits_total Rate limit violations","# TYPE ocpp_rate_limit_hits_total counter",`ocpp_rate_limit_hits_total ${_}`,"# HELP ocpp_auth_failures_total Authentication failures","# TYPE ocpp_auth_failures_total counter",`ocpp_auth_failures_total ${w}`,"# HELP ocpp_evictions_total Client evictions","# TYPE ocpp_evictions_total counter",`ocpp_evictions_total ${b}`,"# HELP ocpp_backpressure_events_total Slow client backpressure events","# TYPE ocpp_backpressure_events_total counter",`ocpp_backpressure_events_total ${v}`,"# HELP ocpp_pong_timeouts_total Dead peer timeouts","# TYPE ocpp_pong_timeouts_total counter",`ocpp_pong_timeouts_total ${k}`,"# HELP ocpp_validation_failures_total Schema validation failures","# TYPE ocpp_validation_failures_total counter",`ocpp_validation_failures_total ${S}`,"# HELP ocpp_security_events_total Security events from anomaly detection","# TYPE ocpp_security_events_total counter",`ocpp_security_events_total ${A}`]},onClose(){t&&(clearInterval(t),t=null),C.clear();}}}function F(r){let u=r.topicPrefix??"ocpp",d=new Set(r.events??["connect","disconnect","message","security"]),c=r.qos??0,a=new Map;function s(e,t){return r.topicBuilder?r.topicBuilder(e,t):t?`${u}/${t}/${e}`:`${u}/${e}`}function n(e,t){let i=r.transform?r.transform(t):t,o=JSON.stringify(i);r.worker?r.worker.enqueue("mqtt-publish",()=>new Promise((l,m)=>{r.client.publish(e,o,{qos:c},g=>g?m(g):l());})):r.client.publish(e,o,{qos:c});}return {name:"mqtt",onConnection(e){a.set(e.identity,Date.now()),d.has("connect")&&n(s("connect",e.identity),{identity:e.identity,ip:e.handshake.remoteAddress,protocol:e.protocol,timestamp:new Date().toISOString()});},onDisconnect(e,t,i){if(!d.has("disconnect")){a.delete(e.identity);return}let o=a.get(e.identity),l=o?Math.round((Date.now()-o)/1e3):0;a.delete(e.identity),n(s("disconnect",e.identity),{identity:e.identity,code:t,reason:i,durationSec:l,timestamp:new Date().toISOString()});},onMessage(e,t){if(!d.has("message"))return;let i={identity:e.identity,direction:t.direction,messageType:t.message[0],timestamp:t.ctx.timestamp};t.message[0]===2&&t.message[2]&&(i.method=t.message[2]),t.ctx.latencyMs!==void 0&&(i.latencyMs=t.ctx.latencyMs),r.includePayload&&(i.payload=t.message),n(s(`message/${t.direction}`,e.identity),i);},onSecurityEvent(e){d.has("security")&&n(s("security",e.identity),{type:e.type,identity:e.identity,ip:e.ip,timestamp:e.timestamp,details:e.details});},onError(e,t){d.has("error")&&n(s("error",e.identity),{identity:e.identity,error:t.message,timestamp:new Date().toISOString()});},onAuthFailed(e,t,i){d.has("auth_failed")&&n(s("auth_failed"),{identity:e.identity,ip:e.remoteAddress,code:t,reason:i,timestamp:new Date().toISOString()});},onEviction(e,t){d.has("eviction")&&n(s("eviction",e.identity),{identity:e.identity,evictedBy:t.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){n(s("closing"),{timestamp:new Date().toISOString()});},onClose(){a.clear();try{r.client.end(!1);}catch{}}}}function H(r){let u=r?.tracer??null,d=new Map;return {name:"otel",async onInit(c){if(!u)try{u=(await import('@opentelemetry/api')).trace.getTracer(r?.serviceName??"ocpp-server","1.0.0");}catch{c.log.warn?.("otelPlugin: @opentelemetry/api not found \u2014 plugin disabled. Install it as a peer dependency."),u=null;}},onConnection(c){if(!u)return;let a=u.startSpan("ocpp.connection",{kind:1});a.setAttribute("ocpp.identity",c.identity),a.setAttribute("ocpp.protocol",c.protocol??"unknown"),a.setAttribute("net.peer.ip",c.handshake.remoteAddress),d.set(c.identity,{span:a,startTime:Date.now()});},onDisconnect(c,a){let s=d.get(c.identity);if(!s)return;let n=Date.now()-s.startTime;s.span.setAttribute("ocpp.close_code",a),s.span.setAttribute("ocpp.duration_ms",n),s.span.setStatus({code:1}),s.span.end(),d.delete(c.identity);},onMessage(c,a){if(!u)return;let s=a.message[0];if(s!==2){let t=d.get(c.identity);t&&t.span.addEvent(s===3?"ocpp.call_result":"ocpp.call_error",{direction:a.direction,"ocpp.message_id":String(a.message[1]),...a.ctx.latencyMs!==void 0&&{"ocpp.latency_ms":a.ctx.latencyMs}});return}let n=String(a.message[2]??"unknown"),e=u.startSpan(`ocpp.call.${n}`,{kind:a.direction==="IN"?1:2});e.setAttribute("ocpp.identity",c.identity),e.setAttribute("ocpp.method",n),e.setAttribute("ocpp.direction",a.direction),e.setAttribute("ocpp.message_id",String(a.message[1])),a.ctx.latencyMs!==void 0&&e.setAttribute("ocpp.latency_ms",a.ctx.latencyMs),e.setStatus({code:1}),e.end();},onError(c,a){let s=d.get(c.identity);s&&(s.span.recordException(a),s.span.addEvent("ocpp.error",{"error.message":a.message}));},onHandlerError(c,a,s){let n=d.get(c.identity);n&&(n.span.recordException(s),n.span.addEvent("ocpp.handler_error",{"ocpp.method":a,"error.message":s.message}));},onBadMessage(c,a,s){let n=d.get(c.identity);n&&(n.span.recordException(s),n.span.addEvent("ocpp.bad_message",{"raw.preview":typeof a=="string"?a.slice(0,200):"<buffer>","error.message":s.message}));},onValidationFailure(c,a,s){let n=d.get(c.identity);n&&(n.span.recordException(s),n.span.addEvent("ocpp.validation_failure",{"error.message":s.message}));},onRateLimitExceeded(c){let a=d.get(c.identity);a&&a.span.addEvent("ocpp.rate_limit_exceeded");},onPongTimeout(c){let a=d.get(c.identity);a&&a.span.addEvent("ocpp.pong_timeout");},onBackpressure(c,a){let s=d.get(c.identity);s&&s.span.addEvent("ocpp.backpressure",{"ocpp.buffered_bytes":a});},onEviction(c,a){let s=d.get(c.identity);s&&s.span.addEvent("ocpp.evicted",{"net.peer.ip.new":a.handshake.remoteAddress});},onTelemetry(c){if(!u)return;let a=u.startSpan("ocpp.telemetry_push",{kind:0});a.setAttribute("ocpp.connected_clients",c.connectedClients),a.setAttribute("ocpp.active_sessions",c.activeSessions),a.setAttribute("ocpp.uptime_seconds",c.uptimeSeconds),a.setAttribute("ocpp.memory_rss",c.memoryUsage.rss),a.setAttribute("ocpp.memory_heap_used",c.memoryUsage.heapUsed),a.setAttribute("ocpp.pid",c.pid),c.webSockets&&(a.setAttribute("ocpp.ws_total",c.webSockets.total),a.setAttribute("ocpp.ws_buffered_amount",c.webSockets.bufferedAmount)),a.setStatus({code:1}),a.end();},onSecurityEvent(c){if(!u)return;let a=u.startSpan("ocpp.security_event",{kind:0});a.setAttribute("security.event_type",c.type),c.identity&&a.setAttribute("ocpp.identity",c.identity),c.ip&&a.setAttribute("net.peer.ip",c.ip),a.setStatus({code:2,message:c.type}),a.end();},onAuthFailed(c,a,s){if(!u)return;let n=u.startSpan("ocpp.auth_failed",{kind:1});n.setAttribute("ocpp.identity",c.identity),n.setAttribute("net.peer.ip",c.remoteAddress),n.setAttribute("ocpp.close_code",a),n.setAttribute("ocpp.close_reason",s),n.setStatus({code:2,message:"Auth failed"}),n.end();},onClosing(){for(let[,c]of d)c.span.addEvent("ocpp.server_closing");},onClose(){for(let[,c]of d)c.span.setStatus({code:2,message:"Server shutdown"}),c.span.end();d.clear();}}}function W(r={}){let u=new Set(r.sensitiveKeys??["idTag","authorizationKey","token","password","securityCode"]),d=r.replacement??"***REDACTED***",c=r.incoming??true,a=r.outgoing??true;function s(e){if(!e||typeof e!="object")return e;if(Array.isArray(e))return e.map(s);let t={};for(let[i,o]of Object.entries(e))u.has(i)?t[i]=d:o&&typeof o=="object"?t[i]=s(o):t[i]=o;return t}let n=async(e,t)=>{c&&(e.type==="incoming_call"&&e.params?e.params=s(e.params):e.type==="incoming_result"&&e.payload&&(e.payload=s(e.payload))),a&&(e.type==="outgoing_call"&&e.params?e.params=s(e.params):e.type==="outgoing_result"&&e.payload&&(e.payload=s(e.payload))),await t();};return {name:"pii-redactor",onConnection(e){e.use(n);}}}function j(r){let u=r.cooldownMs??6e4,d=r.threshold??1,c=r.windowMs??3e5,a=r.logger,s=new Map,n=new Map;function e(){return typeof r.sink=="string"?{async send(o){await fetch(r.sink,{method:"POST",headers:{"Content-Type":"application/json",...r.headers},body:JSON.stringify(o)});}}:r.sink}function t(o){let l=Date.now(),g=(s.get(o)??[]).filter(y=>l-y<c);return s.set(o,g),g}function i(o,l,m){let g=o??l??"unknown",y=Date.now(),p=t(g);if(p.push(y),p.length<d)return;let f=n.get(g)??0;if(y-f<u)return;n.set(g,y);let _=e(),w={eventType:m,identity:o,ip:l,timestamp:new Date().toISOString(),count:p.length,windowMs:c};Promise.resolve(_.send(w)).catch(b=>{a?.error?.("[rate-limit-notifier] Alert delivery failed:",b);});}return {name:"rate-limit-notifier",onRateLimitExceeded(o,l){i(o.identity,o.handshake.remoteAddress,"RATE_LIMIT_EXCEEDED");},onSecurityEvent(o){(o.type==="RATE_LIMIT_EXCEEDED"||o.type==="CONNECTION_RATE_LIMIT")&&i(o.identity,o.ip,o.type);},onClose(){s.clear(),n.clear();}}}function Y(r){let u=r.mode??"pubsub",d=r.prefix??"ocpp",c=new Set(r.events??["connect","disconnect","message","security"]),a=r.maxStreamLength??1e4,s=r.serialize??JSON.stringify,n=new Map;function e(i){return `${d}:${i}`}function t(i,o){if(!c.has(i))return;let l=e(i),m=s(o),g=async()=>{u==="stream"&&r.client.xadd?await r.client.xadd(l,"MAXLEN","~",a,"*","data",m):await r.client.publish(l,m);};if(r.worker)r.worker.enqueue(`redis-${u}`,()=>g().catch(()=>{}));else try{g().catch?.(()=>{});}catch{}}return {name:"redis-pubsub",onConnection(i){n.set(i.identity,Date.now()),t("connect",{identity:i.identity,ip:i.handshake.remoteAddress,protocol:i.protocol,timestamp:new Date().toISOString()});},onDisconnect(i,o,l){let m=n.get(i.identity),g=m?Math.round((Date.now()-m)/1e3):0;n.delete(i.identity),t("disconnect",{identity:i.identity,code:o,reason:l,durationSec:g,timestamp:new Date().toISOString()});},onMessage(i,o){let l={identity:i.identity,direction:o.direction,messageType:o.message[0],timestamp:o.ctx.timestamp};o.message[0]===2&&o.message[2]&&(l.method=o.message[2]),o.ctx.latencyMs!==void 0&&(l.latencyMs=o.ctx.latencyMs),r.includePayload&&(l.payload=o.message),t(`message:${o.direction}`,l);},onSecurityEvent(i){t("security",{type:i.type,identity:i.identity,ip:i.ip,timestamp:i.timestamp,details:i.details});},onAuthFailed(i,o,l){t("auth_failed",{identity:i.identity,ip:i.remoteAddress,code:o,reason:l,timestamp:new Date().toISOString()});},onEviction(i,o){t("eviction",{identity:i.identity,evictedBy:o.handshake.remoteAddress,timestamp:new Date().toISOString()});},onClosing(){t("closing",{timestamp:new Date().toISOString()});},onClose(){n.clear();try{r.client.quit?r.client.quit():r.client.disconnect&&r.client.disconnect();}catch{}}}}function K(r){let u=r.redis,d=r.prefix??"ocpp:replay:",c=r.syntheticResponse??true,a=r.flushConcurrency??5,s=r.flushDelayMs??200,n=r.logger,e=new Set;function t(i){return new Promise(o=>setTimeout(o,i))}return {name:"replay-buffer",onConnection(i){let o=`${d}${i.identity}`,l=async(g,y)=>{if(g.type!=="outgoing_call")return y();try{return await y()}catch(p){let f=p instanceof Error?p.message:String(p);if(!(f.includes("WebSocket is not open")||f.includes("offline")||f.includes("CLOSED")||f.includes("CLOSING")))throw p;let w=JSON.stringify([2,g.messageId,g.method,g.params]);try{await u.rpush(o,w),n?.warn?.(`[replay-buffer] Queued offline command: ${g.method} for ${i.identity}`);}catch(b){throw n?.error?.(`[replay-buffer] Redis rpush failed for ${i.identity}:`,b),p}if(c)return {status:"Accepted",note:"Queued offline (ReplayBuffer)"};throw p}};i.use(l);let m=(async()=>{try{let g=0;for(;;){let y=await u.lpop(o);if(!y)break;let p;try{p=JSON.parse(y);}catch{n?.warn?.(`[replay-buffer] Skipping unparseable queued message for ${i.identity}`);continue}!Array.isArray(p)||p[0]!==2||(i.call(p[2],p[3]).catch(f=>{n?.warn?.(`[replay-buffer] Flush call failed for ${i.identity}/${p[2]}:`,f);}),g++,g>=a&&(await t(s),g=0));}}catch(g){n?.error?.(`[replay-buffer] Error flushing queue for ${i.identity}:`,g);}})();e.add(m),m.finally(()=>e.delete(m));},async onClosing(){e.size>0&&await Promise.allSettled([...e]);},onClose(){e.clear();}}}function V(r){let u=r.unmatchedBehavior??"passthrough",d=r.logger,c=new Map,a;for(let n of r.rules)n.method==="*"?a=n:c.set(n.method,n);function s(n){return c.get(n)??a}return {name:"schema-versioning",onConnection(n){if(r.applyWhen&&n.protocol!==r.applyWhen)return;let e=async(t,i)=>{let o=t.method,l=s(o);if(!l){if(u==="reject")throw d?.warn?.(`[schema-versioning] No transform rule for method "${o}", rejecting`),new Error(`Schema versioning: no transform rule for "${o}" (${r.sourceVersion} \u2192 ${r.targetVersion})`);return i()}if(t.type==="incoming_call")try{let m=l.transform(t.params,"up");t.params=m,d?.debug?.(`[schema-versioning] Transformed ${o} UP: ${r.sourceVersion} \u2192 ${r.targetVersion}`);}catch(m){d?.warn?.(`[schema-versioning] Transform UP failed for ${o}:`,m);}else if(t.type==="outgoing_call")try{let m=l.transform(t.params,"down");t.params=m,d?.debug?.(`[schema-versioning] Transformed ${o} DOWN: ${r.targetVersion} \u2192 ${r.sourceVersion}`);}catch(m){d?.warn?.(`[schema-versioning] Transform DOWN failed for ${o}:`,m);}else if(t.type==="outgoing_result")try{let m=l.transform(t.payload,"down");t.payload=m;}catch(m){d?.warn?.(`[schema-versioning] Transform DOWN (result) failed for ${o}:`,m);}return i()};n.use(e);}}}function z(r){let u=r?.logger??console,d=r?.logLevel??"standard",c=d==="standard"||d==="verbose",a=d==="verbose",s=new Map;return {name:"session-log",onConnection(n){s.set(n.identity,Date.now()),u.info("[session] connected",{identity:n.identity,ip:n.handshake.remoteAddress,protocol:n.protocol});},onDisconnect(n,e,t){let i=s.get(n.identity),o=i?Math.round((Date.now()-i)/1e3):0;s.delete(n.identity),u.info("[session] disconnected",{identity:n.identity,code:e,reason:t,durationSec:o});},onError(n,e){c&&(u.error??u.warn)("[session] error",{identity:n.identity,error:e.message});},onAuthFailed(n,e,t){c&&u.warn("[session] auth failed",{identity:n.identity,ip:n.remoteAddress,code:e,reason:t});},onEviction(n,e){c&&u.warn("[session] evicted",{identity:n.identity,evictedIp:n.handshake.remoteAddress,newIp:e.handshake.remoteAddress});},onBadMessage(n,e){a&&u.warn("[session] bad message",{identity:n.identity,raw:typeof e=="string"?e.slice(0,200):"<buffer>"});},onSecurityEvent(n){a&&u.warn("[session] security event",{type:n.type,identity:n.identity,ip:n.ip,details:n.details});},onHandlerError(n,e,t){a&&(u.error??u.warn)("[session] handler error",{identity:n.identity,method:e,error:t.message});},onValidationFailure(n,e,t){a&&u.warn("[session] validation failure",{identity:n.identity,error:t.message});},onRateLimitExceeded(n){c&&u.warn("[session] rate limit exceeded",{identity:n.identity,ip:n.handshake.remoteAddress});},onPongTimeout(n){a&&u.warn("[session] pong timeout (dead peer)",{identity:n.identity});},onBackpressure(n,e){a&&u.warn("[session] backpressure",{identity:n.identity,bufferedBytes:e});},onClose(){s.clear();}}}function G(r){let u=new Set(r.events??["init","connect","disconnect","close"]),d=r.timeout??5e3,c=r.retries??1;async function a(s){if(!u.has(s.event))return;let n=JSON.stringify(s),e={"Content-Type":"application/json",...r.headers};if(r.secret){let t=createHmac("sha256",r.secret).update(n).digest("hex");e["X-Signature"]=t;}for(let t=0;t<=c;t++)try{let i=new AbortController,o=setTimeout(()=>i.abort(),d);await fetch(r.url,{method:"POST",headers:e,body:n,signal:i.signal}),clearTimeout(o);return}catch{}}return {name:"webhook",onInit(){a({event:"init",timestamp:new Date().toISOString()}).catch(()=>{});},onConnection(s){a({event:"connect",timestamp:new Date().toISOString(),data:{identity:s.identity,ip:s.handshake.remoteAddress,protocol:s.protocol}}).catch(()=>{});},onDisconnect(s,n,e){a({event:"disconnect",timestamp:new Date().toISOString(),data:{identity:s.identity,code:n,reason:e}}).catch(()=>{});},onSecurityEvent(s){a({event:"security",timestamp:s.timestamp,data:{type:s.type,identity:s.identity,ip:s.ip,details:s.details}}).catch(()=>{});},onAuthFailed(s,n,e){a({event:"auth_failed",timestamp:new Date().toISOString(),data:{identity:s.identity,ip:s.remoteAddress,code:n,reason:e}}).catch(()=>{});},onEviction(s,n){a({event:"eviction",timestamp:new Date().toISOString(),data:{identity:s.identity,evictedIp:s.handshake.remoteAddress,newIp:n.handshake.remoteAddress}}).catch(()=>{});},onClosing(){a({event:"closing",timestamp:new Date().toISOString()}).catch(()=>{});},onClose(){}}}export{R as amqpPlugin,x as anomalyPlugin,L as asyncWorkerPlugin,D as circuitBreakerPlugin,$ as connectionGuardPlugin,I as heartbeatPlugin,N as kafkaPlugin,q as messageDedupPlugin,B as metricsPlugin,F as mqttPlugin,H as otelPlugin,W as piiRedactorPlugin,j as rateLimitNotifierPlugin,Y as redisPubSubPlugin,K as replayBufferPlugin,V as schemaVersioningPlugin,z as sessionLogPlugin,G as webhookPlugin};
1
+ import {createHmac}from'crypto';function R(n){let d=n.exchange??"ocpp.events",l=n.routingKey??"ocpp.{event}.{identity}",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};
@@ -4815,7 +4815,16 @@ declare class WorkerPool {
4815
4815
  private _pending;
4816
4816
  private _maxQueueSize;
4817
4817
  private _terminated;
4818
+ private readonly _workerPath;
4818
4819
  constructor(options?: WorkerPoolOptions);
4820
+ /**
4821
+ * Create (or recreate) a worker bound to a fixed pool index, wiring up
4822
+ * message/error/exit handlers. On a crash the worker is respawned at the
4823
+ * same index and any tasks it owned are rejected so callers never hang.
4824
+ */
4825
+ private _createWorker;
4826
+ /** Reject every pending task that was dispatched to the given worker index. */
4827
+ private _failWorkerTasks;
4819
4828
  /** Number of worker threads in the pool */
4820
4829
  get size(): number;
4821
4830
  /** Number of pending (unresolved) parse tasks */
@@ -5126,6 +5135,13 @@ declare class OCPPServer extends OCPPServer_base {
5126
5135
  * @param options Call options
5127
5136
  */
5128
5137
  broadcastBatch<V extends AllMethodNames<any>>(identities: string[], method: V, params: OCPPRequestType<any, V>, options?: CallOptions): Promise<void>;
5138
+ /**
5139
+ * Builds a `noServer` WebSocketServer with the configured transport-layer
5140
+ * settings (max payload + per-message compression). Centralized so every
5141
+ * (re)creation site — constructor, upgrade re-init, and close/restart —
5142
+ * stays consistent.
5143
+ */
5144
+ private _createWss;
5129
5145
  private _buildCompressionConfig;
5130
5146
  }
5131
5147
 
@@ -5548,9 +5564,20 @@ interface RouterConfig {
5548
5564
  rateLimit?: RateLimitOptions;
5549
5565
  }
5550
5566
  interface CORSOptions {
5551
- /** Allowed IPv4, IPv6, or CIDR ranges (e.g. "10.0.0.0/8") */
5567
+ /**
5568
+ * Allowed exact IPv4/IPv6 addresses or CIDR ranges
5569
+ * (e.g. "10.0.0.0/8", "2001:db8::/32").
5570
+ */
5552
5571
  allowedIPs?: string[];
5553
- /** Allowed Origin header values (e.g. "https://dashboard.example.com") */
5572
+ /**
5573
+ * Allowed `Origin` header values (e.g. "https://dashboard.example.com").
5574
+ *
5575
+ * **Note:** this is a *browser-side* defense only. Requests with **no**
5576
+ * `Origin` header pass through (physical charging stations don't send one),
5577
+ * so a non-browser client can bypass this by simply omitting the header.
5578
+ * Do not rely on `allowedOrigins` for authentication — use `auth()` / Basic
5579
+ * Auth / mTLS for that.
5580
+ */
5554
5581
  allowedOrigins?: string[];
5555
5582
  /** Allowed WebSocket protocol schemes */
5556
5583
  allowedSchemes?: ("ws" | "wss")[];
@@ -4815,7 +4815,16 @@ declare class WorkerPool {
4815
4815
  private _pending;
4816
4816
  private _maxQueueSize;
4817
4817
  private _terminated;
4818
+ private readonly _workerPath;
4818
4819
  constructor(options?: WorkerPoolOptions);
4820
+ /**
4821
+ * Create (or recreate) a worker bound to a fixed pool index, wiring up
4822
+ * message/error/exit handlers. On a crash the worker is respawned at the
4823
+ * same index and any tasks it owned are rejected so callers never hang.
4824
+ */
4825
+ private _createWorker;
4826
+ /** Reject every pending task that was dispatched to the given worker index. */
4827
+ private _failWorkerTasks;
4819
4828
  /** Number of worker threads in the pool */
4820
4829
  get size(): number;
4821
4830
  /** Number of pending (unresolved) parse tasks */
@@ -5126,6 +5135,13 @@ declare class OCPPServer extends OCPPServer_base {
5126
5135
  * @param options Call options
5127
5136
  */
5128
5137
  broadcastBatch<V extends AllMethodNames<any>>(identities: string[], method: V, params: OCPPRequestType<any, V>, options?: CallOptions): Promise<void>;
5138
+ /**
5139
+ * Builds a `noServer` WebSocketServer with the configured transport-layer
5140
+ * settings (max payload + per-message compression). Centralized so every
5141
+ * (re)creation site — constructor, upgrade re-init, and close/restart —
5142
+ * stays consistent.
5143
+ */
5144
+ private _createWss;
5129
5145
  private _buildCompressionConfig;
5130
5146
  }
5131
5147
 
@@ -5548,9 +5564,20 @@ interface RouterConfig {
5548
5564
  rateLimit?: RateLimitOptions;
5549
5565
  }
5550
5566
  interface CORSOptions {
5551
- /** Allowed IPv4, IPv6, or CIDR ranges (e.g. "10.0.0.0/8") */
5567
+ /**
5568
+ * Allowed exact IPv4/IPv6 addresses or CIDR ranges
5569
+ * (e.g. "10.0.0.0/8", "2001:db8::/32").
5570
+ */
5552
5571
  allowedIPs?: string[];
5553
- /** Allowed Origin header values (e.g. "https://dashboard.example.com") */
5572
+ /**
5573
+ * Allowed `Origin` header values (e.g. "https://dashboard.example.com").
5574
+ *
5575
+ * **Note:** this is a *browser-side* defense only. Requests with **no**
5576
+ * `Origin` header pass through (physical charging stations don't send one),
5577
+ * so a non-browser client can bypass this by simply omitting the header.
5578
+ * Do not rely on `allowedOrigins` for authentication — use `auth()` / Basic
5579
+ * Auth / mTLS for that.
5580
+ */
5554
5581
  allowedOrigins?: string[];
5555
5582
  /** Allowed WebSocket protocol schemes */
5556
5583
  allowedSchemes?: ("ws" | "wss")[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocpp-ws-io",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
4
4
  "description": "OCPP RPC WebSocket client and server for OCPP 1.6J, 2.0.1, and 2.1. Type-safe TypeScript toolkit for EV charging, CSMS backends, Redis scaling, and protocol validation.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -171,6 +171,6 @@
171
171
  "reflect-metadata": "^0.2.2",
172
172
  "tsup": "^8.5.1",
173
173
  "typescript": "^5.9.3",
174
- "vitest": "^3.2.4"
174
+ "vitest": "^4.1.8"
175
175
  }
176
176
  }