ocpp-ws-io 2.2.0 → 2.2.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugins.js CHANGED
@@ -1 +1 @@
1
- 'use strict';var crypto=require('crypto');var P=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(i,r)=>(typeof require<"u"?require:i)[r]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});function y(t){let i=t?.reconnectThreshold??5,r=t?.windowMs??6e4,e=new Map,n=null,o=null;function u(a,s){let l=s-r,c=0;for(;c<a.length&&a[c]<l;)c++;return c>0?a.slice(c):a}return {name:"anomaly",onInit(a){n=a,o=setInterval(()=>{let s=Date.now();for(let[l,c]of e){let g=u(c,s);g.length===0?e.delete(l):e.set(l,g);}},r).unref();},onConnection(a){let s=Date.now(),l=a.identity,c=e.get(l)??[];c=u(c,s),c.push(s),e.set(l,c),c.length>i&&n&&n.emit("securityEvent",{type:"ANOMALY_RAPID_RECONNECT",identity:l,ip:a.handshake.remoteAddress,timestamp:new Date().toISOString(),details:{connectionsInWindow:c.length,threshold:i,windowMs:r}});},onClose(){o&&(clearInterval(o),o=null),e.clear(),n=null;}}}function f(t){let i=0;return {name:"connection-guard",onConnection(r){i++,i>t.maxConnections&&r.close({code:4001,reason:"Connection limit reached",force:true}).catch(()=>{});},onDisconnect(){i=Math.max(0,i-1);},onClose(){i=0;}}}function h(){return {name:"heartbeat",onConnection(t){t.handle("Heartbeat",()=>({currentTime:new Date().toISOString()}));}}}function b(t){let i=t?.intervalMs??3e4,r=0,e=0,n=0,o=0,u=0,a=Date.now(),s=null,l=new Map;function c(){return {totalConnections:r,totalDisconnections:e,activeConnections:n,peakConnections:o,connectionDurationAvgMs:e>0?Math.round(u/e):0,uptimeMs:Date.now()-a,timestamp:new Date().toISOString()}}return {name:"metrics",getMetrics:c,onInit(){a=Date.now(),i>0&&t?.onSnapshot&&(s=setInterval(()=>{t.onSnapshot(c());},i),s&&typeof s=="object"&&"unref"in s&&s.unref());},onConnection(m){r++,n++,n>o&&(o=n),l.set(m.identity,Date.now());},onDisconnect(m){e++,n=Math.max(0,n-1);let d=l.get(m.identity);d&&(u+=Date.now()-d,l.delete(m.identity));},onClose(){s&&(clearInterval(s),s=null),l.clear();}}}function O(t){let i=t?.tracer??null,r=new Map;return {name:"otel",onInit(e){if(!i)try{let{createRequire:n}=P("module");i=n(__filename)("@opentelemetry/api").trace.getTracer(t?.serviceName??"ocpp-server","1.0.0");}catch{e.log.warn?.("otelPlugin: @opentelemetry/api not found \u2014 plugin disabled. Install it as a peer dependency."),i=null;}},onConnection(e){if(!i)return;let n=i.startSpan("ocpp.connection",{kind:1});n.setAttribute("ocpp.identity",e.identity),n.setAttribute("ocpp.protocol",e.protocol??"unknown"),n.setAttribute("net.peer.ip",e.handshake.remoteAddress),r.set(e.identity,{span:n,startTime:Date.now()});},onDisconnect(e,n){let o=r.get(e.identity);if(!o)return;let u=Date.now()-o.startTime;o.span.setAttribute("ocpp.close_code",n),o.span.setAttribute("ocpp.duration_ms",u),o.span.setStatus({code:1}),o.span.end(),r.delete(e.identity);},onClose(){for(let[,e]of r)e.span.setStatus({code:2,message:"Server shutdown"}),e.span.end();r.clear();}}}function C(t){let i=t?.logger??console,r=new Map;return {name:"session-log",onConnection(e){r.set(e.identity,Date.now()),i.info("Connected",{identity:e.identity,ip:e.handshake.remoteAddress,protocol:e.protocol});},onDisconnect(e,n,o){let u=r.get(e.identity),a=u?Math.round((Date.now()-u)/1e3):0;r.delete(e.identity),i.info("Disconnected",{identity:e.identity,durationSec:a,code:n,reason:o});},onClose(){r.clear();}}}function v(t){let i=new Set(t.events??["init","connect","disconnect","close"]),r=t.timeout??5e3,e=t.retries??1;async function n(o){if(!i.has(o.event))return;let u=JSON.stringify(o),a={"Content-Type":"application/json",...t.headers};if(t.secret){let s=crypto.createHmac("sha256",t.secret).update(u).digest("hex");a["X-Signature"]=s;}for(let s=0;s<=e;s++)try{let l=new AbortController,c=setTimeout(()=>l.abort(),r);await fetch(t.url,{method:"POST",headers:a,body:u,signal:l.signal}),clearTimeout(c);return}catch{}}return {name:"webhook",onInit(){n({event:"init",timestamp:new Date().toISOString()}).catch(()=>{});},onConnection(o){n({event:"connect",timestamp:new Date().toISOString(),data:{identity:o.identity,ip:o.handshake.remoteAddress,protocol:o.protocol}}).catch(()=>{});},onDisconnect(o,u,a){n({event:"disconnect",timestamp:new Date().toISOString(),data:{identity:o.identity,code:u,reason:a}}).catch(()=>{});},onClose(){n({event:"close",timestamp:new Date().toISOString()}).catch(()=>{});}}}exports.anomalyPlugin=y;exports.connectionGuardPlugin=f;exports.heartbeatPlugin=h;exports.metricsPlugin=b;exports.otelPlugin=O;exports.sessionLogPlugin=C;exports.webhookPlugin=v;
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;
package/dist/plugins.mjs CHANGED
@@ -1 +1 @@
1
- import'path';import {fileURLToPath}from'url';import {createHmac}from'crypto';var y=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(i,r)=>(typeof require<"u"?require:i)[r]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var h=()=>fileURLToPath(import.meta.url);var p=h();function b(t){let i=t?.reconnectThreshold??5,r=t?.windowMs??6e4,e=new Map,n=null,o=null;function l(a,s){let u=s-r,c=0;for(;c<a.length&&a[c]<u;)c++;return c>0?a.slice(c):a}return {name:"anomaly",onInit(a){n=a,o=setInterval(()=>{let s=Date.now();for(let[u,c]of e){let d=l(c,s);d.length===0?e.delete(u):e.set(u,d);}},r).unref();},onConnection(a){let s=Date.now(),u=a.identity,c=e.get(u)??[];c=l(c,s),c.push(s),e.set(u,c),c.length>i&&n&&n.emit("securityEvent",{type:"ANOMALY_RAPID_RECONNECT",identity:u,ip:a.handshake.remoteAddress,timestamp:new Date().toISOString(),details:{connectionsInWindow:c.length,threshold:i,windowMs:r}});},onClose(){o&&(clearInterval(o),o=null),e.clear(),n=null;}}}function O(t){let i=0;return {name:"connection-guard",onConnection(r){i++,i>t.maxConnections&&r.close({code:4001,reason:"Connection limit reached",force:true}).catch(()=>{});},onDisconnect(){i=Math.max(0,i-1);},onClose(){i=0;}}}function C(){return {name:"heartbeat",onConnection(t){t.handle("Heartbeat",()=>({currentTime:new Date().toISOString()}));}}}function w(t){let i=t?.intervalMs??3e4,r=0,e=0,n=0,o=0,l=0,a=Date.now(),s=null,u=new Map;function c(){return {totalConnections:r,totalDisconnections:e,activeConnections:n,peakConnections:o,connectionDurationAvgMs:e>0?Math.round(l/e):0,uptimeMs:Date.now()-a,timestamp:new Date().toISOString()}}return {name:"metrics",getMetrics:c,onInit(){a=Date.now(),i>0&&t?.onSnapshot&&(s=setInterval(()=>{t.onSnapshot(c());},i),s&&typeof s=="object"&&"unref"in s&&s.unref());},onConnection(g){r++,n++,n>o&&(o=n),u.set(g.identity,Date.now());},onDisconnect(g){e++,n=Math.max(0,n-1);let P=u.get(g.identity);P&&(l+=Date.now()-P,u.delete(g.identity));},onClose(){s&&(clearInterval(s),s=null),u.clear();}}}function v(t){let i=t?.tracer??null,r=new Map;return {name:"otel",onInit(e){if(!i)try{let{createRequire:n}=y("module");i=n(p)("@opentelemetry/api").trace.getTracer(t?.serviceName??"ocpp-server","1.0.0");}catch{e.log.warn?.("otelPlugin: @opentelemetry/api not found \u2014 plugin disabled. Install it as a peer dependency."),i=null;}},onConnection(e){if(!i)return;let n=i.startSpan("ocpp.connection",{kind:1});n.setAttribute("ocpp.identity",e.identity),n.setAttribute("ocpp.protocol",e.protocol??"unknown"),n.setAttribute("net.peer.ip",e.handshake.remoteAddress),r.set(e.identity,{span:n,startTime:Date.now()});},onDisconnect(e,n){let o=r.get(e.identity);if(!o)return;let l=Date.now()-o.startTime;o.span.setAttribute("ocpp.close_code",n),o.span.setAttribute("ocpp.duration_ms",l),o.span.setStatus({code:1}),o.span.end(),r.delete(e.identity);},onClose(){for(let[,e]of r)e.span.setStatus({code:2,message:"Server shutdown"}),e.span.end();r.clear();}}}function S(t){let i=t?.logger??console,r=new Map;return {name:"session-log",onConnection(e){r.set(e.identity,Date.now()),i.info("Connected",{identity:e.identity,ip:e.handshake.remoteAddress,protocol:e.protocol});},onDisconnect(e,n,o){let l=r.get(e.identity),a=l?Math.round((Date.now()-l)/1e3):0;r.delete(e.identity),i.info("Disconnected",{identity:e.identity,durationSec:a,code:n,reason:o});},onClose(){r.clear();}}}function x(t){let i=new Set(t.events??["init","connect","disconnect","close"]),r=t.timeout??5e3,e=t.retries??1;async function n(o){if(!i.has(o.event))return;let l=JSON.stringify(o),a={"Content-Type":"application/json",...t.headers};if(t.secret){let s=createHmac("sha256",t.secret).update(l).digest("hex");a["X-Signature"]=s;}for(let s=0;s<=e;s++)try{let u=new AbortController,c=setTimeout(()=>u.abort(),r);await fetch(t.url,{method:"POST",headers:a,body:l,signal:u.signal}),clearTimeout(c);return}catch{}}return {name:"webhook",onInit(){n({event:"init",timestamp:new Date().toISOString()}).catch(()=>{});},onConnection(o){n({event:"connect",timestamp:new Date().toISOString(),data:{identity:o.identity,ip:o.handshake.remoteAddress,protocol:o.protocol}}).catch(()=>{});},onDisconnect(o,l,a){n({event:"disconnect",timestamp:new Date().toISOString(),data:{identity:o.identity,code:l,reason:a}}).catch(()=>{});},onClose(){n({event:"close",timestamp:new Date().toISOString()}).catch(()=>{});}}}export{b as anomalyPlugin,O as connectionGuardPlugin,C as heartbeatPlugin,w as metricsPlugin,v as otelPlugin,S as sessionLogPlugin,x as webhookPlugin};
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};
@@ -4772,6 +4772,11 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4772
4772
  private _callWithRetry;
