@cosyte/mllp 0.0.1 → 0.0.3

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.
@@ -2,7 +2,7 @@ import { AckCode, AckErrorDetail, Hl7Message, AckMode, BuildAckOptions, Hl7Parse
2
2
  export { AckCode, AckErrorDetail, AckMode } from '@cosyte/hl7';
3
3
 
4
4
  /**
5
- * `buildMllpAck` — a THIN transport adapter over `@cosyte/hl7`'s `buildAck`.
5
+ * `buildMllpAck`, a THIN transport adapter over `@cosyte/hl7`'s `buildAck`.
6
6
  *
7
7
  * `@cosyte/hl7` owns ACK **content** (MSH/MSA/ERR construction, the fail-safe
8
8
  * no-correlation downgrade); this module owns **transport policy**: accepting
@@ -53,7 +53,7 @@ interface MllpAckWarning {
53
53
  declare const MLLP_ACK_INBOUND_UNPARSEABLE = "MLLP_ACK_INBOUND_UNPARSEABLE";
54
54
  /**
55
55
  * Warning code emitted when the built ACK's MSA-2 bytes are **not** byte-identical
56
- * to the inbound MSH-10 bytes — i.e. the ACK does not echo the message control ID
56
+ * to the inbound MSH-10 bytes, i.e. the ACK does not echo the message control ID
57
57
  * verbatim, as HL7 v2.5.1 §2.9.2.2 requires.
58
58
  *
59
59
  * This is a **correlation failure**, and the reason it is a loud warning rather
@@ -61,7 +61,7 @@ declare const MLLP_ACK_INBOUND_UNPARSEABLE = "MLLP_ACK_INBOUND_UNPARSEABLE";
61
61
  * it put on the wire. An ACK whose MSA-2 differs by even one byte will not match
62
62
  * that key, so the send is never settled → ACK timeout → resend → **duplicate
63
63
  * clinical message**. Emitting the ACK anyway (rather than throwing) is the
64
- * fail-safe choice — a mismatched ACK still tells the peer *something* — but the
64
+ * fail-safe choice, a mismatched ACK still tells the peer *something*, but the
65
65
  * mismatch must never pass unremarked.
66
66
  *
67
67
  * `buildMllpAck` verifies this on every build, against the very same byte-level
@@ -73,7 +73,7 @@ declare const MLLP_ACK_INBOUND_UNPARSEABLE = "MLLP_ACK_INBOUND_UNPARSEABLE";
73
73
  * `string`/`Hl7Message` inbound has **already been decoded** before this module sees it:
74
74
  * the only thing left to compare against is that same text, encoded with the same codec,
75
75
  * so the codec cancels on both sides and a codec-induced mismatch is **structurally
76
- * invisible** — it warns about nothing. See {@link BuildMllpAckOptions.encoding}. Pass a
76
+ * invisible**, it warns about nothing. See {@link BuildMllpAckOptions.encoding}. Pass a
77
77
  * `Buffer` if you want this guarantee to mean anything.
78
78
  *
79
79
  * Two known ways to provoke it, both documented in the package limitations:
@@ -84,7 +84,7 @@ declare const MLLP_ACK_INBOUND_UNPARSEABLE = "MLLP_ACK_INBOUND_UNPARSEABLE";
84
84
  * 2. An inbound that declares **non-default encoding characters** (MSH-1/MSH-2).
85
85
  * `@cosyte/hl7`'s `buildMessage` always emits the HL7 default `|^~\&`, so an
86
86
  * inbound MSH-10 of `ID#X` under a `#` component separator is re-delimited to
87
- * `ID^X` in MSA-2 — different bytes, unmatchable key. Use `buildRawAck` (root
87
+ * `ID^X` in MSA-2, different bytes, unmatchable key. Use `buildRawAck` (root
88
88
  * export), which echoes the inbound's own MSH-1/MSH-2, if you must serve such
89
89
  * a peer.
90
90
  *
@@ -93,11 +93,63 @@ declare const MLLP_ACK_INBOUND_UNPARSEABLE = "MLLP_ACK_INBOUND_UNPARSEABLE";
93
93
  * import { MLLP_ACK_CONTROL_ID_NOT_VERBATIM, buildAckAA } from '@cosyte/mllp/ack-from-hl7';
94
94
  * const { warnings } = buildAckAA(inbound, { encoding: "ascii" });
95
95
  * if (warnings.some((w) => w.code === MLLP_ACK_CONTROL_ID_NOT_VERBATIM)) {
96
- * // this ACK will NOT correlate at the sender — investigate before shipping it
96
+ * // this ACK will NOT correlate at the sender, investigate before shipping it
97
97
  * }
98
98
  * ```
99
99
  */
100
100
  declare const MLLP_ACK_CONTROL_ID_NOT_VERBATIM = "MLLP_ACK_CONTROL_ID_NOT_VERBATIM";
101
+ /**
102
+ * Warning code emitted when the inbound was handed in as **text** (a `string` or an
103
+ * already-parsed `Hl7Message`) rather than as raw `Buffer` bytes, and the ACK's MSA-2
104
+ * carries a **non-ASCII** control ID, a combination whose byte-verbatim echo
105
+ * (HL7 v2.5.1 §2.9.2.2) `buildMllpAck` **cannot verify**, and may silently be breaking.
106
+ *
107
+ * ## Why a distinct code from {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}
108
+ *
109
+ * `MLLP_ACK_CONTROL_ID_NOT_VERBATIM` is a *proof of failure*: on a `Buffer` inbound the
110
+ * wire bytes are in hand, the check compares MSA-2 against them, and it fires only when
111
+ * they provably differ. This code is the opposite, a *confession that the proof cannot
112
+ * be run*. A `string`/`Hl7Message` inbound was decoded from its wire bytes **before** this
113
+ * module ever saw it, so the only thing left to compare the ACK against is that same text
114
+ * re-encoded with the same codec: the codec cancels on both sides and the verbatim check
115
+ * becomes a tautology that always passes (see {@link verifyVerbatimEcho}). It would report
116
+ * clean even for `buildAckAA(payload.toString("latin1"))` on a high-bit control ID, where
117
+ * a `latin1`-decoded `0x8B` is re-encoded as the two `utf8` bytes `0xC2 0x8B`, a *different*
118
+ * control ID the sender cannot correlate (timeout → resend → **duplicate clinical message**).
119
+ *
120
+ * The guard cannot be grown to catch that; by the time text arrives the bytes are gone.
121
+ * The API can, and does: rather than let the text path pass a codec-sensitive control ID
122
+ * off as verified, `buildMllpAck` refuses to stay silent about it. Whenever the ACK's MSA-2
123
+ * control ID holds a code unit outside `0x00`–`0x7F` on a text inbound, the range where the
124
+ * codec choice is load-bearing, it emits this warning. An all-ASCII control ID round-trips
125
+ * identically under every codec, so the common case stays quiet; a non-ASCII one is flagged as
126
+ * *unverifiable*, not as *known-broken*, because from a decoded string the two are genuinely
127
+ * indistinguishable.
128
+ *
129
+ * The check reads the **pre-encoding code units** of MSA-2, not the emitted bytes, so a lossy
130
+ * `{ encoding: "ascii" }` override, which truncates a code unit to its low 8 bits (`str -> byte &
131
+ * 0xFF`) and so masks a code unit above `0xFF` *into* the ASCII byte range on the way out (`U+0153`
132
+ * -> `0x53`), cannot slip a corrupted control ID past it silently.
133
+ * The strongly-discouraged text-plus-override path is covered for the same reason the default `utf8`
134
+ * text path is; the `Buffer` overload remains the answer.
135
+ *
136
+ * **The fix the warning points at is the `Buffer`-first API rule.** Pass the raw payload,
137
+ * the bytes the server handed you, and you get the real byte-level check (and, if it breaks,
138
+ * the definite {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}) instead of this "cannot tell".
139
+ *
140
+ * Like its sibling, this warning carries byte **lengths** only, never the field bytes
141
+ * (MSH-10 is inbound payload content and a warning goes to a log; see {@link verifyVerbatimEcho}).
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * import { MLLP_ACK_CONTROL_ID_UNVERIFIABLE, buildAckAA } from '@cosyte/mllp/ack-from-hl7';
146
+ * const { warnings } = buildAckAA(payload.toString("latin1")); // text inbound, high-bit id
147
+ * if (warnings.some((w) => w.code === MLLP_ACK_CONTROL_ID_UNVERIFIABLE)) {
148
+ * // pass the raw Buffer instead, the echo cannot be verified from decoded text
149
+ * }
150
+ * ```
151
+ */
152
+ declare const MLLP_ACK_CONTROL_ID_UNVERIFIABLE = "MLLP_ACK_CONTROL_ID_UNVERIFIABLE";
101
153
  /**
102
154
  * Options for {@link buildMllpAck} and its six convenience wrappers.
103
155
  *
@@ -113,7 +165,7 @@ interface BuildMllpAckOptions {
113
165
  /** Optional error detail; one entry emits one ERR segment. */
114
166
  readonly error?: AckErrorDetail | readonly AckErrorDetail[];
115
167
  /**
116
- * The codec used to move the ACK between bytes and text — **both** directions,
168
+ * The codec used to move the ACK between bytes and text, **both** directions,
117
169
  * as a single symmetric choice. It decodes a raw-`Buffer` `inbound` on the way in
118
170
  * and encodes the serialized ACK on the way out, so the ACK is emitted in the same
119
171
  * codec the inbound was read in.
@@ -122,13 +174,13 @@ interface BuildMllpAckOptions {
122
174
  * **verbatim** (HL7 v2.5.1 §2.9.2.2). Decoding in one codec and encoding in another
123
175
  * corrupts every non-ASCII byte it round-trips: a `latin1`-decoded `0x8B` re-encoded
124
176
  * as `utf8` goes out as the two bytes `0xC2 0x8B`, which is a *different control ID*
125
- * — and the sender, which keyed its in-flight store on `0x8B`, cannot match the ACK.
177
+ * and the sender, which keyed its in-flight store on `0x8B`, cannot match the ACK.
126
178
  *
127
179
  * **Defaults, by inbound type:**
128
180
  *
129
181
  * - `Buffer` → **`"latin1"`**. We own the decode, and `latin1` is the only codec
130
182
  * that round-trips arbitrary bytes losslessly, so MSA-2 is byte-verbatim for any
131
- * charset — including an MSH-18 of `8859/1` with high-bit bytes in the control ID.
183
+ * charset, including an MSH-18 of `8859/1` with high-bit bytes in the control ID.
132
184
  * - `string` / `Hl7Message` → **`"utf8"`**. The caller already decoded; we re-encode
133
185
  * with the JS-native codec.
134
186
  *
@@ -136,33 +188,52 @@ interface BuildMllpAckOptions {
136
188
  * raw-`Buffer` inbound gives up the verbatim guarantee for non-ASCII bytes (`"ascii"`
137
189
  * masks the high bit; `"utf8"` folds invalid sequences onto `U+FFFD`). On a `Buffer`,
138
190
  * `buildMllpAck` **checks** the result and emits {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}
139
- * if the echo did not survive, so a bad override there is loud rather than silent — but
191
+ * if the echo did not survive, so a bad override there is loud rather than silent, but
140
192
  * it is still a broken ACK. Set this only when the receiving system genuinely demands a
141
193
  * specific codec.
142
194
  *
143
- * **On a `string` / `Hl7Message` inbound the check cannot see a codec problem at all.**
144
- * The wire bytes were decoded to text *before* this function saw them, so there is
145
- * nothing left to compare the ACK against except that same text — the codec cancels on
146
- * both sides and the comparison is a tautology. Concretely,
147
- * `buildAckAA(payload.toString("latin1"))` on a high-bit control ID (`0x8B`, legal under
148
- * an `MSH-18` of `8859/1`) re-encodes it as the two `utf8` bytes `0xC2 0x8B`, emits a
149
- * **different** control ID, and warns about **nothing** — the sender cannot correlate it.
150
- * This is not a hole the guard can be grown to cover; the bytes are gone by then.
195
+ * **On a `string` / `Hl7Message` inbound the verbatim check cannot see a codec problem,
196
+ * so a *different* warning does the honest thing instead.** The wire bytes were decoded to
197
+ * text *before* this function saw them, so there is nothing left to compare the ACK against
198
+ * except that same text: the codec cancels on both sides and {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}
199
+ * can never fire. Concretely, `buildAckAA(payload.toString("latin1"))` on a high-bit control ID
200
+ * (`0x8B`, legal under an `MSH-18` of `8859/1`) re-encodes it as the two `utf8` bytes `0xC2 0x8B`,
201
+ * emitting a **different** control ID the sender cannot correlate. The verbatim check is blind to
202
+ * that, but the byte-safety of a text inbound cannot be *proven*, so `buildMllpAck` refuses to
203
+ * pass it off as verified: whenever the emitted MSA-2 holds a non-ASCII byte on a text inbound it
204
+ * emits {@link MLLP_ACK_CONTROL_ID_UNVERIFIABLE}, an explicit "cannot verify, pass a `Buffer`".
205
+ * The guard is not grown to *catch* the mismatch (the bytes are gone by then); the API stops
206
+ * being silent about the fact that it can't.
151
207
  *
152
208
  * **`Buffer` is the byte-safe path.** Pass the raw payload. It is what the server hands
153
209
  * you, it is what the `Buffer`-first API rule exists for, and it is the only input for
154
- * which the verbatim guarantee — or a warning that it broke — actually means anything.
210
+ * which the verbatim guarantee, or a proof that it broke, actually means anything.
211
+ *
212
+ * **Only a *text* codec is accepted, on every input shape.** This codec serializes the ACK
213
+ * back to bytes, so it must be one that writes characters as a byte stream the peer can read as
214
+ * HL7: `"utf8"`, `"ascii"`, `"latin1"`, or `"binary"`. A **non-text** codec,
215
+ * `"base64"`/`"base64url"`/`"hex"` (which reinterpret the ACK *string* as encoded data) or
216
+ * `"utf16le"`/`"ucs2"` (which NUL-pad every byte), emits a wholesale-garbage frame the receiver
217
+ * cannot parse, so `buildMllpAck` **throws a `TypeError`** for it here rather than hand back an
218
+ * unusable ACK. This applies to a `Buffer` inbound too: a
219
+ * non-text codec there garbles the *inbound* decode into the unparseable fallback (empty MSA-2,
220
+ * so the {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM} check never runs) and then serializes the
221
+ * fallback ACK to garbage bytes that intermittently trip the strict frame encoder, it is never
222
+ * the "loud AE" it was once documented to be. The legitimate byte-level escape hatch is
223
+ * unchanged: a lossy **charset** override on a `Buffer` (`"ascii"` masking a high bit) is still
224
+ * accepted and still caught loudly by {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}; only the
225
+ * categorically-non-serializing codecs are refused.
155
226
  */
156
227
  readonly encoding?: BufferEncoding;
157
228
  /**
158
229
  * Passthrough to `encodeFrame`'s `allowDelimiterBytesInPayload`. Defaults
159
- * to `false` (strict — throws if the serialized ACK somehow contains a
230
+ * to `false` (strict, throws if the serialized ACK somehow contains a
160
231
  * VT/FS byte, which a spec-clean `@cosyte/hl7` emit never does).
161
232
  */
162
233
  readonly allowDelimiterBytesInPayload?: boolean;
163
234
  }
