ocpp-ws-io 2.1.15 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +271 -170
- package/dist/adapters/redis.d.mts +2 -2
- package/dist/adapters/redis.d.ts +2 -2
- package/dist/adapters/redis.js +1 -1
- package/dist/adapters/redis.mjs +1 -1
- package/dist/browser.d.mts +11 -0
- package/dist/browser.d.ts +11 -0
- package/dist/browser.js +1 -1
- package/dist/browser.mjs +1 -1
- package/dist/{index-s9f97CmV.d.ts → index-BefjKqkS.d.ts} +1 -1
- package/dist/{index-C0mn42-8.d.mts → index-Defn9aOS.d.mts} +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +7 -7
- package/dist/index.mjs +7 -7
- package/dist/plugins.d.mts +1017 -26
- package/dist/plugins.d.ts +1017 -26
- package/dist/plugins.js +1 -1
- package/dist/plugins.mjs +1 -1
- package/dist/{types-BZXEmDQ1.d.mts → types-BHIHsj__.d.mts} +144 -3
- package/dist/{types-BZXEmDQ1.d.ts → types-BHIHsj__.d.ts} +144 -3
- package/package.json +17 -13
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};
|
|
@@ -4735,6 +4735,16 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
|
|
|
4735
4735
|
sendRaw(message: string): void;
|
|
4736
4736
|
reconfigure(options: Partial<ClientOptions>): void;
|
|
4737
4737
|
protected _attachWebsocket(ws: WebSocket__default): void;
|
|
4738
|
+
/**
|
|
4739
|
+
* Build an enriched message event context from middleware context.
|
|
4740
|
+
* Adds timestamp and optional latency metadata.
|
|
4741
|
+
*/
|
|
4742
|
+
private _buildMessageEventContext;
|
|
4743
|
+
/**
|
|
4744
|
+
* Emit a message event with enriched payload (direction + context).
|
|
4745
|
+
* Replaces separate call/callResult/callError events for unified observability.
|
|
4746
|
+
*/
|
|
4747
|
+
private _emitMessageEvent;
|
|
4738
4748
|
protected _onMessage(rawData: WebSocket__default.RawData, preParsed?: unknown): void;
|
|
4739
4749
|
private _handleIncomingCall;
|
|
4740
4750
|
private _handleCallResult;
|
|
@@ -4762,6 +4772,11 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
|
|
|
4762
4772
|
private _callWithRetry;
|
|
4763
4773
|
/** Maximum bytes allowed in the ws send buffer before applying backpressure (512KB) */
|
|
4764
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>;
|
|
4765
4780
|
/**
|
|
4766
4781
|
* Wraps ws.send() with backpressure protection.
|
|
4767
4782
|
* If bufferedAmount exceeds the threshold, waits for the buffer to drain
|
|
@@ -4826,6 +4841,8 @@ declare class WorkerPool {
|
|
|
4826
4841
|
declare class OCPPServerClient extends OCPPClient {
|
|
4827
4842
|
private _serverSession;
|
|
4828
4843
|
private _serverHandshake;
|
|
4844
|
+
/** Plugins passed from OCPPServer for hook execution */
|
|
4845
|
+
private _serverPlugins;
|
|
4829
4846
|
constructor(options: ClientOptions, context: {
|
|
4830
4847
|
ws: WebSocket$1;
|
|
4831
4848
|
handshake: HandshakeInfo;
|
|
@@ -4835,11 +4852,14 @@ declare class OCPPServerClient extends OCPPClient {
|
|
|
4835
4852
|
adaptiveMultiplier?: () => number;
|
|
4836
4853
|
/** Optional worker pool for off-thread JSON parsing */
|
|
4837
4854
|
workerPool?: WorkerPool;
|
|
4855
|
+
/** Plugins from the server for hook execution */
|
|
4856
|
+
plugins?: OCPPPlugin[];
|
|
4838
4857
|
});
|
|
4839
4858
|
private _rateLimits;
|
|
4840
4859
|
private _adaptiveMultiplier;
|
|
4841
4860
|
private _workerPool;
|
|
4842
4861
|
private _checkRateLimit;
|
|
4862
|
+
protected _invokeBeforeSend(message: OCPPMessage): boolean | Promise<boolean>;
|
|
4843
4863
|
private _attachServerWebsocket;
|
|
4844
4864
|
private _handleRateLimitExceeded;
|
|
4845
4865
|
/**
|
|
@@ -4899,6 +4919,7 @@ declare class OCPPServer extends OCPPServer_base {
|
|
|
4899
4919
|
private _adaptiveLimiter;
|
|
4900
4920
|
private _plugins;
|
|
4901
4921
|
private _workerPool;
|
|
4922
|
+
private _telemetryInterval;
|
|
4902
4923
|
private readonly _nodeId;
|
|
4903
4924
|
private _sessions;
|
|
4904
4925
|
private _gcInterval;
|
|
@@ -4977,6 +4998,11 @@ declare class OCPPServer extends OCPPServer_base {
|
|
|
4977
4998
|
* ```
|
|
4978
4999
|
*/
|
|
4979
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;
|
|
4980
5006
|
/**
|
|
4981
5007
|
* Registers middleware chain(s) as a wildcard/catch-all router.
|
|
4982
5008
|
*
|
|
@@ -5621,6 +5647,15 @@ interface ServerOptionsBase {
|
|
|
5621
5647
|
* (default: false)
|
|
5622
5648
|
*/
|
|
5623
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;
|
|
5624
5659
|
}
|
|
5625
5660
|
/** When strictMode is enabled, protocols MUST be specified */
|
|
5626
5661
|
interface StrictServerOptions extends ServerOptionsBase {
|
|
@@ -5633,6 +5668,13 @@ interface RelaxedServerOptions extends ServerOptionsBase {
|
|
|
5633
5668
|
protocols?: AnyOCPPProtocol[];
|
|
5634
5669
|
}
|
|
5635
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
|
+
}
|
|
5636
5678
|
interface OCPPServerStats {
|
|
5637
5679
|
/** Number of currently connected WebSockets */
|
|
5638
5680
|
connectedClients: number;
|
|
@@ -5669,6 +5711,30 @@ interface AuthAccept<TSession = Record<string, unknown>> {
|
|
|
5669
5711
|
session?: TSession;
|
|
5670
5712
|
}
|
|
5671
5713
|
type AuthCallback<TSession = Record<string, unknown>> = (ctx: AuthContext<TSession>) => void | Promise<void>;
|
|
5714
|
+
/** Indicates whether a message is incoming (from peer) or outgoing (to peer) */
|
|
5715
|
+
type MessageDirection = "IN" | "OUT";
|
|
5716
|
+
/**
|
|
5717
|
+
* Enriched context for message events, combining metadata from middleware
|
|
5718
|
+
* and message flow tracking. Uses type intersection since MiddlewareContext is a union.
|
|
5719
|
+
*/
|
|
5720
|
+
type MessageEventContext = MiddlewareContext & {
|
|
5721
|
+
/** Message timestamp (ISO 8601) */
|
|
5722
|
+
timestamp: string;
|
|
5723
|
+
/** Latency in milliseconds (only for responses/results) */
|
|
5724
|
+
latencyMs?: number;
|
|
5725
|
+
};
|
|
5726
|
+
/**
|
|
5727
|
+
* Enriched message event payload with direction and context.
|
|
5728
|
+
* Replaces the simple OCPPMessage tuple for better observability.
|
|
5729
|
+
*/
|
|
5730
|
+
interface MessageEventPayload {
|
|
5731
|
+
/** The raw OCPP message */
|
|
5732
|
+
message: OCPPMessage;
|
|
5733
|
+
/** Direction of the message: IN (from peer) or OUT (to peer) */
|
|
5734
|
+
direction: MessageDirection;
|
|
5735
|
+
/** Enriched context with metadata */
|
|
5736
|
+
ctx: MessageEventContext;
|
|
5737
|
+
}
|
|
5672
5738
|
interface ClientEvents {
|
|
5673
5739
|
open: [{
|
|
5674
5740
|
response: IncomingMessage;
|
|
@@ -5689,7 +5755,7 @@ interface ClientEvents {
|
|
|
5689
5755
|
attempt: number;
|
|
5690
5756
|
delay: number;
|
|
5691
5757
|
}];
|
|
5692
|
-
message: [
|
|
5758
|
+
message: [MessageEventPayload];
|
|
5693
5759
|
call: [OCPPCall];
|
|
5694
5760
|
callResult: [OCPPCallResult];
|
|
5695
5761
|
callError: [OCPPCallError];
|
|
@@ -5697,6 +5763,20 @@ interface ClientEvents {
|
|
|
5697
5763
|
message: string;
|
|
5698
5764
|
error: Error;
|
|
5699
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
|
+
}];
|
|
5700
5780
|
ping: [];
|
|
5701
5781
|
pong: [];
|
|
5702
5782
|
strictValidationFailure: [{
|
|
@@ -5711,7 +5791,7 @@ interface ClientEvents {
|
|
|
5711
5791
|
*/
|
|
5712
5792
|
interface SecurityEvent {
|
|
5713
5793
|
/** Event type identifier */
|
|
5714
|
-
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";
|
|
5715
5795
|
/** Station identity (if known) */
|
|
5716
5796
|
identity?: string;
|
|
5717
5797
|
/** Remote IP address */
|
|
@@ -5740,6 +5820,8 @@ interface ServerEvents {
|
|
|
5740
5820
|
close: [];
|
|
5741
5821
|
/** I3: Structured security event for SIEM/audit pipelines */
|
|
5742
5822
|
securityEvent: [SecurityEvent];
|
|
5823
|
+
/** Enriched message event with direction and context */
|
|
5824
|
+
message: [MessageEventPayload];
|
|
5743
5825
|
connection: [
|
|
5744
5826
|
socket: WebSocket.WebSocket,
|
|
5745
5827
|
request: node_http.IncomingMessage
|
|
@@ -5803,6 +5885,54 @@ interface OCPPPlugin {
|
|
|
5803
5885
|
onDisconnect?(client: OCPPServerClient, code: number, reason: string): void;
|
|
5804
5886
|
/** Called during server.close() for plugin cleanup */
|
|
5805
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>;
|
|
5806
5936
|
}
|
|
5807
5937
|
declare const NOREPLY: unique symbol;
|
|
5808
5938
|
type MiddlewareContext = {
|
|
@@ -5827,6 +5957,17 @@ type MiddlewareContext = {
|
|
|
5827
5957
|
messageId: string;
|
|
5828
5958
|
error: OCPPCallError;
|
|
5829
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;
|
|
5830
5971
|
};
|
|
5831
5972
|
|
|
5832
5973
|
interface BaseConnectionContext {
|
|
@@ -5849,4 +5990,4 @@ interface AuthContext<TSession = Record<string, unknown>> extends BaseConnection
|
|
|
5849
5990
|
}
|
|
5850
5991
|
type ConnectionMiddleware = (ctx: ConnectionContext) => Promise<void> | void;
|
|
5851
5992
|
|
|
5852
|
-
export {
|
|
5993
|
+
export { type ServerOptions as $, type AuthCallback as A, type OCPPCallError as B, type ConnectionMiddleware as C, type OCPPCallResult as D, type EventAdapterInterface as E, OCPPClient as F, type OCPPMessage as G, type HandlerContext as H, type OCPPMethodMap as I, type OCPPProtocol as J, type OCPPProtocolKey as K, type LoggerLike as L, type MiddlewareFunction as M, NOREPLY as N, type OCPPPlugin as O, type OCPPRequestType as P, type OCPPResponseType as Q, OCPPRouter as R, OCPPServer as S, OCPPServerClient as T, type OCPPServerStats as U, Validator as V, type RateLimitOptions as W, type RouterConfig as X, type SecurityEvent as Y, SecurityProfile as Z, type ServerEvents as _, type LoggingConfig as a, type SessionData as a0, type TLSOptions as a1, type TelemetryConfig as a2, type TypedEventEmitter as a3, type WildcardHandler as a4, createRouter as a5, createValidator as a6, 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, type MessageDirection as q, type MessageEventContext as r, type MessageEventPayload as s, MessageType as t, type MiddlewareNext as u, MiddlewareStack as v, type OCPP16Methods as w, type OCPP201Methods as x, type OCPP21Methods as y, type OCPPCall as z };
|