4773
4773
  /** Maximum bytes allowed in the ws send buffer before applying backpressure (512KB) */
4774
4774
  private static readonly _BACKPRESSURE_THRESHOLD;
4775
+ /**
4776
+ * Protected hook for plugins to intercept outbound messages before serialization.
4777
+ * Return `false` to suppress the message transmission.
4778
+ */
4779
+ protected _invokeBeforeSend(_message: OCPPMessage): boolean | Promise<boolean>;
4775
4780
  /**
4776
4781
  * Wraps ws.send() with backpressure protection.
4777
4782
  * If bufferedAmount exceeds the threshold, waits for the buffer to drain
@@ -4836,6 +4841,8 @@ declare class WorkerPool {
4836
4841
  declare class OCPPServerClient extends OCPPClient {
4837
4842
  private _serverSession;
4838
4843
  private _serverHandshake;
4844
+ /** Plugins passed from OCPPServer for hook execution */
4845
+ private _serverPlugins;
4839
4846
  constructor(options: ClientOptions, context: {
4840
4847
  ws: WebSocket$1;
4841
4848
  handshake: HandshakeInfo;
@@ -4845,11 +4852,14 @@ declare class OCPPServerClient extends OCPPClient {
4845
4852
  adaptiveMultiplier?: () => number;
4846
4853
  /** Optional worker pool for off-thread JSON parsing */
4847
4854
  workerPool?: WorkerPool;
4855
+ /** Plugins from the server for hook execution */
4856
+ plugins?: OCPPPlugin[];
4848
4857
  });
4849
4858
  private _rateLimits;
4850
4859
  private _adaptiveMultiplier;
4851
4860
  private _workerPool;
4852
4861
  private _checkRateLimit;
4862
+ protected _invokeBeforeSend(message: OCPPMessage): boolean | Promise<boolean>;
4853
4863
  private _attachServerWebsocket;
4854
4864
  private _handleRateLimitExceeded;
4855
4865
  /**
@@ -4909,6 +4919,7 @@ declare class OCPPServer extends OCPPServer_base {
4909
4919
  private _adaptiveLimiter;
4910
4920
  private _plugins;
4911
4921
  private _workerPool;
4922
+ private _telemetryInterval;
4912
4923
  private readonly _nodeId;
4913
4924
  private _sessions;
4914
4925
  private _gcInterval;
@@ -4987,6 +4998,11 @@ declare class OCPPServer extends OCPPServer_base {
4987
4998
  * ```
4988
4999
  */
4989
5000
  plugin(...plugins: OCPPPlugin[]): this;
5001
+ /**
5002
+ * Starts the periodic telemetry push engine if configured.
5003
+ * Pushes OCPPServerStats to all plugins implementing onTelemetry.
5004
+ */
5005
+ private _startTelemetryPush;
4990
5006
  /**
4991
5007
  * Registers middleware chain(s) as a wildcard/catch-all router.
4992
5008
  *
@@ -5631,6 +5647,15 @@ interface ServerOptionsBase {
5631
5647
  * (default: false)
5632
5648
  */
5633
5649
  compression?: boolean | CompressionOptions;
5650
+ /**
5651
+ * Telemetry configuration for plugin stats push.
5652
+ * When configured and plugins implement `onTelemetry`, the server will
5653
+ * push `OCPPServerStats` at the configured interval.
5654
+ * - `{ pushIntervalMs: 10000 }` → push stats every 10s
5655
+ * - `{ pushIntervalMs: 0 }` → disable periodic push
5656
+ * (default: disabled)
5657
+ */
5658
+ telemetry?: TelemetryConfig;
5634
5659
  }