164
235
  /**
165
- * Result of {@link buildMllpAck} — a ready-to-write MLLP frame plus the
236
+ * Result of {@link buildMllpAck}, a ready-to-write MLLP frame plus the
166
237
  * metadata needed to log or assert on what was actually emitted. Frozen
167
238
  * (including the `warnings` array) so subscribers cannot mutate shared state.
168
239
  *
@@ -193,24 +264,27 @@ interface MllpAck {
193
264
  readonly warnings: readonly MllpAckWarning[];
194
265
  }
195
266
  /**
196
- * Build a framed MLLP ACK from an inbound HL7 v2 message — the core
267
+ * Build a framed MLLP ACK from an inbound HL7 v2 message, the core
197
268
  * `ack-from-hl7` adapter. `inbound` may be an already-parsed `Hl7Message`, or
198
269
  * raw bytes/text to parse first.
199
270
  *
200
271
  * Behavior:
201
- * - **Fatally unparseable inbound** (`Hl7ParseError`) — never emits a
272
+ * - **Fatally unparseable inbound** (`Hl7ParseError`), never emits a
202
273
  * positive code: `AA`→`AE`, `CA`→`CE`; other codes pass through. MSA-2 is
203
274
  * empty, `correlationId` and `mode` are `undefined`, and `warnings`
204
275
  * contains one {@link MLLP_ACK_INBOUND_UNPARSEABLE} entry naming the fatal
205
- * code — never any inbound payload bytes.
206
- * - **Normal path** — detects the ack mode from inbound MSH-15/16, delegates
276
+ * code, never any inbound payload bytes.
277
+ * - **Normal path**, detects the ack mode from inbound MSH-15/16, delegates
207
278
  * to `@cosyte/hl7`'s `buildAck` (which applies its own no-correlation
208
279
  * fail-safe), and forwards `buildAck`'s own warnings (mapped to
209
- * `{ code, message }`). Inbound parse warnings are NOT forwarded — they are
280
+ * `{ code, message }`). Inbound parse warnings are NOT forwarded, they are
210
281
  * the caller's concern if they parsed the inbound themselves.
211
282
  *
212
283
  * @throws {MllpPeerMissingError} when `@cosyte/hl7` is not installed.
213
- * @throws {TypeError} when `options.code` is not a known HL7 Table 0008 code.
284
+ * @throws {TypeError} when `options.code` is not a known HL7 Table 0008 code, or when
285
+ * `options.encoding` is a **non-text** codec (`base64`/`base64url`/`hex`/`utf16le`/`ucs2`)
286
+ * on **any** inbound, it would serialize a garbage frame the receiver cannot parse.
287
+ * See {@link BuildMllpAckOptions.encoding}.
214
288
  *
215
289
  * @example
216
290
  * ```typescript
@@ -287,14 +361,14 @@ declare function buildAckCR(inbound: Hl7Message | Buffer | string, options?: Omi
287
361
  * who import both from `@cosyte/hl7` and `@cosyte/mllp/ack-from-hl7`.
288
362
  *
289
363
  * `inbound` may be an already-parsed `Hl7Message`, or raw bytes/text to parse
290
- * first. A fatal parse failure rethrows the `Hl7ParseError` as-is — unlike
364
+ * first. A fatal parse failure rethrows the `Hl7ParseError` as-is, unlike
291
365
  * {@link buildMllpAck}, there is no ACK to build here, so there is no
292
366
  * fallback to construct.
293
367
  *
294
368
  * **Module-realm note:** this adapter loads `@cosyte/hl7` lazily via
295
369
  * `createRequire` (see `peer.ts`). If your code ALSO does
296
370
  * `import { Hl7ParseError } from "@cosyte/hl7"` directly, Node's module
297
- * resolution can — depending on your bundler/runtime — hand you a distinct
371
+ * resolution can, depending on your bundler/runtime, hand you a distinct
298
372
  * module instance than the one this adapter loaded, in which case
299
373
  * `caught instanceof Hl7ParseError` (your import) may be `false` even though
300
374
  * `caught.name === "Hl7ParseError"` and `caught.code` is a valid HL7 fatal
@@ -318,8 +392,8 @@ declare function detectMode(inbound: Hl7Message | Buffer | string): AckMode;
318
392
  *
319
393
  * `@cosyte/mllp` ships **zero runtime dependencies**; `@cosyte/hl7` is an
320
394
  * optional peer dep referenced only from this `/ack-from-hl7` subpath. To keep
321
- * the rest of the package fully dependency-free, the peer is required lazily —
322
- * on first call into `ack-from-hl7` — via `createRequire`, not at module load
395
+ * the rest of the package fully dependency-free, the peer is required lazily,
396
+ * on first call into `ack-from-hl7`, via `createRequire`, not at module load
323
397
  * time. The public API of this subpath stays synchronous throughout.
324
398
  *
325
399
  * @example
@@ -334,7 +408,7 @@ declare function detectMode(inbound: Hl7Message | Buffer | string): AckMode;
334
408
  /**
335
409
  * The subset of the `@cosyte/hl7` runtime surface the `ack-from-hl7` adapter
336
410
  * consumes. Kept narrow and typed via `import type` (erased at compile time)
337
- * so the runtime dependency stays fully lazy — only the shape is known
411
+ * so the runtime dependency stays fully lazy, only the shape is known
338
412
  * statically, the values are resolved via `require()` at first call.
339
413
  *
340
414
  * @example
@@ -354,7 +428,7 @@ interface Hl7Peer {
354
428
  }) => Hl7Message;
355
429
  /**
356
430
  * The peer's own `Hl7Message` class. Exposed (not just used as a type) so
357
- * callers can detect a cross-realm instance — see the dual-package-hazard
431
+ * callers can detect a cross-realm instance, see the dual-package-hazard
358
432
  * note on `resolveInbound` in `build.ts`. Typed as `abstract new` (rather
359
433
  * than a concrete constructor signature) purely so TypeScript accepts it as
360
434
  * an `instanceof` right-hand side without also implying callers can `new`
@@ -366,14 +440,14 @@ interface Hl7Peer {
366
440
  readonly ACK_CODES: typeof ACK_CODES;
367
441
  /**
368
442
  * The peer's fail-safe downgrade primitive (`AA`→`AE`, `CA`→`CE`, everything
369
- * else unchanged) — the single upstream source of truth for the pair; this
443
+ * else unchanged), the single upstream source of truth for the pair; this
370
444
  * adapter never carries its own copy of the mapping.
371
445
  */
