@cosyte/mllp 0.0.1 → 0.0.2

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.
@@ -16,13 +16,13 @@ var MllpFramingError = class _MllpFramingError extends Error {
16
16
  /**
17
17
  * Up to 64 bytes copied from around the anomaly.
18
18
  *
19
- * This is a **copied** Buffer — isolated from the source buffer so it remains
19
+ * This is a **copied** Buffer, isolated from the source buffer so it remains
20
20
  * valid after the underlying buffer is reused or overwritten.
21
21
  *
22
22
  * **PHI contract (MLLP-9):** the decoder only ever populates this with the single
23
23
  * framing-boundary byte that violated the frame structure (whose hex value the
24
- * `message` already discloses) — never a run of payload content bytes. For anomalies
25
- * whose fault is not a specific byte (`MLLP_FRAME_TOO_LARGE` — the accumulated size),
24
+ * `message` already discloses), never a run of payload content bytes. For anomalies
25
+ * whose fault is not a specific byte (`MLLP_FRAME_TOO_LARGE`, the accumulated size),
26
26
  * the snippet is **empty**: a payload slice on a public error field would leak a
27
27
  * field-body slice of clinical content. Callers constructing this directly are
28
28
  * responsible for the same discipline.
@@ -104,9 +104,9 @@ var UNSAFE_FIELD_SEPARATORS = /* @__PURE__ */ new Set([
104
104
  SEGMENT_SEPARATOR_CR,
105
105
  SEGMENT_SEPARATOR_LF,
106
106
  11,
107
- // VT — MLLP start block
107
+ // VT, MLLP start block
108
108
  28
109
- // FS — MLLP end block
109
+ // FS, MLLP end block
110
110
  ]);
111
111
  function isMshSegmentAt(buf, i) {
112
112
  return i + 3 < buf.length && buf[i] === ASCII_M && buf[i + 1] === ASCII_S && buf[i + 2] === ASCII_H && !UNSAFE_FIELD_SEPARATORS.has(buf[i + 3]);
@@ -205,7 +205,7 @@ var MllpPeerMissingError = class _MllpPeerMissingError extends Error {
205
205
  */
206
206
  constructor(cause) {
207
207
  super(
208
- `${PEER_PACKAGE_NAME} is required by ${SUBPATH_NAME} but is not installed. ${PEER_PACKAGE_NAME} is an optional peer dependency of @cosyte/mllp \u2014 install it alongside @cosyte/mllp to use ${SUBPATH_NAME}: \`npm install ${PEER_PACKAGE_NAME}\` (or the equivalent for your package manager).`,
208
+ `${PEER_PACKAGE_NAME} is required by ${SUBPATH_NAME} but is not installed. ${PEER_PACKAGE_NAME} is an optional peer dependency of @cosyte/mllp, install it alongside @cosyte/mllp to use ${SUBPATH_NAME}: \`npm install ${PEER_PACKAGE_NAME}\` (or the equivalent for your package manager).`,
209
209
  { cause }
210
210
  );
211
211
  if (Error.captureStackTrace) {
@@ -246,8 +246,16 @@ function loadHl7Peer(requireFn) {
246
246
  // src/ack-from-hl7/build.ts
247
247
  var MLLP_ACK_INBOUND_UNPARSEABLE = "MLLP_ACK_INBOUND_UNPARSEABLE";
248
248
  var MLLP_ACK_CONTROL_ID_NOT_VERBATIM = "MLLP_ACK_CONTROL_ID_NOT_VERBATIM";
249
+ var MLLP_ACK_CONTROL_ID_UNVERIFIABLE = "MLLP_ACK_CONTROL_ID_UNVERIFIABLE";
249
250
  var BYTES_INBOUND_ENCODING = "latin1";
250
251
  var TEXT_INBOUND_ENCODING = "utf8";
252
+ var SERIALIZABLE_ACK_ENCODINGS = /* @__PURE__ */ new Set([
253
+ "utf8",
254
+ "utf-8",
255
+ "ascii",
256
+ "latin1",
257
+ "binary"
258
+ ]);
251
259
  function assertKnownAckCode(peer, code) {
252
260
  const known = Object.values(peer.ACK_CODES);
253
261
  if (typeof code !== "string" || !known.includes(code)) {
@@ -260,6 +268,12 @@ function resolveEncoding(inbound, options) {
260
268
  if (options.encoding !== void 0) return options.encoding;
261
269
  return Buffer.isBuffer(inbound) ? BYTES_INBOUND_ENCODING : TEXT_INBOUND_ENCODING;
262
270
  }
271
+ function assertSerializableAckEncoding(encoding) {
272
+ if (SERIALIZABLE_ACK_ENCODINGS.has(encoding.toLowerCase())) return;
273
+ throw new TypeError(
274
+ `buildMllpAck: encoding ${JSON.stringify(encoding)} is not a serializable ACK codec. Serializing an ACK to a non-text codec (base64/base64url/hex/utf16le/ucs2) produces a wholesale-garbage frame the receiver cannot parse, on a Buffer inbound it also garbles the inbound decode, so it can never correlate. Use a text codec ("utf8", "ascii", or "latin1"); on a Buffer, "latin1" is byte-verbatim and is the default.`
275
+ );
276
+ }
263
277
  function inboundBytes(inbound, encoding) {
264
278
  if (Buffer.isBuffer(inbound)) return inbound;
265
279
  if (typeof inbound === "string") return Buffer.from(inbound, encoding);
@@ -275,7 +289,23 @@ function verifyVerbatimEcho(ackPayload, inboundRaw) {
275
289
  const echoed = echoedId === null ? "absent" : `${String(echoedId.length)} bytes`;
276
290
  return {
277
291
  code: MLLP_ACK_CONTROL_ID_NOT_VERBATIM,
278
- message: `ACK does not echo the inbound MSH-10 verbatim (HL7 v2.5.1 \xA72.9.2.2): inbound MSH-10 is ${inboundLen} bytes, emitted MSA-2 is ${echoed}, and they differ. The sender keys its in-flight store on the inbound bytes, so it will NOT match this ACK (ACK timeout -> resend -> duplicate message). Field values are withheld \u2014 MSH-10 is inbound payload content and this warning goes to a log; compare your inbound payload against MllpAck.payload to see the bytes.`
292
+ message: `ACK does not echo the inbound MSH-10 verbatim (HL7 v2.5.1 \xA72.9.2.2): inbound MSH-10 is ${inboundLen} bytes, emitted MSA-2 is ${echoed}, and they differ. The sender keys its in-flight store on the inbound bytes, so it will NOT match this ACK (ACK timeout -> resend -> duplicate message). Field values are withheld, MSH-10 is inbound payload content and this warning goes to a log; compare your inbound payload against MllpAck.payload to see the bytes.`
293
+ };
294
+ }
295
+ function verifyTextInboundEcho(controlId, inbound) {
296
+ if (Buffer.isBuffer(inbound)) return null;
297
+ if (controlId === "") return null;
298
+ let hasNonAscii = false;
299
+ for (let i = 0; i < controlId.length; i++) {
300
+ if (controlId.charCodeAt(i) > 127) {
301
+ hasNonAscii = true;
302
+ break;
303
+ }
304
+ }
305
+ if (!hasNonAscii) return null;
306
+ return {
307
+ code: MLLP_ACK_CONTROL_ID_UNVERIFIABLE,
308
+ message: `ACK's byte-verbatim echo of MSH-10 (HL7 v2.5.1 \xA72.9.2.2) cannot be verified: the inbound was decoded text (a string or Hl7Message), not raw bytes, so its wire bytes are gone and the ${String(controlId.length)}-code-unit MSA-2 control ID holds a non-ASCII code unit the decode/encode codec could have altered (a lossy override such as ascii can even mask it into the ASCII byte range). It may not correlate at the sender (ACK timeout -> resend -> duplicate message). Pass the raw payload Buffer to get the byte-level guarantee. Field values are withheld, MSH-10 is inbound payload content and this warning goes to a log.`
279
309
  };
280
310
  }
281
311
  function toMllpAck(params) {
@@ -285,7 +315,13 @@ function toMllpAck(params) {
285
315
  allowDelimiterBytesInPayload: options.allowDelimiterBytesInPayload ?? false
286
316
  });
287
317
  const echoWarning = verifyVerbatimEcho(payload, params.verifyAgainst);
288
- const allWarnings = echoWarning === null ? warnings : [...warnings, echoWarning];
318
+ const echoedControlId = ack.segments("MSA")[0]?.field(2).text ?? "";
319
+ const unverifiableWarning = params.inbound === void 0 ? null : verifyTextInboundEcho(echoedControlId, params.inbound);
320
+ const allWarnings = [
321
+ ...warnings,
322
+ ...echoWarning === null ? [] : [echoWarning],
323
+ ...unverifiableWarning === null ? [] : [unverifiableWarning]
324
+ ];
289
325
  const result = {
290
326
  frame,
291
327
  payload,
@@ -316,11 +352,12 @@ function buildUnparseableFallback(peer, requestedCode, fatalCode, options, encod
316
352
  warnings,
317
353
  options,
318
354
  encoding,
319
- // MSA-2 is INTENTIONALLY empty here — the inbound could not be parsed, so there is no
320
- // control id to echo and we refuse to fabricate one. Running the verbatim check would
321
- // report that deliberate choice as a violation; `MLLP_ACK_INBOUND_UNPARSEABLE` (above)
355
+ // MSA-2 is INTENTIONALLY empty here, the inbound could not be parsed, so there is no
356
+ // control id to echo and we refuse to fabricate one. Running either echo check would
357
+ // report that deliberate choice as a problem; `MLLP_ACK_INBOUND_UNPARSEABLE` (above)
322
358
  // is the accurate warning for this case, and it is already attached.
323
- verifyAgainst: void 0
359
+ verifyAgainst: void 0,
360
+ inbound: void 0
324
361
  });
325
362
  }
326
363
  function resolveInbound(peer, inbound, encoding) {
@@ -337,6 +374,7 @@ function buildMllpAck(inbound, options) {
337
374
  const peer = loadHl7Peer();
338
375
  assertKnownAckCode(peer, options.code);
339
376
  const encoding = resolveEncoding(inbound, options);
377
+ assertSerializableAckEncoding(encoding);
340
378
  let msg;
341
379
  try {
342
380
  msg = resolveInbound(peer, inbound, encoding);
@@ -368,7 +406,8 @@ function buildMllpAck(inbound, options) {
368
406
  warnings,
369
407
  options,
370
408
  encoding,
371
- verifyAgainst: inboundBytes(inbound, encoding)
409
+ verifyAgainst: inboundBytes(inbound, encoding),
410
+ inbound
372
411
  });
373
412
  }
374
413
  function buildAckAA(inbound, options) {
@@ -395,6 +434,6 @@ function detectMode(inbound) {
395
434
  return peer.detectAckMode(msg);
396
435
  }
397
436
 
398
- export { MLLP_ACK_CONTROL_ID_NOT_VERBATIM, MLLP_ACK_INBOUND_UNPARSEABLE, MllpPeerMissingError, buildAckAA, buildAckAE, buildAckAR, buildAckCA, buildAckCE, buildAckCR, buildMllpAck, detectMode, loadHl7Peer };
437
+ export { MLLP_ACK_CONTROL_ID_NOT_VERBATIM, MLLP_ACK_CONTROL_ID_UNVERIFIABLE, MLLP_ACK_INBOUND_UNPARSEABLE, MllpPeerMissingError, buildAckAA, buildAckAE, buildAckAR, buildAckCA, buildAckCE, buildAckCR, buildMllpAck, detectMode, loadHl7Peer };
399
438
  //# sourceMappingURL=index.mjs.map
400
439
  //# sourceMappingURL=index.mjs.map