ocpp-ws-io 2.2.4 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4465,6 +4465,11 @@ declare class OCPPRouter extends OCPPRouter_base {
4465
4465
  * @internal
4466
4466
  */
4467
4467
  _regexPatterns: CompiledRegexPattern[];
4468
+ /**
4469
+ * @internal Set by OCPPServer when the router is registered, so patterns
4470
+ * added after registration still reach the trie / regex list.
4471
+ */
4472
+ _onPatternAdded?: (pattern: string | RegExp) => void;
4468
4473
  constructor(patterns?: Array<string | RegExp>, middlewares?: ConnectionMiddleware[]);
4469
4474
  /**
4470
4475
  * Appends URL paths or regular expressions to this router's match condition.
@@ -4573,7 +4578,7 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4573
4578
  private _pendingResponses;
4574
4579
  private _callQueue;
4575
4580
  private _pingTimer;
4576
- private _pongTimer;
4581
+ protected _pongTimer: ReturnType<typeof setTimeout> | null;
4577
4582
  private _closePromise;
4578
4583
  private _reconnectAttempt;
4579
4584
  private _reconnectTimer;
@@ -4608,6 +4613,11 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4608
4613
  * The connection endpoint URL.
4609
4614
  */
4610
4615
  get endpoint(): string;
4616
+ /**
4617
+ * Bytes currently queued in the underlying WebSocket send buffer
4618
+ * (0 when disconnected). Useful for backpressure monitoring and drain checks.
4619
+ */
4620
+ get bufferedAmount(): number;
4611
4621
  /**
4612
4622
  * The current configuration options for this client.
4613
4623
  */
@@ -4690,6 +4700,11 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4690
4700
  * Remove a registered handler for a specific version and method.
4691
4701
  */
4692
4702
  removeHandler(version: OCPPProtocol, method: string): void;
4703
+ /**
4704
+ * Check whether a handler is registered for a method
4705
+ * (optionally version-scoped, matching the handle() overloads).
4706
+ */
4707
+ hasHandler(method: string, version?: string): boolean;
4693
4708
  /**
4694
4709
  * Remove all registered handlers for this client, including the wildcard handler.
4695
4710
  */
@@ -4715,6 +4730,12 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4715
4730
  call<M extends AllMethodNames<P>>(method: M, params: OCPPRequestType<P, M>, options?: CallOptions): Promise<OCPPResponseType<P, M>>;
4716
4731
  /** Call a known typed method with explicit response type. */
4717
4732
  call<TResult = unknown>(method: string, params?: Record<string, unknown>, options?: CallOptions): Promise<TResult>;
4733
+ /**
4734
+ * Execute a call immediately, bypassing the callConcurrency queue.
4735
+ * Used by OCPPServer.sendBatch to pipeline warm-up calls without
4736
+ * mutating the client's configured concurrency (report M9).
4737
+ */
4738
+ callImmediate<TResult = unknown>(method: string, params?: Record<string, unknown>, options?: CallOptions): Promise<TResult>;
4718
4739
  /**
4719
4740
  * Version-specific safe call. Returns `undefined` on error instead of throwing.
4720
4741
  */
@@ -4754,7 +4775,7 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4754
4775
  * Reject all in-flight calls and clear pending state.
4755
4776
  */
4756
4777
  private _rejectPendingCalls;
4757
- private _onClose;
4778
+ protected _onClose(code: number, reason: Buffer): void;
4758
4779
  /** Errors that should stop reconnection immediately */
4759
4780
  private static readonly _INTOLERABLE_ERRORS;
4760
4781
  private _scheduleReconnect;
@@ -4772,20 +4793,27 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4772
4793
  private _callWithRetry;
4773
4794
  /** Maximum bytes allowed in the ws send buffer before applying backpressure (512KB) */
4774
4795
  private static readonly _BACKPRESSURE_THRESHOLD;
4796
+ /** Sends queued while the socket is backpressured (FIFO). */
4797
+ private _backpressureQueue;
4798
+ private _backpressureTimer;
4775
4799
  /**
4776
4800
  * Protected hook for plugins to intercept outbound messages before serialization.
4777
4801
  * Return `false` to suppress the message transmission.
4778
4802
  */
4779
4803
  protected _invokeBeforeSend(_message: OCPPMessage): boolean | Promise<boolean>;
4780
4804
  /**
4781
- * Wraps ws.send() with backpressure protection.
4782
- * If bufferedAmount exceeds the threshold, waits for the buffer to drain
4783
- * before sending. Prevents OOM on slow 2G/3G charger connections.
4805
+ * Wraps ws.send() with backpressure protection. When bufferedAmount
4806
+ * exceeds the threshold, sends are queued and flushed FIFO by a single
4807
+ * shared 50ms drain timer (one per client, not one per send — report M10).
4808
+ * Entries older than 10s are sent regardless, preserving the previous
4809
+ * timeout semantics. Prevents OOM on slow 2G/3G charger connections.
4784
4810
  */
4785
4811
  private _safeSend;
4786
- private _startPing;
4812
+ private _startBackpressureDrain;
4813
+ private _stopBackpressureDrain;
4814
+ protected _startPing(): void;
4787
4815
  private _stopPing;
4788
- private _recordActivity;
4816
+ protected _recordActivity(): void;
4789
4817
  private _setupValidators;
4790
4818
  private _validateOutbound;
4791
4819
  private _validateInbound;
@@ -4800,6 +4828,8 @@ interface WorkerPoolOptions {
4800
4828
  poolSize?: number;
4801
4829
  /** Max pending parse jobs before rejecting (default: 10000) */
4802
4830
  maxQueueSize?: number;
4831
+ /** Override the worker entry path (used by tests; defaults to parse-worker.cjs next to this file) */
4832
+ workerPath?: string;
4803
4833
  }
4804
4834
  interface ParseResult {
4805
4835
  message: unknown;
@@ -4867,9 +4897,17 @@ declare class OCPPServerClient extends OCPPClient {
4867
4897
  private _rateLimits;
4868
4898
  private _adaptiveMultiplier;
4869
4899
  private _workerPool;
4900
+ /**
4901
+ * Per-connection inbound pipeline. Serializes async pre-processing
4902
+ * (plugin onBeforeReceive, rate-limit parse, worker-pool parse) so
4903
+ * messages are dispatched in wire order — OCPP transaction semantics
4904
+ * depend on it (e.g. StartTransaction before StopTransaction).
4905
+ */
4906
+ private _inboundChain;
4870
4907
  private _checkRateLimit;
4871
4908
  protected _invokeBeforeSend(message: OCPPMessage): boolean | Promise<boolean>;
4872
4909
  private _attachServerWebsocket;
4910
+ private _processInboundMessage;
4873
4911
  private _handleRateLimitExceeded;
4874
4912
  /**
4875
4913
  * Session data associated with this client connection.
@@ -4918,6 +4956,10 @@ declare class OCPPServer extends OCPPServer_base {
4918
4956
  private _clients;
4919
4957
  private _clientsByIdentity;
4920
4958
  private _httpServers;
4959
+ /** HTTP servers created by listen() — we own their lifecycle. */
4960
+ private _ownedHttpServers;
4961
+ /** Listeners we attached per server, for removal on close(). */
4962
+ private _attachedHttpHandlers;
4921
4963
  private _wss;
4922
4964
  private _state;
4923
4965
  private _adapter;
@@ -4929,7 +4971,12 @@ declare class OCPPServer extends OCPPServer_base {
4929
4971
  private _plugins;
4930
4972
  private _workerPool;
4931
4973
  private _telemetryInterval;
4974
+ private _presenceInterval;
4932
4975
  private readonly _nodeId;
4976
+ /** Pending cross-node RPC calls awaiting a correlated response. */
4977
+ private _pendingRemoteCalls;
4978
+ /** Extra wait on top of the call timeout to absorb cross-node transit. */
4979
+ private static readonly _REMOTE_RESPONSE_GRACE_MS;
4933
4980
  private _sessions;
4934
4981
  private _gcInterval;
4935
4982
  private readonly _sessionTimeoutMs;
@@ -5073,21 +5120,25 @@ declare class OCPPServer extends OCPPServer_base {
5073
5120
  */
5074
5121
  private _handleUpgrade;
5075
5122
  private _updateSessionActivity;
5123
+ /**
5124
+ * Evict per-IP connection buckets that have been idle longer than the
5125
+ * rate-limit window. Recreating a bucket grants a full token allowance,
5126
+ * which is exactly what a full refill after `windowMs` idle would yield —
5127
+ * so eviction is behavior-preserving while bounding memory (report H4).
5128
+ */
5129
+ private _sweepConnectionBuckets;
5076
5130
  close(options?: CloseOptions): Promise<void>;
5077
5131
  reconfigure(options: Partial<ServerOptions>): void;
5078
5132
  /**
5079
5133
  * Send a request to a specific client (local or remote).
5080
5134
  *
5081
- * 1. Checks local clients.
5082
- * 2. Checks Presence Registry -> Unicast.
5083
- * 3. Fallback: Broadcast.
5084
- */
5085
- /**
5086
- * Send a request to a specific client (local or remote).
5135
+ * 1. Local clients are called directly.
5136
+ * 2. Otherwise the presence registry routes the call to the owning node,
5137
+ * and the response is correlated back over the adapter (cross-node RPC).
5138
+ * 3. Unknown identity → rejects with "Client <identity> not found".
5087
5139
  *
5088
- * 1. Checks local clients.
5089
- * 2. Checks Presence Registry -> Unicast.
5090
- * 3. Fallback: Error (Client not found).
5140
+ * Backward compatibility: nodes running older versions deliver the call
5141
+ * but never publish a response — such calls reject with TimeoutError.
5091
5142
  */
5092
5143
  sendToClient<V extends OCPPProtocol, M extends AllMethodNames<V>>(identity: string, version: V, method: M, params: OCPPRequestType<V, M>, options?: CallOptions): Promise<OCPPResponseType<V, M> | undefined>;
5093
5144
  sendToClient<M extends AllMethodNames<any>>(identity: string, method: M, params: OCPPRequestType<any, M>, options?: CallOptions): Promise<OCPPResponseType<any, M> | undefined>;
@@ -5120,8 +5171,12 @@ declare class OCPPServer extends OCPPServer_base {
5120
5171
  options?: CallOptions;
5121
5172
  }>): Promise<Array<unknown | undefined>>;
5122
5173
  setAdapter(adapter: EventAdapterInterface): Promise<void>;
5174
+ private _startPresenceRefresh;
5175
+ private _refreshPresence;
5123
5176
  private _onBroadcast;
5124
5177
  private _onUnicast;
5178
+ /** Publish the result of a remotely requested call back to the origin node. */
5179
+ private _publishRemoteResult;
5125
5180
  publish(channel: string, data: unknown): Promise<void>;
5126
5181
  broadcast<V extends AllMethodNames<any>>(method: V, params: OCPPRequestType<any, V>): Promise<void>;
5127
5182
  /**
@@ -5179,9 +5234,11 @@ declare class Validator {
5179
5234
  hasSchema(schemaId: string): boolean;
5180
5235
  }
5181
5236
  /**
5182
- * Create or retrieve a cached validator for a specific subprotocol version.
5183
- * E5: Returns an existing instance if one was already created for this subprotocol,
5184
- * preventing duplicate AJV instances and saving memory.
5237
+ * Create a validator for a specific subprotocol version.
5238
+ *
5239
+ * Always returns a fresh instance so custom schema sets are never shadowed
5240
+ * by previously created validators for the same subprotocol (report M8).
5241
+ * Standard validators are cached separately by getStandardValidators().
5185
5242
  */
5186
5243
  declare function createValidator(subprotocol: string, schemas: ValidatorSchema[]): Validator;
5187
5244
 
@@ -5458,7 +5515,11 @@ interface ClientOptions {
5458
5515
  strictModeMethods?: Array<AllMethodNames<OCPPProtocol>>;
5459
5516
  /** Custom validators for strict mode */
5460
5517
  strictModeValidators?: Validator[];
5461
- /** Max number of bad messages before closing (default: Infinity) */
5518
+ /**
5519
+ * Max number of bad messages before closing.
5520
+ * Default: Infinity (never disconnects on bad messages) — set a finite
5521
+ * value in production.
5522
+ */
5462
5523
  maxBadMessages?: number;
5463
5524
  /** Include error details in responses (default: false) */
5464
5525
  respondWithDetailedErrors?: boolean;
@@ -5581,6 +5642,13 @@ interface CORSOptions {
5581
5642
  allowedOrigins?: string[];
5582
5643
  /** Allowed WebSocket protocol schemes */
5583
5644
  allowedSchemes?: ("ws" | "wss")[];
5645
+ /**
5646
+ * Honor `X-Forwarded-Proto` from a reverse proxy when evaluating
5647
+ * `allowedSchemes`. Leave false (default) unless the server is only
5648
+ * reachable through a trusted proxy — otherwise clients can spoof the
5649
+ * header to bypass wss-only rules.
5650
+ */
5651
+ trustProxy?: boolean;
5584
5652
  }
5585
5653
  interface ServerOptionsBase {
5586
5654
  /** OCPP Security Profile (default: NONE) */
@@ -5601,7 +5669,11 @@ interface ServerOptionsBase {
5601
5669
  strictModeValidators?: Validator[];
5602
5670
  /** Rate Limiting configuration — inherited */
5603
5671
  rateLimit?: RateLimitOptions;
5604
- /** Max bad messages — inherited (default: Infinity) */
5672
+ /**
5673
+ * Max bad messages — inherited.
5674
+ * Default: Infinity (never disconnects on bad messages) — set a finite
5675
+ * value in production.
5676
+ */
5605
5677
  maxBadMessages?: number;
5606
5678
  /** Include error details in responses — inherited (default: false) */
5607
5679
  respondWithDetailedErrors?: boolean;
@@ -5610,6 +5682,12 @@ interface ServerOptionsBase {
5610
5682
  * (default: 7200000 / 2 hours)
5611
5683
  */
5612
5684
  sessionTtlMs?: number;
5685
+ /**
5686
+ * TTL (seconds) for cluster presence registry entries, and the basis for
5687
+ * the automatic presence heartbeat (refreshed every ttl/2 while clients
5688
+ * are connected). Default: 300.
5689
+ */
5690
+ presenceTtlSeconds?: number;
5613
5691
  /**
5614
5692
  * Maximum time (ms) to wait for the auth callback to resolve during
5615
5693
  * a WebSocket upgrade handshake. If the callback does not settle within
@@ -5640,10 +5718,20 @@ interface ServerOptionsBase {
5640
5718
  * (default: 50000)
5641
5719
  */
5642
5720
  maxSessions?: number;
5721
+ /**
5722
+ * Hard cap on concurrent client connections, enforced before the TLS/auth
5723
+ * handshake work is done (the connection-guard plugin only closes after
5724
+ * the fact). Excess upgrades are rejected with HTTP 503.
5725
+ */
5726
+ maxConnections?: number;
5643
5727
  /**
5644
5728
  * Enable the built-in HTTP health/metrics endpoint.
5645
5729
  * When enabled, non-upgrade HTTP requests to `/health` return a JSON health check,
5646
5730
  * and requests to `/metrics` return Prometheus-compatible text metrics.
5731
+ * When attaching to a user-provided server (listen(..., { server })),
5732
+ * only /health and /metrics are handled; all other routes are left to
5733
+ * the application, and close() will not close the external server.
5734
+ * Ensure your app does not also write responses for /health or /metrics.
5647
5735
  * (default: false)
5648
5736
  */
5649
5737
  healthEndpoint?: boolean;
@@ -5818,7 +5906,7 @@ interface ClientEvents {
5818
5906
  */
5819
5907
  interface SecurityEvent {
5820
5908
  /** Event type identifier */
5821
- 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";
5909
+ type: "AUTH_FAILED" | "RATE_LIMIT_EXCEEDED" | "UPGRADE_ABORTED" | "CONNECTION_RATE_LIMIT" | "CONNECTION_LIMIT" | "INVALID_PAYLOAD" | "ANOMALY_RAPID_RECONNECT" | "ANOMALY_AUTH_BRUTE_FORCE" | "ANOMALY_MESSAGE_FUZZING" | "ANOMALY_IDENTITY_COLLISION";
5822
5910
  /** Station identity (if known) */
5823
5911
  identity?: string;
5824
5912
  /** Remote IP address */
@@ -4465,6 +4465,11 @@ declare class OCPPRouter extends OCPPRouter_base {
4465
4465
  * @internal
4466
4466
  */
4467
4467
  _regexPatterns: CompiledRegexPattern[];
4468
+ /**
4469
+ * @internal Set by OCPPServer when the router is registered, so patterns
4470
+ * added after registration still reach the trie / regex list.
4471
+ */
4472
+ _onPatternAdded?: (pattern: string | RegExp) => void;
4468
4473
  constructor(patterns?: Array<string | RegExp>, middlewares?: ConnectionMiddleware[]);
4469
4474
  /**
4470
4475
  * Appends URL paths or regular expressions to this router's match condition.
@@ -4573,7 +4578,7 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4573
4578
  private _pendingResponses;
4574
4579
  private _callQueue;
4575
4580
  private _pingTimer;
4576
- private _pongTimer;
4581
+ protected _pongTimer: ReturnType<typeof setTimeout> | null;
4577
4582
  private _closePromise;
4578
4583
  private _reconnectAttempt;
4579
4584
  private _reconnectTimer;
@@ -4608,6 +4613,11 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4608
4613
  * The connection endpoint URL.
4609
4614
  */
4610
4615
  get endpoint(): string;
4616
+ /**
4617
+ * Bytes currently queued in the underlying WebSocket send buffer
4618
+ * (0 when disconnected). Useful for backpressure monitoring and drain checks.
4619
+ */
4620
+ get bufferedAmount(): number;
4611
4621
  /**
4612
4622
  * The current configuration options for this client.
4613
4623
  */
@@ -4690,6 +4700,11 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4690
4700
  * Remove a registered handler for a specific version and method.
4691
4701
  */
4692
4702
  removeHandler(version: OCPPProtocol, method: string): void;
4703
+ /**
4704
+ * Check whether a handler is registered for a method
4705
+ * (optionally version-scoped, matching the handle() overloads).
4706
+ */
4707
+ hasHandler(method: string, version?: string): boolean;
4693
4708
  /**
4694
4709
  * Remove all registered handlers for this client, including the wildcard handler.
4695
4710
  */
@@ -4715,6 +4730,12 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4715
4730
  call<M extends AllMethodNames<P>>(method: M, params: OCPPRequestType<P, M>, options?: CallOptions): Promise<OCPPResponseType<P, M>>;
4716
4731
  /** Call a known typed method with explicit response type. */
4717
4732
  call<TResult = unknown>(method: string, params?: Record<string, unknown>, options?: CallOptions): Promise<TResult>;
4733
+ /**
4734
+ * Execute a call immediately, bypassing the callConcurrency queue.
4735
+ * Used by OCPPServer.sendBatch to pipeline warm-up calls without
4736
+ * mutating the client's configured concurrency (report M9).
4737
+ */
4738
+ callImmediate<TResult = unknown>(method: string, params?: Record<string, unknown>, options?: CallOptions): Promise<TResult>;
4718
4739
  /**
4719
4740
  * Version-specific safe call. Returns `undefined` on error instead of throwing.
4720
4741
  */
@@ -4754,7 +4775,7 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4754
4775
  * Reject all in-flight calls and clear pending state.
4755
4776
  */
4756
4777
  private _rejectPendingCalls;
4757
- private _onClose;
4778
+ protected _onClose(code: number, reason: Buffer): void;
4758
4779
  /** Errors that should stop reconnection immediately */
4759
4780
  private static readonly _INTOLERABLE_ERRORS;
4760
4781
  private _scheduleReconnect;
@@ -4772,20 +4793,27 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4772
4793
  private _callWithRetry;
4773
4794
  /** Maximum bytes allowed in the ws send buffer before applying backpressure (512KB) */
4774
4795
  private static readonly _BACKPRESSURE_THRESHOLD;
4796
+ /** Sends queued while the socket is backpressured (FIFO). */
4797
+ private _backpressureQueue;
4798
+ private _backpressureTimer;
4775
4799
  /**
4776
4800
  * Protected hook for plugins to intercept outbound messages before serialization.
4777
4801
  * Return `false` to suppress the message transmission.
4778
4802
  */
4779
4803
  protected _invokeBeforeSend(_message: OCPPMessage): boolean | Promise<boolean>;
4780
4804
  /**
4781
- * Wraps ws.send() with backpressure protection.
4782
- * If bufferedAmount exceeds the threshold, waits for the buffer to drain
4783
- * before sending. Prevents OOM on slow 2G/3G charger connections.
4805
+ * Wraps ws.send() with backpressure protection. When bufferedAmount
4806
+ * exceeds the threshold, sends are queued and flushed FIFO by a single
4807
+ * shared 50ms drain timer (one per client, not one per send — report M10).
4808
+ * Entries older than 10s are sent regardless, preserving the previous
4809
+ * timeout semantics. Prevents OOM on slow 2G/3G charger connections.
4784
4810
  */
4785
4811
  private _safeSend;
4786
- private _startPing;
4812
+ private _startBackpressureDrain;
4813
+ private _stopBackpressureDrain;
4814
+ protected _startPing(): void;
4787
4815
  private _stopPing;
4788
- private _recordActivity;
4816
+ protected _recordActivity(): void;
4789
4817
  private _setupValidators;
4790
4818
  private _validateOutbound;
4791
4819
  private _validateInbound;
@@ -4800,6 +4828,8 @@ interface WorkerPoolOptions {
4800
4828
  poolSize?: number;
4801
4829
  /** Max pending parse jobs before rejecting (default: 10000) */
4802
4830
  maxQueueSize?: number;
4831
+ /** Override the worker entry path (used by tests; defaults to parse-worker.cjs next to this file) */
4832
+ workerPath?: string;
4803
4833
  }
4804
4834
  interface ParseResult {
4805
4835
  message: unknown;
@@ -4867,9 +4897,17 @@ declare class OCPPServerClient extends OCPPClient {
4867
4897
  private _rateLimits;
4868
4898
  private _adaptiveMultiplier;
4869
4899
  private _workerPool;
4900
+ /**
4901
+ * Per-connection inbound pipeline. Serializes async pre-processing
4902
+ * (plugin onBeforeReceive, rate-limit parse, worker-pool parse) so
4903
+ * messages are dispatched in wire order — OCPP transaction semantics
4904
+ * depend on it (e.g. StartTransaction before StopTransaction).
4905
+ */
4906
+ private _inboundChain;
4870
4907
  private _checkRateLimit;
4871
4908
  protected _invokeBeforeSend(message: OCPPMessage): boolean | Promise<boolean>;
4872
4909
  private _attachServerWebsocket;
4910
+ private _processInboundMessage;
4873
4911
  private _handleRateLimitExceeded;
4874
4912
  /**
4875
4913
  * Session data associated with this client connection.
@@ -4918,6 +4956,10 @@ declare class OCPPServer extends OCPPServer_base {
4918
4956
  private _clients;
4919
4957
  private _clientsByIdentity;
4920
4958
  private _httpServers;
4959
+ /** HTTP servers created by listen() — we own their lifecycle. */
4960
+ private _ownedHttpServers;
4961
+ /** Listeners we attached per server, for removal on close(). */
4962
+ private _attachedHttpHandlers;
4921
4963
  private _wss;
4922
4964
  private _state;
4923
4965
  private _adapter;
@@ -4929,7 +4971,12 @@ declare class OCPPServer extends OCPPServer_base {
4929
4971
  private _plugins;
4930
4972
  private _workerPool;
4931
4973
  private _telemetryInterval;
4974
+ private _presenceInterval;
4932
4975
  private readonly _nodeId;
4976
+ /** Pending cross-node RPC calls awaiting a correlated response. */
4977
+ private _pendingRemoteCalls;
4978
+ /** Extra wait on top of the call timeout to absorb cross-node transit. */
4979
+ private static readonly _REMOTE_RESPONSE_GRACE_MS;
4933
4980
  private _sessions;
4934
4981
  private _gcInterval;
4935
4982
  private readonly _sessionTimeoutMs;
@@ -5073,21 +5120,25 @@ declare class OCPPServer extends OCPPServer_base {
5073
5120
  */
5074
5121
  private _handleUpgrade;
5075
5122
  private _updateSessionActivity;
5123
+ /**
5124
+ * Evict per-IP connection buckets that have been idle longer than the
5125
+ * rate-limit window. Recreating a bucket grants a full token allowance,
5126
+ * which is exactly what a full refill after `windowMs` idle would yield —
5127
+ * so eviction is behavior-preserving while bounding memory (report H4).
5128
+ */
5129
+ private _sweepConnectionBuckets;
5076
5130
  close(options?: CloseOptions): Promise<void>;
5077
5131
  reconfigure(options: Partial<ServerOptions>): void;
5078
5132
  /**
5079
5133
  * Send a request to a specific client (local or remote).
5080
5134
  *
5081
- * 1. Checks local clients.
5082
- * 2. Checks Presence Registry -> Unicast.
5083
- * 3. Fallback: Broadcast.
5084
- */
5085
- /**
5086
- * Send a request to a specific client (local or remote).
5135
+ * 1. Local clients are called directly.
5136
+ * 2. Otherwise the presence registry routes the call to the owning node,
5137
+ * and the response is correlated back over the adapter (cross-node RPC).
5138
+ * 3. Unknown identity → rejects with "Client <identity> not found".
5087
5139
  *
5088
- * 1. Checks local clients.
5089
- * 2. Checks Presence Registry -> Unicast.
5090
- * 3. Fallback: Error (Client not found).
5140
+ * Backward compatibility: nodes running older versions deliver the call
5141
+ * but never publish a response — such calls reject with TimeoutError.
5091
5142
  */
5092
5143
  sendToClient<V extends OCPPProtocol, M extends AllMethodNames<V>>(identity: string, version: V, method: M, params: OCPPRequestType<V, M>, options?: CallOptions): Promise<OCPPResponseType<V, M> | undefined>;
5093
5144
  sendToClient<M extends AllMethodNames<any>>(identity: string, method: M, params: OCPPRequestType<any, M>, options?: CallOptions): Promise<OCPPResponseType<any, M> | undefined>;
@@ -5120,8 +5171,12 @@ declare class OCPPServer extends OCPPServer_base {
5120
5171
  options?: CallOptions;
5121
5172
  }>): Promise<Array<unknown | undefined>>;
5122
5173
  setAdapter(adapter: EventAdapterInterface): Promise<void>;
5174
+ private _startPresenceRefresh;
5175
+ private _refreshPresence;
5123
5176
  private _onBroadcast;
5124
5177
  private _onUnicast;
5178
+ /** Publish the result of a remotely requested call back to the origin node. */
5179
+ private _publishRemoteResult;
5125
5180
  publish(channel: string, data: unknown): Promise<void>;
5126
5181
  broadcast<V extends AllMethodNames<any>>(method: V, params: OCPPRequestType<any, V>): Promise<void>;
5127
5182
  /**
@@ -5179,9 +5234,11 @@ declare class Validator {
5179
5234
  hasSchema(schemaId: string): boolean;
5180
5235
  }
5181
5236
  /**
5182
- * Create or retrieve a cached validator for a specific subprotocol version.
5183
- * E5: Returns an existing instance if one was already created for this subprotocol,
5184
- * preventing duplicate AJV instances and saving memory.
5237
+ * Create a validator for a specific subprotocol version.
5238
+ *
5239
+ * Always returns a fresh instance so custom schema sets are never shadowed
5240
+ * by previously created validators for the same subprotocol (report M8).
5241
+ * Standard validators are cached separately by getStandardValidators().
5185
5242
  */
5186
5243
  declare function createValidator(subprotocol: string, schemas: ValidatorSchema[]): Validator;
5187
5244
 
@@ -5458,7 +5515,11 @@ interface ClientOptions {
5458
5515
  strictModeMethods?: Array<AllMethodNames<OCPPProtocol>>;
5459
5516
  /** Custom validators for strict mode */
5460
5517
  strictModeValidators?: Validator[];
5461
- /** Max number of bad messages before closing (default: Infinity) */
5518
+ /**
5519
+ * Max number of bad messages before closing.
5520
+ * Default: Infinity (never disconnects on bad messages) — set a finite
5521
+ * value in production.
5522
+ */
5462
5523
  maxBadMessages?: number;
5463
5524
  /** Include error details in responses (default: false) */
5464
5525
  respondWithDetailedErrors?: boolean;
@@ -5581,6 +5642,13 @@ interface CORSOptions {
5581
5642
  allowedOrigins?: string[];
5582
5643
  /** Allowed WebSocket protocol schemes */
5583
5644
  allowedSchemes?: ("ws" | "wss")[];
5645
+ /**
5646
+ * Honor `X-Forwarded-Proto` from a reverse proxy when evaluating
5647
+ * `allowedSchemes`. Leave false (default) unless the server is only
5648
+ * reachable through a trusted proxy — otherwise clients can spoof the
5649
+ * header to bypass wss-only rules.
5650
+ */
5651
+ trustProxy?: boolean;
5584
5652
  }
5585
5653
  interface ServerOptionsBase {
5586
5654
  /** OCPP Security Profile (default: NONE) */
@@ -5601,7 +5669,11 @@ interface ServerOptionsBase {
5601
5669
  strictModeValidators?: Validator[];
5602
5670
  /** Rate Limiting configuration — inherited */
5603
5671
  rateLimit?: RateLimitOptions;
5604
- /** Max bad messages — inherited (default: Infinity) */
5672
+ /**
5673
+ * Max bad messages — inherited.
5674
+ * Default: Infinity (never disconnects on bad messages) — set a finite
5675
+ * value in production.
5676
+ */
5605
5677
  maxBadMessages?: number;
5606
5678
  /** Include error details in responses — inherited (default: false) */
5607
5679
  respondWithDetailedErrors?: boolean;
@@ -5610,6 +5682,12 @@ interface ServerOptionsBase {
5610
5682
  * (default: 7200000 / 2 hours)
5611
5683
  */
5612
5684
  sessionTtlMs?: number;
5685
+ /**
5686
+ * TTL (seconds) for cluster presence registry entries, and the basis for
5687
+ * the automatic presence heartbeat (refreshed every ttl/2 while clients
5688
+ * are connected). Default: 300.
5689
+ */
5690
+ presenceTtlSeconds?: number;
5613
5691
  /**
5614
5692
  * Maximum time (ms) to wait for the auth callback to resolve during
5615
5693
  * a WebSocket upgrade handshake. If the callback does not settle within
@@ -5640,10 +5718,20 @@ interface ServerOptionsBase {
5640
5718
  * (default: 50000)
5641
5719
  */
5642
5720
  maxSessions?: number;
5721
+ /**
5722
+ * Hard cap on concurrent client connections, enforced before the TLS/auth
5723
+ * handshake work is done (the connection-guard plugin only closes after
5724
+ * the fact). Excess upgrades are rejected with HTTP 503.
5725
+ */
5726
+ maxConnections?: number;
5643
5727
  /**
5644
5728
  * Enable the built-in HTTP health/metrics endpoint.
5645
5729
  * When enabled, non-upgrade HTTP requests to `/health` return a JSON health check,
5646
5730
  * and requests to `/metrics` return Prometheus-compatible text metrics.
5731
+ * When attaching to a user-provided server (listen(..., { server })),
5732
+ * only /health and /metrics are handled; all other routes are left to
5733
+ * the application, and close() will not close the external server.
5734
+ * Ensure your app does not also write responses for /health or /metrics.
5647
5735
  * (default: false)
5648
5736
  */
5649
5737
  healthEndpoint?: boolean;
@@ -5818,7 +5906,7 @@ interface ClientEvents {
5818
5906
  */
5819
5907
  interface SecurityEvent {
5820
5908
  /** Event type identifier */
5821
- 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";
5909
+ type: "AUTH_FAILED" | "RATE_LIMIT_EXCEEDED" | "UPGRADE_ABORTED" | "CONNECTION_RATE_LIMIT" | "CONNECTION_LIMIT" | "INVALID_PAYLOAD" | "ANOMALY_RAPID_RECONNECT" | "ANOMALY_AUTH_BRUTE_FORCE" | "ANOMALY_MESSAGE_FUZZING" | "ANOMALY_IDENTITY_COLLISION";
5822
5910
  /** Station identity (if known) */
5823
5911
  identity?: string;
5824
5912
  /** Remote IP address */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocpp-ws-io",
3
- "version": "2.2.4",
3
+ "version": "2.3.0",
4
4
  "description": "OCPP RPC WebSocket client and server for OCPP 1.6J, 2.0.1, and 2.1. Type-safe TypeScript toolkit for EV charging, CSMS backends, Redis scaling, and protocol validation.",
5
5
  "repository": {
6
6
  "type": "git",