372
446
  readonly downgradePositiveAck: (code: AckCode) => AckCode;
373
447
  }
374
448
  /**
375
- * Thrown when `@cosyte/hl7` — the optional peer dependency required by
376
- * `@cosyte/mllp/ack-from-hl7` — cannot be resolved at runtime.
449
+ * Thrown when `@cosyte/hl7`, the optional peer dependency required by
450
+ * `@cosyte/mllp/ack-from-hl7`, cannot be resolved at runtime.
377
451
  *
378
452
  * `@cosyte/hl7` is declared as an optional peer dependency, so a consumer of
379
453
  * the root `@cosyte/mllp` entry point is never forced to install it. Only
@@ -405,7 +479,7 @@ declare class MllpPeerMissingError extends Error {
405
479
  }
406
480
  /**
407
481
  * Load the `@cosyte/hl7` peer module, caching the result for subsequent
408
- * calls. The public adapter functions in `build.ts` call this internally —
482
+ * calls. The public adapter functions in `build.ts` call this internally,
409
483
  * most consumers never call it directly.
410
484
  *
411
485
  * @param requireFn - Injectable `require`-like function, used by tests to
@@ -422,4 +496,4 @@ declare class MllpPeerMissingError extends Error {
422
496
  */
423
497
  declare function loadHl7Peer(requireFn?: (id: string) => unknown): Hl7Peer;
