@cosyte/mllp 0.0.4 → 0.0.6
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 +129 -0
- package/dist/ack-from-hl7/index.cjs.map +1 -1
- package/dist/ack-from-hl7/index.mjs.map +1 -1
- package/dist/index.cjs +53 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +132 -9
- package/dist/index.d.ts +132 -9
- package/dist/index.mjs +53 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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
|
|
@@ -139,6 +185,89 @@ begins its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` unt
|
|
|
139
185
|
|
|
140
186
|
### Fixed
|
|
141
187
|
|
|
188
|
+
- **The PHI scanner no longer refuses an entire sweep because a transient file went away while it
|
|
189
|
+
was reading.** `all` mode lists `test/` + `src/`, then reads each file; anything created and
|
|
190
|
+
removed inside that window made a read throw `ENOENT` and took the whole scan down with it
|
|
191
|
+
(exit 2, the invocation-error code). The refusal was never wrong, the **enumeration** was, so the
|
|
192
|
+
enumeration is what changed. `PHI-SCAN-ENUMERATE-THEN-READ-CLASS`, porting the remedy that
|
|
193
|
+
shipped in `ccda#80`.
|
|
194
|
+
|
|
195
|
+
**This repo is genuinely reachable, and that was measured rather than read off the code.** With a
|
|
196
|
+
`git` shim first on `PATH` (the scanner runs `git` between the walk and the first read, so the
|
|
197
|
+
shim is a deterministic hook into exactly that gap), a `test/phi-scan-cap-*/capture.txt` removed
|
|
198
|
+
in the window made `pnpm phi-scan` on this checkout exit 2. Those directories are not
|
|
199
|
+
hypothetical: `test/scripts/phi-scan.test.ts` `mkdtemp`s two of them inside `test/`, which IS a
|
|
200
|
+
walk root, and polling the tree during a suite run measured each existing for about **510 ms**
|
|
201
|
+
(about 1.03 s of exposure per `pnpm test`). The siblings are spared only because their walk roots
|
|
202
|
+
are not the repo root, which is where a build transient lands. **The exposure is real but not
|
|
203
|
+
self-triggering:** nothing in this repo runs an `all`-mode sweep beside the suite (CI runs
|
|
204
|
+
`pnpm phi-scan` as a step strictly before `pnpm test` in one sequential job, the pre-commit hook
|
|
205
|
+
runs `--staged` and reads blobs from the index, and inside the test file the sweep is ordered
|
|
206
|
+
before both capture tests), so what is live is a hand-run or agent-run sweep during a test run.
|
|
207
|
+
Racing 88 sweeps against 6 suite runs produced 0 refusals, which is consistent with the measured
|
|
208
|
+
duty cycle: walk 2-4 ms, `git check-ignore` 6-9 ms, reads 11-19 ms, inside a ~4 s process.
|
|
209
|
+
|
|
210
|
+
**Exactly one case is now tolerated:** a file the walk enumerated **itself**, that **git does not
|
|
211
|
+
track**, failing with **`ENOENT`**. It is reported on stderr as skipped, naming the path, and is
|
|
212
|
+
never silent. **Still refusing:** a tracked file that cannot be read, any non-`ENOENT` failure, a
|
|
213
|
+
tolerated file back on disk at sweep end, a `git` that cannot report the tracked set, and an
|
|
214
|
+
**empty** tracked set (a removed `.git/index` exits 0 with no output, which would make every file
|
|
215
|
+
untracked and quietly delete the tracked-file bound; a corrupt one exits 128 and was already
|
|
216
|
+
caught). `all` mode additionally refuses when it observed **no** files, so the tolerance can never
|
|
217
|
+
decay into a clean report of nothing.
|
|
218
|
+
|
|
219
|
+
**Which bounds are pinned, and which are only disclosed.** Pinned by eight tests in
|
|
220
|
+
`test/scripts/phi-scan.test.ts`, each against a throwaway tree under `tmpdir()` (seven of them a
|
|
221
|
+
git repo, one deliberately not) and **five** of them driven by the `git`-shim technique, with no
|
|
222
|
+
sleep and no real build: the tolerance itself; the tracked-file bound; the `ENOENT`-only bound
|
|
223
|
+
(`EISDIR` via a decoy replaced by a directory); git unable to answer; the empty tracked set;
|
|
224
|
+
`observed no files`; a violator in an untracked file that does not vanish; and a violator inside a
|
|
225
|
+
transient that is still there when the read arrives. Each bound was **mutation-tested**: widening
|
|
226
|
+
`tolerateVanish` to `true` fails three of them, dropping the `ENOENT` check fails one, dropping
|
|
227
|
+
the `size > 0` guard fails one, and dropping the `observed === 0` refusal fails one. **Not pinned,
|
|
228
|
+
and measured as not pinned:** the back-on-disk re-check. Stubbing it out leaves all 40 tests
|
|
229
|
+
green. Reaching it needs a timed re-create against a deliberately slowed sweep, and a
|
|
230
|
+
load-sensitive sleep guarding a load-dependent race is the failure mode this defect itself
|
|
231
|
+
teaches; losing that branch loses the re-check, never the tolerance's bounds. **The gate drove
|
|
232
|
+
that branch by hand** (shim anchored on the second git call, `git ls-files`, against a 20,000-file
|
|
233
|
+
read loop with a tuned re-create delay) and confirmed the code right at all three delays, so the
|
|
234
|
+
branch is measured-correct and unpinned rather than unknown.
|
|
235
|
+
|
|
236
|
+
**Two residuals carried, not closed.** The post-sweep re-check is keyed on the enumerated PATH,
|
|
237
|
+
not on content, so an untracked file RENAMED inside the window goes unscanned under a clean
|
|
238
|
+
report; it is bounded (committing the file means `git add`, after which it is tracked and
|
|
239
|
+
untolerable) and closing it needs a content-addressed sweep, a different design rather than a
|
|
240
|
+
wider bound. And `walk()`'s own `existsSync` -> `readdirSync` race, one phase earlier, is
|
|
241
|
+
untouched: a directory removed in that window throws a plain `SystemError` `main()` does not
|
|
242
|
+
convert, so Node exits **1**, the code the contract reserves for "hits found". It matters more
|
|
243
|
+
here than in the siblings because this repo's transient is a **directory**, removed wholesale, and
|
|
244
|
+
it is unpinnable by the same technique because nothing runs before the walk.
|
|
245
|
+
|
|
246
|
+
**The suite keeps writing inside the walk root, deliberately.** The two capture tests exist to
|
|
247
|
+
prove that a file whose repo-relative path starts with `test/` earns the structured scan, so the
|
|
248
|
+
path is the fixture and moving them would delete what they test.
|
|
249
|
+
|
|
250
|
+
**Three things the gate checked hardest and could not break, recorded so they are not re-derived.**
|
|
251
|
+
The tracked-set spelling comparison holds: instrumented against this checkout, **zero** files come
|
|
252
|
+
out `tolerateVanish: true`, so the tolerance is inert on a clean tree, and a purpose-built repo of
|
|
253
|
+
15 adversarial tracked filenames (space, tab, newline, quote, backslash, `$`, `*`, `;`, non-ASCII,
|
|
254
|
+
CJK, emoji, plus an invalid-UTF-8 name) never marked a tracked file untracked, because `-z`
|
|
255
|
+
suppresses `core.quotePath` on both sides. The tolerance cannot reach `paths` or `staged` mode,
|
|
256
|
+
and `--allow-fixture` forces `mode = "paths"`, so an allow-list subtraction can never be used to
|
|
257
|
+
drive `all` mode to zero observed files. And in `all` mode `report()` is unreachable while
|
|
258
|
+
`observed === 0`, while in every mode a sweep that observed nothing has no hits to publish.
|
|
259
|
+
(`staged` legitimately reaches `report()` at `observed === 0` and prints `OK, no hits` when a
|
|
260
|
+
commit stages nothing in scope, which is why the refusal is scoped to `all`.)
|
|
261
|
+
|
|
262
|
+
**Two scope notes surfaced by the same review, one pre-existing and one introduced here.**
|
|
263
|
+
PRE-EXISTING: `walk()` tests `e.isFile()`, which is false for a symlink Dirent, so a symlinked
|
|
264
|
+
file under `test/` or `src/` is never swept in `all` mode; it is bounded, because git cannot
|
|
265
|
+
commit content through a symlink and `--staged` reads the index. INTRODUCED by this change: a
|
|
266
|
+
tolerated skip exits **0** with only a stderr line, including in CI, where the tree is at rest and
|
|
267
|
+
a skip is therefore anomalous by definition. On the base commit that same input exited 2. Both are
|
|
268
|
+
stated rather than changed: the first is not this defect, and the second **is** the shipped design
|
|
269
|
+
under review.
|
|
270
|
+
|
|
142
271
|
- **Three doc comments that had outlived the code they described**, all of which shipped into the
|
|
143
272
|
published type declarations. The `'reconnecting'` payload was documented as carrying
|
|
144
273
|
`connectionId` only, while `MllpClient` populates `attempt` and `delayMs` when it schedules a
|