@homaiohq/napi-quickfix 0.1.0 → 0.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.
Files changed (145) hide show
  1. package/CMakeLists.txt +5 -1
  2. package/README.md +344 -18
  3. package/cpp/acceptor_wrap.cpp +253 -19
  4. package/cpp/acceptor_wrap.h +60 -1
  5. package/cpp/addon.cpp +10 -6
  6. package/cpp/application_bridge.cpp +114 -30
  7. package/cpp/application_bridge.h +57 -4
  8. package/cpp/data_dictionary_wrap.cpp +89 -3
  9. package/cpp/data_dictionary_wrap.h +27 -5
  10. package/cpp/engine_common.h +108 -0
  11. package/cpp/engine_workers.h +99 -18
  12. package/cpp/field_map_ops.h +492 -0
  13. package/cpp/group_wrap.cpp +221 -0
  14. package/cpp/group_wrap.h +76 -0
  15. package/cpp/initiator_wrap.cpp +262 -25
  16. package/cpp/initiator_wrap.h +37 -2
  17. package/cpp/message_wrap.cpp +171 -78
  18. package/cpp/message_wrap.h +47 -5
  19. package/cpp/session_op_gate.h +201 -0
  20. package/cpp/session_static.cpp +139 -12
  21. package/cpp/session_wrap.cpp +438 -0
  22. package/cpp/session_wrap.h +144 -0
  23. package/dist/cjs/acceptor.d.ts +0 -1
  24. package/dist/cjs/acceptor.js +0 -1
  25. package/dist/cjs/application.d.ts +0 -1
  26. package/dist/cjs/application.js +0 -1
  27. package/dist/cjs/data-dictionary.d.ts +21 -3
  28. package/dist/cjs/data-dictionary.js +32 -4
  29. package/dist/cjs/engine.d.ts +42 -4
  30. package/dist/cjs/engine.js +50 -5
  31. package/dist/cjs/enums.d.ts +48 -14
  32. package/dist/cjs/enums.js +20 -25
  33. package/dist/cjs/field-value.d.ts +12 -0
  34. package/dist/cjs/field-value.js +40 -0
  35. package/dist/cjs/generated/fields.d.ts +0 -1
  36. package/dist/cjs/generated/fields.js +0 -1
  37. package/dist/cjs/generated/values.d.ts +8548 -0
  38. package/dist/cjs/generated/values.js +8580 -0
  39. package/dist/cjs/group.d.ts +126 -0
  40. package/dist/cjs/group.js +185 -0
  41. package/dist/cjs/index.d.ts +28 -5
  42. package/dist/cjs/index.js +30 -28
  43. package/dist/cjs/initiator.d.ts +0 -1
  44. package/dist/cjs/initiator.js +0 -1
  45. package/dist/cjs/message.d.ts +129 -5
  46. package/dist/cjs/message.js +146 -9
  47. package/dist/cjs/native.d.ts +179 -17
  48. package/dist/cjs/native.js +0 -1
  49. package/dist/cjs/session-id.d.ts +12 -1
  50. package/dist/cjs/session-id.js +21 -1
  51. package/dist/cjs/session-settings.d.ts +0 -1
  52. package/dist/cjs/session-settings.js +0 -1
  53. package/dist/cjs/session.d.ts +213 -0
  54. package/dist/cjs/session.js +337 -0
  55. package/dist/cjs/tls-tunnel.d.ts +143 -0
  56. package/dist/cjs/tls-tunnel.js +261 -0
  57. package/dist/esm/acceptor.d.ts +0 -1
  58. package/dist/esm/acceptor.js +0 -1
  59. package/dist/esm/application.d.ts +0 -1
  60. package/dist/esm/application.js +0 -1
  61. package/dist/esm/data-dictionary.d.ts +21 -3
  62. package/dist/esm/data-dictionary.js +32 -4
  63. package/dist/esm/engine.d.ts +42 -4
  64. package/dist/esm/engine.js +51 -6
  65. package/dist/esm/enums.d.ts +48 -14
  66. package/dist/esm/enums.js +14 -24
  67. package/dist/esm/field-value.d.ts +12 -0
  68. package/dist/esm/field-value.js +37 -0
  69. package/dist/esm/generated/fields.d.ts +0 -1
  70. package/dist/esm/generated/fields.js +0 -1
  71. package/dist/esm/generated/values.d.ts +8548 -0
  72. package/dist/esm/generated/values.js +8564 -0
  73. package/dist/esm/group.d.ts +126 -0
  74. package/dist/esm/group.js +181 -0
  75. package/dist/esm/index.d.ts +28 -5
  76. package/dist/esm/index.js +18 -26
  77. package/dist/esm/initiator.d.ts +0 -1
  78. package/dist/esm/initiator.js +0 -1
  79. package/dist/esm/message.d.ts +129 -5
  80. package/dist/esm/message.js +146 -9
  81. package/dist/esm/native.d.ts +179 -17
  82. package/dist/esm/native.js +0 -1
  83. package/dist/esm/session-id.d.ts +12 -1
  84. package/dist/esm/session-id.js +20 -1
  85. package/dist/esm/session-settings.d.ts +0 -1
  86. package/dist/esm/session-settings.js +0 -1
  87. package/dist/esm/session.d.ts +213 -0
  88. package/dist/esm/session.js +329 -0
  89. package/dist/esm/tls-tunnel.d.ts +143 -0
  90. package/dist/esm/tls-tunnel.js +256 -0
  91. package/package.json +13 -1
  92. package/prebuilds/darwin-arm64/node.napi.node +0 -0
  93. package/prebuilds/darwin-x64/node.napi.node +0 -0
  94. package/prebuilds/linux-x64/node.napi.glibc.node +0 -0
  95. package/prebuilds/linux-x64/node.napi.musl.node +0 -0
  96. package/prebuilds/win32-x64/node.napi.node +0 -0
  97. package/cpp/enums.cpp +0 -66
  98. package/dist/cjs/acceptor.d.ts.map +0 -1
  99. package/dist/cjs/acceptor.js.map +0 -1
  100. package/dist/cjs/application.d.ts.map +0 -1
  101. package/dist/cjs/application.js.map +0 -1
  102. package/dist/cjs/data-dictionary.d.ts.map +0 -1
  103. package/dist/cjs/data-dictionary.js.map +0 -1
  104. package/dist/cjs/engine.d.ts.map +0 -1
  105. package/dist/cjs/engine.js.map +0 -1
  106. package/dist/cjs/enums.d.ts.map +0 -1
  107. package/dist/cjs/enums.js.map +0 -1
  108. package/dist/cjs/generated/fields.d.ts.map +0 -1
  109. package/dist/cjs/generated/fields.js.map +0 -1
  110. package/dist/cjs/index.d.ts.map +0 -1
  111. package/dist/cjs/index.js.map +0 -1
  112. package/dist/cjs/initiator.d.ts.map +0 -1
  113. package/dist/cjs/initiator.js.map +0 -1
  114. package/dist/cjs/message.d.ts.map +0 -1
  115. package/dist/cjs/message.js.map +0 -1
  116. package/dist/cjs/native.d.ts.map +0 -1
  117. package/dist/cjs/native.js.map +0 -1
  118. package/dist/cjs/session-id.d.ts.map +0 -1
  119. package/dist/cjs/session-id.js.map +0 -1
  120. package/dist/cjs/session-settings.d.ts.map +0 -1
  121. package/dist/cjs/session-settings.js.map +0 -1
  122. package/dist/esm/acceptor.d.ts.map +0 -1
  123. package/dist/esm/acceptor.js.map +0 -1
  124. package/dist/esm/application.d.ts.map +0 -1
  125. package/dist/esm/application.js.map +0 -1
  126. package/dist/esm/data-dictionary.d.ts.map +0 -1
  127. package/dist/esm/data-dictionary.js.map +0 -1
  128. package/dist/esm/engine.d.ts.map +0 -1
  129. package/dist/esm/engine.js.map +0 -1
  130. package/dist/esm/enums.d.ts.map +0 -1
  131. package/dist/esm/enums.js.map +0 -1
  132. package/dist/esm/generated/fields.d.ts.map +0 -1
  133. package/dist/esm/generated/fields.js.map +0 -1
  134. package/dist/esm/index.d.ts.map +0 -1
  135. package/dist/esm/index.js.map +0 -1
  136. package/dist/esm/initiator.d.ts.map +0 -1
  137. package/dist/esm/initiator.js.map +0 -1
  138. package/dist/esm/message.d.ts.map +0 -1
  139. package/dist/esm/message.js.map +0 -1
  140. package/dist/esm/native.d.ts.map +0 -1
  141. package/dist/esm/native.js.map +0 -1
  142. package/dist/esm/session-id.d.ts.map +0 -1
  143. package/dist/esm/session-id.js.map +0 -1
  144. package/dist/esm/session-settings.d.ts.map +0 -1
  145. package/dist/esm/session-settings.js.map +0 -1
