ocpp-ws-io 1.0.0-alpha
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.
Potentially problematic release.
This version of ocpp-ws-io might be problematic. Click here for more details.
- package/.github/workflows/publish.yml +52 -0
- package/LICENSE +21 -0
- package/README.md +773 -0
- package/dist/adapters/redis.d.mts +73 -0
- package/dist/adapters/redis.d.ts +73 -0
- package/dist/adapters/redis.js +96 -0
- package/dist/adapters/redis.js.map +1 -0
- package/dist/adapters/redis.mjs +71 -0
- package/dist/adapters/redis.mjs.map +1 -0
- package/dist/index.d.mts +268 -0
- package/dist/index.d.ts +268 -0
- package/dist/index.js +38919 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +38855 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types-6LVUoXof.d.mts +284 -0
- package/dist/types-6LVUoXof.d.ts +284 -0
- package/package.json +59 -0
- package/src/adapters/adapter.ts +40 -0
- package/src/adapters/redis.ts +144 -0
- package/src/client.ts +882 -0
- package/src/errors.ts +183 -0
- package/src/event-buffer.ts +73 -0
- package/src/index.ts +68 -0
- package/src/queue.ts +65 -0
- package/src/schemas/ocpp1_6.json +2376 -0
- package/src/schemas/ocpp2_0_1.json +11878 -0
- package/src/schemas/ocpp2_1.json +23176 -0
- package/src/server-client.ts +65 -0
- package/src/server.ts +374 -0
- package/src/standard-validators.ts +18 -0
- package/src/types.ts +316 -0
- package/src/util.ts +119 -0
- package/src/validator.ts +148 -0
- package/src/ws-util.ts +186 -0
- package/test/adapter.test.ts +88 -0
- package/test/client.test.ts +297 -0
- package/test/errors.test.ts +132 -0
- package/test/queue.test.ts +133 -0
- package/test/server.test.ts +274 -0
- package/test/util.test.ts +103 -0
- package/test/ws-util.test.ts +93 -0
- package/tsconfig.json +25 -0
- package/tsup.config.ts +16 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import * as node_https from 'node:https';
|
|
2
|
+
import * as node_http from 'node:http';
|
|
3
|
+
import { IncomingMessage } from 'node:http';
|
|
4
|
+
import { TLSSocket } from 'node:tls';
|
|
5
|
+
import { Duplex } from 'node:stream';
|
|
6
|
+
import Ajv from 'ajv';
|
|
7
|
+
|
|
8
|
+
interface ValidatorSchema {
|
|
9
|
+
$schema?: string;
|
|
10
|
+
$id?: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Schema validator using AJV for OCPP message validation.
|
|
15
|
+
* Each validator is bound to a specific subprotocol version.
|
|
16
|
+
*/
|
|
17
|
+
declare class Validator {
|
|
18
|
+
readonly subprotocol: string;
|
|
19
|
+
/** @internal */
|
|
20
|
+
_ajv: Ajv;
|
|
21
|
+
constructor(subprotocol: string, schemas: ValidatorSchema[]);
|
|
22
|
+
/**
|
|
23
|
+
* Normalize a schema ID from OCPP URN format to internal path format.
|
|
24
|
+
*/
|
|
25
|
+
private _normalizeSchemaId;
|
|
26
|
+
/**
|
|
27
|
+
* Validate a payload against a schema identified by its $id.
|
|
28
|
+
* Throws a typed RPCError if validation fails.
|
|
29
|
+
*/
|
|
30
|
+
validate(schemaId: string, params: unknown): void;
|
|
31
|
+
/**
|
|
32
|
+
* Check if a schema exists for the given $id.
|
|
33
|
+
*/
|
|
34
|
+
hasSchema(schemaId: string): boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create a validator for a specific subprotocol version.
|
|
38
|
+
*/
|
|
39
|
+
declare function createValidator(subprotocol: string, schemas: ValidatorSchema[]): Validator;
|
|
40
|
+
|
|
41
|
+
type OCPPProtocol = "ocpp1.6" | "ocpp2.0.1" | "ocpp2.1";
|
|
42
|
+
declare const ConnectionState: {
|
|
43
|
+
readonly CONNECTING: 0;
|
|
44
|
+
readonly OPEN: 1;
|
|
45
|
+
readonly CLOSING: 2;
|
|
46
|
+
readonly CLOSED: 3;
|
|
47
|
+
};
|
|
48
|
+
type ConnectionState = (typeof ConnectionState)[keyof typeof ConnectionState];
|
|
49
|
+
declare enum SecurityProfile {
|
|
50
|
+
/** No security — plain WS, no auth (dev/testing only) */
|
|
51
|
+
NONE = 0,
|
|
52
|
+
/** Profile 1: Basic Auth over unsecured WS (ws://) — password-based */
|
|
53
|
+
BASIC_AUTH = 1,
|
|
54
|
+
/** Profile 2: TLS + Basic Auth (wss://) — server cert + password */
|
|
55
|
+
TLS_BASIC_AUTH = 2,
|
|
56
|
+
/** Profile 3: Mutual TLS (wss://) — client + server certificates */
|
|
57
|
+
TLS_CLIENT_CERT = 3
|
|
58
|
+
}
|
|
59
|
+
declare const MessageType: {
|
|
60
|
+
readonly CALL: 2;
|
|
61
|
+
readonly CALLRESULT: 3;
|
|
62
|
+
readonly CALLERROR: 4;
|
|
63
|
+
};
|
|
64
|
+
type MessageType = (typeof MessageType)[keyof typeof MessageType];
|
|
65
|
+
type OCPPCall<T = unknown> = [2, string, string, T];
|
|
66
|
+
type OCPPCallResult<T = unknown> = [3, string, T];
|
|
67
|
+
type OCPPCallError = [
|
|
68
|
+
4,
|
|
69
|
+
string,
|
|
70
|
+
string,
|
|
71
|
+
string,
|
|
72
|
+
Record<string, unknown>
|
|
73
|
+
];
|
|
74
|
+
type OCPPMessage<T = unknown> = OCPPCall<T> | OCPPCallResult<T> | OCPPCallError;
|
|
75
|
+
interface TLSOptions {
|
|
76
|
+
/** Server/client certificate (PEM) */
|
|
77
|
+
cert?: string | Buffer;
|
|
78
|
+
/** Private key (PEM) */
|
|
79
|
+
key?: string | Buffer;
|
|
80
|
+
/** CA certificate(s) for verification */
|
|
81
|
+
ca?: string | Buffer | Array<string | Buffer>;
|
|
82
|
+
/** Reject unauthorized certs (default: true) */
|
|
83
|
+
rejectUnauthorized?: boolean;
|
|
84
|
+
/** Passphrase for encrypted private key */
|
|
85
|
+
passphrase?: string;
|
|
86
|
+
}
|
|
87
|
+
interface HandlerContext<T = unknown> {
|
|
88
|
+
messageId: string;
|
|
89
|
+
method: string;
|
|
90
|
+
params: T;
|
|
91
|
+
signal: AbortSignal;
|
|
92
|
+
}
|
|
93
|
+
type CallHandler<TParams = unknown, TResult = unknown> = (context: HandlerContext<TParams>) => TResult | Promise<TResult>;
|
|
94
|
+
type WildcardHandler = (method: string, context: HandlerContext) => unknown | Promise<unknown>;
|
|
95
|
+
interface CallOptions {
|
|
96
|
+
/** Timeout in milliseconds for this specific call */
|
|
97
|
+
timeoutMs?: number;
|
|
98
|
+
/** Abort signal */
|
|
99
|
+
signal?: AbortSignal;
|
|
100
|
+
/** Suppress sending a response (server-side, NOREPLY) */
|
|
101
|
+
noReply?: boolean;
|
|
102
|
+
}
|
|
103
|
+
interface CloseOptions {
|
|
104
|
+
/** WebSocket close code (default: 1000) */
|
|
105
|
+
code?: number;
|
|
106
|
+
/** Close reason string */
|
|
107
|
+
reason?: string;
|
|
108
|
+
/** Wait for pending calls to complete before closing */
|
|
109
|
+
awaitPending?: boolean;
|
|
110
|
+
/** Force-close without waiting */
|
|
111
|
+
force?: boolean;
|
|
112
|
+
}
|
|
113
|
+
interface HandshakeInfo {
|
|
114
|
+
/** Charging station identity (from URL path) */
|
|
115
|
+
identity: string;
|
|
116
|
+
/** Remote IP address */
|
|
117
|
+
remoteAddress: string;
|
|
118
|
+
/** Request headers */
|
|
119
|
+
headers: Record<string, string | string[] | undefined>;
|
|
120
|
+
/** Negotiated subprotocols */
|
|
121
|
+
protocols: Set<string>;
|
|
122
|
+
/** Request endpoint URL path */
|
|
123
|
+
endpoint: string;
|
|
124
|
+
/** URL query parameters */
|
|
125
|
+
query: URLSearchParams;
|
|
126
|
+
/** Original HTTP request */
|
|
127
|
+
request: IncomingMessage;
|
|
128
|
+
/** Password from Basic Auth (Profile 1 & 2) */
|
|
129
|
+
password?: Buffer;
|
|
130
|
+
/** Client certificate (Profile 3 — mTLS) */
|
|
131
|
+
clientCertificate?: ReturnType<TLSSocket["getPeerCertificate"]>;
|
|
132
|
+
/** Active security profile */
|
|
133
|
+
securityProfile: SecurityProfile;
|
|
134
|
+
}
|
|
135
|
+
type SessionData<T = Record<string, unknown>> = T;
|
|
136
|
+
interface ClientOptions {
|
|
137
|
+
/** Unique identity for this client (charging station ID) */
|
|
138
|
+
identity: string;
|
|
139
|
+
/** WebSocket endpoint URL (ws:// or wss://) */
|
|
140
|
+
endpoint: string;
|
|
141
|
+
/** OCPP Security Profile (default: NONE) */
|
|
142
|
+
securityProfile?: SecurityProfile;
|
|
143
|
+
/** Password for Basic Auth (Profile 1 & 2) */
|
|
144
|
+
password?: string | Buffer;
|
|
145
|
+
/** TLS options (Profile 2 & 3) */
|
|
146
|
+
tls?: TLSOptions;
|
|
147
|
+
/** OCPP subprotocols to negotiate */
|
|
148
|
+
protocols?: string[];
|
|
149
|
+
/** Additional WebSocket headers */
|
|
150
|
+
headers?: Record<string, string>;
|
|
151
|
+
/** Additional query parameters */
|
|
152
|
+
query?: Record<string, string>;
|
|
153
|
+
/** Enable automatic reconnection (default: true) */
|
|
154
|
+
reconnect?: boolean;
|
|
155
|
+
/** Maximum reconnection attempts (default: Infinity) */
|
|
156
|
+
maxReconnects?: number;
|
|
157
|
+
/** Back-off base delay in ms (default: 1000) */
|
|
158
|
+
backoffMin?: number;
|
|
159
|
+
/** Back-off max delay in ms (default: 30000) */
|
|
160
|
+
backoffMax?: number;
|
|
161
|
+
/** Call timeout in ms (default: 30000) */
|
|
162
|
+
callTimeoutMs?: number;
|
|
163
|
+
/** Ping interval in ms (default: 30000, 0 to disable) */
|
|
164
|
+
pingIntervalMs?: number;
|
|
165
|
+
/** Defer pings if activity detected (default: false) */
|
|
166
|
+
deferPingsOnActivity?: boolean;
|
|
167
|
+
/** Maximum concurrent outbound calls (default: 1) */
|
|
168
|
+
callConcurrency?: number;
|
|
169
|
+
/** Enable strict mode validation (default: false) */
|
|
170
|
+
strictMode?: boolean | string[];
|
|
171
|
+
/** Custom validators for strict mode */
|
|
172
|
+
strictModeValidators?: Validator[];
|
|
173
|
+
/** Max number of bad messages before closing (default: Infinity) */
|
|
174
|
+
maxBadMessages?: number;
|
|
175
|
+
/** Include error details in responses (default: false) */
|
|
176
|
+
respondWithDetailedErrors?: boolean;
|
|
177
|
+
}
|
|
178
|
+
interface ServerOptions {
|
|
179
|
+
/** OCPP Security Profile (default: NONE) */
|
|
180
|
+
securityProfile?: SecurityProfile;
|
|
181
|
+
/** TLS options for HTTPS server (Profile 2 & 3) */
|
|
182
|
+
tls?: TLSOptions;
|
|
183
|
+
/** Accepted OCPP subprotocols */
|
|
184
|
+
protocols?: string[];
|
|
185
|
+
/** Call timeout in ms — inherited by server clients (default: 30000) */
|
|
186
|
+
callTimeoutMs?: number;
|
|
187
|
+
/** Ping interval in ms — inherited by server clients (default: 30000) */
|
|
188
|
+
pingIntervalMs?: number;
|
|
189
|
+
/** Defer pings if activity detected — inherited (default: false) */
|
|
190
|
+
deferPingsOnActivity?: boolean;
|
|
191
|
+
/** Max concurrent outbound calls — inherited (default: 1) */
|
|
192
|
+
callConcurrency?: number;
|
|
193
|
+
/** Enable strict mode — inherited (default: false) */
|
|
194
|
+
strictMode?: boolean | string[];
|
|
195
|
+
/** Custom validators — inherited */
|
|
196
|
+
strictModeValidators?: Validator[];
|
|
197
|
+
/** Max bad messages — inherited (default: Infinity) */
|
|
198
|
+
maxBadMessages?: number;
|
|
199
|
+
/** Include error details in responses — inherited (default: false) */
|
|
200
|
+
respondWithDetailedErrors?: boolean;
|
|
201
|
+
}
|
|
202
|
+
interface ListenOptions {
|
|
203
|
+
/** Existing HTTP/HTTPS server to attach to */
|
|
204
|
+
server?: node_http.Server | node_https.Server;
|
|
205
|
+
/** Hostname to bind to */
|
|
206
|
+
host?: string;
|
|
207
|
+
/** Signal to abort the listen */
|
|
208
|
+
signal?: AbortSignal;
|
|
209
|
+
}
|
|
210
|
+
interface AuthAccept<TSession = Record<string, unknown>> {
|
|
211
|
+
/** Subprotocol to use for this client */
|
|
212
|
+
protocol?: string;
|
|
213
|
+
/** Session data attached to the client */
|
|
214
|
+
session?: TSession;
|
|
215
|
+
}
|
|
216
|
+
type AuthCallback<TSession = Record<string, unknown>> = (accept: (options?: AuthAccept<TSession>) => void, reject: (code?: number, message?: string) => void, handshake: HandshakeInfo, signal: AbortSignal) => void | Promise<void>;
|
|
217
|
+
interface ClientEvents {
|
|
218
|
+
open: [{
|
|
219
|
+
response: IncomingMessage;
|
|
220
|
+
}];
|
|
221
|
+
close: [{
|
|
222
|
+
code: number;
|
|
223
|
+
reason: string;
|
|
224
|
+
}];
|
|
225
|
+
error: [Error];
|
|
226
|
+
connecting: [{
|
|
227
|
+
url: string;
|
|
228
|
+
}];
|
|
229
|
+
reconnect: [{
|
|
230
|
+
attempt: number;
|
|
231
|
+
delay: number;
|
|
232
|
+
}];
|
|
233
|
+
message: [OCPPMessage];
|
|
234
|
+
call: [OCPPCall];
|
|
235
|
+
callResult: [OCPPCallResult];
|
|
236
|
+
callError: [OCPPCallError];
|
|
237
|
+
badMessage: [{
|
|
238
|
+
message: string;
|
|
239
|
+
error: Error;
|
|
240
|
+
}];
|
|
241
|
+
ping: [];
|
|
242
|
+
pong: [];
|
|
243
|
+
strictValidationFailure: [{
|
|
244
|
+
message: unknown;
|
|
245
|
+
error: Error;
|
|
246
|
+
}];
|
|
247
|
+
}
|
|
248
|
+
interface ServerEvents<TSession = Record<string, unknown>> {
|
|
249
|
+
client: [ServerClientInstance<TSession>];
|
|
250
|
+
error: [Error];
|
|
251
|
+
upgradeError: [{
|
|
252
|
+
error: Error;
|
|
253
|
+
socket: Duplex;
|
|
254
|
+
}];
|
|
255
|
+
}
|
|
256
|
+
type ServerClientInstance<TSession = Record<string, unknown>> = {
|
|
257
|
+
readonly identity: string;
|
|
258
|
+
readonly protocol: string | undefined;
|
|
259
|
+
readonly session: TSession;
|
|
260
|
+
readonly handshake: HandshakeInfo;
|
|
261
|
+
readonly state: ConnectionState;
|
|
262
|
+
close(options?: CloseOptions): Promise<{
|
|
263
|
+
code: number;
|
|
264
|
+
reason: string;
|
|
265
|
+
}>;
|
|
266
|
+
handle<TParams, TResult>(method: string, handler: CallHandler<TParams, TResult>): void;
|
|
267
|
+
handle(handler: WildcardHandler): void;
|
|
268
|
+
call<TResult>(method: string, params?: unknown, options?: CallOptions): Promise<TResult>;
|
|
269
|
+
removeHandler(method?: string): void;
|
|
270
|
+
removeAllHandlers(): void;
|
|
271
|
+
reconfigure(options: Partial<ClientOptions>): void;
|
|
272
|
+
on<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): void;
|
|
273
|
+
once<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): void;
|
|
274
|
+
off<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): void;
|
|
275
|
+
};
|
|
276
|
+
interface EventAdapterInterface {
|
|
277
|
+
publish(channel: string, data: unknown): Promise<void>;
|
|
278
|
+
subscribe(channel: string, handler: (data: unknown) => void): Promise<void>;
|
|
279
|
+
unsubscribe(channel: string): Promise<void>;
|
|
280
|
+
disconnect(): Promise<void>;
|
|
281
|
+
}
|
|
282
|
+
declare const NOREPLY: unique symbol;
|
|
283
|
+
|
|
284
|
+
export { type AuthCallback as A, type ClientOptions as C, type EventAdapterInterface as E, type HandshakeInfo as H, type ListenOptions as L, MessageType as M, NOREPLY as N, type OCPPCall as O, SecurityProfile as S, type TLSOptions as T, Validator as V, type WildcardHandler as W, ConnectionState as a, type CloseOptions as b, type CallHandler as c, type CallOptions as d, type ServerOptions as e, type AuthAccept as f, type ClientEvents as g, type HandlerContext as h, type OCPPCallError as i, type OCPPCallResult as j, type OCPPMessage as k, type OCPPProtocol as l, type ServerEvents as m, type SessionData as n, createValidator as o };
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import * as node_https from 'node:https';
|
|
2
|
+
import * as node_http from 'node:http';
|
|
3
|
+
import { IncomingMessage } from 'node:http';
|
|
4
|
+
import { TLSSocket } from 'node:tls';
|
|
5
|
+
import { Duplex } from 'node:stream';
|
|
6
|
+
import Ajv from 'ajv';
|
|
7
|
+
|
|
8
|
+
interface ValidatorSchema {
|
|
9
|
+
$schema?: string;
|
|
10
|
+
$id?: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Schema validator using AJV for OCPP message validation.
|
|
15
|
+
* Each validator is bound to a specific subprotocol version.
|
|
16
|
+
*/
|
|
17
|
+
declare class Validator {
|
|
18
|
+
readonly subprotocol: string;
|
|
19
|
+
/** @internal */
|
|
20
|
+
_ajv: Ajv;
|
|
21
|
+
constructor(subprotocol: string, schemas: ValidatorSchema[]);
|
|
22
|
+
/**
|
|
23
|
+
* Normalize a schema ID from OCPP URN format to internal path format.
|
|
24
|
+
*/
|
|
25
|
+
private _normalizeSchemaId;
|
|
26
|
+
/**
|
|
27
|
+
* Validate a payload against a schema identified by its $id.
|
|
28
|
+
* Throws a typed RPCError if validation fails.
|
|
29
|
+
*/
|
|
30
|
+
validate(schemaId: string, params: unknown): void;
|
|
31
|
+
/**
|
|
32
|
+
* Check if a schema exists for the given $id.
|
|
33
|
+
*/
|
|
34
|
+
hasSchema(schemaId: string): boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create a validator for a specific subprotocol version.
|
|
38
|
+
*/
|
|
39
|
+
declare function createValidator(subprotocol: string, schemas: ValidatorSchema[]): Validator;
|
|
40
|
+
|
|
41
|
+
type OCPPProtocol = "ocpp1.6" | "ocpp2.0.1" | "ocpp2.1";
|
|
42
|
+
declare const ConnectionState: {
|
|
43
|
+
readonly CONNECTING: 0;
|
|
44
|
+
readonly OPEN: 1;
|
|
45
|
+
readonly CLOSING: 2;
|
|
46
|
+
readonly CLOSED: 3;
|
|
47
|
+
};
|
|
48
|
+
type ConnectionState = (typeof ConnectionState)[keyof typeof ConnectionState];
|
|
49
|
+
declare enum SecurityProfile {
|
|
50
|
+
/** No security — plain WS, no auth (dev/testing only) */
|
|
51
|
+
NONE = 0,
|
|
52
|
+
/** Profile 1: Basic Auth over unsecured WS (ws://) — password-based */
|
|
53
|
+
BASIC_AUTH = 1,
|
|
54
|
+
/** Profile 2: TLS + Basic Auth (wss://) — server cert + password */
|
|
55
|
+
TLS_BASIC_AUTH = 2,
|
|
56
|
+
/** Profile 3: Mutual TLS (wss://) — client + server certificates */
|
|
57
|
+
TLS_CLIENT_CERT = 3
|
|
58
|
+
}
|
|
59
|
+
declare const MessageType: {
|
|
60
|
+
readonly CALL: 2;
|
|
61
|
+
readonly CALLRESULT: 3;
|
|
62
|
+
readonly CALLERROR: 4;
|
|
63
|
+
};
|
|
64
|
+
type MessageType = (typeof MessageType)[keyof typeof MessageType];
|
|
65
|
+
type OCPPCall<T = unknown> = [2, string, string, T];
|
|
66
|
+
type OCPPCallResult<T = unknown> = [3, string, T];
|
|
67
|
+
type OCPPCallError = [
|
|
68
|
+
4,
|
|
69
|
+
string,
|
|
70
|
+
string,
|
|
71
|
+
string,
|
|
72
|
+
Record<string, unknown>
|
|
73
|
+
];
|
|
74
|
+
type OCPPMessage<T = unknown> = OCPPCall<T> | OCPPCallResult<T> | OCPPCallError;
|
|
75
|
+
interface TLSOptions {
|
|
76
|
+
/** Server/client certificate (PEM) */
|
|
77
|
+
cert?: string | Buffer;
|
|
78
|
+
/** Private key (PEM) */
|
|
79
|
+
key?: string | Buffer;
|
|
80
|
+
/** CA certificate(s) for verification */
|
|
81
|
+
ca?: string | Buffer | Array<string | Buffer>;
|
|
82
|
+
/** Reject unauthorized certs (default: true) */
|
|
83
|
+
rejectUnauthorized?: boolean;
|
|
84
|
+
/** Passphrase for encrypted private key */
|
|
85
|
+
passphrase?: string;
|
|
86
|
+
}
|
|
87
|
+
interface HandlerContext<T = unknown> {
|
|
88
|
+
messageId: string;
|
|
89
|
+
method: string;
|
|
90
|
+
params: T;
|
|
91
|
+
signal: AbortSignal;
|
|
92
|
+
}
|
|
93
|
+
type CallHandler<TParams = unknown, TResult = unknown> = (context: HandlerContext<TParams>) => TResult | Promise<TResult>;
|
|
94
|
+
type WildcardHandler = (method: string, context: HandlerContext) => unknown | Promise<unknown>;
|
|
95
|
+
interface CallOptions {
|
|
96
|
+
/** Timeout in milliseconds for this specific call */
|
|
97
|
+
timeoutMs?: number;
|
|
98
|
+
/** Abort signal */
|
|
99
|
+
signal?: AbortSignal;
|
|
100
|
+
/** Suppress sending a response (server-side, NOREPLY) */
|
|
101
|
+
noReply?: boolean;
|
|
102
|
+
}
|
|
103
|
+
interface CloseOptions {
|
|
104
|
+
/** WebSocket close code (default: 1000) */
|
|
105
|
+
code?: number;
|
|
106
|
+
/** Close reason string */
|
|
107
|
+
reason?: string;
|
|
108
|
+
/** Wait for pending calls to complete before closing */
|
|
109
|
+
awaitPending?: boolean;
|
|
110
|
+
/** Force-close without waiting */
|
|
111
|
+
force?: boolean;
|
|
112
|
+
}
|
|
113
|
+
interface HandshakeInfo {
|
|
114
|
+
/** Charging station identity (from URL path) */
|
|
115
|
+
identity: string;
|
|
116
|
+
/** Remote IP address */
|
|
117
|
+
remoteAddress: string;
|
|
118
|
+
/** Request headers */
|
|
119
|
+
headers: Record<string, string | string[] | undefined>;
|
|
120
|
+
/** Negotiated subprotocols */
|
|
121
|
+
protocols: Set<string>;
|
|
122
|
+
/** Request endpoint URL path */
|
|
123
|
+
endpoint: string;
|
|
124
|
+
/** URL query parameters */
|
|
125
|
+
query: URLSearchParams;
|
|
126
|
+
/** Original HTTP request */
|
|
127
|
+
request: IncomingMessage;
|
|
128
|
+
/** Password from Basic Auth (Profile 1 & 2) */
|
|
129
|
+
password?: Buffer;
|
|
130
|
+
/** Client certificate (Profile 3 — mTLS) */
|
|
131
|
+
clientCertificate?: ReturnType<TLSSocket["getPeerCertificate"]>;
|
|
132
|
+
/** Active security profile */
|
|
133
|
+
securityProfile: SecurityProfile;
|
|
134
|
+
}
|
|
135
|
+
type SessionData<T = Record<string, unknown>> = T;
|
|
136
|
+
interface ClientOptions {
|
|
137
|
+
/** Unique identity for this client (charging station ID) */
|
|
138
|
+
identity: string;
|
|
139
|
+
/** WebSocket endpoint URL (ws:// or wss://) */
|
|
140
|
+
endpoint: string;
|
|
141
|
+
/** OCPP Security Profile (default: NONE) */
|
|
142
|
+
securityProfile?: SecurityProfile;
|
|
143
|
+
/** Password for Basic Auth (Profile 1 & 2) */
|
|
144
|
+
password?: string | Buffer;
|
|
145
|
+
/** TLS options (Profile 2 & 3) */
|
|
146
|
+
tls?: TLSOptions;
|
|
147
|
+
/** OCPP subprotocols to negotiate */
|
|
148
|
+
protocols?: string[];
|
|
149
|
+
/** Additional WebSocket headers */
|
|
150
|
+
headers?: Record<string, string>;
|
|
151
|
+
/** Additional query parameters */
|
|
152
|
+
query?: Record<string, string>;
|
|
153
|
+
/** Enable automatic reconnection (default: true) */
|
|
154
|
+
reconnect?: boolean;
|
|
155
|
+
/** Maximum reconnection attempts (default: Infinity) */
|
|
156
|
+
maxReconnects?: number;
|
|
157
|
+
/** Back-off base delay in ms (default: 1000) */
|
|
158
|
+
backoffMin?: number;
|
|
159
|
+
/** Back-off max delay in ms (default: 30000) */
|
|
160
|
+
backoffMax?: number;
|
|
161
|
+
/** Call timeout in ms (default: 30000) */
|
|
162
|
+
callTimeoutMs?: number;
|
|
163
|
+
/** Ping interval in ms (default: 30000, 0 to disable) */
|
|
164
|
+
pingIntervalMs?: number;
|
|
165
|
+
/** Defer pings if activity detected (default: false) */
|
|
166
|
+
deferPingsOnActivity?: boolean;
|
|
167
|
+
/** Maximum concurrent outbound calls (default: 1) */
|
|
168
|
+
callConcurrency?: number;
|
|
169
|
+
/** Enable strict mode validation (default: false) */
|
|
170
|
+
strictMode?: boolean | string[];
|
|
171
|
+
/** Custom validators for strict mode */
|
|
172
|
+
strictModeValidators?: Validator[];
|
|
173
|
+
/** Max number of bad messages before closing (default: Infinity) */
|
|
174
|
+
maxBadMessages?: number;
|
|
175
|
+
/** Include error details in responses (default: false) */
|
|
176
|
+
respondWithDetailedErrors?: boolean;
|
|
177
|
+
}
|
|
178
|
+
interface ServerOptions {
|
|
179
|
+
/** OCPP Security Profile (default: NONE) */
|
|
180
|
+
securityProfile?: SecurityProfile;
|
|
181
|
+
/** TLS options for HTTPS server (Profile 2 & 3) */
|
|
182
|
+
tls?: TLSOptions;
|
|
183
|
+
/** Accepted OCPP subprotocols */
|
|
184
|
+
protocols?: string[];
|
|
185
|
+
/** Call timeout in ms — inherited by server clients (default: 30000) */
|
|
186
|
+
callTimeoutMs?: number;
|
|
187
|
+
/** Ping interval in ms — inherited by server clients (default: 30000) */
|
|
188
|
+
pingIntervalMs?: number;
|
|
189
|
+
/** Defer pings if activity detected — inherited (default: false) */
|
|
190
|
+
deferPingsOnActivity?: boolean;
|
|
191
|
+
/** Max concurrent outbound calls — inherited (default: 1) */
|
|
192
|
+
callConcurrency?: number;
|
|
193
|
+
/** Enable strict mode — inherited (default: false) */
|
|
194
|
+
strictMode?: boolean | string[];
|
|
195
|
+
/** Custom validators — inherited */
|
|
196
|
+
strictModeValidators?: Validator[];
|
|
197
|
+
/** Max bad messages — inherited (default: Infinity) */
|
|
198
|
+
maxBadMessages?: number;
|
|
199
|
+
/** Include error details in responses — inherited (default: false) */
|
|
200
|
+
respondWithDetailedErrors?: boolean;
|
|
201
|
+
}
|
|
202
|
+
interface ListenOptions {
|
|
203
|
+
/** Existing HTTP/HTTPS server to attach to */
|
|
204
|
+
server?: node_http.Server | node_https.Server;
|
|
205
|
+
/** Hostname to bind to */
|
|
206
|
+
host?: string;
|
|
207
|
+
/** Signal to abort the listen */
|
|
208
|
+
signal?: AbortSignal;
|
|
209
|
+
}
|
|
210
|
+
interface AuthAccept<TSession = Record<string, unknown>> {
|
|
211
|
+
/** Subprotocol to use for this client */
|
|
212
|
+
protocol?: string;
|
|
213
|
+
/** Session data attached to the client */
|
|
214
|
+
session?: TSession;
|
|
215
|
+
}
|
|
216
|
+
type AuthCallback<TSession = Record<string, unknown>> = (accept: (options?: AuthAccept<TSession>) => void, reject: (code?: number, message?: string) => void, handshake: HandshakeInfo, signal: AbortSignal) => void | Promise<void>;
|
|
217
|
+
interface ClientEvents {
|
|
218
|
+
open: [{
|
|
219
|
+
response: IncomingMessage;
|
|
220
|
+
}];
|
|
221
|
+
close: [{
|
|
222
|
+
code: number;
|
|
223
|
+
reason: string;
|
|
224
|
+
}];
|
|
225
|
+
error: [Error];
|
|
226
|
+
connecting: [{
|
|
227
|
+
url: string;
|
|
228
|
+
}];
|
|
229
|
+
reconnect: [{
|
|
230
|
+
attempt: number;
|
|
231
|
+
delay: number;
|
|
232
|
+
}];
|
|
233
|
+
message: [OCPPMessage];
|
|
234
|
+
call: [OCPPCall];
|
|
235
|
+
callResult: [OCPPCallResult];
|
|
236
|
+
callError: [OCPPCallError];
|
|
237
|
+
badMessage: [{
|
|
238
|
+
message: string;
|
|
239
|
+
error: Error;
|
|
240
|
+
}];
|
|
241
|
+
ping: [];
|
|
242
|
+
pong: [];
|
|
243
|
+
strictValidationFailure: [{
|
|
244
|
+
message: unknown;
|
|
245
|
+
error: Error;
|
|
246
|
+
}];
|
|
247
|
+
}
|
|
248
|
+
interface ServerEvents<TSession = Record<string, unknown>> {
|
|
249
|
+
client: [ServerClientInstance<TSession>];
|
|
250
|
+
error: [Error];
|
|
251
|
+
upgradeError: [{
|
|
252
|
+
error: Error;
|
|
253
|
+
socket: Duplex;
|
|
254
|
+
}];
|
|
255
|
+
}
|
|
256
|
+
type ServerClientInstance<TSession = Record<string, unknown>> = {
|
|
257
|
+
readonly identity: string;
|
|
258
|
+
readonly protocol: string | undefined;
|
|
259
|
+
readonly session: TSession;
|
|
260
|
+
readonly handshake: HandshakeInfo;
|
|
261
|
+
readonly state: ConnectionState;
|
|
262
|
+
close(options?: CloseOptions): Promise<{
|
|
263
|
+
code: number;
|
|
264
|
+
reason: string;
|
|
265
|
+
}>;
|
|
266
|
+
handle<TParams, TResult>(method: string, handler: CallHandler<TParams, TResult>): void;
|
|
267
|
+
handle(handler: WildcardHandler): void;
|
|
268
|
+
call<TResult>(method: string, params?: unknown, options?: CallOptions): Promise<TResult>;
|
|
269
|
+
removeHandler(method?: string): void;
|
|
270
|
+
removeAllHandlers(): void;
|
|
271
|
+
reconfigure(options: Partial<ClientOptions>): void;
|
|
272
|
+
on<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): void;
|
|
273
|
+
once<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): void;
|
|
274
|
+
off<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): void;
|
|
275
|
+
};
|
|
276
|
+
interface EventAdapterInterface {
|
|
277
|
+
publish(channel: string, data: unknown): Promise<void>;
|
|
278
|
+
subscribe(channel: string, handler: (data: unknown) => void): Promise<void>;
|
|
279
|
+
unsubscribe(channel: string): Promise<void>;
|
|
280
|
+
disconnect(): Promise<void>;
|
|
281
|
+
}
|
|
282
|
+
declare const NOREPLY: unique symbol;
|
|
283
|
+
|
|
284
|
+
export { type AuthCallback as A, type ClientOptions as C, type EventAdapterInterface as E, type HandshakeInfo as H, type ListenOptions as L, MessageType as M, NOREPLY as N, type OCPPCall as O, SecurityProfile as S, type TLSOptions as T, Validator as V, type WildcardHandler as W, ConnectionState as a, type CloseOptions as b, type CallHandler as c, type CallOptions as d, type ServerOptions as e, type AuthAccept as f, type ClientEvents as g, type HandlerContext as h, type OCPPCallError as i, type OCPPCallResult as j, type OCPPMessage as k, type OCPPProtocol as l, type ServerEvents as m, type SessionData as n, createValidator as o };
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ocpp-ws-io",
|
|
3
|
+
"version": "1.0.0-alpha",
|
|
4
|
+
"description": "Type-safe OCPP WebSocket RPC client & server with pub/sub, session management, and clustering support",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/rohittiwari-dev/ocpp-ws-io"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"module": "dist/index.mjs",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.mjs",
|
|
16
|
+
"require": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./adapters/redis": {
|
|
19
|
+
"types": "./dist/adapters/redis.d.ts",
|
|
20
|
+
"import": "./dist/adapters/redis.mjs",
|
|
21
|
+
"require": "./dist/adapters/redis.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"ocpp",
|
|
35
|
+
"websocket",
|
|
36
|
+
"rpc",
|
|
37
|
+
"ocpp1.6",
|
|
38
|
+
"ocpp2.0.1",
|
|
39
|
+
"ocpp2.1",
|
|
40
|
+
"ev-charging",
|
|
41
|
+
"iot",
|
|
42
|
+
"pubsub",
|
|
43
|
+
"clustering",
|
|
44
|
+
"type-safe"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"ajv": "^8.17.1",
|
|
49
|
+
"ajv-formats": "^3.0.1",
|
|
50
|
+
"ws": "^8.18.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.13.4",
|
|
54
|
+
"@types/ws": "^8.5.13",
|
|
55
|
+
"tsup": "^8.3.6",
|
|
56
|
+
"typescript": "^5.7.3",
|
|
57
|
+
"vitest": "^3.0.5"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { EventAdapterInterface } from "../types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory event adapter for single-process use.
|
|
5
|
+
* Events are dispatched synchronously within the same process.
|
|
6
|
+
*/
|
|
7
|
+
export class InMemoryAdapter implements EventAdapterInterface {
|
|
8
|
+
private _channels = new Map<string, Set<(data: unknown) => void>>();
|
|
9
|
+
|
|
10
|
+
async publish(channel: string, data: unknown): Promise<void> {
|
|
11
|
+
const handlers = this._channels.get(channel);
|
|
12
|
+
if (handlers) {
|
|
13
|
+
for (const handler of handlers) {
|
|
14
|
+
try {
|
|
15
|
+
handler(data);
|
|
16
|
+
} catch {
|
|
17
|
+
// Swallow handler errors
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async subscribe(
|
|
24
|
+
channel: string,
|
|
25
|
+
handler: (data: unknown) => void,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
if (!this._channels.has(channel)) {
|
|
28
|
+
this._channels.set(channel, new Set());
|
|
29
|
+
}
|
|
30
|
+
this._channels.get(channel)!.add(handler);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async unsubscribe(channel: string): Promise<void> {
|
|
34
|
+
this._channels.delete(channel);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async disconnect(): Promise<void> {
|
|
38
|
+
this._channels.clear();
|
|
39
|
+
}
|
|
40
|
+
}
|