424
498
 
425
- export { type BuildMllpAckOptions, type Hl7Peer, MLLP_ACK_CONTROL_ID_NOT_VERBATIM, MLLP_ACK_INBOUND_UNPARSEABLE, type MllpAck, type MllpAckWarning, MllpPeerMissingError, buildAckAA, buildAckAE, buildAckAR, buildAckCA, buildAckCE, buildAckCR, buildMllpAck, detectMode, loadHl7Peer };
499
+ export { type BuildMllpAckOptions, type Hl7Peer, MLLP_ACK_CONTROL_ID_NOT_VERBATIM, MLLP_ACK_CONTROL_ID_UNVERIFIABLE, MLLP_ACK_INBOUND_UNPARSEABLE, type MllpAck, type MllpAckWarning, MllpPeerMissingError, buildAckAA, buildAckAE, buildAckAR, buildAckCA, buildAckCE, buildAckCR, buildMllpAck, detectMode, loadHl7Peer };
@@ -2,7 +2,7 @@ import { AckCode, AckErrorDetail, Hl7Message, AckMode, BuildAckOptions, Hl7Parse
2
2
  export { AckCode, AckErrorDetail, AckMode } from '@cosyte/hl7';
3
3
 
4
4
  /**
5
- * `buildMllpAck` — a THIN transport adapter over `@cosyte/hl7`'s `buildAck`.
5
+ * `buildMllpAck`, a THIN transport adapter over `@cosyte/hl7`'s `buildAck`.
6
6
  *
7
7
  * `@cosyte/hl7` owns ACK **content** (MSH/MSA/ERR construction, the fail-safe
8
8
  * no-correlation downgrade); this module owns **transport policy**: accepting
@@ -53,7 +53,7 @@ interface MllpAckWarning {
53
53
  declare const MLLP_ACK_INBOUND_UNPARSEABLE = "MLLP_ACK_INBOUND_UNPARSEABLE";
54
54
  /**
55
55
  * Warning code emitted when the built ACK's MSA-2 bytes are **not** byte-identical
56
- * to the inbound MSH-10 bytes — i.e. the ACK does not echo the message control ID
56
+ * to the inbound MSH-10 bytes, i.e. the ACK does not echo the message control ID
57
57
  * verbatim, as HL7 v2.5.1 §2.9.2.2 requires.
58
58
  *
59
59
  * This is a **correlation failure**, and the reason it is a loud warning rather
@@ -61,7 +61,7 @@ declare const MLLP_ACK_INBOUND_UNPARSEABLE = "MLLP_ACK_INBOUND_UNPARSEABLE";
61
61
  * it put on the wire. An ACK whose MSA-2 differs by even one byte will not match
62
62
  * that key, so the send is never settled → ACK timeout → resend → **duplicate
63
63
  * clinical message**. Emitting the ACK anyway (rather than throwing) is the
64
- * fail-safe choice — a mismatched ACK still tells the peer *something* — but the
64
+ * fail-safe choice, a mismatched ACK still tells the peer *something*, but the
65
65
  * mismatch must never pass unremarked.
66
66
  *
67
67
  * `buildMllpAck` verifies this on every build, against the very same byte-level
@@ -73,7 +73,7 @@ declare const MLLP_ACK_INBOUND_UNPARSEABLE = "MLLP_ACK_INBOUND_UNPARSEABLE";
73
73
  * `string`/`Hl7Message` inbound has **already been decoded** before this module sees it:
74
74
  * the only thing left to compare against is that same text, encoded with the same codec,
75
75
  * so the codec cancels on both sides and a codec-induced mismatch is **structurally
76
- * invisible** — it warns about nothing. See {@link BuildMllpAckOptions.encoding}. Pass a
76
+ * invisible**, it warns about nothing. See {@link BuildMllpAckOptions.encoding}. Pass a
77
77
  * `Buffer` if you want this guarantee to mean anything.
78
78
  *
79
79
  * Two known ways to provoke it, both documented in the package limitations:
@@ -84,7 +84,7 @@ declare const MLLP_ACK_INBOUND_UNPARSEABLE = "MLLP_ACK_INBOUND_UNPARSEABLE";
84
84
  * 2. An inbound that declares **non-default encoding characters** (MSH-1/MSH-2).
85
85
  * `@cosyte/hl7`'s `buildMessage` always emits the HL7 default `|^~\&`, so an
86
86
  * inbound MSH-10 of `ID#X` under a `#` component separator is re-delimited to
87
- * `ID^X` in MSA-2 — different bytes, unmatchable key. Use `buildRawAck` (root
87
+ * `ID^X` in MSA-2, different bytes, unmatchable key. Use `buildRawAck` (root
88
88
  * export), which echoes the inbound's own MSH-1/MSH-2, if you must serve such
89
89
  * a peer.
90
90
  *
@@ -93,11 +93,63 @@ declare const MLLP_ACK_INBOUND_UNPARSEABLE = "MLLP_ACK_INBOUND_UNPARSEABLE";
93
93
  * import { MLLP_ACK_CONTROL_ID_NOT_VERBATIM, buildAckAA } from '@cosyte/mllp/ack-from-hl7';
94
94
  * const { warnings } = buildAckAA(inbound, { encoding: "ascii" });
95
95
  * if (warnings.some((w) => w.code === MLLP_ACK_CONTROL_ID_NOT_VERBATIM)) {
96
- * // this ACK will NOT correlate at the sender — investigate before shipping it
96
+ * // this ACK will NOT correlate at the sender, investigate before shipping it
97
97
  * }
98
98
  * ```
99
99
  */
100
100
  declare const MLLP_ACK_CONTROL_ID_NOT_VERBATIM = "MLLP_ACK_CONTROL_ID_NOT_VERBATIM";
101
+ /**
102
+ * Warning code emitted when the inbound was handed in as **text** (a `string` or an
103
+ * already-parsed `Hl7Message`) rather than as raw `Buffer` bytes, and the ACK's MSA-2
104
+ * carries a **non-ASCII** control ID, a combination whose byte-verbatim echo
105
+ * (HL7 v2.5.1 §2.9.2.2) `buildMllpAck` **cannot verify**, and may silently be breaking.
106
+ *
107
+ * ## Why a distinct code from {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}
108
+ *
109
+ * `MLLP_ACK_CONTROL_ID_NOT_VERBATIM` is a *proof of failure*: on a `Buffer` inbound the
110
+ * wire bytes are in hand, the check compares MSA-2 against them, and it fires only when
111
+ * they provably differ. This code is the opposite, a *confession that the proof cannot
112
+ * be run*. A `string`/`Hl7Message` inbound was decoded from its wire bytes **before** this
113
+ * module ever saw it, so the only thing left to compare the ACK against is that same text
114
+ * re-encoded with the same codec: the codec cancels on both sides and the verbatim check
115
+ * becomes a tautology that always passes (see {@link verifyVerbatimEcho}). It would report
116
+ * clean even for `buildAckAA(payload.toString("latin1"))` on a high-bit control ID, where
117
+ * a `latin1`-decoded `0x8B` is re-encoded as the two `utf8` bytes `0xC2 0x8B`, a *different*
118
+ * control ID the sender cannot correlate (timeout → resend → **duplicate clinical message**).
119
+ *
120
+ * The guard cannot be grown to catch that; by the time text arrives the bytes are gone.
121
+ * The API can, and does: rather than let the text path pass a codec-sensitive control ID
122
+ * off as verified, `buildMllpAck` refuses to stay silent about it. Whenever the ACK's MSA-2
123
+ * control ID holds a code unit outside `0x00`–`0x7F` on a text inbound, the range where the
124
+ * codec choice is load-bearing, it emits this warning. An all-ASCII control ID round-trips
125
+ * identically under every codec, so the common case stays quiet; a non-ASCII one is flagged as
126
+ * *unverifiable*, not as *known-broken*, because from a decoded string the two are genuinely
127
+ * indistinguishable.
128
+ *
129
+ * The check reads the **pre-encoding code units** of MSA-2, not the emitted bytes, so a lossy
130
+ * `{ encoding: "ascii" }` override, which truncates a code unit to its low 8 bits (`str -> byte &
131
+ * 0xFF`) and so masks a code unit above `0xFF` *into* the ASCII byte range on the way out (`U+0153`
132
+ * -> `0x53`), cannot slip a corrupted control ID past it silently.
133
+ * The strongly-discouraged text-plus-override path is covered for the same reason the default `utf8`
134
+ * text path is; the `Buffer` overload remains the answer.
135
+ *
136
+ * **The fix the warning points at is the `Buffer`-first API rule.** Pass the raw payload,
137
+ * the bytes the server handed you, and you get the real byte-level check (and, if it breaks,
138
+ * the definite {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}) instead of this "cannot tell".
139
+ *
140
+ * Like its sibling, this warning carries byte **lengths** only, never the field bytes
141
+ * (MSH-10 is inbound payload content and a warning goes to a log; see {@link verifyVerbatimEcho}).
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * import { MLLP_ACK_CONTROL_ID_UNVERIFIABLE, buildAckAA } from '@cosyte/mllp/ack-from-hl7';
146
+ * const { warnings } = buildAckAA(payload.toString("latin1")); // text inbound, high-bit id
147
+ * if (warnings.some((w) => w.code === MLLP_ACK_CONTROL_ID_UNVERIFIABLE)) {
148
+ * // pass the raw Buffer instead, the echo cannot be verified from decoded text
149
+ * }
150
+ * ```
151
+ */
152
+ declare const MLLP_ACK_CONTROL_ID_UNVERIFIABLE = "MLLP_ACK_CONTROL_ID_UNVERIFIABLE";
101
153
  /**
102
154
  * Options for {@link buildMllpAck} and its six convenience wrappers.
103
155
  *
@@ -113,7 +165,7 @@ interface BuildMllpAckOptions {
113
165
  /** Optional error detail; one entry emits one ERR segment. */
114
166
  readonly error?: AckErrorDetail | readonly AckErrorDetail[];
115
167
  /**
116
- * The codec used to move the ACK between bytes and text — **both** directions,
168
+ * The codec used to move the ACK between bytes and text, **both** directions,
117
169
  * as a single symmetric choice. It decodes a raw-`Buffer` `inbound` on the way in
118
170
  * and encodes the serialized ACK on the way out, so the ACK is emitted in the same
119
171
  * codec the inbound was read in.
@@ -122,13 +174,13 @@ interface BuildMllpAckOptions {
122
174
  * **verbatim** (HL7 v2.5.1 §2.9.2.2). Decoding in one codec and encoding in another
123
175
  * corrupts every non-ASCII byte it round-trips: a `latin1`-decoded `0x8B` re-encoded
124
176
  * as `utf8` goes out as the two bytes `0xC2 0x8B`, which is a *different control ID*
125
- * — and the sender, which keyed its in-flight store on `0x8B`, cannot match the ACK.
177
+ * and the sender, which keyed its in-flight store on `0x8B`, cannot match the ACK.
126
178
  *
127
179
  * **Defaults, by inbound type:**
128
180
  *
129
181
  * - `Buffer` → **`"latin1"`**. We own the decode, and `latin1` is the only codec
130
182
  * that round-trips arbitrary bytes losslessly, so MSA-2 is byte-verbatim for any
131
- * charset — including an MSH-18 of `8859/1` with high-bit bytes in the control ID.
183
+ * charset, including an MSH-18 of `8859/1` with high-bit bytes in the control ID.
132
184
  * - `string` / `Hl7Message` → **`"utf8"`**. The caller already decoded; we re-encode
133
185
  * with the JS-native codec.
134
186
  *
@@ -136,33 +188,52 @@ interface BuildMllpAckOptions {
136
188
  * raw-`Buffer` inbound gives up the verbatim guarantee for non-ASCII bytes (`"ascii"`
137
189
  * masks the high bit; `"utf8"` folds invalid sequences onto `U+FFFD`). On a `Buffer`,
138
190
  * `buildMllpAck` **checks** the result and emits {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}
139
- * if the echo did not survive, so a bad override there is loud rather than silent — but
191
+ * if the echo did not survive, so a bad override there is loud rather than silent, but
140
192
  * it is still a broken ACK. Set this only when the receiving system genuinely demands a
141
193
  * specific codec.
142
194
  *
143
- * **On a `string` / `Hl7Message` inbound the check cannot see a codec problem at all.**
144
- * The wire bytes were decoded to text *before* this function saw them, so there is
145
- * nothing left to compare the ACK against except that same text — the codec cancels on
146
- * both sides and the comparison is a tautology. Concretely,
147
- * `buildAckAA(payload.toString("latin1"))` on a high-bit control ID (`0x8B`, legal under
148
- * an `MSH-18` of `8859/1`) re-encodes it as the two `utf8` bytes `0xC2 0x8B`, emits a
149
- * **different** control ID, and warns about **nothing** — the sender cannot correlate it.
150
- * This is not a hole the guard can be grown to cover; the bytes are gone by then.
195
+ * **On a `string` / `Hl7Message` inbound the verbatim check cannot see a codec problem,
196
+ * so a *different* warning does the honest thing instead.** The wire bytes were decoded to
197
+ * text *before* this function saw them, so there is nothing left to compare the ACK against
198
+ * except that same text: the codec cancels on both sides and {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}
199
+ * can never fire. Concretely, `buildAckAA(payload.toString("latin1"))` on a high-bit control ID
200
+ * (`0x8B`, legal under an `MSH-18` of `8859/1`) re-encodes it as the two `utf8` bytes `0xC2 0x8B`,
201
+ * emitting a **different** control ID the sender cannot correlate. The verbatim check is blind to
202
+ * that, but the byte-safety of a text inbound cannot be *proven*, so `buildMllpAck` refuses to
203
+ * pass it off as verified: whenever the emitted MSA-2 holds a non-ASCII byte on a text inbound it
204
+ * emits {@link MLLP_ACK_CONTROL_ID_UNVERIFIABLE}, an explicit "cannot verify, pass a `Buffer`".
205
+ * The guard is not grown to *catch* the mismatch (the bytes are gone by then); the API stops
206
+ * being silent about the fact that it can't.
151
207
  *
152
208
  * **`Buffer` is the byte-safe path.** Pass the raw payload. It is what the server hands
153
209
  * you, it is what the `Buffer`-first API rule exists for, and it is the only input for
154
- * which the verbatim guarantee — or a warning that it broke — actually means anything.
210
+ * which the verbatim guarantee, or a proof that it broke, actually means anything.
211
+ *
212
+ * **Only a *text* codec is accepted, on every input shape.** This codec serializes the ACK
213
+ * back to bytes, so it must be one that writes characters as a byte stream the peer can read as
214
+ * HL7: `"utf8"`, `"ascii"`, `"latin1"`, or `"binary"`. A **non-text** codec,
215
+ * `"base64"`/`"base64url"`/`"hex"` (which reinterpret the ACK *string* as encoded data) or
216
+ * `"utf16le"`/`"ucs2"` (which NUL-pad every byte), emits a wholesale-garbage frame the receiver
217
+ * cannot parse, so `buildMllpAck` **throws a `TypeError`** for it here rather than hand back an
218
+ * unusable ACK. This applies to a `Buffer` inbound too: a
219
+ * non-text codec there garbles the *inbound* decode into the unparseable fallback (empty MSA-2,
220
+ * so the {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM} check never runs) and then serializes the
221
+ * fallback ACK to garbage bytes that intermittently trip the strict frame encoder, it is never
222
+ * the "loud AE" it was once documented to be. The legitimate byte-level escape hatch is
223
+ * unchanged: a lossy **charset** override on a `Buffer` (`"ascii"` masking a high bit) is still
224
+ * accepted and still caught loudly by {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}; only the
225
+ * categorically-non-serializing codecs are refused.
155
226
  */
156
227
  readonly encoding?: BufferEncoding;
157
228
  /**
158
229
  * Passthrough to `encodeFrame`'s `allowDelimiterBytesInPayload`. Defaults
159
- * to `false` (strict — throws if the serialized ACK somehow contains a
230
+ * to `false` (strict, throws if the serialized ACK somehow contains a
160
231
  * VT/FS byte, which a spec-clean `@cosyte/hl7` emit never does).
161
232
  */
162
233
  readonly allowDelimiterBytesInPayload?: boolean;
163
234
  }