@@ -0,0 +1,143 @@
1
+ /**
2
+ * In-process TLS tunnel for initiators: the stunnel client model.
3
+ *
4
+ * QuickFIX is built without its OpenSSL-based `SSLSocketInitiator` (see
5
+ * CMakeLists.txt), so TLS is terminated in Node instead: the tunnel listens on
6
+ * a loopback port, the plain `FIX::SocketInitiator` connects to it (point
7
+ * `SocketConnectHost`/`SocketConnectPort` at {@link TlsTunnel.host} /
8
+ * {@link TlsTunnel.port}), and every accepted connection is carried to the real
9
+ * counterparty over `tls.connect`.
10
+ */
11
+ import { EventEmitter } from 'node:events';
12
+ import { type Server } from 'node:net';
13
+ import { type CipherNameAndProtocol, type ConnectionOptions, type PeerCertificate } from 'node:tls';
14
+ /** Options for {@link createTlsTunnel}. */
15
+ export interface TlsTunnelOptions {
16
+ /** Host name or IP of the counterparty's TLS endpoint. */
17
+ host: string;
18
+ /** Port of the counterparty's TLS endpoint. */
19
+ port: number;
20
+ /**
21
+ * Passed to `tls.connect()` for every connection (CA, client certificate for
22
+ * mTLS, `minVersion`, ciphers, ...). `servername` (SNI, and the name the
23
+ * server certificate is checked against) defaults to `host` unless `host` is
24
+ * an IP address.
25
+ */
26
+ tls?: Omit<ConnectionOptions, 'host' | 'port' | 'socket' | 'path'>;
27
+ /**
28
+ * Loopback port to listen on. Defaults to `0`, an ephemeral port read back
29
+ * from {@link TlsTunnel.port}. Set it to point a static `.cfg` file at the
30
+ * tunnel, like stunnel's `accept = 127.0.0.1:<port>`.
31
+ */
32
+ localPort?: number;
33
+ }
34
+ /** Details of an established TLS connection, emitted as `'secureConnect'`. */
35
+ export interface TlsTunnelSecureConnectInfo {
36
+ /** Whether the server certificate was verified against the trusted CAs. */
37
+ authorized: boolean;
38
+ /**
39
+ * Why verification failed, as the OpenSSL verification code (e.g.
40
+ * `'UNABLE_TO_VERIFY_LEAF_SIGNATURE'`, the `code` a `'tlsError'` carries
41
+ * otherwise). Only reachable with `rejectUnauthorized: false`.
42
+ */
43
+ authorizationError?: string;
44
+ /** Negotiated protocol, e.g. `'TLSv1.3'`. */
45
+ protocol: string | null;
46
+ /** Negotiated cipher. */
47
+ cipher: CipherNameAndProtocol;
48
+ /** The server's certificate (an empty object if it sent none). */
49
+ peerCertificate: PeerCertificate;
50
+ }
51
+ /**
52
+ * Events emitted by a {@link TlsTunnel}. There is deliberately no `'error'`
53
+ * event: connection failures are reported as `'tlsError'`, so a tunnel without
54
+ * listeners never crashes the process.
55
+ */
56
+ export interface TlsTunnelEvents {
57
+ /** A connection to the counterparty completed its TLS handshake. */
58
+ secureConnect: [info: TlsTunnelSecureConnectInfo];
59
+ /**
60
+ * A connection to the counterparty failed (DNS, TCP, handshake, certificate
61
+ * verification, `tls` options `tls.connect` rejects) or broke. The
62
+ * initiator's loopback connection is closed with it, so QuickFIX reconnects
63
+ * after its `ReconnectInterval`. Also emitted when the loopback server fails
64
+ * to accept a connection (e.g. `EMFILE`).
65
+ */
66
+ tlsError: [error: Error];
67
+ }
68
+ /**
69
+ * A running TLS tunnel, created by {@link createTlsTunnel}.
70
+ *
71
+ * Its handles are unref'd: the tunnel alone never keeps the process alive (a
72
+ * started engine does). Close it after the engine has stopped, so a graceful
73
+ * Logout still reaches the counterparty.
74
+ */
75
+ export declare class TlsTunnel extends EventEmitter {
76
+ #private;
77
+ /** Loopback address the tunnel listens on (`SocketConnectHost`). */
78
+ readonly host = "127.0.0.1";
79
+ /** Loopback port the tunnel listens on (`SocketConnectPort`). */
80
+ readonly port: number;
81
+ /** The counterparty's TLS endpoint. */
82
+ readonly remote: {
83
+ readonly host: string;
84
+ readonly port: number;
85
+ };
86
+ /** @internal Use {@link createTlsTunnel}. */
87
+ constructor(server: Server, port: number, options: TlsTunnelOptions);
88
+ /**
89
+ * Stop listening and destroy every live connection. Idempotent: later calls
90
+ * return the same Promise.
91
+ */
92
+ close(): Promise<void>;
93
+ /** Same as {@link TlsTunnel.close}; enables `await using tunnel = ...`. */
94
+ [Symbol.asyncDispose](): Promise<void>;
95
+ }
96
+ /**
97
+ * Open a TLS tunnel to `options.host`:`options.port` and resolve once it
98
+ * listens on a loopback port.
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * const tunnel = await createTlsTunnel({
103
+ * host: 'fix.broker.com',
104
+ * port: 9443,
105
+ * tls: { ca: readFileSync('broker-ca.pem') },
106
+ * });
107
+ * tunnel.on('tlsError', (err) => console.error('TLS:', err.message));
108
+ * const settings = SessionSettings.fromString(`
109
+ * [DEFAULT]
110
+ * ConnectionType=initiator
111
+ * SocketConnectHost=${tunnel.host}
112
+ * SocketConnectPort=${tunnel.port}
113
+ * ...`);
114
+ * const initiator = new Initiator({ settings, handlers });
115
+ * await initiator.start();
116
+ * // ... later: engine first, so the Logout still crosses the tunnel
117
+ * await initiator.stop();
118
+ * await tunnel.close();
119
+ * ```
120
+ *
121
+ * @returns A Promise that rejects with a `TypeError` if `host`, `port` or
122
+ * `localPort` is invalid, and with the listen error (e.g. `EADDRINUSE` for a
123
+ * fixed `localPort` already in use) if the loopback port cannot be bound.
124
+ */
125
+ export declare function createTlsTunnel(options: TlsTunnelOptions): Promise<TlsTunnel>;
126
+ export interface TlsTunnel {
127
+ /** Register a listener for a tunnel event (typed per {@link TlsTunnelEvents}). */
128
+ on<E extends keyof TlsTunnelEvents>(event: E, listener: (...args: TlsTunnelEvents[E]) => void): this;
129
+ /** Register a one-shot listener for a tunnel event. */
130
+ once<E extends keyof TlsTunnelEvents>(event: E, listener: (...args: TlsTunnelEvents[E]) => void): this;
131
+ /** Alias of {@link TlsTunnel.on}. */
132
+ addListener<E extends keyof TlsTunnelEvents>(event: E, listener: (...args: TlsTunnelEvents[E]) => void): this;
133
+ /** Remove a previously registered listener. */
134
+ off<E extends keyof TlsTunnelEvents>(event: E, listener: (...args: TlsTunnelEvents[E]) => void): this;
135
+ /** Alias of {@link TlsTunnel.off}. */
136
+ removeListener<E extends keyof TlsTunnelEvents>(event: E, listener: (...args: TlsTunnelEvents[E]) => void): this;
137
+ /** Remove all listeners (optionally for a single event). */
138
+ removeAllListeners<E extends keyof TlsTunnelEvents>(event?: E): this;
139
+ /** Emit a tunnel event with its typed arguments. */
140
+ emit<E extends keyof TlsTunnelEvents>(event: E, ...args: TlsTunnelEvents[E]): boolean;
141
+ /** List the listeners registered for an event. */
142
+ listeners<E extends keyof TlsTunnelEvents>(event: E): Array<(...args: TlsTunnelEvents[E]) => void>;
143
+ }
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TlsTunnel = void 0;
4
+ exports.createTlsTunnel = createTlsTunnel;
5
+ /**
6
+ * In-process TLS tunnel for initiators: the stunnel client model.
7
+ *
8
+ * QuickFIX is built without its OpenSSL-based `SSLSocketInitiator` (see
9
+ * CMakeLists.txt), so TLS is terminated in Node instead: the tunnel listens on
10
+ * a loopback port, the plain `FIX::SocketInitiator` connects to it (point
11
+ * `SocketConnectHost`/`SocketConnectPort` at {@link TlsTunnel.host} /
12
+ * {@link TlsTunnel.port}), and every accepted connection is carried to the real
13
+ * counterparty over `tls.connect`.
14
+ */
15
+ const node_events_1 = require("node:events");
16
+ const node_net_1 = require("node:net");
17
+ const node_tls_1 = require("node:tls");
18
+ const LOOPBACK = '127.0.0.1';
19
+ /** How long a connection may take to wind down once either side has ended. */
20
+ const DRAIN_TIMEOUT_MS = 5000;
21
+ /**
22
+ * A running TLS tunnel, created by {@link createTlsTunnel}.
23
+ *
24
+ * Its handles are unref'd: the tunnel alone never keeps the process alive (a
25
+ * started engine does). Close it after the engine has stopped, so a graceful
26
+ * Logout still reaches the counterparty.
27
+ */
28
+ class TlsTunnel extends node_events_1.EventEmitter {
29
+ /** Loopback address the tunnel listens on (`SocketConnectHost`). */
30
+ host = LOOPBACK;
31
+ /** Loopback port the tunnel listens on (`SocketConnectPort`). */
32
+ port;
33
+ /** The counterparty's TLS endpoint. */
34
+ remote;
35
+ #server;
36
+ #tls;
37
+ #pairs = new Set();
38
+ #closing;
39
+ /** @internal Use {@link createTlsTunnel}. */
40
+ constructor(server, port, options) {
41
+ super();
42
+ this.#server = server;
43
+ this.port = port;
44
+ this.remote = Object.freeze({ host: options.host, port: options.port });
45
+ // `socket` and `path` are excluded by the types only: drop them for JS
46
+ // callers too, or every connection would go to them instead of host:port.
47
+ const { socket: _socket, path: _path, ...tls } = (options.tls ?? {});
48
+ const servername = tls.servername ?? ((0, node_net_1.isIP)(options.host) === 0 ? options.host : undefined);
49
+ this.#tls = {
50
+ ...tls,
51
+ ...(servername === undefined ? {} : { servername }),
52
+ host: options.host,
53
+ port: options.port,
54
+ };
55
+ server.on('connection', (local) => this.#onConnection(local));
56
+ // A failed accept() (EMFILE, ENFILE, ...) is emitted on the server: without
57
+ // a listener it would be thrown.
58
+ server.on('error', (err) => {
59
+ if (!this.#closing)
60
+ this.emit('tlsError', err);
61
+ });
62
+ }
63
+ #onConnection(local) {
64
+ if (this.#closing) {
65
+ local.destroy();
66
+ return;
67
+ }
68
+ local.unref();
69
+ // QuickFIX writes its Logon as soon as it connects: hold it until the
70
+ // handshake is done and the pipe below takes over. It is read rather than
71
+ // left in a paused socket, which is not read at all: the engine's EOF or
72
+ // reset would go unseen, and a handshake completing after the engine gave
73
+ // up would still deliver its stale Logon.
74
+ const pending = [];
75
+ const hold = (chunk) => pending.push(chunk);
76
+ local.on('data', hold);
77
+ // tls.connect throws synchronously on options it cannot build a secure
78
+ // context from (bad PEM, unknown minVersion, ...): report it like any other
79
+ // connection failure instead of letting it escape the 'connection' handler.
80
+ let remote;
81
+ try {
82
+ remote = (0, node_tls_1.connect)(this.#tls);
83
+ }
84
+ catch (err) {
85
+ local.on('error', () => { });
86
+ local.destroy();
87
+ this.emit('tlsError', err instanceof Error ? err : new Error(String(err)));
88
+ return;
89
+ }
90
+ remote.unref();
91
+ const pair = { local, remote };
92
+ this.#pairs.add(pair);
93
+ // Once piped, a side that ends or closes is wound down gracefully on the
94
+ // other: it is ended, so the data still queued in the tunnel (a Logout, the
95
+ // TLS close_notify) is delivered rather than reset, and drained of what it
96
+ // still receives so it sees its peer's EOF. Anything still open
97
+ // DRAIN_TIMEOUT_MS after the first EOF is destroyed. Before the handshake
98
+ // completes there is nothing to deliver: an EOF or close drops both sides,
99
+ // and QuickFIX reconnects on its own schedule.
100
+ let piped = false;
101
+ let open = 2;
102
+ let drainTimer;
103
+ const linger = () => {
104
+ drainTimer ??= setTimeout(() => {
105
+ local.destroy();
106
+ remote.destroy();
107
+ }, DRAIN_TIMEOUT_MS).unref();
108
+ };
109
+ const onClose = (closed, other) => () => {
110
+ if (--open === 0) {
111
+ clearTimeout(drainTimer);
112
+ this.#pairs.delete(pair);
113
+ }
114
+ else if (!piped) {
115
+ other.destroy();
116
+ }
117
+ else {
118
+ other.unpipe(closed);
119
+ other.resume();
120
+ other.end();
121
+ linger();
122
+ }
123
+ };
124
+ local.on('error', () => {
125
+ // ECONNRESET and the like from the engine's side: nothing to report.
126
+ });
127
+ remote.on('error', (err) => {
128
+ if (!this.#closing)
129
+ this.emit('tlsError', err);
130
+ });
131
+ local.once('close', onClose(local, remote));
132
+ remote.once('close', onClose(remote, local));
133
+ local.once('end', () => {
134
+ if (piped) {
135
+ linger();
136
+ }
137
+ else {
138
+ local.destroy();
139
+ remote.destroy();
140
+ }
141
+ });
142
+ remote.once('secureConnect', () => {
143
+ local.setNoDelay(true);
144
+ remote.setNoDelay(true);
145
+ // Each side's EOF ends the other. The loopback socket is half-open, so
146
+ // after QuickFIX's EOF the counterparty's last bytes still reach it; the
147
+ // TLS socket ends itself on the counterparty's EOF, so what QuickFIX
148
+ // still sends is discarded (its peer has gone).
149
+ local.off('data', hold);
150
+ for (const chunk of pending)
151
+ remote.write(chunk);
152
+ pending.length = 0;
153
+ local.pipe(remote);
154
+ remote.pipe(local);
155
+ remote.once('end', () => {
156
+ local.unpipe(remote);
157
+ local.resume();
158
+ linger();
159
+ });
160
+ piped = true;
161
+ // Typed as an Error, but a string code at runtime, and null once the
162
+ // certificate verified.
163
+ const authorizationError = remote.authorizationError;
164
+ this.emit('secureConnect', {
165
+ authorized: remote.authorized,
166
+ ...(authorizationError ? { authorizationError } : {}),
167
+ protocol: remote.getProtocol(),
168
+ cipher: remote.getCipher(),
169
+ peerCertificate: remote.getPeerCertificate(),
170
+ });
171
+ });
172
+ }
173
+ /**
174
+ * Stop listening and destroy every live connection. Idempotent: later calls
175
+ * return the same Promise.
176
+ */
177
+ close() {
178
+ this.#closing ??= new Promise((resolve) => {
179
+ for (const { local, remote } of this.#pairs) {
180
+ local.destroy();
181
+ remote.destroy();
182
+ }
183
+ this.#pairs.clear();
184
+ // The only possible error is "not running", which still means closed.
185
+ this.#server.close(() => resolve());
186
+ });
187
+ return this.#closing;
188
+ }
189
+ /** Same as {@link TlsTunnel.close}; enables `await using tunnel = ...`. */
190
+ [Symbol.asyncDispose]() {
191
+ return this.close();
192
+ }
193
+ }
194
+ exports.TlsTunnel = TlsTunnel;
195
+ /**
196
+ * Open a TLS tunnel to `options.host`:`options.port` and resolve once it
197
+ * listens on a loopback port.
198
+ *
199
+ * @example
200
+ * ```ts
201
+ * const tunnel = await createTlsTunnel({
202
+ * host: 'fix.broker.com',
203
+ * port: 9443,
204
+ * tls: { ca: readFileSync('broker-ca.pem') },
205
+ * });
206
+ * tunnel.on('tlsError', (err) => console.error('TLS:', err.message));
207
+ * const settings = SessionSettings.fromString(`
208
+ * [DEFAULT]
209
+ * ConnectionType=initiator
210
+ * SocketConnectHost=${tunnel.host}
211
+ * SocketConnectPort=${tunnel.port}
212
+ * ...`);
213
+ * const initiator = new Initiator({ settings, handlers });
214
+ * await initiator.start();
215
+ * // ... later: engine first, so the Logout still crosses the tunnel
216
+ * await initiator.stop();
217
+ * await tunnel.close();
218
+ * ```
219
+ *
220
+ * @returns A Promise that rejects with a `TypeError` if `host`, `port` or
221
+ * `localPort` is invalid, and with the listen error (e.g. `EADDRINUSE` for a
222
+ * fixed `localPort` already in use) if the loopback port cannot be bound.
223
+ */
224
+ async function createTlsTunnel(options) {
225
+ if (typeof options !== 'object' || options === null) {
226
+ throw new TypeError('createTlsTunnel({ host, port, tls?, localPort? })');
227
+ }
228
+ if (typeof options.host !== 'string' || options.host === '') {
229
+ throw new TypeError('host must be a non-empty string');
230
+ }
231
+ if (!isPort(options.port, 1)) {
232
+ throw new TypeError('port must be an integer from 1 to 65535');
233
+ }
234
+ const localPort = options.localPort ?? 0;
235
+ if (!isPort(localPort, 0)) {
236
+ throw new TypeError('localPort must be an integer from 0 to 65535');
237
+ }
238
+ // Half-open, so QuickFIX's EOF does not end the loopback socket before the
239
+ // counterparty's last bytes have crossed it (see #onConnection).
240
+ const server = (0, node_net_1.createServer)({ allowHalfOpen: true });
241
+ await new Promise((resolve, reject) => {
242
+ const onError = (err) => {
243
+ server.off('listening', onListening);
244
+ reject(err);
245
+ };
246
+ const onListening = () => {
247
+ server.off('error', onError);
248
+ resolve();
249
+ };
250
+ server.once('error', onError);
251
+ server.once('listening', onListening);
252
+ server.listen(localPort, LOOPBACK);
253
+ });
254
+ server.unref();
255
+ const addr = server.address();
256
+ const port = typeof addr === 'object' && addr ? addr.port : localPort;
257
+ return new TlsTunnel(server, port, options);
258
+ }
259
+ function isPort(value, min) {
260
+ return Number.isInteger(value) && value >= min && value <= 65535;
261
+ }
@@ -27,4 +27,3 @@ export declare class Acceptor extends Engine {
27
27
  /** @param options Engine options. */
28
28
  constructor(options: EngineOptions);
29
29
  }
