@cosyte/mllp 0.0.4 → 0.0.5

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.
package/CHANGELOG.md CHANGED
@@ -76,6 +76,52 @@ begins its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` unt
76
76
 
77
77
  ### Security
78
78
 
79
+ - **BREAKING: ACK-correlation diagnostics no longer carry the control ID (`PHI-WARNING-MESSAGE-LEAK`).**
80
+ The opt-in `correlateByControlId` path built its diagnostics by interpolation, and both ends were
81
+ consumer-controlled and unbounded. The unmatched-ACK path read the **peer's** MSA-2 straight off an
82
+ inbound frame into `MllpFramingError.message` and onto a `controlId` field of the frozen `'error'`
83
+ payload; measured against a peer sending a 1,000,000-byte MSA-2, that produced a 1,000,026-byte
84
+ `Error.message` and an equally large event field, both headed for a log. `MLLP_ACK_AFTER_TIMEOUT`
85
+ did the same with the timed-out send's own MSH-10, and `MllpTimeoutError.messageControlId` carried
86
+ it on an `Error`, whose `stack` is what an error reporter ships off the box.
87
+ The fix is the frozen-registry shape: `src/client/ack-diagnostics.ts` holds one literal per code and
88
+ `ackDiagnosticMessage(code)` takes **no value parameter**, so no caller can widen it into an
89
+ interpolation site. The `Correlator` withholds the string at the source: `onWarning` now receives
90
+ `controlIdBytes` and `onUnmatchedAck` receives a byte length (or `null` when no MSA-2 could be read
91
+ at all), so nothing downstream of it has a control ID to interpolate.
92
+ Breaking field changes, all pre-alpha: `MllpTimeoutError.messageControlId: string | undefined` is
93
+ now `messageControlIdBytes: number | undefined`; the `'error'` event's `controlId` is now
94
+ `controlIdBytes`; the `'warning'` payload for the two correlation codes is the new exported
95
+ `AckCorrelationWarning` (`MllpWarning` plus `controlIdBytes` and `elapsedSinceSendMs`). Warning
96
+ codes are unchanged. A truncated ID was considered and rejected, as was a hex rendering: both still
97
+ grow the diagnostic with the input and both still disclose the bytes. This is the same answer the
98
+ ACK adapter's verbatim-echo warning already reached.
99
+ - **A diagnostic-surface PHI gate that instantiates a client (`test/phi/diagnostic-phi-leak.test.ts`).**
100
+ Binds the shared `assertNoDiagnosticPhiLeak` runner (`@cosyte/test-utils`, pin raised to `^0.0.2`;
101
+ a caret on a `0.0.x` version resolves to that version exactly, so without the bump the runner is not
102
+ even installed). Nineteen declared slots, each naming the diagnostic code it must reach, covering
103
+ the message control ID through correlation, an ACK's own MSH-10 and MSA-3, the outbound payload
104
+ body, embedded VT and FS bytes inbound and outbound, an oversized frame, missing leading VT,
105
+ leading whitespace, FS without CR, LF after FS, an empty payload after a marker-bearing frame, the
106
+ in-flight orphan on disconnect, and all three of the ACK adapter's control-ID paths (unparseable
107
+ inbound, a provably non-verbatim echo, and an unverifiable text echo). The suite it supplements
108
+ scoped itself to the framing layer and never instantiated a client, which is exactly why the three
109
+ correlation surfaces were the three it could not reach. Verified red on the base commit on three
110
+ slots before any fix existed, and re-verified by four mutations afterwards, one of which is a
111
+ hex-encoded echo that a verbatim match cannot see and `checkLengthInvariance` catches.
112
+ `checkLengthInvariance` is decided **per slot and by measurement**: 8 of the 19 hold it, and the
113
+ other 11 give it up because a byte offset or a byte count is the prescribed report there and
114
+ growth is correct. A first draft split the table in two by argument and was wrong about four
115
+ slots, which were giving up the re-encoded-echo check for nothing.
116
+ Scope, stated precisely rather than generously: this closes **unbounded** consumer-controlled input
117
+ on the `correlateByControlId` path. The slot table covers three surfaces (the client, framing via
118
+ `Connection`, and `ack-from-hl7`) and **no slot instantiates `MllpServer`**; the server's diagnostic
119
+ surfaces were read and carry static strings, counts, or Node's own TLS and socket text, so that gap
120
+ holds no HL7 payload today, which is why it was not urgent rather than why the table is complete.
121
+ Two framing codes still render the hex of a **single** byte, and
122
+ `MllpFramingError.snippet` still carries one, both bounded by design and both now described
123
+ accurately on `MllpWarning.message` (whose doc comment claimed the opposite, and shipped that claim
124
+ into the published declarations).
79
125
  - **Repo-side PHI commit-scanner (`scripts/phi-scan.ts`), matching the `@cosyte/hl7` pilot.**
80
126
  mllp transports HL7 v2 payloads (MLLP wraps HL7 in `VT … FS CR`), so its data fixtures
81
127
  (`test/**` `.frame.bin` frames) carry the same PHI shapes hl7's do. The scanner is a direct
@@ -1 +1 @@
1
- {"version":3,"sources":["../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.10_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","../../src/framing/constants.ts","../../src/framing/error.ts","../../src/framing/registry.ts","../../src/framing/encoder.ts","../../src/internal/control-id.ts","../../src/ack-from-hl7/peer.ts","../../src/ack-from-hl7/build.ts"],"names":["createRequire"],"mappings":"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","file":"index.cjs","sourcesContent":["// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","/**\n * MLLP framing byte constants.\n *\n * All byte values match the MLLP v1 wire protocol as specified in HL7 appendix C.\n *\n * @example\n * ```typescript\n * import { VT, FS, CR } from '@cosyte/mllp';\n * // VT = 0x0B, FS = 0x1C, CR = 0x0D\n * ```\n *\n * @packageDocumentation\n */\n\n/** Vertical Tab (0x0B), marks the start of an MLLP frame. */\nexport const VT = 0x0b;\n\n/** File Separator (0x1C), marks the end of MLLP payload bytes. */\nexport const FS = 0x1c;\n\n/** Carriage Return (0x0D), terminates the FS byte; together FS+CR ends the frame. */\nexport const CR = 0x0d;\n\n/** Line Feed (0x0A), used by the MLLP_LF_AFTER_FS tolerance. */\nexport const LF = 0x0a;\n\n/**\n * Default maximum accumulated payload size in bytes (16 MiB).\n *\n * Exceeding this limit throws `MllpFramingError('MLLP_FRAME_TOO_LARGE')`.\n * Satisfies the DoS-prevention requirement on accumulator growth.\n */\nexport const DEFAULT_MAX_FRAME_SIZE = 16 * 1024 * 1024;\n","/**\n * Typed framing error class for unrecoverable MLLP wire-format violations.\n *\n * @example\n * ```typescript\n * import { MllpFramingError } from '@cosyte/mllp';\n * try {\n * encodeFrame(payloadWithVt);\n * } catch (err) {\n * if (err instanceof MllpFramingError) {\n * logger.error({ code: err.code, offset: err.byteOffset });\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { WarningCode } from \"./registry.js\";\n\n/** Maximum number of bytes captured in the `snippet` field. */\nconst MAX_SNIPPET_BYTES = 64;\n\n/**\n * Thrown for unrecoverable MLLP wire-format violations.\n *\n * - `code`, stable `WarningCode` identifying the violation\n * - `byteOffset`, absolute stream position where the violation was detected\n * - `snippet`, the single framing-boundary byte that broke the structure, or empty when the\n * anomaly is not a specific byte. **Never a run of payload content** (see the `snippet` PHI\n * contract below); the constructor caps whatever it is given to 64 bytes as a backstop.\n *\n * @example\n * ```typescript\n * // Pass only the offending framing-boundary byte, never a slice of payload content.\n * throw new MllpFramingError('MLLP_MISSING_LEADING_VT', byteOffset, Buffer.from([byte]));\n * ```\n */\nexport class MllpFramingError extends Error {\n override readonly name = \"MllpFramingError\" as const;\n\n /** Stable warning code identifying the violation type. */\n readonly code: WarningCode;\n\n /** Absolute byte offset in the stream where the error was detected. */\n readonly byteOffset: number;\n\n /**\n * Up to 64 bytes copied from around the anomaly.\n *\n * This is a **copied** Buffer, isolated from the source buffer so it remains\n * valid after the underlying buffer is reused or overwritten.\n *\n * **PHI contract:** the decoder only ever populates this with the single\n * framing-boundary byte that violated the frame structure (whose hex value the\n * `message` already discloses), never a run of payload content bytes. For anomalies\n * whose fault is not a specific byte (`MLLP_FRAME_TOO_LARGE`, the accumulated size),\n * the snippet is **empty**: a payload slice on a public error field would leak a\n * field-body slice of clinical content. Callers constructing this directly are\n * responsible for the same discipline.\n */\n readonly snippet: Buffer;\n\n /**\n * Construct an MLLP framing error.\n *\n * @param code - The stable warning code classifying the violation.\n * @param byteOffset - Absolute stream byte offset where the violation was detected.\n * @param snippet - Raw bytes copied from around the anomaly (capped to 64 bytes).\n * @param message - Optional human-readable override message.\n */\n constructor(\n code: WarningCode,\n byteOffset: number,\n /** Raw bytes around the anomaly. Capped to MAX_SNIPPET_BYTES (64). */\n snippet: Buffer,\n message?: string,\n ) {\n super(message ?? `MLLP framing error: ${code} at byte offset ${byteOffset}`);\n this.code = code;\n this.byteOffset = byteOffset;\n // Copy and cap, error must outlive source buffer; never store a view.\n this.snippet = Buffer.from(snippet.subarray(0, MAX_SNIPPET_BYTES));\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, MllpFramingError);\n }\n }\n}\n","/**\n * MLLP warning code registry and `MllpWarning` factory.\n *\n * `WarningCode` is a **stable public API**. Renaming or removing a code is a breaking change.\n * New codes may only be added in minor versions.\n *\n * @example\n * ```typescript\n * import { createWarning, type MllpWarning, type WarningCode } from '@cosyte/mllp';\n * const w: MllpWarning = createWarning('MLLP_EMPTY_PAYLOAD', 128, 'Empty payload between VT and FS');\n * ```\n *\n * @packageDocumentation\n */\n\n/**\n * Union of all stable MLLP warning codes.\n *\n * These codes are a **public API**, they appear in `onWarning` handlers, log pipelines,\n * monitoring dashboards, and error messages. Renaming is a breaking change.\n *\n * @example\n * ```typescript\n * const code: WarningCode = 'MLLP_FS_WITHOUT_CR';\n * ```\n */\nexport type WarningCode =\n | \"MLLP_MISSING_LEADING_VT\"\n | \"MLLP_FS_WITHOUT_CR\"\n | \"MLLP_LF_AFTER_FS\"\n | \"MLLP_LEADING_WHITESPACE\"\n | \"MLLP_TRAILING_BYTES\"\n | \"MLLP_PAYLOAD_CONTAINS_VT\"\n | \"MLLP_PAYLOAD_CONTAINS_FS\"\n | \"MLLP_EMPTY_PAYLOAD\"\n | \"MLLP_FRAME_TOO_LARGE\"\n | \"MLLP_ACK_UNMATCHED_CONTROL_ID\"\n | \"MLLP_ACK_AFTER_TIMEOUT\";\n\n/**\n * A frozen warning object emitted when the decoder tolerates a framing deviation.\n *\n * `connectionId` is `undefined` when emitted by a standalone `FrameReader`.\n * `Connection` enriches it to the real UUIDv4 before forwarding upstream.\n *\n * @example\n * ```typescript\n * const reader = new FrameReader({\n * onFrame: (p) => process(p),\n * onWarning: (w: MllpWarning) => logger.warn(w),\n * allowFsOnly: true,\n * });\n * ```\n */\nexport interface MllpWarning {\n readonly code: WarningCode;\n /** Stable human-readable description. Never contains payload bytes or secrets. */\n readonly message: string;\n /** Absolute stream byte offset where the anomaly was detected. */\n readonly byteOffset: number;\n /**\n * Connection identifier. `undefined` at the framing layer;\n * enriched by `Connection` before emitting upstream.\n */\n readonly connectionId: string | undefined;\n /** Wall-clock time at point of emission. */\n readonly timestamp: Date;\n}\n\n/**\n * Callback type for receiving MLLP framing warnings.\n *\n * @example\n * ```typescript\n * const onWarning: OnWarning = (w) => logger.warn({ code: w.code, offset: w.byteOffset });\n * ```\n */\nexport type OnWarning = (warning: MllpWarning) => void;\n\n/**\n * Create a frozen `MllpWarning` object. The returned object is `Object.freeze()`'d\n * so subscribers cannot mutate shared warning state.\n *\n * `connectionId` is always `undefined` here; `Connection` enriches it via\n * `{ ...w, connectionId: this.connectionId }` then re-freezes.\n *\n * @example\n * ```typescript\n * const w = createWarning('MLLP_EMPTY_PAYLOAD', 64, 'Empty payload between VT and FS');\n * // w.connectionId === undefined\n * ```\n */\nexport function createWarning(code: WarningCode, byteOffset: number, message: string): MllpWarning {\n return Object.freeze<MllpWarning>({\n code,\n message,\n byteOffset,\n connectionId: undefined,\n timestamp: new Date(),\n });\n}\n","/**\n * MLLP frame encoder, canonical `VT + payload + FS + CR` wrapping.\n *\n * The encoder is a **strict emitter** (Postel's Law: conservative on write).\n * There is no option to loosen the output format, it always emits canonical framing.\n * Tolerance for delimiter bytes in the payload is available via `allowDelimiterBytesInPayload`,\n * which passes bytes through verbatim and emits an `MllpWarning` for each offending byte.\n *\n * @example\n * ```typescript\n * import { encodeFrame } from '@cosyte/mllp';\n * const frame = encodeFrame(Buffer.from('MSH|^~\\\\&|...'));\n * // frame[0] === 0x0B (VT), frame[frame.length - 2] === 0x1C (FS), frame[frame.length - 1] === 0x0D (CR)\n * ```\n *\n * @packageDocumentation\n */\n\nimport { VT, FS, CR } from \"./constants.js\";\nimport { MllpFramingError } from \"./error.js\";\nimport { createWarning } from \"./registry.js\";\nimport type { MllpWarning } from \"./registry.js\";\n\n/**\n * Options for {@link encodeFrame}.\n *\n * @example\n * ```typescript\n * const frame = encodeFrame(payload, {\n * allowDelimiterBytesInPayload: true,\n * onWarning: (w) => logger.warn(w),\n * });\n * ```\n */\nexport interface EncoderOptions {\n /**\n * When `true`, VT (0x0B) or FS (0x1C) bytes within the payload are preserved\n * verbatim in the output frame instead of throwing `MllpFramingError`.\n * An `MllpWarning` is emitted per offending byte if `onWarning` is provided.\n *\n * Default: `false` (strict, throws on delimiter bytes).\n */\n allowDelimiterBytesInPayload?: boolean;\n\n /**\n * Called for each offending delimiter byte when `allowDelimiterBytesInPayload` is `true`.\n * Invocation is wrapped in try/catch, a throwing handler does not interrupt encoding.\n *\n * @example\n * ```typescript\n * const frame = encodeFrame(payload, {\n * allowDelimiterBytesInPayload: true,\n * onWarning: (w) => logger.warn({ code: w.code, offset: w.byteOffset }),\n * });\n * ```\n */\n onWarning?: (w: MllpWarning) => void;\n}\n\n/**\n * Encode `payload` in canonical MLLP framing: `VT (0x0B) + payload + FS (0x1C) + CR (0x0D)`.\n *\n * Strict by default: throws `MllpFramingError` if the payload contains VT or FS bytes.\n * Set `{ allowDelimiterBytesInPayload: true }` to pass those bytes through verbatim\n * and receive an `MllpWarning` per offending byte via `onWarning` instead.\n *\n * The returned Buffer has length `payload.length + 3`. Bytes at positions 1 through\n * `payload.length` are an exact copy of the payload (independent of the input buffer).\n *\n * @throws {MllpFramingError} with code `MLLP_PAYLOAD_CONTAINS_VT` when payload contains\n * byte `0x0B` and `allowDelimiterBytesInPayload` is `false` (default).\n * @throws {MllpFramingError} with code `MLLP_PAYLOAD_CONTAINS_FS` when payload contains\n * byte `0x1C` and `allowDelimiterBytesInPayload` is `false` (default).\n *\n * @example\n * ```typescript\n * import { encodeFrame } from '@cosyte/mllp';\n *\n * // Strict (default), throws on delimiter bytes in payload\n * const frame = encodeFrame(Buffer.from('MSH|^~\\\\&|SEND|FAC|RECV|FAC|...'));\n * socket.write(frame);\n *\n * // Tolerant, passes delimiter bytes through with a warning\n * const frame2 = encodeFrame(dirtyPayload, {\n * allowDelimiterBytesInPayload: true,\n * onWarning: (w) => logger.warn(w),\n * });\n * ```\n */\nexport function encodeFrame(payload: Buffer, opts?: EncoderOptions): Buffer {\n const allow = opts?.allowDelimiterBytesInPayload === true;\n const onWarning = opts?.onWarning;\n\n // Scan for delimiter bytes, O(n) but required to ensure correct framing (FRAME-03).\n for (let i = 0; i < payload.length; i++) {\n const byte = payload[i];\n if (byte === undefined) break; // noUncheckedIndexedAccess guard\n\n if (byte === VT || byte === FS) {\n const code = byte === VT ? \"MLLP_PAYLOAD_CONTAINS_VT\" : \"MLLP_PAYLOAD_CONTAINS_FS\";\n\n if (!allow) {\n // PHI: the snippet must NOT carry the surrounding payload bytes, that is a\n // field-body slice of clinical content on a public error field (MLLP-9 PHI\n // audit, mirroring the decoder's MLLP_FRAME_TOO_LARGE fix). The offending byte\n // is itself a framing delimiter (VT/FS, a control byte, never PHI); the `code`\n // already names which and the offset is on the error, so the snippet is just\n // that one boundary byte.\n throw new MllpFramingError(code, i, Buffer.from([byte]));\n }\n\n // Tolerant path: emit warning and continue, bytes pass through verbatim.\n if (onWarning !== undefined) {\n const byteHex = byte === VT ? \"0x0B\" : \"0x1C\";\n const warning = createWarning(\n code,\n i,\n `Payload contains ${byte === VT ? \"VT\" : \"FS\"} byte (${byteHex}) at offset ${i}; frame will be ambiguous`,\n );\n try {\n onWarning(warning);\n } catch {\n // WARN-06: a throwing handler must not interrupt encoding.\n }\n }\n }\n }\n\n // Allocate output: VT + payload + FS + CR, canonical, immutable shape (FRAME-03).\n // allocUnsafe is safe here because all bytes are set before the buffer is returned.\n const frame = Buffer.allocUnsafe(payload.length + 3);\n frame[0] = VT;\n payload.copy(frame, 1);\n frame[payload.length + 1] = FS;\n frame[payload.length + 2] = CR;\n return frame;\n}\n","/**\n * Byte-level MSH-10 / MSA-2 control-ID scanners, the **single** implementation\n * of \"read the control ID off the wire\", shared by everything in this package\n * that has to agree on what a control ID *is*.\n *\n * Three call sites depend on these bytes agreeing, and a disagreement between\n * any two of them is a lost clinical message:\n *\n * * `src/client/correlator.ts` keys its in-flight store by the MSH-10 it\n * extracts from the **outbound** frame, and looks an inbound ACK up by the\n * MSA-2 it extracts from the **ACK** frame.\n * * `src/server/ack.ts` (`buildRawAck`) echoes the inbound MSH-10 into MSA-2.\n * * `src/ack-from-hl7/build.ts` verifies, against these same scanners, that\n * the ACK it hands back really does echo MSH-10 verbatim.\n *\n * They live here, not in `client/`, because none of that is client policy.\n * It is the package's byte-level reading of HL7 v2.5.1 §2.5.4 (MSH-1 *defines*\n * the field separator) and §2.9.2.2 (MSA-2 echoes MSH-10 verbatim).\n *\n * **INTERNAL**, not part of the public API.\n *\n * @packageDocumentation\n */\n\nconst ASCII_M = 0x4d;\nconst ASCII_S = 0x53;\nconst ASCII_H = 0x48;\nconst ASCII_A = 0x41;\nconst ASCII_B = 0x42;\nconst ASCII_F = 0x46;\nconst ASCII_T = 0x54;\nconst SEGMENT_SEPARATOR_CR = 0x0d;\nconst SEGMENT_SEPARATOR_LF = 0x0a;\n\n/**\n * Decode used for MSH-10 / MSA-2 control-ID extraction: `latin1`, NOT `ascii`.\n *\n * Node's `ascii` codec masks the high bit (`byte & 0x7f`), so it is a **lossy**\n * decode of the raw HL7 v2 bytes. Two consequences, both of which land squarely\n * on ACK correlation:\n *\n * 1. **Key collision.** The correlator keys its live store, its graveyard, and\n * its ACK lookups by the extracted control-ID *string*. Under `ascii`, the\n * distinct control IDs `A\\x8B` and `A\\x0B` both decode to `A\\x0B`, so two\n * concurrently in-flight sends collapse onto one key: the second `enqueue()`\n * overwrites the first in the `Map`, and the first send's promise can never\n * be settled by its own ACK. `latin1` is a 1:1 byte↔code-unit mapping, so\n * every distinct byte string stays a distinct key.\n * 2. **Corrupted observability.** The extracted ID is what we hand to\n * `MLLP_ACK_UNMATCHED_CONTROL_ID` / `MLLP_ACK_AFTER_TIMEOUT` warnings and to\n * `MllpTimeoutError.messageControlId`. A masked ID is a control ID that never\n * existed on the wire, it misdirects exactly the operator who is trying to\n * trace a lost message.\n *\n * Reachable whenever MSH-18 declares a non-ASCII charset (e.g. `8859/1`), where\n * high-bit bytes in a control ID are legal.\n *\n * **`latin1` is the only byte-faithful decode available.** It is not merely the\n * best of several, it is the one that exists. Node's `Buffer` `latin1` codec is\n * true ISO-8859-1: a 1:1 map between the 256 byte values and U+0000–U+00FF, so\n * `Buffer.from(buf.toString(\"latin1\"), \"latin1\")` is the identity for *every*\n * byte string. Every alternative loses bytes in exactly the range that matters:\n *\n * * `ascii` masks the high bit (above).\n * * `utf8` folds every invalid sequence onto `U+FFFD`, so all high-bit bytes in\n * a non-UTF-8 payload collapse onto one key, the same collision as `ascii`.\n * * `TextDecoder(\"iso-8859-1\")`, which is what a charset-driven decode such as\n * `@cosyte/hl7`'s `parseHL7(buffer, { charset })` uses, is **not** ISO-8859-1\n * at all. The WHATWG Encoding Standard aliases the label `iso-8859-1` to\n * **windows-1252**, which maps 0x80–0x9F to typography (`0x8B` → `U+2039`,\n * `0x9C` → `U+0153`) rather than to `U+008B`/`U+009C`. Re-encoding that back\n * to bytes does not round-trip. That is precisely the byte range a high-bit\n * control ID lives in, which is why this package decodes control-ID bytes\n * itself rather than delegating to a charset-aware parser.\n *\n * The key is byte-faithful, not text: under a multi-byte charset (`UNICODE UTF-8`)\n * a control ID reads back as its `latin1` bytes. Correlation stays correct, the\n * map is injective for any charset, which is the property the key needs, and the\n * one a charset-driven decode would lose.\n *\n * @internal\n */\nexport const CONTROL_ID_ENCODING = \"latin1\" as const;\n\n/**\n * MSH-1 values we refuse to read a message with.\n *\n * MSH-1 *is* the field separator (§2.5.4), but a segment terminator (`CR`/`LF`) or\n * an MLLP framing byte (`VT`/`FS`) cannot be one: the first would end the segment it\n * is supposed to delimit, and the second cannot be written back into an ACK without\n * making the ACK unframeable (strict `encodeFrame` throws `MLLP_PAYLOAD_CONTAINS_VT`/`_FS`).\n *\n * **How a payload with such a byte reaches us.** The plainest route is that `buildRawAck` is\n * a **public export**, so a caller can hand any `Buffer` straight to it, decoder or no\n * decoder; these scanners also run on **outbound** payloads the caller supplies. But it can\n * come off the wire too: a `VT` mid-payload makes `FrameReader` discard what it accumulated\n * and start over (`MLLP_TRAILING_BYTES`), so a *delivered* payload never contains a `VT`, but\n * a delivered payload CAN contain an `FS`. Under the `allowMissingLeadingVt` tolerance a non-VT,\n * non-whitespace first byte is taken as payload byte 0, and `FS` (0x1C) is neither, so\n * `FS \"MSH…\" FS CR` delivers a payload whose byte 0 is `0x1C`. Either way the guard is\n * warranted, and neither route depends on the false premise that the decoder screens these out.\n * @internal\n */\nconst UNSAFE_FIELD_SEPARATORS: ReadonlySet<number> = new Set([\n SEGMENT_SEPARATOR_CR,\n SEGMENT_SEPARATOR_LF,\n 0x0b, // VT, MLLP start block\n 0x1c, // FS, MLLP end block\n]);\n\n/** True iff the `CR`/`LF`-delimited segment starting at `i` is an `MSH`. @internal */\nfunction isMshSegmentAt(buf: Buffer, i: number): boolean {\n return (\n i + 3 < buf.length &&\n buf[i] === ASCII_M &&\n buf[i + 1] === ASCII_S &&\n buf[i + 2] === ASCII_H &&\n !UNSAFE_FIELD_SEPARATORS.has(buf[i + 3] as number)\n );\n}\n\n/** Index of the byte after the `CR`/`LF`-delimited segment starting at `i`. @internal */\nfunction segmentEnd(buf: Buffer, i: number): number {\n let end = i;\n while (\n end < buf.length &&\n buf[end] !== SEGMENT_SEPARATOR_CR &&\n buf[end] !== SEGMENT_SEPARATOR_LF\n ) {\n end++;\n }\n return end;\n}\n\n/**\n * Locate the message's `MSH` segment: the **first** `CR`/`LF`-delimited segment whose\n * first three bytes are `MSH` and whose 4th byte is a usable field separator. Returns\n * its `[start, end)` bounds, or `null` if the payload has no such segment.\n *\n * ## Why we search rather than demand `MSH` at byte 0\n *\n * §2.5.1 does make `MSH` the first segment of a *message*, and it is tempting to read\n * that as \"byte 0 or it is not HL7\", that is what an earlier version of this module\n * did, to force its three consumers into agreement. It was a **tolerance regression**,\n * and it caused the exact harm this module exists to prevent. Two shapes reach us with\n * a perfectly good MSH-10 that is not at byte 0:\n *\n * * **A leading `CR`.** The MLLP decoder passes it straight through into the payload\n * (it special-cases only `VT`/`FS`), and real senders emit it.\n * * **A batch header.** `FHS`/`BHS` precede the `MSH` (§2.10.3).\n *\n * Under the byte-0 rule both read as \"no MSH\", so `buildRawAck` emitted a **positive\n * `AA` with an empty MSA-2 and no warning**: the sender, which keyed on the MSH-10 it\n * sent, cannot correlate it, times out, resends, and the receiver commits a\n * **duplicate clinical message**. Silently discarding a field that is *present* is the\n * worst thing a lenient reader can do, and this package's decoder is emphatically\n * lenient (Postel's Law; see CLAUDE.md).\n *\n * The consumers still agree, they simply agree at the **tolerant** fixed point rather\n * than the lossy one. Agreement was never the hard part; agreeing on the *right* answer\n * is.\n * @internal\n */\nfunction findMshSegment(buf: Buffer): { start: number; end: number } | null {\n let segStart = 0;\n while (segStart < buf.length) {\n const end = segmentEnd(buf, segStart);\n if (isMshSegmentAt(buf, segStart)) return { start: segStart, end };\n // Advance past this segment's terminator bytes (handles `CRLF` as one break).\n let next = end;\n while (\n next < buf.length &&\n (buf[next] === SEGMENT_SEPARATOR_CR || buf[next] === SEGMENT_SEPARATOR_LF)\n ) {\n next++;\n }\n if (next === segStart) return null; // no progress, malformed\n segStart = next;\n }\n return null;\n}\n\n/**\n * True iff the `CR`/`LF`-delimited segment starting at `i` is an HL7 **batch/file\n * envelope** header, `FHS`, `BHS`, `BTS`, or `FTS` (§2.10.3). Matched on the\n * three uppercase ASCII segment-ID bytes only; the 4th byte (the separator) is not\n * inspected, because the mere *presence* of a batch segment is what disqualifies a\n * single-message ACK, regardless of that segment's own delimiter. @internal\n */\nfunction isBatchEnvelopeSegmentAt(buf: Buffer, i: number): boolean {\n if (i + 2 >= buf.length) return false;\n const a = buf[i] as number;\n const b = buf[i + 1] as number;\n const c = buf[i + 2] as number;\n // FHS / FTS\n if (a === ASCII_F && ((b === ASCII_H && c === ASCII_S) || (b === ASCII_T && c === ASCII_S))) {\n return true;\n }\n // BHS / BTS\n if (a === ASCII_B && ((b === ASCII_H && c === ASCII_S) || (b === ASCII_T && c === ASCII_S))) {\n return true;\n }\n return false;\n}\n\n/**\n * True iff the payload is **not a single bare HL7 message**, it carries a batch/file\n * envelope (`FHS`/`BHS`/`BTS`/`FTS`, §2.10.3) or a **second `MSH` segment** (two or more\n * messages concatenated in one frame, a documented real-world quirk).\n *\n * ## Why this is a fail-safe refusal signal, not tolerance\n *\n * A single MSA-2 can echo exactly **one** MSH-10. A batch is a *sequence* of messages with\n * a `BTS-1` count; a concatenated frame is N messages back to back. Either way, a positive\n * `AA` that names the first control ID silently leaves messages 2..N unacknowledged, the\n * sender reads \"batch accepted\", so they are lost outright or time out and resend as\n * **duplicate clinical messages**. This predicate lets the ACK builders **downgrade to a\n * loud non-positive answer** for such a payload rather than fabricate a positive\n * disposition for messages nobody looked at.\n *\n * It does **not** parse the batch, re-base on the first `MSH`, or otherwise widen any\n * reader, it only *detects* the multi-message shape so callers can refuse it. Batch ACK\n * is its own feature; this is the refusal that keeps the door shut until it\n * is designed. A normal single message (`MSH` followed by `PID`/`PV1`/`OBR`/…) contains no\n * batch segment and exactly one `MSH`, so this returns `false` for it.\n *\n * Pure byte-level, never throws.\n *\n * @internal\n */\nexport function containsBatchOrExtraMessage(buf: Buffer): boolean {\n let mshCount = 0;\n let segStart = 0;\n while (segStart < buf.length) {\n const end = segmentEnd(buf, segStart);\n if (isMshSegmentAt(buf, segStart)) {\n mshCount++;\n if (mshCount > 1) return true;\n } else if (isBatchEnvelopeSegmentAt(buf, segStart)) {\n return true;\n }\n // Advance past this segment's terminator bytes (handles `CRLF` as one break).\n let next = end;\n while (\n next < buf.length &&\n (buf[next] === SEGMENT_SEPARATOR_CR || buf[next] === SEGMENT_SEPARATOR_LF)\n ) {\n next++;\n }\n if (next === segStart) return false; // no progress, malformed\n segStart = next;\n }\n return false;\n}\n\n/** The MSH segment, decoded and split into its fields. @internal */\nexport interface MshSegment {\n /** MSH-1, the field separator this message declares (§2.5.4). One `latin1` char. */\n readonly fieldSep: string;\n /**\n * The MSH segment split on {@link fieldSep}. `[0]` is the literal `\"MSH\"`, `[1]` is\n * MSH-2 (the encoding characters), and thereafter the index **is** the field number:\n * `[9]` is MSH-10, `[10]` is MSH-11, `[11]` is MSH-12.\n */\n readonly fields: readonly string[];\n}\n\n/**\n * Read the MSH segment of an HL7 v2 payload, **the** scan, which every consumer in\n * this package goes through.\n *\n * Pure byte-level, never throws, returns `null` for anything it cannot read (Postel's\n * Law decoder side; CLAUDE.md guardrail). It:\n *\n * * **locates** the MSH, the first `CR`/`LF`-delimited segment that starts with\n * `MSH`, rather than demanding it at byte 0, so a leading `CR` or an `FHS`/`BHS`\n * batch header cannot hide a control ID that is plainly there ({@link findMshSegment});\n * * takes the field separator from MSH-1 rather than assuming `|` (§2.5.4);\n * * **bounds the field scan at that segment's terminator**;\n * * decodes only that segment, as `latin1` (see {@link CONTROL_ID_ENCODING}), so a\n * high-bit byte survives and a 16 MB payload is not decoded to read its header.\n *\n * ## The segment terminator is load-bearing, not a detail\n *\n * An earlier version of this scan counted field separators without ever stopping at\n * the segment terminator. On a **truncated MSH**, `MSH|^~\\&|EPIC|HOSP|MIRTH|LAB\\r`,\n * which has only 6 fields, the count therefore ran *past the `CR`* and kept counting\n * separators inside the next segment. The \"MSH-10\" it returned was `PID-3`: the\n * patient's **MRN**. That value became the client's correlation key, and was carried\n * into `MllpTimeoutError.messageControlId` and the `MLLP_ACK_UNMATCHED_CONTROL_ID` /\n * `MLLP_ACK_AFTER_TIMEOUT` warnings, a patient identifier in a log line, and a\n * mis-read one at that. A field that does not exist must read as absent, never as the\n * next segment's contents.\n *\n * The two rules are independent, and both are needed. Bounding the scan is what kills\n * the PID-3 read. **Locating** the MSH (rather than demanding it at byte 0) is what\n * stops the bound from turning into a tolerance regression that silently drops a\n * control ID which is present, see {@link findMshSegment}.\n *\n * @example\n * ```typescript\n * const msh = readMshSegment(payloadBuffer);\n * // msh?.fields[9] === 'MSG00001'; msh?.fieldSep === '|'\n * ```\n *\n * @internal\n */\nexport function readMshSegment(buf: Buffer): MshSegment | null {\n const at = findMshSegment(buf);\n if (at === null) return null;\n\n // The separator is MSH-1, the 4th byte OF THE MSH SEGMENT, wherever that segment\n // begins. `findMshSegment` has already rejected an unusable one.\n const fieldSep = String.fromCharCode(buf[at.start + 3] as number);\n // `at.end` is the segment's terminator: the field split cannot reach past it.\n const fields = buf.subarray(at.start, at.end).toString(CONTROL_ID_ENCODING).split(fieldSep);\n return { fieldSep, fields };\n}\n\n/**\n * Strip **leading segment terminators only**, `CR`/`LF` bytes before the first segment.\n *\n * This is the whole of what the parser-backed ACK builder needs re-based, and it is\n * deliberately the *minimum*. `@cosyte/hl7`'s `parseHL7` requires `MSH` to be the first\n * segment and throws `NO_MSH_SEGMENT` otherwise, so a leading `CR`, which the MLLP\n * decoder passes straight through into the payload, would otherwise send `buildMllpAck`\n * down its unparseable fallback for a message whose MSH-10 is plainly readable. A leading\n * `CR`/`LF` is pure segment-terminator noise: it carries **no data**, so dropping it\n * cannot hide anything.\n *\n * ## What this deliberately does NOT do: skip an `FHS`/`BHS` batch header\n *\n * An earlier version of this function re-based on the *located `MSH`*, which also skipped\n * a batch envelope. That was a serious mistake. A batch (§2.10.3) is\n * `[FHS] { [BHS] { MSH … } [BTS] } [FTS]`, a **sequence of messages**. Re-basing on the\n * first `MSH` handed `parseHL7` message 1 and silently discarded every later `MSH`, the\n * `BTS` count, and the `FTS`. `buildMllpAck` then returned a confident positive **`AA`\n * correlated to message 1, with zero warnings**, for a batch whose messages 2..N it had\n * never looked at. The sender reads that as \"the batch is accepted\", so those messages\n * are lost outright, or time out and resend as **duplicate clinical messages**.\n *\n * Batch ACK is a real feature and is tracked separately. It is not something to arrive at\n * by accident, via a byte-offset helper, on the way to fixing something else. Until it is\n * built deliberately, an `FHS`/`BHS` envelope must fall through to `parseHL7`'s\n * `NO_MSH_SEGMENT` and out into the **warned, non-positive `AE` fallback**, a loud\n * refusal to ACK what we did not read. That is the fail-safe answer and it is what the\n * package did before this item touched it.\n *\n * @internal\n */\nexport function stripLeadingSegmentTerminators(buf: Buffer): Buffer {\n let i = 0;\n while (i < buf.length && (buf[i] === SEGMENT_SEPARATOR_CR || buf[i] === SEGMENT_SEPARATOR_LF)) {\n i++;\n }\n return i === 0 ? buf : buf.subarray(i);\n}\n\n/**\n * Extract MSH-10 (Message Control ID) from an HL7 v2 payload, the correlation key.\n *\n * A thin read off {@link readMshSegment}: `null` when the payload has no readable MSH,\n * when the MSH segment is too short to reach MSH-10, or when MSH-10 is present but\n * empty. It is **never** a value taken from another segment.\n *\n * @example\n * ```typescript\n * const id = extractMshControlId(payloadBuffer);\n * // id === 'MSG00001' | null\n * ```\n *\n * @internal\n */\nexport function extractMshControlId(buf: Buffer): string | null {\n const msh = readMshSegment(buf);\n if (msh === null) return null;\n const id = msh.fields[9];\n return id === undefined || id === \"\" ? null : id;\n}\n\n/**\n * Extract MSA-2 (acknowledged Message Control ID) from an HL7 v2 ACK payload.\n *\n * Pure byte-level scan, never throws, returns `null` for malformed input. The field\n * separator comes from the ACK's own MSH-1, read through {@link readMshSegment}, the\n * same tolerant locate every other read in this module uses, so an ACK whose `MSH` is\n * not at byte 0 is still read rather than silently discarded. The `MSA` segment is\n * then located by scanning segment boundaries (`\\r` / `\\n`).\n *\n * Decoded as `latin1` (see {@link CONTROL_ID_ENCODING}), matching both\n * {@link extractMshControlId} and `buildRawAck`'s verbatim MSH-10 → MSA-2 echo,\n * so an ACK for a high-bit control ID looks up the key the send actually\n * enqueued.\n *\n * @example\n * ```typescript\n * const acked = extractMsaControlId(ackPayloadBuffer);\n * // acked === 'MSG00001' | null\n * ```\n *\n * @internal\n */\nexport function extractMsaControlId(buf: Buffer): string | null {\n // MSH-1 establishes the separator for the whole message, including its MSA.\n const msh = readMshSegment(buf);\n if (msh === null) return null;\n const fieldSep = msh.fieldSep.charCodeAt(0);\n const end = buf.length;\n let segStart = 0;\n while (segStart < end) {\n let segEnd = segStart;\n while (\n segEnd < end &&\n buf[segEnd] !== SEGMENT_SEPARATOR_CR &&\n buf[segEnd] !== SEGMENT_SEPARATOR_LF\n ) {\n segEnd++;\n }\n if (\n segEnd - segStart >= 4 &&\n buf[segStart] === ASCII_M &&\n buf[segStart + 1] === ASCII_S &&\n buf[segStart + 2] === ASCII_A &&\n buf[segStart + 3] === fieldSep\n ) {\n // MSA fields: [0]'MSA' [1]MSA-1 [2]MSA-2 ...\n // Iterate from the first separator (segStart+3) up to and including\n // segEnd. Treat `segEnd` as a synthetic separator so the final field\n // closes cleanly even without a trailing CR.\n let fieldIndex = 0;\n let fieldStart = 0;\n for (let i = segStart + 3; i <= segEnd; i++) {\n const b = i < segEnd ? (buf[i] as number) : fieldSep;\n if (b === fieldSep || b === SEGMENT_SEPARATOR_CR || b === SEGMENT_SEPARATOR_LF) {\n fieldIndex++;\n if (fieldIndex === 2) {\n fieldStart = i + 1;\n } else if (fieldIndex === 3) {\n if (fieldStart >= i) return null; // empty MSA-2\n return buf.subarray(fieldStart, i).toString(CONTROL_ID_ENCODING);\n }\n }\n }\n return null;\n }\n // Skip segment terminator bytes to advance to the next segment.\n while (\n segEnd < end &&\n (buf[segEnd] === SEGMENT_SEPARATOR_CR || buf[segEnd] === SEGMENT_SEPARATOR_LF)\n ) {\n segEnd++;\n }\n if (segEnd === segStart) return null; // no progress, malformed\n segStart = segEnd;\n }\n return null;\n}\n","/**\n * Lazy loader for the optional `@cosyte/hl7` peer dependency.\n *\n * `@cosyte/mllp` ships **zero runtime dependencies**; `@cosyte/hl7` is an\n * optional peer dep referenced only from this `/ack-from-hl7` subpath. To keep\n * the rest of the package fully dependency-free, the peer is required lazily,\n * on first call into `ack-from-hl7`, via `createRequire`, not at module load\n * time. The public API of this subpath stays synchronous throughout.\n *\n * @example\n * ```typescript\n * import { loadHl7Peer } from '@cosyte/mllp/ack-from-hl7';\n * const hl7 = loadHl7Peer(); // throws MllpPeerMissingError if not installed\n * ```\n *\n * @packageDocumentation\n */\n\nimport { createRequire } from \"node:module\";\n\nimport type {\n ACK_CODES,\n AckCode,\n BuildAckOptions,\n FATAL_CODES,\n Hl7Message as Hl7MessageType,\n Hl7ParseError as Hl7ParseErrorType,\n} from \"@cosyte/hl7\";\n\n/**\n * The subset of the `@cosyte/hl7` runtime surface the `ack-from-hl7` adapter\n * consumes. Kept narrow and typed via `import type` (erased at compile time)\n * so the runtime dependency stays fully lazy, only the shape is known\n * statically, the values are resolved via `require()` at first call.\n *\n * @example\n * ```typescript\n * import type { Hl7Peer } from '@cosyte/mllp/ack-from-hl7';\n * function useAck(peer: Hl7Peer) {\n * return peer.buildAck;\n * }\n * ```\n */\nexport interface Hl7Peer {\n readonly parseHL7: (raw: string | Buffer) => Hl7MessageType;\n readonly buildAck: (inbound: Hl7MessageType, options: BuildAckOptions) => Hl7MessageType;\n readonly detectAckMode: (inbound: Hl7MessageType) => \"original\" | \"enhanced\";\n readonly buildMessage: (init: { readonly type: string }) => Hl7MessageType;\n /**\n * The peer's own `Hl7Message` class. Exposed (not just used as a type) so\n * callers can detect a cross-realm instance, see the dual-package-hazard\n * note on `resolveInbound` in `build.ts`. Typed as `abstract new` (rather\n * than a concrete constructor signature) purely so TypeScript accepts it as\n * an `instanceof` right-hand side without also implying callers can `new`\n * it directly with arbitrary args.\n */\n readonly Hl7Message: abstract new (...args: never[]) => Hl7MessageType;\n readonly Hl7ParseError: typeof Hl7ParseErrorType;\n readonly FATAL_CODES: typeof FATAL_CODES;\n readonly ACK_CODES: typeof ACK_CODES;\n /**\n * The peer's fail-safe downgrade primitive (`AA`→`AE`, `CA`→`CE`, everything\n * else unchanged), the single upstream source of truth for the pair; this\n * adapter never carries its own copy of the mapping.\n */\n readonly downgradePositiveAck: (code: AckCode) => AckCode;\n}\n\n/** The subpath's own name, used in {@link MllpPeerMissingError}'s message. */\nconst SUBPATH_NAME = \"@cosyte/mllp/ack-from-hl7\";\n\n/** The peer package name, as seen by Node module resolution. */\nconst PEER_PACKAGE_NAME = \"@cosyte/hl7\";\n\n/** Node error codes that indicate a module could not be found at all. */\nconst MODULE_NOT_FOUND_CODES: ReadonlySet<string> = new Set([\n \"ERR_MODULE_NOT_FOUND\",\n \"MODULE_NOT_FOUND\",\n]);\n\n/**\n * Thrown when `@cosyte/hl7`, the optional peer dependency required by\n * `@cosyte/mllp/ack-from-hl7`, cannot be resolved at runtime.\n *\n * `@cosyte/hl7` is declared as an optional peer dependency, so a consumer of\n * the root `@cosyte/mllp` entry point is never forced to install it. Only\n * code that imports from the `/ack-from-hl7` subpath needs it, and only once\n * that code actually runs (the loader is lazy).\n *\n * @example\n * ```typescript\n * import { buildAckAA, MllpPeerMissingError } from '@cosyte/mllp/ack-from-hl7';\n * try {\n * buildAckAA(inboundBuffer);\n * } catch (err) {\n * if (err instanceof MllpPeerMissingError) {\n * console.error(err.message); // explains how to install @cosyte/hl7\n * }\n * }\n * ```\n */\nexport class MllpPeerMissingError extends Error {\n override readonly name = \"MllpPeerMissingError\" as const;\n\n /** Stable machine-readable code identifying this error class. */\n readonly code = \"MLLP_PEER_MISSING\" as const;\n\n /**\n * Construct an `MllpPeerMissingError`.\n *\n * @param cause - The original module-resolution error, preserved for debugging.\n */\n constructor(cause: unknown) {\n super(\n `${PEER_PACKAGE_NAME} is required by ${SUBPATH_NAME} but is not installed. ` +\n `${PEER_PACKAGE_NAME} is an optional peer dependency of @cosyte/mllp, install it ` +\n `alongside @cosyte/mllp to use ${SUBPATH_NAME}: \\`npm install ${PEER_PACKAGE_NAME}\\` ` +\n `(or the equivalent for your package manager).`,\n { cause },\n );\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, MllpPeerMissingError);\n }\n }\n}\n\n/** Cached peer module, populated on first successful {@link loadHl7Peer} call. */\nlet cachedPeer: Hl7Peer | undefined;\n\n/**\n * Matches the *quoted unresolvable module id* in Node's resolution-failure\n * messages, `Cannot find module '@cosyte/hl7'` (CJS) / `Cannot find package\n * '@cosyte/hl7'` (ESM). Anchoring on the quoted id (rather than a bare\n * substring test) keeps a module-not-found thrown from *inside* `@cosyte/hl7`\n * whose message names a different module but whose require-stack still\n * contains the peer's path, propagating unchanged: that is a real bug in the\n * installed peer, not a missing peer.\n * @internal\n */\nconst PEER_UNRESOLVABLE_RE = /Cannot find (?:module|package) '@cosyte\\/hl7'/;\n\n/**\n * True iff `err` is a Node \"module could not be resolved\" error specifically\n * for the `@cosyte/hl7` peer package itself.\n * @internal\n */\nfunction isPeerModuleNotFound(err: unknown): boolean {\n if (typeof err !== \"object\" || err === null) return false;\n const code = (err as { code?: unknown }).code;\n if (typeof code !== \"string\" || !MODULE_NOT_FOUND_CODES.has(code)) return false;\n const message = (err as { message?: unknown }).message;\n return typeof message === \"string\" && PEER_UNRESOLVABLE_RE.test(message);\n}\n\n/**\n * Load the `@cosyte/hl7` peer module, caching the result for subsequent\n * calls. The public adapter functions in `build.ts` call this internally,\n * most consumers never call it directly.\n *\n * @param requireFn - Injectable `require`-like function, used by tests to\n * simulate a missing peer without actually uninstalling it. Defaults to a\n * real `createRequire(import.meta.url)`.\n * @internal\n *\n * @example\n * ```typescript\n * import { loadHl7Peer } from '@cosyte/mllp/ack-from-hl7';\n * const hl7 = loadHl7Peer();\n * const ack = hl7.buildAck(hl7.parseHL7(raw), { code: \"AA\" });\n * ```\n */\nexport function loadHl7Peer(requireFn?: (id: string) => unknown): Hl7Peer {\n if (cachedPeer !== undefined && requireFn === undefined) {\n return cachedPeer;\n }\n\n const doRequire = requireFn ?? createRequire(import.meta.url);\n\n let mod: unknown;\n try {\n mod = doRequire(PEER_PACKAGE_NAME);\n } catch (err) {\n if (isPeerModuleNotFound(err)) {\n throw new MllpPeerMissingError(err);\n }\n throw err;\n }\n\n const peer = mod as Hl7Peer;\n if (requireFn === undefined) {\n cachedPeer = peer;\n }\n return peer;\n}\n","/**\n * `buildMllpAck`, a THIN transport adapter over `@cosyte/hl7`'s `buildAck`.\n *\n * `@cosyte/hl7` owns ACK **content** (MSH/MSA/ERR construction, the fail-safe\n * no-correlation downgrade); this module owns **transport policy**: accepting\n * raw inbound bytes (or an already-parsed message), tolerating an unparseable\n * inbound without ever fabricating a positive disposition, and framing the\n * result into a ready-to-write MLLP `Buffer` via `encodeFrame`.\n *\n * @example\n * ```typescript\n * import { buildAckAA } from '@cosyte/mllp/ack-from-hl7';\n * const { frame } = buildAckAA(inboundBuffer);\n * socket.write(frame);\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { AckCode, AckErrorDetail, AckMode, Hl7Message } from \"@cosyte/hl7\";\n\nimport { encodeFrame } from \"../framing/encoder.js\";\nimport {\n extractMsaControlId,\n extractMshControlId,\n stripLeadingSegmentTerminators,\n} from \"../internal/control-id.js\";\n\nimport { loadHl7Peer } from \"./peer.js\";\nimport type { Hl7Peer } from \"./peer.js\";\n\n/**\n * A single ACK-generation warning, surfaced by {@link buildMllpAck}.\n *\n * `code` is either an upstream `@cosyte/hl7` warning code passed through\n * verbatim (e.g. `\"ACK_NO_CORRELATION_ID\"`) or the one `mllp`-owned code,\n * {@link MLLP_ACK_INBOUND_UNPARSEABLE}.\n *\n * @example\n * ```typescript\n * import { buildAckAA } from '@cosyte/mllp/ack-from-hl7';\n * const { warnings } = buildAckAA(inbound);\n * for (const w of warnings) console.warn(w.code, w.message);\n * ```\n */\nexport interface MllpAckWarning {\n readonly code: string;\n readonly message: string;\n}\n\n/**\n * Warning code emitted when the inbound message could not be parsed at all\n * (a fatal `Hl7ParseError`) and {@link buildMllpAck} fell back to a minimal,\n * uncorrelated ACK rather than fabricating a positive disposition.\n *\n * @example\n * ```typescript\n * import { MLLP_ACK_INBOUND_UNPARSEABLE, buildAckAA } from '@cosyte/mllp/ack-from-hl7';\n * const { warnings } = buildAckAA(\"not hl7 at all\");\n * warnings.some((w) => w.code === MLLP_ACK_INBOUND_UNPARSEABLE); // true\n * ```\n */\nexport const MLLP_ACK_INBOUND_UNPARSEABLE = \"MLLP_ACK_INBOUND_UNPARSEABLE\";\n\n/**\n * Warning code emitted when the built ACK's MSA-2 bytes are **not** byte-identical\n * to the inbound MSH-10 bytes, i.e. the ACK does not echo the message control ID\n * verbatim, as HL7 v2.5.1 §2.9.2.2 requires.\n *\n * This is a **correlation failure**, and the reason it is a loud warning rather\n * than a silent success: the sender keys its in-flight store on the MSH-10 bytes\n * it put on the wire. An ACK whose MSA-2 differs by even one byte will not match\n * that key, so the send is never settled → ACK timeout → resend → **duplicate\n * clinical message**. Emitting the ACK anyway (rather than throwing) is the\n * fail-safe choice, a mismatched ACK still tells the peer *something*, but the\n * mismatch must never pass unremarked.\n *\n * `buildMllpAck` verifies this on every build, against the very same byte-level\n * scanners the `@cosyte/mllp` client uses to correlate (`src/internal/control-id.ts`).\n *\n * **The verification is meaningful for a `Buffer` inbound, and only for a `Buffer`.**\n * A `Buffer` *is* the wire bytes, so comparing the ACK to it is a real byte-level check\n * and a clean result means a `@cosyte/mllp` sender will match this ACK. A\n * `string`/`Hl7Message` inbound has **already been decoded** before this module sees it:\n * the only thing left to compare against is that same text, encoded with the same codec,\n * so the codec cancels on both sides and a codec-induced mismatch is **structurally\n * invisible**, it warns about nothing. See {@link BuildMllpAckOptions.encoding}. Pass a\n * `Buffer` if you want this guarantee to mean anything.\n *\n * Two known ways to provoke it, both documented in the package limitations:\n *\n * 1. Overriding {@link BuildMllpAckOptions.encoding} to a codec that cannot\n * round-trip the inbound bytes (`\"utf8\"`/`\"ascii\"` against a high-bit,\n * 8859/1-charset control ID). The default never does this.\n * 2. An inbound that declares **non-default encoding characters** (MSH-1/MSH-2).\n * `@cosyte/hl7`'s `buildMessage` always emits the HL7 default `|^~\\&`, so an\n * inbound MSH-10 of `ID#X` under a `#` component separator is re-delimited to\n * `ID^X` in MSA-2, different bytes, unmatchable key. Use `buildRawAck` (root\n * export), which echoes the inbound's own MSH-1/MSH-2, if you must serve such\n * a peer.\n *\n * @example\n * ```typescript\n * import { MLLP_ACK_CONTROL_ID_NOT_VERBATIM, buildAckAA } from '@cosyte/mllp/ack-from-hl7';\n * const { warnings } = buildAckAA(inbound, { encoding: \"ascii\" });\n * if (warnings.some((w) => w.code === MLLP_ACK_CONTROL_ID_NOT_VERBATIM)) {\n * // this ACK will NOT correlate at the sender, investigate before shipping it\n * }\n * ```\n */\nexport const MLLP_ACK_CONTROL_ID_NOT_VERBATIM = \"MLLP_ACK_CONTROL_ID_NOT_VERBATIM\";\n\n/**\n * Warning code emitted when the inbound was handed in as **text** (a `string` or an\n * already-parsed `Hl7Message`) rather than as raw `Buffer` bytes, and the ACK's MSA-2\n * carries a **non-ASCII** control ID, a combination whose byte-verbatim echo\n * (HL7 v2.5.1 §2.9.2.2) `buildMllpAck` **cannot verify**, and may silently be breaking.\n *\n * ## Why a distinct code from {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}\n *\n * `MLLP_ACK_CONTROL_ID_NOT_VERBATIM` is a *proof of failure*: on a `Buffer` inbound the\n * wire bytes are in hand, the check compares MSA-2 against them, and it fires only when\n * they provably differ. This code is the opposite, a *confession that the proof cannot\n * be run*. A `string`/`Hl7Message` inbound was decoded from its wire bytes **before** this\n * module ever saw it, so the only thing left to compare the ACK against is that same text\n * re-encoded with the same codec: the codec cancels on both sides and the verbatim check\n * becomes a tautology that always passes (see {@link verifyVerbatimEcho}). It would report\n * clean even for `buildAckAA(payload.toString(\"latin1\"))` on a high-bit control ID, where\n * a `latin1`-decoded `0x8B` is re-encoded as the two `utf8` bytes `0xC2 0x8B`, a *different*\n * control ID the sender cannot correlate (timeout → resend → **duplicate clinical message**).\n *\n * The guard cannot be grown to catch that; by the time text arrives the bytes are gone.\n * The API can, and does: rather than let the text path pass a codec-sensitive control ID\n * off as verified, `buildMllpAck` refuses to stay silent about it. Whenever the ACK's MSA-2\n * control ID holds a code unit outside `0x00`–`0x7F` on a text inbound, the range where the\n * codec choice is load-bearing, it emits this warning. An all-ASCII control ID round-trips\n * identically under every codec, so the common case stays quiet; a non-ASCII one is flagged as\n * *unverifiable*, not as *known-broken*, because from a decoded string the two are genuinely\n * indistinguishable.\n *\n * The check reads the **pre-encoding code units** of MSA-2, not the emitted bytes, so a lossy\n * `{ encoding: \"ascii\" }` override, which truncates a code unit to its low 8 bits (`str -> byte &\n * 0xFF`) and so masks a code unit above `0xFF` *into* the ASCII byte range on the way out (`U+0153`\n * -> `0x53`), cannot slip a corrupted control ID past it silently.\n * The strongly-discouraged text-plus-override path is covered for the same reason the default `utf8`\n * text path is; the `Buffer` overload remains the answer.\n *\n * **The fix the warning points at is the `Buffer`-first API rule.** Pass the raw payload,\n * the bytes the server handed you, and you get the real byte-level check (and, if it breaks,\n * the definite {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}) instead of this \"cannot tell\".\n *\n * Like its sibling, this warning carries byte **lengths** only, never the field bytes\n * (MSH-10 is inbound payload content and a warning goes to a log; see {@link verifyVerbatimEcho}).\n *\n * @example\n * ```typescript\n * import { MLLP_ACK_CONTROL_ID_UNVERIFIABLE, buildAckAA } from '@cosyte/mllp/ack-from-hl7';\n * const { warnings } = buildAckAA(payload.toString(\"latin1\")); // text inbound, high-bit id\n * if (warnings.some((w) => w.code === MLLP_ACK_CONTROL_ID_UNVERIFIABLE)) {\n * // pass the raw Buffer instead, the echo cannot be verified from decoded text\n * }\n * ```\n */\nexport const MLLP_ACK_CONTROL_ID_UNVERIFIABLE = \"MLLP_ACK_CONTROL_ID_UNVERIFIABLE\";\n\n/**\n * Byte-faithful codec for raw-`Buffer` inbound, the default (HL7 v2.5.1 §2.9.2.2).\n *\n * `latin1` is Node's true ISO-8859-1: a 1:1 map between the 256 byte values and\n * U+0000–U+00FF, so `Buffer.from(buf.toString(\"latin1\"), \"latin1\")` is the identity\n * for **every** byte string. It is the only codec for which that holds, which makes\n * it the only one under which MSA-2 can echo an arbitrary inbound MSH-10 verbatim.\n * See `src/internal/control-id.ts` for why `ascii`, `utf8`, and a charset-driven\n * `TextDecoder` each lose bytes in exactly the range a high-bit control ID lives in.\n * @internal\n */\nconst BYTES_INBOUND_ENCODING: BufferEncoding = \"latin1\";\n\n/**\n * Codec for `string` / `Hl7Message` inbound, JS-native `utf8`.\n *\n * When the caller hands us text, **they** already chose the decode; we cannot know\n * which codec produced those code units, so we re-encode with the JS-native one and\n * let them override. (For raw bytes we own the decode, so we can guarantee the\n * round-trip, see {@link BYTES_INBOUND_ENCODING}.)\n * @internal\n */\nconst TEXT_INBOUND_ENCODING: BufferEncoding = \"utf8\";\n\n/**\n * The codecs {@link buildMllpAck} accepts as the ACK **serialization** codec, for a\n * `Buffer` inbound and a `string` / `Hl7Message` inbound alike. The character-faithful\n * single-byte / UTF-8 codecs, under which `Buffer.from(ackText, codec)` serializes the\n * ACK's characters to a byte stream a peer reads back as HL7: `utf8`/`utf-8` (the default\n * for text), `ascii`, and `latin1`/`binary` (the default for bytes).\n *\n * Every *other* `BufferEncoding` is a **non-text** codec that does not serialize\n * characters at all, `base64`/`base64url`/`hex` reinterpret the ACK **string** as\n * encoded data and decode it to unrelated bytes; `utf16le`/`ucs2` interleave a NUL after\n * every ASCII byte. Either way the emitted frame is wholesale garbage, so they are\n * rejected on **every** input shape (see {@link assertSerializableAckEncoding}).\n *\n * This set is exactly the legitimate \"byte-level codec\" escape hatch a `Buffer` inbound\n * offers: a receiving system that demands `ascii` or `latin1` bytes is served here; a\n * non-text codec never is, because it cannot serialize a readable HL7 ACK on any path.\n * @internal\n */\nconst SERIALIZABLE_ACK_ENCODINGS: ReadonlySet<string> = new Set([\n \"utf8\",\n \"utf-8\",\n \"ascii\",\n \"latin1\",\n \"binary\",\n]);\n\n/**\n * Options for {@link buildMllpAck} and its six convenience wrappers.\n *\n * @example\n * ```typescript\n * import type { BuildMllpAckOptions } from '@cosyte/mllp/ack-from-hl7';\n * const opts: BuildMllpAckOptions = { code: \"AE\", error: { conditionCode: \"101\" } };\n * ```\n */\nexport interface BuildMllpAckOptions {\n /** Required acknowledgment disposition to emit in MSA-1 (HL7 Table 0008). */\n readonly code: AckCode;\n /** Optional error detail; one entry emits one ERR segment. */\n readonly error?: AckErrorDetail | readonly AckErrorDetail[];\n /**\n * The codec used to move the ACK between bytes and text, **both** directions,\n * as a single symmetric choice. It decodes a raw-`Buffer` `inbound` on the way in\n * and encodes the serialized ACK on the way out, so the ACK is emitted in the same\n * codec the inbound was read in.\n *\n * That symmetry is the whole point, and it is what makes MSA-2 echo MSH-10\n * **verbatim** (HL7 v2.5.1 §2.9.2.2). Decoding in one codec and encoding in another\n * corrupts every non-ASCII byte it round-trips: a `latin1`-decoded `0x8B` re-encoded\n * as `utf8` goes out as the two bytes `0xC2 0x8B`, which is a *different control ID*\n * and the sender, which keyed its in-flight store on `0x8B`, cannot match the ACK.\n *\n * **Defaults, by inbound type:**\n *\n * - `Buffer` → **`\"latin1\"`**. We own the decode, and `latin1` is the only codec\n * that round-trips arbitrary bytes losslessly, so MSA-2 is byte-verbatim for any\n * charset, including an MSH-18 of `8859/1` with high-bit bytes in the control ID.\n * - `string` / `Hl7Message` → **`\"utf8\"`**. The caller already decoded; we re-encode\n * with the JS-native codec.\n *\n * **Override with care, and only on a `Buffer`.** Any value other than `\"latin1\"` on a\n * raw-`Buffer` inbound gives up the verbatim guarantee for non-ASCII bytes (`\"ascii\"`\n * masks the high bit; `\"utf8\"` folds invalid sequences onto `U+FFFD`). On a `Buffer`,\n * `buildMllpAck` **checks** the result and emits {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}\n * if the echo did not survive, so a bad override there is loud rather than silent, but\n * it is still a broken ACK. Set this only when the receiving system genuinely demands a\n * specific codec.\n *\n * **On a `string` / `Hl7Message` inbound the verbatim check cannot see a codec problem,\n * so a *different* warning does the honest thing instead.** The wire bytes were decoded to\n * text *before* this function saw them, so there is nothing left to compare the ACK against\n * except that same text: the codec cancels on both sides and {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}\n * can never fire. Concretely, `buildAckAA(payload.toString(\"latin1\"))` on a high-bit control ID\n * (`0x8B`, legal under an `MSH-18` of `8859/1`) re-encodes it as the two `utf8` bytes `0xC2 0x8B`,\n * emitting a **different** control ID the sender cannot correlate. The verbatim check is blind to\n * that, but the byte-safety of a text inbound cannot be *proven*, so `buildMllpAck` refuses to\n * pass it off as verified: whenever the emitted MSA-2 holds a non-ASCII byte on a text inbound it\n * emits {@link MLLP_ACK_CONTROL_ID_UNVERIFIABLE}, an explicit \"cannot verify, pass a `Buffer`\".\n * The guard is not grown to *catch* the mismatch (the bytes are gone by then); the API stops\n * being silent about the fact that it can't.\n *\n * **`Buffer` is the byte-safe path.** Pass the raw payload. It is what the server hands\n * you, it is what the `Buffer`-first API rule exists for, and it is the only input for\n * which the verbatim guarantee, or a proof that it broke, actually means anything.\n *\n * **Only a *text* codec is accepted, on every input shape.** This codec serializes the ACK\n * back to bytes, so it must be one that writes characters as a byte stream the peer can read as\n * HL7: `\"utf8\"`, `\"ascii\"`, `\"latin1\"`, or `\"binary\"`. A **non-text** codec,\n * `\"base64\"`/`\"base64url\"`/`\"hex\"` (which reinterpret the ACK *string* as encoded data) or\n * `\"utf16le\"`/`\"ucs2\"` (which NUL-pad every byte), emits a wholesale-garbage frame the receiver\n * cannot parse, so `buildMllpAck` **throws a `TypeError`** for it here rather than hand back an\n * unusable ACK. This applies to a `Buffer` inbound too: a\n * non-text codec there garbles the *inbound* decode into the unparseable fallback (empty MSA-2,\n * so the {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM} check never runs) and then serializes the\n * fallback ACK to garbage bytes that intermittently trip the strict frame encoder, it is never\n * the \"loud AE\" it was once documented to be. The legitimate byte-level escape hatch is\n * unchanged: a lossy **charset** override on a `Buffer` (`\"ascii\"` masking a high bit) is still\n * accepted and still caught loudly by {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}; only the\n * categorically-non-serializing codecs are refused.\n */\n readonly encoding?: BufferEncoding;\n /**\n * Passthrough to `encodeFrame`'s `allowDelimiterBytesInPayload`. Defaults\n * to `false` (strict, throws if the serialized ACK somehow contains a\n * VT/FS byte, which a spec-clean `@cosyte/hl7` emit never does).\n */\n readonly allowDelimiterBytesInPayload?: boolean;\n}\n\n/**\n * Result of {@link buildMllpAck}, a ready-to-write MLLP frame plus the\n * metadata needed to log or assert on what was actually emitted. Frozen\n * (including the `warnings` array) so subscribers cannot mutate shared state.\n *\n * @example\n * ```typescript\n * import { buildAckAA } from '@cosyte/mllp/ack-from-hl7';\n * const ack = buildAckAA(inbound);\n * socket.write(ack.frame);\n * console.log(ack.code, ack.correlationId);\n * ```\n */\nexport interface MllpAck {\n /** `VT + payload + FS + CR`, ready to write to a socket. */\n readonly frame: Buffer;\n /** The serialized ACK, unframed. */\n readonly payload: Buffer;\n /** The built ACK `Hl7Message`. */\n readonly ack: Hl7Message;\n /** The disposition the caller asked for. */\n readonly requestedCode: AckCode;\n /** The disposition MSA-1 actually carries (post fail-safe downgrade). */\n readonly code: AckCode;\n /** Inbound MSH-10, echoed in MSA-2. `undefined` when unfindable. */\n readonly correlationId: string | undefined;\n /** Detected from inbound MSH-15/16. `undefined` when inbound unparseable. */\n readonly mode: AckMode | undefined;\n /** Warnings collected while building the ACK. */\n readonly warnings: readonly MllpAckWarning[];\n}\n\n/**\n * Validate that `code` is one of the six known HL7 Table 0008 acknowledgment\n * codes, using the peer's own `ACK_CODES` registry as the source of truth.\n * Mirrors `@cosyte/hl7`'s own `buildAck` runtime guard so JS callers (who\n * bypass the TypeScript union) get the same `TypeError` behavior.\n * @internal\n */\nfunction assertKnownAckCode(peer: Hl7Peer, code: unknown): asserts code is AckCode {\n const known: readonly string[] = Object.values(peer.ACK_CODES);\n if (typeof code !== \"string\" || !known.includes(code)) {\n throw new TypeError(\n `buildMllpAck: \\`code\\` must be a known HL7 Table 0008 acknowledgment code ` +\n `(AA/AE/AR/CA/CE/CR). Received: ${JSON.stringify(code)}.`,\n );\n }\n}\n\n/**\n * Resolve the single codec used for BOTH the inbound decode and the outbound\n * encode. An explicit `options.encoding` always wins; otherwise raw bytes get\n * the byte-faithful `latin1` and caller-supplied text gets JS-native `utf8`.\n * @internal\n */\nfunction resolveEncoding(\n inbound: Hl7Message | Buffer | string,\n options: BuildMllpAckOptions,\n): BufferEncoding {\n if (options.encoding !== undefined) return options.encoding;\n return Buffer.isBuffer(inbound) ? BYTES_INBOUND_ENCODING : TEXT_INBOUND_ENCODING;\n}\n\n/**\n * Reject a **non-text** codec as the ACK serialization codec, on **every** input shape,\n * `Buffer` and `string`/`Hl7Message` alike, loudly and before anything is emitted.\n *\n * The resolved codec serializes the built ACK back to bytes (`Buffer.from(ack.toString(),\n * codec)`). A *text* codec ({@link SERIALIZABLE_ACK_ENCODINGS}, `utf8`/`ascii`/`latin1`)\n * writes the ACK's characters as a byte stream the peer reads back as HL7. A *non-text* codec\n * does not: `base64`/`base64url`/`hex` reinterpret the ACK **string** as encoded data and\n * decode it to unrelated bytes, and `utf16le`/`ucs2` interleave a NUL after every byte, so the\n * emitted frame is wholesale garbage the receiver cannot parse. A frame nothing can read is a\n * **caller mistake**, not a runtime condition, and the honest place to report a caller mistake\n * is the boundary, not a garbage `Buffer` handed back for the caller to write to a socket and\n * discover broken a round trip later. So this throws a `TypeError` here, exactly as\n * {@link assertKnownAckCode} does for a bad `code`.\n *\n * ## Why this applies to the `Buffer` path too\n *\n * An earlier iteration scoped this to the text path, reasoning\n * that a lossy override on a `Buffer` was *already* caught loudly by the byte-level\n * {@link verifyVerbatimEcho} → {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM} check. That is true for\n * a lossy **charset** codec (`ascii` masking a high bit), but **not** for a genuinely non-text\n * one. A non-text codec garbles the *inbound* decode too: `stripLeadingSegmentTerminators(buf)\n * .toString(\"base64\" | \"hex\" | \"utf16le\" | \"ucs2\")` never yields a string that begins with\n * `MSH`, so it **always** routes to the unparseable fallback, whose MSA-2 is intentionally empty\n * {@link verifyVerbatimEcho} short-circuits on `inboundId === null` and the NOT_VERBATIM proof\n * *never runs*. The supposed safety net is not reachable. Worse, the fallback ACK is then\n * serialized with that same non-text codec: `Buffer.from(ackText, \"base64\")` decodes the ACK\n * text to random bytes that, roughly 3–4 % of the time (identically on Node 22 and Node 24,\n * this was never a runtime divergence, only a flaky draw of the fallback's generated MSH-10),\n * contain a `VT`/`FS` delimiter byte and make the strict {@link encodeFrame} throw a\n * nondeterministic `MllpFramingError`. So the non-text-codec-on-`Buffer` path is neither the\n * \"loud AE\" it was documented to be nor caught by any falsifiable check, it is an unreadable\n * frame that sometimes crashes.\n *\n * The legitimate `Buffer` escape hatch is **untouched**: every codec that can actually serialize\n * an HL7 ACK, `latin1` (the byte-verbatim default), `ascii`, `utf8`, `binary`, is in\n * {@link SERIALIZABLE_ACK_ENCODINGS} and still accepted, and a lossy charset override there is\n * still caught by {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM} exactly as before. What is rejected is\n * only the categorically-non-serializing codec, which had no valid use on any path.\n * @internal\n */\nfunction assertSerializableAckEncoding(encoding: BufferEncoding): void {\n if (SERIALIZABLE_ACK_ENCODINGS.has(encoding.toLowerCase())) return;\n throw new TypeError(\n `buildMllpAck: encoding ${JSON.stringify(encoding)} is not a serializable ACK codec. ` +\n `Serializing an ACK to a non-text codec (base64/base64url/hex/utf16le/ucs2) produces a ` +\n `wholesale-garbage frame the receiver cannot parse, on a Buffer inbound it also garbles ` +\n `the inbound decode, so it can never correlate. Use a text codec (\"utf8\", \"ascii\", or ` +\n `\"latin1\"); on a Buffer, \"latin1\" is byte-verbatim and is the default.`,\n );\n}\n\n/**\n * The inbound as bytes, for the verbatim-echo check, or `undefined` when the\n * inbound has no bytes to name.\n *\n * A `Buffer` *is* its bytes. A `string`'s bytes are that string under the\n * resolved codec. A pre-parsed `Hl7Message` has none: the caller already consumed\n * whatever bytes it came from, so there is nothing to compare MSA-2 against and\n * the check is skipped rather than invented.\n * @internal\n */\nfunction inboundBytes(\n inbound: Hl7Message | Buffer | string,\n encoding: BufferEncoding,\n): Buffer | undefined {\n if (Buffer.isBuffer(inbound)) return inbound;\n if (typeof inbound === \"string\") return Buffer.from(inbound, encoding);\n return undefined;\n}\n\n/**\n * Verify HL7 v2.5.1 §2.9.2.2 on the bytes we are about to put on the wire: the\n * ACK's MSA-2 must be byte-identical to the inbound's MSH-10.\n *\n * Deliberately checked against `src/internal/control-id.ts`, the very scanners\n * the `@cosyte/mllp` client uses to key its in-flight store and to look an ACK\n * back up. This is therefore not only a spec assertion but an end-to-end\n * correlation assertion: if it passes, a `@cosyte/mllp` sender will match this\n * ACK to its send.\n *\n * ## Its reach is bounded by what the caller gave us, and that bound is REAL\n *\n * `inboundBytes` re-encodes a `string`/`Hl7Message` inbound with the *same* codec the\n * ACK is encoded with, so on that path the codec **cancels on both sides** and this\n * check is a **tautology**: it cannot, even in principle, catch a codec-induced\n * non-verbatim echo. `buildAckAA(wire.toString(\"latin1\"))` on a `0x8B` control ID emits\n * `0xC2 0x8B` and this function returns `null`, this proof simply does not apply there.\n *\n * Do **not** try to grow *this* guard to cover the text path: by the time a `string` reaches\n * us the wire bytes are already gone, and there is nothing left to compare against. The check\n * is honest and complete for a `Buffer` inbound, the `Buffer`-first path the API rule points\n * at, and it claims nothing beyond that. The text path is not left silent, though: it is\n * handled by a *separate* signal, {@link verifyTextInboundEcho} /\n * {@link MLLP_ACK_CONTROL_ID_UNVERIFIABLE}, which does not attempt the impossible comparison,\n * it flags a non-ASCII echo on a text inbound as *unverifiable* and points the caller at the\n * `Buffer` overload. Two inputs, two checks; neither pretends to the other's certainty.\n *\n * Returns `null` when the check passes, or cannot be made at all, the inbound has no\n * readable MSH (it does not lead with `MSH`, e.g. it is still MLLP-framed), or it\n * carries no MSH-10 (the peer's own no-correlation fail-safe already warns about that).\n * We never warn on a comparison we could not actually perform.\n *\n * ## The message names NO field content, deliberately\n *\n * An earlier version hex-encoded both control IDs into the warning text, reasoning that\n * a control ID is routing metadata rather than clinical content and that an operator\n * tracing a lost message needs the bytes. That reasoning was wrong twice:\n *\n * 1. **It is not this function's call to make.** MSH-10 is inbound payload content,\n * this module's contract is that its warnings carry *never any inbound payload\n * bytes*, and a warning goes to a log, which in this domain is a place PHI must\n * not reach. A field is not safe to log merely because it is *usually* an opaque id.\n * 2. **It was demonstrably PHI.** Paired with a scanner that ran past the segment\n * terminator (since fixed, see `readMshSegment`), a truncated MSH made \"MSH-10\"\n * resolve to PID-3, and this warning rendered the patient's **MRN** in hex. The\n * scanner bug is fixed; withholding the bytes stays, as defence in depth: the next\n * such bug must not have a paved road into a log line.\n *\n * The warning says *that* the echo broke and what to compare. Nothing is lost, the\n * caller already holds both byte strings (the inbound is their own `payload`; the ACK is\n * the `MllpAck.payload` this call returns).\n * @internal\n */\nfunction verifyVerbatimEcho(\n ackPayload: Buffer,\n inboundRaw: Buffer | undefined,\n): MllpAckWarning | null {\n if (inboundRaw === undefined) return null;\n const inboundId = extractMshControlId(inboundRaw);\n if (inboundId === null) return null;\n const echoedId = extractMsaControlId(ackPayload);\n if (echoedId === inboundId) return null;\n\n // Byte LENGTHS only, never the bytes themselves. The ids are `latin1`, which is 1:1\n // with bytes, so `.length` is a byte count. A length is a shape, not content.\n const inboundLen = String(inboundId.length);\n const echoed = echoedId === null ? \"absent\" : `${String(echoedId.length)} bytes`;\n return {\n code: MLLP_ACK_CONTROL_ID_NOT_VERBATIM,\n message:\n `ACK does not echo the inbound MSH-10 verbatim (HL7 v2.5.1 §2.9.2.2): ` +\n `inbound MSH-10 is ${inboundLen} bytes, emitted MSA-2 is ${echoed}, and they differ. ` +\n `The sender keys its in-flight store on the inbound bytes, so it will NOT match this ACK ` +\n `(ACK timeout -> resend -> duplicate message). ` +\n `Field values are withheld, MSH-10 is inbound payload content and this warning goes to a log; ` +\n `compare your inbound payload against MllpAck.payload to see the bytes.`,\n };\n}\n\n/**\n * The counterpart to {@link verifyVerbatimEcho} for the path that check cannot reach:\n * a **text** inbound (`string` / `Hl7Message`), whose wire bytes are gone before this\n * module sees them.\n *\n * `verifyVerbatimEcho` is a tautology on the text path (it re-derives \"the inbound bytes\"\n * from the same text, with the same codec, that produced the ACK). So instead of comparing\n * (which cannot fail), this looks at the one property that actually signals danger: does the\n * control ID hold a code unit the codec choice could have corrupted? It reads the ACK's MSA-2\n * as its **pre-encoding code units** (`Field.text`), so any code unit `> 0x7F` is a non-ASCII,\n * codec-sensitive control ID. ASCII code units round-trip identically under `latin1`/`utf8`/`ascii`\n * alike, so an all-ASCII control ID is provably safe from any text input and stays quiet; a\n * non-ASCII one cannot be certified from decoded text and is flagged.\n *\n * It reads the pre-encode **code units**, not the emitted **bytes**, on purpose. A lossy\n * `{ encoding: \"ascii\" }` override truncates a code unit to its low 8 bits (`str -> byte & 0xFF`),\n * masking a code unit above `0xFF` *into* the ASCII byte range on the way out (`U+0153` -> `0x53`),\n * so an emitted-byte proxy would fall silent on exactly the corruption that matters.\n * The code units still carry the high bit whatever\n * the codec did to the bytes, and, since encoding ASCII code units can never yield a non-ASCII\n * byte, this is a strict superset of the emitted-non-ASCII test, so the default `utf8` text path\n * is unchanged.\n *\n * This is deliberately *unverifiable*, not *not-verbatim*: from a `string` we cannot know\n * whether the caller's decode matched our encode, so we do not claim the echo broke, only\n * that we cannot prove it held. The remedy is structural, and the message says so: pass the\n * raw `Buffer`.\n *\n * Returns `null` on a `Buffer` inbound (the verified path, {@link verifyVerbatimEcho} owns\n * it), on an inbound with no readable MSA-2 to inspect, or when that MSA-2 is pure ASCII.\n *\n * Byte **lengths** only in the message, never the bytes, same PHI discipline as its sibling.\n * @internal\n */\nfunction verifyTextInboundEcho(\n controlId: string,\n inbound: Hl7Message | Buffer | string,\n): MllpAckWarning | null {\n if (Buffer.isBuffer(inbound)) return null;\n if (controlId === \"\") return null;\n // Inspect the control ID's **pre-encoding code units**, the decoded-text form the caller\n // handed us, read straight off the built ACK's MSA-2 (`Field.text`), BEFORE it is serialized\n // to bytes. Any code unit > 0x7F is a non-ASCII, codec-sensitive control ID: on a text inbound\n // we cannot know whether the caller's decode matched our encode, so the byte-verbatim echo\n // cannot be verified.\n //\n // We check the CODE UNITS, not the emitted bytes, on purpose. A lossy `encoding` override\n // (`{ encoding: \"ascii\" }`) truncates a code unit to its low 8 bits (`str -> byte & 0xFF`), so a\n // code unit above `0xFF` is masked *into* the ASCII byte range on the way out (`U+0153` -> `0x53`),\n // and an emitted-byte proxy would go quiet on exactly the corruption that matters\n // (MLLP-ACK-ASCII-OVERRIDE-BLEED). The pre-encode code units still hold the high bit, so they see\n // it regardless of the codec, and this is a strict superset of the emitted non-ASCII case\n // (encoding ASCII code units can never produce a non-ASCII byte), so the default `utf8` text path\n // is unchanged.\n let hasNonAscii = false;\n for (let i = 0; i < controlId.length; i++) {\n if (controlId.charCodeAt(i) > 0x7f) {\n hasNonAscii = true;\n break;\n }\n }\n if (!hasNonAscii) return null;\n\n // LENGTH (code-unit count) only, never the field content itself (PHI; see `verifyVerbatimEcho`).\n return {\n code: MLLP_ACK_CONTROL_ID_UNVERIFIABLE,\n message:\n `ACK's byte-verbatim echo of MSH-10 (HL7 v2.5.1 §2.9.2.2) cannot be verified: the inbound ` +\n `was decoded text (a string or Hl7Message), not raw bytes, so its wire bytes are gone and ` +\n `the ${String(controlId.length)}-code-unit MSA-2 control ID holds a non-ASCII code unit the ` +\n `decode/encode codec could have altered (a lossy override such as ascii can even mask it into ` +\n `the ASCII byte range). It may not correlate at the sender (ACK timeout -> resend -> ` +\n `duplicate message). Pass the raw payload Buffer to get the byte-level guarantee. ` +\n `Field values are withheld, MSH-10 is inbound payload content and this warning goes to a log.`,\n };\n}\n\n/**\n * Serialize a built ACK message into a framed `MllpAck` result, encoding it with\n * the same codec the inbound was decoded with, and verifying the MSA-2 echo.\n * @internal\n */\nfunction toMllpAck(params: {\n readonly ack: Hl7Message;\n readonly requestedCode: AckCode;\n readonly code: AckCode;\n readonly correlationId: string | undefined;\n readonly mode: AckMode | undefined;\n readonly warnings: readonly MllpAckWarning[];\n readonly options: BuildMllpAckOptions;\n readonly encoding: BufferEncoding;\n /** Inbound bytes to verify the MSA-2 echo against; `undefined` skips the check. */\n readonly verifyAgainst: Buffer | undefined;\n /**\n * The original inbound, to classify the echo's byte-safety. A `Buffer` is verified\n * against its own bytes ({@link verifyVerbatimEcho}); a `string`/`Hl7Message` gets the\n * \"cannot verify\" flag ({@link verifyTextInboundEcho}). `undefined` on the unparseable\n * fallback path, which has no correlated MSA-2 to classify.\n */\n readonly inbound: Hl7Message | Buffer | string | undefined;\n}): MllpAck {\n const { ack, requestedCode, code, correlationId, mode, warnings, options, encoding } = params;\n const payload = Buffer.from(ack.toString(), encoding);\n const frame = encodeFrame(payload, {\n allowDelimiterBytesInPayload: options.allowDelimiterBytesInPayload ?? false,\n });\n\n // Two mutually exclusive echo checks, one per input shape. A `Buffer` inbound is checked\n // against its own wire bytes (a real, falsifiable comparison). A `string`/`Hl7Message` cannot\n // be, the bytes are gone, so it is flagged as unverifiable when the echoed control ID holds a\n // non-ASCII code unit. The text check reads the MSA-2 as PRE-ENCODE code units (not the emitted\n // bytes), so a lossy `ascii` override masking a high bit into the ASCII range cannot hide it.\n const echoWarning = verifyVerbatimEcho(payload, params.verifyAgainst);\n const echoedControlId = ack.segments(\"MSA\")[0]?.field(2).text ?? \"\";\n const unverifiableWarning =\n params.inbound === undefined ? null : verifyTextInboundEcho(echoedControlId, params.inbound);\n const allWarnings = [\n ...warnings,\n ...(echoWarning === null ? [] : [echoWarning]),\n ...(unverifiableWarning === null ? [] : [unverifiableWarning]),\n ];\n\n const result: MllpAck = {\n frame,\n payload,\n ack,\n requestedCode,\n code,\n correlationId,\n mode,\n warnings: Object.freeze([...allWarnings]),\n };\n return Object.freeze(result);\n}\n\n/**\n * Build the minimal fallback ACK for an inbound message that failed to parse\n * at all (a fatal `Hl7ParseError`). Never emits a positive disposition, a\n * requested `AA`/`CA` is downgraded to `AE`/`CE`; every other code passes\n * through unchanged. MSA-2 is left empty (no fabricated correlation) and no\n * ERR segment is added, table-level detail stays the caller's concern via\n * `options.error` on the normal path; this fallback only guarantees a\n * spec-shaped, framed, non-positive ACK when the inbound could not be read\n * at all.\n * @internal\n */\nfunction buildUnparseableFallback(\n peer: Hl7Peer,\n requestedCode: AckCode,\n fatalCode: string,\n options: BuildMllpAckOptions,\n encoding: BufferEncoding,\n): MllpAck {\n // The downgrade pair lives UPSTREAM (single source of truth), this adapter\n // never carries its own AA→AE / CA→CE copy.\n const code = peer.downgradePositiveAck(requestedCode);\n const ack = peer.buildMessage({ type: \"ACK\" }).addSegment(\"MSA\", [code, \"\"]);\n\n const warnings: readonly MllpAckWarning[] = [\n {\n code: MLLP_ACK_INBOUND_UNPARSEABLE,\n message: `Inbound message unparseable (${fatalCode}); emitted ${code} with no correlation id.`,\n },\n ];\n\n return toMllpAck({\n ack,\n requestedCode,\n code,\n correlationId: undefined,\n mode: undefined,\n warnings,\n options,\n encoding,\n // MSA-2 is INTENTIONALLY empty here, the inbound could not be parsed, so there is no\n // control id to echo and we refuse to fabricate one. Running either echo check would\n // report that deliberate choice as a problem; `MLLP_ACK_INBOUND_UNPARSEABLE` (above)\n // is the accurate warning for this case, and it is already attached.\n verifyAgainst: undefined,\n inbound: undefined,\n });\n}\n\n/**\n * Parse `inbound` if it is a `Buffer`/`string`; pass an already-parsed\n * `Hl7Message` through unchanged, UNLESS it was constructed by a different\n * loaded copy of `@cosyte/hl7` than the one this adapter lazily loaded (the\n * classic Node \"dual package hazard\": an ESM `import` and this loader's\n * `createRequire` can each resolve a distinct module instance with distinct\n * class identities, so the peer's own `inbound instanceof Hl7Message` guard\n * inside `buildAck` would otherwise reject a perfectly valid message). In\n * that case, re-derive an in-realm `Hl7Message` by round-tripping through\n * `.toString()`, cheap, and the wire format is the one true interchange\n * boundary between two copies of the same library.\n *\n * ## Why a `Buffer` is decoded here rather than handed to `parseHL7`\n *\n * `parseHL7(buffer)` does its own charset resolution (MSH-18 discovery, or an\n * `options.charset` override) and decodes through a WHATWG `TextDecoder`. That is\n * the right behaviour for reading *clinical text*, and the wrong one for an ACK\n * builder, which needs the control ID to survive as **bytes**:\n *\n * * `TextDecoder` has no byte-faithful codec. Its `iso-8859-1` label is aliased\n * by the WHATWG Encoding Standard to **windows-1252**, which maps `0x8B` to\n * `U+2039` and `0x9C` to `U+0153`, re-encoding those does not give back the\n * bytes that arrived. `utf8` (the default when MSH-18 is absent) folds every\n * invalid sequence onto `U+FFFD`, collapsing all high-bit bytes onto one value.\n * * Node's `Buffer` `latin1` codec, by contrast, IS a 1:1 byte↔code-unit map, so\n * `Buffer.from(buf.toString(\"latin1\"), \"latin1\")` is the identity for every\n * byte string.\n *\n * So we decode the bytes ourselves with the resolved `encoding` and hand `parseHL7`\n * a `string`, which it takes verbatim. Charset is the *caller's* concern (MSH-18,\n * and the `Buffer`-first API rule), this adapter's concern is that the control ID\n * it echoes is the one that arrived.\n * @internal\n */\nfunction resolveInbound(\n peer: Hl7Peer,\n inbound: Hl7Message | Buffer | string,\n encoding: BufferEncoding,\n): Hl7Message {\n // Strip LEADING SEGMENT TERMINATORS ONLY, and do it for bytes and text alike so the two\n // give the same answer for the same message. `parseHL7` requires MSH to be the FIRST\n // segment and throws `NO_MSH_SEGMENT` otherwise; a leading `CR` (which the MLLP decoder\n // passes straight through) carries no data, so dropping it cannot hide anything.\n //\n // It deliberately stops there. It does NOT skip an `FHS`/`BHS` batch envelope, doing so\n // would hand `parseHL7` only the batch's FIRST message and yield a positive `AA` for\n // messages 2..N that were never read. A batch falls through to `NO_MSH_SEGMENT` and out\n // into the warned, non-positive `AE` fallback: a loud refusal to ACK what we did not\n // read. See `stripLeadingSegmentTerminators`.\n if (Buffer.isBuffer(inbound)) {\n return peer.parseHL7(stripLeadingSegmentTerminators(inbound).toString(encoding));\n }\n if (typeof inbound === \"string\") {\n return peer.parseHL7(inbound.replace(/^[\\r\\n]+/, \"\"));\n }\n const isPeerNative: boolean = inbound instanceof peer.Hl7Message;\n return isPeerNative ? inbound : peer.parseHL7(inbound.toString());\n}\n\n/**\n * Build a framed MLLP ACK from an inbound HL7 v2 message, the core\n * `ack-from-hl7` adapter. `inbound` may be an already-parsed `Hl7Message`, or\n * raw bytes/text to parse first.\n *\n * Behavior:\n * - **Fatally unparseable inbound** (`Hl7ParseError`), never emits a\n * positive code: `AA`→`AE`, `CA`→`CE`; other codes pass through. MSA-2 is\n * empty, `correlationId` and `mode` are `undefined`, and `warnings`\n * contains one {@link MLLP_ACK_INBOUND_UNPARSEABLE} entry naming the fatal\n * code, never any inbound payload bytes.\n * - **Normal path**, detects the ack mode from inbound MSH-15/16, delegates\n * to `@cosyte/hl7`'s `buildAck` (which applies its own no-correlation\n * fail-safe), and forwards `buildAck`'s own warnings (mapped to\n * `{ code, message }`). Inbound parse warnings are NOT forwarded, they are\n * the caller's concern if they parsed the inbound themselves.\n *\n * @throws {MllpPeerMissingError} when `@cosyte/hl7` is not installed.\n * @throws {TypeError} when `options.code` is not a known HL7 Table 0008 code, or when\n * `options.encoding` is a **non-text** codec (`base64`/`base64url`/`hex`/`utf16le`/`ucs2`)\n * on **any** inbound, it would serialize a garbage frame the receiver cannot parse.\n * See {@link BuildMllpAckOptions.encoding}.\n *\n * @example\n * ```typescript\n * import { buildMllpAck } from '@cosyte/mllp/ack-from-hl7';\n * const ack = buildMllpAck(inboundBuffer, { code: \"AA\" });\n * socket.write(ack.frame);\n * ```\n */\nexport function buildMllpAck(\n inbound: Hl7Message | Buffer | string,\n options: BuildMllpAckOptions,\n): MllpAck {\n const peer = loadHl7Peer();\n assertKnownAckCode(peer, options.code);\n\n // ONE codec for the whole build, the inbound decode and the outbound encode are\n // the same choice, because an asymmetric pair is exactly what breaks the verbatim\n // MSA-2 echo (§2.9.2.2) and with it the sender's ACK correlation.\n const encoding = resolveEncoding(inbound, options);\n\n // Fail loud at the boundary on a non-text codec (`base64`/`hex`/`utf16le`/…), on every input\n // shape: it would serialize the ACK to a wholesale-garbage frame the receiver cannot parse, and\n // on a `Buffer` it also garbles the inbound decode into the unparseable fallback (so the\n // byte-level NOT_VERBATIM check never runs) while intermittently throwing a framing error from\n // the garbage outbound bytes. See {@link assertSerializableAckEncoding}. The legitimate charset\n // escape hatch (`latin1`/`ascii`/`utf8`/`binary`) is untouched.\n assertSerializableAckEncoding(encoding);\n\n let msg: Hl7Message;\n try {\n msg = resolveInbound(peer, inbound, encoding);\n } catch (err) {\n if (err instanceof peer.Hl7ParseError) {\n return buildUnparseableFallback(peer, options.code, err.code, options, encoding);\n }\n throw err;\n }\n\n const mode = peer.detectAckMode(msg);\n const ack = peer.buildAck(\n msg,\n options.error === undefined\n ? { code: options.code, mode }\n : { code: options.code, error: options.error, mode },\n );\n\n const emittedCode = ack.get(\"MSA.1\");\n assertKnownAckCode(peer, emittedCode);\n\n // The correlation id is what MSA-2 ACTUALLY carries on the wire, the\n // verbatim field text (`Field.text`), never the component-1-only scalar.\n // A vendor-quirk inbound MSH-10 like `ID^X` surfaces here byte-for-byte,\n // matching what a raw-bytes-correlating sender (this package's own client\n // correlator) will compare against.\n const msa2 = ack.segments(\"MSA\")[0]?.field(2).text ?? \"\";\n const correlationId = msa2 !== \"\" ? msa2 : undefined;\n\n const warnings: readonly MllpAckWarning[] = ack.warnings.map((w) => ({\n code: w.code,\n message: w.message,\n }));\n\n return toMllpAck({\n ack,\n requestedCode: options.code,\n code: emittedCode,\n correlationId,\n mode,\n warnings,\n options,\n encoding,\n verifyAgainst: inboundBytes(inbound, encoding),\n inbound,\n });\n}\n\n/**\n * Build an `AA` (Application Accept) MLLP ACK.\n *\n * @example\n * ```typescript\n * import { buildAckAA } from '@cosyte/mllp/ack-from-hl7';\n * socket.write(buildAckAA(inboundBuffer).frame);\n * ```\n */\nexport function buildAckAA(\n inbound: Hl7Message | Buffer | string,\n options?: Omit<BuildMllpAckOptions, \"code\">,\n): MllpAck {\n return buildMllpAck(inbound, { ...options, code: \"AA\" });\n}\n\n/**\n * Build an `AE` (Application Error) MLLP ACK.\n *\n * @example\n * ```typescript\n * import { buildAckAE } from '@cosyte/mllp/ack-from-hl7';\n * socket.write(buildAckAE(inboundBuffer, { error: { conditionCode: \"101\" } }).frame);\n * ```\n */\nexport function buildAckAE(\n inbound: Hl7Message | Buffer | string,\n options?: Omit<BuildMllpAckOptions, \"code\">,\n): MllpAck {\n return buildMllpAck(inbound, { ...options, code: \"AE\" });\n}\n\n/**\n * Build an `AR` (Application Reject) MLLP ACK.\n *\n * @example\n * ```typescript\n * import { buildAckAR } from '@cosyte/mllp/ack-from-hl7';\n * socket.write(buildAckAR(inboundBuffer, { error: { conditionCode: \"200\" } }).frame);\n * ```\n */\nexport function buildAckAR(\n inbound: Hl7Message | Buffer | string,\n options?: Omit<BuildMllpAckOptions, \"code\">,\n): MllpAck {\n return buildMllpAck(inbound, { ...options, code: \"AR\" });\n}\n\n/**\n * Build a `CA` (Commit Accept) MLLP ACK (enhanced mode).\n *\n * @example\n * ```typescript\n * import { buildAckCA } from '@cosyte/mllp/ack-from-hl7';\n * socket.write(buildAckCA(inboundBuffer).frame);\n * ```\n */\nexport function buildAckCA(\n inbound: Hl7Message | Buffer | string,\n options?: Omit<BuildMllpAckOptions, \"code\">,\n): MllpAck {\n return buildMllpAck(inbound, { ...options, code: \"CA\" });\n}\n\n/**\n * Build a `CE` (Commit Error) MLLP ACK (enhanced mode).\n *\n * @example\n * ```typescript\n * import { buildAckCE } from '@cosyte/mllp/ack-from-hl7';\n * socket.write(buildAckCE(inboundBuffer, { error: { conditionCode: \"101\" } }).frame);\n * ```\n */\nexport function buildAckCE(\n inbound: Hl7Message | Buffer | string,\n options?: Omit<BuildMllpAckOptions, \"code\">,\n): MllpAck {\n return buildMllpAck(inbound, { ...options, code: \"CE\" });\n}\n\n/**\n * Build a `CR` (Commit Reject) MLLP ACK (enhanced mode).\n *\n * @example\n * ```typescript\n * import { buildAckCR } from '@cosyte/mllp/ack-from-hl7';\n * socket.write(buildAckCR(inboundBuffer, { error: { conditionCode: \"200\" } }).frame);\n * ```\n */\nexport function buildAckCR(\n inbound: Hl7Message | Buffer | string,\n options?: Omit<BuildMllpAckOptions, \"code\">,\n): MllpAck {\n return buildMllpAck(inbound, { ...options, code: \"CR\" });\n}\n\n/**\n * Detect the HL7 acknowledgment mode (`\"original\"` vs `\"enhanced\"`) of an\n * inbound message, from MSH-15/16. Named `detectMode` (not `detectAckMode`)\n * to avoid colliding with the peer's own `detectAckMode` export for callers\n * who import both from `@cosyte/hl7` and `@cosyte/mllp/ack-from-hl7`.\n *\n * `inbound` may be an already-parsed `Hl7Message`, or raw bytes/text to parse\n * first. A fatal parse failure rethrows the `Hl7ParseError` as-is, unlike\n * {@link buildMllpAck}, there is no ACK to build here, so there is no\n * fallback to construct.\n *\n * **Module-realm note:** this adapter loads `@cosyte/hl7` lazily via\n * `createRequire` (see `peer.ts`). If your code ALSO does\n * `import { Hl7ParseError } from \"@cosyte/hl7\"` directly, Node's module\n * resolution can, depending on your bundler/runtime, hand you a distinct\n * module instance than the one this adapter loaded, in which case\n * `caught instanceof Hl7ParseError` (your import) may be `false` even though\n * `caught.name === \"Hl7ParseError\"` and `caught.code` is a valid HL7 fatal\n * code (`NO_MSH_SEGMENT` / `MSH_TOO_SHORT` / `INVALID_ENCODING_CHARACTERS` /\n * `EMPTY_INPUT`). Prefer narrowing on `.code`/`.name`, or obtain the exact\n * class via this subpath's own `loadHl7Peer().Hl7ParseError`.\n *\n * @throws {MllpPeerMissingError} when `@cosyte/hl7` is not installed.\n * @throws {Hl7ParseError} when `inbound` is raw bytes/text that fails to parse.\n *\n * @example\n * ```typescript\n * import { detectMode } from '@cosyte/mllp/ack-from-hl7';\n * detectMode(inboundBuffer); // \"original\" | \"enhanced\"\n * ```\n */\nexport function detectMode(inbound: Hl7Message | Buffer | string): AckMode {\n const peer = loadHl7Peer();\n // Same byte-faithful decode as `buildMllpAck` (see `resolveInbound`), so a\n // high-bit byte anywhere in the message cannot make the two disagree about\n // which message they are looking at. MSH-15/16 are ASCII either way, so this\n // is about consistency, not about the mode itself.\n const msg = resolveInbound(peer, inbound, BYTES_INBOUND_ENCODING);\n return peer.detectAckMode(msg);\n}\n"]}
1
+ {"version":3,"sources":["../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.10_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","../../src/framing/constants.ts","../../src/framing/error.ts","../../src/framing/registry.ts","../../src/framing/encoder.ts","../../src/internal/control-id.ts","../../src/ack-from-hl7/peer.ts","../../src/ack-from-hl7/build.ts"],"names":["createRequire"],"mappings":"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","file":"index.cjs","sourcesContent":["// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","/**\n * MLLP framing byte constants.\n *\n * All byte values match the MLLP v1 wire protocol as specified in HL7 appendix C.\n *\n * @example\n * ```typescript\n * import { VT, FS, CR } from '@cosyte/mllp';\n * // VT = 0x0B, FS = 0x1C, CR = 0x0D\n * ```\n *\n * @packageDocumentation\n */\n\n/** Vertical Tab (0x0B), marks the start of an MLLP frame. */\nexport const VT = 0x0b;\n\n/** File Separator (0x1C), marks the end of MLLP payload bytes. */\nexport const FS = 0x1c;\n\n/** Carriage Return (0x0D), terminates the FS byte; together FS+CR ends the frame. */\nexport const CR = 0x0d;\n\n/** Line Feed (0x0A), used by the MLLP_LF_AFTER_FS tolerance. */\nexport const LF = 0x0a;\n\n/**\n * Default maximum accumulated payload size in bytes (16 MiB).\n *\n * Exceeding this limit throws `MllpFramingError('MLLP_FRAME_TOO_LARGE')`.\n * Satisfies the DoS-prevention requirement on accumulator growth.\n */\nexport const DEFAULT_MAX_FRAME_SIZE = 16 * 1024 * 1024;\n","/**\n * Typed framing error class for unrecoverable MLLP wire-format violations.\n *\n * @example\n * ```typescript\n * import { MllpFramingError } from '@cosyte/mllp';\n * try {\n * encodeFrame(payloadWithVt);\n * } catch (err) {\n * if (err instanceof MllpFramingError) {\n * logger.error({ code: err.code, offset: err.byteOffset });\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { WarningCode } from \"./registry.js\";\n\n/** Maximum number of bytes captured in the `snippet` field. */\nconst MAX_SNIPPET_BYTES = 64;\n\n/**\n * Thrown for unrecoverable MLLP wire-format violations.\n *\n * - `code`, stable `WarningCode` identifying the violation\n * - `byteOffset`, absolute stream position where the violation was detected\n * - `snippet`, the single framing-boundary byte that broke the structure, or empty when the\n * anomaly is not a specific byte. **Never a run of payload content** (see the `snippet` PHI\n * contract below); the constructor caps whatever it is given to 64 bytes as a backstop.\n *\n * @example\n * ```typescript\n * // Pass only the offending framing-boundary byte, never a slice of payload content.\n * throw new MllpFramingError('MLLP_MISSING_LEADING_VT', byteOffset, Buffer.from([byte]));\n * ```\n */\nexport class MllpFramingError extends Error {\n override readonly name = \"MllpFramingError\" as const;\n\n /** Stable warning code identifying the violation type. */\n readonly code: WarningCode;\n\n /** Absolute byte offset in the stream where the error was detected. */\n readonly byteOffset: number;\n\n /**\n * Up to 64 bytes copied from around the anomaly.\n *\n * This is a **copied** Buffer, isolated from the source buffer so it remains\n * valid after the underlying buffer is reused or overwritten.\n *\n * **PHI contract:** the decoder only ever populates this with the single\n * framing-boundary byte that violated the frame structure (whose hex value the\n * `message` already discloses), never a run of payload content bytes. For anomalies\n * whose fault is not a specific byte (`MLLP_FRAME_TOO_LARGE`, the accumulated size),\n * the snippet is **empty**: a payload slice on a public error field would leak a\n * field-body slice of clinical content. Callers constructing this directly are\n * responsible for the same discipline.\n */\n readonly snippet: Buffer;\n\n /**\n * Construct an MLLP framing error.\n *\n * @param code - The stable warning code classifying the violation.\n * @param byteOffset - Absolute stream byte offset where the violation was detected.\n * @param snippet - Raw bytes copied from around the anomaly (capped to 64 bytes).\n * @param message - Optional human-readable override message.\n */\n constructor(\n code: WarningCode,\n byteOffset: number,\n /** Raw bytes around the anomaly. Capped to MAX_SNIPPET_BYTES (64). */\n snippet: Buffer,\n message?: string,\n ) {\n super(message ?? `MLLP framing error: ${code} at byte offset ${byteOffset}`);\n this.code = code;\n this.byteOffset = byteOffset;\n // Copy and cap, error must outlive source buffer; never store a view.\n this.snippet = Buffer.from(snippet.subarray(0, MAX_SNIPPET_BYTES));\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, MllpFramingError);\n }\n }\n}\n","/**\n * MLLP warning code registry and `MllpWarning` factory.\n *\n * `WarningCode` is a **stable public API**. Renaming or removing a code is a breaking change.\n * New codes may only be added in minor versions.\n *\n * @example\n * ```typescript\n * import { createWarning, type MllpWarning, type WarningCode } from '@cosyte/mllp';\n * const w: MllpWarning = createWarning('MLLP_EMPTY_PAYLOAD', 128, 'Empty payload between VT and FS');\n * ```\n *\n * @packageDocumentation\n */\n\n/**\n * Union of all stable MLLP warning codes.\n *\n * These codes are a **public API**, they appear in `onWarning` handlers, log pipelines,\n * monitoring dashboards, and error messages. Renaming is a breaking change.\n *\n * @example\n * ```typescript\n * const code: WarningCode = 'MLLP_FS_WITHOUT_CR';\n * ```\n */\nexport type WarningCode =\n | \"MLLP_MISSING_LEADING_VT\"\n | \"MLLP_FS_WITHOUT_CR\"\n | \"MLLP_LF_AFTER_FS\"\n | \"MLLP_LEADING_WHITESPACE\"\n | \"MLLP_TRAILING_BYTES\"\n | \"MLLP_PAYLOAD_CONTAINS_VT\"\n | \"MLLP_PAYLOAD_CONTAINS_FS\"\n | \"MLLP_EMPTY_PAYLOAD\"\n | \"MLLP_FRAME_TOO_LARGE\"\n | \"MLLP_ACK_UNMATCHED_CONTROL_ID\"\n | \"MLLP_ACK_AFTER_TIMEOUT\";\n\n/**\n * A frozen warning object emitted when the decoder tolerates a framing deviation.\n *\n * `connectionId` is `undefined` when emitted by a standalone `FrameReader`.\n * `Connection` enriches it to the real UUIDv4 before forwarding upstream.\n *\n * @example\n * ```typescript\n * const reader = new FrameReader({\n * onFrame: (p) => process(p),\n * onWarning: (w: MllpWarning) => logger.warn(w),\n * allowFsOnly: true,\n * });\n * ```\n */\nexport interface MllpWarning {\n readonly code: WarningCode;\n /**\n * Stable human-readable description. It carries structural facts (a byte\n * offset, an accumulated size) and **never a run of payload content**.\n *\n * Two codes carry exactly one input byte, and it is worth knowing which:\n * `MLLP_MISSING_LEADING_VT` and `MLLP_FS_WITHOUT_CR` render the hex of the\n * single byte found where a framing byte was expected, and on a stream that\n * omits its leading `VT` that byte is the first byte of the unframed content.\n * One byte, never a run, the same bound `MllpFramingError.snippet` carries. If\n * that is more than your threat model allows, log `code` and `byteOffset`\n * rather than `message`.\n *\n * The ACK-correlation codes are stricter: their text comes from a frozen\n * registry and does not vary with the wire at all.\n */\n readonly message: string;\n /** Absolute stream byte offset where the anomaly was detected. */\n readonly byteOffset: number;\n /**\n * Connection identifier. `undefined` at the framing layer;\n * enriched by `Connection` before emitting upstream.\n */\n readonly connectionId: string | undefined;\n /** Wall-clock time at point of emission. */\n readonly timestamp: Date;\n}\n\n/**\n * Callback type for receiving MLLP framing warnings.\n *\n * @example\n * ```typescript\n * const onWarning: OnWarning = (w) => logger.warn({ code: w.code, offset: w.byteOffset });\n * ```\n */\nexport type OnWarning = (warning: MllpWarning) => void;\n\n/**\n * Create a frozen `MllpWarning` object. The returned object is `Object.freeze()`'d\n * so subscribers cannot mutate shared warning state.\n *\n * `connectionId` is always `undefined` here; `Connection` enriches it via\n * `{ ...w, connectionId: this.connectionId }` then re-freezes.\n *\n * @example\n * ```typescript\n * const w = createWarning('MLLP_EMPTY_PAYLOAD', 64, 'Empty payload between VT and FS');\n * // w.connectionId === undefined\n * ```\n */\nexport function createWarning(code: WarningCode, byteOffset: number, message: string): MllpWarning {\n return Object.freeze<MllpWarning>({\n code,\n message,\n byteOffset,\n connectionId: undefined,\n timestamp: new Date(),\n });\n}\n","/**\n * MLLP frame encoder, canonical `VT + payload + FS + CR` wrapping.\n *\n * The encoder is a **strict emitter** (Postel's Law: conservative on write).\n * There is no option to loosen the output format, it always emits canonical framing.\n * Tolerance for delimiter bytes in the payload is available via `allowDelimiterBytesInPayload`,\n * which passes bytes through verbatim and emits an `MllpWarning` for each offending byte.\n *\n * @example\n * ```typescript\n * import { encodeFrame } from '@cosyte/mllp';\n * const frame = encodeFrame(Buffer.from('MSH|^~\\\\&|...'));\n * // frame[0] === 0x0B (VT), frame[frame.length - 2] === 0x1C (FS), frame[frame.length - 1] === 0x0D (CR)\n * ```\n *\n * @packageDocumentation\n */\n\nimport { VT, FS, CR } from \"./constants.js\";\nimport { MllpFramingError } from \"./error.js\";\nimport { createWarning } from \"./registry.js\";\nimport type { MllpWarning } from \"./registry.js\";\n\n/**\n * Options for {@link encodeFrame}.\n *\n * @example\n * ```typescript\n * const frame = encodeFrame(payload, {\n * allowDelimiterBytesInPayload: true,\n * onWarning: (w) => logger.warn(w),\n * });\n * ```\n */\nexport interface EncoderOptions {\n /**\n * When `true`, VT (0x0B) or FS (0x1C) bytes within the payload are preserved\n * verbatim in the output frame instead of throwing `MllpFramingError`.\n * An `MllpWarning` is emitted per offending byte if `onWarning` is provided.\n *\n * Default: `false` (strict, throws on delimiter bytes).\n */\n allowDelimiterBytesInPayload?: boolean;\n\n /**\n * Called for each offending delimiter byte when `allowDelimiterBytesInPayload` is `true`.\n * Invocation is wrapped in try/catch, a throwing handler does not interrupt encoding.\n *\n * @example\n * ```typescript\n * const frame = encodeFrame(payload, {\n * allowDelimiterBytesInPayload: true,\n * onWarning: (w) => logger.warn({ code: w.code, offset: w.byteOffset }),\n * });\n * ```\n */\n onWarning?: (w: MllpWarning) => void;\n}\n\n/**\n * Encode `payload` in canonical MLLP framing: `VT (0x0B) + payload + FS (0x1C) + CR (0x0D)`.\n *\n * Strict by default: throws `MllpFramingError` if the payload contains VT or FS bytes.\n * Set `{ allowDelimiterBytesInPayload: true }` to pass those bytes through verbatim\n * and receive an `MllpWarning` per offending byte via `onWarning` instead.\n *\n * The returned Buffer has length `payload.length + 3`. Bytes at positions 1 through\n * `payload.length` are an exact copy of the payload (independent of the input buffer).\n *\n * @throws {MllpFramingError} with code `MLLP_PAYLOAD_CONTAINS_VT` when payload contains\n * byte `0x0B` and `allowDelimiterBytesInPayload` is `false` (default).\n * @throws {MllpFramingError} with code `MLLP_PAYLOAD_CONTAINS_FS` when payload contains\n * byte `0x1C` and `allowDelimiterBytesInPayload` is `false` (default).\n *\n * @example\n * ```typescript\n * import { encodeFrame } from '@cosyte/mllp';\n *\n * // Strict (default), throws on delimiter bytes in payload\n * const frame = encodeFrame(Buffer.from('MSH|^~\\\\&|SEND|FAC|RECV|FAC|...'));\n * socket.write(frame);\n *\n * // Tolerant, passes delimiter bytes through with a warning\n * const frame2 = encodeFrame(dirtyPayload, {\n * allowDelimiterBytesInPayload: true,\n * onWarning: (w) => logger.warn(w),\n * });\n * ```\n */\nexport function encodeFrame(payload: Buffer, opts?: EncoderOptions): Buffer {\n const allow = opts?.allowDelimiterBytesInPayload === true;\n const onWarning = opts?.onWarning;\n\n // Scan for delimiter bytes, O(n) but required to ensure correct framing (FRAME-03).\n for (let i = 0; i < payload.length; i++) {\n const byte = payload[i];\n if (byte === undefined) break; // noUncheckedIndexedAccess guard\n\n if (byte === VT || byte === FS) {\n const code = byte === VT ? \"MLLP_PAYLOAD_CONTAINS_VT\" : \"MLLP_PAYLOAD_CONTAINS_FS\";\n\n if (!allow) {\n // PHI: the snippet must NOT carry the surrounding payload bytes, that is a\n // field-body slice of clinical content on a public error field (MLLP-9 PHI\n // audit, mirroring the decoder's MLLP_FRAME_TOO_LARGE fix). The offending byte\n // is itself a framing delimiter (VT/FS, a control byte, never PHI); the `code`\n // already names which and the offset is on the error, so the snippet is just\n // that one boundary byte.\n throw new MllpFramingError(code, i, Buffer.from([byte]));\n }\n\n // Tolerant path: emit warning and continue, bytes pass through verbatim.\n if (onWarning !== undefined) {\n const byteHex = byte === VT ? \"0x0B\" : \"0x1C\";\n const warning = createWarning(\n code,\n i,\n `Payload contains ${byte === VT ? \"VT\" : \"FS\"} byte (${byteHex}) at offset ${i}; frame will be ambiguous`,\n );\n try {\n onWarning(warning);\n } catch {\n // WARN-06: a throwing handler must not interrupt encoding.\n }\n }\n }\n }\n\n // Allocate output: VT + payload + FS + CR, canonical, immutable shape (FRAME-03).\n // allocUnsafe is safe here because all bytes are set before the buffer is returned.\n const frame = Buffer.allocUnsafe(payload.length + 3);\n frame[0] = VT;\n payload.copy(frame, 1);\n frame[payload.length + 1] = FS;\n frame[payload.length + 2] = CR;\n return frame;\n}\n","/**\n * Byte-level MSH-10 / MSA-2 control-ID scanners, the **single** implementation\n * of \"read the control ID off the wire\", shared by everything in this package\n * that has to agree on what a control ID *is*.\n *\n * Three call sites depend on these bytes agreeing, and a disagreement between\n * any two of them is a lost clinical message:\n *\n * * `src/client/correlator.ts` keys its in-flight store by the MSH-10 it\n * extracts from the **outbound** frame, and looks an inbound ACK up by the\n * MSA-2 it extracts from the **ACK** frame.\n * * `src/server/ack.ts` (`buildRawAck`) echoes the inbound MSH-10 into MSA-2.\n * * `src/ack-from-hl7/build.ts` verifies, against these same scanners, that\n * the ACK it hands back really does echo MSH-10 verbatim.\n *\n * They live here, not in `client/`, because none of that is client policy.\n * It is the package's byte-level reading of HL7 v2.5.1 §2.5.4 (MSH-1 *defines*\n * the field separator) and §2.9.2.2 (MSA-2 echoes MSH-10 verbatim).\n *\n * **INTERNAL**, not part of the public API.\n *\n * @packageDocumentation\n */\n\nconst ASCII_M = 0x4d;\nconst ASCII_S = 0x53;\nconst ASCII_H = 0x48;\nconst ASCII_A = 0x41;\nconst ASCII_B = 0x42;\nconst ASCII_F = 0x46;\nconst ASCII_T = 0x54;\nconst SEGMENT_SEPARATOR_CR = 0x0d;\nconst SEGMENT_SEPARATOR_LF = 0x0a;\n\n/**\n * Decode used for MSH-10 / MSA-2 control-ID extraction: `latin1`, NOT `ascii`.\n *\n * Node's `ascii` codec masks the high bit (`byte & 0x7f`), so it is a **lossy**\n * decode of the raw HL7 v2 bytes. Two consequences, both of which land squarely\n * on ACK correlation:\n *\n * 1. **Key collision.** The correlator keys its live store, its graveyard, and\n * its ACK lookups by the extracted control-ID *string*. Under `ascii`, the\n * distinct control IDs `A\\x8B` and `A\\x0B` both decode to `A\\x0B`, so two\n * concurrently in-flight sends collapse onto one key: the second `enqueue()`\n * overwrites the first in the `Map`, and the first send's promise can never\n * be settled by its own ACK. `latin1` is a 1:1 byte↔code-unit mapping, so\n * every distinct byte string stays a distinct key.\n * 2. **Corrupted observability.** The extracted ID is what the correlator keys\n * its graveyard on, so a masked ID is a control ID that never existed on the\n * wire: a late ACK for the real one finds no graveyard entry and is reported\n * as unmatched, misdirecting exactly the operator who is trying to trace a\n * lost message. (The diagnostics themselves report the ID's byte length and\n * never the ID, so a masked ID cannot reach a log line; a masked KEY still\n * breaks correlation.)\n *\n * Reachable whenever MSH-18 declares a non-ASCII charset (e.g. `8859/1`), where\n * high-bit bytes in a control ID are legal.\n *\n * **`latin1` is the only byte-faithful decode available.** It is not merely the\n * best of several, it is the one that exists. Node's `Buffer` `latin1` codec is\n * true ISO-8859-1: a 1:1 map between the 256 byte values and U+0000–U+00FF, so\n * `Buffer.from(buf.toString(\"latin1\"), \"latin1\")` is the identity for *every*\n * byte string. Every alternative loses bytes in exactly the range that matters:\n *\n * * `ascii` masks the high bit (above).\n * * `utf8` folds every invalid sequence onto `U+FFFD`, so all high-bit bytes in\n * a non-UTF-8 payload collapse onto one key, the same collision as `ascii`.\n * * `TextDecoder(\"iso-8859-1\")`, which is what a charset-driven decode such as\n * `@cosyte/hl7`'s `parseHL7(buffer, { charset })` uses, is **not** ISO-8859-1\n * at all. The WHATWG Encoding Standard aliases the label `iso-8859-1` to\n * **windows-1252**, which maps 0x80–0x9F to typography (`0x8B` → `U+2039`,\n * `0x9C` → `U+0153`) rather than to `U+008B`/`U+009C`. Re-encoding that back\n * to bytes does not round-trip. That is precisely the byte range a high-bit\n * control ID lives in, which is why this package decodes control-ID bytes\n * itself rather than delegating to a charset-aware parser.\n *\n * The key is byte-faithful, not text: under a multi-byte charset (`UNICODE UTF-8`)\n * a control ID reads back as its `latin1` bytes. Correlation stays correct, the\n * map is injective for any charset, which is the property the key needs, and the\n * one a charset-driven decode would lose.\n *\n * @internal\n */\nexport const CONTROL_ID_ENCODING = \"latin1\" as const;\n\n/**\n * MSH-1 values we refuse to read a message with.\n *\n * MSH-1 *is* the field separator (§2.5.4), but a segment terminator (`CR`/`LF`) or\n * an MLLP framing byte (`VT`/`FS`) cannot be one: the first would end the segment it\n * is supposed to delimit, and the second cannot be written back into an ACK without\n * making the ACK unframeable (strict `encodeFrame` throws `MLLP_PAYLOAD_CONTAINS_VT`/`_FS`).\n *\n * **How a payload with such a byte reaches us.** The plainest route is that `buildRawAck` is\n * a **public export**, so a caller can hand any `Buffer` straight to it, decoder or no\n * decoder; these scanners also run on **outbound** payloads the caller supplies. But it can\n * come off the wire too: a `VT` mid-payload makes `FrameReader` discard what it accumulated\n * and start over (`MLLP_TRAILING_BYTES`), so a *delivered* payload never contains a `VT`, but\n * a delivered payload CAN contain an `FS`. Under the `allowMissingLeadingVt` tolerance a non-VT,\n * non-whitespace first byte is taken as payload byte 0, and `FS` (0x1C) is neither, so\n * `FS \"MSH…\" FS CR` delivers a payload whose byte 0 is `0x1C`. Either way the guard is\n * warranted, and neither route depends on the false premise that the decoder screens these out.\n * @internal\n */\nconst UNSAFE_FIELD_SEPARATORS: ReadonlySet<number> = new Set([\n SEGMENT_SEPARATOR_CR,\n SEGMENT_SEPARATOR_LF,\n 0x0b, // VT, MLLP start block\n 0x1c, // FS, MLLP end block\n]);\n\n/** True iff the `CR`/`LF`-delimited segment starting at `i` is an `MSH`. @internal */\nfunction isMshSegmentAt(buf: Buffer, i: number): boolean {\n return (\n i + 3 < buf.length &&\n buf[i] === ASCII_M &&\n buf[i + 1] === ASCII_S &&\n buf[i + 2] === ASCII_H &&\n !UNSAFE_FIELD_SEPARATORS.has(buf[i + 3] as number)\n );\n}\n\n/** Index of the byte after the `CR`/`LF`-delimited segment starting at `i`. @internal */\nfunction segmentEnd(buf: Buffer, i: number): number {\n let end = i;\n while (\n end < buf.length &&\n buf[end] !== SEGMENT_SEPARATOR_CR &&\n buf[end] !== SEGMENT_SEPARATOR_LF\n ) {\n end++;\n }\n return end;\n}\n\n/**\n * Locate the message's `MSH` segment: the **first** `CR`/`LF`-delimited segment whose\n * first three bytes are `MSH` and whose 4th byte is a usable field separator. Returns\n * its `[start, end)` bounds, or `null` if the payload has no such segment.\n *\n * ## Why we search rather than demand `MSH` at byte 0\n *\n * §2.5.1 does make `MSH` the first segment of a *message*, and it is tempting to read\n * that as \"byte 0 or it is not HL7\", that is what an earlier version of this module\n * did, to force its three consumers into agreement. It was a **tolerance regression**,\n * and it caused the exact harm this module exists to prevent. Two shapes reach us with\n * a perfectly good MSH-10 that is not at byte 0:\n *\n * * **A leading `CR`.** The MLLP decoder passes it straight through into the payload\n * (it special-cases only `VT`/`FS`), and real senders emit it.\n * * **A batch header.** `FHS`/`BHS` precede the `MSH` (§2.10.3).\n *\n * Under the byte-0 rule both read as \"no MSH\", so `buildRawAck` emitted a **positive\n * `AA` with an empty MSA-2 and no warning**: the sender, which keyed on the MSH-10 it\n * sent, cannot correlate it, times out, resends, and the receiver commits a\n * **duplicate clinical message**. Silently discarding a field that is *present* is the\n * worst thing a lenient reader can do, and this package's decoder is emphatically\n * lenient (Postel's Law; see CLAUDE.md).\n *\n * The consumers still agree, they simply agree at the **tolerant** fixed point rather\n * than the lossy one. Agreement was never the hard part; agreeing on the *right* answer\n * is.\n * @internal\n */\nfunction findMshSegment(buf: Buffer): { start: number; end: number } | null {\n let segStart = 0;\n while (segStart < buf.length) {\n const end = segmentEnd(buf, segStart);\n if (isMshSegmentAt(buf, segStart)) return { start: segStart, end };\n // Advance past this segment's terminator bytes (handles `CRLF` as one break).\n let next = end;\n while (\n next < buf.length &&\n (buf[next] === SEGMENT_SEPARATOR_CR || buf[next] === SEGMENT_SEPARATOR_LF)\n ) {\n next++;\n }\n if (next === segStart) return null; // no progress, malformed\n segStart = next;\n }\n return null;\n}\n\n/**\n * True iff the `CR`/`LF`-delimited segment starting at `i` is an HL7 **batch/file\n * envelope** header, `FHS`, `BHS`, `BTS`, or `FTS` (§2.10.3). Matched on the\n * three uppercase ASCII segment-ID bytes only; the 4th byte (the separator) is not\n * inspected, because the mere *presence* of a batch segment is what disqualifies a\n * single-message ACK, regardless of that segment's own delimiter. @internal\n */\nfunction isBatchEnvelopeSegmentAt(buf: Buffer, i: number): boolean {\n if (i + 2 >= buf.length) return false;\n const a = buf[i] as number;\n const b = buf[i + 1] as number;\n const c = buf[i + 2] as number;\n // FHS / FTS\n if (a === ASCII_F && ((b === ASCII_H && c === ASCII_S) || (b === ASCII_T && c === ASCII_S))) {\n return true;\n }\n // BHS / BTS\n if (a === ASCII_B && ((b === ASCII_H && c === ASCII_S) || (b === ASCII_T && c === ASCII_S))) {\n return true;\n }\n return false;\n}\n\n/**\n * True iff the payload is **not a single bare HL7 message**, it carries a batch/file\n * envelope (`FHS`/`BHS`/`BTS`/`FTS`, §2.10.3) or a **second `MSH` segment** (two or more\n * messages concatenated in one frame, a documented real-world quirk).\n *\n * ## Why this is a fail-safe refusal signal, not tolerance\n *\n * A single MSA-2 can echo exactly **one** MSH-10. A batch is a *sequence* of messages with\n * a `BTS-1` count; a concatenated frame is N messages back to back. Either way, a positive\n * `AA` that names the first control ID silently leaves messages 2..N unacknowledged, the\n * sender reads \"batch accepted\", so they are lost outright or time out and resend as\n * **duplicate clinical messages**. This predicate lets the ACK builders **downgrade to a\n * loud non-positive answer** for such a payload rather than fabricate a positive\n * disposition for messages nobody looked at.\n *\n * It does **not** parse the batch, re-base on the first `MSH`, or otherwise widen any\n * reader, it only *detects* the multi-message shape so callers can refuse it. Batch ACK\n * is its own feature; this is the refusal that keeps the door shut until it\n * is designed. A normal single message (`MSH` followed by `PID`/`PV1`/`OBR`/…) contains no\n * batch segment and exactly one `MSH`, so this returns `false` for it.\n *\n * Pure byte-level, never throws.\n *\n * @internal\n */\nexport function containsBatchOrExtraMessage(buf: Buffer): boolean {\n let mshCount = 0;\n let segStart = 0;\n while (segStart < buf.length) {\n const end = segmentEnd(buf, segStart);\n if (isMshSegmentAt(buf, segStart)) {\n mshCount++;\n if (mshCount > 1) return true;\n } else if (isBatchEnvelopeSegmentAt(buf, segStart)) {\n return true;\n }\n // Advance past this segment's terminator bytes (handles `CRLF` as one break).\n let next = end;\n while (\n next < buf.length &&\n (buf[next] === SEGMENT_SEPARATOR_CR || buf[next] === SEGMENT_SEPARATOR_LF)\n ) {\n next++;\n }\n if (next === segStart) return false; // no progress, malformed\n segStart = next;\n }\n return false;\n}\n\n/** The MSH segment, decoded and split into its fields. @internal */\nexport interface MshSegment {\n /** MSH-1, the field separator this message declares (§2.5.4). One `latin1` char. */\n readonly fieldSep: string;\n /**\n * The MSH segment split on {@link fieldSep}. `[0]` is the literal `\"MSH\"`, `[1]` is\n * MSH-2 (the encoding characters), and thereafter the index **is** the field number:\n * `[9]` is MSH-10, `[10]` is MSH-11, `[11]` is MSH-12.\n */\n readonly fields: readonly string[];\n}\n\n/**\n * Read the MSH segment of an HL7 v2 payload, **the** scan, which every consumer in\n * this package goes through.\n *\n * Pure byte-level, never throws, returns `null` for anything it cannot read (Postel's\n * Law decoder side; CLAUDE.md guardrail). It:\n *\n * * **locates** the MSH, the first `CR`/`LF`-delimited segment that starts with\n * `MSH`, rather than demanding it at byte 0, so a leading `CR` or an `FHS`/`BHS`\n * batch header cannot hide a control ID that is plainly there ({@link findMshSegment});\n * * takes the field separator from MSH-1 rather than assuming `|` (§2.5.4);\n * * **bounds the field scan at that segment's terminator**;\n * * decodes only that segment, as `latin1` (see {@link CONTROL_ID_ENCODING}), so a\n * high-bit byte survives and a 16 MB payload is not decoded to read its header.\n *\n * ## The segment terminator is load-bearing, not a detail\n *\n * An earlier version of this scan counted field separators without ever stopping at\n * the segment terminator. On a **truncated MSH**, `MSH|^~\\&|EPIC|HOSP|MIRTH|LAB\\r`,\n * which has only 6 fields, the count therefore ran *past the `CR`* and kept counting\n * separators inside the next segment. The \"MSH-10\" it returned was `PID-3`: the\n * patient's **MRN**. That value became the client's correlation key, and was carried\n * into the ACK timeout error and the `MLLP_ACK_UNMATCHED_CONTROL_ID` /\n * `MLLP_ACK_AFTER_TIMEOUT` warnings, a patient identifier in a log line, and a\n * mis-read one at that. Those diagnostics now report byte lengths only, so that\n * particular route out is closed; the reader must still be right, because a field\n * that does not exist must read as absent, never as the next segment's contents,\n * and the correlation KEY is still the value this returns.\n *\n * The two rules are independent, and both are needed. Bounding the scan is what kills\n * the PID-3 read. **Locating** the MSH (rather than demanding it at byte 0) is what\n * stops the bound from turning into a tolerance regression that silently drops a\n * control ID which is present, see {@link findMshSegment}.\n *\n * @example\n * ```typescript\n * const msh = readMshSegment(payloadBuffer);\n * // msh?.fields[9] === 'MSG00001'; msh?.fieldSep === '|'\n * ```\n *\n * @internal\n */\nexport function readMshSegment(buf: Buffer): MshSegment | null {\n const at = findMshSegment(buf);\n if (at === null) return null;\n\n // The separator is MSH-1, the 4th byte OF THE MSH SEGMENT, wherever that segment\n // begins. `findMshSegment` has already rejected an unusable one.\n const fieldSep = String.fromCharCode(buf[at.start + 3] as number);\n // `at.end` is the segment's terminator: the field split cannot reach past it.\n const fields = buf.subarray(at.start, at.end).toString(CONTROL_ID_ENCODING).split(fieldSep);\n return { fieldSep, fields };\n}\n\n/**\n * Strip **leading segment terminators only**, `CR`/`LF` bytes before the first segment.\n *\n * This is the whole of what the parser-backed ACK builder needs re-based, and it is\n * deliberately the *minimum*. `@cosyte/hl7`'s `parseHL7` requires `MSH` to be the first\n * segment and throws `NO_MSH_SEGMENT` otherwise, so a leading `CR`, which the MLLP\n * decoder passes straight through into the payload, would otherwise send `buildMllpAck`\n * down its unparseable fallback for a message whose MSH-10 is plainly readable. A leading\n * `CR`/`LF` is pure segment-terminator noise: it carries **no data**, so dropping it\n * cannot hide anything.\n *\n * ## What this deliberately does NOT do: skip an `FHS`/`BHS` batch header\n *\n * An earlier version of this function re-based on the *located `MSH`*, which also skipped\n * a batch envelope. That was a serious mistake. A batch (§2.10.3) is\n * `[FHS] { [BHS] { MSH … } [BTS] } [FTS]`, a **sequence of messages**. Re-basing on the\n * first `MSH` handed `parseHL7` message 1 and silently discarded every later `MSH`, the\n * `BTS` count, and the `FTS`. `buildMllpAck` then returned a confident positive **`AA`\n * correlated to message 1, with zero warnings**, for a batch whose messages 2..N it had\n * never looked at. The sender reads that as \"the batch is accepted\", so those messages\n * are lost outright, or time out and resend as **duplicate clinical messages**.\n *\n * Batch ACK is a real feature and is tracked separately. It is not something to arrive at\n * by accident, via a byte-offset helper, on the way to fixing something else. Until it is\n * built deliberately, an `FHS`/`BHS` envelope must fall through to `parseHL7`'s\n * `NO_MSH_SEGMENT` and out into the **warned, non-positive `AE` fallback**, a loud\n * refusal to ACK what we did not read. That is the fail-safe answer and it is what the\n * package did before this item touched it.\n *\n * @internal\n */\nexport function stripLeadingSegmentTerminators(buf: Buffer): Buffer {\n let i = 0;\n while (i < buf.length && (buf[i] === SEGMENT_SEPARATOR_CR || buf[i] === SEGMENT_SEPARATOR_LF)) {\n i++;\n }\n return i === 0 ? buf : buf.subarray(i);\n}\n\n/**\n * Extract MSH-10 (Message Control ID) from an HL7 v2 payload, the correlation key.\n *\n * A thin read off {@link readMshSegment}: `null` when the payload has no readable MSH,\n * when the MSH segment is too short to reach MSH-10, or when MSH-10 is present but\n * empty. It is **never** a value taken from another segment.\n *\n * @example\n * ```typescript\n * const id = extractMshControlId(payloadBuffer);\n * // id === 'MSG00001' | null\n * ```\n *\n * @internal\n */\nexport function extractMshControlId(buf: Buffer): string | null {\n const msh = readMshSegment(buf);\n if (msh === null) return null;\n const id = msh.fields[9];\n return id === undefined || id === \"\" ? null : id;\n}\n\n/**\n * Extract MSA-2 (acknowledged Message Control ID) from an HL7 v2 ACK payload.\n *\n * Pure byte-level scan, never throws, returns `null` for malformed input. The field\n * separator comes from the ACK's own MSH-1, read through {@link readMshSegment}, the\n * same tolerant locate every other read in this module uses, so an ACK whose `MSH` is\n * not at byte 0 is still read rather than silently discarded. The `MSA` segment is\n * then located by scanning segment boundaries (`\\r` / `\\n`).\n *\n * Decoded as `latin1` (see {@link CONTROL_ID_ENCODING}), matching both\n * {@link extractMshControlId} and `buildRawAck`'s verbatim MSH-10 → MSA-2 echo,\n * so an ACK for a high-bit control ID looks up the key the send actually\n * enqueued.\n *\n * @example\n * ```typescript\n * const acked = extractMsaControlId(ackPayloadBuffer);\n * // acked === 'MSG00001' | null\n * ```\n *\n * @internal\n */\nexport function extractMsaControlId(buf: Buffer): string | null {\n // MSH-1 establishes the separator for the whole message, including its MSA.\n const msh = readMshSegment(buf);\n if (msh === null) return null;\n const fieldSep = msh.fieldSep.charCodeAt(0);\n const end = buf.length;\n let segStart = 0;\n while (segStart < end) {\n let segEnd = segStart;\n while (\n segEnd < end &&\n buf[segEnd] !== SEGMENT_SEPARATOR_CR &&\n buf[segEnd] !== SEGMENT_SEPARATOR_LF\n ) {\n segEnd++;\n }\n if (\n segEnd - segStart >= 4 &&\n buf[segStart] === ASCII_M &&\n buf[segStart + 1] === ASCII_S &&\n buf[segStart + 2] === ASCII_A &&\n buf[segStart + 3] === fieldSep\n ) {\n // MSA fields: [0]'MSA' [1]MSA-1 [2]MSA-2 ...\n // Iterate from the first separator (segStart+3) up to and including\n // segEnd. Treat `segEnd` as a synthetic separator so the final field\n // closes cleanly even without a trailing CR.\n let fieldIndex = 0;\n let fieldStart = 0;\n for (let i = segStart + 3; i <= segEnd; i++) {\n const b = i < segEnd ? (buf[i] as number) : fieldSep;\n if (b === fieldSep || b === SEGMENT_SEPARATOR_CR || b === SEGMENT_SEPARATOR_LF) {\n fieldIndex++;\n if (fieldIndex === 2) {\n fieldStart = i + 1;\n } else if (fieldIndex === 3) {\n if (fieldStart >= i) return null; // empty MSA-2\n return buf.subarray(fieldStart, i).toString(CONTROL_ID_ENCODING);\n }\n }\n }\n return null;\n }\n // Skip segment terminator bytes to advance to the next segment.\n while (\n segEnd < end &&\n (buf[segEnd] === SEGMENT_SEPARATOR_CR || buf[segEnd] === SEGMENT_SEPARATOR_LF)\n ) {\n segEnd++;\n }\n if (segEnd === segStart) return null; // no progress, malformed\n segStart = segEnd;\n }\n return null;\n}\n","/**\n * Lazy loader for the optional `@cosyte/hl7` peer dependency.\n *\n * `@cosyte/mllp` ships **zero runtime dependencies**; `@cosyte/hl7` is an\n * optional peer dep referenced only from this `/ack-from-hl7` subpath. To keep\n * the rest of the package fully dependency-free, the peer is required lazily,\n * on first call into `ack-from-hl7`, via `createRequire`, not at module load\n * time. The public API of this subpath stays synchronous throughout.\n *\n * @example\n * ```typescript\n * import { loadHl7Peer } from '@cosyte/mllp/ack-from-hl7';\n * const hl7 = loadHl7Peer(); // throws MllpPeerMissingError if not installed\n * ```\n *\n * @packageDocumentation\n */\n\nimport { createRequire } from \"node:module\";\n\nimport type {\n ACK_CODES,\n AckCode,\n BuildAckOptions,\n FATAL_CODES,\n Hl7Message as Hl7MessageType,\n Hl7ParseError as Hl7ParseErrorType,\n} from \"@cosyte/hl7\";\n\n/**\n * The subset of the `@cosyte/hl7` runtime surface the `ack-from-hl7` adapter\n * consumes. Kept narrow and typed via `import type` (erased at compile time)\n * so the runtime dependency stays fully lazy, only the shape is known\n * statically, the values are resolved via `require()` at first call.\n *\n * @example\n * ```typescript\n * import type { Hl7Peer } from '@cosyte/mllp/ack-from-hl7';\n * function useAck(peer: Hl7Peer) {\n * return peer.buildAck;\n * }\n * ```\n */\nexport interface Hl7Peer {\n readonly parseHL7: (raw: string | Buffer) => Hl7MessageType;\n readonly buildAck: (inbound: Hl7MessageType, options: BuildAckOptions) => Hl7MessageType;\n readonly detectAckMode: (inbound: Hl7MessageType) => \"original\" | \"enhanced\";\n readonly buildMessage: (init: { readonly type: string }) => Hl7MessageType;\n /**\n * The peer's own `Hl7Message` class. Exposed (not just used as a type) so\n * callers can detect a cross-realm instance, see the dual-package-hazard\n * note on `resolveInbound` in `build.ts`. Typed as `abstract new` (rather\n * than a concrete constructor signature) purely so TypeScript accepts it as\n * an `instanceof` right-hand side without also implying callers can `new`\n * it directly with arbitrary args.\n */\n readonly Hl7Message: abstract new (...args: never[]) => Hl7MessageType;\n readonly Hl7ParseError: typeof Hl7ParseErrorType;\n readonly FATAL_CODES: typeof FATAL_CODES;\n readonly ACK_CODES: typeof ACK_CODES;\n /**\n * The peer's fail-safe downgrade primitive (`AA`→`AE`, `CA`→`CE`, everything\n * else unchanged), the single upstream source of truth for the pair; this\n * adapter never carries its own copy of the mapping.\n */\n readonly downgradePositiveAck: (code: AckCode) => AckCode;\n}\n\n/** The subpath's own name, used in {@link MllpPeerMissingError}'s message. */\nconst SUBPATH_NAME = \"@cosyte/mllp/ack-from-hl7\";\n\n/** The peer package name, as seen by Node module resolution. */\nconst PEER_PACKAGE_NAME = \"@cosyte/hl7\";\n\n/** Node error codes that indicate a module could not be found at all. */\nconst MODULE_NOT_FOUND_CODES: ReadonlySet<string> = new Set([\n \"ERR_MODULE_NOT_FOUND\",\n \"MODULE_NOT_FOUND\",\n]);\n\n/**\n * Thrown when `@cosyte/hl7`, the optional peer dependency required by\n * `@cosyte/mllp/ack-from-hl7`, cannot be resolved at runtime.\n *\n * `@cosyte/hl7` is declared as an optional peer dependency, so a consumer of\n * the root `@cosyte/mllp` entry point is never forced to install it. Only\n * code that imports from the `/ack-from-hl7` subpath needs it, and only once\n * that code actually runs (the loader is lazy).\n *\n * @example\n * ```typescript\n * import { buildAckAA, MllpPeerMissingError } from '@cosyte/mllp/ack-from-hl7';\n * try {\n * buildAckAA(inboundBuffer);\n * } catch (err) {\n * if (err instanceof MllpPeerMissingError) {\n * console.error(err.message); // explains how to install @cosyte/hl7\n * }\n * }\n * ```\n */\nexport class MllpPeerMissingError extends Error {\n override readonly name = \"MllpPeerMissingError\" as const;\n\n /** Stable machine-readable code identifying this error class. */\n readonly code = \"MLLP_PEER_MISSING\" as const;\n\n /**\n * Construct an `MllpPeerMissingError`.\n *\n * @param cause - The original module-resolution error, preserved for debugging.\n */\n constructor(cause: unknown) {\n super(\n `${PEER_PACKAGE_NAME} is required by ${SUBPATH_NAME} but is not installed. ` +\n `${PEER_PACKAGE_NAME} is an optional peer dependency of @cosyte/mllp, install it ` +\n `alongside @cosyte/mllp to use ${SUBPATH_NAME}: \\`npm install ${PEER_PACKAGE_NAME}\\` ` +\n `(or the equivalent for your package manager).`,\n { cause },\n );\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, MllpPeerMissingError);\n }\n }\n}\n\n/** Cached peer module, populated on first successful {@link loadHl7Peer} call. */\nlet cachedPeer: Hl7Peer | undefined;\n\n/**\n * Matches the *quoted unresolvable module id* in Node's resolution-failure\n * messages, `Cannot find module '@cosyte/hl7'` (CJS) / `Cannot find package\n * '@cosyte/hl7'` (ESM). Anchoring on the quoted id (rather than a bare\n * substring test) keeps a module-not-found thrown from *inside* `@cosyte/hl7`\n * whose message names a different module but whose require-stack still\n * contains the peer's path, propagating unchanged: that is a real bug in the\n * installed peer, not a missing peer.\n * @internal\n */\nconst PEER_UNRESOLVABLE_RE = /Cannot find (?:module|package) '@cosyte\\/hl7'/;\n\n/**\n * True iff `err` is a Node \"module could not be resolved\" error specifically\n * for the `@cosyte/hl7` peer package itself.\n * @internal\n */\nfunction isPeerModuleNotFound(err: unknown): boolean {\n if (typeof err !== \"object\" || err === null) return false;\n const code = (err as { code?: unknown }).code;\n if (typeof code !== \"string\" || !MODULE_NOT_FOUND_CODES.has(code)) return false;\n const message = (err as { message?: unknown }).message;\n return typeof message === \"string\" && PEER_UNRESOLVABLE_RE.test(message);\n}\n\n/**\n * Load the `@cosyte/hl7` peer module, caching the result for subsequent\n * calls. The public adapter functions in `build.ts` call this internally,\n * most consumers never call it directly.\n *\n * @param requireFn - Injectable `require`-like function, used by tests to\n * simulate a missing peer without actually uninstalling it. Defaults to a\n * real `createRequire(import.meta.url)`.\n * @internal\n *\n * @example\n * ```typescript\n * import { loadHl7Peer } from '@cosyte/mllp/ack-from-hl7';\n * const hl7 = loadHl7Peer();\n * const ack = hl7.buildAck(hl7.parseHL7(raw), { code: \"AA\" });\n * ```\n */\nexport function loadHl7Peer(requireFn?: (id: string) => unknown): Hl7Peer {\n if (cachedPeer !== undefined && requireFn === undefined) {\n return cachedPeer;\n }\n\n const doRequire = requireFn ?? createRequire(import.meta.url);\n\n let mod: unknown;\n try {\n mod = doRequire(PEER_PACKAGE_NAME);\n } catch (err) {\n if (isPeerModuleNotFound(err)) {\n throw new MllpPeerMissingError(err);\n }\n throw err;\n }\n\n const peer = mod as Hl7Peer;\n if (requireFn === undefined) {\n cachedPeer = peer;\n }\n return peer;\n}\n","/**\n * `buildMllpAck`, a THIN transport adapter over `@cosyte/hl7`'s `buildAck`.\n *\n * `@cosyte/hl7` owns ACK **content** (MSH/MSA/ERR construction, the fail-safe\n * no-correlation downgrade); this module owns **transport policy**: accepting\n * raw inbound bytes (or an already-parsed message), tolerating an unparseable\n * inbound without ever fabricating a positive disposition, and framing the\n * result into a ready-to-write MLLP `Buffer` via `encodeFrame`.\n *\n * @example\n * ```typescript\n * import { buildAckAA } from '@cosyte/mllp/ack-from-hl7';\n * const { frame } = buildAckAA(inboundBuffer);\n * socket.write(frame);\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { AckCode, AckErrorDetail, AckMode, Hl7Message } from \"@cosyte/hl7\";\n\nimport { encodeFrame } from \"../framing/encoder.js\";\nimport {\n extractMsaControlId,\n extractMshControlId,\n stripLeadingSegmentTerminators,\n} from \"../internal/control-id.js\";\n\nimport { loadHl7Peer } from \"./peer.js\";\nimport type { Hl7Peer } from \"./peer.js\";\n\n/**\n * A single ACK-generation warning, surfaced by {@link buildMllpAck}.\n *\n * `code` is either an upstream `@cosyte/hl7` warning code passed through\n * verbatim (e.g. `\"ACK_NO_CORRELATION_ID\"`) or the one `mllp`-owned code,\n * {@link MLLP_ACK_INBOUND_UNPARSEABLE}.\n *\n * @example\n * ```typescript\n * import { buildAckAA } from '@cosyte/mllp/ack-from-hl7';\n * const { warnings } = buildAckAA(inbound);\n * for (const w of warnings) console.warn(w.code, w.message);\n * ```\n */\nexport interface MllpAckWarning {\n readonly code: string;\n readonly message: string;\n}\n\n/**\n * Warning code emitted when the inbound message could not be parsed at all\n * (a fatal `Hl7ParseError`) and {@link buildMllpAck} fell back to a minimal,\n * uncorrelated ACK rather than fabricating a positive disposition.\n *\n * @example\n * ```typescript\n * import { MLLP_ACK_INBOUND_UNPARSEABLE, buildAckAA } from '@cosyte/mllp/ack-from-hl7';\n * const { warnings } = buildAckAA(\"not hl7 at all\");\n * warnings.some((w) => w.code === MLLP_ACK_INBOUND_UNPARSEABLE); // true\n * ```\n */\nexport const MLLP_ACK_INBOUND_UNPARSEABLE = \"MLLP_ACK_INBOUND_UNPARSEABLE\";\n\n/**\n * Warning code emitted when the built ACK's MSA-2 bytes are **not** byte-identical\n * to the inbound MSH-10 bytes, i.e. the ACK does not echo the message control ID\n * verbatim, as HL7 v2.5.1 §2.9.2.2 requires.\n *\n * This is a **correlation failure**, and the reason it is a loud warning rather\n * than a silent success: the sender keys its in-flight store on the MSH-10 bytes\n * it put on the wire. An ACK whose MSA-2 differs by even one byte will not match\n * that key, so the send is never settled → ACK timeout → resend → **duplicate\n * clinical message**. Emitting the ACK anyway (rather than throwing) is the\n * fail-safe choice, a mismatched ACK still tells the peer *something*, but the\n * mismatch must never pass unremarked.\n *\n * `buildMllpAck` verifies this on every build, against the very same byte-level\n * scanners the `@cosyte/mllp` client uses to correlate (`src/internal/control-id.ts`).\n *\n * **The verification is meaningful for a `Buffer` inbound, and only for a `Buffer`.**\n * A `Buffer` *is* the wire bytes, so comparing the ACK to it is a real byte-level check\n * and a clean result means a `@cosyte/mllp` sender will match this ACK. A\n * `string`/`Hl7Message` inbound has **already been decoded** before this module sees it:\n * the only thing left to compare against is that same text, encoded with the same codec,\n * so the codec cancels on both sides and a codec-induced mismatch is **structurally\n * invisible**, it warns about nothing. See {@link BuildMllpAckOptions.encoding}. Pass a\n * `Buffer` if you want this guarantee to mean anything.\n *\n * Two known ways to provoke it, both documented in the package limitations:\n *\n * 1. Overriding {@link BuildMllpAckOptions.encoding} to a codec that cannot\n * round-trip the inbound bytes (`\"utf8\"`/`\"ascii\"` against a high-bit,\n * 8859/1-charset control ID). The default never does this.\n * 2. An inbound that declares **non-default encoding characters** (MSH-1/MSH-2).\n * `@cosyte/hl7`'s `buildMessage` always emits the HL7 default `|^~\\&`, so an\n * inbound MSH-10 of `ID#X` under a `#` component separator is re-delimited to\n * `ID^X` in MSA-2, different bytes, unmatchable key. Use `buildRawAck` (root\n * export), which echoes the inbound's own MSH-1/MSH-2, if you must serve such\n * a peer.\n *\n * @example\n * ```typescript\n * import { MLLP_ACK_CONTROL_ID_NOT_VERBATIM, buildAckAA } from '@cosyte/mllp/ack-from-hl7';\n * const { warnings } = buildAckAA(inbound, { encoding: \"ascii\" });\n * if (warnings.some((w) => w.code === MLLP_ACK_CONTROL_ID_NOT_VERBATIM)) {\n * // this ACK will NOT correlate at the sender, investigate before shipping it\n * }\n * ```\n */\nexport const MLLP_ACK_CONTROL_ID_NOT_VERBATIM = \"MLLP_ACK_CONTROL_ID_NOT_VERBATIM\";\n\n/**\n * Warning code emitted when the inbound was handed in as **text** (a `string` or an\n * already-parsed `Hl7Message`) rather than as raw `Buffer` bytes, and the ACK's MSA-2\n * carries a **non-ASCII** control ID, a combination whose byte-verbatim echo\n * (HL7 v2.5.1 §2.9.2.2) `buildMllpAck` **cannot verify**, and may silently be breaking.\n *\n * ## Why a distinct code from {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}\n *\n * `MLLP_ACK_CONTROL_ID_NOT_VERBATIM` is a *proof of failure*: on a `Buffer` inbound the\n * wire bytes are in hand, the check compares MSA-2 against them, and it fires only when\n * they provably differ. This code is the opposite, a *confession that the proof cannot\n * be run*. A `string`/`Hl7Message` inbound was decoded from its wire bytes **before** this\n * module ever saw it, so the only thing left to compare the ACK against is that same text\n * re-encoded with the same codec: the codec cancels on both sides and the verbatim check\n * becomes a tautology that always passes (see {@link verifyVerbatimEcho}). It would report\n * clean even for `buildAckAA(payload.toString(\"latin1\"))` on a high-bit control ID, where\n * a `latin1`-decoded `0x8B` is re-encoded as the two `utf8` bytes `0xC2 0x8B`, a *different*\n * control ID the sender cannot correlate (timeout → resend → **duplicate clinical message**).\n *\n * The guard cannot be grown to catch that; by the time text arrives the bytes are gone.\n * The API can, and does: rather than let the text path pass a codec-sensitive control ID\n * off as verified, `buildMllpAck` refuses to stay silent about it. Whenever the ACK's MSA-2\n * control ID holds a code unit outside `0x00`–`0x7F` on a text inbound, the range where the\n * codec choice is load-bearing, it emits this warning. An all-ASCII control ID round-trips\n * identically under every codec, so the common case stays quiet; a non-ASCII one is flagged as\n * *unverifiable*, not as *known-broken*, because from a decoded string the two are genuinely\n * indistinguishable.\n *\n * The check reads the **pre-encoding code units** of MSA-2, not the emitted bytes, so a lossy\n * `{ encoding: \"ascii\" }` override, which truncates a code unit to its low 8 bits (`str -> byte &\n * 0xFF`) and so masks a code unit above `0xFF` *into* the ASCII byte range on the way out (`U+0153`\n * -> `0x53`), cannot slip a corrupted control ID past it silently.\n * The strongly-discouraged text-plus-override path is covered for the same reason the default `utf8`\n * text path is; the `Buffer` overload remains the answer.\n *\n * **The fix the warning points at is the `Buffer`-first API rule.** Pass the raw payload,\n * the bytes the server handed you, and you get the real byte-level check (and, if it breaks,\n * the definite {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}) instead of this \"cannot tell\".\n *\n * Like its sibling, this warning carries byte **lengths** only, never the field bytes\n * (MSH-10 is inbound payload content and a warning goes to a log; see {@link verifyVerbatimEcho}).\n *\n * @example\n * ```typescript\n * import { MLLP_ACK_CONTROL_ID_UNVERIFIABLE, buildAckAA } from '@cosyte/mllp/ack-from-hl7';\n * const { warnings } = buildAckAA(payload.toString(\"latin1\")); // text inbound, high-bit id\n * if (warnings.some((w) => w.code === MLLP_ACK_CONTROL_ID_UNVERIFIABLE)) {\n * // pass the raw Buffer instead, the echo cannot be verified from decoded text\n * }\n * ```\n */\nexport const MLLP_ACK_CONTROL_ID_UNVERIFIABLE = \"MLLP_ACK_CONTROL_ID_UNVERIFIABLE\";\n\n/**\n * Byte-faithful codec for raw-`Buffer` inbound, the default (HL7 v2.5.1 §2.9.2.2).\n *\n * `latin1` is Node's true ISO-8859-1: a 1:1 map between the 256 byte values and\n * U+0000–U+00FF, so `Buffer.from(buf.toString(\"latin1\"), \"latin1\")` is the identity\n * for **every** byte string. It is the only codec for which that holds, which makes\n * it the only one under which MSA-2 can echo an arbitrary inbound MSH-10 verbatim.\n * See `src/internal/control-id.ts` for why `ascii`, `utf8`, and a charset-driven\n * `TextDecoder` each lose bytes in exactly the range a high-bit control ID lives in.\n * @internal\n */\nconst BYTES_INBOUND_ENCODING: BufferEncoding = \"latin1\";\n\n/**\n * Codec for `string` / `Hl7Message` inbound, JS-native `utf8`.\n *\n * When the caller hands us text, **they** already chose the decode; we cannot know\n * which codec produced those code units, so we re-encode with the JS-native one and\n * let them override. (For raw bytes we own the decode, so we can guarantee the\n * round-trip, see {@link BYTES_INBOUND_ENCODING}.)\n * @internal\n */\nconst TEXT_INBOUND_ENCODING: BufferEncoding = \"utf8\";\n\n/**\n * The codecs {@link buildMllpAck} accepts as the ACK **serialization** codec, for a\n * `Buffer` inbound and a `string` / `Hl7Message` inbound alike. The character-faithful\n * single-byte / UTF-8 codecs, under which `Buffer.from(ackText, codec)` serializes the\n * ACK's characters to a byte stream a peer reads back as HL7: `utf8`/`utf-8` (the default\n * for text), `ascii`, and `latin1`/`binary` (the default for bytes).\n *\n * Every *other* `BufferEncoding` is a **non-text** codec that does not serialize\n * characters at all, `base64`/`base64url`/`hex` reinterpret the ACK **string** as\n * encoded data and decode it to unrelated bytes; `utf16le`/`ucs2` interleave a NUL after\n * every ASCII byte. Either way the emitted frame is wholesale garbage, so they are\n * rejected on **every** input shape (see {@link assertSerializableAckEncoding}).\n *\n * This set is exactly the legitimate \"byte-level codec\" escape hatch a `Buffer` inbound\n * offers: a receiving system that demands `ascii` or `latin1` bytes is served here; a\n * non-text codec never is, because it cannot serialize a readable HL7 ACK on any path.\n * @internal\n */\nconst SERIALIZABLE_ACK_ENCODINGS: ReadonlySet<string> = new Set([\n \"utf8\",\n \"utf-8\",\n \"ascii\",\n \"latin1\",\n \"binary\",\n]);\n\n/**\n * Options for {@link buildMllpAck} and its six convenience wrappers.\n *\n * @example\n * ```typescript\n * import type { BuildMllpAckOptions } from '@cosyte/mllp/ack-from-hl7';\n * const opts: BuildMllpAckOptions = { code: \"AE\", error: { conditionCode: \"101\" } };\n * ```\n */\nexport interface BuildMllpAckOptions {\n /** Required acknowledgment disposition to emit in MSA-1 (HL7 Table 0008). */\n readonly code: AckCode;\n /** Optional error detail; one entry emits one ERR segment. */\n readonly error?: AckErrorDetail | readonly AckErrorDetail[];\n /**\n * The codec used to move the ACK between bytes and text, **both** directions,\n * as a single symmetric choice. It decodes a raw-`Buffer` `inbound` on the way in\n * and encodes the serialized ACK on the way out, so the ACK is emitted in the same\n * codec the inbound was read in.\n *\n * That symmetry is the whole point, and it is what makes MSA-2 echo MSH-10\n * **verbatim** (HL7 v2.5.1 §2.9.2.2). Decoding in one codec and encoding in another\n * corrupts every non-ASCII byte it round-trips: a `latin1`-decoded `0x8B` re-encoded\n * as `utf8` goes out as the two bytes `0xC2 0x8B`, which is a *different control ID*\n * and the sender, which keyed its in-flight store on `0x8B`, cannot match the ACK.\n *\n * **Defaults, by inbound type:**\n *\n * - `Buffer` → **`\"latin1\"`**. We own the decode, and `latin1` is the only codec\n * that round-trips arbitrary bytes losslessly, so MSA-2 is byte-verbatim for any\n * charset, including an MSH-18 of `8859/1` with high-bit bytes in the control ID.\n * - `string` / `Hl7Message` → **`\"utf8\"`**. The caller already decoded; we re-encode\n * with the JS-native codec.\n *\n * **Override with care, and only on a `Buffer`.** Any value other than `\"latin1\"` on a\n * raw-`Buffer` inbound gives up the verbatim guarantee for non-ASCII bytes (`\"ascii\"`\n * masks the high bit; `\"utf8\"` folds invalid sequences onto `U+FFFD`). On a `Buffer`,\n * `buildMllpAck` **checks** the result and emits {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}\n * if the echo did not survive, so a bad override there is loud rather than silent, but\n * it is still a broken ACK. Set this only when the receiving system genuinely demands a\n * specific codec.\n *\n * **On a `string` / `Hl7Message` inbound the verbatim check cannot see a codec problem,\n * so a *different* warning does the honest thing instead.** The wire bytes were decoded to\n * text *before* this function saw them, so there is nothing left to compare the ACK against\n * except that same text: the codec cancels on both sides and {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}\n * can never fire. Concretely, `buildAckAA(payload.toString(\"latin1\"))` on a high-bit control ID\n * (`0x8B`, legal under an `MSH-18` of `8859/1`) re-encodes it as the two `utf8` bytes `0xC2 0x8B`,\n * emitting a **different** control ID the sender cannot correlate. The verbatim check is blind to\n * that, but the byte-safety of a text inbound cannot be *proven*, so `buildMllpAck` refuses to\n * pass it off as verified: whenever the emitted MSA-2 holds a non-ASCII byte on a text inbound it\n * emits {@link MLLP_ACK_CONTROL_ID_UNVERIFIABLE}, an explicit \"cannot verify, pass a `Buffer`\".\n * The guard is not grown to *catch* the mismatch (the bytes are gone by then); the API stops\n * being silent about the fact that it can't.\n *\n * **`Buffer` is the byte-safe path.** Pass the raw payload. It is what the server hands\n * you, it is what the `Buffer`-first API rule exists for, and it is the only input for\n * which the verbatim guarantee, or a proof that it broke, actually means anything.\n *\n * **Only a *text* codec is accepted, on every input shape.** This codec serializes the ACK\n * back to bytes, so it must be one that writes characters as a byte stream the peer can read as\n * HL7: `\"utf8\"`, `\"ascii\"`, `\"latin1\"`, or `\"binary\"`. A **non-text** codec,\n * `\"base64\"`/`\"base64url\"`/`\"hex\"` (which reinterpret the ACK *string* as encoded data) or\n * `\"utf16le\"`/`\"ucs2\"` (which NUL-pad every byte), emits a wholesale-garbage frame the receiver\n * cannot parse, so `buildMllpAck` **throws a `TypeError`** for it here rather than hand back an\n * unusable ACK. This applies to a `Buffer` inbound too: a\n * non-text codec there garbles the *inbound* decode into the unparseable fallback (empty MSA-2,\n * so the {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM} check never runs) and then serializes the\n * fallback ACK to garbage bytes that intermittently trip the strict frame encoder, it is never\n * the \"loud AE\" it was once documented to be. The legitimate byte-level escape hatch is\n * unchanged: a lossy **charset** override on a `Buffer` (`\"ascii\"` masking a high bit) is still\n * accepted and still caught loudly by {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM}; only the\n * categorically-non-serializing codecs are refused.\n */\n readonly encoding?: BufferEncoding;\n /**\n * Passthrough to `encodeFrame`'s `allowDelimiterBytesInPayload`. Defaults\n * to `false` (strict, throws if the serialized ACK somehow contains a\n * VT/FS byte, which a spec-clean `@cosyte/hl7` emit never does).\n */\n readonly allowDelimiterBytesInPayload?: boolean;\n}\n\n/**\n * Result of {@link buildMllpAck}, a ready-to-write MLLP frame plus the\n * metadata needed to log or assert on what was actually emitted. Frozen\n * (including the `warnings` array) so subscribers cannot mutate shared state.\n *\n * @example\n * ```typescript\n * import { buildAckAA } from '@cosyte/mllp/ack-from-hl7';\n * const ack = buildAckAA(inbound);\n * socket.write(ack.frame);\n * console.log(ack.code, ack.correlationId);\n * ```\n */\nexport interface MllpAck {\n /** `VT + payload + FS + CR`, ready to write to a socket. */\n readonly frame: Buffer;\n /** The serialized ACK, unframed. */\n readonly payload: Buffer;\n /** The built ACK `Hl7Message`. */\n readonly ack: Hl7Message;\n /** The disposition the caller asked for. */\n readonly requestedCode: AckCode;\n /** The disposition MSA-1 actually carries (post fail-safe downgrade). */\n readonly code: AckCode;\n /** Inbound MSH-10, echoed in MSA-2. `undefined` when unfindable. */\n readonly correlationId: string | undefined;\n /** Detected from inbound MSH-15/16. `undefined` when inbound unparseable. */\n readonly mode: AckMode | undefined;\n /** Warnings collected while building the ACK. */\n readonly warnings: readonly MllpAckWarning[];\n}\n\n/**\n * Validate that `code` is one of the six known HL7 Table 0008 acknowledgment\n * codes, using the peer's own `ACK_CODES` registry as the source of truth.\n * Mirrors `@cosyte/hl7`'s own `buildAck` runtime guard so JS callers (who\n * bypass the TypeScript union) get the same `TypeError` behavior.\n * @internal\n */\nfunction assertKnownAckCode(peer: Hl7Peer, code: unknown): asserts code is AckCode {\n const known: readonly string[] = Object.values(peer.ACK_CODES);\n if (typeof code !== \"string\" || !known.includes(code)) {\n throw new TypeError(\n `buildMllpAck: \\`code\\` must be a known HL7 Table 0008 acknowledgment code ` +\n `(AA/AE/AR/CA/CE/CR). Received: ${JSON.stringify(code)}.`,\n );\n }\n}\n\n/**\n * Resolve the single codec used for BOTH the inbound decode and the outbound\n * encode. An explicit `options.encoding` always wins; otherwise raw bytes get\n * the byte-faithful `latin1` and caller-supplied text gets JS-native `utf8`.\n * @internal\n */\nfunction resolveEncoding(\n inbound: Hl7Message | Buffer | string,\n options: BuildMllpAckOptions,\n): BufferEncoding {\n if (options.encoding !== undefined) return options.encoding;\n return Buffer.isBuffer(inbound) ? BYTES_INBOUND_ENCODING : TEXT_INBOUND_ENCODING;\n}\n\n/**\n * Reject a **non-text** codec as the ACK serialization codec, on **every** input shape,\n * `Buffer` and `string`/`Hl7Message` alike, loudly and before anything is emitted.\n *\n * The resolved codec serializes the built ACK back to bytes (`Buffer.from(ack.toString(),\n * codec)`). A *text* codec ({@link SERIALIZABLE_ACK_ENCODINGS}, `utf8`/`ascii`/`latin1`)\n * writes the ACK's characters as a byte stream the peer reads back as HL7. A *non-text* codec\n * does not: `base64`/`base64url`/`hex` reinterpret the ACK **string** as encoded data and\n * decode it to unrelated bytes, and `utf16le`/`ucs2` interleave a NUL after every byte, so the\n * emitted frame is wholesale garbage the receiver cannot parse. A frame nothing can read is a\n * **caller mistake**, not a runtime condition, and the honest place to report a caller mistake\n * is the boundary, not a garbage `Buffer` handed back for the caller to write to a socket and\n * discover broken a round trip later. So this throws a `TypeError` here, exactly as\n * {@link assertKnownAckCode} does for a bad `code`.\n *\n * ## Why this applies to the `Buffer` path too\n *\n * An earlier iteration scoped this to the text path, reasoning\n * that a lossy override on a `Buffer` was *already* caught loudly by the byte-level\n * {@link verifyVerbatimEcho} → {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM} check. That is true for\n * a lossy **charset** codec (`ascii` masking a high bit), but **not** for a genuinely non-text\n * one. A non-text codec garbles the *inbound* decode too: `stripLeadingSegmentTerminators(buf)\n * .toString(\"base64\" | \"hex\" | \"utf16le\" | \"ucs2\")` never yields a string that begins with\n * `MSH`, so it **always** routes to the unparseable fallback, whose MSA-2 is intentionally empty\n * {@link verifyVerbatimEcho} short-circuits on `inboundId === null` and the NOT_VERBATIM proof\n * *never runs*. The supposed safety net is not reachable. Worse, the fallback ACK is then\n * serialized with that same non-text codec: `Buffer.from(ackText, \"base64\")` decodes the ACK\n * text to random bytes that, roughly 3–4 % of the time (identically on Node 22 and Node 24,\n * this was never a runtime divergence, only a flaky draw of the fallback's generated MSH-10),\n * contain a `VT`/`FS` delimiter byte and make the strict {@link encodeFrame} throw a\n * nondeterministic `MllpFramingError`. So the non-text-codec-on-`Buffer` path is neither the\n * \"loud AE\" it was documented to be nor caught by any falsifiable check, it is an unreadable\n * frame that sometimes crashes.\n *\n * The legitimate `Buffer` escape hatch is **untouched**: every codec that can actually serialize\n * an HL7 ACK, `latin1` (the byte-verbatim default), `ascii`, `utf8`, `binary`, is in\n * {@link SERIALIZABLE_ACK_ENCODINGS} and still accepted, and a lossy charset override there is\n * still caught by {@link MLLP_ACK_CONTROL_ID_NOT_VERBATIM} exactly as before. What is rejected is\n * only the categorically-non-serializing codec, which had no valid use on any path.\n * @internal\n */\nfunction assertSerializableAckEncoding(encoding: BufferEncoding): void {\n if (SERIALIZABLE_ACK_ENCODINGS.has(encoding.toLowerCase())) return;\n throw new TypeError(\n `buildMllpAck: encoding ${JSON.stringify(encoding)} is not a serializable ACK codec. ` +\n `Serializing an ACK to a non-text codec (base64/base64url/hex/utf16le/ucs2) produces a ` +\n `wholesale-garbage frame the receiver cannot parse, on a Buffer inbound it also garbles ` +\n `the inbound decode, so it can never correlate. Use a text codec (\"utf8\", \"ascii\", or ` +\n `\"latin1\"); on a Buffer, \"latin1\" is byte-verbatim and is the default.`,\n );\n}\n\n/**\n * The inbound as bytes, for the verbatim-echo check, or `undefined` when the\n * inbound has no bytes to name.\n *\n * A `Buffer` *is* its bytes. A `string`'s bytes are that string under the\n * resolved codec. A pre-parsed `Hl7Message` has none: the caller already consumed\n * whatever bytes it came from, so there is nothing to compare MSA-2 against and\n * the check is skipped rather than invented.\n * @internal\n */\nfunction inboundBytes(\n inbound: Hl7Message | Buffer | string,\n encoding: BufferEncoding,\n): Buffer | undefined {\n if (Buffer.isBuffer(inbound)) return inbound;\n if (typeof inbound === \"string\") return Buffer.from(inbound, encoding);\n return undefined;\n}\n\n/**\n * Verify HL7 v2.5.1 §2.9.2.2 on the bytes we are about to put on the wire: the\n * ACK's MSA-2 must be byte-identical to the inbound's MSH-10.\n *\n * Deliberately checked against `src/internal/control-id.ts`, the very scanners\n * the `@cosyte/mllp` client uses to key its in-flight store and to look an ACK\n * back up. This is therefore not only a spec assertion but an end-to-end\n * correlation assertion: if it passes, a `@cosyte/mllp` sender will match this\n * ACK to its send.\n *\n * ## Its reach is bounded by what the caller gave us, and that bound is REAL\n *\n * `inboundBytes` re-encodes a `string`/`Hl7Message` inbound with the *same* codec the\n * ACK is encoded with, so on that path the codec **cancels on both sides** and this\n * check is a **tautology**: it cannot, even in principle, catch a codec-induced\n * non-verbatim echo. `buildAckAA(wire.toString(\"latin1\"))` on a `0x8B` control ID emits\n * `0xC2 0x8B` and this function returns `null`, this proof simply does not apply there.\n *\n * Do **not** try to grow *this* guard to cover the text path: by the time a `string` reaches\n * us the wire bytes are already gone, and there is nothing left to compare against. The check\n * is honest and complete for a `Buffer` inbound, the `Buffer`-first path the API rule points\n * at, and it claims nothing beyond that. The text path is not left silent, though: it is\n * handled by a *separate* signal, {@link verifyTextInboundEcho} /\n * {@link MLLP_ACK_CONTROL_ID_UNVERIFIABLE}, which does not attempt the impossible comparison,\n * it flags a non-ASCII echo on a text inbound as *unverifiable* and points the caller at the\n * `Buffer` overload. Two inputs, two checks; neither pretends to the other's certainty.\n *\n * Returns `null` when the check passes, or cannot be made at all, the inbound has no\n * readable MSH (it does not lead with `MSH`, e.g. it is still MLLP-framed), or it\n * carries no MSH-10 (the peer's own no-correlation fail-safe already warns about that).\n * We never warn on a comparison we could not actually perform.\n *\n * ## The message names NO field content, deliberately\n *\n * An earlier version hex-encoded both control IDs into the warning text, reasoning that\n * a control ID is routing metadata rather than clinical content and that an operator\n * tracing a lost message needs the bytes. That reasoning was wrong twice:\n *\n * 1. **It is not this function's call to make.** MSH-10 is inbound payload content,\n * this module's contract is that its warnings carry *never any inbound payload\n * bytes*, and a warning goes to a log, which in this domain is a place PHI must\n * not reach. A field is not safe to log merely because it is *usually* an opaque id.\n * 2. **It was demonstrably PHI.** Paired with a scanner that ran past the segment\n * terminator (since fixed, see `readMshSegment`), a truncated MSH made \"MSH-10\"\n * resolve to PID-3, and this warning rendered the patient's **MRN** in hex. The\n * scanner bug is fixed; withholding the bytes stays, as defence in depth: the next\n * such bug must not have a paved road into a log line.\n *\n * The warning says *that* the echo broke and what to compare. Nothing is lost, the\n * caller already holds both byte strings (the inbound is their own `payload`; the ACK is\n * the `MllpAck.payload` this call returns).\n * @internal\n */\nfunction verifyVerbatimEcho(\n ackPayload: Buffer,\n inboundRaw: Buffer | undefined,\n): MllpAckWarning | null {\n if (inboundRaw === undefined) return null;\n const inboundId = extractMshControlId(inboundRaw);\n if (inboundId === null) return null;\n const echoedId = extractMsaControlId(ackPayload);\n if (echoedId === inboundId) return null;\n\n // Byte LENGTHS only, never the bytes themselves. The ids are `latin1`, which is 1:1\n // with bytes, so `.length` is a byte count. A length is a shape, not content.\n const inboundLen = String(inboundId.length);\n const echoed = echoedId === null ? \"absent\" : `${String(echoedId.length)} bytes`;\n return {\n code: MLLP_ACK_CONTROL_ID_NOT_VERBATIM,\n message:\n `ACK does not echo the inbound MSH-10 verbatim (HL7 v2.5.1 §2.9.2.2): ` +\n `inbound MSH-10 is ${inboundLen} bytes, emitted MSA-2 is ${echoed}, and they differ. ` +\n `The sender keys its in-flight store on the inbound bytes, so it will NOT match this ACK ` +\n `(ACK timeout -> resend -> duplicate message). ` +\n `Field values are withheld, MSH-10 is inbound payload content and this warning goes to a log; ` +\n `compare your inbound payload against MllpAck.payload to see the bytes.`,\n };\n}\n\n/**\n * The counterpart to {@link verifyVerbatimEcho} for the path that check cannot reach:\n * a **text** inbound (`string` / `Hl7Message`), whose wire bytes are gone before this\n * module sees them.\n *\n * `verifyVerbatimEcho` is a tautology on the text path (it re-derives \"the inbound bytes\"\n * from the same text, with the same codec, that produced the ACK). So instead of comparing\n * (which cannot fail), this looks at the one property that actually signals danger: does the\n * control ID hold a code unit the codec choice could have corrupted? It reads the ACK's MSA-2\n * as its **pre-encoding code units** (`Field.text`), so any code unit `> 0x7F` is a non-ASCII,\n * codec-sensitive control ID. ASCII code units round-trip identically under `latin1`/`utf8`/`ascii`\n * alike, so an all-ASCII control ID is provably safe from any text input and stays quiet; a\n * non-ASCII one cannot be certified from decoded text and is flagged.\n *\n * It reads the pre-encode **code units**, not the emitted **bytes**, on purpose. A lossy\n * `{ encoding: \"ascii\" }` override truncates a code unit to its low 8 bits (`str -> byte & 0xFF`),\n * masking a code unit above `0xFF` *into* the ASCII byte range on the way out (`U+0153` -> `0x53`),\n * so an emitted-byte proxy would fall silent on exactly the corruption that matters.\n * The code units still carry the high bit whatever\n * the codec did to the bytes, and, since encoding ASCII code units can never yield a non-ASCII\n * byte, this is a strict superset of the emitted-non-ASCII test, so the default `utf8` text path\n * is unchanged.\n *\n * This is deliberately *unverifiable*, not *not-verbatim*: from a `string` we cannot know\n * whether the caller's decode matched our encode, so we do not claim the echo broke, only\n * that we cannot prove it held. The remedy is structural, and the message says so: pass the\n * raw `Buffer`.\n *\n * Returns `null` on a `Buffer` inbound (the verified path, {@link verifyVerbatimEcho} owns\n * it), on an inbound with no readable MSA-2 to inspect, or when that MSA-2 is pure ASCII.\n *\n * Byte **lengths** only in the message, never the bytes, same PHI discipline as its sibling.\n * @internal\n */\nfunction verifyTextInboundEcho(\n controlId: string,\n inbound: Hl7Message | Buffer | string,\n): MllpAckWarning | null {\n if (Buffer.isBuffer(inbound)) return null;\n if (controlId === \"\") return null;\n // Inspect the control ID's **pre-encoding code units**, the decoded-text form the caller\n // handed us, read straight off the built ACK's MSA-2 (`Field.text`), BEFORE it is serialized\n // to bytes. Any code unit > 0x7F is a non-ASCII, codec-sensitive control ID: on a text inbound\n // we cannot know whether the caller's decode matched our encode, so the byte-verbatim echo\n // cannot be verified.\n //\n // We check the CODE UNITS, not the emitted bytes, on purpose. A lossy `encoding` override\n // (`{ encoding: \"ascii\" }`) truncates a code unit to its low 8 bits (`str -> byte & 0xFF`), so a\n // code unit above `0xFF` is masked *into* the ASCII byte range on the way out (`U+0153` -> `0x53`),\n // and an emitted-byte proxy would go quiet on exactly the corruption that matters\n // (MLLP-ACK-ASCII-OVERRIDE-BLEED). The pre-encode code units still hold the high bit, so they see\n // it regardless of the codec, and this is a strict superset of the emitted non-ASCII case\n // (encoding ASCII code units can never produce a non-ASCII byte), so the default `utf8` text path\n // is unchanged.\n let hasNonAscii = false;\n for (let i = 0; i < controlId.length; i++) {\n if (controlId.charCodeAt(i) > 0x7f) {\n hasNonAscii = true;\n break;\n }\n }\n if (!hasNonAscii) return null;\n\n // LENGTH (code-unit count) only, never the field content itself (PHI; see `verifyVerbatimEcho`).\n return {\n code: MLLP_ACK_CONTROL_ID_UNVERIFIABLE,\n message:\n `ACK's byte-verbatim echo of MSH-10 (HL7 v2.5.1 §2.9.2.2) cannot be verified: the inbound ` +\n `was decoded text (a string or Hl7Message), not raw bytes, so its wire bytes are gone and ` +\n `the ${String(controlId.length)}-code-unit MSA-2 control ID holds a non-ASCII code unit the ` +\n `decode/encode codec could have altered (a lossy override such as ascii can even mask it into ` +\n `the ASCII byte range). It may not correlate at the sender (ACK timeout -> resend -> ` +\n `duplicate message). Pass the raw payload Buffer to get the byte-level guarantee. ` +\n `Field values are withheld, MSH-10 is inbound payload content and this warning goes to a log.`,\n };\n}\n\n/**\n * Serialize a built ACK message into a framed `MllpAck` result, encoding it with\n * the same codec the inbound was decoded with, and verifying the MSA-2 echo.\n * @internal\n */\nfunction toMllpAck(params: {\n readonly ack: Hl7Message;\n readonly requestedCode: AckCode;\n readonly code: AckCode;\n readonly correlationId: string | undefined;\n readonly mode: AckMode | undefined;\n readonly warnings: readonly MllpAckWarning[];\n readonly options: BuildMllpAckOptions;\n readonly encoding: BufferEncoding;\n /** Inbound bytes to verify the MSA-2 echo against; `undefined` skips the check. */\n readonly verifyAgainst: Buffer | undefined;\n /**\n * The original inbound, to classify the echo's byte-safety. A `Buffer` is verified\n * against its own bytes ({@link verifyVerbatimEcho}); a `string`/`Hl7Message` gets the\n * \"cannot verify\" flag ({@link verifyTextInboundEcho}). `undefined` on the unparseable\n * fallback path, which has no correlated MSA-2 to classify.\n */\n readonly inbound: Hl7Message | Buffer | string | undefined;\n}): MllpAck {\n const { ack, requestedCode, code, correlationId, mode, warnings, options, encoding } = params;\n const payload = Buffer.from(ack.toString(), encoding);\n const frame = encodeFrame(payload, {\n allowDelimiterBytesInPayload: options.allowDelimiterBytesInPayload ?? false,\n });\n\n // Two mutually exclusive echo checks, one per input shape. A `Buffer` inbound is checked\n // against its own wire bytes (a real, falsifiable comparison). A `string`/`Hl7Message` cannot\n // be, the bytes are gone, so it is flagged as unverifiable when the echoed control ID holds a\n // non-ASCII code unit. The text check reads the MSA-2 as PRE-ENCODE code units (not the emitted\n // bytes), so a lossy `ascii` override masking a high bit into the ASCII range cannot hide it.\n const echoWarning = verifyVerbatimEcho(payload, params.verifyAgainst);\n const echoedControlId = ack.segments(\"MSA\")[0]?.field(2).text ?? \"\";\n const unverifiableWarning =\n params.inbound === undefined ? null : verifyTextInboundEcho(echoedControlId, params.inbound);\n const allWarnings = [\n ...warnings,\n ...(echoWarning === null ? [] : [echoWarning]),\n ...(unverifiableWarning === null ? [] : [unverifiableWarning]),\n ];\n\n const result: MllpAck = {\n frame,\n payload,\n ack,\n requestedCode,\n code,\n correlationId,\n mode,\n warnings: Object.freeze([...allWarnings]),\n };\n return Object.freeze(result);\n}\n\n/**\n * Build the minimal fallback ACK for an inbound message that failed to parse\n * at all (a fatal `Hl7ParseError`). Never emits a positive disposition, a\n * requested `AA`/`CA` is downgraded to `AE`/`CE`; every other code passes\n * through unchanged. MSA-2 is left empty (no fabricated correlation) and no\n * ERR segment is added, table-level detail stays the caller's concern via\n * `options.error` on the normal path; this fallback only guarantees a\n * spec-shaped, framed, non-positive ACK when the inbound could not be read\n * at all.\n * @internal\n */\nfunction buildUnparseableFallback(\n peer: Hl7Peer,\n requestedCode: AckCode,\n fatalCode: string,\n options: BuildMllpAckOptions,\n encoding: BufferEncoding,\n): MllpAck {\n // The downgrade pair lives UPSTREAM (single source of truth), this adapter\n // never carries its own AA→AE / CA→CE copy.\n const code = peer.downgradePositiveAck(requestedCode);\n const ack = peer.buildMessage({ type: \"ACK\" }).addSegment(\"MSA\", [code, \"\"]);\n\n const warnings: readonly MllpAckWarning[] = [\n {\n code: MLLP_ACK_INBOUND_UNPARSEABLE,\n message: `Inbound message unparseable (${fatalCode}); emitted ${code} with no correlation id.`,\n },\n ];\n\n return toMllpAck({\n ack,\n requestedCode,\n code,\n correlationId: undefined,\n mode: undefined,\n warnings,\n options,\n encoding,\n // MSA-2 is INTENTIONALLY empty here, the inbound could not be parsed, so there is no\n // control id to echo and we refuse to fabricate one. Running either echo check would\n // report that deliberate choice as a problem; `MLLP_ACK_INBOUND_UNPARSEABLE` (above)\n // is the accurate warning for this case, and it is already attached.\n verifyAgainst: undefined,\n inbound: undefined,\n });\n}\n\n/**\n * Parse `inbound` if it is a `Buffer`/`string`; pass an already-parsed\n * `Hl7Message` through unchanged, UNLESS it was constructed by a different\n * loaded copy of `@cosyte/hl7` than the one this adapter lazily loaded (the\n * classic Node \"dual package hazard\": an ESM `import` and this loader's\n * `createRequire` can each resolve a distinct module instance with distinct\n * class identities, so the peer's own `inbound instanceof Hl7Message` guard\n * inside `buildAck` would otherwise reject a perfectly valid message). In\n * that case, re-derive an in-realm `Hl7Message` by round-tripping through\n * `.toString()`, cheap, and the wire format is the one true interchange\n * boundary between two copies of the same library.\n *\n * ## Why a `Buffer` is decoded here rather than handed to `parseHL7`\n *\n * `parseHL7(buffer)` does its own charset resolution (MSH-18 discovery, or an\n * `options.charset` override) and decodes through a WHATWG `TextDecoder`. That is\n * the right behaviour for reading *clinical text*, and the wrong one for an ACK\n * builder, which needs the control ID to survive as **bytes**:\n *\n * * `TextDecoder` has no byte-faithful codec. Its `iso-8859-1` label is aliased\n * by the WHATWG Encoding Standard to **windows-1252**, which maps `0x8B` to\n * `U+2039` and `0x9C` to `U+0153`, re-encoding those does not give back the\n * bytes that arrived. `utf8` (the default when MSH-18 is absent) folds every\n * invalid sequence onto `U+FFFD`, collapsing all high-bit bytes onto one value.\n * * Node's `Buffer` `latin1` codec, by contrast, IS a 1:1 byte↔code-unit map, so\n * `Buffer.from(buf.toString(\"latin1\"), \"latin1\")` is the identity for every\n * byte string.\n *\n * So we decode the bytes ourselves with the resolved `encoding` and hand `parseHL7`\n * a `string`, which it takes verbatim. Charset is the *caller's* concern (MSH-18,\n * and the `Buffer`-first API rule), this adapter's concern is that the control ID\n * it echoes is the one that arrived.\n * @internal\n */\nfunction resolveInbound(\n peer: Hl7Peer,\n inbound: Hl7Message | Buffer | string,\n encoding: BufferEncoding,\n): Hl7Message {\n // Strip LEADING SEGMENT TERMINATORS ONLY, and do it for bytes and text alike so the two\n // give the same answer for the same message. `parseHL7` requires MSH to be the FIRST\n // segment and throws `NO_MSH_SEGMENT` otherwise; a leading `CR` (which the MLLP decoder\n // passes straight through) carries no data, so dropping it cannot hide anything.\n //\n // It deliberately stops there. It does NOT skip an `FHS`/`BHS` batch envelope, doing so\n // would hand `parseHL7` only the batch's FIRST message and yield a positive `AA` for\n // messages 2..N that were never read. A batch falls through to `NO_MSH_SEGMENT` and out\n // into the warned, non-positive `AE` fallback: a loud refusal to ACK what we did not\n // read. See `stripLeadingSegmentTerminators`.\n if (Buffer.isBuffer(inbound)) {\n return peer.parseHL7(stripLeadingSegmentTerminators(inbound).toString(encoding));\n }\n if (typeof inbound === \"string\") {\n return peer.parseHL7(inbound.replace(/^[\\r\\n]+/, \"\"));\n }\n const isPeerNative: boolean = inbound instanceof peer.Hl7Message;\n return isPeerNative ? inbound : peer.parseHL7(inbound.toString());\n}\n\n/**\n * Build a framed MLLP ACK from an inbound HL7 v2 message, the core\n * `ack-from-hl7` adapter. `inbound` may be an already-parsed `Hl7Message`, or\n * raw bytes/text to parse first.\n *\n * Behavior:\n * - **Fatally unparseable inbound** (`Hl7ParseError`), never emits a\n * positive code: `AA`→`AE`, `CA`→`CE`; other codes pass through. MSA-2 is\n * empty, `correlationId` and `mode` are `undefined`, and `warnings`\n * contains one {@link MLLP_ACK_INBOUND_UNPARSEABLE} entry naming the fatal\n * code, never any inbound payload bytes.\n * - **Normal path**, detects the ack mode from inbound MSH-15/16, delegates\n * to `@cosyte/hl7`'s `buildAck` (which applies its own no-correlation\n * fail-safe), and forwards `buildAck`'s own warnings (mapped to\n * `{ code, message }`). Inbound parse warnings are NOT forwarded, they are\n * the caller's concern if they parsed the inbound themselves.\n *\n * @throws {MllpPeerMissingError} when `@cosyte/hl7` is not installed.\n * @throws {TypeError} when `options.code` is not a known HL7 Table 0008 code, or when\n * `options.encoding` is a **non-text** codec (`base64`/`base64url`/`hex`/`utf16le`/`ucs2`)\n * on **any** inbound, it would serialize a garbage frame the receiver cannot parse.\n * See {@link BuildMllpAckOptions.encoding}.\n *\n * @example\n * ```typescript\n * import { buildMllpAck } from '@cosyte/mllp/ack-from-hl7';\n * const ack = buildMllpAck(inboundBuffer, { code: \"AA\" });\n * socket.write(ack.frame);\n * ```\n */\nexport function buildMllpAck(\n inbound: Hl7Message | Buffer | string,\n options: BuildMllpAckOptions,\n): MllpAck {\n const peer = loadHl7Peer();\n assertKnownAckCode(peer, options.code);\n\n // ONE codec for the whole build, the inbound decode and the outbound encode are\n // the same choice, because an asymmetric pair is exactly what breaks the verbatim\n // MSA-2 echo (§2.9.2.2) and with it the sender's ACK correlation.\n const encoding = resolveEncoding(inbound, options);\n\n // Fail loud at the boundary on a non-text codec (`base64`/`hex`/`utf16le`/…), on every input\n // shape: it would serialize the ACK to a wholesale-garbage frame the receiver cannot parse, and\n // on a `Buffer` it also garbles the inbound decode into the unparseable fallback (so the\n // byte-level NOT_VERBATIM check never runs) while intermittently throwing a framing error from\n // the garbage outbound bytes. See {@link assertSerializableAckEncoding}. The legitimate charset\n // escape hatch (`latin1`/`ascii`/`utf8`/`binary`) is untouched.\n assertSerializableAckEncoding(encoding);\n\n let msg: Hl7Message;\n try {\n msg = resolveInbound(peer, inbound, encoding);\n } catch (err) {\n if (err instanceof peer.Hl7ParseError) {\n return buildUnparseableFallback(peer, options.code, err.code, options, encoding);\n }\n throw err;\n }\n\n const mode = peer.detectAckMode(msg);\n const ack = peer.buildAck(\n msg,\n options.error === undefined\n ? { code: options.code, mode }\n : { code: options.code, error: options.error, mode },\n );\n\n const emittedCode = ack.get(\"MSA.1\");\n assertKnownAckCode(peer, emittedCode);\n\n // The correlation id is what MSA-2 ACTUALLY carries on the wire, the\n // verbatim field text (`Field.text`), never the component-1-only scalar.\n // A vendor-quirk inbound MSH-10 like `ID^X` surfaces here byte-for-byte,\n // matching what a raw-bytes-correlating sender (this package's own client\n // correlator) will compare against.\n const msa2 = ack.segments(\"MSA\")[0]?.field(2).text ?? \"\";\n const correlationId = msa2 !== \"\" ? msa2 : undefined;\n\n const warnings: readonly MllpAckWarning[] = ack.warnings.map((w) => ({\n code: w.code,\n message: w.message,\n }));\n\n return toMllpAck({\n ack,\n requestedCode: options.code,\n code: emittedCode,\n correlationId,\n mode,\n warnings,\n options,\n encoding,\n verifyAgainst: inboundBytes(inbound, encoding),\n inbound,\n });\n}\n\n/**\n * Build an `AA` (Application Accept) MLLP ACK.\n *\n * @example\n * ```typescript\n * import { buildAckAA } from '@cosyte/mllp/ack-from-hl7';\n * socket.write(buildAckAA(inboundBuffer).frame);\n * ```\n */\nexport function buildAckAA(\n inbound: Hl7Message | Buffer | string,\n options?: Omit<BuildMllpAckOptions, \"code\">,\n): MllpAck {\n return buildMllpAck(inbound, { ...options, code: \"AA\" });\n}\n\n/**\n * Build an `AE` (Application Error) MLLP ACK.\n *\n * @example\n * ```typescript\n * import { buildAckAE } from '@cosyte/mllp/ack-from-hl7';\n * socket.write(buildAckAE(inboundBuffer, { error: { conditionCode: \"101\" } }).frame);\n * ```\n */\nexport function buildAckAE(\n inbound: Hl7Message | Buffer | string,\n options?: Omit<BuildMllpAckOptions, \"code\">,\n): MllpAck {\n return buildMllpAck(inbound, { ...options, code: \"AE\" });\n}\n\n/**\n * Build an `AR` (Application Reject) MLLP ACK.\n *\n * @example\n * ```typescript\n * import { buildAckAR } from '@cosyte/mllp/ack-from-hl7';\n * socket.write(buildAckAR(inboundBuffer, { error: { conditionCode: \"200\" } }).frame);\n * ```\n */\nexport function buildAckAR(\n inbound: Hl7Message | Buffer | string,\n options?: Omit<BuildMllpAckOptions, \"code\">,\n): MllpAck {\n return buildMllpAck(inbound, { ...options, code: \"AR\" });\n}\n\n/**\n * Build a `CA` (Commit Accept) MLLP ACK (enhanced mode).\n *\n * @example\n * ```typescript\n * import { buildAckCA } from '@cosyte/mllp/ack-from-hl7';\n * socket.write(buildAckCA(inboundBuffer).frame);\n * ```\n */\nexport function buildAckCA(\n inbound: Hl7Message | Buffer | string,\n options?: Omit<BuildMllpAckOptions, \"code\">,\n): MllpAck {\n return buildMllpAck(inbound, { ...options, code: \"CA\" });\n}\n\n/**\n * Build a `CE` (Commit Error) MLLP ACK (enhanced mode).\n *\n * @example\n * ```typescript\n * import { buildAckCE } from '@cosyte/mllp/ack-from-hl7';\n * socket.write(buildAckCE(inboundBuffer, { error: { conditionCode: \"101\" } }).frame);\n * ```\n */\nexport function buildAckCE(\n inbound: Hl7Message | Buffer | string,\n options?: Omit<BuildMllpAckOptions, \"code\">,\n): MllpAck {\n return buildMllpAck(inbound, { ...options, code: \"CE\" });\n}\n\n/**\n * Build a `CR` (Commit Reject) MLLP ACK (enhanced mode).\n *\n * @example\n * ```typescript\n * import { buildAckCR } from '@cosyte/mllp/ack-from-hl7';\n * socket.write(buildAckCR(inboundBuffer, { error: { conditionCode: \"200\" } }).frame);\n * ```\n */\nexport function buildAckCR(\n inbound: Hl7Message | Buffer | string,\n options?: Omit<BuildMllpAckOptions, \"code\">,\n): MllpAck {\n return buildMllpAck(inbound, { ...options, code: \"CR\" });\n}\n\n/**\n * Detect the HL7 acknowledgment mode (`\"original\"` vs `\"enhanced\"`) of an\n * inbound message, from MSH-15/16. Named `detectMode` (not `detectAckMode`)\n * to avoid colliding with the peer's own `detectAckMode` export for callers\n * who import both from `@cosyte/hl7` and `@cosyte/mllp/ack-from-hl7`.\n *\n * `inbound` may be an already-parsed `Hl7Message`, or raw bytes/text to parse\n * first. A fatal parse failure rethrows the `Hl7ParseError` as-is, unlike\n * {@link buildMllpAck}, there is no ACK to build here, so there is no\n * fallback to construct.\n *\n * **Module-realm note:** this adapter loads `@cosyte/hl7` lazily via\n * `createRequire` (see `peer.ts`). If your code ALSO does\n * `import { Hl7ParseError } from \"@cosyte/hl7\"` directly, Node's module\n * resolution can, depending on your bundler/runtime, hand you a distinct\n * module instance than the one this adapter loaded, in which case\n * `caught instanceof Hl7ParseError` (your import) may be `false` even though\n * `caught.name === \"Hl7ParseError\"` and `caught.code` is a valid HL7 fatal\n * code (`NO_MSH_SEGMENT` / `MSH_TOO_SHORT` / `INVALID_ENCODING_CHARACTERS` /\n * `EMPTY_INPUT`). Prefer narrowing on `.code`/`.name`, or obtain the exact\n * class via this subpath's own `loadHl7Peer().Hl7ParseError`.\n *\n * @throws {MllpPeerMissingError} when `@cosyte/hl7` is not installed.\n * @throws {Hl7ParseError} when `inbound` is raw bytes/text that fails to parse.\n *\n * @example\n * ```typescript\n * import { detectMode } from '@cosyte/mllp/ack-from-hl7';\n * detectMode(inboundBuffer); // \"original\" | \"enhanced\"\n * ```\n */\nexport function detectMode(inbound: Hl7Message | Buffer | string): AckMode {\n const peer = loadHl7Peer();\n // Same byte-faithful decode as `buildMllpAck` (see `resolveInbound`), so a\n // high-bit byte anywhere in the message cannot make the two disagree about\n // which message they are looking at. MSH-15/16 are ASCII either way, so this\n // is about consistency, not about the mode itself.\n const msg = resolveInbound(peer, inbound, BYTES_INBOUND_ENCODING);\n return peer.detectAckMode(msg);\n}\n"]}