fhirsmith 0.12.0 → 0.13.1

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +323 -4
  2. package/README.md +1 -1
  3. package/config-template.json +13 -0
  4. package/library/languages.js +191 -34
  5. package/library/path-safety.js +51 -0
  6. package/library/request-token.js +56 -0
  7. package/package.json +2 -2
  8. package/packages/packages.js +17 -118
  9. package/publisher/publisher.js +60 -30
  10. package/publisher/validation.js +15 -3
  11. package/server.js +12 -3
  12. package/translations/Messages.properties +1 -0
  13. package/tx/README.md +21 -0
  14. package/tx/cs/cs-api.js +30 -1
  15. package/tx/cs/cs-cs.js +14 -1
  16. package/tx/cs/cs-icd11.js +844 -0
  17. package/tx/cs/cs-lang.js +331 -85
  18. package/tx/cs/cs-loinc.js +45 -4
  19. package/tx/cs/cs-mimetypes.js +492 -21
  20. package/tx/cs/cs-snomed.js +38 -4
  21. package/tx/cs/cs-ucum.js +33 -3
  22. package/tx/cs/icd11-expressions.js +243 -0
  23. package/tx/importers/export-icd11-codesystem.js +823 -0
  24. package/tx/importers/icd11-schema.md +285 -0
  25. package/tx/importers/import-icd11.js +861 -0
  26. package/tx/importers/import-loinc.module.js +14 -7
  27. package/tx/importers/import-sct.module.js +10 -1
  28. package/tx/library/codesystem.js +21 -27
  29. package/tx/library/nesting-limit.js +79 -0
  30. package/tx/library/operation-outcome.js +97 -3
  31. package/tx/library/valueset.js +16 -20
  32. package/tx/library.js +23 -1
  33. package/tx/ocl/cache/cache-utils.cjs +5 -3
  34. package/tx/ocl/cs-ocl.cjs +73 -17
  35. package/tx/ocl/jobs/background-queue.cjs +6 -3
  36. package/tx/ocl/mappers/concept-mapper.cjs +9 -1
  37. package/tx/ocl/shared/patches.cjs +48 -1
  38. package/tx/ocl/vs-ocl.cjs +15 -13
  39. package/tx/provider.js +1 -1
  40. package/tx/sct/expressions.js +194 -17
  41. package/tx/tx.fhir.org.yml +2 -1
  42. package/tx/tx.js +15 -42
  43. package/tx/workers/batch-validate.js +6 -16
  44. package/tx/workers/batch.js +16 -28
  45. package/tx/workers/cache-control.js +6 -12
  46. package/tx/workers/closure.js +3 -8
  47. package/tx/workers/compare.js +6 -34
  48. package/tx/workers/expand.js +36 -45
  49. package/tx/workers/lookup.js +21 -39
  50. package/tx/workers/read.js +6 -40
  51. package/tx/workers/search.js +2 -8
  52. package/tx/workers/subsumes.js +58 -41
  53. package/tx/workers/translate.js +11 -17
  54. package/tx/workers/validate.js +76 -77
  55. package/tx/workers/worker.js +70 -1
  56. package/tx/xml/xml-base.js +243 -76
  57. package/tx/xversion/xv-valueset.js +39 -1