30
- //# sourceMappingURL=acceptor.d.ts.map
@@ -32,4 +32,3 @@ export class Acceptor extends Engine {
32
32
  super(native.AcceptorWrap, options);
33
33
  }
34
34
  }
35
- //# sourceMappingURL=acceptor.js.map
@@ -113,4 +113,3 @@ export declare class FixReject extends Error {
113
113
  * ```
114
114
  */
115
115
  export declare function fixReject(kind: FixRejectKind, message?: string, detail?: string): FixReject;
116
- //# sourceMappingURL=application.d.ts.map
@@ -55,4 +55,3 @@ export class FixReject extends Error {
55
55
  export function fixReject(kind, message, detail) {
56
56
  return new FixReject(kind, message, detail);
57
57
  }
58
- //# sourceMappingURL=application.js.map
@@ -13,6 +13,8 @@ import type { Message } from './message.js';
13
13
  * ```ts
14
14
  * const dd = DataDictionary.fromFile('./spec/FIX44.xml');
15
15
  * dd.validate(msg); // throws QuickFixError if invalid
16
+ * dd.getFieldName(35); // 'MsgType'
17
+ * Message.parse(raw, { dictionary: dd }); // structural parse (repeating groups)
16
18
  * ```
17
19
  */
18
20
  export declare class DataDictionary {
@@ -41,9 +43,25 @@ export declare class DataDictionary {
41
43
  * Validate a message against this dictionary.
42
44
  *
43
45
  * @param msg The message to validate.
46
+ * @param bodyOnly When `true`, validate only as the *application* dictionary
47
+ * (QuickFIX's `validate(msg, true)`): the BeginString version check and the
48
+ * tag/value validation of the header and trailer fields are skipped. It is
49
+ * not a check of a draft body: `BeginString` and `MsgType` must be set, and
50
+ * every field the dictionary requires, header and trailer included (e.g.
51
+ * `BodyLength`, `CheckSum`), must be present. Defaults to `false`.
44
52
  * @throws A `QuickFixError` on any validation failure (e.g.
45
- * `fixError: 'InvalidMessage'` / `'FieldNotFound'` / `'IncorrectTagValue'`).
53
+ * `fixErrorName: 'InvalidMessage'` / `'RequiredTagMissing'` /
54
+ * `'IncorrectTagValue'` / `'UnsupportedVersion'`).
46
55
  */
47
- validate(msg: Message): void;
56
+ validate(msg: Message, bodyOnly?: boolean): void;
57
+ /** The BeginString the spec declares (e.g. `'FIX.4.4'`), or `''` if it has none. */
58
+ getVersion(): string;
59
+ /** The name of a field tag (e.g. `35` → `'MsgType'`), or `undefined` if the spec does not define it. */
60
+ getFieldName(tag: number): string | undefined;
61
+ /** The tag of a field name (e.g. `'MsgType'` → `35`), or `undefined` if the spec does not define it. */
62
+ getFieldTag(name: string): number | undefined;
63
+ /** Whether the spec defines a field with this tag. */
64
+ isField(tag: number): boolean;
65
+ /** Whether the spec defines a message with this MsgType (e.g. `'D'`). */
66
+ isMsgType(msgType: string): boolean;
48
67
  }
49
- //# sourceMappingURL=data-dictionary.d.ts.map
@@ -12,6 +12,8 @@ import { native } from './native.js';
12
12
  * ```ts
13
13
  * const dd = DataDictionary.fromFile('./spec/FIX44.xml');
14
14
  * dd.validate(msg); // throws QuickFixError if invalid
15
+ * dd.getFieldName(35); // 'MsgType'
16
+ * Message.parse(raw, { dictionary: dd }); // structural parse (repeating groups)
15
17
  * ```
16
18
  */
17
19
  export class DataDictionary {
@@ -63,11 +65,37 @@ export class DataDictionary {
63
65
  * Validate a message against this dictionary.
64
66
  *
65
67
  * @param msg The message to validate.
68
+ * @param bodyOnly When `true`, validate only as the *application* dictionary
69
+ * (QuickFIX's `validate(msg, true)`): the BeginString version check and the
70
+ * tag/value validation of the header and trailer fields are skipped. It is
71
+ * not a check of a draft body: `BeginString` and `MsgType` must be set, and
72
+ * every field the dictionary requires, header and trailer included (e.g.
73
+ * `BodyLength`, `CheckSum`), must be present. Defaults to `false`.
66
74
  * @throws A `QuickFixError` on any validation failure (e.g.
67
- * `fixError: 'InvalidMessage'` / `'FieldNotFound'` / `'IncorrectTagValue'`).
75
+ * `fixErrorName: 'InvalidMessage'` / `'RequiredTagMissing'` /
76
+ * `'IncorrectTagValue'` / `'UnsupportedVersion'`).
68
77
  */
69
- validate(msg) {
70
- this.#native.validate(msg.nativeHandle);
78
+ validate(msg, bodyOnly = false) {
79
+ this.#native.validate(msg.nativeHandle, bodyOnly);
80
+ }
81
+ /** The BeginString the spec declares (e.g. `'FIX.4.4'`), or `''` if it has none. */
82
+ getVersion() {
83
+ return this.#native.getVersion();
84
+ }
85
+ /** The name of a field tag (e.g. `35` → `'MsgType'`), or `undefined` if the spec does not define it. */
86
+ getFieldName(tag) {
87
+ return this.#native.getFieldName(tag);
88
+ }
89
+ /** The tag of a field name (e.g. `'MsgType'` → `35`), or `undefined` if the spec does not define it. */
90
+ getFieldTag(name) {
91
+ return this.#native.getFieldTag(name);
92
+ }
93
+ /** Whether the spec defines a field with this tag. */
94
+ isField(tag) {
95
+ return this.#native.isField(tag);
96
+ }
97
+ /** Whether the spec defines a message with this MsgType (e.g. `'D'`). */
98
+ isMsgType(msgType) {
99
+ return this.#native.isMsgType(msgType);
71
100
  }
72
101
  }
73
- //# sourceMappingURL=data-dictionary.js.map
@@ -8,6 +8,7 @@ import { EventEmitter } from 'node:events';
8
8
  import { type NativeEngineConstructor } from './native.js';
9
9
  import { Message } from './message.js';
10
10
  import { SessionID } from './session-id.js';
11
+ import { Session } from './session.js';
11
12
  import type { ApplicationHandlers } from './application.js';
12
13
  import type { SessionSettings } from './session-settings.js';
13
14
  /** Options for constructing an {@link Initiator} or {@link Acceptor}. */
@@ -73,7 +74,11 @@ export declare abstract class Engine extends EventEmitter {
73
74
  * Stop the engine.
74
75
  *
75
76
  * Runs off the main thread and resolves once shutdown completes, keeping the
76
- * event loop free for any in-flight callbacks.
77
+ * event loop free for any in-flight callbacks. Once it settles the engine's
78
+ * sessions are destroyed. A `stop()` called while another is still in flight
79
+ * (even from a `'logout'` listener that stop fired) returns the same Promise,
80
+ * so it too settles only once the sessions are gone; the first call's
81
+ * `force` applies to both.
77
82
  *
78
83
  * @param force When `true`, aborts immediately instead of draining gracefully.
79
84
  *
@@ -83,8 +88,42 @@ export declare abstract class Engine extends EventEmitter {
83
88
  * ```
84
89
  */
85
90
  stop(force?: boolean): Promise<void>;
86
- /** Whether any session is currently logged on. */
87
- isLoggedOn(): boolean;
91
+ /**
92
+ * Whether a session is logged on.
93
+ *
94
+ * With no argument: `true` if **any** of this engine's sessions is logged on.
95
+ * With a {@link SessionID}: `true` only if that session belongs to this engine
96
+ * and is logged on (`false`, not an error, for a foreign id). Always `false`
97
+ * before {@link Engine.start} and once {@link Engine.stop} has settled; while
98
+ * a graceful stop is still in progress it reports the live state.
99
+ *
100
+ * @throws {TypeError} if `sessionID` is given but is not a {@link SessionID}
101
+ * (it does not fall back to the engine-wide form).
102
+ */
103
+ isLoggedOn(sessionID?: SessionID): boolean;
104
+ /**
105
+ * The ids of the sessions this engine was configured with (every `[SESSION]`
106
+ * in its settings). Stable for the engine's lifetime, including after
107
+ * {@link Engine.stop}.
108
+ */
109
+ getSessions(): SessionID[];
110
+ /**
111
+ * A handle on one of this engine's sessions.
112
+ *
113
+ * @returns The {@link Session}, or `undefined` if `sessionID` is not one of
114
+ * this engine's sessions or {@link Engine.stop} has settled (that destroys
115
+ * its sessions). While a stop is still in progress the sessions are alive,
116
+ * so a `'logout'` listener can still read e.g. the final sequence numbers.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * initiator.on('logon', (id) => {
121
+ * const session = initiator.getSession(id)!;
122
+ * console.log('next seq', session.getExpectedSenderNum());
123
+ * });
124
+ * ```
125
+ */
126
+ getSession(sessionID: SessionID): Session | undefined;
88
127
  /**
89
128
  * Keep the Node.js event loop alive while the engine is running (default once
90
129
  * started). Pair with {@link Engine.unref} to let a short script exit.
@@ -111,4 +150,3 @@ export interface Engine {
111
150
  /** List the listeners registered for an event. */
112
151
  listeners<E extends keyof EngineEvents>(event: E): Array<(...args: EngineEvents[E]) => void>;
113
152
  }
