@cosyte/mllp 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3112 @@
1
+ import { T as Transport } from './index-DjGQJtYE.js';
2
+ import { EventEmitter } from 'node:events';
3
+ import { Socket } from 'node:net';
4
+ import { TLSSocket } from 'node:tls';
5
+
6
+ /**
7
+ * MLLP warning code registry and `MllpWarning` factory.
8
+ *
9
+ * `WarningCode` is a **stable public API**. Renaming or removing a code is a breaking change.
10
+ * New codes may only be added in minor versions.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { createWarning, type MllpWarning, type WarningCode } from '@cosyte/mllp';
15
+ * const w: MllpWarning = createWarning('MLLP_EMPTY_PAYLOAD', 128, 'Empty payload between VT and FS');
16
+ * ```
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+ /**
21
+ * Union of all stable MLLP warning codes.
22
+ *
23
+ * These codes are a **public API** — they appear in `onWarning` handlers, log pipelines,
24
+ * monitoring dashboards, and error messages. Renaming is a breaking change.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const code: WarningCode = 'MLLP_FS_WITHOUT_CR';
29
+ * ```
30
+ */
31
+ type WarningCode = "MLLP_MISSING_LEADING_VT" | "MLLP_FS_WITHOUT_CR" | "MLLP_LF_AFTER_FS" | "MLLP_LEADING_WHITESPACE" | "MLLP_TRAILING_BYTES" | "MLLP_PAYLOAD_CONTAINS_VT" | "MLLP_PAYLOAD_CONTAINS_FS" | "MLLP_EMPTY_PAYLOAD" | "MLLP_FRAME_TOO_LARGE" | "MLLP_ACK_UNMATCHED_CONTROL_ID" | "MLLP_ACK_AFTER_TIMEOUT";
32
+ /**
33
+ * A frozen warning object emitted when the decoder tolerates a framing deviation.
34
+ *
35
+ * `connectionId` is `undefined` when emitted by a standalone `FrameReader` (Phase 2).
36
+ * Phase 3 `Connection` enriches it to the real UUIDv4 before forwarding upstream.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const reader = new FrameReader({
41
+ * onFrame: (p) => process(p),
42
+ * onWarning: (w: MllpWarning) => logger.warn(w),
43
+ * allowFsOnly: true,
44
+ * });
45
+ * ```
46
+ */
47
+ interface MllpWarning {
48
+ readonly code: WarningCode;
49
+ /** Stable human-readable description. Never contains payload bytes or secrets. */
50
+ readonly message: string;
51
+ /** Absolute stream byte offset where the anomaly was detected. */
52
+ readonly byteOffset: number;
53
+ /**
54
+ * Connection identifier. `undefined` at the framing layer;
55
+ * enriched by Phase 3 `Connection` before emitting upstream.
56
+ */
57
+ readonly connectionId: string | undefined;
58
+ /** Wall-clock time at point of emission. */
59
+ readonly timestamp: Date;
60
+ }
61
+ /**
62
+ * Create a frozen `MllpWarning` object. The returned object is `Object.freeze()`'d
63
+ * so subscribers cannot mutate shared warning state.
64
+ *
65
+ * `connectionId` is always `undefined` here — Phase 3 enriches via
66
+ * `{ ...w, connectionId: this.connectionId }` then re-freezes (D-08).
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * const w = createWarning('MLLP_EMPTY_PAYLOAD', 64, 'Empty payload between VT and FS');
71
+ * // w.connectionId === undefined
72
+ * ```
73
+ */
74
+ declare function createWarning(code: WarningCode, byteOffset: number, message: string): MllpWarning;
75
+
76
+ /**
77
+ * Typed framing error class for unrecoverable MLLP wire-format violations.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * import { MllpFramingError } from '@cosyte/mllp';
82
+ * try {
83
+ * encodeFrame(payloadWithVt);
84
+ * } catch (err) {
85
+ * if (err instanceof MllpFramingError) {
86
+ * logger.error({ code: err.code, offset: err.byteOffset });
87
+ * }
88
+ * }
89
+ * ```
90
+ *
91
+ * @packageDocumentation
92
+ */
93
+
94
+ /**
95
+ * Thrown for unrecoverable MLLP wire-format violations.
96
+ *
97
+ * - `code` — stable `WarningCode` identifying the violation
98
+ * - `byteOffset` — absolute stream position where the violation was detected
99
+ * - `snippet` — the single framing-boundary byte that broke the structure, or empty when the
100
+ * anomaly is not a specific byte. **Never a run of payload content** (see the `snippet` PHI
101
+ * contract below); the constructor caps whatever it is given to 64 bytes as a backstop.
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * // Pass only the offending framing-boundary byte — never a slice of payload content.
106
+ * throw new MllpFramingError('MLLP_MISSING_LEADING_VT', byteOffset, Buffer.from([byte]));
107
+ * ```
108
+ */
109
+ declare class MllpFramingError extends Error {
110
+ readonly name: "MllpFramingError";
111
+ /** Stable warning code identifying the violation type. */
112
+ readonly code: WarningCode;
113
+ /** Absolute byte offset in the stream where the error was detected. */
114
+ readonly byteOffset: number;
115
+ /**
116
+ * Up to 64 bytes copied from around the anomaly.
117
+ *
118
+ * This is a **copied** Buffer — isolated from the source buffer so it remains
119
+ * valid after the underlying buffer is reused or overwritten.
120
+ *
121
+ * **PHI contract (MLLP-9):** the decoder only ever populates this with the single
122
+ * framing-boundary byte that violated the frame structure (whose hex value the
123
+ * `message` already discloses) — never a run of payload content bytes. For anomalies
124
+ * whose fault is not a specific byte (`MLLP_FRAME_TOO_LARGE` — the accumulated size),
125
+ * the snippet is **empty**: a payload slice on a public error field would leak a
126
+ * field-body slice of clinical content. Callers constructing this directly are
127
+ * responsible for the same discipline.
128
+ */
129
+ readonly snippet: Buffer;
130
+ /**
131
+ * Construct an MLLP framing error.
132
+ *
133
+ * @param code - The stable warning code classifying the violation.
134
+ * @param byteOffset - Absolute stream byte offset where the violation was detected.
135
+ * @param snippet - Raw bytes copied from around the anomaly (capped to 64 bytes).
136
+ * @param message - Optional human-readable override message.
137
+ */
138
+ constructor(code: WarningCode, byteOffset: number,
139
+ /** Raw bytes around the anomaly. Capped to MAX_SNIPPET_BYTES (64). */
140
+ snippet: Buffer, message?: string);
141
+ }
142
+
143
+ /**
144
+ * MLLP frame encoder — canonical `VT + payload + FS + CR` wrapping.
145
+ *
146
+ * The encoder is a **strict emitter** (Postel's Law: conservative on write).
147
+ * There is no option to loosen the output format — it always emits canonical framing.
148
+ * Tolerance for delimiter bytes in the payload is available via `allowDelimiterBytesInPayload`,
149
+ * which passes bytes through verbatim and emits an `MllpWarning` for each offending byte.
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * import { encodeFrame } from '@cosyte/mllp';
154
+ * const frame = encodeFrame(Buffer.from('MSH|^~\\&|...'));
155
+ * // frame[0] === 0x0B (VT), frame[frame.length - 2] === 0x1C (FS), frame[frame.length - 1] === 0x0D (CR)
156
+ * ```
157
+ *
158
+ * @packageDocumentation
159
+ */
160
+
161
+ /**
162
+ * Options for {@link encodeFrame}.
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * const frame = encodeFrame(payload, {
167
+ * allowDelimiterBytesInPayload: true,
168
+ * onWarning: (w) => logger.warn(w),
169
+ * });
170
+ * ```
171
+ */
172
+ interface EncoderOptions {
173
+ /**
174
+ * When `true`, VT (0x0B) or FS (0x1C) bytes within the payload are preserved
175
+ * verbatim in the output frame instead of throwing `MllpFramingError`.
176
+ * An `MllpWarning` is emitted per offending byte if `onWarning` is provided.
177
+ *
178
+ * Default: `false` (strict — throws on delimiter bytes).
179
+ */
180
+ allowDelimiterBytesInPayload?: boolean;
181
+ /**
182
+ * Called for each offending delimiter byte when `allowDelimiterBytesInPayload` is `true`.
183
+ * Invocation is wrapped in try/catch — a throwing handler does not interrupt encoding (WARN-06).
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * const frame = encodeFrame(payload, {
188
+ * allowDelimiterBytesInPayload: true,
189
+ * onWarning: (w) => logger.warn({ code: w.code, offset: w.byteOffset }),
190
+ * });
191
+ * ```
192
+ */
193
+ onWarning?: (w: MllpWarning) => void;
194
+ }
195
+ /**
196
+ * Encode `payload` in canonical MLLP framing: `VT (0x0B) + payload + FS (0x1C) + CR (0x0D)`.
197
+ *
198
+ * Strict by default: throws `MllpFramingError` if the payload contains VT or FS bytes.
199
+ * Set `{ allowDelimiterBytesInPayload: true }` to pass those bytes through verbatim
200
+ * and receive an `MllpWarning` per offending byte via `onWarning` instead.
201
+ *
202
+ * The returned Buffer has length `payload.length + 3`. Bytes at positions 1 through
203
+ * `payload.length` are an exact copy of the payload (independent of the input buffer).
204
+ *
205
+ * @throws {MllpFramingError} with code `MLLP_PAYLOAD_CONTAINS_VT` when payload contains
206
+ * byte `0x0B` and `allowDelimiterBytesInPayload` is `false` (default).
207
+ * @throws {MllpFramingError} with code `MLLP_PAYLOAD_CONTAINS_FS` when payload contains
208
+ * byte `0x1C` and `allowDelimiterBytesInPayload` is `false` (default).
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * import { encodeFrame } from '@cosyte/mllp';
213
+ *
214
+ * // Strict (default) — throws on delimiter bytes in payload
215
+ * const frame = encodeFrame(Buffer.from('MSH|^~\\&|SEND|FAC|RECV|FAC|...'));
216
+ * socket.write(frame);
217
+ *
218
+ * // Tolerant — passes delimiter bytes through with a warning
219
+ * const frame2 = encodeFrame(dirtyPayload, {
220
+ * allowDelimiterBytesInPayload: true,
221
+ * onWarning: (w) => logger.warn(w),
222
+ * });
223
+ * ```
224
+ */
225
+ declare function encodeFrame(payload: Buffer, opts?: EncoderOptions): Buffer;
226
+
227
+ /**
228
+ * MLLP frame decoder — stateful 3-state FSM for chunked byte-stream parsing.
229
+ *
230
+ * Feed raw TCP chunks via `push(chunk)`. Complete MLLP frames fire synchronously
231
+ * via the `onFrame` callback during each `push()` call.
232
+ *
233
+ * The decoder is **strict by default**: any framing deviation throws `MllpFramingError`
234
+ * unless the matching tolerance option is explicitly enabled (Postel's Law — liberal receiver
235
+ * means explicit tolerance, not silent acceptance).
236
+ *
237
+ * @example
238
+ * ```typescript
239
+ * import { FrameReader } from '@cosyte/mllp';
240
+ * const reader = new FrameReader({
241
+ * onFrame: (payload) => handleMessage(payload),
242
+ * onWarning: (w) => logger.warn(w),
243
+ * allowFsOnly: true,
244
+ * });
245
+ * socket.on('data', (chunk) => reader.push(chunk));
246
+ * socket.on('close', () => reader.reset());
247
+ * ```
248
+ *
249
+ * @packageDocumentation
250
+ */
251
+
252
+ /**
253
+ * Options for `FrameReader`.
254
+ *
255
+ * All tolerance opts default to `false` — every framing deviation throws unless
256
+ * explicitly enabled. Server-level defaults (allowFsOnly, allowLfAfterFs, allowLeadingWhitespace)
257
+ * are applied by Phase 4 when constructing readers per SERVER-12.
258
+ *
259
+ * @example
260
+ * ```typescript
261
+ * const opts: FrameReaderOptions = {
262
+ * onFrame: (payload, byteOffset, warnings) => process(payload, byteOffset, warnings),
263
+ * onWarning: (w) => logger.warn(w),
264
+ * maxFrameSizeBytes: 4 * 1024 * 1024, // 4 MiB limit
265
+ * allowFsOnly: true,
266
+ * allowLfAfterFs: true,
267
+ * };
268
+ * ```
269
+ */
270
+ interface FrameReaderOptions {
271
+ /**
272
+ * Called synchronously during `push()` for each complete MLLP payload.
273
+ *
274
+ * @param payload - Raw MLLP payload bytes (framing stripped).
275
+ * @param byteOffset - Stream byte offset of the VT byte that opened this frame.
276
+ * Monotonic across the connection lifetime; reset to 0 by `reset()`.
277
+ * @param warnings - Framing warnings emitted during decoding of this specific frame.
278
+ * Empty array when no tolerance deviations were detected.
279
+ */
280
+ onFrame: (payload: Buffer, byteOffset: number, warnings: readonly MllpWarning[]) => void;
281
+ /**
282
+ * Called for each tolerated framing deviation. Wrapped in try/catch per WARN-06.
283
+ * A throwing handler will not corrupt FSM state.
284
+ */
285
+ onWarning?: (w: MllpWarning) => void;
286
+ /**
287
+ * Maximum accumulated payload bytes before `MllpFramingError('MLLP_FRAME_TOO_LARGE')`.
288
+ * Default: 16 MiB (FRAME-11 DoS prevention).
289
+ */
290
+ maxFrameSizeBytes?: number;
291
+ /** FRAME-07: Tolerate FS without trailing CR; emits `MLLP_FS_WITHOUT_CR`. */
292
+ allowFsOnly?: boolean;
293
+ /** FRAME-08: Tolerate FS+LF instead of FS+CR; emits `MLLP_LF_AFTER_FS`. */
294
+ allowLfAfterFs?: boolean;
295
+ /** FRAME-09: Tolerate missing leading VT; emits `MLLP_MISSING_LEADING_VT`. */
296
+ allowMissingLeadingVt?: boolean;
297
+ /** FRAME-10: Tolerate SP/TAB/LF/CR before VT; emits `MLLP_LEADING_WHITESPACE`. */
298
+ allowLeadingWhitespace?: boolean;
299
+ /**
300
+ * When `true`, escalates the following tolerances to thrown `MllpFramingError`
301
+ * even if individual opt-ins (`allowFsOnly`, `allowLfAfterFs`, `allowMissingLeadingVt`,
302
+ * `allowLeadingWhitespace`) are enabled (WARN-08):
303
+ * - `MLLP_MISSING_LEADING_VT`
304
+ * - `MLLP_FS_WITHOUT_CR`
305
+ * - `MLLP_LF_AFTER_FS`
306
+ * - `MLLP_LEADING_WHITESPACE` (leading whitespace escalates as `MLLP_MISSING_LEADING_VT`)
307
+ *
308
+ * `MLLP_EMPTY_PAYLOAD` and `MLLP_TRAILING_BYTES` remain warnings even in strict mode.
309
+ *
310
+ * @example
311
+ * ```typescript
312
+ * // Hardened enforcement — no tolerance, all violations throw
313
+ * const reader = new FrameReader({ onFrame: fn, strict: true });
314
+ * ```
315
+ */
316
+ strict?: boolean;
317
+ }
318
+ /**
319
+ * Stateful MLLP frame decoder. Feed raw TCP byte chunks via `push(chunk)`.
320
+ * Complete frames fire synchronously via `onFrame` callback during `push()`.
321
+ *
322
+ * The decoder operates as a 3-state FSM:
323
+ * - `SCANNING_FOR_VT` — waiting for the 0x0B frame-start byte
324
+ * - `READING_PAYLOAD` — accumulating payload bytes until FS (0x1C)
325
+ * - `EXPECTING_CR` — received FS, waiting for CR (0x0D) to complete the frame
326
+ *
327
+ * @example
328
+ * ```typescript
329
+ * const reader = new FrameReader({
330
+ * onFrame: (payload, byteOffset, warnings) => process(payload, byteOffset, warnings),
331
+ * onWarning: (w) => logger.warn(w),
332
+ * });
333
+ * socket.on('data', (chunk) => reader.push(chunk));
334
+ * ```
335
+ */
336
+ declare class FrameReader {
337
+ private readonly _opts;
338
+ private readonly _maxFrameSize;
339
+ private _state;
340
+ /** Reusable accumulator buffer — grown by doubling as needed. */
341
+ private _accumulator;
342
+ /** Number of payload bytes written to accumulator in current frame. */
343
+ private _writePos;
344
+ /** Monotonic absolute stream byte offset across the entire connection lifetime. */
345
+ private _byteOffset;
346
+ /** Byte offset where current leading-whitespace run started (FRAME-10). */
347
+ private _wsStart;
348
+ /** Count of leading whitespace bytes accumulated in current run (FRAME-10). */
349
+ private _wsCount;
350
+ /** Byte offset of the VT byte that started the current frame. */
351
+ private _frameStartOffset;
352
+ /** Per-frame warning accumulator — cleared after each _deliverFrame(). */
353
+ private _frameWarnings;
354
+ /**
355
+ * Construct a chunked MLLP frame reader.
356
+ *
357
+ * @param opts - Reader options (`onFrame`/`onWarning` callbacks, tolerance, `maxFrameSizeBytes`).
358
+ */
359
+ constructor(opts: FrameReaderOptions);
360
+ /**
361
+ * Feed a chunk of raw TCP bytes into the FSM. Frames fire synchronously via `onFrame`
362
+ * during this call. May throw `MllpFramingError` for unrecoverable framing violations
363
+ * when the matching tolerance is not enabled.
364
+ *
365
+ * @example
366
+ * ```typescript
367
+ * socket.on('data', (chunk) => reader.push(chunk));
368
+ * ```
369
+ */
370
+ push(chunk: Buffer): void;
371
+ /**
372
+ * Clear internal accumulator state and reset byte offset to 0.
373
+ *
374
+ * Call on reconnect / connection reuse to start fresh without allocating a new reader (D-03).
375
+ * Pending partial frame state is discarded silently.
376
+ *
377
+ * @example
378
+ * ```typescript
379
+ * socket.on('close', () => reader.reset());
380
+ * ```
381
+ */
382
+ reset(): void;
383
+ private _processByte;
384
+ private _scanForVt;
385
+ private _readPayload;
386
+ private _expectCr;
387
+ /** Grow the internal accumulator by doubling and write one byte. */
388
+ private _appendByte;
389
+ /**
390
+ * Copy accumulated payload bytes into a new Buffer and deliver via `onFrame`.
391
+ * The copy isolates callers from internal accumulator reuse (T-02-03-03).
392
+ */
393
+ private _deliverFrame;
394
+ /** Emit a warning via the `onWarning` callback, swallowing any handler exceptions (WARN-06). */
395
+ private _emitWarning;
396
+ }
397
+
398
+ /**
399
+ * NetTransport — `Transport` implementation wrapping a `net.Socket`.
400
+ *
401
+ * This is the **only** place in the codebase that consumes `net.Socket`'s
402
+ * EventEmitter surface. All higher layers (Connection, Server, Client) work
403
+ * against the `Transport` interface, never against `net.Socket` directly.
404
+ *
405
+ * @example
406
+ * ```typescript
407
+ * import { createConnection } from 'node:net';
408
+ * import { NetTransport } from '@cosyte/mllp';
409
+ *
410
+ * const socket = createConnection({ host: 'mllp.example.com', port: 2575 });
411
+ * const transport = new NetTransport(socket);
412
+ * transport.onConnect(() => console.log('connected'));
413
+ * transport.onData((chunk) => reader.push(chunk));
414
+ * ```
415
+ *
416
+ * @packageDocumentation
417
+ */
418
+
419
+ /**
420
+ * Wraps a `net.Socket` as a `Transport`, mapping socket EventEmitter events
421
+ * to registered single-handler callbacks.
422
+ *
423
+ * Each `onXxx(fn)` call replaces the prior handler and re-registers on the socket
424
+ * (uses `socket.removeAllListeners(event)` + `socket.on(event, fn)`).
425
+ *
426
+ * @example
427
+ * ```typescript
428
+ * const t = new NetTransport(socket);
429
+ * t.onData((chunk) => process(chunk));
430
+ * t.onError((err) => handleError(err));
431
+ * ```
432
+ */
433
+ declare class NetTransport implements Transport {
434
+ private readonly _socket;
435
+ /**
436
+ * Wrap an existing `net.Socket` (or `tls.TLSSocket`) as a `Transport`.
437
+ *
438
+ * @param socket - The connected (or connecting) socket to adapt.
439
+ */
440
+ constructor(socket: Socket);
441
+ /** See {@link Transport.write}. */
442
+ write(buf: Buffer): boolean;
443
+ /** See {@link Transport.close}. */
444
+ close(): void;
445
+ /** See {@link Transport.destroy}. */
446
+ destroy(reason?: Error): void;
447
+ /** See {@link Transport.onData}. */
448
+ onData(fn: (chunk: Buffer) => void): void;
449
+ /** See {@link Transport.onConnect}. */
450
+ onConnect(fn: () => void): void;
451
+ /** See {@link Transport.onClose}. */
452
+ onClose(fn: () => void): void;
453
+ /** See {@link Transport.onError}. */
454
+ onError(fn: (err: Error) => void): void;
455
+ }
456
+
457
+ /**
458
+ * TlsTransport — `Transport` implementation wrapping a `tls.TLSSocket` (Phase 8).
459
+ *
460
+ * Identical wiring to `NetTransport` with one deliberate difference: `onConnect`
461
+ * maps to the socket's `'secureConnect'` event (fires once the TLS handshake
462
+ * completes) rather than `'connect'` (fires once the raw TCP handshake
463
+ * completes). Consumers of `Transport` (Connection, Client, Server) never see
464
+ * the difference — they only ever observe "the transport is ready."
465
+ *
466
+ * @example
467
+ * ```typescript
468
+ * import { connect } from 'node:tls';
469
+ * import { TlsTransport } from '@cosyte/mllp';
470
+ *
471
+ * const socket = connect({ host: 'mllp.example.com', port: 2575, ca: caPem });
472
+ * const transport = new TlsTransport(socket);
473
+ * transport.onConnect(() => console.log('TLS handshake complete'));
474
+ * transport.onData((chunk) => reader.push(chunk));
475
+ * ```
476
+ *
477
+ * @packageDocumentation
478
+ */
479
+
480
+ /**
481
+ * Wraps a `tls.TLSSocket` as a `Transport`. `onConnect(fn)` is armed on the
482
+ * `'secureConnect'` event (handshake complete, including certificate
483
+ * verification when `rejectUnauthorized` is on) rather than the raw TCP
484
+ * `'connect'` event that `NetTransport` uses.
485
+ *
486
+ * Each `onXxx(fn)` call replaces the prior handler and re-registers on the
487
+ * socket (`socket.removeAllListeners(event)` + `socket.on(event, fn)`) —
488
+ * the same set-once semantics as `NetTransport`.
489
+ *
490
+ * @example
491
+ * ```typescript
492
+ * const t = new TlsTransport(tlsSocket);
493
+ * t.onConnect(() => logger.info('secure'));
494
+ * t.onError((err) => handleTlsError(err));
495
+ * ```
496
+ */
497
+ declare class TlsTransport implements Transport {
498
+ private readonly _socket;
499
+ /**
500
+ * Wrap an existing `tls.TLSSocket` (connecting or already secure) as a `Transport`.
501
+ *
502
+ * @param socket - The TLS socket to adapt.
503
+ */
504
+ constructor(socket: TLSSocket);
505
+ /** See {@link Transport.write}. */
506
+ write(buf: Buffer): boolean;
507
+ /** See {@link Transport.close}. */
508
+ close(): void;
509
+ /** See {@link Transport.destroy}. */
510
+ destroy(reason?: Error): void;
511
+ /** See {@link Transport.onData}. */
512
+ onData(fn: (chunk: Buffer) => void): void;
513
+ /**
514
+ * See {@link Transport.onConnect}. Armed on `'secureConnect'` — the TLS
515
+ * handshake-complete event — not the raw TCP `'connect'` event.
516
+ */
517
+ onConnect(fn: () => void): void;
518
+ /** See {@link Transport.onClose}. */
519
+ onClose(fn: () => void): void;
520
+ /** See {@link Transport.onError}. */
521
+ onError(fn: (err: Error) => void): void;
522
+ }
523
+
524
+ /**
525
+ * Security-warning codes and payload shape shared by the client and server (Phase 8).
526
+ *
527
+ * These codes are a **public API** — they appear in `'securityWarning'` event
528
+ * handlers, log pipelines, and monitoring dashboards. Renaming or removing a
529
+ * code is a breaking change (mirrors the `WarningCode` guardrail in
530
+ * `src/framing/registry.ts`).
531
+ *
532
+ * @example
533
+ * ```typescript
534
+ * import { MLLP_TLS_VERIFY_DISABLED, MLLP_BIND_ALL_INTERFACES } from '@cosyte/mllp';
535
+ * client.on('securityWarning', (w) => {
536
+ * if (w.code === MLLP_TLS_VERIFY_DISABLED) logger.warn(w.message);
537
+ * });
538
+ * ```
539
+ *
540
+ * @packageDocumentation
541
+ */
542
+ /**
543
+ * Emitted (client-side) on every successful `secureConnect` — initial connect
544
+ * AND every reconnect — when {@link TlsOptions.allowUnverified} is `true`.
545
+ * Certificate verification is disabled for the connection; this is the loud,
546
+ * per-connection reminder that the channel is not authenticated per IHE ATNA
547
+ * ITI-19 (https://profiles.ihe.net/ITI/TF/Volume2/ITI-19.html).
548
+ *
549
+ * @example
550
+ * ```typescript
551
+ * import { MLLP_TLS_VERIFY_DISABLED } from '@cosyte/mllp';
552
+ * client.on('securityWarning', (w) => {
553
+ * if (w.code === MLLP_TLS_VERIFY_DISABLED) metrics.increment('mllp.tls_verify_disabled');
554
+ * });
555
+ * ```
556
+ */
557
+ declare const MLLP_TLS_VERIFY_DISABLED = "MLLP_TLS_VERIFY_DISABLED";
558
+ /**
559
+ * Emitted (server-side) once at `listen()` time when the server binds a
560
+ * wildcard host (`'0.0.0.0'` or `'::'`) with `ServerOptions.allowWildcardBind: true`.
561
+ * Binding all interfaces widens the network exposure of the listener; this is
562
+ * the loud, one-time reminder that the operator opted in.
563
+ *
564
+ * @example
565
+ * ```typescript
566
+ * import { MLLP_BIND_ALL_INTERFACES } from '@cosyte/mllp';
567
+ * server.on('securityWarning', (w) => {
568
+ * if (w.code === MLLP_BIND_ALL_INTERFACES) logger.warn(w.message);
569
+ * });
570
+ * ```
571
+ */
572
+ declare const MLLP_BIND_ALL_INTERFACES = "MLLP_BIND_ALL_INTERFACES";
573
+ /**
574
+ * Union of the two stable security-warning codes.
575
+ *
576
+ * @example
577
+ * ```typescript
578
+ * const code: SecurityWarningCode = 'MLLP_TLS_VERIFY_DISABLED';
579
+ * ```
580
+ */
581
+ type SecurityWarningCode = typeof MLLP_TLS_VERIFY_DISABLED | typeof MLLP_BIND_ALL_INTERFACES;
582
+ /**
583
+ * Frozen payload of the `'securityWarning'` event, emitted by both
584
+ * {@link MllpClient} (`MLLP_TLS_VERIFY_DISABLED`) and {@link MllpServer}
585
+ * (`MLLP_BIND_ALL_INTERFACES`).
586
+ *
587
+ * Never carries payload bytes or PHI — only routing metadata (host/port) and
588
+ * a fixed, static message string.
589
+ *
590
+ * @example
591
+ * ```typescript
592
+ * client.on('securityWarning', (w: SecurityWarning) => {
593
+ * logger.warn({ code: w.code, host: w.host, port: w.port });
594
+ * });
595
+ * ```
596
+ */
597
+ interface SecurityWarning {
598
+ /** The stable security-warning code. */
599
+ readonly code: SecurityWarningCode;
600
+ /** Fixed, human-readable description. Never contains payload bytes or PHI. */
601
+ readonly message: string;
602
+ /** Host associated with the warning (the target host for clients; the bind host for servers). */
603
+ readonly host: string;
604
+ /** Port associated with the warning. */
605
+ readonly port: number;
606
+ /** Wall-clock time at point of emission. */
607
+ readonly timestamp: Date;
608
+ }
609
+
610
+ /**
611
+ * TLS (MLLPS) option types for the client and server (Phase 8).
612
+ *
613
+ * Spec anchor: IHE ATNA, ITI-19 Authenticate Node
614
+ * (https://profiles.ihe.net/ITI/TF/Volume2/ITI-19.html), the "STX: TLS 1.2
615
+ * Floor using BCP195" option, ITI TF-2 §3.19.6.2.3 — TLS >= 1.2 required;
616
+ * mutual node authentication; certificate validation via chain-of-trust or
617
+ * direct comparison. ITI TF-2 §3.19.6.2.3 mandates four TLS 1.2 cipher
618
+ * suites: `TLS_DHE_RSA_WITH_AES_128_GCM_SHA256`,
619
+ * `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256`, `TLS_DHE_RSA_WITH_AES_256_GCM_SHA384`,
620
+ * `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384`. Node's default TLS 1.2 cipher list
621
+ * already includes both mandated ECDHE suites, so this package pins to Node
622
+ * defaults rather than bundling a cipher list — see `docs-content/tls.md`.
623
+ *
624
+ * @example
625
+ * ```typescript
626
+ * import type { TlsOptions } from '@cosyte/mllp';
627
+ * const tls: TlsOptions = { ca: caPem, servername: 'mllp.example.com' };
628
+ * ```
629
+ *
630
+ * @packageDocumentation
631
+ */
632
+ /**
633
+ * A PEM-encoded credential (certificate, key, or CA), matching Node's
634
+ * `tls.connect`/`tls.createServer` input shape — a single PEM string/Buffer,
635
+ * or an array of them (chain / multiple trust anchors).
636
+ *
637
+ * @example
638
+ * ```typescript
639
+ * import { readFileSync } from 'node:fs';
640
+ * const ca: PemInput = readFileSync('ca.pem');
641
+ * ```
642
+ */
643
+ type PemInput = string | Buffer | Array<string | Buffer>;
644
+ /**
645
+ * Client-side TLS options (Phase 8, `ClientOptions.tls`).
646
+ *
647
+ * Passing `true` for `ClientOptions.tls` is equivalent to `{}` — TLS enabled
648
+ * with all defaults, including certificate verification **on**.
649
+ *
650
+ * @example
651
+ * ```typescript
652
+ * import { createClient } from '@cosyte/mllp';
653
+ * const client = createClient({
654
+ * host: 'mllp.example.com',
655
+ * port: 2575,
656
+ * tls: { ca: caPem, minVersion: 'TLSv1.2' },
657
+ * });
658
+ * ```
659
+ */
660
+ interface TlsOptions {
661
+ /** Trust anchor(s) for verifying the server's certificate chain. */
662
+ readonly ca?: PemInput;
663
+ /** Client certificate presented for mutual TLS (ATNA ITI-19). */
664
+ readonly cert?: PemInput;
665
+ /** Private key matching {@link TlsOptions.cert}. */
666
+ readonly key?: PemInput;
667
+ /** Passphrase for an encrypted {@link TlsOptions.key}. */
668
+ readonly passphrase?: string;
669
+ /**
670
+ * SNI hostname and the identity-check target (matched against the server
671
+ * certificate's Subject/SAN). Defaults to `ClientOptions.host` when unset.
672
+ */
673
+ readonly servername?: string;
674
+ /**
675
+ * Minimum negotiated TLS protocol version.
676
+ *
677
+ * Default `'TLSv1.2'` — the IHE ATNA ITI-19 "TLS 1.2 Floor" (BCP195) floor
678
+ * (ITI TF-2 §3.19.6.2.3). `'TLSv1.0'`/`'TLSv1.1'` are intentionally **not**
679
+ * expressible by this type — the floor cannot be lowered through this API.
680
+ *
681
+ * @default 'TLSv1.2'
682
+ */
683
+ readonly minVersion?: "TLSv1.2" | "TLSv1.3";
684
+ /** Maximum negotiated TLS protocol version. */
685
+ readonly maxVersion?: "TLSv1.2" | "TLSv1.3";
686
+ /**
687
+ * OpenSSL cipher-list string passthrough (`tls.connect`'s `ciphers`).
688
+ * Unset uses Node's compiled-in defaults, which already include both
689
+ * ATNA-mandated ECDHE suites (see the module doc comment). Set this to
690
+ * restrict to a stricter list (e.g. DHE suites) if your deployment requires it.
691
+ *
692
+ * @default undefined (Node defaults)
693
+ */
694
+ readonly ciphers?: string;
695
+ /**
696
+ * Loud, explicit dev opt-out from certificate verification (maps to
697
+ * `tls.connect`'s `rejectUnauthorized: false`). There is deliberately no
698
+ * raw `rejectUnauthorized` surface on this type — this is the only door,
699
+ * and it is loud: every successful connection (initial + every reconnect)
700
+ * emits a `'securityWarning'` (`MLLP_TLS_VERIFY_DISABLED`) event and calls
701
+ * `process.emitWarning`.
702
+ *
703
+ * Never set this in production against an untrusted network.
704
+ *
705
+ * @default false
706
+ */
707
+ readonly allowUnverified?: boolean;
708
+ }
709
+ /**
710
+ * ATNA ITI-19 mutual-authentication modes for {@link ServerTlsOptions.clientAuth}.
711
+ *
712
+ * Mirrors the IHE ATNA "Authenticate Node" mutual-auth requirement
713
+ * (https://profiles.ihe.net/ITI/TF/Volume2/ITI-19.html):
714
+ *
715
+ * - `'NONE'` — no client certificate requested (default).
716
+ * - `'WANT'` — client certificate requested but NOT required; an untrusted or
717
+ * absent client certificate does not reject the connection. The peer
718
+ * certificate (if any) is surfaced on the `'connection'` event.
719
+ * - `'MUST'` — client certificate required AND verified against
720
+ * {@link ServerTlsOptions.ca}; the ATNA mutual-authentication mode. A
721
+ * missing or untrusted client certificate rejects the handshake.
722
+ *
723
+ * @example
724
+ * ```typescript
725
+ * import type { ClientAuth } from '@cosyte/mllp';
726
+ * const mode: ClientAuth = 'MUST'; // ATNA ITI-19 mutual node authentication
727
+ * ```
728
+ */
729
+ type ClientAuth = "NONE" | "WANT" | "MUST";
730
+ /**
731
+ * Server-side TLS options (Phase 8, `ServerOptions.tls`).
732
+ *
733
+ * @example
734
+ * ```typescript
735
+ * import { createServer } from '@cosyte/mllp';
736
+ * const server = createServer({
737
+ * tls: { cert: certPem, key: keyPem, clientAuth: 'MUST', ca: clientCaPem },
738
+ * });
739
+ * ```
740
+ */
741
+ interface ServerTlsOptions {
742
+ /** Server certificate (PEM). Required. */
743
+ readonly cert: PemInput;
744
+ /** Private key matching {@link ServerTlsOptions.cert}. Required. */
745
+ readonly key: PemInput;
746
+ /** Trust anchor(s) for verifying client certificates under `WANT`/`MUST`. */
747
+ readonly ca?: PemInput;
748
+ /** Passphrase for an encrypted {@link ServerTlsOptions.key}. */
749
+ readonly passphrase?: string;
750
+ /**
751
+ * ATNA ITI-19 mutual-authentication mode. `'WANT'` requests a client
752
+ * certificate without rejecting unauthorized/absent ones (surfaced, not
753
+ * enforced); `'MUST'` requests AND enforces verification (ATNA mutual
754
+ * auth). See {@link ClientAuth}.
755
+ *
756
+ * @default 'NONE'
757
+ */
758
+ readonly clientAuth?: ClientAuth;
759
+ /**
760
+ * Minimum negotiated TLS protocol version — the IHE ATNA ITI-19 "TLS 1.2
761
+ * Floor" (BCP195) floor (ITI TF-2 §3.19.6.2.3).
762
+ *
763
+ * @default 'TLSv1.2'
764
+ */
765
+ readonly minVersion?: "TLSv1.2" | "TLSv1.3";
766
+ /** Maximum negotiated TLS protocol version. */
767
+ readonly maxVersion?: "TLSv1.2" | "TLSv1.3";
768
+ /**
769
+ * OpenSSL cipher-list string passthrough. Unset uses Node's defaults
770
+ * (includes both ATNA-mandated ECDHE suites).
771
+ *
772
+ * @default undefined (Node defaults)
773
+ */
774
+ readonly ciphers?: string;
775
+ }
776
+
777
+ /**
778
+ * Connection — 6-state FSM over a Transport with lifecycle events, warning streams,
779
+ * and getStats() observability.
780
+ *
781
+ * @example
782
+ * ```typescript
783
+ * import { Connection, NetTransport } from '@cosyte/mllp';
784
+ * import { createConnection } from 'node:net';
785
+ *
786
+ * const socket = createConnection({ host: 'localhost', port: 2575 });
787
+ * const transport = new NetTransport(socket);
788
+ * const conn = new Connection({ transport });
789
+ * conn.on('message', ({ payload }) => conn.send(ackBuffer));
790
+ * conn.on('stateChange', ({ from, to }) => logger.info({ from, to }));
791
+ * socket.on('connect', () => conn.notifyConnect(socket.remoteAddress ?? null, socket.remotePort ?? null));
792
+ * ```
793
+ *
794
+ * @packageDocumentation
795
+ */
796
+
797
+ /**
798
+ * The 6 connection states from LIFE-01.
799
+ *
800
+ * Transitions are validated against the LIFE-02 edge graph. Illegal transitions
801
+ * are silently ignored to preserve FSM integrity.
802
+ */
803
+ type ConnectionState = "CONNECTING" | "CONNECTED" | "DRAINING" | "RECONNECTING" | "DISCONNECTED" | "CLOSED";
804
+ /**
805
+ * Payload for the 'stateChange' event. Always `Object.freeze`'d.
806
+ *
807
+ * @example
808
+ * ```typescript
809
+ * conn.on('stateChange', ({ from, to, reason }) => {
810
+ * logger.info({ from, to, reason });
811
+ * });
812
+ * ```
813
+ */
814
+ interface StateChangeEvent {
815
+ readonly from: ConnectionState;
816
+ readonly to: ConnectionState;
817
+ readonly reason?: string;
818
+ }
819
+ /**
820
+ * Payload for the 'reconnecting' event. Always `Object.freeze`'d.
821
+ *
822
+ * Phase 5 will populate `attempt` and `delayMs` once the reconnect backoff loop
823
+ * is implemented. For now only `connectionId` is present at the point of emission.
824
+ *
825
+ * @example
826
+ * ```typescript
827
+ * conn.on('reconnecting', ({ connectionId }) => {
828
+ * logger.info(`Reconnecting connection ${connectionId}`);
829
+ * });
830
+ * ```
831
+ */
832
+ interface ReconnectingEvent {
833
+ readonly connectionId: string;
834
+ readonly attempt?: number;
835
+ readonly delayMs?: number;
836
+ }
837
+ /**
838
+ * Return type of `connection.getStats()` — JSON-serializable per OBS-04.
839
+ *
840
+ * All timestamps are `Date | null` (not epoch milliseconds). `JSON.stringify()`
841
+ * serialises them to ISO 8601 strings by default with no information loss.
842
+ *
843
+ * @example
844
+ * ```typescript
845
+ * const stats = conn.getStats();
846
+ * logger.info(JSON.stringify(stats)); // safe: all values are JSON-serializable
847
+ * ```
848
+ */
849
+ interface ConnectionStats {
850
+ readonly state: ConnectionState;
851
+ readonly connectionId: string;
852
+ readonly remoteAddress: string | null;
853
+ readonly remotePort: number | null;
854
+ readonly warningsByCode: Record<string, number>;
855
+ readonly bytesIn: number;
856
+ readonly bytesOut: number;
857
+ readonly lastByteInAt: Date | null;
858
+ readonly lastByteOutAt: Date | null;
859
+ readonly connectedAt: Date | null;
860
+ readonly warningsTruncated: boolean;
861
+ }
862
+ /**
863
+ * Options for constructing a {@link Connection}.
864
+ *
865
+ * @example
866
+ * ```typescript
867
+ * const opts: ConnectionOptions = {
868
+ * transport: new NetTransport(socket),
869
+ * onMessage: (payload) => handleMessage(payload),
870
+ * onWarning: (w) => logger.warn(w),
871
+ * drainTimeoutMs: 10_000,
872
+ * };
873
+ * ```
874
+ */
875
+ interface ConnectionOptions {
876
+ /** The transport this Connection will drive. */
877
+ transport: Transport;
878
+ /** Called for each decoded MLLP frame (raw payload bytes, framing stripped). */
879
+ onMessage?: (payload: Buffer) => void;
880
+ /** Per-connection warning subscriber (WARN-10). Replaces previous subscription. */
881
+ onWarning?: (w: MllpWarning) => void;
882
+ /** Drain timeout used by close() in Phase 4/5 (default: 30_000 ms). */
883
+ drainTimeoutMs?: number;
884
+ /** FrameReader options (tolerance, maxFrameSizeBytes). onFrame/onWarning are managed internally. */
885
+ framing?: Omit<FrameReaderOptions, "onFrame" | "onWarning">;
886
+ }
887
+ /**
888
+ * A single MLLP connection wrapping a {@link Transport} with a 6-state FSM,
889
+ * lifecycle events, per-connection warning streams, and `getStats()` observability.
890
+ *
891
+ * Emits events: `stateChange`, `connect`, `disconnect`, `reconnecting`, `close`,
892
+ * `message`, `warning`, `error`.
893
+ *
894
+ * @example
895
+ * ```typescript
896
+ * import { Connection, NetTransport } from '@cosyte/mllp';
897
+ * import { createConnection } from 'node:net';
898
+ *
899
+ * const socket = createConnection({ host: 'localhost', port: 2575 });
900
+ * const conn = new Connection({ transport: new NetTransport(socket) });
901
+ * conn.on('stateChange', ({ from, to }) => console.log(from, '->', to));
902
+ * conn.on('message', ({ payload }) => console.log('received', payload.length, 'bytes'));
903
+ * socket.on('connect', () =>
904
+ * conn.notifyConnect(socket.remoteAddress ?? null, socket.remotePort ?? null)
905
+ * );
906
+ * ```
907
+ */
908
+ declare class Connection extends EventEmitter {
909
+ /** Stable UUIDv4 identifier for this connection (D-11). */
910
+ readonly connectionId: string;
911
+ private _state;
912
+ private readonly _transport;
913
+ private readonly _reader;
914
+ private readonly _opts;
915
+ private _remoteAddress;
916
+ private _remotePort;
917
+ private _connectedAt;
918
+ private _bytesIn;
919
+ private _bytesOut;
920
+ private _lastByteInAt;
921
+ private _lastByteOutAt;
922
+ /** Ring buffer: last MAX_WARNINGS entries (OBS-05). */
923
+ private _warningBuffer;
924
+ /** Accurate total counts per code — unaffected by ring buffer truncation (OBS-05). */
925
+ private readonly _warningsByCode;
926
+ /** Set to true once the ring buffer overflows (OBS-05). */
927
+ private _warningsTruncated;
928
+ private _onWarningFn;
929
+ private _drainPromise;
930
+ /**
931
+ * beforeClose hook — no-op default that resolves immediately.
932
+ *
933
+ * Phase 4 (Server) and Phase 5 (Client) override this instance property to
934
+ * register ACK-drain and send-queue drain logic respectively (D-07/D-08).
935
+ * Connection calls this during `close()`, racing against `drainTimeoutMs`.
936
+ */
937
+ beforeClose: (drainTimeoutMs: number) => Promise<void>;
938
+ /**
939
+ * Construct a connection over the given transport. Starts in `CONNECTING`; call
940
+ * {@link Connection.notifyConnect} once the transport handshake completes.
941
+ *
942
+ * @param opts - Connection options (the transport, message/warning handlers, drain timeout, framing).
943
+ */
944
+ constructor(opts: ConnectionOptions);
945
+ /**
946
+ * Current FSM state. One of the 6 `ConnectionState` values (LIFE-01).
947
+ *
948
+ * Subscribe to `'stateChange'` events for reactive state monitoring.
949
+ */
950
+ get state(): ConnectionState;
951
+ /**
952
+ * Register or replace the per-connection warning subscriber (WARN-10).
953
+ *
954
+ * Subsequent calls replace the previous handler (set-once semantics prevent leaks).
955
+ * The handler is called synchronously and any exception it throws is swallowed (WARN-06).
956
+ *
957
+ * @example
958
+ * ```typescript
959
+ * conn.onWarning((w) => logger.warn({ code: w.code, connectionId: w.connectionId }));
960
+ * ```
961
+ */
962
+ onWarning(fn: (w: MllpWarning) => void): void;
963
+ /**
964
+ * Notify the connection that the transport handshake has completed.
965
+ *
966
+ * Transitions `CONNECTING → CONNECTED` and emits the `'connect'` event.
967
+ * Called externally by Server/Client after the socket connects.
968
+ *
969
+ * @param remoteAddress - Remote peer IP address, or `null` if unavailable.
970
+ * @param remotePort - Remote peer port, or `null` if unavailable.
971
+ *
972
+ * @example
973
+ * ```typescript
974
+ * socket.on('connect', () =>
975
+ * conn.notifyConnect(socket.remoteAddress ?? null, socket.remotePort ?? null)
976
+ * );
977
+ * ```
978
+ */
979
+ notifyConnect(remoteAddress: string | null, remotePort: number | null): void;
980
+ /**
981
+ * Write raw bytes to the transport (no framing applied).
982
+ *
983
+ * Phase 4 (Server) and Phase 5 (Client) wrap this with `encodeFrame()` to add MLLP framing.
984
+ * Returns `false` if the connection is CLOSED or DISCONNECTED (no bytes written).
985
+ *
986
+ * @returns `true` if bytes flushed immediately; `false` if buffered (backpressure) or not writable.
987
+ *
988
+ * @example
989
+ * ```typescript
990
+ * const flushed = conn.send(encodeFrame(payload));
991
+ * if (!flushed) logger.warn('backpressure detected');
992
+ * ```
993
+ */
994
+ send(data: Buffer): boolean;
995
+ /**
996
+ * Initiate graceful close of the connection.
997
+ *
998
+ * - If `CONNECTING` or `RECONNECTING`: cancels the pending attempt, transitions
999
+ * directly to `CLOSED`, and calls `transport.destroy()` (LIFE-05).
1000
+ * - If `CONNECTED`: transitions to `DRAINING`, calls `beforeClose(drainTimeoutMs)`,
1001
+ * then `DRAINING → DISCONNECTED` once the hook resolves, or `DRAINING → CLOSED`
1002
+ * if the drain timeout elapses first.
1003
+ * - If `DRAINING`: waits for the existing drain to complete (idempotent).
1004
+ * - If `CLOSED` or `DISCONNECTED`: no-op.
1005
+ *
1006
+ * @param opts.drainTimeoutMs - Override drain timeout (default: `opts.drainTimeoutMs ?? 30_000`).
1007
+ *
1008
+ * @example
1009
+ * ```typescript
1010
+ * await conn.close({ drainTimeoutMs: 5_000 });
1011
+ * ```
1012
+ */
1013
+ close(opts?: {
1014
+ drainTimeoutMs?: number;
1015
+ }): Promise<void>;
1016
+ /**
1017
+ * Race `beforeClose()` against the drain timeout.
1018
+ *
1019
+ * On normal completion: `DRAINING → DISCONNECTED` + `transport.close()`.
1020
+ * On timeout: `DRAINING → CLOSED` + `transport.destroy()` (T-03-04-01).
1021
+ */
1022
+ private _drainWithTimeout;
1023
+ /**
1024
+ * Abruptly destroy the connection, discarding any pending writes.
1025
+ *
1026
+ * Transitions from any non-terminal state directly to `CLOSED` and calls
1027
+ * `transport.destroy(reason)`. Emits `'close'` event. Idempotent — safe to
1028
+ * call multiple times.
1029
+ *
1030
+ * @param reason - Optional error to propagate to the transport.
1031
+ *
1032
+ * @example
1033
+ * ```typescript
1034
+ * conn.destroy(new Error('Timeout exceeded'));
1035
+ * ```
1036
+ */
1037
+ destroy(reason?: Error): void;
1038
+ /**
1039
+ * Return a JSON-serializable observability snapshot (OBS-03/04/05).
1040
+ *
1041
+ * All timestamp fields are `Date | null` (not epoch milliseconds). `JSON.stringify()`
1042
+ * serialises them to ISO 8601 strings with no information loss.
1043
+ *
1044
+ * `warningsByCode` reflects every warning ever received regardless of ring buffer truncation.
1045
+ * `warningsTruncated` is `true` if the 100-entry ring buffer has overflowed.
1046
+ *
1047
+ * @example
1048
+ * ```typescript
1049
+ * const stats = conn.getStats();
1050
+ * console.log(JSON.stringify(stats)); // log-pipeline friendly
1051
+ * ```
1052
+ */
1053
+ getStats(): ConnectionStats;
1054
+ private _transition;
1055
+ private _onTransportClose;
1056
+ /**
1057
+ * Emit on `'error'` only when a listener is attached.
1058
+ *
1059
+ * Node throws `ERR_UNHANDLED_ERROR` when `'error'` is emitted on an `EventEmitter` with no
1060
+ * listener. Every `'error'` emission in this class is reached from inside a transport callback
1061
+ * (the socket's `'data'`/`'error'` listener), so an unguarded emit would become an **uncaught
1062
+ * exception that kills the process** — the exact failure the containment above exists to
1063
+ * prevent, merely relocated one frame up the stack.
1064
+ *
1065
+ * `Connection` is a public export and can legitimately be driven with no `'error'` listener
1066
+ * (`MllpServer` and `MllpClient` each attach one; a bare `Connection` need not). The connection
1067
+ * is still torn down and `'stateChange'`/`'close'` still fire, so a caller is never blind —
1068
+ * they simply do not get the typed error object they never asked for. Mirrors
1069
+ * `MllpServer._emitErrorIfListened`.
1070
+ */
1071
+ private _emitErrorIfListened;
1072
+ /**
1073
+ * Handle a fatal framing error thrown out of `FrameReader.push` (FRAME-FATAL).
1074
+ *
1075
+ * Contract: surface it as a connection `'error'` (frozen payload, `phase: 'receive'`,
1076
+ * `connectionCause: 'framing-fatal'`, the original `MllpFramingError` preserved as `cause` so
1077
+ * consumers keep the stable `code` and `byteOffset`), then **destroy this connection only**.
1078
+ *
1079
+ * Why destroy rather than resynchronize: once `push` has thrown, the reader's position within
1080
+ * the byte stream is no longer trustworthy — the next bytes could be the middle of a message.
1081
+ * Guessing where the next frame starts is how a clinical message gets silently mis-split, so
1082
+ * the connection is dropped instead. A server drops that one peer and keeps serving everyone
1083
+ * else.
1084
+ *
1085
+ * `'framing-fatal'` is classified **permanent** by `isTransientConnectionError`, so a client
1086
+ * does **not** auto-reconnect into it. That matters: a peer speaking something that is not MLLP
1087
+ * (an HTTP probe, a health check, a wrong-port misconfiguration) would otherwise be retried
1088
+ * forever, and the backoff would become an unbounded reconnect storm against an interface
1089
+ * engine that is already unwell. A compatibility failure is not a network blip.
1090
+ *
1091
+ * If a peer's quirk is *expected* (trailing junk, keepalive bytes, a missing leading VT), the
1092
+ * decoder's tolerance opt-ins are the supported answer — they turn the throw into a warning.
1093
+ */
1094
+ private _onFatalFramingError;
1095
+ /**
1096
+ * Backstop for a NON-framing throw escaping `FrameReader.push` — i.e. our own bug, or a
1097
+ * subscriber's, rather than the peer's bytes.
1098
+ *
1099
+ * `_onFrameDecoded` already contains subscriber throws at the dispatch site, so this should be
1100
+ * unreachable. It exists because "unreachable" and "cannot kill the process from inside a
1101
+ * socket `'data'` handler" are different claims, and only the second one is safe to bet a
1102
+ * clinical interface on. Reported honestly as an internal receive error — never dressed up as
1103
+ * a peer framing fault.
1104
+ */
1105
+ private _onInternalReceiveError;
1106
+ private _onTransportError;
1107
+ private _onFrameDecoded;
1108
+ /**
1109
+ * Run a subscriber callback, containing any throw as a connection `'error'` rather than letting
1110
+ * it unwind into the socket callback that (transitively) invoked us.
1111
+ */
1112
+ private _dispatchContained;
1113
+ private _onFramingWarning;
1114
+ }
1115
+
1116
+ /**
1117
+ * Typed error for socket-layer connection problems.
1118
+ *
1119
+ * @example
1120
+ * ```typescript
1121
+ * import { MllpConnectionError } from '@cosyte/mllp';
1122
+ * try {
1123
+ * await client.connect();
1124
+ * } catch (err) {
1125
+ * if (err instanceof MllpConnectionError) {
1126
+ * console.log(err.phase, err.cause);
1127
+ * }
1128
+ * }
1129
+ * ```
1130
+ *
1131
+ * @packageDocumentation
1132
+ */
1133
+ /**
1134
+ * Connection lifecycle phase where the error occurred.
1135
+ *
1136
+ * All 5 phases are defined in Phase 3 even though `'reconnect'` is only
1137
+ * exercised in Phase 5 (CLIENT-17). Locking the full union now prevents a
1138
+ * breaking type change later (ERR-03).
1139
+ */
1140
+ type ConnectionErrorPhase = "connect" | "send" | "receive" | "close" | "reconnect";
1141
+ /**
1142
+ * Stable cause codes for `MllpConnectionError`.
1143
+ *
1144
+ * These codes are a **public API** — they appear in error inspection
1145
+ * by callers, log pipelines, and monitoring dashboards. Renaming or
1146
+ * removing a member is a breaking change.
1147
+ *
1148
+ * - `'fifo-unsafe'` — A queued send was rejected during reconnect because
1149
+ * FIFO ordering cannot be safely resumed across sessions (CLIENT-17).
1150
+ * - `'in-flight-orphan'` — An in-flight send (already write-flushed,
1151
+ * ACK timer started) was rejected during reconnect in FIFO mode because
1152
+ * the at-most-once delivery contract cannot be preserved across a
1153
+ * socket drop (D-08, healthcare medication/orders semantics).
1154
+ * - `'tls-verify'` (Phase 8) — The TLS handshake failed certificate
1155
+ * verification (untrusted chain, expired/not-yet-valid cert, hostname
1156
+ * mismatch, revocation, …). See {@link isTlsVerificationErrorCode} for the
1157
+ * exact underlying error codes. Classified **permanent** by
1158
+ * `isTransientConnectionError` — never auto-reconnect-looped into a
1159
+ * misconfigured or MITM'd endpoint.
1160
+ * - `'tls-handshake'` (Phase 8) — A TLS-**protocol**-shaped handshake
1161
+ * failure observed before `'secureConnect'`: `ERR_SSL_*` codes, `EPROTO`,
1162
+ * or an OpenSSL alert-bearing error (protocol version mismatch, no shared
1163
+ * cipher, a required mutual-TLS client certificate rejected by the
1164
+ * server, …). See `isTlsProtocolError` for the boundary. Pure TCP-level
1165
+ * failures (`ECONNREFUSED`, `ETIMEDOUT`, …) on a TLS-configured
1166
+ * connection carry **no** `connectionCause` — the same shape as plaintext.
1167
+ *
1168
+ * Scope note: the `'tls-verify'`/`'tls-handshake'` values are attached on
1169
+ * the client's **initial `connect()` path**. Failures on the auto-reconnect
1170
+ * path surface as raw socket errors — their transient/permanent
1171
+ * classification still applies, but they do not (yet) carry a
1172
+ * `connectionCause`.
1173
+ *
1174
+ * @example
1175
+ * ```typescript
1176
+ * if (err instanceof MllpConnectionError && err.connectionCause === 'in-flight-orphan') {
1177
+ * // Treat as at-most-once: do NOT auto-retry; bubble up to caller for
1178
+ * // application-level dedupe / replay decisions.
1179
+ * }
1180
+ * ```
1181
+ */
1182
+ type ConnectionErrorCause = "fifo-unsafe" | "in-flight-orphan" | "tls-verify" | "tls-handshake" | "framing-fatal";
1183
+ /**
1184
+ * Thrown (or emitted via `onError`) for socket-layer problems such as
1185
+ * connection refused, ECONNRESET, ETIMEDOUT, or DNS failure.
1186
+ *
1187
+ * - `cause` — the original OS or TLS error
1188
+ * - `phase` — which connection lifecycle phase the failure occurred in
1189
+ * - `connectionCause` — optional stable cause-code (e.g. for FIFO reconnect rejections)
1190
+ *
1191
+ * @example
1192
+ * ```typescript
1193
+ * throw new MllpConnectionError('Connection refused', {
1194
+ * cause: osError,
1195
+ * phase: 'connect',
1196
+ * });
1197
+ * ```
1198
+ */
1199
+ declare class MllpConnectionError extends Error {
1200
+ readonly name: "MllpConnectionError";
1201
+ /** The original OS or TLS error that caused this connection failure. */
1202
+ readonly cause: Error;
1203
+ /** Which connection lifecycle phase the failure occurred in. */
1204
+ readonly phase: ConnectionErrorPhase;
1205
+ /**
1206
+ * Optional stable cause code. Present on FIFO reconnect rejections
1207
+ * (`'fifo-unsafe'` for queued sends, `'in-flight-orphan'` for in-flight sends).
1208
+ *
1209
+ * The set of values is a **public API**; see {@link ConnectionErrorCause}.
1210
+ */
1211
+ readonly connectionCause?: ConnectionErrorCause;
1212
+ /**
1213
+ * Construct an MLLP connection error.
1214
+ *
1215
+ * @param message - Human-readable error message.
1216
+ * @param opts - Error context (underlying `cause`, the lifecycle `phase`, optional stable cause code).
1217
+ */
1218
+ constructor(message: string, opts: {
1219
+ cause: Error;
1220
+ phase: ConnectionErrorPhase;
1221
+ connectionCause?: ConnectionErrorCause;
1222
+ });
1223
+ }
1224
+
1225
+ /**
1226
+ * Server-side MLLP acknowledgement construction and the **fail-safe commit contract**.
1227
+ *
1228
+ * This module owns the byte-level ACK builder used by {@link MllpServer}'s auto-ACK
1229
+ * path, the HL7 Table 0008 acknowledgement-code union, and the error type a message
1230
+ * handler throws to request a specific negative acknowledgement.
1231
+ *
1232
+ * ## Why this is safety-critical
1233
+ *
1234
+ * A positive acknowledgement (`AA`) tells the sender "you may forget this message — I
1235
+ * have it." If a server emits `AA` *before* the message is durably handled and then the
1236
+ * process crashes, the message is **silently lost** with no record on either side. For
1237
+ * clinical messages (an admit, an order, a result) that is a patient-safety failure.
1238
+ *
1239
+ * The contract this module enforces: a positive ACK (`AA`) is only built once the
1240
+ * application handler has resolved success. A handler that throws or rejects yields a
1241
+ * **negative** ACK (`AE` by default, `AR` when the handler asks for it) — never `AA`.
1242
+ *
1243
+ * Spec: HL7 v2.5.1 ch. 2 §2.9.2 (original acknowledgement mode), §2.9.3 (enhanced
1244
+ * accept/application acknowledgement), Table 0008 (Acknowledgment Code), §2.9.2.2
1245
+ * (MSA-2 echoes the inbound MSH-10 message control ID).
1246
+ *
1247
+ * @packageDocumentation
1248
+ */
1249
+ /**
1250
+ * HL7 Table 0008 — Acknowledgment Code. A **stable public API**.
1251
+ *
1252
+ * Two families, by mode (HL7 v2.5.1 §2.9):
1253
+ *
1254
+ * - **Original mode (§2.9.2):** `AA` accept, `AE` application error, `AR` application
1255
+ * reject. The single ACK reports application-level outcome.
1256
+ * - **Enhanced mode (§2.9.3):** `CA` commit accept, `CE` commit error, `CR` commit
1257
+ * reject — the *accept* acknowledgement, distinct from a later application ACK.
1258
+ *
1259
+ * `AE` vs `AR`: `AE` is a processing **error** (the sender may resend later, e.g. a
1260
+ * transient downstream outage); `AR` is a **reject** (the sender should not resend the
1261
+ * message unchanged, e.g. it is structurally unacceptable). `@cosyte/mllp` builds
1262
+ * original-mode ACKs; the `C*` codes are surfaced in the type for completeness and for
1263
+ * callers that build their own enhanced-mode ACKs via `autoAck: fn`.
1264
+ *
1265
+ * @example
1266
+ * ```typescript
1267
+ * import type { AckCode } from '@cosyte/mllp';
1268
+ * const code: AckCode = 'AE';
1269
+ * ```
1270
+ */
1271
+ type AckCode = "AA" | "AE" | "AR" | "CA" | "CE" | "CR";
1272
+ /**
1273
+ * The negative original-mode acknowledgement codes a failed handler can produce.
1274
+ *
1275
+ * @example
1276
+ * ```typescript
1277
+ * import type { NegativeAckCode } from '@cosyte/mllp';
1278
+ * const code: NegativeAckCode = 'AR';
1279
+ * ```
1280
+ */
1281
+ type NegativeAckCode = "AE" | "AR";
1282
+ /**
1283
+ * Error a message handler throws to control the negative acknowledgement the server
1284
+ * returns. Throwing **any** error from a commit-gated handler yields `AE` by default;
1285
+ * throw an `MllpAckError` to choose `AR` (application reject) instead.
1286
+ *
1287
+ * The `message` is **never** copied into the ACK bytes or any emitted event — it may
1288
+ * carry PHI. Only the static, non-PHI `ackCode` influences the wire response.
1289
+ *
1290
+ * @example
1291
+ * ```typescript
1292
+ * import { createServer, MllpAckError } from '@cosyte/mllp';
1293
+ *
1294
+ * createServer({
1295
+ * autoAck: 'AA',
1296
+ * onMessage: async (payload) => {
1297
+ * if (!isAcceptable(payload)) {
1298
+ * // sender should NOT resend unchanged -> AR
1299
+ * throw new MllpAckError('unsupported message type', { ackCode: 'AR' });
1300
+ * }
1301
+ * await db.commit(payload); // throw here -> AE (resend may succeed)
1302
+ * },
1303
+ * });
1304
+ * ```
1305
+ */
1306
+ declare class MllpAckError extends Error {
1307
+ /** The negative acknowledgement code to return (`AE` or `AR`). */
1308
+ readonly ackCode: NegativeAckCode;
1309
+ /**
1310
+ * @param message - Diagnostic text for the thrower. Never placed on the wire (may carry PHI).
1311
+ * @param opts.ackCode - Negative code to return. Default `'AE'`.
1312
+ * @param opts.cause - Underlying error, preserved on `.cause`.
1313
+ */
1314
+ constructor(message: string, opts?: {
1315
+ ackCode?: NegativeAckCode;
1316
+ cause?: unknown;
1317
+ });
1318
+ }
1319
+ /**
1320
+ * Resolve the negative acknowledgement code for a handler failure.
1321
+ *
1322
+ * An {@link MllpAckError} carries an explicit `ackCode`; any other thrown value maps to
1323
+ * `AE` (application error — the default, since most handler failures are transient and
1324
+ * a resend may succeed).
1325
+ *
1326
+ * @example
1327
+ * ```typescript
1328
+ * import { resolveNackCode, MllpAckError } from '@cosyte/mllp';
1329
+ * resolveNackCode(new Error('boom')); // 'AE'
1330
+ * resolveNackCode(new MllpAckError('nope', { ackCode: 'AR' })); // 'AR'
1331
+ * ```
1332
+ */
1333
+ declare function resolveNackCode(err: unknown): NegativeAckCode;
1334
+ /**
1335
+ * True iff a **positive** raw acknowledgement (`AA`/`CA`) cannot be safely correlated to
1336
+ * this payload, so {@link buildRawAck} (and the server's auto-ACK path) must **downgrade**
1337
+ * it to a non-positive `AE`/`CE` rather than tell the sender "I have it."
1338
+ *
1339
+ * The rule this enforces: **never answer `AA` for a message you could not correlate.** A
1340
+ * positive ACK is a promise the sender may forget the message; if it names a control ID
1341
+ * the sender cannot match — or names one of several messages it never read — the sender
1342
+ * times out and resends, committing a **duplicate clinical message** (or worse, believes a
1343
+ * destroyed message was delivered). Three payload-shaped reasons make a positive ACK
1344
+ * uncorrelatable, all peer-reachable off the wire:
1345
+ *
1346
+ * 1. **No readable `MSH`.** `readMshSegment` returns `null` — e.g. a `BOM`/`SP`/`TAB`
1347
+ * before `MSH` (which shares the `MSH`'s segment line, so `MSH` heads no segment), or
1348
+ * a bare fragment delivered after a mid-payload `VT` discard (`MLLP_TRAILING_BYTES`).
1349
+ * MSA-2 would be empty: an ACK that correlates to nothing.
1350
+ * 2. **Empty MSH-10.** The `MSH` is readable but carries no message control ID, so there
1351
+ * is nothing to echo — again `MSA|AA|` with an empty MSA-2.
1352
+ * 3. **A batch or concatenated messages.** An `FHS`/`BHS`/`BTS`/`FTS` envelope (§2.10.3)
1353
+ * or a second `MSH` in the same frame: a single MSA-2 can echo only ONE control ID, so
1354
+ * a positive ACK naming the first silently drops the rest (see
1355
+ * {@link containsBatchOrExtraMessage}). Batch ACK is its own feature (`MLLP-BATCH`) —
1356
+ * until it is designed, a batch must stay a **loud non-positive** answer.
1357
+ *
1358
+ * This is a **refusal**, not a tolerance widening: it never makes an unreadable message
1359
+ * readable, never re-bases on a located `MSH`, never parses a batch. It only recognizes
1360
+ * the shapes for which a positive disposition would be a lie, so the builder can fall back
1361
+ * to `AE`. A **negative** requested code (`AE`/`AR`/`CE`/`CR`) is unaffected — it is
1362
+ * already non-positive, and echoing whatever control ID it can find is still useful.
1363
+ *
1364
+ * Pure, byte-level, never throws.
1365
+ *
1366
+ * @example
1367
+ * ```typescript
1368
+ * import { rawAckUncorrelatable } from '@cosyte/mllp';
1369
+ * rawAckUncorrelatable(oneGoodMessage); // false → AA is safe
1370
+ * rawAckUncorrelatable(twoConcatenatedMsh); // true → downgrade AA to AE
1371
+ * ```
1372
+ */
1373
+ declare function rawAckUncorrelatable(payload: Buffer): boolean;
1374
+ /**
1375
+ * Build a minimal original-mode HL7 v2 acknowledgement from raw inbound payload bytes,
1376
+ * **without a parser** (parser-driven ACKs are the `@cosyte/mllp/ack-from-hl7` subpath).
1377
+ *
1378
+ * Locates the `MSH` segment (the first `CR`/`LF`-delimited segment starting with `MSH` —
1379
+ * so a leading `CR` or an `FHS`/`BHS` batch header does not hide it), reads the field
1380
+ * separator from **MSH-1** and the encoding characters from **MSH-2**, then splits that
1381
+ * segment on that separator to read fields. The ACK swaps sender/receiver per HL7 ACK
1382
+ * rules, echoes the inbound MSH-10 into MSA-2 (§2.9.2.2), sets MSA-1 to `code`, and — for
1383
+ * negative codes — adds a **static, PHI-free** MSA-3 reason. A fresh control ID fills the
1384
+ * ACK's own MSH-10.
1385
+ *
1386
+ * ## Why the delimiters are read, not assumed
1387
+ *
1388
+ * MSH-1 **is** the field separator (HL7 v2.5.1 §2.5.4): the byte at offset 3 of the MSH
1389
+ * segment *defines* it for the whole message. `|` is overwhelmingly common but it is a
1390
+ * convention, not the spec. Assuming it was wrong in two compounding ways:
1391
+ *
1392
+ * 1. **Reading.** Splitting a `!`-delimited message on `|` yields ONE field, so every
1393
+ * echoed field — including MSH-10 — came back empty. The ACK went out as
1394
+ * `MSA|AA|` with **no correlation id at all**: the sender cannot match it, times
1395
+ * out, and resends → a duplicate clinical message. That is the exact failure this
1396
+ * package's correlator exists to prevent, manufactured by the ACK builder itself.
1397
+ * 2. **Writing.** The echoed MSH-3..6 and MSH-10 field *content* is copied verbatim
1398
+ * from the inbound, still escaped against the **inbound's** encoding characters. Re-
1399
+ * emitting that content under a different delimiter set silently reinterprets it:
1400
+ * an inbound whose component separator is `#` and whose MSH-10 is `ID#X` would be
1401
+ * re-emitted under `^~\&` as the literal `ID#X`, which the sender then reads as a
1402
+ * single component rather than two. Echoing MSH-1 and MSH-2 keeps the content and
1403
+ * the delimiters that define it together.
1404
+ *
1405
+ * ## It reads the MSH through the SHARED scanner
1406
+ *
1407
+ * The MSH read is `readMshSegment` from `src/internal/control-id.ts` — the same call the
1408
+ * client's correlator makes to derive the key it will later match this ACK against. That
1409
+ * is deliberate and it is the whole point: this builder used to re-derive the read itself
1410
+ * (`payload.toString("latin1").split("\r")`, hunting for an `MSH` anywhere in the
1411
+ * payload), and the two disagreed on real inputs. On a **truncated MSH** followed by a
1412
+ * `PID` the correlator keyed on the PID's MRN while this builder echoed an empty MSA-2. On
1413
+ * a payload with a **leading `CR`** — which the MLLP decoder passes straight through, and
1414
+ * which real senders emit — this builder found the `MSH` and echoed MSH-10 correctly while
1415
+ * the correlator, requiring `MSH` at byte 0, gave up. Every such disagreement is an ACK the
1416
+ * sender cannot match → timeout → resend → **duplicate clinical message**.
1417
+ *
1418
+ * The fix is one scan — but note *which* scan. The first attempt made them agree by
1419
+ * requiring `MSH` at byte 0 everywhere, which "resolved" the leading-`CR` disagreement by
1420
+ * degrading the side that had been **right**: `buildRawAck` began emitting a positive `AA`
1421
+ * with an empty MSA-2, silently, for a message whose MSH-10 was plainly present. Agreement
1422
+ * is not the goal; agreeing on the *correct*, *tolerant* answer is. A lenient reader may
1423
+ * never drop data that is there (Postel's Law — CLAUDE.md).
1424
+ *
1425
+ * ## The fail-safe downgrade: never a positive `AA`/`CA` it cannot correlate
1426
+ *
1427
+ * A positive acknowledgement is a promise the sender may forget the message. If it names a
1428
+ * control ID the sender cannot match — or names one message out of several it never read —
1429
+ * the sender times out and resends, committing a **duplicate clinical message**. So a
1430
+ * requested positive code is **downgraded** to its non-positive counterpart (`AA`→`AE`,
1431
+ * `CA`→`CE`) whenever the payload cannot carry a correlatable positive ACK: no readable
1432
+ * `MSH`, an empty MSH-10, or a batch/concatenated-message shape that a single MSA-2 cannot
1433
+ * acknowledge. See {@link rawAckUncorrelatable} for the exact conditions and why each is a
1434
+ * refusal rather than a widened reader. A requested **negative** code (`AE`/`AR`/`CE`/`CR`)
1435
+ * is never touched. This mirrors the parser-backed `buildMllpAck`, which downgrades and
1436
+ * warns on an unparseable inbound — the two builders' fail-safe semantics now agree.
1437
+ *
1438
+ * **Never throws** and **never copies payload content** beyond the routing/control
1439
+ * metadata above — `readMshSegment` stops at the MSH's segment terminator, so no field of
1440
+ * any later segment (PID and friends) can be reached, let alone echoed. On a missing or
1441
+ * unreadable `MSH` it returns a minimal well-formed ACK carrying the (downgraded) `code` so
1442
+ * the caller can still respond.
1443
+ *
1444
+ * @param payload - Raw decoded HL7 v2 payload bytes (MLLP framing already stripped).
1445
+ * @param code - Requested MSA-1 acknowledgement code. A positive `AA`/`CA` is downgraded to
1446
+ * `AE`/`CE` when the message cannot be correlated (see above).
1447
+ * @returns ACK payload bytes (no framing — the caller wraps with `encodeFrame`).
1448
+ *
1449
+ * @example
1450
+ * ```typescript
1451
+ * import { buildRawAck } from '@cosyte/mllp';
1452
+ * const ack = buildRawAck(inboundPayload, 'AE'); // MSA|AE|<inbound MSH-10>|message could not be processed
1453
+ * ```
1454
+ */
1455
+ declare function buildRawAck(payload: Buffer, code: AckCode): Buffer;
1456
+
1457
+ /**
1458
+ * MLLP Server — createServer(), createStarterServer(), and MllpServer class.
1459
+ *
1460
+ * Provides the server-side MLLP transport: listen for inbound TCP connections,
1461
+ * decode MLLP-framed messages, surface them as Buffer payloads, and support
1462
+ * optional auto-ACK, keepalive, graceful shutdown, and AbortSignal cancellation.
1463
+ *
1464
+ * @example
1465
+ * ```typescript
1466
+ * import { createServer } from '@cosyte/mllp';
1467
+ *
1468
+ * const server = createServer({
1469
+ * onMessage: (payload, meta, conn) => {
1470
+ * console.log('received', payload.length, 'bytes from', meta.connectionId);
1471
+ * },
1472
+ * });
1473
+ * await server.listen(2575);
1474
+ * ```
1475
+ *
1476
+ * @packageDocumentation
1477
+ */
1478
+
1479
+ /**
1480
+ * Metadata attached to each decoded MLLP message (SERVER-03).
1481
+ *
1482
+ * All fields are `readonly` — the object is `Object.freeze()`'d before emission.
1483
+ *
1484
+ * @example
1485
+ * ```typescript
1486
+ * server.on('message', ({ payload, meta }) => {
1487
+ * console.log(meta.connectionId, '@', meta.byteOffset, 'warnings:', meta.warnings.length);
1488
+ * });
1489
+ * ```
1490
+ */
1491
+ interface MessageMeta {
1492
+ /** Stable UUID identifying the connection that delivered this message. */
1493
+ readonly connectionId: string;
1494
+ /** Byte offset of the frame start in the connection's data stream. */
1495
+ readonly byteOffset: number;
1496
+ /** Framing warnings emitted during decoding of this frame. */
1497
+ readonly warnings: readonly MllpWarning[];
1498
+ }
1499
+ /**
1500
+ * Why the server sent a **negative** acknowledgement instead of `AA` on the auto-ACK path.
1501
+ * A stable, **PHI-free** enum (no payload bytes, no control ID) — safe to log and to key
1502
+ * metrics on.
1503
+ *
1504
+ * - `'handler-rejected'` — a commit-gated `onMessage` handler threw/rejected (the commit
1505
+ * contract: a positive ACK cannot precede a successful commit).
1506
+ * - `'uncorrelatable-inbound'` — the inbound could not carry a correlatable positive ACK
1507
+ * (no readable `MSH`, an empty MSH-10, or a batch/concatenated-message shape). A positive
1508
+ * `AA` here names a control ID the sender cannot match → timeout → resend → **duplicate
1509
+ * clinical message**. See {@link rawAckUncorrelatable}.
1510
+ * - `'discarded-bytes'` — the decoder flagged `MLLP_TRAILING_BYTES` for this frame: a
1511
+ * mid-payload `VT` made it **discard accumulated bytes** and deliver only the fragment
1512
+ * after it. The clinical message was destroyed in transit; a positive `AA` would tell the
1513
+ * sender a message we never received was delivered.
1514
+ */
1515
+ type NackReason = "handler-rejected" | "uncorrelatable-inbound" | "discarded-bytes";
1516
+ /**
1517
+ * Payload of the server `'nack'` event — emitted whenever the server responds with a
1518
+ * **negative** acknowledgement instead of `AA` on the auto-ACK path (the fail-safe commit
1519
+ * contract). This fires both when a commit-gated `autoAck: 'AA'` handler throws/rejects and
1520
+ * when the server **downgrades** a positive auto-ACK because the inbound could not be
1521
+ * correlated ({@link NackReason}).
1522
+ *
1523
+ * **PHI-safe by construction:** carries only the connection ID, the resolved
1524
+ * acknowledgement code, and a static `reason` — never the payload, the inbound control ID,
1525
+ * or the thrown error's message (which may carry PHI). The object is `Object.freeze()`'d
1526
+ * before emission.
1527
+ *
1528
+ * @example
1529
+ * ```typescript
1530
+ * server.on('nack', ({ connectionId, ackCode, reason }) => {
1531
+ * metrics.increment('mllp.nack', { code: ackCode, reason }); // e.g. reason='discarded-bytes'
1532
+ * });
1533
+ * ```
1534
+ */
1535
+ interface NackEvent {
1536
+ /** Connection that produced the negative acknowledgement. */
1537
+ readonly connectionId: string;
1538
+ /** The negative acknowledgement code sent to the peer (`AE` or `AR`). */
1539
+ readonly ackCode: NegativeAckCode;
1540
+ /** Why the negative acknowledgement was sent (PHI-free). */
1541
+ readonly reason: NackReason;
1542
+ }
1543
+ /**
1544
+ * Observability snapshot returned by `server.getStats()` (OBS-02).
1545
+ *
1546
+ * All fields are JSON-serializable. `connections` and `activeConnections` both
1547
+ * reflect the current live connection count (ROADMAP SC-5 / OBS-02).
1548
+ *
1549
+ * @example
1550
+ * ```typescript
1551
+ * const stats = server.getStats();
1552
+ * console.log(JSON.stringify(stats)); // log-pipeline friendly
1553
+ * ```
1554
+ */
1555
+ interface ServerStats {
1556
+ /** Whether the server is currently accepting connections. */
1557
+ readonly listening: boolean;
1558
+ /** Bound port, or `null` before listen(). */
1559
+ readonly port: number | null;
1560
+ /** Bound host, or `null` before listen(). */
1561
+ readonly host: string | null;
1562
+ /**
1563
+ * Current live connection count (OBS-02, ROADMAP SC-5).
1564
+ * Same value as `activeConnections`.
1565
+ */
1566
+ readonly connections: number;
1567
+ /** Current live connection count. Same value as `connections`. */
1568
+ readonly activeConnections: number;
1569
+ /** Aggregate bytes received across all current connections. */
1570
+ readonly totalBytesIn: number;
1571
+ /** Aggregate bytes sent across all current connections. */
1572
+ readonly totalBytesOut: number;
1573
+ /** Total connections accepted since listen() (monotonically increasing). */
1574
+ readonly acceptedTotal: number;
1575
+ /** Total connections closed since listen() (monotonically increasing). */
1576
+ readonly closedTotal: number;
1577
+ /** Whether this server is configured for TLS (Phase 8). Mirrors `ServerOptions.tls` being set. */
1578
+ readonly tls: boolean;
1579
+ /** Total `'tlsClientError'` events (failed TLS handshakes, incl. rejected client certs) since listen() (Phase 8). */
1580
+ readonly tlsClientErrorsTotal: number;
1581
+ }
1582
+ /**
1583
+ * Options for `createServer()`.
1584
+ *
1585
+ * @example
1586
+ * ```typescript
1587
+ * const opts: ServerOptions = {
1588
+ * onMessage: (payload, meta, conn) => {
1589
+ * const ack = buildAck(payload);
1590
+ * conn.send(ack);
1591
+ * },
1592
+ * framing: { maxFrameSizeBytes: 4 * 1024 * 1024 },
1593
+ * keepaliveIntervalMs: 60_000,
1594
+ * deadPeerTimeoutMs: 300_000,
1595
+ * drainTimeoutMs: 30_000,
1596
+ * };
1597
+ * ```
1598
+ */
1599
+ interface ServerOptions {
1600
+ /**
1601
+ * Called for each decoded MLLP message.
1602
+ *
1603
+ * Its role depends on `autoAck` — this is the **commit contract** (HL7 v2.5.1 §2.9.2):
1604
+ *
1605
+ * - **`autoAck: 'AA'` + this handler ⇒ commit-gated (the safe default).** The handler
1606
+ * is the durable-commit step. The server **awaits** it and only then sends the ACK:
1607
+ * resolve ⇒ `AA`; **throw/reject ⇒ `AE`** (or `AR` via {@link MllpAckError}) — a
1608
+ * positive ACK can never precede a successful commit. Do **not** call `conn.send()`
1609
+ * here in this mode.
1610
+ * - **`autoAck` unset ⇒ manual mode.** The handler owns the response; build and send the
1611
+ * ACK yourself via `conn.send(encodeFrame(ackPayload))`. Its return value is ignored.
1612
+ * - **`autoAck: fn` ⇒ observation only.** `fn` builds the ACK; this handler runs first as
1613
+ * a side effect and its return value is ignored. Do not call `conn.send()` here.
1614
+ *
1615
+ * May be sync or async; an async handler is awaited in commit-gated mode.
1616
+ */
1617
+ onMessage?: (payload: Buffer, meta: MessageMeta, conn: Connection) => void | Promise<void>;
1618
+ /**
1619
+ * FrameReader tolerance options applied to every accepted connection (SERVER-12).
1620
+ * Merged with SERVER_DEFAULT_FRAMING — caller-supplied values override defaults.
1621
+ * `onFrame` and `onWarning` are managed internally and must not be supplied here.
1622
+ */
1623
+ framing?: Omit<FrameReaderOptions, "onFrame" | "onWarning">;
1624
+ /**
1625
+ * Per-connection warning subscriber. Called for every framing warning on every connection.
1626
+ */
1627
+ onWarning?: (w: MllpWarning) => void;
1628
+ /**
1629
+ * Auto-ACK mode. When set, the server builds and sends the ACK for each message.
1630
+ *
1631
+ * - **`'AA'`** — auto-acknowledge with the fail-safe **commit contract**:
1632
+ * - **With an `onMessage` handler ⇒ commit-gated (recommended).** The server awaits
1633
+ * `onMessage` (the durable-commit step), then sends `AA` on success or a **negative**
1634
+ * ACK on failure (`AE` by default; `AR` via {@link MllpAckError}). The positive ACK
1635
+ * **cannot precede a successful commit** — a handler throw can never yield `AA`.
1636
+ * - **Without an `onMessage` handler ⇒ transport-accept.** `AA` is sent on frame
1637
+ * receipt. This `AA` means only **"bytes received and framed"** — *not*
1638
+ * "application-processed". ⚠️ For clinical messages this is unsafe on its own:
1639
+ * pair `'AA'` with an `onMessage` handler that durably commits, so the ACK reflects
1640
+ * real processing.
1641
+ * - **`fn`** — `fn(payload, meta, conn)` builds the ACK bytes the server sends; the
1642
+ * caller fully owns MSA-1 (e.g. to emit enhanced-mode `CA`/`CE`/`CR`).
1643
+ *
1644
+ * The `'message'` event always fires BEFORE the ACK is sent (D-03). Do NOT call
1645
+ * `conn.send()` in `onMessage` when `autoAck` is set — this results in two ACKs.
1646
+ */
1647
+ autoAck?: "AA" | ((payload: Buffer, meta: MessageMeta, conn: Connection) => Buffer | Promise<Buffer>);
1648
+ /**
1649
+ * TCP keepalive probe interval in ms (D-10).
1650
+ * Calls `socket.setKeepAlive(true, ms)` on each accepted socket.
1651
+ * Uses OS TCP stack to detect dead peers (half-open, network partitions).
1652
+ * Distinct from `deadPeerTimeoutMs` (application-level idle close).
1653
+ */
1654
+ keepaliveIntervalMs?: number;
1655
+ /**
1656
+ * Application-level idle close timeout in ms (D-11, ROADMAP SC-5).
1657
+ * If no HL7 messages are received on a connection for this interval, the connection
1658
+ * is destroyed via `conn.destroy(new Error('idle timeout'))`.
1659
+ * Resets on every `'message'` event. Distinct from `keepaliveIntervalMs` (OS TCP probe).
1660
+ */
1661
+ deadPeerTimeoutMs?: number;
1662
+ /**
1663
+ * Graceful drain timeout passed to `conn.close()` during `server.close()` (D-06).
1664
+ * Default: 30 000 ms.
1665
+ */
1666
+ drainTimeoutMs?: number;
1667
+ /**
1668
+ * Enable TLS (MLLPS) for this server (Phase 8). When set, the server binds a
1669
+ * `tls.Server` instead of a plain `net.Server`, consumes `'secureConnection'`
1670
+ * (post-handshake sockets) instead of `'connection'`, and surfaces failed
1671
+ * handshakes via the `'tlsClientError'` event rather than crashing.
1672
+ *
1673
+ * Spec anchor: IHE ATNA ITI-19 (https://profiles.ihe.net/ITI/TF/Volume2/ITI-19.html).
1674
+ *
1675
+ * @default undefined (plaintext TCP)
1676
+ */
1677
+ tls?: ServerTlsOptions;
1678
+ /**
1679
+ * Opt-in required to bind a wildcard host — a bind-safety guardrail
1680
+ * (Phase 8). Without this flag, `listen()` rejects a wildcard host in
1681
+ * two tiers: **literal spellings** (`'0.0.0.0'`, `'::'`, `''`, `'::0'`,
1682
+ * `'0:0:0:0:0:0:0:0'`, `'::ffff:0.0.0.0'`, …) reject **pre-bind** (fast
1683
+ * path, nothing is ever bound); **resolver-only shorthands** (`'0'`,
1684
+ * `'0.0'`, `'0x0.0.0.0'`, hostnames resolving to the unspecified
1685
+ * address, …) are caught **post-bind** against the OS-normalized bound
1686
+ * address — the just-bound server closes before any connection can be
1687
+ * accepted and `listen()` rejects, with no listening state and no
1688
+ * `'listening'` event either way. When `true`, binding a wildcard host
1689
+ * emits a one-time `'securityWarning'` (`MLLP_BIND_ALL_INTERFACES`) at
1690
+ * listen time.
1691
+ *
1692
+ * @default false
1693
+ */
1694
+ allowWildcardBind?: boolean;
1695
+ }
1696
+ /**
1697
+ * Options for `createStarterServer()` — the "three lines of code" factory (SERVER-08).
1698
+ *
1699
+ * Extends `ServerOptions` with `port`, `host`, and `handleSignals`. Defaults:
1700
+ * `autoAck: 'AA'`, `drainTimeoutMs: 30_000`, `Symbol.asyncDispose` wired.
1701
+ *
1702
+ * @example
1703
+ * ```typescript
1704
+ * import { createStarterServer } from '@cosyte/mllp';
1705
+ *
1706
+ * const server = await createStarterServer({
1707
+ * port: 2575,
1708
+ * onMessage: (buf) => buildAckBuffer(buf),
1709
+ * });
1710
+ * // server is listening, auto-ACK enabled, Symbol.asyncDispose wired
1711
+ * await using _ = server; // closes on scope exit
1712
+ * ```
1713
+ */
1714
+ interface StarterServerOptions extends ServerOptions {
1715
+ /** Port to listen on. */
1716
+ port: number;
1717
+ /**
1718
+ * Host to bind to. Default `'127.0.0.1'` (Phase 8 bind-safety hardening —
1719
+ * was `'0.0.0.0'`; binding all interfaces now requires
1720
+ * `ServerOptions.allowWildcardBind: true`).
1721
+ */
1722
+ host?: string;
1723
+ /**
1724
+ * Register `process.once('SIGTERM')` and `process.once('SIGINT')` handlers that
1725
+ * call `server.close()` then `process.exit(0)` (D-09). Default: `false`.
1726
+ *
1727
+ * Handlers are automatically removed when `server.close()` is called, so
1728
+ * `process.listenerCount('SIGTERM') === 0` after close() completes — preventing
1729
+ * handler accumulation across test instances or multiple server restarts.
1730
+ */
1731
+ handleSignals?: boolean;
1732
+ }
1733
+ /**
1734
+ * MLLP TCP server — wraps `net.Server` without extending it (D-02).
1735
+ *
1736
+ * Each accepted socket is wrapped in a `NetTransport`, connected to a `Connection`
1737
+ * with server-level framing options, and added to `_connections`. Messages are
1738
+ * surfaced via the `'message'` event and the `onMessage` callback.
1739
+ *
1740
+ * Public events: `'listening'`, `'connection'`, `'message'`, `'nack'`, `'error'`, `'close'`.
1741
+ * The `'nack'` event ({@link NackEvent}) fires whenever the server returns a negative ACK
1742
+ * instead of `AA` on the auto-ACK path — a commit-gated `autoAck: 'AA'` handler failing, or
1743
+ * a positive auto-ACK **downgraded** because the inbound could not be correlated (an
1744
+ * unreadable/uncorrelatable message, or one whose bytes the decoder discarded). The
1745
+ * {@link NackReason} distinguishes the causes.
1746
+ *
1747
+ * **`'error'` contract:** underlying `net.Server`/`tls.Server` errors are forwarded to the
1748
+ * `'error'` event whenever a listener is attached. With **no** listener, the outcome depends on
1749
+ * server state: during a `listen()` (the bind window) the error **rejects the `listen()` promise**
1750
+ * — the primary error surface — and the process never crashes on a bind error; with no `listen()`
1751
+ * in flight and the server not serving (e.g. a stale async error after `close()`) the error is
1752
+ * dropped; but **while serving**, an unlistened runtime error (e.g. accept-loop `EMFILE`) keeps
1753
+ * Node's fail-loud crash-on-unlistened-`'error'` convention — a silent accept outage is
1754
+ * impossible. Caveat for `'error'` listeners: the forwarder runs **before** the internal
1755
+ * `listen()` rejection handler, so an `'error'` listener that synchronously calls `close()`
1756
+ * during the bind window changes the `listen()` rejection from the bind error to the typed
1757
+ * close-during-listen `MllpConnectionError` — match on the `'error'` event payload, not the
1758
+ * rejection, if you do that.
1759
+ * Phase 8 (TLS/MLLPS) adds two more: `'tlsClientError'` (a failed TLS handshake — the
1760
+ * server logs it and keeps serving other connections) and `'securityWarning'` (loud,
1761
+ * one-time notice when a wildcard host is bound via `allowWildcardBind: true`).
1762
+ * All event payloads are `Object.freeze()`'d before emission (SERVER-10).
1763
+ *
1764
+ * @example
1765
+ * ```typescript
1766
+ * import { createServer } from '@cosyte/mllp';
1767
+ *
1768
+ * const server = createServer({
1769
+ * onMessage: (payload, meta, conn) => {
1770
+ * console.log('received from', meta.connectionId);
1771
+ * },
1772
+ * });
1773
+ * await server.listen(2575);
1774
+ * // await using server = createServer({ ... }); // Symbol.asyncDispose
1775
+ * ```
1776
+ */
1777
+ declare class MllpServer extends EventEmitter {
1778
+ private readonly _netServer;
1779
+ private readonly _connections;
1780
+ private readonly _opts;
1781
+ /** `true` when `ServerOptions.tls` is set — `_netServer` is actually a `tls.Server`. */
1782
+ private readonly _isTls;
1783
+ private _listening;
1784
+ /** Phase 8 — single-flight guard: `true` while a `listen()` call is settling. */
1785
+ private _listenInFlight;
1786
+ /**
1787
+ * Phase 8 — settle hook for an in-flight `listen()`. Non-null only while a
1788
+ * `listen()` is in flight; invoked by `close()` so a close racing startup
1789
+ * rejects the pending `listen()` (typed) instead of leaving it hung with
1790
+ * the single-flight guard stuck. Cleared on every settle path.
1791
+ */
1792
+ private _pendingListenSettle;
1793
+ private _port;
1794
+ private _host;
1795
+ private _acceptedTotal;
1796
+ private _closedTotal;
1797
+ /** Phase 8 — count of `'tlsClientError'` events since listen(). */
1798
+ private _tlsClientErrorsTotal;
1799
+ /**
1800
+ * Construct an MLLP server. Created idle; call `listen()` (or use
1801
+ * {@link createServer}/{@link createStarterServer}) to begin accepting connections.
1802
+ *
1803
+ * @param opts - Server options (bind host/port, auto-ACK policy, message handler, framing, …).
1804
+ */
1805
+ constructor(opts: ServerOptions);
1806
+ /**
1807
+ * Start listening on the given port.
1808
+ *
1809
+ * Resolves once the TCP socket is bound and emits `'listening'` with
1810
+ * `Object.freeze({ port: actualPort, host: actualHost })`.
1811
+ *
1812
+ * **Single-flight:** one `listen()` per server lifecycle at a time. A call
1813
+ * while the server is already listening — or while another `listen()` is
1814
+ * still in flight — rejects with a typed `MllpConnectionError` (concurrent
1815
+ * binds raced each other's post-bind safety checks). Call `close()` before
1816
+ * re-listening; sequential `listen()` → `close()` → `listen()` is fine.
1817
+ *
1818
+ * **`close()` during an in-flight `listen()`** rejects that `listen()` with
1819
+ * a typed `MllpConnectionError` (never a hang) and clears the single-flight
1820
+ * guard — a subsequent `listen()` on the same server works. This makes
1821
+ * `Symbol.asyncDispose` (which delegates to `close()`) safe even before a
1822
+ * `listen()` has settled. (Exception: `close({ signal })` with an
1823
+ * already-aborted signal is a no-op AbortError rejection — the in-flight
1824
+ * `listen()` is left to settle on its own bind outcome; see `close()`.)
1825
+ *
1826
+ * @param port - TCP port to bind. Use `0` to let the OS assign an ephemeral port.
1827
+ * @param hostOrOpts - Host string or object with `host` and optional `signal`.
1828
+ * Default host: `'127.0.0.1'` (Phase 8 bind-safety hardening). Wildcard hosts
1829
+ * are rejected unless `ServerOptions.allowWildcardBind: true` — literal
1830
+ * spellings pre-bind, resolver-only shorthands post-bind via the
1831
+ * OS-normalized bound address.
1832
+ *
1833
+ * @example
1834
+ * ```typescript
1835
+ * await server.listen(2575); // binds 127.0.0.1
1836
+ * await server.listen(0, '127.0.0.1');
1837
+ * await server.listen(0, { host: '127.0.0.1', signal: ac.signal });
1838
+ * ```
1839
+ */
1840
+ listen(port: number, hostOrOpts?: string | {
1841
+ host?: string;
1842
+ signal?: AbortSignal;
1843
+ }): Promise<void>;
1844
+ /**
1845
+ * Stop accepting new connections and gracefully close all active connections.
1846
+ *
1847
+ * Sequence (D-06):
1848
+ * 1. `net.Server.close()` — stops accepting new connections immediately
1849
+ * 2. If `_connections` is empty: resolves immediately (no drain needed)
1850
+ * 3. Calls `_drainAll(drainTimeoutMs)` — Promise.all + side-effect setTimeout
1851
+ * that force-destroys stragglers after the drain window
1852
+ *
1853
+ * Calling `close()` while a `listen()` is still in flight proactively
1854
+ * settles that `listen()` with a typed `MllpConnectionError` rejection
1855
+ * and clears the single-flight guard — the server is immediately
1856
+ * re-listenable. `Symbol.asyncDispose` (which delegates here) is
1857
+ * therefore safe before `listen()` settles. **Qualification:** a
1858
+ * `close({ signal })` whose signal is **already aborted** rejects
1859
+ * immediately with `AbortError` and performs no work — it does NOT close
1860
+ * the server and does NOT settle the in-flight `listen()`, which simply
1861
+ * continues and settles on its own bind outcome.
1862
+ *
1863
+ * @param opts.drainTimeoutMs - Override drain timeout (default: `opts.drainTimeoutMs ?? 30_000`).
1864
+ * @param opts.signal - AbortSignal to cancel the close operation. On abort, all
1865
+ * active connections are destroyed and the promise rejects with AbortError.
1866
+ *
1867
+ * @example
1868
+ * ```typescript
1869
+ * await server.close({ drainTimeoutMs: 5_000 });
1870
+ * ```
1871
+ */
1872
+ close(opts?: {
1873
+ drainTimeoutMs?: number;
1874
+ signal?: AbortSignal;
1875
+ }): Promise<void>;
1876
+ /**
1877
+ * Drain all active connections with a shared timeout.
1878
+ *
1879
+ * Uses Promise.all (not Promise.race) for coordination — the timeout is a
1880
+ * side effect that calls `conn.destroy()` on stragglers after `drainTimeoutMs`.
1881
+ * Promise.all resolves when all `conn.close()` promises settle (which happens
1882
+ * because `conn.destroy()` transitions connections to CLOSED).
1883
+ *
1884
+ * @param drainTimeoutMs - Maximum time to wait for connections to drain.
1885
+ */
1886
+ private _drainAll;
1887
+ /**
1888
+ * Async disposal — delegates to `close()` for `await using` support.
1889
+ *
1890
+ * @example
1891
+ * ```typescript
1892
+ * await using server = createServer({ onMessage: handler });
1893
+ * await server.listen(2575);
1894
+ * // server.close() is called automatically at end of block
1895
+ * ```
1896
+ */
1897
+ [Symbol.asyncDispose](): Promise<void>;
1898
+ /**
1899
+ * Return a JSON-serializable observability snapshot (OBS-02).
1900
+ *
1901
+ * `totalBytesIn` and `totalBytesOut` aggregate from live connections at call time.
1902
+ * Plan 04 adds full aggregation; at Plan 01 scope they return 0.
1903
+ *
1904
+ * @example
1905
+ * ```typescript
1906
+ * const stats = server.getStats();
1907
+ * logger.info(JSON.stringify(stats));
1908
+ * ```
1909
+ */
1910
+ getStats(): ServerStats;
1911
+ /**
1912
+ * Emit on `'error'` only when a listener is attached — an unlistened
1913
+ * `EventEmitter` `'error'` emission throws (see the class JSDoc error
1914
+ * contract). The single guard shared by the optional-tap forwarding sites
1915
+ * in this class; the constructor's net.Server forwarder deliberately does
1916
+ * NOT use it (its while-serving branch re-emits unguarded to keep Node's
1917
+ * fail-loud convention).
1918
+ */
1919
+ /**
1920
+ * Emit a server event, containing a throwing subscriber.
1921
+ *
1922
+ * Every event this server emits is reachable from a callback we do not own — the `net.Server`
1923
+ * `'connection'` listener, the `tls.Server` `'tlsClientError'` listener, a socket's `'data'`
1924
+ * listener (via `Connection`), or the `catch` block of a `void`-ed async ACK task. A throwing
1925
+ * subscriber would therefore unwind into that callback and kill the process (or reject a void-ed
1926
+ * promise, which does the same and additionally skips the rest of the method).
1927
+ *
1928
+ * See `src/internal/safe-emit.ts` for the full rationale. The throw is re-surfaced on `'error'`.
1929
+ */
1930
+ private _emitContained;
1931
+ private _emitErrorIfListened;
1932
+ private _onSocketAccepted;
1933
+ /**
1934
+ * Extract a minimal, content-free peer-certificate summary (Phase 8, ATNA
1935
+ * ITI-19 mutual authentication) — CN strings, expiry, and the verification
1936
+ * outcome (`authorized`) only; never the full certificate object. Returns
1937
+ * `null` when no peer certificate was presented (e.g. `clientAuth: 'WANT'`
1938
+ * with no client cert).
1939
+ */
1940
+ private _extractPeerCertificateSummary;
1941
+ /**
1942
+ * The fail-safe commit path for `autoAck: 'AA'` (HL7 v2.5.1 §2.9.2).
1943
+ *
1944
+ * With an `onMessage` handler this **awaits** it (the durable-commit step) and only
1945
+ * then dispatches: resolve ⇒ `AA`; throw/reject ⇒ a negative ACK (`AE` by default,
1946
+ * `AR` via {@link MllpAckError}). A positive ACK can never precede a successful commit.
1947
+ *
1948
+ * Without an `onMessage` handler, `'AA'` degrades to a **transport-accept** — `AA`
1949
+ * meaning "bytes received and framed", not "application-processed".
1950
+ *
1951
+ * A handler failure is **expected flow**, not a server error: it produces a negative
1952
+ * ACK and a `'nack'` event. The thrown error's message is never placed on the wire or
1953
+ * in the event (it may carry PHI) — only the static `ackCode` and the inbound control
1954
+ * metadata reach the peer.
1955
+ */
1956
+ private _sendCommitAck;
1957
+ /**
1958
+ * Resolve the auto-ACK code for a message the server would otherwise acknowledge
1959
+ * **positively** (`AA`) — the no-handler transport-accept and the post-commit success
1960
+ * paths. Fail-safe: downgrade to `AE` and emit a PHI-safe `'nack'` whenever the message
1961
+ * cannot carry a correlatable positive ACK, so `AA` never names a control ID the sender
1962
+ * cannot match (→ timeout → resend → **duplicate clinical message**).
1963
+ *
1964
+ * Two independent disqualifiers, one from the bytes and one from the frame:
1965
+ *
1966
+ * - **`rawAckUncorrelatable(payload)`** — no readable `MSH`, an empty MSH-10, or a
1967
+ * batch/concatenated-message shape. This is the SAME predicate `buildRawAck` enforces
1968
+ * on the wire; it is re-checked here so the downgrade is **observable** (a `'nack'`
1969
+ * event) rather than a silent code change inside the builder.
1970
+ * - **`MLLP_TRAILING_BYTES`** in the frame's warnings — a mid-payload `VT` made the
1971
+ * decoder discard accumulated bytes and deliver only the fragment after it. The
1972
+ * clinical message was destroyed in transit, so even a fragment that happens to carry
1973
+ * a readable MSH must not be positively acknowledged. This condition is invisible to
1974
+ * `buildRawAck` (it sees only the fragment), which is why it lives here. The code is
1975
+ * **reserved** for that discard (see the decoder's `_readPayload`) and is emitted while
1976
+ * accumulating the delivered frame, so it is frame-scoped: it names *this* payload's own
1977
+ * discard, never a neighbouring frame's trailing junk.
1978
+ *
1979
+ * `'discarded-bytes'` takes precedence in the reason when both hold — it is the root cause.
1980
+ */
1981
+ private _resolveAutoAckPositive;
1982
+ /**
1983
+ * Run `onMessage` for its side effects only (custom-ACK and manual modes), guarding
1984
+ * against a synchronous throw or a rejected promise. In these modes the handler does
1985
+ * **not** gate the ACK, so a failure is re-emitted as a PHI-safe `'error'` on the
1986
+ * connection (D-04: a handler error never crashes the server) rather than escaping as
1987
+ * an unhandled rejection. The error text is never placed on the wire.
1988
+ */
1989
+ private _observeMessage;
1990
+ /**
1991
+ * The custom-builder path for `autoAck: fn`. Awaits the builder and dispatches its
1992
+ * Buffer as the ACK; the caller owns MSA-1. A builder error is re-emitted as `'error'`
1993
+ * on the connection — the server never crashes (D-04).
1994
+ */
1995
+ private _sendCustomAck;
1996
+ /**
1997
+ * Frame an ACK payload and write it to the connection. `Connection.send()` returns
1998
+ * `false` under backpressure (socket write buffer full); that drops the ACK and emits
1999
+ * a `MllpConnectionError({ phase: 'send' })` on the connection (D-04) — the peer will
2000
+ * time out waiting and may retry. The server never crashes.
2001
+ */
2002
+ private _dispatchAck;
2003
+ }
2004
+ /**
2005
+ * Factory function — creates a new `MllpServer` with the supplied options.
2006
+ *
2007
+ * Prefer `createServer()` over `new MllpServer()` for forward-compatible construction.
2008
+ *
2009
+ * @example
2010
+ * ```typescript
2011
+ * import { createServer } from '@cosyte/mllp';
2012
+ *
2013
+ * const server = createServer({
2014
+ * onMessage: (payload, meta, conn) => {
2015
+ * console.log('received', payload.length, 'bytes');
2016
+ * },
2017
+ * });
2018
+ * await server.listen(2575);
2019
+ * ```
2020
+ */
2021
+ declare function createServer(opts?: ServerOptions): MllpServer;
2022
+ /**
2023
+ * Starter factory — creates, configures, and starts an `MllpServer` in one call.
2024
+ *
2025
+ * Provides the "three lines of code" north-star experience with sensible defaults:
2026
+ * `autoAck: 'AA'`, `drainTimeoutMs: 30_000`, `Symbol.asyncDispose` wired.
2027
+ *
2028
+ * NOTE: Stub at Plan 01 scope — Plan 04 fills in the full implementation.
2029
+ *
2030
+ * @example
2031
+ * ```typescript
2032
+ * import { createStarterServer } from '@cosyte/mllp';
2033
+ *
2034
+ * const server = await createStarterServer({
2035
+ * port: 2575,
2036
+ * onMessage: (payload) => buildAck(payload),
2037
+ * });
2038
+ * ```
2039
+ */
2040
+ declare function createStarterServer(opts: StarterServerOptions): Promise<MllpServer>;
2041
+
2042
+ /**
2043
+ * MLLP Client — `createClient()` factory and `MllpClient` class.
2044
+ *
2045
+ * Provides the client-side MLLP transport: connect to an MLLP server over TCP,
2046
+ * encode and send framed messages, decode inbound ACKs, and surface lifecycle
2047
+ * events with frozen payloads. Supports `AbortSignal` cancellation on every
2048
+ * awaitable and `Symbol.asyncDispose` for `await using` ergonomics.
2049
+ *
2050
+ * Phase 5 PLAN-01 shipped the lifecycle scaffolding — `connect()`, `close()`,
2051
+ * `destroy()`, event re-emission. PLAN-02 added `send()` + `MllpTimeoutError`.
2052
+ * The `correlateByControlId` option (MSH-10 → MSA-2 ACK matching) lights up
2053
+ * out-of-order ACK handling. Subsequent plans add auto-reconnect (Plan 04),
2054
+ * backpressure (Plan 05), and `createStarterClient` + `getStats()` (PLAN-06).
2055
+ *
2056
+ * @example
2057
+ * ```typescript
2058
+ * import { createClient } from '@cosyte/mllp';
2059
+ *
2060
+ * const client = createClient({ host: 'localhost', port: 2575 });
2061
+ * await client.connect();
2062
+ * // PLAN-02 will add: const ack = await client.send(payloadBuffer);
2063
+ * await client.close();
2064
+ * ```
2065
+ *
2066
+ * @packageDocumentation
2067
+ */
2068
+
2069
+ /**
2070
+ * Context passed to a custom `retryStrategy` hook on each reconnect attempt
2071
+ * (CLIENT-12, D-15).
2072
+ *
2073
+ * Frozen via `Object.freeze` before invocation — handlers cannot mutate
2074
+ * (T-05-04-04 mitigation).
2075
+ *
2076
+ * @example
2077
+ * ```typescript
2078
+ * const retryStrategy: RetryStrategy = (ctx) => {
2079
+ * if (ctx.attempt >= 5) return null;
2080
+ * if (ctx.classifiedAs === 'permanent') return null;
2081
+ * return Math.min(30_000, 1000 * (ctx.attempt + 1));
2082
+ * };
2083
+ * ```
2084
+ */
2085
+ interface RetryContext {
2086
+ /** 0-indexed attempt counter for the current reconnect cycle. */
2087
+ readonly attempt: number;
2088
+ /** The error that triggered the disconnect. */
2089
+ readonly lastError: Error;
2090
+ /** Delay used for the previous attempt (ms). 0 on the first attempt. */
2091
+ readonly lastDelayMs: number;
2092
+ /** Total wall-clock ms elapsed since the disconnect that started this cycle. */
2093
+ readonly totalElapsedMs: number;
2094
+ /** Ms since the last successful ACK. `Infinity` if no success seen. */
2095
+ readonly sinceLastSuccessMs: number;
2096
+ /** CLIENT-18 classification (Composition A — D-16). */
2097
+ readonly classifiedAs: "transient" | "permanent";
2098
+ /**
2099
+ * The same `AbortSignal` passed into `connect()`. If no signal was
2100
+ * supplied, the module-level `NEVER_ABORTING_SIGNAL` sentinel is provided
2101
+ * so handlers always have a real `AbortSignal` to inspect. (D-18, W-07)
2102
+ */
2103
+ readonly signal: AbortSignal;
2104
+ }
2105
+ /**
2106
+ * Custom reconnect-backoff hook (CLIENT-12). Return `null` to halt
2107
+ * reconnection (D-17) — the FSM transitions to `CLOSED`.
2108
+ */
2109
+ type RetryStrategy = (ctx: RetryContext) => number | null;
2110
+ /**
2111
+ * Combined count + byte-based queue cap. Stricter-of-two wins (D-23).
2112
+ *
2113
+ * - `number` — count cap only (default 64).
2114
+ * - `{ bytes }` — byte cap only.
2115
+ * - `{ count, bytes }` — both caps; whichever trips first wins.
2116
+ *
2117
+ * @example
2118
+ * ```typescript
2119
+ * const opts: ClientOptions = {
2120
+ * host: 'localhost', port: 2575,
2121
+ * highWaterMark: { count: 100, bytes: 1_000_000 },
2122
+ * };
2123
+ * ```
2124
+ */
2125
+ type HighWaterMark = number | {
2126
+ readonly count?: number;
2127
+ readonly bytes?: number;
2128
+ };
2129
+ /**
2130
+ * Options for {@link createClient} and the {@link MllpClient} constructor.
2131
+ *
2132
+ * Phase 5 plans extend this incrementally — new fields are additive and optional.
2133
+ *
2134
+ * @example
2135
+ * ```typescript
2136
+ * const opts: ClientOptions = { host: 'localhost', port: 2575, drainTimeoutMs: 10_000 };
2137
+ * ```
2138
+ */
2139
+ interface ClientOptions {
2140
+ /** Host to connect to (e.g. `'localhost'` or `'mllp.example.com'`). */
2141
+ readonly host: string;
2142
+ /** TCP port. */
2143
+ readonly port: number;
2144
+ /** FrameReader tolerance / size options. `onFrame` and `onWarning` are managed internally. */
2145
+ readonly framing?: Omit<FrameReaderOptions, "onFrame" | "onWarning">;
2146
+ /** Drain timeout for {@link MllpClient.close} (default: `30_000` ms). */
2147
+ readonly drainTimeoutMs?: number;
2148
+ /**
2149
+ * Per-message ACK timeout in milliseconds (CLIENT-04). The clock starts at
2150
+ * the underlying `write()` flush callback, NOT at the `send()` call —
2151
+ * pre-flush queue time is not charged to the peer. Default: `30_000`.
2152
+ */
2153
+ readonly ackTimeoutMs?: number;
2154
+ /**
2155
+ * If `true`, ACKs are matched against outgoing sends by MSH-10 → MSA-2
2156
+ * (CLIENT-03 controlId branch). Default `false` (FIFO mode).
2157
+ *
2158
+ * Out-of-order ACKs from the peer are supported in this mode. MSH-10 is
2159
+ * extracted from the outbound payload before send; MSA-2 is extracted from
2160
+ * the inbound ACK payload. An ACK whose MSA-2 matches no pending send
2161
+ * (and is not in the late-ACK graveyard) emits a frozen
2162
+ * `MllpFramingError('MLLP_ACK_UNMATCHED_CONTROL_ID')` to the `'error'` event
2163
+ * (CLIENT-15). A late ACK whose MSA-2 matches a graveyard entry emits a
2164
+ * `MLLP_ACK_AFTER_TIMEOUT` warning (CLIENT-16) and is dropped.
2165
+ *
2166
+ * @default false
2167
+ */
2168
+ readonly correlateByControlId?: boolean;
2169
+ /**
2170
+ * Auto-reconnect on transient disconnect (CLIENT-05). Default `false`.
2171
+ *
2172
+ * When `true`, dropped connections caused by transient errors (per
2173
+ * {@link isTransientConnectionError}) trigger the FSM cycle
2174
+ * `CONNECTED → DISCONNECTED → RECONNECTING → CONNECTING → CONNECTED`
2175
+ * with exponential backoff per D-19 unless overridden by
2176
+ * {@link ClientOptions.retryStrategy}. Permanent errors halt and
2177
+ * transition directly to `CLOSED` (Composition A — D-16).
2178
+ */
2179
+ readonly autoReconnect?: boolean;
2180
+ /**
2181
+ * Custom reconnect-backoff hook (CLIENT-12, D-15). Return `null` to halt
2182
+ * reconnection (D-17). Receives a frozen {@link RetryContext}. Defaults
2183
+ * to the exponential strategy described in D-19.
2184
+ */
2185
+ readonly retryStrategy?: RetryStrategy;
2186
+ /** First delay (ms) on auto-reconnect; default 100. (CLIENT-05, D-19) */
2187
+ readonly initialDelayMs?: number;
2188
+ /** Maximum backoff cap (ms); default 30_000. (CLIENT-05, D-19) */
2189
+ readonly maxDelayMs?: number;
2190
+ /** Backoff multiplier; default 2. (CLIENT-05, D-19) */
2191
+ readonly multiplier?: number;
2192
+ /** Jitter fraction, e.g. 0.2 = ±20%; default 0.2. (CLIENT-05, D-19) */
2193
+ readonly jitter?: number;
2194
+ /**
2195
+ * Application-level high-water mark on the in-flight + queued send set
2196
+ * (CLIENT-07, D-23). `number` configures a count cap (default 64);
2197
+ * `{ bytes }` configures a byte cap; `{ count, bytes }` configures
2198
+ * both, with the stricter-of-two trigger winning.
2199
+ *
2200
+ * When the cap is exceeded, behavior is governed by
2201
+ * {@link ClientOptions.onBackpressure}.
2202
+ *
2203
+ * @default 64
2204
+ */
2205
+ readonly highWaterMark?: HighWaterMark;
2206
+ /**
2207
+ * Behavior when the high-water mark is exceeded (CLIENT-07).
2208
+ *
2209
+ * - `'reject'` (default) — `send()` rejects with `MllpBackpressureError`.
2210
+ * - `'wait'` — `send()` awaits the `'drain'` event OR the per-message
2211
+ * `ackTimeoutMs` OR `signal` abort, whichever fires first (CLIENT-11).
2212
+ *
2213
+ * @default 'reject'
2214
+ */
2215
+ readonly onBackpressure?: "reject" | "wait";
2216
+ /**
2217
+ * Strict serialization send → await-ACK → send (CLIENT-19, D-06).
2218
+ *
2219
+ * - `true` (default) — concurrent in-flight sends up to
2220
+ * {@link ClientOptions.highWaterMark}.
2221
+ * - `false` — collapses the in-flight set to ≤1 (the unified Correlator's
2222
+ * `maxInFlight=1`); the next `send()` waits for the prior ACK before
2223
+ * reaching the wire.
2224
+ *
2225
+ * @default true
2226
+ */
2227
+ readonly pipeline?: boolean;
2228
+ /**
2229
+ * TCP keepalive interval (ms). Sets `socket.setKeepAlive(true, ms)` on
2230
+ * the underlying `net.Socket` BEFORE wrapping in `NetTransport`. OS-level
2231
+ * half-open detection (network partitions, NAT-table eviction). Independent
2232
+ * of {@link ClientOptions.deadPeerTimeoutMs} (CLIENT-08, D-11/A3).
2233
+ *
2234
+ * @default undefined (off)
2235
+ */
2236
+ readonly keepaliveIntervalMs?: number;
2237
+ /**
2238
+ * Application-idle timeout (ms) keyed on last inbound bytes / ACK / warning
2239
+ * (CLIENT-08, D-11). On trip, calls `connection.destroy(new Error('dead
2240
+ * peer timeout'))` which surfaces as `MllpConnectionError({ phase: 'receive' })`.
2241
+ * Trip honors {@link ClientOptions.autoReconnect} (D-13). Independent of
2242
+ * {@link ClientOptions.keepaliveIntervalMs} (D-11/A3).
2243
+ *
2244
+ * @default undefined (off)
2245
+ */
2246
+ readonly deadPeerTimeoutMs?: number;
2247
+ /**
2248
+ * Enable TLS (MLLPS) for this connection (Phase 8). `true` enables TLS with
2249
+ * all defaults — including certificate verification **on**. Pass a
2250
+ * {@link TlsOptions} object to customize (`ca`/`cert`/`key`, minimum
2251
+ * version, ciphers, `allowUnverified`, …).
2252
+ *
2253
+ * Spec anchor: IHE ATNA ITI-19 (https://profiles.ihe.net/ITI/TF/Volume2/ITI-19.html).
2254
+ *
2255
+ * @default undefined (plaintext TCP)
2256
+ */
2257
+ readonly tls?: TlsOptions | true;
2258
+ }
2259
+ /**
2260
+ * Observability snapshot returned by {@link MllpClient.getStats} (OBS-01, D-26).
2261
+ *
2262
+ * All fields are JSON-serializable (OBS-04) — no Buffers, no class instances,
2263
+ * no Maps, no circular references. `lastConnectedAt` and `lastAckAt` are
2264
+ * **epoch milliseconds** (numbers), NOT `Date` instances — log-pipeline
2265
+ * friendly per D-26.
2266
+ *
2267
+ * `warningsByCode` keys are constrained to the public {@link WarningCode}
2268
+ * union — adding/removing a code is a breaking change (CLAUDE.md
2269
+ * stable-codes guardrail enforced at the type boundary, B-05).
2270
+ *
2271
+ * @example
2272
+ * ```typescript
2273
+ * const stats = client.getStats();
2274
+ * logger.info(JSON.stringify(stats));
2275
+ * // {"state":"CONNECTED","connectionId":"…","queueDepth":0, … }
2276
+ * ```
2277
+ */
2278
+ interface ClientStats {
2279
+ /** Current FSM state — mirrors `client.state`. */
2280
+ readonly state: ConnectionState;
2281
+ /** Live Connection's id, or `null` before the first connect (or post-CLOSED). */
2282
+ readonly connectionId: string | null;
2283
+ /** Total live correlator entries (in-flight + pre-flush + serialization-queued). */
2284
+ readonly queueDepth: number;
2285
+ /** Sum of `frame.length` across live correlator entries. */
2286
+ readonly queueBytes: number;
2287
+ /** Entries with `sentAt !== null` — actually written to the wire / awaiting ACK. */
2288
+ readonly inFlight: number;
2289
+ /**
2290
+ * Aggregated warning counts. Keys are constrained to the public
2291
+ * {@link WarningCode} union (B-05). Connection-level warnings + Correlator
2292
+ * `MLLP_ACK_*` warnings are merged.
2293
+ */
2294
+ readonly warningsByCode: Partial<Record<WarningCode, number>>;
2295
+ /** Bytes received from the peer (current Connection). */
2296
+ readonly totalBytesIn: number;
2297
+ /** Bytes written to the peer (current Connection). */
2298
+ readonly totalBytesOut: number;
2299
+ /** Total successful `connection.send()` calls since construction. */
2300
+ readonly sentTotal: number;
2301
+ /** Total ACKs matched + resolved since construction. */
2302
+ readonly ackedTotal: number;
2303
+ /** Total ACK timeouts since construction. */
2304
+ readonly timedOutTotal: number;
2305
+ /** Total reconnect attempts since construction (W-02). */
2306
+ readonly reconnectAttempts: number;
2307
+ /** Epoch ms of the last `CONNECTED` transition. `null` until first connect. */
2308
+ readonly lastConnectedAt: number | null;
2309
+ /** Epoch ms of the most recent successful ACK. `null` until first ACK. */
2310
+ readonly lastAckAt: number | null;
2311
+ /** Whether this client is configured for TLS (Phase 8). Mirrors `ClientOptions.tls` being set. */
2312
+ readonly tls: boolean;
2313
+ }
2314
+ /**
2315
+ * MLLP client — composes a single Phase 3 {@link Connection} over a {@link NetTransport}
2316
+ * (production) or any other `Transport` (testing via {@link InMemoryTransport}).
2317
+ *
2318
+ * Public events — every payload `Object.freeze`'d before emission (D-25):
2319
+ * - `'stateChange'` — `{ from, to, reason? }` from the underlying Connection FSM
2320
+ * - `'connect'` — `{ connectionId }` once the FSM enters `CONNECTED`
2321
+ * - `'disconnect'` — `{ connectionId }` once the FSM enters `DISCONNECTED`
2322
+ * - `'reconnecting'` — `{ connectionId, attempt?, delayMs? }` (Plan 04 populates)
2323
+ * - `'close'` — `{ connectionId }` once the FSM enters terminal `CLOSED`
2324
+ * - `'message'` — `{ payload, connectionId, byteOffset, warnings }` for every inbound frame
2325
+ * - `'warning'` — `MllpWarning` enriched with `connectionId` from the Connection layer
2326
+ * - `'securityWarning'` — `SecurityWarning` (Phase 8). Emitted on every successful
2327
+ * `secureConnect` (initial + every reconnect) when `tls.allowUnverified` is `true`
2328
+ * — code `MLLP_TLS_VERIFY_DISABLED`. Also mirrored to `process.emitWarning`.
2329
+ * - `'error'` — re-emitted from Connection. Guarded by `listenerCount('error') > 0` so
2330
+ * absence of a listener does NOT crash the process (server precedent).
2331
+ *
2332
+ * @example
2333
+ * ```typescript
2334
+ * const client = createClient({ host: 'localhost', port: 2575 });
2335
+ * client.on('stateChange', ({ from, to }) => logger.info({ from, to }));
2336
+ * client.on('message', ({ payload }) => logger.info({ bytes: payload.length }));
2337
+ * await client.connect();
2338
+ * // PLAN-02 will add: const ack = await client.send(payloadBuffer);
2339
+ * await client.close();
2340
+ * ```
2341
+ */
2342
+ declare class MllpClient extends EventEmitter {
2343
+ private readonly _opts;
2344
+ private _connection;
2345
+ private _socket;
2346
+ /**
2347
+ * Initial state for `get state()` before `_connection` exists. Once a Connection
2348
+ * is attached, `state` mirrors `_connection.state`.
2349
+ */
2350
+ private _state;
2351
+ /** Per-message ACK timeout in ms (CLIENT-04). Resolved at construction. */
2352
+ private readonly _ackTimeoutMs;
2353
+ /** controlId-mode flag (CLIENT-03 branch). `false` → FIFO. */
2354
+ private readonly _correlateByControlId;
2355
+ /** Unified ACK correlator (D-03/A1). Built during `_attachConnection`. */
2356
+ private _correlator;
2357
+ /**
2358
+ * Periodic ACK-timeout sweep timer. Drives `_correlator.expireDue()` because
2359
+ * the Correlator is timer-free per D-03. Cleared on close/destroy.
2360
+ */
2361
+ private _ackSweepTimer;
2362
+ private readonly _autoReconnect;
2363
+ private readonly _initialDelayMs;
2364
+ private readonly _maxDelayMs;
2365
+ private readonly _multiplier;
2366
+ private readonly _jitter;
2367
+ private readonly _retryStrategy;
2368
+ /** 0-indexed attempt counter for the current reconnect cycle. */
2369
+ private _attempt;
2370
+ /**
2371
+ * W-02 — total reconnect attempts since construction. Read by PLAN-06 for
2372
+ * `getStats().reconnectAttempts`. Incremented at the entry of every
2373
+ * `_handleDisconnect` invocation that proceeds to schedule a backoff.
2374
+ */
2375
+ private _reconnectAttempts;
2376
+ /** Epoch ms of the last successful ACK. Drives W-01 backoff-reset. */
2377
+ private _lastSuccessAt;
2378
+ /** Epoch ms when the current reconnect cycle began. `null` outside a cycle. */
2379
+ private _reconnectCycleStartedAt;
2380
+ /** Active backoff `setTimeout` handle. `null` when no backoff is armed. */
2381
+ private _backoffTimer;
2382
+ /** Last delay (ms) used by the strategy. Surfaced via RetryContext.lastDelayMs. */
2383
+ private _lastDelayMs;
2384
+ /** Count cap; `Number.POSITIVE_INFINITY` when only bytes configured. */
2385
+ private readonly _hwmCount;
2386
+ /** Byte cap; `Number.POSITIVE_INFINITY` when only count configured. */
2387
+ private readonly _hwmBytes;
2388
+ /** Backpressure policy; default `'reject'`. */
2389
+ private readonly _onBackpressure;
2390
+ /** Pipeline flag; default `true` (parallel up to highWaterMark). */
2391
+ private readonly _pipeline;
2392
+ /** Dead-peer idle timer (Plan 05 — D-11). `null` when not armed. */
2393
+ private _deadPeerTimer;
2394
+ /** True once the self-'ack' listener that resets the dead-peer timer
2395
+ * has been attached. Guards against duplicate listeners on reconnect. */
2396
+ private _ackResetWired;
2397
+ /** Most recently bound `connect()` signal. Reread on every RetryContext build (W-07). */
2398
+ private _connectSignal;
2399
+ /** Set when close()/destroy()/abort fires; reconnect handler short-circuits. */
2400
+ private _userClosed;
2401
+ /** Captured Connection error feeding `RetryContext.lastError`. */
2402
+ private _lastError;
2403
+ /** Listener-removal handle for the abort listener bound in connect(). */
2404
+ private _abortListener;
2405
+ /**
2406
+ * Test-only seam — when set, `_beginReconnectAttempt` builds the new
2407
+ * Connection through this factory instead of opening a real net.Socket.
2408
+ *
2409
+ * @internal
2410
+ */
2411
+ private _reconnectFactory;
2412
+ /** Total successful `conn.send()` flushes since construction. */
2413
+ private _sentTotal;
2414
+ /** Total ACKs resolved since construction. */
2415
+ private _ackedTotal;
2416
+ /** Total ACK timeouts since construction. */
2417
+ private _timedOutTotal;
2418
+ /** Epoch ms of the most recent CONNECTED transition (null until first). */
2419
+ private _lastConnectedAt;
2420
+ /** Epoch ms of the most recent successful ACK (null until first). */
2421
+ private _lastAckAt;
2422
+ /**
2423
+ * Aggregated Correlator-emitted warning counts (MLLP_ACK_*). Connection-level
2424
+ * warnings are read directly from `_connection.getStats().warningsByCode`
2425
+ * at observation time and merged into the snapshot (D-26).
2426
+ */
2427
+ private _aggregatedWarningsByCode;
2428
+ /**
2429
+ * Construct an MLLP client. Created idle; call `connect()` (or use
2430
+ * {@link createClient}/{@link createStarterClient}) to open the connection.
2431
+ *
2432
+ * @param opts - Client options (host/port, ACK timeout, reconnect/backpressure policy, …).
2433
+ */
2434
+ constructor(opts: ClientOptions);
2435
+ /**
2436
+ * Current FSM state. Mirrors the underlying Connection's state once attached;
2437
+ * before `connect()` (or after a `CLOSED` Connection is dropped) reports
2438
+ * the client-level baseline (`'DISCONNECTED'`).
2439
+ */
2440
+ get state(): ConnectionState;
2441
+ /**
2442
+ * Open a TCP (or TLS — Phase 8) connection to the configured `host:port`
2443
+ * and attach a Phase 3 {@link Connection} to it. Resolves once the FSM
2444
+ * enters `CONNECTED` — for TLS, on `'secureConnect'` (handshake complete,
2445
+ * including certificate verification when it is on).
2446
+ *
2447
+ * Rejects with:
2448
+ * - `DOMException('Aborted', 'AbortError')` if `signal` is provided and aborts
2449
+ * before the connect resolves.
2450
+ * - `MllpConnectionError({ phase: 'connect' })` if the underlying socket emits
2451
+ * `error` before connecting, OR if the client is already connecting/connected.
2452
+ * TLS failures carry a `connectionCause`: `'tls-verify'` for certificate
2453
+ * verification failures, `'tls-handshake'` for TLS-protocol-shaped failures
2454
+ * ({@link isTlsProtocolError}); pure TCP failures carry none.
2455
+ *
2456
+ * **Dual failure signal on initial connect:** the Connection's transport
2457
+ * error handler is attached before this promise's own error listener, so a
2458
+ * pre-connect socket error produces BOTH the promise rejection AND a
2459
+ * client `'error'` event (when an `'error'` listener is attached). Handle
2460
+ * whichever fits your flow; they describe the same underlying failure.
2461
+ *
2462
+ * **TLS 1.3 + mutual TLS caveat (RFC 8446 §4.4.2):** `connect()` resolving
2463
+ * does NOT guarantee that a `clientAuth: 'MUST'` server accepted your
2464
+ * client certificate. Under TLS 1.3 the client's handshake — and its
2465
+ * `'secureConnect'` — can complete before the server finishes validating
2466
+ * the certificate; a rejection then surfaces moments later as a typed
2467
+ * post-connect error (`'error'` event with an `ERR_SSL_*`/alert cause,
2468
+ * classified **permanent** — no auto-reconnect loop). ACK correlation via
2469
+ * {@link MllpClient.send} remains the delivery guarantee: no send resolves
2470
+ * without its ACK, so a rejected session can never silently "deliver".
2471
+ *
2472
+ * @example
2473
+ * ```typescript
2474
+ * const ac = new AbortController();
2475
+ * setTimeout(() => ac.abort(), 5_000);
2476
+ * await client.connect({ signal: ac.signal });
2477
+ * ```
2478
+ */
2479
+ connect(opts?: {
2480
+ signal?: AbortSignal;
2481
+ }): Promise<void>;
2482
+ /**
2483
+ * Build the raw socket + `Transport` pair for a connect / reconnect attempt.
2484
+ *
2485
+ * Plaintext (`ClientOptions.tls` unset): a `net.Socket` wrapped in
2486
+ * `NetTransport`. TLS (Phase 8): a `tls.TLSSocket` wrapped in
2487
+ * `TlsTransport` — verification defaults **on**
2488
+ * (`rejectUnauthorized: !allowUnverified`), floor `minVersion: 'TLSv1.2'`
2489
+ * (the IHE ATNA ITI-19 BCP195 floor), `servername` defaulting to
2490
+ * `ClientOptions.host`.
2491
+ */
2492
+ private _createSocketAndTransport;
2493
+ /**
2494
+ * Wrap a connect-phase socket error as `MllpConnectionError`, classifying
2495
+ * TLS failures (Phase 8) into the additive `connectionCause`:
2496
+ *
2497
+ * - `'tls-verify'` — certificate-verification failures
2498
+ * ({@link isTlsVerificationErrorCode}).
2499
+ * - `'tls-handshake'` — TLS-**protocol**-shaped failures only
2500
+ * ({@link isTlsProtocolError}): `ERR_SSL_*`, `EPROTO`, OpenSSL
2501
+ * alert-bearing errors.
2502
+ * - **No `connectionCause`** — pure TCP-level failures (`ECONNREFUSED`,
2503
+ * `ETIMEDOUT`, …) even on a TLS-configured connection; these carry the
2504
+ * same shape as plaintext connect failures.
2505
+ */
2506
+ private _wrapConnectError;
2507
+ /**
2508
+ * Emit a client event, containing a throwing subscriber.
2509
+ *
2510
+ * Every event this client emits is reached from a callback we do not own — a socket's
2511
+ * `'connect'`/`'secureConnect'`/`'data'`/`'error'` listener, or a backoff timer. A throwing
2512
+ * subscriber would unwind into it and kill the process, and on several paths it would also skip
2513
+ * the work queued after the emit (the `resolve()` of `connect()`, the backoff scheduling).
2514
+ *
2515
+ * See `src/internal/safe-emit.ts`. The throw is re-surfaced on `'error'`.
2516
+ */
2517
+ private _emitContained;
2518
+ /**
2519
+ * Emit the per-connection insecure-TLS warning (Phase 8) when
2520
+ * `tls.allowUnverified === true` — fires on EVERY successful
2521
+ * `secureConnect`, initial connect and every reconnect. Emits both a frozen
2522
+ * `'securityWarning'` event and `process.emitWarning`. No-op for plaintext
2523
+ * connections or when verification is on.
2524
+ */
2525
+ private _emitInsecureWarningIfNeeded;
2526
+ /**
2527
+ * Wire a Connection's events through to this MllpClient. Every re-emitted
2528
+ * payload is `Object.freeze`'d before emission (D-25), even though the
2529
+ * Connection layer already freezes — defense-in-depth, harmless on
2530
+ * already-frozen objects.
2531
+ *
2532
+ * Builds the unified `Correlator` (D-03/A1) bound to this Connection and
2533
+ * arms the periodic ACK-timeout sweep. The Correlator is teardown-aware:
2534
+ * `close()` / `destroy()` clear the sweep timer and reject pending sends.
2535
+ *
2536
+ * @param conn - Connection to subscribe to.
2537
+ */
2538
+ private _attachConnection;
2539
+ /**
2540
+ * Arm (or re-arm) the dead-peer idle timer (Plan 05 — CLIENT-08, D-11).
2541
+ * No-op when `deadPeerTimeoutMs` is unset.
2542
+ */
2543
+ private _armDeadPeerTimer;
2544
+ /**
2545
+ * Clear the dead-peer idle timer (Plan 05 — D-14 timer cleanup on
2546
+ * every transition out of CONNECTED).
2547
+ */
2548
+ private _clearDeadPeerTimer;
2549
+ /**
2550
+ * Disconnect handler — Plan 04 reconnect FSM core.
2551
+ *
2552
+ * Implements:
2553
+ * - CLIENT-17 hybrid in-flight handling (D-08): controlId mode preserves
2554
+ * pending sends for resend; FIFO mode rejects in-flight with
2555
+ * `connectionCause: 'in-flight-orphan'` and queued with `'fifo-unsafe'`.
2556
+ * - CLIENT-18 classifier-first (Composition A — D-16): permanent errors
2557
+ * transition straight to CLOSED without invoking `retryStrategy`.
2558
+ * - W-01 backoff-reset on recent success: first disconnect after a
2559
+ * successful ACK on the prior session resets `_attempt` to 0.
2560
+ * - W-02 `_reconnectAttempts` counter increment.
2561
+ * - D-15 frozen RetryContext + D-17 null-return halts.
2562
+ * - D-19 default exponential strategy.
2563
+ *
2564
+ * Invoked from the SINGLE `_onStateChange` hook — no parallel listener
2565
+ * (B-04). Idempotent across same-cycle re-entry: cycle-start flag
2566
+ * coordinates first-disconnect vs subsequent-within-cycle behavior.
2567
+ */
2568
+ private _handleDisconnect;
2569
+ /**
2570
+ * Default retry strategy (D-19): `min(maxDelay, initialDelay * multiplier^attempt)`
2571
+ * with ±jitter applied.
2572
+ */
2573
+ private _defaultRetryStrategy;
2574
+ /**
2575
+ * Open a fresh Connection for the next reconnect attempt.
2576
+ *
2577
+ * In production, builds a new `net.Socket` + `NetTransport` + `Connection`.
2578
+ * In tests, the `_reconnectFactory` seam returns a pre-built Connection
2579
+ * over `InMemoryTransport.pair()`.
2580
+ *
2581
+ * On successful CONNECTED transition:
2582
+ * - controlId mode: re-transmits every live correlator entry via the
2583
+ * already-encoded `PendingAck.frame`, then `markFlushed`'s each one
2584
+ * so ACK timeouts restart from the new flush time.
2585
+ * - All modes: clears the cycle-start flag so the next disconnect
2586
+ * can re-enter `_handleDisconnect` cleanly.
2587
+ */
2588
+ private _beginReconnectAttempt;
2589
+ /**
2590
+ * Post-CONNECTED steps for a reconnect attempt:
2591
+ * - controlId mode: resend every live correlator entry's frame, then
2592
+ * re-stamp `markFlushed` so ACK timers reset from the new flush.
2593
+ * - Clear the cycle-start flag so the next disconnect re-enters cleanly.
2594
+ */
2595
+ private _afterReconnectArmed;
2596
+ /**
2597
+ * Test seam — install a factory that produces the next reconnect Connection.
2598
+ *
2599
+ * @internal
2600
+ */
2601
+ _setReconnectFactory(factory: () => {
2602
+ conn: Connection;
2603
+ arm: () => void;
2604
+ }): void;
2605
+ /**
2606
+ * Test seam — capture or rebind the connect-signal mid-flight (W-07).
2607
+ *
2608
+ * @internal
2609
+ */
2610
+ _captureConnectSignal(signal: AbortSignal): void;
2611
+ /**
2612
+ * Single source-of-truth for inbound ACK payload handling.
2613
+ *
2614
+ * - FIFO mode: passes `null` controlId so `matchAck` returns the head of
2615
+ * the live store.
2616
+ * - controlId mode: extracts MSA-2 at the `HOOK_EXTENSION_POINT: ack-payload`
2617
+ * anchor and passes it to `matchAck` for keyed lookup.
2618
+ *
2619
+ * Called from the SINGLE `'message'` listener registered in `_attachConnection`.
2620
+ * No parallel listener is registered — downstream plans extend at the named
2621
+ * anchor (B-04).
2622
+ */
2623
+ private _onAckPayload;
2624
+ /**
2625
+ * Single source-of-truth for a successfully matched ACK (live-store hit).
2626
+ *
2627
+ * PLAN-02: emit frozen 'ack' event, call matched.resolve().
2628
+ * Plan 04 extends at HOOK_EXTENSION_POINT: ack-matched to update _lastSuccessAt.
2629
+ * PLAN-06 extends at HOOK_EXTENSION_POINT: ack-matched to bump _ackedTotal and
2630
+ * set _lastAckAt.
2631
+ *
2632
+ * Called from _onAckPayload when matchAck() returns a non-null PendingAck.
2633
+ */
2634
+ private _onAckMatched;
2635
+ /**
2636
+ * Emit a frozen `'drain'` event when the queue depth and bytes fall below
2637
+ * both configured caps (Plan 05 — D-24). Called from `_onAckMatched`
2638
+ * (every successful ACK) and from the Correlator's `onTimeout` callback
2639
+ * (every expired send) — both code paths free a live-store slot.
2640
+ */
2641
+ private _maybeEmitDrain;
2642
+ /**
2643
+ * Single source-of-truth for Connection FSM transitions.
2644
+ *
2645
+ * PLAN-02: re-emit frozen 'stateChange' (was inline in PLAN-01; centralized
2646
+ * here so Plan 04 / Plan 05 can extend at named anchors).
2647
+ * Plan 04 extends at HOOK_EXTENSION_POINT: state-change to detect
2648
+ * CONNECTED → DISCONNECTED|RECONNECTING and trigger _handleDisconnect.
2649
+ * Plan 05 extends at HOOK_EXTENSION_POINT: state-change to clear/arm
2650
+ * dead-peer timer on transitions out of / into CONNECTED.
2651
+ *
2652
+ * Called from the SINGLE 'stateChange' listener registered in _attachConnection.
2653
+ */
2654
+ private _onStateChange;
2655
+ /**
2656
+ * Send an MLLP-framed payload and await the inbound ACK (CLIENT-02).
2657
+ *
2658
+ * Resolves with the ACK Buffer (framing stripped). Rejects with:
2659
+ * - `DOMException('Aborted', 'AbortError')` if `signal` aborts before ACK
2660
+ * (CLIENT-11 send branch).
2661
+ * - `MllpTimeoutError` if no ACK arrives within `ackTimeoutMs` (ERR-02).
2662
+ * The clock starts at the underlying `write()` flush callback, NOT at
2663
+ * the `send()` call (CLIENT-04, D-19).
2664
+ * - `MllpConnectionError({ phase: 'send' })` if the client is not connected.
2665
+ *
2666
+ * Emits a frozen `'ack'` event on every successful match (D-25 + Specifics).
2667
+ *
2668
+ * @example
2669
+ * ```typescript
2670
+ * const ack = await client.send(payloadBuffer);
2671
+ * logger.info({ ack: ack.toString('utf8') });
2672
+ * ```
2673
+ *
2674
+ * @param payload Raw bytes; MLLP framing is added internally via `encodeFrame`.
2675
+ * @param opts.signal AbortSignal — aborting cancels the ACK wait (CLIENT-11).
2676
+ */
2677
+ send(payload: Buffer, opts?: {
2678
+ signal?: AbortSignal;
2679
+ ackTimeoutMs?: number;
2680
+ }): Promise<Buffer>;
2681
+ /**
2682
+ * 'wait'-mode backpressure handler (Plan 05 — CLIENT-07/CLIENT-11).
2683
+ *
2684
+ * Awaits one of three terminating signals, in order:
2685
+ * - `'drain'` event → re-enter `send()` (the gate will now pass).
2686
+ * - `ackTimeoutMs` elapses → reject with `MllpTimeoutError`.
2687
+ * - Caller's `signal` aborts → reject with `AbortError` (B-06). Cleanup
2688
+ * removes the drain listener AND the abort listener AND clears the
2689
+ * timer to prevent leaks.
2690
+ */
2691
+ private _waitThenSend;
2692
+ /**
2693
+ * Tear down per-connection state (sweep timer + correlator) when the
2694
+ * client is closing or destroying. Safe to call multiple times.
2695
+ */
2696
+ private _teardownCorrelator;
2697
+ /**
2698
+ * **Test seam** — attach an externally-built {@link Connection} directly,
2699
+ * bypassing the `net.createConnection` + `NetTransport` path. Used by
2700
+ * lifecycle tests driving `InMemoryTransport.pair()` for determinism.
2701
+ *
2702
+ * @internal
2703
+ */
2704
+ _attachExistingConnection(conn: Connection): void;
2705
+ /**
2706
+ * Gracefully close the client.
2707
+ *
2708
+ * Delegates to {@link Connection.close}, which transitions `CONNECTED → DRAINING
2709
+ * → DISCONNECTED` (or `CLOSED` on drain timeout). No-op if no Connection is
2710
+ * attached.
2711
+ *
2712
+ * Rejects with `DOMException('Aborted', 'AbortError')` if `signal` aborts mid-drain;
2713
+ * on abort, the underlying Connection is force-destroyed.
2714
+ *
2715
+ * @example
2716
+ * ```typescript
2717
+ * await client.close({ drainTimeoutMs: 5_000 });
2718
+ * ```
2719
+ */
2720
+ close(opts?: {
2721
+ drainTimeoutMs?: number;
2722
+ signal?: AbortSignal;
2723
+ }): Promise<void>;
2724
+ /**
2725
+ * Abruptly destroy the client — force-transitions the underlying Connection
2726
+ * to `CLOSED` immediately. No-op if no Connection is attached. Idempotent.
2727
+ *
2728
+ * @example
2729
+ * ```typescript
2730
+ * client.destroy(new Error('shutting down'));
2731
+ * ```
2732
+ */
2733
+ destroy(reason?: Error): void;
2734
+ /**
2735
+ * Returns a JSON-serializable observability snapshot (OBS-01, D-26).
2736
+ *
2737
+ * All fields are plain values — no Buffers, no class instances, no Maps,
2738
+ * no circular refs. Safe to `JSON.stringify` directly.
2739
+ *
2740
+ * `inFlight` is the count of correlator entries with `sentAt !== null`
2741
+ * (entries actually written to the wire and awaiting ACK), distinct from
2742
+ * `queueDepth` which counts ALL live correlator entries (including
2743
+ * pre-flush and serialization-queued sends).
2744
+ *
2745
+ * @example
2746
+ * ```typescript
2747
+ * setInterval(() => logger.info(JSON.stringify(client.getStats())), 60_000);
2748
+ * ```
2749
+ */
2750
+ getStats(): ClientStats;
2751
+ /**
2752
+ * Async disposal — delegates to {@link MllpClient.close} for `await using` support.
2753
+ *
2754
+ * @example
2755
+ * ```typescript
2756
+ * await using client = createClient({ host: 'localhost', port: 2575 });
2757
+ * await client.connect();
2758
+ * // client.close() is called automatically at end of block
2759
+ * ```
2760
+ */
2761
+ [Symbol.asyncDispose](): Promise<void>;
2762
+ }
2763
+ /**
2764
+ * Create an {@link MllpClient}. Equivalent to `new MllpClient(opts)`.
2765
+ *
2766
+ * @example
2767
+ * ```typescript
2768
+ * import { createClient } from '@cosyte/mllp';
2769
+ *
2770
+ * const client = createClient({ host: 'localhost', port: 2575 });
2771
+ * await client.connect();
2772
+ * // PLAN-02 will add: const ack = await client.send(payloadBuffer);
2773
+ * await client.close();
2774
+ * ```
2775
+ */
2776
+ declare function createClient(opts: ClientOptions): MllpClient;
2777
+ /**
2778
+ * Options for {@link createStarterClient} (PLAN-06, CLIENT-10).
2779
+ *
2780
+ * The starter applies opinionated D-22 defaults on top of `ClientOptions`,
2781
+ * so every override here is optional except `host` + `port`. The
2782
+ * starter-specific addition is `handleSignals` (mirrors `createStarterServer`).
2783
+ *
2784
+ * @example
2785
+ * ```typescript
2786
+ * const opts: StarterClientOptions = {
2787
+ * host: 'localhost',
2788
+ * port: 2575,
2789
+ * onMessage: (payload) => logger.info({ bytes: payload.length }),
2790
+ * handleSignals: true,
2791
+ * };
2792
+ * ```
2793
+ */
2794
+ interface StarterClientOptions {
2795
+ /** Host to connect to. */
2796
+ readonly host: string;
2797
+ /** TCP port. */
2798
+ readonly port: number;
2799
+ /**
2800
+ * Inbound-message callback (any framed payload from the peer, including
2801
+ * non-ACK messages on bidirectional channels). Mirrors the server-side
2802
+ * `onMessage` ergonomics.
2803
+ */
2804
+ readonly onMessage?: (payload: Buffer) => void;
2805
+ /** Override default `30_000`. */
2806
+ readonly ackTimeoutMs?: number;
2807
+ /** Override default `false` (FIFO mode). */
2808
+ readonly correlateByControlId?: boolean;
2809
+ /** Override default `true` (parallel up to highWaterMark). */
2810
+ readonly pipeline?: boolean;
2811
+ /** Override default `64`. */
2812
+ readonly highWaterMark?: HighWaterMark;
2813
+ /** Override default `'reject'`. */
2814
+ readonly onBackpressure?: "reject" | "wait";
2815
+ /** Override default `true` (auto-reconnect on transient errors). */
2816
+ readonly autoReconnect?: boolean;
2817
+ /** Custom reconnect-backoff hook (CLIENT-12). */
2818
+ readonly retryStrategy?: RetryStrategy;
2819
+ /** Drain timeout for `close()` (default `30_000`). */
2820
+ readonly drainTimeoutMs?: number;
2821
+ /** FrameReader options (passthrough). */
2822
+ readonly framing?: ClientOptions["framing"];
2823
+ /** TCP keepalive interval ms (CLIENT-08). */
2824
+ readonly keepaliveIntervalMs?: number;
2825
+ /** Application-idle dead-peer timeout ms (CLIENT-08). */
2826
+ readonly deadPeerTimeoutMs?: number;
2827
+ /** Enable TLS (MLLPS) for this connection (Phase 8). Passthrough to `ClientOptions.tls`. */
2828
+ readonly tls?: TlsOptions | true;
2829
+ /**
2830
+ * Register process SIGTERM/SIGINT handlers that close the client. Default
2831
+ * `false` (D-22). When `true`, SIGTERM/SIGINT both call `client.close()`
2832
+ * and exit the process. Handlers self-deregister on `'close'` (T-05-06-01).
2833
+ */
2834
+ readonly handleSignals?: boolean;
2835
+ }
2836
+ /**
2837
+ * Three-line MLLP client with batteries-included defaults (PLAN-06,
2838
+ * CLIENT-10, D-22). The returned client is already CONNECTED — `connect()`
2839
+ * has been awaited.
2840
+ *
2841
+ * D-22 defaults:
2842
+ * - `autoReconnect: true`
2843
+ * - `ackTimeoutMs: 30_000`
2844
+ * - `correlateByControlId: false` (FIFO mode — simplest mental model)
2845
+ * - `pipeline: true`
2846
+ * - `highWaterMark: 64`
2847
+ * - `onBackpressure: 'reject'`
2848
+ * - `handleSignals: false` (opt-in)
2849
+ *
2850
+ * The factory is **async**, so the literal three-line north-star snippet
2851
+ * has an explicit `await` BEFORE `createStarterClient(...)` — without it,
2852
+ * the `using` declaration would receive a `Promise`, not an `MllpClient`,
2853
+ * and `Symbol.asyncDispose` would not run at scope exit.
2854
+ *
2855
+ * @example
2856
+ * ```typescript
2857
+ * import { createStarterClient } from '@cosyte/mllp';
2858
+ * await using c = await createStarterClient({ host: 'localhost', port: 2575 });
2859
+ * const ack = await c.send(payloadBuffer);
2860
+ * ```
2861
+ */
2862
+ declare function createStarterClient(opts: StarterClientOptions): Promise<MllpClient>;
2863
+
2864
+ /**
2865
+ * MLLP Client typed errors and classifiers.
2866
+ *
2867
+ * Exports:
2868
+ * - `MllpTimeoutError` (PLAN-02) — ACK timeout (ERR-02)
2869
+ * - `MllpBackpressureError` — high-water mark exceeded (ERR-04, plan 05)
2870
+ * - `isTransientConnectionError` (PLAN-04) — transient/permanent classifier (CLIENT-18)
2871
+ *
2872
+ * Re-exported from `src/client/index.ts` and `src/index.ts`.
2873
+ *
2874
+ * @packageDocumentation
2875
+ */
2876
+ /**
2877
+ * Thrown (or rejects the `send()` promise) when an ACK does not arrive within
2878
+ * the configured `ackTimeoutMs` (ERR-02).
2879
+ *
2880
+ * The timeout clock starts at the underlying `write()` flush callback, NOT
2881
+ * at the `send()` call — pre-flush queue time is not charged to the peer
2882
+ * (CLIENT-04).
2883
+ *
2884
+ * @example
2885
+ * ```typescript
2886
+ * try {
2887
+ * await client.send(payload);
2888
+ * } catch (err) {
2889
+ * if (err instanceof MllpTimeoutError) {
2890
+ * logger.warn({ elapsedMs: err.elapsedMs, controlId: err.messageControlId });
2891
+ * }
2892
+ * }
2893
+ * ```
2894
+ */
2895
+ declare class MllpTimeoutError extends Error {
2896
+ readonly name: "MllpTimeoutError";
2897
+ /** MSH-10 control ID of the timed-out send (FIFO mode: `undefined`). */
2898
+ readonly messageControlId: string | undefined;
2899
+ /** Milliseconds elapsed between write-flush and timeout fire. */
2900
+ readonly elapsedMs: number;
2901
+ /** Epoch ms timestamp recorded at write-flush callback. */
2902
+ readonly sentAt: number;
2903
+ /**
2904
+ * Construct an MLLP timeout error.
2905
+ *
2906
+ * @param message - Human-readable error message.
2907
+ * @param opts - Timeout context (originating message control id, elapsed time, flush timestamp).
2908
+ */
2909
+ constructor(message: string, opts: {
2910
+ messageControlId: string | undefined;
2911
+ elapsedMs: number;
2912
+ sentAt: number;
2913
+ });
2914
+ }
2915
+ /**
2916
+ * Set of Node/OpenSSL error codes that indicate a **certificate-verification**
2917
+ * failure (as opposed to some other TLS handshake failure) — untrusted chain,
2918
+ * expired/not-yet-valid certificate, hostname mismatch, revocation, etc.
2919
+ *
2920
+ * Used by `MllpClient` (Phase 8) to classify a TLS connect failure's
2921
+ * `connectionCause` as `'tls-verify'` (this set) vs `'tls-handshake'`
2922
+ * (everything else observed before `'secureConnect'`). Exported so callers
2923
+ * can apply the same classification to their own error handling.
2924
+ *
2925
+ * @example
2926
+ * ```typescript
2927
+ * import { isTlsVerificationErrorCode } from '@cosyte/mllp';
2928
+ * if (isTlsVerificationErrorCode('CERT_HAS_EXPIRED')) {
2929
+ * // definitely a verification failure, not a protocol/cipher mismatch
2930
+ * }
2931
+ * ```
2932
+ */
2933
+ declare function isTlsVerificationErrorCode(code: string): boolean;
2934
+ /**
2935
+ * Detects **TLS-protocol-shaped** errors — failures of the TLS protocol
2936
+ * itself, as opposed to plain TCP-level network failures (Phase 8).
2937
+ *
2938
+ * Apply this only to errors raised on a **TLS** connection; `MllpClient`
2939
+ * does exactly that (the predicate is consulted only when `ClientOptions.tls`
2940
+ * is set). The boundary:
2941
+ *
2942
+ * **TLS-protocol-shaped (`true`):**
2943
+ * - `code` starting `ERR_SSL_` (Node's TLS alert codes, e.g.
2944
+ * `ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED` — a `clientAuth: 'MUST'`
2945
+ * server rejecting the client's certificate).
2946
+ * - `code === 'EPROTO'` — on a TLS connection this is OpenSSL failing the
2947
+ * handshake (protocol version mismatch, no shared cipher, a TLS ≤1.2
2948
+ * mTLS rejection).
2949
+ * - `message` containing `ssl` or `alert` (`/\bssl\b|\balert\b/i` — "SSL
2950
+ * routines", "tlsv13 alert certificate required", …). This message check
2951
+ * is a **heuristic backstop** over the code-based checks above, not a
2952
+ * precise boundary: it exists to catch OpenSSL errors that surface without
2953
+ * a usable `code`, and `MllpClient` consults it only on connections where
2954
+ * TLS is configured. An arbitrary non-TLS error whose message happens to
2955
+ * contain those words would also match.
2956
+ *
2957
+ * **NOT TLS-protocol-shaped (`false`) — plain network failures, which stay
2958
+ * transient for the reconnect classifier:** `ECONNREFUSED`, `ETIMEDOUT`,
2959
+ * `EHOSTUNREACH`, `ENETUNREACH`, `EPIPE`, and a plain `ECONNRESET` carrying
2960
+ * no TLS alert context — a network blip during (or after) a handshake
2961
+ * should still auto-heal.
2962
+ *
2963
+ * Certificate-**verification** failures are a separate class — see
2964
+ * {@link isTlsVerificationErrorCode}; `MllpClient` checks that first and
2965
+ * labels those `connectionCause: 'tls-verify'`.
2966
+ *
2967
+ * Why this matters: under TLS 1.3 (RFC 8446 §4.4.2) a `clientAuth: 'MUST'`
2968
+ * server can reject the client's certificate AFTER the client's own
2969
+ * `'secureConnect'` — the rejection then surfaces as a post-connect socket
2970
+ * error. A misconfigured mTLS client must never auto-reconnect-loop against
2971
+ * a server that will always reject it, so `MllpClient` classifies
2972
+ * TLS-protocol-shaped errors as **permanent**.
2973
+ *
2974
+ * @example
2975
+ * ```typescript
2976
+ * import { isTlsProtocolError, MllpConnectionError } from '@cosyte/mllp';
2977
+ * client.on('error', ({ error }) => {
2978
+ * if (error instanceof MllpConnectionError && isTlsProtocolError(error.cause)) {
2979
+ * // TLS protocol failure — a configuration problem, not a network blip.
2980
+ * }
2981
+ * });
2982
+ * ```
2983
+ */
2984
+ declare function isTlsProtocolError(err: unknown): boolean;
2985
+ /**
2986
+ * Classifies a connection error as transient (eligible for auto-reconnect)
2987
+ * or permanent (halts auto-reconnect, transitions to CLOSED).
2988
+ *
2989
+ * Used internally by `MllpClient` BEFORE invoking `retryStrategy` (Composition A
2990
+ * — see `RetryContext.classifiedAs`). Re-exported so callers can implement
2991
+ * their own retry policies.
2992
+ *
2993
+ * Classification table (CLIENT-18, D-16):
2994
+ * - `ENOTFOUND`, `EACCES` → **permanent** (`false`)
2995
+ * - `ECONNREFUSED`, `ECONNRESET`, `ETIMEDOUT`, `EHOSTUNREACH`,
2996
+ * `ENETUNREACH`, `EPIPE` → **transient** (`true`)
2997
+ * - `CERT_*` and `UNABLE_TO_VERIFY_LEAF_SIGNATURE` /
2998
+ * `DEPTH_ZERO_SELF_SIGNED_CERT` / `SELF_SIGNED_CERT_IN_CHAIN`
2999
+ * (any {@link isTlsVerificationErrorCode} code) → **permanent** (`false`) —
3000
+ * never auto-reconnect-loop into a misconfigured or MITM'd endpoint (Phase 8).
3001
+ * - `ERR_SSL_*` (Node TLS alert codes) → **permanent** (`false`) — a TLS
3002
+ * protocol failure such as a `clientAuth: 'MUST'` server rejecting the
3003
+ * client certificate recurs on every attempt (Phase 8). On TLS-configured
3004
+ * connections `MllpClient` additionally consults {@link isTlsProtocolError},
3005
+ * which also catches `EPROTO`/alert-bearing OpenSSL errors that this
3006
+ * generic classifier (which cannot know the connection was TLS) leaves
3007
+ * transient.
3008
+ * - `MLLP_*` (any {@link MllpFramingError} code — a fatal decoder throw, surfaced with
3009
+ * `connectionCause: 'framing-fatal'`) → **permanent** (`false`) (MLLP-10). The peer is not
3010
+ * speaking MLLP — an HTTP probe, a health check, a wrong-port misconfiguration — or is emitting
3011
+ * frames past `maxFrameSizeBytes`. Every reconnect meets the same bytes, so retrying is an
3012
+ * unbounded storm against a peer that is already misconfigured. If a peer's quirk is *expected*,
3013
+ * the decoder's tolerance opt-ins are the supported answer — they make it a warning, not a fatal.
3014
+ * - non-Error / unknown / no-code → **transient** (`true`) — Postel's Law
3015
+ * default. Reconnect attempts are bounded by `retryStrategy` and the
3016
+ * 30s backoff cap, so the default is safe.
3017
+ *
3018
+ * @example
3019
+ * ```typescript
3020
+ * import { isTransientConnectionError } from '@cosyte/mllp';
3021
+ * client.on('error', (err) => {
3022
+ * if (isTransientConnectionError(err)) {
3023
+ * metrics.increment('mllp.transient_error');
3024
+ * } else {
3025
+ * metrics.increment('mllp.permanent_error');
3026
+ * }
3027
+ * });
3028
+ * ```
3029
+ */
3030
+ declare function isTransientConnectionError(err: unknown): boolean;
3031
+ /**
3032
+ * Thrown (or rejects the `send()` promise) when the in-flight queue exceeds
3033
+ * the configured high-water mark and `onBackpressure: 'reject'` is set
3034
+ * (CLIENT-07, ERR-04).
3035
+ *
3036
+ * `highWaterMark` accepts a count cap, a byte cap, or both — when both are
3037
+ * present, the stricter-of-two trigger wins (D-23).
3038
+ *
3039
+ * @example
3040
+ * ```typescript
3041
+ * try {
3042
+ * await client.send(payload);
3043
+ * } catch (err) {
3044
+ * if (err instanceof MllpBackpressureError) {
3045
+ * logger.warn({
3046
+ * queueDepth: err.queueDepth,
3047
+ * queueBytes: err.queueBytes,
3048
+ * cap: err.highWaterMark,
3049
+ * });
3050
+ * }
3051
+ * }
3052
+ * ```
3053
+ */
3054
+ declare class MllpBackpressureError extends Error {
3055
+ readonly name: "MllpBackpressureError";
3056
+ /** Number of in-flight + queued sends at the moment of rejection. */
3057
+ readonly queueDepth: number;
3058
+ /** Total bytes of in-flight + queued frames at the moment of rejection. */
3059
+ readonly queueBytes: number;
3060
+ /** The high-water-mark configuration that was triggered (D-23). */
3061
+ readonly highWaterMark: {
3062
+ readonly count?: number;
3063
+ readonly bytes?: number;
3064
+ };
3065
+ /**
3066
+ * Construct an MLLP backpressure error.
3067
+ *
3068
+ * @param message - Human-readable error message.
3069
+ * @param opts - Backpressure context (queue depth, queued bytes, the high-water-mark hit).
3070
+ */
3071
+ constructor(message: string, opts: {
3072
+ queueDepth: number;
3073
+ queueBytes: number;
3074
+ highWaterMark: {
3075
+ count?: number;
3076
+ bytes?: number;
3077
+ };
3078
+ });
3079
+ }
3080
+
3081
+ /**
3082
+ * `@cosyte/mllp` — Production-grade MLLP client and server for Node.js.
3083
+ *
3084
+ * Transport-only sibling to `@cosyte/hl7`. Handles framing, ACKs, reconnects,
3085
+ * backpressure, and TLS without requiring knowledge of the MLLP spec.
3086
+ *
3087
+ * @example
3088
+ * ```typescript
3089
+ * import { createStarterServer } from '@cosyte/mllp';
3090
+ * const server = await createStarterServer({ port: 2575, onMessage: (buf) => buf });
3091
+ * ```
3092
+ *
3093
+ * @packageDocumentation
3094
+ */
3095
+ /**
3096
+ * Package version marker exported from the `@cosyte/mllp` root.
3097
+ *
3098
+ * Kept in lockstep with `package.json` by `scripts/sync-version.mjs`, which the `version` script
3099
+ * runs immediately after `changeset version`. The `: string` annotation is deliberate — without it
3100
+ * TypeScript infers the *literal* type (`declare const VERSION = "0.0.0"`), which leaks the current
3101
+ * release into consumers' types and makes an equality check against any other version a compile
3102
+ * error.
3103
+ *
3104
+ * @example
3105
+ * ```typescript
3106
+ * import { VERSION } from '@cosyte/mllp';
3107
+ * console.log(VERSION);
3108
+ * ```
3109
+ */
3110
+ declare const VERSION: string;
3111
+
3112
+ export { type AckCode, type ClientAuth, type ClientOptions, type ClientStats, Connection, type ConnectionErrorCause, type ConnectionErrorPhase, type ConnectionOptions, type ConnectionState, type ConnectionStats, type EncoderOptions, FrameReader, type FrameReaderOptions, MLLP_BIND_ALL_INTERFACES, MLLP_TLS_VERIFY_DISABLED, type MessageMeta, MllpAckError, MllpBackpressureError, MllpClient, MllpConnectionError, MllpFramingError, MllpServer, MllpTimeoutError, type MllpWarning, type NackEvent, type NackReason, type NegativeAckCode, NetTransport, type PemInput, type ReconnectingEvent, type RetryContext, type RetryStrategy, type SecurityWarning, type SecurityWarningCode, type ServerOptions, type ServerStats, type ServerTlsOptions, type StarterClientOptions, type StarterServerOptions, type StateChangeEvent, type TlsOptions, TlsTransport, Transport, VERSION, type WarningCode, buildRawAck, createClient, createServer, createStarterClient, createStarterServer, createWarning, encodeFrame, isTlsProtocolError, isTlsVerificationErrorCode, isTransientConnectionError, rawAckUncorrelatable, resolveNackCode };