164
235
  /**
165
- * Result of {@link buildMllpAck} — a ready-to-write MLLP frame plus the
236
+ * Result of {@link buildMllpAck}, a ready-to-write MLLP frame plus the
166
237
  * metadata needed to log or assert on what was actually emitted. Frozen
167
238
  * (including the `warnings` array) so subscribers cannot mutate shared state.
168
239
  *
@@ -193,24 +264,27 @@ interface MllpAck {
193
264
  readonly warnings: readonly MllpAckWarning[];
194
265
  }
195
266
  /**
196
- * Build a framed MLLP ACK from an inbound HL7 v2 message — the core
267
+ * Build a framed MLLP ACK from an inbound HL7 v2 message, the core
197
268
  * `ack-from-hl7` adapter. `inbound` may be an already-parsed `Hl7Message`, or
198
269
  * raw bytes/text to parse first.
199
270
  *
200
271
  * Behavior:
201
- * - **Fatally unparseable inbound** (`Hl7ParseError`) — never emits a
272
+ * - **Fatally unparseable inbound** (`Hl7ParseError`), never emits a
202
273
  * positive code: `AA`→`AE`, `CA`→`CE`; other codes pass through. MSA-2 is
203
274
  * empty, `correlationId` and `mode` are `undefined`, and `warnings`
204
275
  * contains one {@link MLLP_ACK_INBOUND_UNPARSEABLE} entry naming the fatal
205
- * code — never any inbound payload bytes.
206
- * - **Normal path** — detects the ack mode from inbound MSH-15/16, delegates
276
+ * code, never any inbound payload bytes.
277
+ * - **Normal path**, detects the ack mode from inbound MSH-15/16, delegates
207
278
  * to `@cosyte/hl7`'s `buildAck` (which applies its own no-correlation
208
279
  * fail-safe), and forwards `buildAck`'s own warnings (mapped to
209
- * `{ code, message }`). Inbound parse warnings are NOT forwarded — they are
280
+ * `{ code, message }`). Inbound parse warnings are NOT forwarded, they are
210
281
  * the caller's concern if they parsed the inbound themselves.
211
282
  *
212
283
  * @throws {MllpPeerMissingError} when `@cosyte/hl7` is not installed.
213
- * @throws {TypeError} when `options.code` is not a known HL7 Table 0008 code.
284
+ * @throws {TypeError} when `options.code` is not a known HL7 Table 0008 code, or when
285
+ * `options.encoding` is a **non-text** codec (`base64`/`base64url`/`hex`/`utf16le`/`ucs2`)
286
+ * on **any** inbound, it would serialize a garbage frame the receiver cannot parse.
287
+ * See {@link BuildMllpAckOptions.encoding}.
214
288
  *
215
289
  * @example
216
290
  * ```typescript
@@ -287,14 +361,14 @@ declare function buildAckCR(inbound: Hl7Message | Buffer | string, options?: Omi
287
361
  * who import both from `@cosyte/hl7` and `@cosyte/mllp/ack-from-hl7`.
288
362
  *
289
363
  * `inbound` may be an already-parsed `Hl7Message`, or raw bytes/text to parse
290
- * first. A fatal parse failure rethrows the `Hl7ParseError` as-is — unlike
364
+ * first. A fatal parse failure rethrows the `Hl7ParseError` as-is, unlike
291
365
  * {@link buildMllpAck}, there is no ACK to build here, so there is no
292
366
  * fallback to construct.
293
367
  *
294
368
  * **Module-realm note:** this adapter loads `@cosyte/hl7` lazily via
295
369
  * `createRequire` (see `peer.ts`). If your code ALSO does
296
370
  * `import { Hl7ParseError } from "@cosyte/hl7"` directly, Node's module
297
- * resolution can — depending on your bundler/runtime — hand you a distinct
371
+ * resolution can, depending on your bundler/runtime, hand you a distinct
298
372
  * module instance than the one this adapter loaded, in which case
299
373
  * `caught instanceof Hl7ParseError` (your import) may be `false` even though
300
374
  * `caught.name === "Hl7ParseError"` and `caught.code` is a valid HL7 fatal
@@ -318,8 +392,8 @@ declare function detectMode(inbound: Hl7Message | Buffer | string): AckMode;
318
392
  *
319
393
  * `@cosyte/mllp` ships **zero runtime dependencies**; `@cosyte/hl7` is an
320
394
  * optional peer dep referenced only from this `/ack-from-hl7` subpath. To keep
321
- * the rest of the package fully dependency-free, the peer is required lazily —
322
- * on first call into `ack-from-hl7` — via `createRequire`, not at module load
395
+ * the rest of the package fully dependency-free, the peer is required lazily,
396
+ * on first call into `ack-from-hl7`, via `createRequire`, not at module load
323
397
  * time. The public API of this subpath stays synchronous throughout.
324
398
  *
325
399
  * @example
@@ -334,7 +408,7 @@ declare function detectMode(inbound: Hl7Message | Buffer | string): AckMode;
334
408
  /**
335
409
  * The subset of the `@cosyte/hl7` runtime surface the `ack-from-hl7` adapter
336
410
  * consumes. Kept narrow and typed via `import type` (erased at compile time)
337
- * so the runtime dependency stays fully lazy — only the shape is known
411
+ * so the runtime dependency stays fully lazy, only the shape is known
338
412
  * statically, the values are resolved via `require()` at first call.
339
413
  *
340
414
  * @example
@@ -354,7 +428,7 @@ interface Hl7Peer {
354
428
  }) => Hl7Message;
355
429
  /**
356
430
  * The peer's own `Hl7Message` class. Exposed (not just used as a type) so
357
- * callers can detect a cross-realm instance — see the dual-package-hazard
431
+ * callers can detect a cross-realm instance, see the dual-package-hazard
358
432
  * note on `resolveInbound` in `build.ts`. Typed as `abstract new` (rather
359
433
  * than a concrete constructor signature) purely so TypeScript accepts it as
360
434
  * an `instanceof` right-hand side without also implying callers can `new`
@@ -366,14 +440,14 @@ interface Hl7Peer {
366
440
  readonly ACK_CODES: typeof ACK_CODES;
367
441
  /**
368
442
  * The peer's fail-safe downgrade primitive (`AA`→`AE`, `CA`→`CE`, everything
369
- * else unchanged) — the single upstream source of truth for the pair; this
443
+ * else unchanged), the single upstream source of truth for the pair; this
370
444
  * adapter never carries its own copy of the mapping.
371
445
  */