5635
5660
  /** When strictMode is enabled, protocols MUST be specified */
5636
5661
  interface StrictServerOptions extends ServerOptionsBase {
@@ -5643,6 +5668,13 @@ interface RelaxedServerOptions extends ServerOptionsBase {
5643
5668
  protocols?: AnyOCPPProtocol[];
5644
5669
  }
5645
5670
  type ServerOptions = StrictServerOptions | RelaxedServerOptions;
5671
+ interface TelemetryConfig {
5672
+ /**
5673
+ * Interval in ms to push server stats to plugins via `onTelemetry`.
5674
+ * Set to 0 to disable. (default: 0 — disabled)
5675
+ */
5676
+ pushIntervalMs?: number;
5677
+ }
5646
5678
  interface OCPPServerStats {
5647
5679
  /** Number of currently connected WebSockets */
5648
5680
  connectedClients: number;
@@ -5731,6 +5763,20 @@ interface ClientEvents {
5731
5763
  message: string;
5732
5764
  error: Error;
5733
5765
  }];
5766
+ handlerError: [{
5767
+ method: string;
5768
+ error: Error;
5769
+ }];
5770
+ pongTimeout: [{
5771
+ identity: string;
5772
+ }];
5773
+ backpressure: [{
5774
+ identity: string;
5775
+ bufferedAmount: number;
5776
+ }];
5777
+ rateLimitExceeded: [{
5778
+ rawData: unknown;
5779
+ }];
5734
5780
  ping: [];
5735
5781
  pong: [];
5736
5782
  strictValidationFailure: [{
@@ -5745,7 +5791,7 @@ interface ClientEvents {
5745
5791
  */
5746
5792
  interface SecurityEvent {
5747
5793
  /** Event type identifier */
5748
- type: "AUTH_FAILED" | "RATE_LIMIT_EXCEEDED" | "UPGRADE_ABORTED" | "CONNECTION_RATE_LIMIT" | "INVALID_PAYLOAD";
5794
+ type: "AUTH_FAILED" | "RATE_LIMIT_EXCEEDED" | "UPGRADE_ABORTED" | "CONNECTION_RATE_LIMIT" | "INVALID_PAYLOAD" | "ANOMALY_RAPID_RECONNECT" | "ANOMALY_AUTH_BRUTE_FORCE" | "ANOMALY_MESSAGE_FUZZING" | "ANOMALY_IDENTITY_COLLISION";
5749
5795
  /** Station identity (if known) */
5750
5796
  identity?: string;
5751
5797
  /** Remote IP address */
@@ -5839,6 +5885,54 @@ interface OCPPPlugin {
5839
5885
  onDisconnect?(client: OCPPServerClient, code: number, reason: string): void;
5840
5886
  /** Called during server.close() for plugin cleanup */
5841
5887
  onClose?(): void | Promise<void>;
5888
+ /**
5889
+ * Called for every OCPP message (IN + OUT, CALL + CALLRESULT + CALLERROR).
5890
+ * Provides unified observability over all message traffic.
5891
+ */
5892
+ onMessage?(client: OCPPServerClient, payload: MessageEventPayload): void | Promise<void>;
5893
+ /**
5894
+ * Called before a received message is parsed/routed.
5895
+ * Return `false` to silently drop the message.
5896
+ */
5897
+ onBeforeReceive?(client: OCPPServerClient, rawData: unknown): undefined | boolean | Promise<undefined | boolean>;
5898
+ /**
5899
+ * Called before a message is transmitted on the wire.
5900
+ * Return `false` to suppress the send.
5901
+ */
5902
+ onBeforeSend?(client: OCPPServerClient, message: OCPPMessage): undefined | boolean | Promise<undefined | boolean>;
5903
+ /** WebSocket-level or protocol-level error */
5904
+ onError?(client: OCPPServerClient, error: Error): void | Promise<void>;
5905
+ /** Malformed / unparseable message received */
5906
+ onBadMessage?(client: OCPPServerClient, rawMessage: string, error: Error): void | Promise<void>;
5907
+ /** Schema validation failure (strictMode) */
5908
+ onValidationFailure?(client: OCPPServerClient, message: unknown, error: Error): void | Promise<void>;
5909
+ /** Message dropped or client disconnected due to rate limiting */
5910
+ onRateLimitExceeded?(client: OCPPServerClient, rawData: unknown): void | Promise<void>;
5911
+ /** User handler threw an error during CALL processing */
5912
+ onHandlerError?(client: OCPPServerClient, method: string, error: Error): void | Promise<void>;
5913
+ /** Structured security events (AUTH_FAILED, UPGRADE_ABORTED, etc.) */
5914
+ onSecurityEvent?(event: SecurityEvent): void | Promise<void>;
5915
+ /** Auth attempt failed — visible even when onConnection never fires */
5916
+ onAuthFailed?(handshake: HandshakeInfo, code: number, reason: string): void | Promise<void>;
5917
+ /** Existing client with same identity was evicted by a new connection */
5918
+ onEviction?(evictedClient: OCPPServerClient, newClient: OCPPServerClient): void | Promise<void>;
5919
+ /** Send buffer exceeded backpressure threshold (512KB — slow client) */
5920
+ onBackpressure?(client: OCPPServerClient, bufferedAmount: number): void | Promise<void>;
5921
+ /** Pong not received within timeout — dead peer detected */
5922
+ onPongTimeout?(client: OCPPServerClient): void | Promise<void>;
5923
+ /** Periodic server stats snapshot (opt-in via `telemetry.pushIntervalMs`) */
5924
+ onTelemetry?(stats: OCPPServerStats, adapterMetrics?: Record<string, unknown>): void | Promise<void>;
5925
+ /**
5926
+ * Plugin contributes custom Prometheus metric lines to the /metrics endpoint.
5927
+ * Return an array of Prometheus exposition format strings.
5928
+ */
5929
+ getCustomMetrics?(): string[] | Promise<string[]>;
5930
+ /** Server options changed via server.reconfigure() */
5931
+ onReconfigure?(newOptions: Partial<ServerOptions>, oldOptions: ServerOptions): void | Promise<void>;
5932
+ /** TLS certificates hot-reloaded via server.updateTLS() */
5933
+ onTLSUpdate?(tlsOpts: TLSOptions): void | Promise<void>;
5934
+ /** Server entering CLOSING state — pre-shutdown hook (before clients are drained) */
5935
+ onClosing?(): void | Promise<void>;
5842
5936
  }
5843
5937
  declare const NOREPLY: unique symbol;
5844
5938
  type MiddlewareContext = {
@@ -5863,6 +5957,17 @@ type MiddlewareContext = {
5863
5957
  messageId: string;
5864
5958
  error: OCPPCallError;
5865
5959
  method: string;
5960
+ } | {
5961
+ type: "outgoing_result";
5962
+ messageId: string;
5963
+ method: string;
5964
+ payload: unknown;
5965
+ } | {
5966
+ type: "outgoing_error";
5967
+ messageId: string;
5968
+ method: string;
5969
+ errorCode: string;
5970
+ errorDescription: string;
5866
5971
  };
5867
5972
 
5868
5973
  interface BaseConnectionContext {
@@ -5874,7 +5979,7 @@ interface BaseConnectionContext {
5874
5979
  reject: (code?: number, message?: string) => never;
5875
5980
  }
5876
5981
  interface ConnectionContext extends BaseConnectionContext {
5877
- /** Triggers the next middleware in the execution chain, optionally merging a payload into ctx.state */
5982
+ /** Triggers the next middleware in the execution chain, optionally merging a payload into ctx.state and then client.session in the chain*/
5878
5983
  next: (payload?: Record<string, unknown>) => Promise<void>;
5879
5984
  }
5880
5985
  interface AuthContext<TSession = Record<string, unknown>> extends BaseConnectionContext {
@@ -5885,4 +5990,4 @@ interface AuthContext<TSession = Record<string, unknown>> extends BaseConnection
5885
5990
  }
5886
5991
  type ConnectionMiddleware = (ctx: ConnectionContext) => Promise<void> | void;
5887
5992
 
5888
- export { createRouter as $, type AuthCallback as A, type OCPPMessage as B, type ConnectionMiddleware as C, type OCPPMethodMap as D, type EventAdapterInterface as E, type OCPPProtocol as F, type OCPPProtocolKey as G, type HandlerContext as H, type OCPPRequestType as I, type OCPPResponseType as J, OCPPRouter as K, type LoggerLike as L, type MiddlewareFunction as M, NOREPLY as N, type OCPPPlugin as O, OCPPServer as P, OCPPServerClient as Q, type RateLimitOptions as R, type RouterConfig as S, SecurityProfile as T, type ServerEvents as U, Validator as V, type ServerOptions as W, type SessionData as X, type TLSOptions as Y, type TypedEventEmitter as Z, type WildcardHandler as _, type LoggingConfig as a, createValidator as a0, type MiddlewareContext as b, type AllMethodNames as c, type AnyOCPPProtocol as d, type AuthAccept as e, type CORSOptions as f, type CallHandler as g, type CallOptions as h, type ClientEvents as i, type ClientOptions as j, type CloseOptions as k, type CompressionOptions as l, type ConnectionContext as m, ConnectionState as n, type HandshakeInfo as o, type ListenOptions as p, MessageType as q, type MiddlewareNext as r, MiddlewareStack as s, type OCPP16Methods as t, type OCPP201Methods as u, type OCPP21Methods as v, type OCPPCall as w, type OCPPCallError as x, type OCPPCallResult as y, OCPPClient as z };
5993
+ export { type ServerEvents as $, type AllMethodNames as A, MessageType as B, type CallOptions as C, type MiddlewareNext as D, type EventAdapterInterface as E, MiddlewareStack as F, type OCPP16Methods as G, type HandlerContext as H, type OCPP201Methods as I, type OCPP21Methods as J, type OCPPCall as K, type LoggerLike as L, type MiddlewareFunction as M, NOREPLY as N, OCPPServer as O, type OCPPCallError as P, type OCPPCallResult as Q, OCPPClient as R, type OCPPMessage as S, type OCPPMethodMap as T, type OCPPProtocolKey as U, Validator as V, OCPPRouter as W, type RateLimitOptions as X, type RouterConfig as Y, type SecurityEvent as Z, SecurityProfile as _, OCPPServerClient as a, type ServerOptions as a0, type SessionData as a1, type TLSOptions as a2, type TelemetryConfig as a3, type TypedEventEmitter as a4, type WildcardHandler as a5, createRouter as a6, createValidator as a7, type OCPPServerStats as b, type OCPPProtocol as c, type OCPPRequestType as d, type OCPPResponseType as e, type CloseOptions as f, type AuthCallback as g, type LoggingConfig as h, type MiddlewareContext as i, type OCPPPlugin as j, type ConnectionMiddleware as k, type AnyOCPPProtocol as l, type AuthAccept as m, type AuthContext as n, type CORSOptions as o, type CallHandler as p, type ClientEvents as q, type ClientOptions as r, type CompressionOptions as s, type ConnectionContext as t, ConnectionState as u, type HandshakeInfo as v, type ListenOptions as w, type MessageDirection as x, type MessageEventContext as y, type MessageEventPayload as z };
@@ -4772,6 +4772,11 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4772
4772
  private _callWithRetry;
4773
4773
  /** Maximum bytes allowed in the ws send buffer before applying backpressure (512KB) */
4774
4774
  private static readonly _BACKPRESSURE_THRESHOLD;
4775
+ /**
4776
+ * Protected hook for plugins to intercept outbound messages before serialization.
4777
+ * Return `false` to suppress the message transmission.
4778
+ */
4779
+ protected _invokeBeforeSend(_message: OCPPMessage): boolean | Promise<boolean>;
4775
4780
  /**
4776
4781
  * Wraps ws.send() with backpressure protection.
4777
4782
  * If bufferedAmount exceeds the threshold, waits for the buffer to drain
@@ -4836,6 +4841,8 @@ declare class WorkerPool {
4836
4841
  declare class OCPPServerClient extends OCPPClient {
4837
4842
  private _serverSession;
4838
4843
  private _serverHandshake;
4844
+ /** Plugins passed from OCPPServer for hook execution */
4845
+ private _serverPlugins;
4839
4846
  constructor(options: ClientOptions, context: {
4840
4847
  ws: WebSocket$1;
4841
4848
  handshake: HandshakeInfo;
@@ -4845,11 +4852,14 @@ declare class OCPPServerClient extends OCPPClient {
4845
4852
  adaptiveMultiplier?: () => number;
4846
4853
  /** Optional worker pool for off-thread JSON parsing */
4847
4854
  workerPool?: WorkerPool;
4855
+ /** Plugins from the server for hook execution */
4856
+ plugins?: OCPPPlugin[];
4848
4857
  });
4849
4858
  private _rateLimits;
4850
4859
  private _adaptiveMultiplier;
4851
4860
  private _workerPool;
4852
4861
  private _checkRateLimit;
4862
+ protected _invokeBeforeSend(message: OCPPMessage): boolean | Promise<boolean>;
4853
4863
  private _attachServerWebsocket;
4854
4864
  private _handleRateLimitExceeded;
4855
4865
  /**
@@ -4909,6 +4919,7 @@ declare class OCPPServer extends OCPPServer_base {
4909
4919
  private _adaptiveLimiter;
4910
4920
  private _plugins;
4911
4921
  private _workerPool;
4922
+ private _telemetryInterval;
4912
4923
  private readonly _nodeId;
4913
4924
  private _sessions;
4914
4925
  private _gcInterval;
@@ -4987,6 +4998,11 @@ declare class OCPPServer extends OCPPServer_base {
4987
4998
  * ```
4988
4999
  */
4989
5000
  plugin(...plugins: OCPPPlugin[]): this;
5001
+ /**
5002
+ * Starts the periodic telemetry push engine if configured.
5003
+ * Pushes OCPPServerStats to all plugins implementing onTelemetry.
5004
+ */
5005
+ private _startTelemetryPush;
4990
5006
  /**
4991
5007
  * Registers middleware chain(s) as a wildcard/catch-all router.
4992
5008
  *
@@ -5631,6 +5647,15 @@ interface ServerOptionsBase {
5631
5647
  * (default: false)
5632
5648
  */
5633
5649
  compression?: boolean | CompressionOptions;
5650
+ /**
5651
+ * Telemetry configuration for plugin stats push.
5652
+ * When configured and plugins implement `onTelemetry`, the server will
5653
+ * push `OCPPServerStats` at the configured interval.
5654
+ * - `{ pushIntervalMs: 10000 }` → push stats every 10s
5655
+ * - `{ pushIntervalMs: 0 }` → disable periodic push
5656
+ * (default: disabled)
5657
+ */
5658
+ telemetry?: TelemetryConfig;
5634
5659
  }
5635
5660
  /** When strictMode is enabled, protocols MUST be specified */
5636
5661
  interface StrictServerOptions extends ServerOptionsBase {
@@ -5643,6 +5668,13 @@ interface RelaxedServerOptions extends ServerOptionsBase {
5643
5668
  protocols?: AnyOCPPProtocol[];
5644
5669
  }
5645
5670
  type ServerOptions = StrictServerOptions | RelaxedServerOptions;
5671
+ interface TelemetryConfig {
5672
+ /**
5673
+ * Interval in ms to push server stats to plugins via `onTelemetry`.
5674
+ * Set to 0 to disable. (default: 0 — disabled)
5675
+ */
5676
+ pushIntervalMs?: number;
5677
+ }
5646
5678
  interface OCPPServerStats {
5647
5679
  /** Number of currently connected WebSockets */
5648
5680
  connectedClients: number;
@@ -5731,6 +5763,20 @@ interface ClientEvents {
5731
5763
  message: string;
5732
5764
  error: Error;
5733
5765
  }];
5766
+ handlerError: [{
5767
+ method: string;
5768
+ error: Error;
5769
+ }];
5770
+ pongTimeout: [{
5771
+ identity: string;
5772
+ }];
5773
+ backpressure: [{
5774
+ identity: string;
5775
+ bufferedAmount: number;
5776
+ }];
5777
+ rateLimitExceeded: [{
5778
+ rawData: unknown;
5779
+ }];
5734
5780
  ping: [];
5735
5781
  pong: [];
5736
5782
  strictValidationFailure: [{
@@ -5745,7 +5791,7 @@ interface ClientEvents {
5745
5791
  */
5746
5792
  interface SecurityEvent {
5747
5793
  /** Event type identifier */
5748
- type: "AUTH_FAILED" | "RATE_LIMIT_EXCEEDED" | "UPGRADE_ABORTED" | "CONNECTION_RATE_LIMIT" | "INVALID_PAYLOAD";
5794
+ type: "AUTH_FAILED" | "RATE_LIMIT_EXCEEDED" | "UPGRADE_ABORTED" | "CONNECTION_RATE_LIMIT" | "INVALID_PAYLOAD" | "ANOMALY_RAPID_RECONNECT" | "ANOMALY_AUTH_BRUTE_FORCE" | "ANOMALY_MESSAGE_FUZZING" | "ANOMALY_IDENTITY_COLLISION";
5749
5795
  /** Station identity (if known) */
5750
5796
  identity?: string;
5751
5797
  /** Remote IP address */
@@ -5839,6 +5885,54 @@ interface OCPPPlugin {
5839
5885
  onDisconnect?(client: OCPPServerClient, code: number, reason: string): void;
5840
5886
  /** Called during server.close() for plugin cleanup */
5841
5887
  onClose?(): void | Promise<void>;
5888
+ /**
5889
+ * Called for every OCPP message (IN + OUT, CALL + CALLRESULT + CALLERROR).
5890
+ * Provides unified observability over all message traffic.
5891
+ */
5892
+ onMessage?(client: OCPPServerClient, payload: MessageEventPayload): void | Promise<void>;
5893
+ /**
5894
+ * Called before a received message is parsed/routed.
5895
+ * Return `false` to silently drop the message.
5896
+ */
5897
+ onBeforeReceive?(client: OCPPServerClient, rawData: unknown): undefined | boolean | Promise<undefined | boolean>;
5898
+ /**
5899
+ * Called before a message is transmitted on the wire.
5900
+ * Return `false` to suppress the send.
5901
+ */
5902
+ onBeforeSend?(client: OCPPServerClient, message: OCPPMessage): undefined | boolean | Promise<undefined | boolean>;
5903
+ /** WebSocket-level or protocol-level error */
5904
+ onError?(client: OCPPServerClient, error: Error): void | Promise<void>;
5905
+ /** Malformed / unparseable message received */
5906
+ onBadMessage?(client: OCPPServerClient, rawMessage: string, error: Error): void | Promise<void>;
5907
+ /** Schema validation failure (strictMode) */
5908
+ onValidationFailure?(client: OCPPServerClient, message: unknown, error: Error): void | Promise<void>;
5909
+ /** Message dropped or client disconnected due to rate limiting */
5910
+ onRateLimitExceeded?(client: OCPPServerClient, rawData: unknown): void | Promise<void>;
5911
+ /** User handler threw an error during CALL processing */
5912
+ onHandlerError?(client: OCPPServerClient, method: string, error: Error): void | Promise<void>;
5913
+ /** Structured security events (AUTH_FAILED, UPGRADE_ABORTED, etc.) */
5914
+ onSecurityEvent?(event: SecurityEvent): void | Promise<void>;
5915
+ /** Auth attempt failed — visible even when onConnection never fires */
5916
+ onAuthFailed?(handshake: HandshakeInfo, code: number, reason: string): void | Promise<void>;
5917
+ /** Existing client with same identity was evicted by a new connection */
5918
+ onEviction?(evictedClient: OCPPServerClient, newClient: OCPPServerClient): void | Promise<void>;
5919
+ /** Send buffer exceeded backpressure threshold (512KB — slow client) */
5920
+ onBackpressure?(client: OCPPServerClient, bufferedAmount: number): void | Promise<void>;
5921
+ /** Pong not received within timeout — dead peer detected */
5922
+ onPongTimeout?(client: OCPPServerClient): void | Promise<void>;
5923
+ /** Periodic server stats snapshot (opt-in via `telemetry.pushIntervalMs`) */
5924
+ onTelemetry?(stats: OCPPServerStats, adapterMetrics?: Record<string, unknown>): void | Promise<void>;
5925
+ /**
5926
+ * Plugin contributes custom Prometheus metric lines to the /metrics endpoint.
5927
+ * Return an array of Prometheus exposition format strings.
5928
+ */
5929
+ getCustomMetrics?(): string[] | Promise<string[]>;
5930
+ /** Server options changed via server.reconfigure() */
5931
+ onReconfigure?(newOptions: Partial<ServerOptions>, oldOptions: ServerOptions): void | Promise<void>;
5932
+ /** TLS certificates hot-reloaded via server.updateTLS() */
5933
+ onTLSUpdate?(tlsOpts: TLSOptions): void | Promise<void>;
5934
+ /** Server entering CLOSING state — pre-shutdown hook (before clients are drained) */
5935
+ onClosing?(): void | Promise<void>;
5842
5936
  }
5843
5937
  declare const NOREPLY: unique symbol;
5844
5938
  type MiddlewareContext = {
@@ -5863,6 +5957,17 @@ type MiddlewareContext = {
5863
5957
  messageId: string;
5864
5958
  error: OCPPCallError;
5865
5959
  method: string;
5960
+ } | {
5961
+ type: "outgoing_result";
5962
+ messageId: string;
5963
+ method: string;
5964
+ payload: unknown;
5965
+ } | {
5966
+ type: "outgoing_error";
5967
+ messageId: string;
5968
+ method: string;
5969
+ errorCode: string;
5970
+ errorDescription: string;
5866
5971
  };
5867
5972
 
5868
5973
  interface BaseConnectionContext {
@@ -5874,7 +5979,7 @@ interface BaseConnectionContext {
5874
5979
  reject: (code?: number, message?: string) => never;
5875
5980
  }
5876
5981
  interface ConnectionContext extends BaseConnectionContext {
5877
- /** Triggers the next middleware in the execution chain, optionally merging a payload into ctx.state */
5982
+ /** Triggers the next middleware in the execution chain, optionally merging a payload into ctx.state and then client.session in the chain*/
5878
5983
  next: (payload?: Record<string, unknown>) => Promise<void>;
5879
5984
  }
5880
5985
  interface AuthContext<TSession = Record<string, unknown>> extends BaseConnectionContext {
@@ -5885,4 +5990,4 @@ interface AuthContext<TSession = Record<string, unknown>> extends BaseConnection
5885
5990
  }
5886
5991
  type ConnectionMiddleware = (ctx: ConnectionContext) => Promise<void> | void;
5887
5992
 
5888
- export { createRouter as $, type AuthCallback as A, type OCPPMessage as B, type ConnectionMiddleware as C, type OCPPMethodMap as D, type EventAdapterInterface as E, type OCPPProtocol as F, type OCPPProtocolKey as G, type HandlerContext as H, type OCPPRequestType as I, type OCPPResponseType as J, OCPPRouter as K, type LoggerLike as L, type MiddlewareFunction as M, NOREPLY as N, type OCPPPlugin as O, OCPPServer as P, OCPPServerClient as Q, type RateLimitOptions as R, type RouterConfig as S, SecurityProfile as T, type ServerEvents as U, Validator as V, type ServerOptions as W, type SessionData as X, type TLSOptions as Y, type TypedEventEmitter as Z, type WildcardHandler as _, type LoggingConfig as a, createValidator as a0, type MiddlewareContext as b, type AllMethodNames as c, type AnyOCPPProtocol as d, type AuthAccept as e, type CORSOptions as f, type CallHandler as g, type CallOptions as h, type ClientEvents as i, type ClientOptions as j, type CloseOptions as k, type CompressionOptions as l, type ConnectionContext as m, ConnectionState as n, type HandshakeInfo as o, type ListenOptions as p, MessageType as q, type MiddlewareNext as r, MiddlewareStack as s, type OCPP16Methods as t, type OCPP201Methods as u, type OCPP21Methods as v, type OCPPCall as w, type OCPPCallError as x, type OCPPCallResult as y, OCPPClient as z };
5993
+ export { type ServerEvents as $, type AllMethodNames as A, MessageType as B, type CallOptions as C, type MiddlewareNext as D, type EventAdapterInterface as E, MiddlewareStack as F, type OCPP16Methods as G, type HandlerContext as H, type OCPP201Methods as I, type OCPP21Methods as J, type OCPPCall as K, type LoggerLike as L, type MiddlewareFunction as M, NOREPLY as N, OCPPServer as O, type OCPPCallError as P, type OCPPCallResult as Q, OCPPClient as R, type OCPPMessage as S, type OCPPMethodMap as T, type OCPPProtocolKey as U, Validator as V, OCPPRouter as W, type RateLimitOptions as X, type RouterConfig as Y, type SecurityEvent as Z, SecurityProfile as _, OCPPServerClient as a, type ServerOptions as a0, type SessionData as a1, type TLSOptions as a2, type TelemetryConfig as a3, type TypedEventEmitter as a4, type WildcardHandler as a5, createRouter as a6, createValidator as a7, type OCPPServerStats as b, type OCPPProtocol as c, type OCPPRequestType as d, type OCPPResponseType as e, type CloseOptions as f, type AuthCallback as g, type LoggingConfig as h, type MiddlewareContext as i, type OCPPPlugin as j, type ConnectionMiddleware as k, type AnyOCPPProtocol as l, type AuthAccept as m, type AuthContext as n, type CORSOptions as o, type CallHandler as p, type ClientEvents as q, type ClientOptions as r, type CompressionOptions as s, type ConnectionContext as t, ConnectionState as u, type HandshakeInfo as v, type ListenOptions as w, type MessageDirection as x, type MessageEventContext as y, type MessageEventPayload as z };