ocpp-ws-io 2.1.13 → 2.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"logger.js","sourcesContent":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"logger.mjs","sourcesContent":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/plugins/anomaly.ts","../src/plugins/connection-guard.ts","../src/plugins/heartbeat.ts","../src/plugins/metrics.ts","../src/plugins/otel.ts","../src/plugins/session-log.ts","../src/plugins/webhook.ts"],"names":["anomalyPlugin","options","threshold","windowMs","connectLog","server","gcTimer","pruneExpired","timestamps","now","cutoff","i","srv","identity","pruned","client","connectionGuardPlugin","activeCount","heartbeatPlugin","metricsPlugin","intervalMs","totalConnections","totalDisconnections","activeConnections","peakConnections","totalDurationMs","initTime","snapshotTimer","connectionTimes","getMetrics","startTime","otelPlugin","tracer","spans","createRequire","span","code","entry","durationMs","sessionLogPlugin","logger","reason","durationSec","webhookPlugin","allowedEvents","timeout","maxRetries","sendWebhook","payload","body","headers","signature","createHmac","attempt","controller","timer"],"mappings":"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","file":"plugins.js","sourcesContent":["import type { OCPPServer } from \"../server.js\";\nimport type { OCPPPlugin } from \"../types.js\";\n\n/**\n * Options for the anomaly detection plugin.\n */\nexport interface AnomalyPluginOptions {\n /**\n * Maximum number of connections from the same identity\n * within the sliding window before triggering an anomaly.\n * Default: 5\n */\n reconnectThreshold?: number;\n /**\n * Sliding window duration in milliseconds.\n * Default: 60_000 (1 minute)\n */\n windowMs?: number;\n}\n\n/**\n * Detects anomalous connection patterns such as rapid reconnections\n * from the same identity. Emits `securityEvent` on the server with\n * type `ANOMALY_RAPID_RECONNECT`.\n *\n * @example\n * ```ts\n * import { anomalyPlugin } from 'ocpp-ws-io/plugins';\n *\n * server.plugin(anomalyPlugin({\n * reconnectThreshold: 10,\n * windowMs: 60_000,\n * }));\n *\n * server.on('securityEvent', (evt) => {\n * if (evt.type === 'ANOMALY_RAPID_RECONNECT') {\n * console.warn(`Rapid reconnect storm: ${evt.identity}`);\n * }\n * });\n * ```\n */\nexport function anomalyPlugin(options?: AnomalyPluginOptions): OCPPPlugin {\n const threshold = options?.reconnectThreshold ?? 5;\n const windowMs = options?.windowMs ?? 60_000;\n\n /** Map of identity → array of connection timestamps */\n const connectLog = new Map<string, number[]>();\n let server: OCPPServer | null = null;\n let gcTimer: ReturnType<typeof setInterval> | null = null;\n\n function pruneExpired(timestamps: number[], now: number): number[] {\n const cutoff = now - windowMs;\n // Find first index that is within the window\n let i = 0;\n while (i < timestamps.length && timestamps[i] < cutoff) i++;\n return i > 0 ? timestamps.slice(i) : timestamps;\n }\n\n return {\n name: \"anomaly\",\n\n onInit(srv) {\n server = srv;\n // Periodic garbage collection of expired entries\n gcTimer = setInterval(() => {\n const now = Date.now();\n for (const [identity, timestamps] of connectLog) {\n const pruned = pruneExpired(timestamps, now);\n if (pruned.length === 0) {\n connectLog.delete(identity);\n } else {\n connectLog.set(identity, pruned);\n }\n }\n }, windowMs).unref();\n },\n\n onConnection(client) {\n const now = Date.now();\n const identity = client.identity;\n\n let timestamps = connectLog.get(identity) ?? [];\n timestamps = pruneExpired(timestamps, now);\n timestamps.push(now);\n connectLog.set(identity, timestamps);\n\n if (timestamps.length > threshold && server) {\n server.emit(\"securityEvent\" as any, {\n type: \"ANOMALY_RAPID_RECONNECT\",\n identity,\n ip: client.handshake.remoteAddress,\n timestamp: new Date().toISOString(),\n details: {\n connectionsInWindow: timestamps.length,\n threshold,\n windowMs,\n },\n });\n }\n },\n\n onClose() {\n if (gcTimer) {\n clearInterval(gcTimer);\n gcTimer = null;\n }\n connectLog.clear();\n server = null;\n },\n };\n}\n","import type { OCPPPlugin } from \"../types.js\";\n\n/**\n * Options for the connection guard plugin.\n */\nexport interface ConnectionGuardOptions {\n /** Maximum allowed concurrent connections. */\n maxConnections: number;\n}\n\n/**\n * Enforces a hard limit on concurrent connections.\n * New connections exceeding the limit are force-closed with code 4001.\n *\n * @example\n * ```ts\n * import { connectionGuardPlugin } from 'ocpp-ws-io/plugins';\n *\n * server.plugin(connectionGuardPlugin({ maxConnections: 5000 }));\n * ```\n */\nexport function connectionGuardPlugin(\n options: ConnectionGuardOptions,\n): OCPPPlugin {\n let activeCount = 0;\n\n return {\n name: \"connection-guard\",\n\n onConnection(client) {\n activeCount++;\n if (activeCount > options.maxConnections) {\n client\n .close({\n code: 4001,\n reason: \"Connection limit reached\",\n force: true,\n })\n .catch(() => {});\n }\n },\n\n onDisconnect() {\n activeCount = Math.max(0, activeCount - 1);\n },\n\n onClose() {\n activeCount = 0;\n },\n };\n}\n","import type { OCPPPlugin } from \"../types.js\";\n\n/**\n * Auto-responds to `Heartbeat` OCPP calls with `{ currentTime }`.\n * Registers a handler on each connecting client.\n *\n * @example\n * ```ts\n * import { heartbeatPlugin } from 'ocpp-ws-io/plugins';\n *\n * server.plugin(heartbeatPlugin());\n * // All clients will now get automatic Heartbeat responses\n * ```\n */\nexport function heartbeatPlugin(): OCPPPlugin {\n return {\n name: \"heartbeat\",\n\n onConnection(client) {\n client.handle(\"Heartbeat\", () => ({\n currentTime: new Date().toISOString(),\n }));\n },\n };\n}\n","import type { OCPPPlugin } from \"../types.js\";\n\n/**\n * Snapshot of tracked server metrics at a point in time.\n */\nexport interface MetricsSnapshot {\n /** Lifetime total connections since plugin init */\n totalConnections: number;\n /** Lifetime total disconnections since plugin init */\n totalDisconnections: number;\n /** Currently active connections */\n activeConnections: number;\n /** Highest concurrent connections ever observed */\n peakConnections: number;\n /** Average connection duration across all disconnected clients (ms) */\n connectionDurationAvgMs: number;\n /** Time since plugin initialization (ms) */\n uptimeMs: number;\n /** ISO timestamp of this snapshot */\n timestamp: string;\n}\n\n/**\n * Options for the metrics plugin.\n */\nexport interface MetricsPluginOptions {\n /** Interval in ms to emit metric snapshots (default: 30_000). Set to 0 to disable. */\n intervalMs?: number;\n /** Callback fired every interval with the current metrics snapshot. */\n onSnapshot?: (snapshot: MetricsSnapshot) => void;\n}\n\n/**\n * Extended OCPPPlugin with an additional `.getMetrics()` accessor.\n */\nexport interface MetricsPlugin extends OCPPPlugin {\n /** Returns the current metrics snapshot on demand. */\n getMetrics(): MetricsSnapshot;\n}\n\n/**\n * Tracks real-time server metrics: connection counters, peak, average duration.\n * Access metrics anytime via `.getMetrics()` on the returned plugin instance.\n *\n * @example\n * ```ts\n * import { metricsPlugin } from 'ocpp-ws-io/plugins';\n *\n * const metrics = metricsPlugin({\n * intervalMs: 10_000,\n * onSnapshot: (snap) => console.log(`Active: ${snap.activeConnections}`),\n * });\n * server.plugin(metrics);\n *\n * // On demand\n * const snap = metrics.getMetrics();\n * ```\n */\nexport function metricsPlugin(options?: MetricsPluginOptions): MetricsPlugin {\n const intervalMs = options?.intervalMs ?? 30_000;\n\n let totalConnections = 0;\n let totalDisconnections = 0;\n let activeConnections = 0;\n let peakConnections = 0;\n let totalDurationMs = 0;\n let initTime = Date.now();\n let snapshotTimer: ReturnType<typeof setInterval> | null = null;\n\n const connectionTimes = new Map<string, number>();\n\n function getMetrics(): MetricsSnapshot {\n return {\n totalConnections,\n totalDisconnections,\n activeConnections,\n peakConnections,\n connectionDurationAvgMs:\n totalDisconnections > 0\n ? Math.round(totalDurationMs / totalDisconnections)\n : 0,\n uptimeMs: Date.now() - initTime,\n timestamp: new Date().toISOString(),\n };\n }\n\n const plugin: MetricsPlugin = {\n name: \"metrics\",\n getMetrics,\n\n onInit() {\n initTime = Date.now();\n if (intervalMs > 0 && options?.onSnapshot) {\n snapshotTimer = setInterval(() => {\n options.onSnapshot!(getMetrics());\n }, intervalMs);\n // Don't block process exit\n if (\n snapshotTimer &&\n typeof snapshotTimer === \"object\" &&\n \"unref\" in snapshotTimer\n ) {\n snapshotTimer.unref();\n }\n }\n },\n\n onConnection(client) {\n totalConnections++;\n activeConnections++;\n if (activeConnections > peakConnections) {\n peakConnections = activeConnections;\n }\n connectionTimes.set(client.identity, Date.now());\n },\n\n onDisconnect(client) {\n totalDisconnections++;\n activeConnections = Math.max(0, activeConnections - 1);\n\n const startTime = connectionTimes.get(client.identity);\n if (startTime) {\n totalDurationMs += Date.now() - startTime;\n connectionTimes.delete(client.identity);\n }\n },\n\n onClose() {\n if (snapshotTimer) {\n clearInterval(snapshotTimer);\n snapshotTimer = null;\n }\n connectionTimes.clear();\n },\n };\n\n return plugin;\n}\n","import type { OCPPPlugin } from \"../types.js\";\n\n/**\n * Options for the OpenTelemetry plugin.\n */\nexport interface OtelPluginOptions {\n /** Service name for the tracer (default: \"ocpp-server\") */\n serviceName?: string;\n /**\n * Custom OpenTelemetry Tracer instance.\n * If omitted, the plugin will attempt to get a tracer from the global\n * `@opentelemetry/api` module (must be installed as a peer dependency).\n */\n tracer?: {\n startSpan: (\n name: string,\n options?: Record<string, unknown>,\n ) => {\n setAttribute: (key: string, value: string | number) => void;\n setStatus: (status: { code: number; message?: string }) => void;\n end: () => void;\n };\n };\n}\n\ninterface SpanLike {\n setAttribute: (key: string, value: string | number) => void;\n setStatus: (status: { code: number; message?: string }) => void;\n end: () => void;\n}\n\ninterface TracerLike {\n startSpan: (name: string, options?: Record<string, unknown>) => SpanLike;\n}\n\n/**\n * OpenTelemetry integration — creates spans for connection lifecycle events.\n *\n * Requires `@opentelemetry/api` as an **optional peer dependency**.\n * If not installed or no tracer is provided, the plugin becomes a silent no-op.\n *\n * @example\n * ```ts\n * import { otelPlugin } from 'ocpp-ws-io/plugins';\n *\n * server.plugin(otelPlugin({ serviceName: 'my-csms' }));\n * ```\n *\n * @example With custom tracer\n * ```ts\n * import { trace } from '@opentelemetry/api';\n * import { otelPlugin } from 'ocpp-ws-io/plugins';\n *\n * const tracer = trace.getTracer('ocpp');\n * server.plugin(otelPlugin({ tracer }));\n * ```\n */\nexport function otelPlugin(options?: OtelPluginOptions): OCPPPlugin {\n let tracer: TracerLike | null = options?.tracer ?? null;\n const spans = new Map<string, { span: SpanLike; startTime: number }>();\n\n return {\n name: \"otel\",\n\n onInit(server) {\n if (!tracer) {\n // Attempt to load @opentelemetry/api dynamically\n try {\n // Use createRequire to prevent tsup from bundling the optional dep\n const { createRequire } = require(\"node:module\");\n const dynamicRequire = createRequire(__filename);\n const otelApi = dynamicRequire(\"@opentelemetry/api\") as {\n trace: {\n getTracer: (name: string, version?: string) => TracerLike;\n };\n };\n tracer = otelApi.trace.getTracer(\n options?.serviceName ?? \"ocpp-server\",\n \"1.0.0\",\n );\n } catch {\n server.log.warn?.(\n \"otelPlugin: @opentelemetry/api not found — plugin disabled. Install it as a peer dependency.\",\n );\n tracer = null;\n }\n }\n },\n\n onConnection(client) {\n if (!tracer) return;\n\n const span = tracer.startSpan(\"ocpp.connection\", {\n kind: 1, // SpanKind.SERVER\n });\n\n span.setAttribute(\"ocpp.identity\", client.identity);\n span.setAttribute(\"ocpp.protocol\", client.protocol ?? \"unknown\");\n span.setAttribute(\"net.peer.ip\", client.handshake.remoteAddress);\n\n spans.set(client.identity, { span, startTime: Date.now() });\n },\n\n onDisconnect(client, code) {\n const entry = spans.get(client.identity);\n if (!entry) return;\n\n const durationMs = Date.now() - entry.startTime;\n entry.span.setAttribute(\"ocpp.close_code\", code);\n entry.span.setAttribute(\"ocpp.duration_ms\", durationMs);\n entry.span.setStatus({ code: 1 }); // SpanStatusCode.OK\n entry.span.end();\n\n spans.delete(client.identity);\n },\n\n onClose() {\n // End all open spans\n for (const [, entry] of spans) {\n entry.span.setStatus({ code: 2, message: \"Server shutdown\" }); // ERROR\n entry.span.end();\n }\n spans.clear();\n },\n };\n}\n","import type { OCPPPlugin } from \"../types.js\";\n\n/**\n * Options for the session log plugin.\n */\nexport interface SessionLogOptions {\n /**\n * Custom logger instance. Defaults to `console`.\n * Must have `info` method.\n */\n logger?: { info: (msg: string, meta?: Record<string, unknown>) => void };\n}\n\n/**\n * Logs connect/disconnect events with identity, IP, protocol, and connection duration.\n *\n * @example\n * ```ts\n * import { sessionLogPlugin } from 'ocpp-ws-io/plugins';\n *\n * server.plugin(sessionLogPlugin());\n * // => Connected: CP-101 from 192.168.1.1 via ocpp1.6\n * // => Disconnected: CP-101 after 3600s (code: 1000)\n * ```\n */\nexport function sessionLogPlugin(options?: SessionLogOptions): OCPPPlugin {\n const logger = options?.logger ?? console;\n const connectionTimes = new Map<string, number>();\n\n return {\n name: \"session-log\",\n\n onConnection(client) {\n connectionTimes.set(client.identity, Date.now());\n logger.info(\"Connected\", {\n identity: client.identity,\n ip: client.handshake.remoteAddress,\n protocol: client.protocol,\n });\n },\n\n onDisconnect(client, code, reason) {\n const startTime = connectionTimes.get(client.identity);\n const durationSec = startTime\n ? Math.round((Date.now() - startTime) / 1000)\n : 0;\n connectionTimes.delete(client.identity);\n\n logger.info(\"Disconnected\", {\n identity: client.identity,\n durationSec,\n code,\n reason,\n });\n },\n\n onClose() {\n connectionTimes.clear();\n },\n };\n}\n","import { createHmac } from \"node:crypto\";\nimport type { OCPPPlugin } from \"../types.js\";\n\n/**\n * Options for the webhook plugin.\n */\nexport interface WebhookPluginOptions {\n /** Webhook HTTP endpoint URL. */\n url: string;\n /** Which lifecycle events to send (default: all). */\n events?: Array<\"init\" | \"connect\" | \"disconnect\" | \"close\">;\n /** Custom HTTP headers to include (e.g. Authorization). */\n headers?: Record<string, string>;\n /** HMAC-SHA256 secret for signing payloads (sent as `X-Signature` header). */\n secret?: string;\n /** Fetch timeout in ms (default: 5000). */\n timeout?: number;\n /** Number of retries on failure (default: 1). */\n retries?: number;\n}\n\ninterface WebhookPayload {\n event: string;\n timestamp: string;\n data?: Record<string, unknown>;\n}\n\n/**\n * Sends HTTP POST webhooks on server lifecycle events.\n * Uses Node.js built-in `fetch` (Node 18+).\n *\n * @example\n * ```ts\n * import { webhookPlugin } from 'ocpp-ws-io/plugins';\n *\n * server.plugin(webhookPlugin({\n * url: 'https://api.example.com/ocpp-events',\n * secret: process.env.WEBHOOK_SECRET,\n * events: ['connect', 'disconnect'],\n * headers: { Authorization: 'Bearer token123' },\n * }));\n * ```\n */\nexport function webhookPlugin(options: WebhookPluginOptions): OCPPPlugin {\n const allowedEvents = new Set(\n options.events ?? [\"init\", \"connect\", \"disconnect\", \"close\"],\n );\n const timeout = options.timeout ?? 5000;\n const maxRetries = options.retries ?? 1;\n\n async function sendWebhook(payload: WebhookPayload): Promise<void> {\n if (!allowedEvents.has(payload.event as any)) return;\n\n const body = JSON.stringify(payload);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...options.headers,\n };\n\n // HMAC-SHA256 signature\n if (options.secret) {\n const signature = createHmac(\"sha256\", options.secret)\n .update(body)\n .digest(\"hex\");\n headers[\"X-Signature\"] = signature;\n }\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeout);\n\n await fetch(options.url, {\n method: \"POST\",\n headers,\n body,\n signal: controller.signal,\n });\n\n clearTimeout(timer);\n return; // Success\n } catch {\n if (attempt === maxRetries) {\n // Silently fail — webhooks should not crash the server\n }\n }\n }\n }\n\n return {\n name: \"webhook\",\n\n onInit() {\n sendWebhook({\n event: \"init\",\n timestamp: new Date().toISOString(),\n }).catch(() => {});\n },\n\n onConnection(client) {\n sendWebhook({\n event: \"connect\",\n timestamp: new Date().toISOString(),\n data: {\n identity: client.identity,\n ip: client.handshake.remoteAddress,\n protocol: client.protocol,\n },\n }).catch(() => {});\n },\n\n onDisconnect(client, code, reason) {\n sendWebhook({\n event: \"disconnect\",\n timestamp: new Date().toISOString(),\n data: {\n identity: client.identity,\n code,\n reason,\n },\n }).catch(() => {});\n },\n\n onClose() {\n // Fire-and-forget — don't block server shutdown\n sendWebhook({\n event: \"close\",\n timestamp: new Date().toISOString(),\n }).catch(() => {});\n },\n };\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../node_modules/tsup/assets/esm_shims.js","../src/plugins/anomaly.ts","../src/plugins/connection-guard.ts","../src/plugins/heartbeat.ts","../src/plugins/metrics.ts","../src/plugins/otel.ts","../src/plugins/session-log.ts","../src/plugins/webhook.ts"],"names":["getFilename","fileURLToPath","__filename","anomalyPlugin","options","threshold","windowMs","connectLog","server","gcTimer","pruneExpired","timestamps","now","cutoff","i","srv","identity","pruned","client","connectionGuardPlugin","activeCount","heartbeatPlugin","metricsPlugin","intervalMs","totalConnections","totalDisconnections","activeConnections","peakConnections","totalDurationMs","initTime","snapshotTimer","connectionTimes","getMetrics","startTime","otelPlugin","tracer","spans","createRequire","span","code","entry","durationMs","sessionLogPlugin","logger","reason","durationSec","webhookPlugin","allowedEvents","timeout","maxRetries","sendWebhook","payload","body","headers","signature","createHmac","attempt","controller","timer"],"mappings":"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","file":"plugins.mjs","sourcesContent":["// Shim globals in esm bundle\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst getFilename = () => fileURLToPath(import.meta.url)\nconst getDirname = () => path.dirname(getFilename())\n\nexport const __dirname = /* @__PURE__ */ getDirname()\nexport const __filename = /* @__PURE__ */ getFilename()\n","import type { OCPPServer } from \"../server.js\";\nimport type { OCPPPlugin } from \"../types.js\";\n\n/**\n * Options for the anomaly detection plugin.\n */\nexport interface AnomalyPluginOptions {\n /**\n * Maximum number of connections from the same identity\n * within the sliding window before triggering an anomaly.\n * Default: 5\n */\n reconnectThreshold?: number;\n /**\n * Sliding window duration in milliseconds.\n * Default: 60_000 (1 minute)\n */\n windowMs?: number;\n}\n\n/**\n * Detects anomalous connection patterns such as rapid reconnections\n * from the same identity. Emits `securityEvent` on the server with\n * type `ANOMALY_RAPID_RECONNECT`.\n *\n * @example\n * ```ts\n * import { anomalyPlugin } from 'ocpp-ws-io/plugins';\n *\n * server.plugin(anomalyPlugin({\n * reconnectThreshold: 10,\n * windowMs: 60_000,\n * }));\n *\n * server.on('securityEvent', (evt) => {\n * if (evt.type === 'ANOMALY_RAPID_RECONNECT') {\n * console.warn(`Rapid reconnect storm: ${evt.identity}`);\n * }\n * });\n * ```\n */\nexport function anomalyPlugin(options?: AnomalyPluginOptions): OCPPPlugin {\n const threshold = options?.reconnectThreshold ?? 5;\n const windowMs = options?.windowMs ?? 60_000;\n\n /** Map of identity → array of connection timestamps */\n const connectLog = new Map<string, number[]>();\n let server: OCPPServer | null = null;\n let gcTimer: ReturnType<typeof setInterval> | null = null;\n\n function pruneExpired(timestamps: number[], now: number): number[] {\n const cutoff = now - windowMs;\n // Find first index that is within the window\n let i = 0;\n while (i < timestamps.length && timestamps[i] < cutoff) i++;\n return i > 0 ? timestamps.slice(i) : timestamps;\n }\n\n return {\n name: \"anomaly\",\n\n onInit(srv) {\n server = srv;\n // Periodic garbage collection of expired entries\n gcTimer = setInterval(() => {\n const now = Date.now();\n for (const [identity, timestamps] of connectLog) {\n const pruned = pruneExpired(timestamps, now);\n if (pruned.length === 0) {\n connectLog.delete(identity);\n } else {\n connectLog.set(identity, pruned);\n }\n }\n }, windowMs).unref();\n },\n\n onConnection(client) {\n const now = Date.now();\n const identity = client.identity;\n\n let timestamps = connectLog.get(identity) ?? [];\n timestamps = pruneExpired(timestamps, now);\n timestamps.push(now);\n connectLog.set(identity, timestamps);\n\n if (timestamps.length > threshold && server) {\n server.emit(\"securityEvent\" as any, {\n type: \"ANOMALY_RAPID_RECONNECT\",\n identity,\n ip: client.handshake.remoteAddress,\n timestamp: new Date().toISOString(),\n details: {\n connectionsInWindow: timestamps.length,\n threshold,\n windowMs,\n },\n });\n }\n },\n\n onClose() {\n if (gcTimer) {\n clearInterval(gcTimer);\n gcTimer = null;\n }\n connectLog.clear();\n server = null;\n },\n };\n}\n","import type { OCPPPlugin } from \"../types.js\";\n\n/**\n * Options for the connection guard plugin.\n */\nexport interface ConnectionGuardOptions {\n /** Maximum allowed concurrent connections. */\n maxConnections: number;\n}\n\n/**\n * Enforces a hard limit on concurrent connections.\n * New connections exceeding the limit are force-closed with code 4001.\n *\n * @example\n * ```ts\n * import { connectionGuardPlugin } from 'ocpp-ws-io/plugins';\n *\n * server.plugin(connectionGuardPlugin({ maxConnections: 5000 }));\n * ```\n */\nexport function connectionGuardPlugin(\n options: ConnectionGuardOptions,\n): OCPPPlugin {\n let activeCount = 0;\n\n return {\n name: \"connection-guard\",\n\n onConnection(client) {\n activeCount++;\n if (activeCount > options.maxConnections) {\n client\n .close({\n code: 4001,\n reason: \"Connection limit reached\",\n force: true,\n })\n .catch(() => {});\n }\n },\n\n onDisconnect() {\n activeCount = Math.max(0, activeCount - 1);\n },\n\n onClose() {\n activeCount = 0;\n },\n };\n}\n","import type { OCPPPlugin } from \"../types.js\";\n\n/**\n * Auto-responds to `Heartbeat` OCPP calls with `{ currentTime }`.\n * Registers a handler on each connecting client.\n *\n * @example\n * ```ts\n * import { heartbeatPlugin } from 'ocpp-ws-io/plugins';\n *\n * server.plugin(heartbeatPlugin());\n * // All clients will now get automatic Heartbeat responses\n * ```\n */\nexport function heartbeatPlugin(): OCPPPlugin {\n return {\n name: \"heartbeat\",\n\n onConnection(client) {\n client.handle(\"Heartbeat\", () => ({\n currentTime: new Date().toISOString(),\n }));\n },\n };\n}\n","import type { OCPPPlugin } from \"../types.js\";\n\n/**\n * Snapshot of tracked server metrics at a point in time.\n */\nexport interface MetricsSnapshot {\n /** Lifetime total connections since plugin init */\n totalConnections: number;\n /** Lifetime total disconnections since plugin init */\n totalDisconnections: number;\n /** Currently active connections */\n activeConnections: number;\n /** Highest concurrent connections ever observed */\n peakConnections: number;\n /** Average connection duration across all disconnected clients (ms) */\n connectionDurationAvgMs: number;\n /** Time since plugin initialization (ms) */\n uptimeMs: number;\n /** ISO timestamp of this snapshot */\n timestamp: string;\n}\n\n/**\n * Options for the metrics plugin.\n */\nexport interface MetricsPluginOptions {\n /** Interval in ms to emit metric snapshots (default: 30_000). Set to 0 to disable. */\n intervalMs?: number;\n /** Callback fired every interval with the current metrics snapshot. */\n onSnapshot?: (snapshot: MetricsSnapshot) => void;\n}\n\n/**\n * Extended OCPPPlugin with an additional `.getMetrics()` accessor.\n */\nexport interface MetricsPlugin extends OCPPPlugin {\n /** Returns the current metrics snapshot on demand. */\n getMetrics(): MetricsSnapshot;\n}\n\n/**\n * Tracks real-time server metrics: connection counters, peak, average duration.\n * Access metrics anytime via `.getMetrics()` on the returned plugin instance.\n *\n * @example\n * ```ts\n * import { metricsPlugin } from 'ocpp-ws-io/plugins';\n *\n * const metrics = metricsPlugin({\n * intervalMs: 10_000,\n * onSnapshot: (snap) => console.log(`Active: ${snap.activeConnections}`),\n * });\n * server.plugin(metrics);\n *\n * // On demand\n * const snap = metrics.getMetrics();\n * ```\n */\nexport function metricsPlugin(options?: MetricsPluginOptions): MetricsPlugin {\n const intervalMs = options?.intervalMs ?? 30_000;\n\n let totalConnections = 0;\n let totalDisconnections = 0;\n let activeConnections = 0;\n let peakConnections = 0;\n let totalDurationMs = 0;\n let initTime = Date.now();\n let snapshotTimer: ReturnType<typeof setInterval> | null = null;\n\n const connectionTimes = new Map<string, number>();\n\n function getMetrics(): MetricsSnapshot {\n return {\n totalConnections,\n totalDisconnections,\n activeConnections,\n peakConnections,\n connectionDurationAvgMs:\n totalDisconnections > 0\n ? Math.round(totalDurationMs / totalDisconnections)\n : 0,\n uptimeMs: Date.now() - initTime,\n timestamp: new Date().toISOString(),\n };\n }\n\n const plugin: MetricsPlugin = {\n name: \"metrics\",\n getMetrics,\n\n onInit() {\n initTime = Date.now();\n if (intervalMs > 0 && options?.onSnapshot) {\n snapshotTimer = setInterval(() => {\n options.onSnapshot!(getMetrics());\n }, intervalMs);\n // Don't block process exit\n if (\n snapshotTimer &&\n typeof snapshotTimer === \"object\" &&\n \"unref\" in snapshotTimer\n ) {\n snapshotTimer.unref();\n }\n }\n },\n\n onConnection(client) {\n totalConnections++;\n activeConnections++;\n if (activeConnections > peakConnections) {\n peakConnections = activeConnections;\n }\n connectionTimes.set(client.identity, Date.now());\n },\n\n onDisconnect(client) {\n totalDisconnections++;\n activeConnections = Math.max(0, activeConnections - 1);\n\n const startTime = connectionTimes.get(client.identity);\n if (startTime) {\n totalDurationMs += Date.now() - startTime;\n connectionTimes.delete(client.identity);\n }\n },\n\n onClose() {\n if (snapshotTimer) {\n clearInterval(snapshotTimer);\n snapshotTimer = null;\n }\n connectionTimes.clear();\n },\n };\n\n return plugin;\n}\n","import type { OCPPPlugin } from \"../types.js\";\n\n/**\n * Options for the OpenTelemetry plugin.\n */\nexport interface OtelPluginOptions {\n /** Service name for the tracer (default: \"ocpp-server\") */\n serviceName?: string;\n /**\n * Custom OpenTelemetry Tracer instance.\n * If omitted, the plugin will attempt to get a tracer from the global\n * `@opentelemetry/api` module (must be installed as a peer dependency).\n */\n tracer?: {\n startSpan: (\n name: string,\n options?: Record<string, unknown>,\n ) => {\n setAttribute: (key: string, value: string | number) => void;\n setStatus: (status: { code: number; message?: string }) => void;\n end: () => void;\n };\n };\n}\n\ninterface SpanLike {\n setAttribute: (key: string, value: string | number) => void;\n setStatus: (status: { code: number; message?: string }) => void;\n end: () => void;\n}\n\ninterface TracerLike {\n startSpan: (name: string, options?: Record<string, unknown>) => SpanLike;\n}\n\n/**\n * OpenTelemetry integration — creates spans for connection lifecycle events.\n *\n * Requires `@opentelemetry/api` as an **optional peer dependency**.\n * If not installed or no tracer is provided, the plugin becomes a silent no-op.\n *\n * @example\n * ```ts\n * import { otelPlugin } from 'ocpp-ws-io/plugins';\n *\n * server.plugin(otelPlugin({ serviceName: 'my-csms' }));\n * ```\n *\n * @example With custom tracer\n * ```ts\n * import { trace } from '@opentelemetry/api';\n * import { otelPlugin } from 'ocpp-ws-io/plugins';\n *\n * const tracer = trace.getTracer('ocpp');\n * server.plugin(otelPlugin({ tracer }));\n * ```\n */\nexport function otelPlugin(options?: OtelPluginOptions): OCPPPlugin {\n let tracer: TracerLike | null = options?.tracer ?? null;\n const spans = new Map<string, { span: SpanLike; startTime: number }>();\n\n return {\n name: \"otel\",\n\n onInit(server) {\n if (!tracer) {\n // Attempt to load @opentelemetry/api dynamically\n try {\n // Use createRequire to prevent tsup from bundling the optional dep\n const { createRequire } = require(\"node:module\");\n const dynamicRequire = createRequire(__filename);\n const otelApi = dynamicRequire(\"@opentelemetry/api\") as {\n trace: {\n getTracer: (name: string, version?: string) => TracerLike;\n };\n };\n tracer = otelApi.trace.getTracer(\n options?.serviceName ?? \"ocpp-server\",\n \"1.0.0\",\n );\n } catch {\n server.log.warn?.(\n \"otelPlugin: @opentelemetry/api not found — plugin disabled. Install it as a peer dependency.\",\n );\n tracer = null;\n }\n }\n },\n\n onConnection(client) {\n if (!tracer) return;\n\n const span = tracer.startSpan(\"ocpp.connection\", {\n kind: 1, // SpanKind.SERVER\n });\n\n span.setAttribute(\"ocpp.identity\", client.identity);\n span.setAttribute(\"ocpp.protocol\", client.protocol ?? \"unknown\");\n span.setAttribute(\"net.peer.ip\", client.handshake.remoteAddress);\n\n spans.set(client.identity, { span, startTime: Date.now() });\n },\n\n onDisconnect(client, code) {\n const entry = spans.get(client.identity);\n if (!entry) return;\n\n const durationMs = Date.now() - entry.startTime;\n entry.span.setAttribute(\"ocpp.close_code\", code);\n entry.span.setAttribute(\"ocpp.duration_ms\", durationMs);\n entry.span.setStatus({ code: 1 }); // SpanStatusCode.OK\n entry.span.end();\n\n spans.delete(client.identity);\n },\n\n onClose() {\n // End all open spans\n for (const [, entry] of spans) {\n entry.span.setStatus({ code: 2, message: \"Server shutdown\" }); // ERROR\n entry.span.end();\n }\n spans.clear();\n },\n };\n}\n","import type { OCPPPlugin } from \"../types.js\";\n\n/**\n * Options for the session log plugin.\n */\nexport interface SessionLogOptions {\n /**\n * Custom logger instance. Defaults to `console`.\n * Must have `info` method.\n */\n logger?: { info: (msg: string, meta?: Record<string, unknown>) => void };\n}\n\n/**\n * Logs connect/disconnect events with identity, IP, protocol, and connection duration.\n *\n * @example\n * ```ts\n * import { sessionLogPlugin } from 'ocpp-ws-io/plugins';\n *\n * server.plugin(sessionLogPlugin());\n * // => Connected: CP-101 from 192.168.1.1 via ocpp1.6\n * // => Disconnected: CP-101 after 3600s (code: 1000)\n * ```\n */\nexport function sessionLogPlugin(options?: SessionLogOptions): OCPPPlugin {\n const logger = options?.logger ?? console;\n const connectionTimes = new Map<string, number>();\n\n return {\n name: \"session-log\",\n\n onConnection(client) {\n connectionTimes.set(client.identity, Date.now());\n logger.info(\"Connected\", {\n identity: client.identity,\n ip: client.handshake.remoteAddress,\n protocol: client.protocol,\n });\n },\n\n onDisconnect(client, code, reason) {\n const startTime = connectionTimes.get(client.identity);\n const durationSec = startTime\n ? Math.round((Date.now() - startTime) / 1000)\n : 0;\n connectionTimes.delete(client.identity);\n\n logger.info(\"Disconnected\", {\n identity: client.identity,\n durationSec,\n code,\n reason,\n });\n },\n\n onClose() {\n connectionTimes.clear();\n },\n };\n}\n","import { createHmac } from \"node:crypto\";\nimport type { OCPPPlugin } from \"../types.js\";\n\n/**\n * Options for the webhook plugin.\n */\nexport interface WebhookPluginOptions {\n /** Webhook HTTP endpoint URL. */\n url: string;\n /** Which lifecycle events to send (default: all). */\n events?: Array<\"init\" | \"connect\" | \"disconnect\" | \"close\">;\n /** Custom HTTP headers to include (e.g. Authorization). */\n headers?: Record<string, string>;\n /** HMAC-SHA256 secret for signing payloads (sent as `X-Signature` header). */\n secret?: string;\n /** Fetch timeout in ms (default: 5000). */\n timeout?: number;\n /** Number of retries on failure (default: 1). */\n retries?: number;\n}\n\ninterface WebhookPayload {\n event: string;\n timestamp: string;\n data?: Record<string, unknown>;\n}\n\n/**\n * Sends HTTP POST webhooks on server lifecycle events.\n * Uses Node.js built-in `fetch` (Node 18+).\n *\n * @example\n * ```ts\n * import { webhookPlugin } from 'ocpp-ws-io/plugins';\n *\n * server.plugin(webhookPlugin({\n * url: 'https://api.example.com/ocpp-events',\n * secret: process.env.WEBHOOK_SECRET,\n * events: ['connect', 'disconnect'],\n * headers: { Authorization: 'Bearer token123' },\n * }));\n * ```\n */\nexport function webhookPlugin(options: WebhookPluginOptions): OCPPPlugin {\n const allowedEvents = new Set(\n options.events ?? [\"init\", \"connect\", \"disconnect\", \"close\"],\n );\n const timeout = options.timeout ?? 5000;\n const maxRetries = options.retries ?? 1;\n\n async function sendWebhook(payload: WebhookPayload): Promise<void> {\n if (!allowedEvents.has(payload.event as any)) return;\n\n const body = JSON.stringify(payload);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...options.headers,\n };\n\n // HMAC-SHA256 signature\n if (options.secret) {\n const signature = createHmac(\"sha256\", options.secret)\n .update(body)\n .digest(\"hex\");\n headers[\"X-Signature\"] = signature;\n }\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeout);\n\n await fetch(options.url, {\n method: \"POST\",\n headers,\n body,\n signal: controller.signal,\n });\n\n clearTimeout(timer);\n return; // Success\n } catch {\n if (attempt === maxRetries) {\n // Silently fail — webhooks should not crash the server\n }\n }\n }\n }\n\n return {\n name: \"webhook\",\n\n onInit() {\n sendWebhook({\n event: \"init\",\n timestamp: new Date().toISOString(),\n }).catch(() => {});\n },\n\n onConnection(client) {\n sendWebhook({\n event: \"connect\",\n timestamp: new Date().toISOString(),\n data: {\n identity: client.identity,\n ip: client.handshake.remoteAddress,\n protocol: client.protocol,\n },\n }).catch(() => {});\n },\n\n onDisconnect(client, code, reason) {\n sendWebhook({\n event: \"disconnect\",\n timestamp: new Date().toISOString(),\n data: {\n identity: client.identity,\n code,\n reason,\n },\n }).catch(() => {});\n },\n\n onClose() {\n // Fire-and-forget — don't block server shutdown\n sendWebhook({\n event: \"close\",\n timestamp: new Date().toISOString(),\n }).catch(() => {});\n },\n };\n}\n"]}