372
446
  readonly downgradePositiveAck: (code: AckCode) => AckCode;
373
447
  }
374
448
  /**
375
- * Thrown when `@cosyte/hl7` — the optional peer dependency required by
376
- * `@cosyte/mllp/ack-from-hl7` — cannot be resolved at runtime.
449
+ * Thrown when `@cosyte/hl7`, the optional peer dependency required by
450
+ * `@cosyte/mllp/ack-from-hl7`, cannot be resolved at runtime.
377
451
  *
378
452
  * `@cosyte/hl7` is declared as an optional peer dependency, so a consumer of
379
453
  * the root `@cosyte/mllp` entry point is never forced to install it. Only
@@ -405,7 +479,7 @@ declare class MllpPeerMissingError extends Error {
405
479
  }
406
480
  /**
407
481
  * Load the `@cosyte/hl7` peer module, caching the result for subsequent
408
- * calls. The public adapter functions in `build.ts` call this internally —
482
+ * calls. The public adapter functions in `build.ts` call this internally,
409
483
  * most consumers never call it directly.
410
484
  *
411
485
  * @param requireFn - Injectable `require`-like function, used by tests to
@@ -422,4 +496,4 @@ declare class MllpPeerMissingError extends Error {
422
496
  */
423
497
  declare function loadHl7Peer(requireFn?: (id: string) => unknown): Hl7Peer;
424
498
 
425
- export { type BuildMllpAckOptions, type Hl7Peer, MLLP_ACK_CONTROL_ID_NOT_VERBATIM, MLLP_ACK_INBOUND_UNPARSEABLE, type MllpAck, type MllpAckWarning, MllpPeerMissingError, buildAckAA, buildAckAE, buildAckAR, buildAckCA, buildAckCE, buildAckCR, buildMllpAck, detectMode, loadHl7Peer };
499
+ export { type BuildMllpAckOptions, type Hl7Peer, MLLP_ACK_CONTROL_ID_NOT_VERBATIM, MLLP_ACK_CONTROL_ID_UNVERIFIABLE, MLLP_ACK_INBOUND_UNPARSEABLE, type MllpAck, type MllpAckWarning, MllpPeerMissingError, buildAckAA, buildAckAE, buildAckAR, buildAckCA, buildAckCE, buildAckCR, buildMllpAck, detectMode, loadHl7Peer };