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/README.md +29 -0
- package/dist/adapters/redis.d.mts +2 -2
- package/dist/adapters/redis.d.ts +2 -2
- package/dist/adapters/redis.js +1 -1
- package/dist/adapters/redis.mjs +1 -1
- package/dist/browser.js +1 -1
- package/dist/browser.mjs +1 -1
- package/dist/context-Cy7YIKyU.d.mts +21 -0
- package/dist/context-DcTIzhq-.d.ts +21 -0
- package/dist/express.d.mts +70 -0
- package/dist/express.d.ts +70 -0
- package/dist/express.js +2 -0
- package/dist/express.mjs +2 -0
- package/dist/fastify.d.mts +37 -0
- package/dist/fastify.d.ts +37 -0
- package/dist/fastify.js +2 -0
- package/dist/fastify.mjs +2 -0
- package/dist/hono.d.mts +51 -0
- package/dist/hono.d.ts +51 -0
- package/dist/hono.js +2 -0
- package/dist/hono.mjs +2 -0
- package/dist/{index-B98n5Et3.d.mts → index-B9rTwvbn.d.mts} +1 -1
- package/dist/{index-xx7uU8pY.d.ts → index-D5pJ3wS4.d.ts} +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/nestjs.d.mts +169 -0
- package/dist/nestjs.d.ts +169 -0
- package/dist/nestjs.js +4826 -0
- package/dist/nestjs.mjs +4826 -0
- 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-BunMs45p.d.mts → types-xFfIgIuS.d.mts} +108 -3
- package/dist/{types-BunMs45p.d.ts → types-xFfIgIuS.d.ts} +108 -3
- package/package.json +174 -113
- package/dist/browser.d.mts +0 -4971
- package/dist/browser.d.ts +0 -4971
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 {
|
|
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 {
|
|
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 };
|