package/CHANGELOG.md CHANGED
@@ -5,7 +5,91 @@ All notable changes to the Health Intersections Node Server will be documented i
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.12.0] - 2026-08-27
8
+ ## [v0.13.1] - 2026-09-05
9
+
10
+ ### Fixed
11
+
12
+ - Error in ICD-11 provider that made the version page not show
13
+
14
+ ### Tx Conformance Statement
15
+
16
+ FHIRsmith passed all 3489 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed+mimetypes+icd-11, tests v1.9.4, runner v6.10.4)
17
+
18
+ ## [v0.13.0] - 2026-09-04
19
+
20
+ ### Added
21
+
22
+ - ICD-11 support, in three parts. `tx/importers/import-icd11.js` builds a SQLite database from
23
+ the WHO ICD-API - 110,441 concepts across MMS, ICF and the Foundation for the 2026-01 release,
24
+ in every language the API advertises. The native API is the content source because it is the
25
+ only one that can enumerate the classification: the FHIR endpoint exposes no all-codes value
26
+ set, accepts no client-supplied value sets, and its 28 top-level entities report no parent, so
27
+ nothing there tells a client what the roots are. `tx/importers/export-icd11-codesystem.js`
28
+ writes the content back out as FHIR CodeSystems, language supplements and the 25,354
29
+ postcoordination scale ValueSets, optionally as a FHIR NPM package. `tx/cs/cs-icd11.js` is the
30
+ provider; it is a skeleton at this point - metadata, code location, displays and definitions
31
+ work, while designations, properties, extendLookup, filters, iteration, subsumption and
32
+ postcoordinated expressions are still to come. The `icd-11` suite from the tx-ecosystem IG is
33
+ the specification for the rest of it. Postcoordinated expressions now work:
34
+ `tx/cs/icd11-expressions.js` parses the short-code, entity-uri and ICF dotted forms,
35
+ binds each value to the axis it belongs on, and renders the expression back. The binding
36
+ rule is that an axis not yet carrying a value is preferred over one that is, and required
37
+ axes come before WHO's declaration order - which is what keeps the two values of
38
+ 1D01.0Y/1G41/1G40 on the two axes a coder meant them for instead of coalescing them onto
39
+ the first, and what stops a repeated value being silently dropped. `&` asserts that what
40
+ follows is a value on an axis of the stem, so a value on none of them is an error that
41
+ names the axes; `/` asserts only cluster membership, so a member that fits an unfilled
42
+ axis is taken as a value and one that does not starts a new stem. The provider also now
43
+ answers `concept is-a` / `descendent-of` filters and text search, iterates children and
44
+ roots, tests subsumption, and builds the 25,354 postcoordination scale value sets on
45
+ demand from the database rather than shipping them
46
+
47
+ ### Changed
48
+
49
+ - The tx test runner now names its own output folder (`fhirsmith`) and labels each pass (`r4`,
50
+ `r5`, `r5-cached`), so the three passes stop writing over each other's expected/actual files.
51
+ It also passes its mode set to the validator, which was previously falling back to its own
52
+ default - one that does not include `icd-11`, so every icd-11 test came back "n/a" and was
53
+ counted as a failure. Needs validator 6.10.5 and validator-wrapper 1.4
54
+ - The R5 -> R4 cross-version conversion dropped `ValueSet.compose.property` - the element a
55
+ client uses to say which properties it wants back in an expansion. R4 and R3 have nowhere to
56
+ put it, so it now travels as
57
+ `http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.property` (the same
58
+ extension the Java convertors use) and is read back on the way up, with the extension removed
59
+ so it does not show up twice. Before this, an R4 client that asked for properties got an
60
+ expansion without them
61
+ - Every OperationOutcome the server emits now carries `details.text` and a tx-issue-type
62
+ coding in `details.coding`. `diagnostics` is stripped outright by the test harness, so
63
+ anything a client needs had to stop living *only* there -- it is still sent, and is still
64
+ the right place for server-specific detail: the four (in fact nine) private
65
+ `operationOutcome()` helpers that built `{severity, code, diagnostics}` by hand now all
66
+ delegate to `buildOperationOutcome()` in `tx/library/operation-outcome.js`, and
67
+ `Issue.asIssue()` falls back to a tx-issue-type derived from the FHIR issue type when an
68
+ Issue does not name one, so the guarantee holds without revisiting all ~170 Issue
69
+ construction sites. An informational issue that is not about a code or a value set -- the
70
+ server banner -- gets text but no coding, since it is not a tx issue
71
+
72
+ ### Fixed
73
+
74
+ - The exclude branch of `$expand` called `searchFilter(filter, prep, true)` -- the first two
75
+ arguments the wrong way round, so a provider that implements a text search would have been
76
+ handed the filter where it expected its own filter context. The other three call sites,
77
+ and the declared signature, are `(filterContext, filter, sort)`
78
+ - `$lookup` reported a code that is not in the code system as `not-found`; the code system
79
+ was found, so it is `invalid-code`, which is what the other 62 unknown-code expectations
80
+ in the tx-ecosystem suite assert
81
+ - `$lookup`'s two top-level catch blocks put `Issue.issueCode` -- a tx-issue-type -- into
82
+ `OperationOutcome.issue.code`, which takes a FHIR issue type. Two conventions were in use:
83
+ an `Issue` carries the FHIR issue type in `cause` and the tx-issue-type in `issueCode`,
84
+ while a plain Error tagged by its thrower carries the FHIR issue type in `issueCode` and
85
+ the tx-issue-type in `txIssueType`. Every catch block now goes through
86
+ `outcomeFromError()`, which is the one place that knows about both
87
+
88
+ ### Tx Conformance Statement
89
+
90
+ FHIRsmith passed all 3489 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed+mimetypes+icd-11, tests v1.9.4, runner v6.10.4)
91
+
92
+ ## [v0.12.0] - 2026-08-27
9
93
 
