@cosyte/mllp 0.0.3 → 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
@@ -110,6 +156,26 @@ begins its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` unt
110
156
  `InMemoryTransport.pair()`; the receive example's fictional `respond`/`buildAck` → the real
111
157
  `createServer({ onMessage })`). Bumps the `@cosyte/vitest-config` devDependency to `^0.0.2` for the
112
158
  `/snippets` export. Docs and tests only: no runtime or public-API change.
159
+ - **README header swapped from the per-package banner to the shared Cosyte lockup.** Both sides of
160
+ this swap happened inside one unreleased window, so no published version carries the banner and
161
+ the release notes for this version describe only the lockup. It is recorded here, where build
162
+ history belongs, rather than dropped. The banner
163
+ (`cosyte-banner-mllp-1200x300.png`) baked the package name and its one line summary into the
164
+ artwork, and the `# @cosyte/mllp` H1 and the blockquote directly beneath it repeat both, so the
165
+ same two strings appeared three times in the first four lines of the page. The header is now a
166
+ `<picture>` carrying the theme-aware Cosyte lockup tile (`cosyte-lockup-tile-on-dark-1200x300.png`
167
+ and its `-on-light` counterpart), which reads "Cosyte" rather than the package name, so the
168
+ duplication goes and the heading stays untouched. The `<picture>` element was deliberately avoided
169
+ when the banner landed, on the grounds that npm sanitizer behaviour was unverified; that has since
170
+ been verified rather than assumed. On GitHub in dark mode `currentSrc` resolves to the on-dark tile
171
+ with parent element `PICTURE`, and on npm the `<img>` is hoisted out of the `<picture>` by GitHub's
172
+ anchor wrapper so the light fallback renders, which is the correct cut there because npmjs.com has
173
+ no dark mode. The block was copied programmatically out of `hl7`'s README rather than retyped, and
174
+ both URLs were fetched live (`200 image/png`) rather than trusted from the stored
175
+ `published-urls.json` declaration, since a typo in either is a 404 on a public package page. The
176
+ alt text now describes the artwork rather than the package, because a screen reader on the npm page
177
+ should hear what the image is and the package summary is already the next line. Docs only: no
178
+ runtime or public-API change.
113
179
  - **`docs-content/installation.md` publish-status note corrected (README-ORG-SWEEP).** The Status
114
180
  callout said the package was "not yet published to npm" and that the install command was "the shape
115
181
  it will take at first publish": stale now that `@cosyte/mllp` is published on npm at `0.0.1` and
package/README.md CHANGED
@@ -1,3 +1,8 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://cosyte.com/tile/cosyte-lockup-tile-on-dark-1200x300.png">
3
+ <img alt="Cosyte: a plus mark set in two overlapping rounded squares, one solid and one outlined, beside the Cosyte wordmark" src="https://cosyte.com/tile/cosyte-lockup-tile-on-light-1200x300.png">
4
+ </picture>
5
+
1
6
  # @cosyte/mllp
2
7
 
3
8
  > Send and receive HL7 v2 over a production-grade MLLP connection in a few lines, with framing, ACK correlation, auto-reconnect, and backpressure handled for you.
@@ -23,8 +28,15 @@ pnpm add @cosyte/mllp
23
28
  ```ts
24
29
  import { createStarterServer } from "@cosyte/mllp";
25
30
 
26
- // Echoes every received frame straight back as the ACK.
27
- const server = await createStarterServer({ port: 2575, onMessage: (buf) => buf });
31
+ // Auto-ACK is on by default: the server awaits your handler (the durable-commit
32
+ // step) before it answers, so a positive ACK can never precede the commit.
33
+ // A throw answers a negative ACK instead.
34
+ const server = await createStarterServer({
35
+ port: 2575,
36
+ onMessage: async (payload) => {
37
+ await db.commit(payload);
38
+ },
39
+ });
28
40
  ```
29
41
 
30
42
  ```ts
@@ -48,7 +60,7 @@ const server = createServer({ tls: { cert: certPem, key: keyPem } });
48
60
  await server.listen(2575, "127.0.0.1");
49
61
 
50
62
  // Server: mutual TLS (ATNA ITI-19)
51
- const server = createServer({
63
+ const mutualTlsServer = createServer({
52
64
  tls: { cert: certPem, key: keyPem, ca: clientCaPem, clientAuth: "MUST" },
53
65
  });
54
66
  ```
@@ -58,7 +70,7 @@ const server = createServer({
58
70
  const client = createClient({ host: "mllp.example.com", port: 2575, tls: { ca: caPem } });
59
71
 
60
72
  // Client: mutual TLS
61
- const client = createClient({
73
+ const mutualTlsClient = createClient({
62
74
  host: "mllp.example.com",
63
75
  port: 2575,
64
76
  tls: { ca: caPem, cert: clientCertPem, key: clientKeyPem },
@@ -84,7 +96,11 @@ const server = createServer({
84
96
  });
85
97
  ```
86
98
 
87
- Handler resolves ⇒ `AA`. Handler throws ⇒ `AE` (or `AR` via `MllpAckError`). **A positive ACK cannot
99
+ Handler resolves ⇒ `AA`, **unless the inbound could not carry a correlatable positive ACK** (no
100
+ readable `MSH`, an empty `MSH-10`, a batch or concatenated payload, or trailing bytes the framer
101
+ discarded). In that case the commit still happened, but the ACK is downgraded to `AE` and a `nack`
102
+ event names the reason, because a positive ACK the sender cannot match is one it will resend.
103
+ Handler throws ⇒ `AE` (or `AR` via `MllpAckError`). **A positive ACK cannot
88
104
  precede a successful commit.** `autoAck: 'AA'` *without* a handler is documented as a
89
105
  transport-accept: "received and framed", not "processed".
90
106