114
- //# sourceMappingURL=engine.d.ts.map
@@ -6,7 +6,8 @@
6
6
  */
7
7
  import { EventEmitter } from 'node:events';
8
8
  import { Message } from './message.js';
9
- import { SessionID } from './session-id.js';
9
+ import { SessionID, toNativeSessionID } from './session-id.js';
10
+ import { Session } from './session.js';
10
11
  /**
11
12
  * Base class for the FIX session engines.
12
13
  *
@@ -103,7 +104,11 @@ export class Engine extends EventEmitter {
103
104
  * Stop the engine.
104
105
  *
105
106
  * Runs off the main thread and resolves once shutdown completes, keeping the
106
- * event loop free for any in-flight callbacks.
107
+ * event loop free for any in-flight callbacks. Once it settles the engine's
108
+ * sessions are destroyed. A `stop()` called while another is still in flight
109
+ * (even from a `'logout'` listener that stop fired) returns the same Promise,
110
+ * so it too settles only once the sessions are gone; the first call's
111
+ * `force` applies to both.
107
112
  *
108
113
  * @param force When `true`, aborts immediately instead of draining gracefully.
109
114
  *
@@ -115,9 +120,50 @@ export class Engine extends EventEmitter {
115
120
  stop(force) {
116
121
  return this.#native.stop(force);
117
122
  }
118
- /** Whether any session is currently logged on. */
119
- isLoggedOn() {
120
- return this.#native.isLoggedOn();
123
+ /**
124
+ * Whether a session is logged on.
125
+ *
126
+ * With no argument: `true` if **any** of this engine's sessions is logged on.
127
+ * With a {@link SessionID}: `true` only if that session belongs to this engine
128
+ * and is logged on (`false`, not an error, for a foreign id). Always `false`
129
+ * before {@link Engine.start} and once {@link Engine.stop} has settled; while
130
+ * a graceful stop is still in progress it reports the live state.
131
+ *
132
+ * @throws {TypeError} if `sessionID` is given but is not a {@link SessionID}
133
+ * (it does not fall back to the engine-wide form).
134
+ */
135
+ isLoggedOn(sessionID) {
136
+ return sessionID === undefined
137
+ ? this.#native.isLoggedOn()
138
+ : this.#native.isLoggedOn(toNativeSessionID(sessionID, 'sessionID'));
139
+ }
140
+ /**
141
+ * The ids of the sessions this engine was configured with (every `[SESSION]`
142
+ * in its settings). Stable for the engine's lifetime, including after
143
+ * {@link Engine.stop}.
144
+ */
145
+ getSessions() {
146
+ return this.#native.getSessions().map((h) => SessionID.fromNative(h));
147
+ }
148
+ /**
149
+ * A handle on one of this engine's sessions.
150
+ *
151
+ * @returns The {@link Session}, or `undefined` if `sessionID` is not one of
152
+ * this engine's sessions or {@link Engine.stop} has settled (that destroys
153
+ * its sessions). While a stop is still in progress the sessions are alive,
154
+ * so a `'logout'` listener can still read e.g. the final sequence numbers.
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * initiator.on('logon', (id) => {
159
+ * const session = initiator.getSession(id)!;
160
+ * console.log('next seq', session.getExpectedSenderNum());
161
+ * });
162
+ * ```
163
+ */
164
+ getSession(sessionID) {
165
+ const handle = this.#native.getSession(toNativeSessionID(sessionID, 'sessionID'));
166
+ return handle ? Session.fromNative(handle) : undefined;
121
167
  }
122
168
  /**
123
169
  * Keep the Node.js event loop alive while the engine is running (default once
@@ -131,4 +177,3 @@ export class Engine extends EventEmitter {
131
177
  this.#native.unref();
132
178
  }
133
179
  }
134
- //# sourceMappingURL=engine.js.map