10
94
  ### Added
11
95
 
@@ -61,6 +145,153 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
61
145
 
62
146
  ### Fixed
63
147
 
148
+ - The XML parser could be made to loop forever, which on a single-threaded server means the
149
+ process stops answering anything at all - not just the request that carried the input. Several
150
+ scans used `indexOf` without checking for -1 and assigned the result to the cursor, sending it
151
+ backwards; `<ValueSet><x bare></x></ValueSet>` was enough, as was any `<!DOCTYPE` or an
152
+ unterminated comment. The parser has been rewritten to be total: every delimiter search fails
153
+ loudly, every loop asserts that it consumed input, nesting past 1000 elements is an error rather
154
+ than a stack overflow, and a document type declaration is refused outright (FHIR XML has no DTD,
155
+ and accepting one is how a parser ends up expanding entities on someone else's behalf).
156
+ Malformed documents now produce a positioned parse error
157
+ - SNOMED CT: normalising an expression discarded a primitive concept's defining attributes, so
158
+ real subsumption answers were lost. A primitive's definition is necessary but not sufficient, so
159
+ the concept itself must not be replaced by it - but its attributes are still entailed:
160
+ `4846001 |Anicteric viral hepatitis|` is primitive and carries an inferred
161
+ `363698007 |Finding site| = 10200004 |Liver structure|`, so `64572001:{363698007=10200004}`
162
+ really does subsume it, and the server said not-subsumed. The attributes are now added
163
+ alongside the concept, which keeps the other direction sound - the primitive stays in the focus
164
+ as an atom nothing else can match, so its conditions never become sufficient and nothing is
165
+ reported equivalent to it. Read from the concept's relationships at query time rather than from
166
+ the stored normal form, so existing caches need no rebuild
167
+ - IETF language codes (`urn:ietf:bcp:47`) are validated properly. Four things were wrong, all
168
+ visible only through the code system: the registry writes private-use allocations as ranges
169
+ (`QM..QZ`, `Qaaa..Qabx`, `qaa..qtz`, `XA..XZ`) and those were stored as literal keys nobody
170
+ could ever look up, so every code inside them read as unregistered while singly-allocated `AA`
171
+ worked; the 26 grandfathered tags were skipped at load, so registered tags like `i-klingon`
172
+ were rejected; a variant was never checked against the registry at all, so `en-abcdef` parsed
173
+ happily; and the registry's `Prefix` field - its only statement about which subtags may go
174
+ together - was ignored, so `en-cmn` passed even though `cmn` may only follow `zh`. Tags are
175
+ also matched case-insensitively now, per BCP 47 §2.1.1, so `en-us` is accepted as a way of
176
+ writing `en-US`. The stricter checks are options on the parser, applied by the code system;
177
+ Accept-Language parsing is deliberately left as it was. Redundant registry entries such as
178
+ `zh-Hans` still decompose into language plus script rather than being matched whole
179
+ - Language codes gained `=` filters on `language`, `script` and `region`, and three of the
180
+ combinations can now be expanded. The BCP 47 grammar is unbounded, so an expansion is only
181
+ possible where fixing part of a tag leaves a finite list of registry entries to vary: a fixed
182
+ language gives every region plus the bare language (344 codes), a fixed region gives every
183
+ language with it (8787, so too-costly unless paged), and language and region together give
184
+ every script plus the tag with none (275). Anything else - a script on its own, a language with
185
+ a script, an `exists` filter - answers 422 rather than pretending, and `filterMore` throws
186
+ rather than returning false so a mistake cannot surface as an empty expansion that reads like
187
+ "there are none". Every one of these is marked unclosed, since a variant, extension or
188
+ private-use subtag can always be added. A filter value that is not a registered subtag of the
189
+ right kind is refused when the filter is built, rather than silently selecting nothing
190
+ - `$subsumes` works for language codes, where it used to answer `not-subsumed` for everything
191
+ behind a comment reading "No subsumption in language codes". A tag is a set of named
192
+ components rather than an ordered path, so one tag subsumes another when it states a subset of
193
+ what the other states and the languages match: `en` subsumes `en-US`, `zh` subsumes
194
+ `zh-Hans-CN`, `de` subsumes `de-1901`. `en-US` subsumes `en-Latn-US` too, even though the added
195
+ script sits between the two subtags `en-US` names - that is RFC 4647's extended filtering
196
+ rather than basic filtering, and it is what keeps the relation transitive. Comparison is
197
+ case-insensitive, and a grandfathered tag relates only to itself, having no components to
198
+ compare. Suppress-Script is deliberately not consulted, so `en` subsumes `en-Latn` rather than
199
+ being equivalent to it
200
+ - A language code written in the wrong case now comes back with a `normalized-code` and an
201
+ information issue saying so, the way any other case-insensitive code system already behaved -
202
+ `en-us` validates, and the server answers with `en-US`. BCP 47 recommends lower case for the
203
+ language, Titlecase for the script and UPPER CASE for the region; the whole mechanism was
204
+ already in the validator, waiting on the code system to report the canonical spelling of a code
205
+ rather than echoing what the caller wrote
206
+ - Validating a language code now says which subtag is wrong and why - "the extLanguage 'cmn' in
207
+ the code 'en-cmn' may only be used with 'zh'" - instead of only "unknown code". The mechanism
208
+ for this already existed in the validator; the language provider had been discarding the reason
209
+ - `$subsumes` can now report that it cannot determine the relationship, as a 422 carrying
210
+ `cannot-determine` from the tx-issue-type code system (FHIR-58748). There is no outcome code
211
+ meaning "unknown", so a server that cannot decide has no honest answer to give and must report
212
+ an error; answering `not-subsumed` asserts something it has not established. The condition is
213
+ real in more than one code system, so the error is raised through one shared helper
214
+ (`cannotDetermineSubsumption`)
215
+ - Media types: subsumption is only decided on parameters the server understands - `charset`,
216
+ `format`, `delsp` and `version`. Reasoning that a parameter narrows the type, and that two of
217
+ its values exclude one another, are properties of that parameter's definition, and neither can
218
+ be asserted for one the server has never seen: `text/plain` vs `text/plain; foo=bar` might be
219
+ subsumption or might be the same thing. Where an unknown parameter is what the two codes differ
220
+ in, the answer is now cannot-determine. A parameter carried identically by both codes cannot
221
+ affect the relationship and is ignored, so it does not trigger a decline, and a type or subtype
222
+ mismatch stays decidable whatever parameters are present. `boundary` and `profile` are
223
+ deliberately excluded from the understood list: a differing boundary does not narrow anything
224
+ (two multipart bodies with different boundaries are the same media type) and profile values can
225
+ be hierarchical, so neither can be judged by comparing values
226
+
227
+ - SNOMED CT: `$subsumes` no longer answers `not-subsumed` when it has not established it. Comparing
228
+ normal forms is sound when it succeeds and not when it fails: SNOMED's OWL axiom reference set
229
+ carries general concept inclusions and property chains whose entailments are baked into the
230
+ distributed inferred relationships for precoordinated concepts, but a postcoordinated expression
231
+ is one the classifier never saw, so they are not available for it. Where either code is an
232
+ expression and no relationship follows structurally, the server now returns a 422 rather than
233
+ claiming there is none - the claim would be the one that wrongly drops a code out of a cohort,
234
+ and `$subsumes` has no outcome code meaning "unknown". Two plain concepts are unaffected: there
235
+ the transitive closure comes from the classifier's own output and all four outcomes are sound
236
+
237
+ - SNOMED CT: the MRCM `grouped` flag was parsed into the attribute domain rules and then never
238
+ read, so an attribute the concept model requires to be in a relationship group could be written
239
+ outside one and pass validation - `40468003:363698007=10200004` was accepted even though
240
+ `363698007 |Finding site|` is grouped and the expression is not classifiable. It is now
241
+ enforced, for expressions wherever they appear ($validate-code, $lookup and $subsumes all
242
+ validate through the same path), and the message names the attribute and shows the corrected
243
+ form. Attributes the MRCM marks as ungrouped, such as `272741003 |Laterality|`, are unaffected:
244
+ the flag is read from the concept model rather than every brace-less refinement being rejected.
245
+ SNOMED's postcoordination guide treats the ungrouped form as Close To User Form and defines a
246
+ transformation to the classifiable form; that transformation is not implemented, so the
247
+ expression is rejected rather than silently repaired
248
+ - Publisher: with no `sessionSecret` configured, the session middleware fell back to a constant
249
+ written into the source. In an open-source project that is a published signing key - anyone
250
+ could mint themselves an admin session on any deployment that had missed the setting, and
251
+ nothing about the server would look wrong. The fallback is now a random secret plus a warning,
252
+ which fails safe: logins simply do not survive a restart. Session cookies are also marked
253
+ `Secure` by default now (set `modules.publisher.cookieSecure` false to run over plain HTTP),
254
+ and `POST /login` is rate limited - it is the one unauthenticated route that runs bcrypt, and
255
+ bcrypt holds a libuv threadpool thread for ~100ms, so unlimited attempts stall the server's
256
+ file I/O as well as inviting guessing
257
+ - Publisher: twenty more values reached the task, history and admin pages unescaped -
258
+ `npm_package_id`, `version`, `status`, `user_name`, `website_name`, `local_folder`,
259
+ `failure_reason`, the task log messages, and the user and website names on the admin pages.
260
+ Most are constrained elsewhere, but `failure_reason` and the log messages carry build output
261
+ from the IG being published, which is not. The earlier fix covered `github_org`, `github_repo`
262
+ and `git_branch` only; output escaping is the boundary that has to be complete to be worth
263
+ anything
264
+ - The static file handler for `server.webBase` built its filename with `path.join(webBase, req.path)`
265
+ and no containment check, so a literal `..` in the request path walked straight out of the
266
+ directory: `GET /../../etc/passwd` read any file the server user could read. (The encoded forms
267
+ never worked - Express does not decode `req.path` - and a proxy in front normalises `..` away,
268
+ which is why this survived.) Paths are now resolved and required to stay inside the configured
269
+ directory, in `library/path-safety.js` so the rule is testable and reusable; a request that
270
+ escapes falls through to the next handler instead of being served
271
+ - `POST /packages/crawl` triggered a full registry crawl for anyone who asked. It now requires a
272
+ shared secret, `modules.packages.crawlToken`, in an `x-crawl-token` header, compared in constant
273
+ time. With no token configured the endpoint is disabled rather than open: an administrative
274
+ trigger that silently defaults to unauthenticated is how a deployment ends up exposed with
275
+ nothing about it looking wrong. The scheduled crawler is unaffected
276
+
277
+ - `CodeSystem.concept` and `ValueSet.expansion.contains` nest without limit in FHIR, and both are
278
+ walked recursively in a dozen places - the expansion map, the concept counts, the renderer, the
279
+ expander's index, the R5-to-R4 converter. A resource of a couple of hundred KB could nest deeply
280
+ enough to exhaust the JS stack, and hardening those walkers one at a time would not have been
281
+ enough anyway: `JSON.stringify` itself, which we do not control, overflows at a nesting depth of
282
+ about 1800 on node 22, so a deep enough resource could not be serialised even if every walker
283
+ were iterative. Nesting is now bounded once, at construction, at 100 levels - far past SNOMED
284
+ CT's ~30 or LOINC's ~10, and well clear of anything that breaks - which makes every walker safe
285
+ by construction, including ones not yet written. The bounding walk is iterative and doubles as
286
+ the concept count, so the tree is still traversed only once, and an over-nested resource comes
287
+ back as a 400 rather than a 500
288
+
289
+ - Publisher: the task output page wrote `github_org`, `github_repo` and `git_branch` into the HTML
290
+ without escaping. The first two are constrained to letters, digits and punctuation, but a branch
291
+ name was only checked against git's own rules, which say nothing about markup - so a branch
292
+ called `<script>...</script>` was stored XSS on a page that needs no login to read. The fields
293
+ are escaped now, and a branch name carrying `< > " ' &` is refused at the boundary as well
294
+
64
295
  - `$expand`: `status` was lost from imported property declarations, and a concept carrying the
65
296
  same property more than once had the repeats collapsed to a single value
66
297
  - Expansion properties are de-duplicated when they arrive from more than one place (the request,
@@ -70,12 +301,100 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
70
301
  the field the cache key hashes
71
302
  - Boolean parameters passed as strings (as they always are on a GET) are now accepted, which
72
303
  revives five parameters that were dead on GET requests
304
+ - `$subsumes`: an unknown `codeA`/`codeB` returned a bare OperationOutcome - no
305
+ `operationoutcome-message-id` extension and no `tx-issue-type#invalid-code` detail code - so
306
+ clients could not tell what kind of failure it was. It now reports the same issue that
307
+ `$validate-code` does for an unknown code
308
+ - SNOMED CT import did not record definition status, so every concept looked fully defined. A
309
+ normal form was then generated for concepts that have no definition to expand, and the result
310
+ was stored unparseable - which made `$subsumes` fail with a 500 for any post-coordinated
311
+ expression. **Caches built by the affected importer have to be rebuilt**
312
+ - SNOMED CT `isPrimitive` tested bit 0 of the concept flags, which is part of the status, instead
313
+ of the primitive bit
314
+ - A concept's own stored normal form was run through the MRCM postcoordination checks when it was
315
+ read back. Those rules are about expressions a client sends, not about precoordinated content,
316
+ and they rejected the normal form of any concept defined with a precoordinated-only attribute
317
+ - The expression parser did not resolve attribute names to concept references, so refinements were
318
+ compared by reference in one direction and by code in the other. Subsumption between two refined
319
+ expressions could come out true one way and false the other
320
+ - Subsumption checked each refinement group against only the first group whose attribute names
321
+ lined up. Where several groups share attribute names, the answer depended on the order the
322
+ groups happened to be in after normalisation
323
+ - `$subsumes` reported an unexpected error in `diagnostics`, which the test runner strips from
324
+ every issue it compares, so a failing test showed nothing but `severity`/`code`. The message now
325
+ goes in `details.text` (the same helper elsewhere in the server has the same problem)
326
+ - `$subsumes` ignored a front-loaded cache on a GET: it only looked for `tx-resource` and the
327
+ cache-id when the request had a Parameters body, so a client that put its code systems in a
328
+ server-side cache and sent the id in `X-Cache-Id` got "could not be found" for every one of
329
+ them
330
+ - `$subsumes` reported an unknown code against `codeA`/`codeB` in `expression` even when the code
331
+ came in as `codingA`/`codingB`
332
+
333
+ ### Changed
334
+
335
+ - `$subsumes` on UCUM now answers `equivalent` for two different codes that mean the same unit -
336
+ `1/min` and `min-1`, `N` and `kg.m/s2`, `mg{total}` and `mg`. UCUM still has no hierarchy, so
337
+ nothing ever subsumes anything; this compares the canonical *form* (which carries the conversion
338
+ factor), not the canonical units, so `m` and `cm` remain distinct rather than collapsing onto
339
+ their shared canonical unit
340
+ - `$subsumes` on LOINC is implemented against the multiaxial hierarchy. The importer already
341
+ builds a full transitive closure from `PATH_TO_ROOT` in ComponentHierarchyBySystem - the same
342
+ table the `is-a` / `descendent-of` filters use - so subsumption and expansion now agree.
343
+ Relationships run Part-to-Part and Part-to-code, so two LOINC codes still never subsume each
344
+ other
345
+ - Mime types support a `base` filter, taking either `type` or `type/subtype`, which selects the
346
+ codes consistent with it - parameters on the code are ignored, so `text/plain; charset=utf-8`
347
+ matches a base of `text` and of `text/plain`. Media types cannot be enumerated, so the filter
348
+ validates a code rather than expanding; a value carrying parameters, a wildcard, or anything
349
+ that is not a type or type/subtype is rejected
350
+ - A `registered = true` mime type filter can be expanded: the IANA registry is a finite list, so
351
+ the expander walks it, and the expansion is marked `valueset-unclosed` because every registered
352
+ type also has unboundedly many parameterised forms that belong to the value set. A second filter
353
+ in the same include still narrows it, so `registered = true` + `base = text/plain` expands to
354
+ one code
355
+ - Everything else that cannot be enumerated - the whole code system, a `base` filter on its own,
356
+ `registered = false` - answers 422 `not-supported` with `CODESYSTEM_NOT_ENUMERABLE`, the same as
357
+ HGVS, instead of the 500 exception a filter used to raise. Expanding every registered media type
358
+ at once is a couple of thousand codes, so without a `count` it answers `too-costly` like any
359
+ other oversized expansion
360
+ - Mime types support a `registered` filter (`true`/`false`), testing whether a media type is in
361
+ the IANA registry. The provider downloads
362
+ https://www.iana.org/assignments/media-types/media-types.xml into the terminology cache at
363
+ startup and keeps only the names; a failed download falls back to the cached copy, and if there
364
+ is no copy at all, using the filter reports that rather than answering. The download is written
365
+ to a temporary file and only replaces the cached copy once it has parsed, so a truncated
366
+ transfer cannot leave behind a registry that looks valid but is short
367
+ - `$subsumes` on mime types understands parameters. A parameter narrows the media type, so
368
+ `text/plain` subsumes `text/plain; charset=utf-8`, and two spellings of the same type are
369
+ equivalent - type, subtype and parameter names are matched case-insensitively, as are charset
370
+ values, and quoted values are unquoted. Type and subtype themselves still have no hierarchy,
371
+ so a structured syntax suffix does not make `application/fhir+xml` a kind of `application/xml`
372
+ - **Breaking (LOINC import):** the accessory files are now required, not optional. An import
373
+ missing ComponentHierarchyBySystem previously succeeded and produced a cache with an empty
374
+ closure, so the server answered `not-subsumed` to everything with no way to tell that apart
375
+ from a real answer; the same argument applies to the part links and answer lists. Only the
376
+ linguistic variants stay optional, since a main-only import is a reasonable thing to want and
377
+ their absence costs designations rather than correctness
378
+
379
+ - The mime type tests are their own test mode (`mimetypes`) rather than part of `general`, and
380
+ FHIRsmith's test run declares it. fhir-core gains it in all four places the mode set is
381
+ declared: the runner default (`TxTestHTTPHandler`) and the local and external terminology
382
+ service test runs, which will be red until tx-dev implements the filters
383
+
384
+ ### Removed
385
+
386
+ - `POST /packages/update-package`, along with `forceUpdatePackage` and
387
+ `deleteVersionsByIdVersion`. Its authentication was conditional on `updateToken`, which was set
388
+ in no configuration file and not even mentioned in the template, so in practice the endpoint let
389
+ anyone hand the server a URL to fetch and store - server-side request forgery against anything
390
+ the host can reach, and a write into the package registry. It existed to push out a corrected
391
+ package after a bad publish; that is rare enough to do by hand
73
392
 
74
393
  ### Tx Conformance Statement
75
394
 
76
395
  FHIRsmith passed all 2822 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.3, runner v6.10.3)
77
396
 
78
- ## [0.11.2] - 2026-08-12
397
+ ## [v0.11.2] - 2026-08-12
79
398
 
80
399
  ### Fixed
81
400
 
@@ -90,7 +409,7 @@ FHIRsmith passed all 2822 HL7 terminology service tests (modes tx.fhir.org+omop+
90
409
 
91
410
  FHIRsmith passed all 2729 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.3, runner v6.10.1)
92
411
 
93
- ## [0.11.1] - 2026-07-30
412
+ ## [v0.11.1] - 2026-07-30
94
413
 
95
414
  ### Added
96
415
 
@@ -124,7 +443,7 @@ FHIRsmith passed all 2729 HL7 terminology service tests (modes tx.fhir.org+omop+
124
443
 
125
444
  FHIRsmith passed all 2729 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.2, runner v6.9.12)
126
445
 
127
- ## [0.11.0] - 2026-mm-dd
446
+ ## [v0.11.0] - 2026-mm-dd
128
447
 
129
448
  ### Added
130
449
 
package/README.md CHANGED
@@ -283,7 +283,7 @@ GitHub Actions will automatically:
283
283
  ### Tx Conformance Statement
284
284
  {copy content from text-cases-summary.txt}
285
285
  ```
286
- 2. Update `package.json` to have the same release version
286
+ 2. Update `package.json` & `package-lock.json` to have the same release version
287
287
 
288
288
  3. Commit your changes:
289
289
  ```bash
@@ -58,6 +58,10 @@
58
58
  "enabled": false,
59
59
  "database": "/absolute/path/to/packages.db",
60
60
  "mirrorPath": "/absolute/path/to/mirror",
61
+ // shared secret for POST /packages/crawl, which triggers a full registry crawl on
62
+ // demand. Send it in the x-crawl-token header. Omit this and the endpoint is
63
+ // disabled entirely - the scheduled crawler below is unaffected either way.
64
+ "crawlToken": "change-this-to-a-secure-random-string",
61
65
  "crawler": {
62
66
  "enabled": true,
63
67
  "schedule": "0 * * * *"
@@ -74,7 +78,16 @@
74
78
  "publisher": {
75
79
  "enabled": true,
76
80
  "database": "/absolute/path/to/publisher.db",
81
+ // required. Without it the publisher generates a random secret at startup, logs a
82
+ // warning, and logins stop working across restarts.
77
83
  "sessionSecret": "change-this-to-a-secure-random-string",
84
+ // session cookies are marked Secure by default, so they only travel over HTTPS.
85
+ // Set false only to run the publisher over plain HTTP - otherwise the browser will
86
+ // not send the cookie back and logins will appear to do nothing. Behind a TLS
87
+ // proxy, make sure it sets X-Forwarded-Proto.
88
+ "cookieSecure": true,
89
+ // failed-login attempts allowed per IP per 15 minutes (default 20)
90
+ "loginAttemptsPerWindow": 20,
78
91
  "workspaceRoot": "/absolute/path/to/workspaces",
79
92
  "igPublisherTimeoutMinutes": 60,
80
93
  // where to put files the IG Publisher leaves in the web folder that are too big for GitHub