fhirsmith 0.18.2 → 1.0.0

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 (203) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +149 -0
  3. package/CONFIGURATION.md +305 -0
  4. package/LICENSE +29 -0
  5. package/README.md +29 -122
  6. package/cluster.js +1220 -0
  7. package/cluster.md +707 -0
  8. package/config-template.json +77 -8
  9. package/extension-tracker/extension-tracker-template.html +5 -8
  10. package/extension-tracker/extension-tracker.js +0 -2
  11. package/folder/folder.js +7 -6
  12. package/ips/analysis.js +560 -0
  13. package/ips/ips-template.html +5 -8
  14. package/ips/ips.js +132 -59
  15. package/ips/nationality.js +120 -0
  16. package/ips/pipeline.js +74 -21
  17. package/ips/presenter.js +1 -1
  18. package/ips/readme.md +132 -25
  19. package/ips/render.js +466 -77
  20. package/ips/store.js +79 -22
  21. package/ips/structure.js +247 -0
  22. package/ips/terminology.js +1 -1
  23. package/library/body-limit.js +22 -0
  24. package/library/cache-generation.js +15 -0
  25. package/library/cgroup-memory.js +133 -1
  26. package/library/cluster-counts.js +121 -0
  27. package/library/cluster-events.js +153 -0
  28. package/library/cluster-metrics.js +193 -0
  29. package/library/cluster-status.js +468 -0
  30. package/library/config-reader.js +96 -0
  31. package/library/daily-report.js +100 -2
  32. package/library/html-server.js +27 -1
  33. package/library/i18nsupport.js +23 -1
  34. package/library/keep-alive.js +18 -0
  35. package/library/languages.js +17 -3
  36. package/library/logger.js +31 -0
  37. package/library/memory-guard.js +428 -0
  38. package/library/native-memory.js +52 -0
  39. package/library/npm-audit.js +51 -25
  40. package/library/openapi-doc.js +7 -5
  41. package/library/package-manager.js +8 -0
  42. package/library/role.js +197 -0
  43. package/library/server-credentials.js +169 -0
  44. package/library/sqlite-utilities.js +59 -0
  45. package/library/ssrf-guard.js +100 -0
  46. package/library/static-response-stats.js +16 -0
  47. package/library/version-utilities.js +50 -0
  48. package/library/worker-count.js +192 -0
  49. package/nginx.md +30 -11
  50. package/npmprojector/indexer.js +18 -10
  51. package/package.json +11 -8
  52. package/packages/feed-check-page.js +117 -0
  53. package/packages/feed-check.js +279 -0
  54. package/packages/npm-check.js +57 -3
  55. package/packages/npm-signing.js +219 -0
  56. package/packages/openapi.yaml +207 -1
  57. package/packages/package-crawler.js +322 -41
  58. package/packages/packages-template.html +6 -8
  59. package/packages/packages.js +604 -57
  60. package/packages/readme.md +113 -1
  61. package/publisher/publisher-template.html +2 -5
  62. package/publisher/publisher.js +121 -110
  63. package/publisher/readme.md +75 -0
  64. package/registry/crawler.js +84 -108
  65. package/registry/documentation/configuration.md +34 -0
  66. package/registry/registry-template.html +5 -8
  67. package/registry/registry.js +5 -4
  68. package/root-bare-template.html +7 -4
  69. package/root-template.html +5 -8
  70. package/server.js +398 -198
  71. package/shl/shl.js +38 -37
  72. package/static/a11y/a11y-check.js +832 -0
  73. package/static/a11y/axe.min.js +12 -0
  74. package/static/assets/css/bootstrap-fhir.css +15 -15
  75. package/static/assets/images/flags/au.svg +18 -0
  76. package/static/assets/images/flags/unknown.svg +11 -0
  77. package/static/assets/images/flags/us.svg +63 -0
  78. package/static/assets/images/icon_audit.png +0 -0
  79. package/static/assets/images/icon_clinical.png +0 -0
  80. package/static/assets/images/icon_document.png +0 -0
  81. package/static/assets/images/icon_external.png +0 -0
  82. package/static/assets/images/icon_medication.png +0 -0
  83. package/static/assets/images/icon_organization.png +0 -0
  84. package/static/assets/images/icon_other.png +0 -0
  85. package/static/assets/images/icon_person.png +0 -0
  86. package/static/assets/images/icon_section.png +0 -0
  87. package/stats-db.js +45 -0
  88. package/stats.js +197 -10
  89. package/testing/testing-template.html +5 -8
  90. package/token/nginx_passport_setup.md +1 -1
  91. package/token/security_guide.md +1 -1
  92. package/token/token-template.html +2 -5
  93. package/token/token.js +40 -36
  94. package/translations/Messages.properties +19 -0
  95. package/tx/README.md +154 -4
  96. package/tx/closure/closure-store.js +126 -3
  97. package/tx/cm/cm-database.js +69 -57
  98. package/tx/cm/cm-package.js +29 -1
  99. package/tx/cs/cs-api.js +284 -19
  100. package/tx/cs/cs-api.md +41 -4
  101. package/tx/cs/cs-areacode.js +1 -1
  102. package/tx/cs/cs-country.js +17 -5
  103. package/tx/cs/cs-cpt.js +33 -16
  104. package/tx/cs/cs-cs.js +148 -14
  105. package/tx/cs/cs-currency.js +1 -1
  106. package/tx/cs/cs-icd11.js +21 -13
  107. package/tx/cs/cs-icf.js +22 -3
  108. package/tx/cs/cs-lang.js +2 -2
  109. package/tx/cs/cs-loinc.js +185 -66
  110. package/tx/cs/cs-mimetypes.js +12 -2
  111. package/tx/cs/cs-ndc.js +40 -25
  112. package/tx/cs/cs-omop.js +54 -29
  113. package/tx/cs/cs-rxnorm.js +300 -196
  114. package/tx/cs/cs-snomed.js +608 -64
  115. package/tx/cs/cs-ucum.js +14 -4
  116. package/tx/cs/cs-unii.js +18 -14
  117. package/tx/cs/cs-virtual.js +210 -0
  118. package/tx/ecosystem-resolver.js +413 -0
  119. package/tx/html/cluster-dash-metrics.liquid +26 -0
  120. package/tx/html/cluster-home-metrics.liquid +31 -0
  121. package/tx/html/cluster-metrics-script.liquid +251 -0
  122. package/tx/html/codesystem-operations.liquid +3 -3
  123. package/tx/html/ecl-panel.liquid +1 -1
  124. package/tx/html/external-metrics.liquid +17 -0
  125. package/tx/html/operations-form.liquid +17 -17
  126. package/tx/html/search-form.liquid +11 -11
  127. package/tx/html/status-line.liquid +58 -0
  128. package/tx/html/status-panel.liquid +140 -0
  129. package/tx/html/status-tasks.liquid +25 -0
  130. package/tx/html/tx-template.html +2 -5
  131. package/tx/importers/atc-to-fhir.js +17 -16
  132. package/tx/importers/export-icd11-codesystem.js +3 -2
  133. package/tx/importers/import-icd11.js +4 -3
  134. package/tx/importers/import-loinc.module.js +118 -56
  135. package/tx/importers/import-ndc.module.js +56 -44
  136. package/tx/importers/import-rxnorm.module.js +118 -79
  137. package/tx/importers/import-sct.module.js +57 -58
  138. package/tx/importers/import-unii.module.js +37 -36
  139. package/tx/importers/subset-loinc.module.js +49 -48
  140. package/tx/importers/subset-rxnorm.module.js +29 -28
  141. package/tx/importers/terminal.js +18 -0
  142. package/tx/importers/tx-import-base.js +13 -12
  143. package/tx/importers/tx-import-settings.js +5 -4
  144. package/tx/importers/tx-import.js +53 -52
  145. package/tx/library/canonical-resource.js +49 -0
  146. package/tx/library/codesystem.js +94 -5
  147. package/tx/library/designations.js +45 -2
  148. package/tx/library/extensions.js +13 -7
  149. package/tx/library/implicit-valuesets.js +66 -0
  150. package/tx/library/renderer.js +77 -21
  151. package/tx/library/sqlite-utilities.js +2 -0
  152. package/tx/library/ucum-parsers.js +11 -1
  153. package/tx/library/wait-for-cache.js +43 -0
  154. package/tx/library.js +321 -22
  155. package/tx/openapi-operations.js +6 -4
  156. package/tx/operation-context.js +460 -99
  157. package/tx/params.js +25 -0
  158. package/tx/problems.js +3 -5
  159. package/tx/provider.js +278 -42
  160. package/tx/resource-cache-sqlite.js +1050 -0
  161. package/tx/sct/ecl.js +47 -6
  162. package/tx/sct/editions.js +39 -0
  163. package/tx/sct/expressions.js +10 -1
  164. package/tx/sct/load-config.js +107 -0
  165. package/tx/sct/mmap.js +109 -0
  166. package/tx/sct/structures.js +166 -13
  167. package/tx/sct/text-index.js +5 -1
  168. package/tx/syndication/syndication-feed.js +2 -2
  169. package/tx/tx-html.js +44 -8
  170. package/tx/tx.fhir.org.yml +8 -8
  171. package/tx/tx.js +282 -80
  172. package/tx/usage-tracker.js +39 -11
  173. package/tx/vs/vs-api.js +19 -0
  174. package/tx/vs/vs-database.js +386 -62
  175. package/tx/vs/vs-package.js +27 -4
  176. package/tx/vs/vs-vsac.js +648 -100
  177. package/tx/workers/batch-validate.js +37 -10
  178. package/tx/workers/batch.js +6 -1
  179. package/tx/workers/cache-control.js +15 -4
  180. package/tx/workers/closure.js +156 -13
  181. package/tx/workers/compare.js +8 -5
  182. package/tx/workers/expand.js +1173 -289
  183. package/tx/workers/metadata.js +35 -0
  184. package/tx/workers/search.js +53 -1
  185. package/tx/workers/supplement-filters.js +145 -0
  186. package/tx/workers/translate.js +169 -4
  187. package/tx/workers/validate.js +223 -119
  188. package/tx/workers/worker.js +123 -31
  189. package/tx/xversion/xv-valueset.js +7 -4
  190. package/utilities/submit-test-report.js +3 -1
  191. package/utilities/tx-stress-test.js +482 -0
  192. package/vcl/readme.md +37 -0
  193. package/vcl/vcl-parser.js +15 -0
  194. package/xig/readme.md +52 -0
  195. package/xig/xig-template.html +5 -8
  196. package/xig/xig.js +238 -422
  197. package/changelog-template.md +0 -22
  198. package/registry/test-runner.js +0 -237
  199. package/release.sh +0 -80
  200. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  201. package/tsconfig.eslint.json +0 -9
  202. package/tx/html/dash-metrics.liquid +0 -147
  203. package/tx/html/home-metrics.liquid +0 -225
@@ -0,0 +1,119 @@
1
+ # Accessibility
2
+
3
+ ## Project Information
4
+
5
+ | Field | Value |
6
+ |---|---|
7
+ | Project | FHIRsmith |
8
+ | Owner | Health Intersections |
9
+ | Issue tracker | https://github.com/HealthIntersections/FHIRsmith/issues |
10
+ | Target standard | WCAG 2.0 Level AA (at least) |
11
+ | Conformance status | Target only - not yet evaluated |
12
+ | Last reviewed | 2026-11-05 |
13
+
14
+ ## Commitment
15
+
16
+ FHIRsmith aims to be usable by everyone, including people who use assistive
17
+ technologies such as screen readers, screen magnifiers, voice control and
18
+ keyboard-only navigation.
19
+
20
+ We aspire to ensure that the FHIRsmith web UI meets the
21
+ [Web Content Accessibility Guidelines (WCAG) 2.0](https://www.w3.org/TR/WCAG20/)
22
+ at Level AA, at least.
23
+
24
+ Our approach is to make the default HTML accessible wherever we can do so
25
+ without significantly degrading the usual user experience (e.g. semantic
26
+ markup, table headers, form labels, keyboard access, not relying on colour).
27
+
28
+ Where meeting the guidelines would make the usual experience significantly
29
+ worse (e.g. dense tables or interactive tree views), FHIRsmith will provide
30
+ an option (a checkbox in the UI) that switches the generated HTML to a form
31
+ that does meet them. The setting applies to all pages for the remainder of
32
+ the user's session.
33
+
34
+ ## Scope
35
+
36
+ ### In scope
37
+
38
+ The **web user interface** that FHIRsmith presents in a browser: the home
39
+ page, the module pages (tx, packages, registry, xig, publisher, ips, shl,
40
+ testing etc.), and the HTML renderings of resources.
41
+
42
+ ### Not in scope
43
+
44
+ FHIRsmith is primarily a server. Most of what it does is provide FHIR APIs
45
+ (terminology, packages, registry, etc.) that are consumed by other software,
46
+ not by people directly. Those APIs return machine-readable content (JSON, XML,
47
+ npm packages), and their accessibility is the responsibility of the client
48
+ applications that present that content to users.
49
+
50
+ ### Third-party content
51
+
52
+ Some HTML content shown by FHIRsmith (e.g. resource narratives) is authored
53
+ by third parties and published in packages. FHIRsmith presents that content
54
+ as provided and cannot guarantee its accessibility. Accessibility problems in
55
+ that content should be reported to the publisher of the package concerned.
56
+
57
+ ## Conformance Status
58
+
59
+ **Target only.** Conformance has not yet been formally assessed, and no
60
+ claim of conformance is made.
61
+
62
+ ## Known Limitations
63
+
64
+ Pages have not yet been reviewed. Areas that are likely to present
65
+ difficulties include:
66
+
67
+ * Large tables (e.g. value set expansions, package listings, search results)
68
+ * Interactive tree views and editors
69
+ * Colour used to convey status
70
+
71
+ This section will be updated as pages are reviewed.
72
+
73
+ ## Checking Pages
74
+
75
+ Setting `accessibility.checkPanel` to `true` in the server's config adds an
76
+ accessibility check panel to the bottom of every page. Pressing its button
77
+ checks the page as it is in the browser at that moment:
78
+
79
+ * Against WCAG 2.0 A and AA, using [axe-core](https://github.com/dequelabs/axe-core)
80
+ * For heading structure (exactly one top level heading, which comes first,
81
+ and no skipped levels), labels that point at nothing, accesskeys,
82
+ placeholder alt text, generic link text, and click handlers on elements
83
+ that can't be reached from the keyboard
84
+ * For links distinguished from the surrounding text by colour alone
85
+
86
+ Problems are reported by HHS Section 508 checklist item, and marked on the
87
+ page. axe-core is only loaded when the button is pressed. Like all automated
88
+ checks, these find only some problems.
89
+
90
+ ## Reporting an Accessibility Barrier
91
+
92
+ If you find part of the FHIRsmith web UI that is difficult or impossible to
93
+ use with assistive technology, please
94
+ [create an issue on GitHub](https://github.com/HealthIntersections/FHIRsmith/issues)
95
+ and include "Accessibility" in the title. Helpful details include:
96
+
97
+ * The URL of the page
98
+ * What you were trying to do
99
+ * The browser and assistive technology (and versions) you were using
100
+ * What happened, and what you expected
101
+
102
+ Accessibility issues are treated as bugs, not feature requests.
103
+
104
+ ## Contributor Requirements
105
+
106
+ Contributions that improve accessibility are welcome. When contributing
107
+ changes to the web UI, please:
108
+
109
+ * Use semantic HTML (headings, lists, `<table>` with `<th>`/`scope`, `<label>` for form controls)
110
+ * Make sure everything works with the keyboard alone
111
+ * Provide text alternatives for images and icons
112
+ * Don't rely on colour alone to convey meaning
113
+ * If the page has an accessible-mode variant, check that it still works
114
+
115
+ ## References
116
+
117
+ * [WCAG 2.0](https://www.w3.org/TR/WCAG20/)
118
+ * [ACCESSIBILITY.md template](https://mgifford.github.io/ACCESSIBILITY.md/)
119
+ * [Open Source Accessibility Framework](https://open-source-accessibility.github.io/Open-Source-Accessibility-Framework/foundational)
package/CHANGELOG.md CHANGED
@@ -5,6 +5,155 @@ All notable changes to Health Intersections FHIRsmith will be documented in this
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
+ ## [1.0.0] - 2026-11-12
9
+
10
+ **Upgrading**: a big upgrade - every operator should check their settings, though some might not need to change any. See [Upgrading to 1.0.0](CONFIGURATION.md#upgrading-to-100) for what to check.
11
+
12
+ Major new features:
13
+
14
+ - Cluster mode: a master and several tx-workers on one machine and data directory, so terminology can use more than one CPU core (`node cluster.js`, see [cluster.md](cluster.md))
15
+ - Memory protection: a memory guard that refuses new requests (503) when memory is short and aborts running operations near the limit, caps on the caches that could grow without bound, VSAC value sets read on demand, and SNOMED CT caches optionally memory-mapped
16
+ - Lots of performance improvements, particularly in `$expand` paging, `$validate-code` and `$batch-validate-code`
17
+ - Lots of bug fixes around terminology handling (SNOMED CT, LOINC, RxNorm, supplements, filters, limits)
18
+ - Ecosystem resolution: `$validate-code` for code systems this server doesn't have, by asking other servers in the terminology ecosystem
19
+ - Package signatures: npm registry signatures, and publishers' signatures checked by the crawler
20
+ - IPS module
21
+
22
+ ### Security
23
+
24
+ - Publisher: the administrator (`admin`) was created with a fixed password, `admin123`, and there is no page to change a password. Its password is now the module's `adminPassword` setting, set at every start; without one, the administrator gets a random password (a UUID), which is logged. An administrator that still has `admin123` is given a random one at startup
25
+ - Parsers have depth and size limits: ECL, VCL, SNOMED CT expressions and UCUM units nested past the limit were a stack overflow (RangeError) rather than a parse error, and an unterminated VCL filter list looped until memory ran out
26
+ - SNOMED CT ECL `wild:` terms are matched with re2js: as native regular expressions, a term with many `*`s took exponential time
27
+ - Token module: the session cookie is `Secure` when the request came over https (`req.secure`, which follows nginx's `X-Forwarded-Proto` with `server.trustProxy`; `cookieSecure` overrides). It depended on `NODE_ENV`, which nothing set, so it was never Secure
28
+
29
+ ### Added
30
+
31
+ - `config.json` may have comments: `//` to the end of the line, and `/* ... */` (`library/config-reader.js`). A syntax error is reported at its line and column in the file
32
+ - A LICENSE file (BSD 3-Clause); the package's licence is now the SPDX id `BSD-3-Clause` (was `BSD-3`)
33
+ - Memory guard (`server.memory`): above `rejectFraction` (0.85) of the memory limit (the cgroup's - systemd `MemoryMax` - or else the machine's RAM) new requests get 503 + `Retry-After`; above `abortFraction` (0.92) running operations are aborted (422 too-costly). While it refuses, it forces a GC (so start node with `--expose-gc`, as `npm start` and the Docker image now do), and, with `trimAllocator` (on by default only with a role), returns the C allocator's free memory. A client that disconnects, or a memory abort, now stops the whole operation, `$batch` and `$batch-validate-code` included
34
+ - Accessibility: [ACCESSIBILITY.md](ACCESSIBILITY.md) (the target is WCAG 2.0 Level AA, at least), and `accessibility.checkPanel`, a button at the bottom of every page that checks the page against WCAG 2.0 A/AA (axe-core, loaded only when pressed) and its heading structure
35
+ - The largest request body the server accepts is a setting, `server.maxBodyMB` (default 50, as before; about 36x the largest IG Publisher request seen on build.fhir.org, 1.39 MB)
36
+ - Packages
37
+ - `/packages/feed-check` - enter a package feed's URL, and it is read the way the crawler reads it, with nothing stored: for each item, the package (downloaded and checked as the crawler checks it), whether the registry's package-restrictions allow it from that feed, its signature, and whether the registry already holds it (and if it's the same bytes); or what's wrong with the feed itself. The feed's signing keys can be given as JSON (its package-feeds.json `signing` block), to test a feed before its keys are registered. Runs in the background (the report page reloads until done), one at a time, SSRF-guarded, http(s) only (`feedCheck` settings)
38
+ - publishers' signatures. A package can be signed by its publisher (the IG Publisher does, with a key configured in `fhir-settings.json`): a partner file, `{package}.tgz.sigstore.json`, linked from the feed item by `fhir:signature`, checked against the keys `package-feeds.json` lists for the feed (`feeds[].signing`). `crawler.signatures`: `ignore`, `check` (the default: a signature that fails is logged and the package stored anyway, unless the feed says `signing.required`) or `require` (unsigned or badly signed packages are refused, and feeds that list no keys aren't read). Packages already held are checked against their stored bytes when their feed first carries a signature. Each signature file is kept, as checked, in a new `PackageSignatures` table, and the HTML package lists show each version's signature as confirmed, rejected or absent. The package document has `dist.fhir-signature` (`url`, `status`, `keyid`) for each version with a signature, and `/packages/-/fhir/v1/signatures/{name}@{version}` serves the signature as fetched. Not `dist.attestations`, which npm would fail to verify (see the packages readme)
39
+ - npm registry signatures, so `npm audit signatures` can check packages installed from the package server. Each version in a package document now has `dist.integrity` (sha512 SRI) and `dist.signatures` (ECDSA P-256 over `{id}@{version}:{integrity}`, as the npm registry signs), and `/packages/-/npm/v1/keys` lists the key. The signature is the registry's, so every version is signed, wherever it was crawled from. On by default (`npmSigning.enabled`); the key is `npmSigning.keyFile`, default `npm-signing-key.pem` next to the database, made at startup if it isn't there. Versions are signed when stored; existing ones are signed in the background at startup (progress in `/packages/status`), and the keys endpoint is a 404 until they all are. Integrity and signatures are in a new `PackageDist` table. `/packages/npm-check` checks signatures too, when the registry lists keys
40
+ - Terminology Ecosystem
41
+ - OAuth2 client credentials (`oauth`, by server code) for servers that need a token to be scanned (NZHTS); such endpoints are reported as `security: oauth`
42
+ - Terminology
43
+ - A `fhirsmith-cluster` command (`cluster.js`), alongside `fhirsmith` (`server.js`), for a global npm install
44
+ - Cluster: the daily report has a Cluster paragraph: when the launcher started; for each process that exited or was recycled, how many times and why, how long it was unavailable, and the requests lost to it (504, 502, cache-ids that came while it restarted); and the requests the launcher refused itself (503) and the connections it closed for write timeouts. The launcher sends the master what it saw over IPC, and the master keeps it in `stats.db` (`cluster_events`)
45
+ - Cluster: `server.js --role=master|tx-worker` (`--instance-code`, `--port`), and a launcher, `cluster.js`, that starts a master and tx-workers (by default, as many as the CPU cores and the memory limit allow) and sends each request to the right process. The master runs every module; the tx-workers answer terminology requests, opening the master's databases and caches read-only, and never building or downloading them. Settings are in `server.cluster` (see [cluster.md](cluster.md)): with a role a process listens on 127.0.0.1 (`bindAddress`); options `sharedCache` (the `$cache-control` caches in one SQLite file, `databases/resource-cache.db`, at most `sharedCacheMaxMB`, default 256, so any process can answer any cache-id) and `sharedClosure` (every process answers `$closure`), both off by default. The home page and dashboard graph every process, and count every process's requests. With no role, nothing changes
46
+ - SNOMED CT: how each edition's cache file is loaded is set in `modules.tx.snomed`: `load` = `read` (the default, as before) or `mmap` (memory-mapped, so pages are only resident when used and can be dropped under memory pressure; needs the optional dependency koffi, and falls back to read where mapping isn't available), `prewarm` (editions mapped and read through once at startup, in the background) and `read` (editions always read into memory). The memory guard leaves out the file-backed pages of mapped editions
47
+ - LOINC: the `answer-list` filter (`answer-list = [LA code]`: the answer lists the answer is in), as agreed with LOINC (UP-645) and tested by the tx-ecosystem `loinc-*-filter-answer-list` tests. `answers-for` is the defined filter for the answers in a list; `LIST` is still accepted, for backwards compatibility only
48
+ - Extension-codes supplements: a code in a code space the code system reserves for users (`codesystem-reservedCodeSpace`, a regular expression) is a valid code, whether or not anything defines it; with nothing defining it, its display is the code space's `default-display`, or 'User-assigned', and (as a placeholder) another display only gets an information issue. An `extension-codes` supplement in scope defines such codes, with their displays: a global one as if the code system defined them (a different display is an error, or a warning with lenient display validation), one that is referenced as a display that isn't the only correct one (a different display is a warning). Codes from supplements are only in an expansion when the value set lists them. ISO 3166's user-assigned codes work the same way: an extension-codes supplement's display replaces the 'User-assigned' placeholder. So do ICF's qualified (post-coordinated) codes and SNOMED CT post-coordinated expressions (matched as written, or in their minimal rendering): an extension-codes supplement's display is the code's display, and the generated one stays valid
49
+ - Reserved code spaces: an expansion of all of a code system that reserves code spaces (`codesystem-reservedCodeSpace`) is marked unclosed (`valueset-unclosed`, with the reason), since `$validate-code` accepts codes in those spaces that the expansion can't list
50
+ - Terminology: `$cache-control` mode=start also returns the issued cache-id in the `X-Cache-Id` response header (the body is unchanged), so a request log taken at the proxy (nginx `$sent_http_x_cache_id`) can tie each cache-id to the request that created it, for replay
51
+ - Terminology: `$validate-code` (CodeSystem and ValueSet) warns when a coding has no version but its code system says `versionNeeded = true` (`CODESYSTEM_VERSION_NEEDED`); a version pinned by the value set or by the parameters doesn't count, since it's specific to that context and doesn't travel with the coding
52
+ - Ecosystem resolution (`modules.tx.ecosystem`, off by default): for a code system (or version) this server doesn't have, `$validate-code` against a value set asks the ecosystem registry which server has it, and asks that server (a `$lookup` per code, a `$validate-code` per filter). Servers can be excluded, as can code system patterns; API keys and OAuth client credentials can be given per server; requests are SSRF-guarded; answers are cached for an hour. Requests that had to ask another server are counted and graphed separately (External requests). See tx/README.md
53
+ - `$translate`: `targetScope` - with a `targetSystem`, a code system's own targets are held to the value set; without one, the maps into the code systems the value set draws on are used (how a national SNOMED CT edition is translated to the International Edition a value set is built on), and what they give is held to the value set. A source version that is a SNOMED CT edition with no date is the release it resolves to
54
+ - TerminologyCapabilities marks the version of each code system that a request naming no version gets (`isDefault`)
55
+ - Terminology: Resource pages link codes to their code system's browser: SNOMED CT (to the edition, when the browser has it), LOINC (codes, parts and answers), RxNorm (RxNav), the NCI Thesaurus (EVS Explore) and ICD-11 (the WHO browser; a postcoordinated expression links to its stem)
56
+ - SNOMED CT: the `display` `regex` filter (VSAC uses it): every concept with an active description (not only the preferred term) that the pattern matches, whole and case-sensitively, in one language - the request's, else the edition's default. The scan is cached as for ECL
57
+ - SNOMED CT: `concept` filters `descendent-leaf` and `is-not-a`, and `code` is accepted as the same property as `concept` (value sets in production use it)
58
+ - SNOMED CT `$lookup`: `sufficientlyDefined`, `semanticTag` and concrete values (typed by the MRCM range); the module is `moduleId` (`module` still works); the normal forms are returned when asked for by name
59
+ - CodeSystem resources: the `descendent-leaf` filter
60
+ - `modules.tx.pruneTerminologyCache` (default false): once the library and the endpoints' core packages have loaded, what in `terminology-cache` wasn't used is deleted - the `.db` and `.cache` files (with their SQLite side files) and the package folders (old versions, packages no longer in the library, core packages of FHIR versions no endpoint has, failed downloads) - and what was freed is logged. Not other files or folders, not anything changed in the last day, and in a cluster only the master
61
+ - IPS
62
+ - a Structure view (after Narrative) showing a document's logical structure as an expandable tree: what the Composition refers to, then its sections, each with its entries and what else it refers to, and each resource referred to expanding into its own references. A reference that doesn't resolve in the document is `not-in-document`; a resource already shown isn't expanded again
63
+ - a nationality step (the first), which decides the document's nation from what it claims (US and AU so far), shown as a flag on the document page and a Nation column on the project page
64
+ - an analysis step and view - for each resource and section: its extensions (with their definitions, found through the validator), modifier extensions and `implicitRules`, inactive, entered-in-error and unknown statuses, a subject that isn't the document's, dates in the future where one isn't expected, and negation patterns (no known allergies and the like, US Core and IPS styles)
65
+ - the Narrative view switches between the narrative as provided and the one the server generates; the daily report has an IPS section
66
+ - `modules.ips.validator.autoLoadPackages` (default true): the validator finds and loads the package that defines an extension it doesn't have
67
+ - READMEs for the publisher, xig and vcl modules, from their settings
68
+
69
+ ### Changed
70
+
71
+ - The server keeps idle connections open for 65 s (`keepAliveTimeout`; `headersTimeout` 66 s), longer than nginx's 60 s upstream keep-alive: Node's 5 s meant nginx sometimes reused a connection as Node closed it, and a POST then got a 502
72
+ - `node server.js` with `--port` or `--instance-code` but no `--role` stops with an error (they were ignored)
73
+ - `NODE_ENV` defaults to `production` when it isn't set (`npm run dev`: `development`), so Express's error pages no longer include stack traces on a server where nobody set it
74
+ - `npm start` (and the Docker image) run `node --expose-gc server.js`, for the memory guard
75
+ - Dependencies: re2js, koffi (optional, for memory-mapped SNOMED CT), fhir-validator-wrapper 1.7.0, mongoose 9.12.1; rimraf is gone
76
+ - Dashboard: the FHIRsmith release and npm audit's result are one line above "More", with low disk space (under 10% free, still also a problem) in red on the same line (on the home page too, which has no npm audit line); the graphs' key is under "More"
77
+ - Status page (`/` and `/dashboard`): one line (requests, uptime, heap, and problems only when there are any), with the detail - a panel of tiles, a memory meter against the memory guard's thresholds, the caches - under "More"; the graphs are one component for every server, with a line per process in a cluster
78
+ - `config-template.json` has every module turned off: set `"enabled": true` on the ones you run
79
+ - The npm package for FHIRsmith leaves out the release scripts and test reports, and what git ignores (npm doesn't read `.gitignore` when there is a `.npmignore`, so a local `npm publish` included, for one, `private/`)
80
+ - Terminology
81
+ - `$expand` limits: the defaults are now 10000 codes returned (`externalLimit`, was 3000) and 100000 built (`internalLimit`, was 10000). The three expansion limits (`externalLimit`, `internalLimit`, `walkLimit`) are checked at startup - whole numbers above 0, external <= internal <= walk - and logged. The tx-ecosystem test runner (User-Agent `Tools/Java`) is no longer held to the old defaults: it gets the same limits as any client, and the tests that depend on a limit set `X-TOO-COSTLY-THRESHOLD`
82
+ - `$expand`: a page (`count` and `offset`) can lie past the expansion limit. Only the page is built; the codes before it are passed keeping only their keys, so `offset + count` is limited by a new, larger `walkLimit` (default 10 x `internalLimit`), and `count` by the expansion limit as before. Over the walk limit: 422 too-costly, `VALUESET_TOO_COSTLY_PAGE`. Value sets with an exclude are paged too (see below); a request with a text filter is as before. Any page, the first too, builds only its own codes: the rest are counted, by key, for the total (reported, as before, only when the whole expansion is within the limit), so a `count=20` first page of a large value set no longer builds codes up to the limit
83
+ - `$expand` of a code system that caps how many of its codes an expansion may list (`expandLimitation`: CPT, 1000, by agreement with the AMA): no page has codes past the cap, but the total is all the codes (as for `count=0`), and the walk stops at the cap. When the cap keeps codes off the page asked for (e.g. `count=1100`), the expansion says why: a new `expansion-issue` extension refers to a contained OperationOutcome with a business-rule warning (`VALUESET_EXPANSION_CODESYSTEM_LIMITED`). (`addExpansionIssue` is the general mechanism, for other things users of an expansion should know)
84
+ - `$expand`: every expansion is held to the limits. A value set with an exclude was exempt from the response limit, and an imported value set's codes didn't count against it, so a value set with an exclude or an import came back whole whatever its size (and an imported value set with an exclude escaped the internal limit). Now they are limited, and paged, like any other: an expansion that used to succeed may now be a 422 too-costly, and should be paged (`count` and `offset`). `count=0` still only counts
85
+ - Global supplements: a code system supplement is used without being referenced when it is a `lang-pack` or `extension-codes` supplement (`codesystem-supplement-type`) marked with `codesystem-supplement-global` = `true`. The `global-lang-pack` code (0.18.1) is gone, and a `lang-pack` supplement that isn't marked global is only used when it is referenced, like any other supplement
86
+ - VSAC value sets are read from the database when asked for, with the recently used ones kept parsed (`vsacCfg.valueSetCacheMB`, default 64), instead of all ~40,000 being held in memory (about 1 GB); a search reads only the value sets on the page returned
87
+ - SNOMED CT: a loaded edition uses its cache file's sections in place, rather than copies, so it takes its file size in memory, not twice that
88
+ - `$expand` builds only what it returns: a page's codes (the codes before it are only walked, the codes after it counted, or totalled by the provider), and only the codes it returns from an imported value set
89
+ - Performance: supplements are indexed by the code system they supplement (scanning every CodeSystem was 18-24% of CPU under IG Publisher traffic); a code system provider is built once per operation, not once per include or code; a code not in an enumerated include is located once; a response is serialised once; each cache-id's resources are hashed once; long operations give other requests a turn every 10 ms of compute (was 25), as do `$batch-validate-code` between entries and the usage tracker's startup scan; `$batch-validate-code` releases each entry as it is answered; RxNorm has an index on `RXNATOMARCHIVE(RXCUI, SAB)`, added to an existing database at load (an unknown code scanned ~372k rows); `$closure` can use a provider's ancestor sets (`ancestorsOf`)
90
+ - `x-too-costly-threshold` can't be more than `internalLimit` (a bigger value is cut down to it), and a value that isn't a positive whole number is ignored: there is always a limit (`0` meant no limit)
91
+ - `$cache-control`: caches are never carried over a restart. With an `instanceCode`, a cache-id carries the process's start, so a request with a cache-id from before a restart is told the cache was lost to a restart (404 `cache-id-unknown`)
92
+ - ValueSet pages list the first 500 codes of an include or exclude, with "Show all" (`_showAll=true`); VSAC's Medication Brand Name was 33,341 rows, each looked up
93
+ - The code system provider API (`tx/cs/cs-api.js`) has optional hooks for providers: `handlesSelecting`/`processSelection` (now called, with the value set as context, and may refuse as too costly), `prefetchCodes`, and `ancestorsOf`
94
+ - LOINC importer: every linguistic variant's language is loaded, and the variants' axis columns become `PartName` designations on the Parts (for some languages, e.g. ko-KR, the only translations LOINC has); relationships the source files repeat are stored once; the import runs as one transaction (much faster)
95
+ - `$expand` `designation` parameter: a bare code (or `|code`) matches a language, or a use code from any system; `system|code` works as before
96
+ - IPS
97
+ - on a document's page the actions (Edit, Share, Process again, Delete) are on their own line below the processing pipeline, and Edit is a button like the others
98
+ - `fhir-validator-wrapper` 1.7.0; the validator is no longer passed `checkIpsCodes` (validator 7.0.4 refuses it, failing every validation)
99
+
100
+ ### Fixed
101
+
102
+ - A `config.json` with no `server` block failed at startup; every setting in it has a default
103
+ - The home page's folder module entry wasn't closed (`</li>`)
104
+ - Lint: the errors in the project's own code are fixed, and the vendored jQuery files are no longer linted, so `npm run lint` has no errors
105
+ - The release workflow's test job has the heap size that `ci.yml` gives the tests (the suites outgrow Node's default)
106
+ - Terminology
107
+ - SNOMED CT importer: effectiveTimes were stored a day early when a cache was built in a time zone ahead of UTC (the day count to the 1899 epoch used local dates), so `$lookup` reported e.g. 2002-01-30 for 20020131. Caches built in such a zone need rebuilding
108
+ - `CodeSystem/$batch-validate-code`: a top-level `url` (and `version`) is the code system for every entry, as for `CodeSystem/$validate-code`, and every entry is validated against a code system - an entry with its own `url` was validated as a value set
109
+ - `regex` filters match the whole value, for every code system: the pattern is `^(?:pattern)$`. Code systems from CodeSystem resources, ISO 3166 and filters on supplement properties anchored it as `^pattern$`, so an alternation went wrong (`code2|code3` matched every code starting with code2); LOINC matched anywhere in the value (a CLASS regex of `CYTO` matched PANEL.HL7.CYTOGEN). LOINC regex filters may now select fewer codes
110
+ - ValueSet `$validate-code` with `activeOnly=true`: an inactive code was only rejected when the value set includes all of its code system; when the value set lists the code, or selects it with a filter, it was valid (with an inactive warning). It is now not in the value set whatever the include, for every code system
111
+ - Only one version of a supplement applies: when the server has one version and the request passes another in `tx-resource` (or a value set names one and the request another), the version the request asks for by version (`useSupplement url|version`, or the value set's `valueset-supplement`) is used - else the request's own, else the latest - and `used-supplement` reports that exact version. Both were applied before, and the server's version was the one reported (and whose displays won)
112
+ - ValueSet `$validate-code`: a code found through a value set filter didn't report a status the code has while active (`deprecated`: no `status` parameter and no `DEPRECATED_CONCEPT_FOUND` warning), as it does when the value set lists the code
113
+ - `$expand` text filters (`filter`) now search one language: the first language the request asks for (`displayLanguage`, else `Accept-Language`) that the code system has text in, else the code system's own language - the same rule as the SNOMED CT display filter. All the displays and designations in that language take part, including those from supplements in use; designations in other languages no longer match. LOINC searches its names in other languages (`filter=Volumen&displayLanguage=de` finds Glucose [Masse/Volumen] in Serum oder Plasma) and supplement designations (Uzbek); in English it still searches the displays. SNOMED CT builds a text index per language on editions with more than one (the first search in a language takes a second or two), and a national edition with no language asked for searches its default language
114
+ - A value set filter on a property that a supplement in use defines (`catalog-group = renal-diabetes` on LOINC or SNOMED CT, with a supplement giving codes a `catalog-group`) failed (500 for LOINC and SNOMED CT, 'not understood' for code systems from CodeSystem resources). Such filters are now evaluated from the supplement, for any code system, in `$expand` (includes and excludes) and `$validate-code`, alone or with the code system's own filters (=, in, regex and exists = true can select codes on their own; not-in and exists = false only narrow other filters). A filter the code system does itself still goes to the code system
115
+ - Supplements pinned to a version of their code system (`supplements` = `http://loinc.org|2.80`) were applied to whichever version a request without a version resolved to (LOINC 2.83), and reported as used; a value set requiring one didn't fail. Which supplements apply is now decided by the version actually used: one pinned to another version is not applied (so requiring it fails with `VALUESET_SUPPLEMENT_MISSING`), and one from the server's own content pinned to that version now applies when the request doesn't name a version
116
+ - `$expand` returned no properties for codes from LOINC, SNOMED CT, RxNorm, UCUM, OMOP, CPT or NDC, whether asked for with `property`, `ValueSet.compose.property` or `*` (`$lookup` returned them): these code systems now report the requested properties on each code, as `$lookup` does, and only compute them when some are asked for. UCUM's `canonical` property is returned when asked for (`$lookup` never returned it). An R4 expansion declared a property with no uri with an empty uri extension. OMOP codes from a filter were JSON numbers in an expansion
117
+ - LOINC: `$lookup` of a claims attachment term (CLASSTYPE 3: 18719-5, 19005-8, 26436-6... - 1,161 of them) failed with a 500 ('valueString must be a string, not a undefined'), and a `CLASSTYPE = 3` filter selected nothing: the importer stores the class type as 'Claims attachment', and the provider only knew 'Claims attachments'. CLASSTYPE is now reported as 3 with the description 'Claims attachment', and the filter selects those terms
118
+ - The too-costly error (`VALUESET_TOO_COSTLY`) for a value set passed in the request with no url named it 'undefined' or '??'; it now names it by its id, or says it's the value set in the request
119
+ - A value set with a modifier extension the server doesn't know (on the value set, its compose, an include or exclude, a filter or a listed concept - e.g. VSAC's `vsacOpModifier` on a filter) was refused with a 500. It's a problem with the value set, not the server: `$expand` and `$validate-code` now refuse it with a 422 (`VALUESET_MODIFIER_EXTENSION_UNKNOWN`, `vs-invalid`) naming the extension. A resource with implicit rules is refused with a 422 too
120
+ - `$expand`: a value set that carries an expansion (one sent as a `tx-resource`, or from a package), whether expanded itself or imported by another, was answered from that expansion, even when it was made from other code system versions than the request asks for (`force-system-version` etc.) or the server has - the opioid-cds IG's value sets got 1,733 codes from stale LOINC 2.82 expansions instead of 2,221 from 2.83. A value set with a compose is now expanded from its compose, as the Java expander does. Its stored expansion is only used when it has no compose, or when a code system the compose includes isn't available here (not loaded, or not the version the include names), so the compose can't be expanded
121
+ - Search (`CodeSystem?`, `ValueSet?`, `ConceptMap?`): the `version` parameter matched any version containing it (`version=0` found 3.0.0). It is now matched version-aware: exactly, or with wildcards for a semver version (`3.0.x`, `3.x.x`), or as a prefix for others (`2024-01` finds 2024-01-15), using the resource's versionAlgorithm when it has one
122
+ - Search: the `url` parameter is a left match (the url starts with the value, case-sensitive) for ValueSets and ConceptMaps. A ValueSet url had to match exactly, and a ConceptMap url matched anywhere in the url
123
+ - VSAC: a value set asked for at a version VSAC doesn't have (`valueSetVersion=19990101`) was answered from its latest version; it is now not found, as a value set from a package is (#94). `VSACValueSetProvider.VERSION_FALLBACK_TO_LATEST` brings the old behaviour back
124
+ - The expansion caches could fill the heap under steady load (a replay of IG Publisher traffic ran a server with node's default heap out of memory in 7 minutes: 1,000-code pages of whole-SNOMED CT expansions, about 0.8 MB each). The memory threshold (`expansionCacheMemoryThreshold`) was only checked every 5 minutes. The caches of all the endpoints now share a budget of codes (`expansionCacheMaxCodes`; default a quarter of the heap limit at 1 KB a code, about a million codes with a 4 GB heap): each expansion is counted as it's cached, and the least recently used ones are dropped, from whichever endpoint, to stay under it. An expansion bigger than the whole budget isn't cached. The caches also drop entries in least recently used order without sorting
125
+ - ValueSet `$validate-code` with `inferSystem`: when the value set includes a code system that isn't known, the request failed with a 500 ('Exception expanding value set in order to infer system') if the value set also imports a value set, and otherwise reported that the system couldn't be inferred. The system can't be inferred when the code might be in a code system that isn't known, so it is now a 422 naming that code system (`UNABLE_TO_INFER_CODESYSTEM_UNKNOWN_SYSTEM`). The same applies to the value sets it imports
126
+ - ValueSet `$validate-code` with `inferSystem`: a value set that imports another value set was expanded to find the code's system, which failed with a 500 when the expansion was too big (an import of all of UCUM: 'too many codes to produce (>10000)'), and cost as much as the value set was big when it wasn't. The system is now found by checking the code against each include, and each include of the value sets it imports, less the value set's excludes, without expanding anything
127
+ - `$expand` of a value set that lists its codes (as VSAC value sets do), when the codes are more than the expansion limit: `count=0` (the total only) was refused as too costly (422), and a full expansion was refused only after the codes up to the limit had been built (displays, properties) - 0.1 to 3 s for some VSAC value sets. With `count=0` the listed codes are now counted without being built, when every include lists its codes, so the total is returned whatever the limit; otherwise the listed codes are counted first, and an expansion they would take over the limit is refused before any is built (a page that fits within the limit is still served)
128
+ - Client side caches (`$cache-control`) had no size limit: only idle ones were dropped, so a cache in use could grow without bound, and 500 caches of a 20,000-concept code system ran the server out of memory. A cache may now hold at most `cacheMaxConcepts` concepts (default 250,000, about 10x the largest IG Publisher session measured on build.fhir.org, 24,048): a request that would take it over is not performed: the cache is dropped and the request gets a 404 with one issue coded `cache-id-unknown` (`CACHE_ID_DISCARDED_TOO_BIG`: the cache grew too large and was dropped; start a new cache), as for any cache that is gone, so the client starts a new cache and sends the request again (a later request with that cache-id gets the same 404, saying why it was dropped). A request whose own resources hold more than `cacheMaxConcepts`, which no cache could take, is refused (422 too-costly) and the cache is kept. An endpoint's caches hold at most `cacheMaxTotalConcepts` between them (default 1,000,000, 4 full caches), and the least recently used caches are dropped to stay under it; a request with a dropped cache-id is told so (404 `cache-id-unknown`). 0 = no limit, for either
129
+ - The usage tracker (behind the `usage-count` expansion property) kept every system and code a `$validate-code` was asked about, forever: validating unique made-up codes grew the heap by 10 MB a second. It now records only valid codes, at most `usageTrackerMaxCodes` of them (default 1,000,000, 2x the about 500,000 a full library's value sets list), none longer than `usageTrackerMaxCodeLength` characters (default 100, 2.5x the longest of those, 40). 0 = no limit
130
+ - The cache of parsed language tags kept every valid tag forever, and private use tags (`en-x-...`) of any length are valid: unique 8 KB tags in requests grew the heap by 46 KB each. It now keeps the `languageTagCacheSize` tags used most recently (default 1,000: at most about 46 MB of 8 KB tags). 0 = no limit
131
+ - The tx module logs its memory limits at startup, on one line (`Limits: operationTimeLimit=20s, cacheTimeout=30min, cacheMaxConcepts=250000, ...`)
132
+ - VSAC: a sync cut short when the server stopped left the cache partly filled until the next scheduled refresh (24 h), since a sync only ran at startup when the cache was empty. At startup the server now carries on with a sync that was interrupted or failed: a sync keeps its work list and how far it has got, and the next one does the value sets that weren't finished first (including one that was part way through its versions, which a fresh listing wouldn't find), then whatever else has changed. A run left marked running is recorded as interrupted. A failed startup sync no longer stops the refresh timer from starting
133
+ - A tx endpoint answered a request body over the size limit with Express's HTML error page; it now answers with a 413 OperationOutcome (too-costly), in the format the client asked for
134
+ - SNOMED CT: inactive relationship rows are ignored (agreed with the SNOMED leads; they are still imported) - property filters, ECL refinements, is-a and subsumption, and `$lookup`'s properties, parents and children used them
135
+ - SNOMED CT: `concept in` a list of plain concepts selected nothing (only reference sets in the list worked); spaces after commas and concepts named twice are handled too
136
+ - SNOMED CT implicit value sets: the generic form (`http://snomed.info/sct?fhir_vs...`) is resolved against the edition the request asks for (`system-version` and the like), checking the named concept or reference set there, not the default edition; an edition or version named in the url still wins
137
+ - SNOMED CT `$lookup` `property=*`: every property but the normal forms (and ancestors and descendants), which have to be asked for by name
138
+ - RxNorm: filters in one include are ANDed (two of the same kind both apply); results and totals count concepts, not atoms; the display is the concept's main atom; `concept in` lists codes, and `code` is accepted as `concept`; relationship filters with `in`; a concept whose display atom is obsolete (`SUPPRESS = O`) is inactive; `$lookup` returns TTY, STY and SAB by default, and RxNorm's own relationships (SAB = RXNORM) with `*` or by name, valued with the bare RXCUI
139
+ - `$expand` of a value set that includes one version of a code system and excludes another whole version of it: an included code is excluded when the excluded version has it (the excluded version's codes aren't listed, since most providers can't list theirs)
140
+ - `$expand` from a code system with no content (`not-present`, e.g. THO's time-period-ranges) is a 422, not a 500
141
+ - A cached expansion made under one request's `x-too-costly-threshold` could be served to a request with another: the limit is now part of the cache key
142
+ - `CodeSystem/$validate-code` never answered (the connection was held until the client gave up) when an issue was raised before validation started, such as an unknown cache-id
143
+ - `$validate-code` of a CodeableConcept: the display reported is the reported coding's own, not one from a coding before it; when the code is in no include, the code systems' reasons (an error from another server, say) are reported
144
+ - An exception in a SQLite callback left the request hanging with no response; now it fails the request, in every module
145
+ - SQLite connections leaked: `CodeSystem/$validate-code` (and batch entries with no value set) never closed its provider, nor did a request that failed `check-system-version`
146
+ - A supplement's designations matched a text filter when they had any of its words; they need every word, as the code system's own do
147
+ - LOINC importer: each externally defined answer list (65 in 2.83) added an Answer with a blank code, shared by all of them
148
+ - VSAC: a failed sync stopped the refresh timer for good (it now retries after 1, 5 and 30 minutes); after a restart the next sync is due an interval after the last one, not after the restart (a server restarted more than daily never synced); a `refreshIntervalHours` too big for a timer ran syncs back to back
149
+ - Packages crawler: mirror files are written to a temporary name and renamed into place (nginx could serve a half-written file), before the registry advertises the package; each package is stored in one transaction
150
+ - Statistics: requests per minute is divided by the real time since the last sample (a late timer showed a higher rate), and only responses the static file handlers send count as static hits
151
+ - Startup: logging starts before `config.json` is read, so a server that fails to start says why in the log as well as on stderr
152
+
153
+ ### Tx Conformance Statement
154
+
155
+ FHIRsmith v1.0.0 passed all 5230 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed+mimetypes+icd-11+icf+closure+loinc, tests v1.11.1-snapshot, runner v7.0.5)
156
+
8
157
  ## [0.18.2] - 2026-10-29
9
158
 
10
159
  ### Added
@@ -0,0 +1,305 @@
1
+ # FHIRsmith Configuration
2
+
3
+ The server is configured by `config.json` in the data directory (see
4
+ [Data Directory](README.md#data-directory)), plus a few environment variables.
5
+ `config-template.json` is a starting point: it has every section, with comments, and every
6
+ module turned off - set `"enabled": true` on the ones you run. `config.json` is JSON, except that
7
+ it may have comments: `//` to the end of the line, and `/* ... */`.
8
+
9
+ Upgrading? See [Upgrading to 1.0.0](#upgrading-to-100).
10
+
11
+ The top level of `config.json` has the server-wide settings described here, and a
12
+ `modules` section with an entry for each module (see [Modules](#modules)).
13
+
14
+ ## Server
15
+
16
+ Create a `config.json` file in your data directory (use `config-template.json` as a starting point):
17
+
18
+ ```json
19
+ {
20
+ "hostName" : "[descriptive name for the server]",
21
+ "server": {
22
+ "port": 3000,
23
+ "cors": {
24
+ "origin": "*",
25
+ "credentials": true
26
+ }
27
+ },
28
+ "modules": {
29
+ // per modules...
30
+ }
31
+ }
32
+ ```
33
+
34
+ | Option | Default | Description |
35
+ |---|---|---|
36
+ | `server.maxBodyMB` | `50` | Largest request body accepted, in MB. A body is read whole into memory before it is parsed, so this bounds what one request can take. A bigger body gets a 413: an OperationOutcome (too-costly) on a tx endpoint, and the module's own handling elsewhere. The largest IG Publisher request seen on build.fhir.org was 1.39 MB, so 50 leaves plenty of room. The ips module reads its own bodies, with its own limit |
37
+
38
+ Behind nginx, nginx's `client_max_body_size` is checked first. If it is lower than `maxBodyMB`, nginx refuses a bigger body with its own 413 HTML page before FHIRsmith sees it, so set it at or above `maxBodyMB` to get FHIRsmith's OperationOutcome; if it is higher, `maxBodyMB` applies. Requests of over 100 MB have been seen, caused by client-side issues: choose `maxBodyMB` (default 50) and the nginx limit with that in mind. See [nginx.md](nginx.md).
39
+
40
+ ### Memory
41
+
42
+ `server.memory` sets the memory guard: fractions of the memory limit (the cgroup limit, such
43
+ as `MemoryMax` on the systemd unit, or else the machine's RAM), measured against process RSS;
44
+ with a role, against the whole cgroup's private memory (see [cluster.md](cluster.md#memory)).
45
+
46
+ | Option | Default | Description |
47
+ |---|---|---|
48
+ | `server.memory.rejectFraction` | `0.85` | Above this, new requests get 503 + `Retry-After` |
49
+ | `server.memory.abortFraction` | `0.92` | Above this, running operations are aborted with 422 too-costly |
50
+ | `server.memory.trimAllocator` | `false`; `true` with a role | Whether the server returns the C allocator's free memory (`malloc_trim`, Linux) while refusing, and, with a role, once after loading. Without a role it is off unless set `true`; with one, `false` is for diagnosis. See [cluster.md](cluster.md#memory) |
51
+
52
+ ### Cluster
53
+
54
+ To use more than one CPU core for terminology on one machine, a master and several tx
55
+ workers share the data directory and `config.json` (started by `node cluster.js`, or by hand
56
+ with `--role`). Their settings are in `server.cluster`, documented with the arrangement in
57
+ [cluster.md](cluster.md): the launcher's (`workers`, `maxOutstanding`, `queueSeconds`,
58
+ `recycleSeconds`, the fast lane, ...) under
59
+ [The launcher](cluster.md#the-launcher-clusterjs), and what the processes share
60
+ (`sharedCache`, `cacheAffinity`, `sharedCacheMaxMB`, `sharedClosure`) under
61
+ [Options](cluster.md#options). With no role, `server.cluster` is ignored.
62
+ The status panel's snapshot of every process (`statusSeconds`) is under
63
+ [Status](cluster.md#status).
64
+
65
+ ### Statistics
66
+
67
+ `stats` sets the request counters and the graphs on `/` and `/dashboard`.
68
+
69
+ | Option | Default | Description |
70
+ |---|---|---|
71
+ | `stats.enabled` | `true` | `false`: the counters are kept in memory only |
72
+ | `stats.database` | `stats.db` | The counters' SQLite file, under the data folder's `databases` directory |
73
+ | `stats.intervalMinutes` | `10` | How often the counters are written to the database; also the graphs' sample interval, unless `sampleSeconds` is set (a server with no role, as before) |
74
+ | `stats.sampleSeconds` | `intervalMinutes`; with a role, 60 at most | How often the graphs take a sample. With a role (cluster.md), at most a minute unless set, so the graphs of a short session can be read. The graphs show 24 hours; under the launcher at most 1,440 samples a process, so with less than a minute they show less. The counters are still written every `intervalMinutes` |
75
+
76
+ ## Logging
77
+
78
+ Add a `logging` section to `config.json` to control log behaviour. All fields are optional and have sensible defaults:
79
+
80
+ ```json
81
+ {
82
+ "logging": {
83
+ "level": "info",
84
+ "console": true,
85
+ "consoleErrors": false,
86
+ "maxFiles": 14,
87
+ "maxSize": "50m",
88
+ "flushInterval": 2000,
89
+ "flushSize": 200
90
+ }
91
+ }
92
+ ```
93
+
94
+ | Option | Default | Description |
95
+ |---|---|---|
96
+ | `level` | `"info"` | Minimum level to log: `error`, `warn`, `info`, `debug`, or `verbose` |
97
+ | `console` | `true` | Write log lines to stdout/stderr. Disable when running as a systemd service where console output goes to the journal and is redundant |
98
+ | `consoleErrors` | `false` | Whether `error` and `warn` levels appear on the console. When `false`, errors and warnings are written to the log file only |
99
+ | `maxFiles` | `14` | Number of daily log files to retain before old ones are deleted |
100
+ | `maxSize` | `0` (unlimited) | Maximum size per log file before rotation. Accepts human-readable strings: `"20m"`, `"1g"`, or a raw byte count |
101
+ | `flushInterval` | `2000` | Milliseconds between buffered writes to disk. Increase to reduce I/O under heavy load |
102
+ | `flushSize` | `200` | Number of buffered log lines that trigger an immediate flush regardless of the timer |
103
+
104
+ Log files are written to the `logs/` subdirectory of the data directory as `server-YYYY-MM-DD.log`. A `server.log` symlink always points to the current day's file, so `tail -f data/logs/server.log` tracks the active log without needing to know the date.
105
+
106
+ ## Email and the Daily Report
107
+
108
+ The server can email a short markdown report on itself once a day. It needs two things in
109
+ `config.json`: an SMTP server to send through, and someone to send to:
110
+
111
+ ```json
112
+ {
113
+ "email": {
114
+ "host": "smtp.example.org",
115
+ "port": 587,
116
+ "user": "fhirsmith",
117
+ "password": "...",
118
+ "from": "FHIRsmith <fhirsmith@example.org>"
119
+ },
120
+ "dailyReport": {
121
+ "to": ["ops@example.org"]
122
+ }
123
+ }
124
+ ```
125
+
126
+ `user`/`password` are optional (an open relay); `secure` defaults to true for port 465 and
127
+ to STARTTLS otherwise. Without an `email` block nothing is emailed, and without
128
+ `dailyReport.to` there's no report (`dailyReport.enabled: false` turns it off explicitly).
129
+
130
+ The report, titled "`{hostName}` daily report", goes out at midnight (the server's local
131
+ time), or as soon as the server is up again if it was down at midnight. It has a paragraph
132
+ on the server's stability since the last report - how much of the time it was up, restarts,
133
+ the longest outage, and any upgrade (from the run history in the statistics database, so
134
+ `stats.enabled: false` leaves the uptime unknown); under the launcher (`cluster.js`), a
135
+ paragraph on the cluster - processes that exited or were recycled, how long they were
136
+ unavailable, and the requests the launcher refused or abandoned itself (see
137
+ [cluster.md](cluster.md#the-daily-report)); a paragraph from each running module that
138
+ has something to report (by default, none do); and a paragraph on the system - version,
139
+ Node.js and OS, how long this run has been up, the log files and the free disk space they're
140
+ drawing on, plus anything the npm audit check has found (packages with known vulnerabilities and
141
+ what to do about them, or a newer FHIRsmith release). If any background task's last run failed, a
142
+ final section lists them. When the last report was sent is kept in `daily-report.json` in the data
143
+ directory.
144
+
145
+ A module takes part by implementing `dailyReport({since, now})`, returning its paragraph as
146
+ markdown (or a promise of it); an empty return means it has nothing to say that day. The
147
+ request counts a module quotes come from `stats.countsSince(since, now)`, the per-operation
148
+ counts in the statistics database for the period. The modules that report:
149
+
150
+ | Module | Reports |
151
+ |---|---|
152
+ | Terminology server | how many of each operation were performed, over all its endpoints |
153
+ | Terminology server registry | client requests answered by the API, servers registered, and any server that answered none of the registry's scans in the period |
154
+ | Packages | packages served (tarball downloads) and new package versions found by the crawler |
155
+ | XIG | pages served, and whether the database could be accessed (and whether the last download failed) |
156
+ | Testing | pages served, test reports received, and the number held |
157
+ | SMART Health Links | QR codes created (SMART Health Links and Verifiable Health Links), link accesses, and the documents stored to serve them |
158
+ | IPS | pages served, IPS documents received, and the number stored |
159
+ | Publisher | publication runs started, and how many finished successfully |
160
+
161
+ ## Environment Variables
162
+
163
+ | Variable | Description | Default |
164
+ |---|---|---|
165
+ | `FHIRSMITH_DATA_DIR` | Path to the data directory | `./data` |
166
+ | `PORT` | Server port (overrides config) | from config.json |
167
+ | `FHIRSMITH_ROLE` | `master` or `tx-worker`, as `--role` (see [cluster.md](cluster.md#roles)). Not set: an ordinary single server | none |
168
+ | `FHIRSMITH_INSTANCE_CODE` | This process's cache-id prefix, as `--instance-code` (see [cluster.md](cluster.md#roles)) | a master: `modules.tx.instanceCode`, and with none it stops with an error; a worker: none, and it stops with an error. The launcher gives `m0`, `tx1`, ... |
169
+ | `FHIRSMITH_WORKERS` | How many tx-workers `node cluster.js` starts, as `--workers` (`server.cluster.workers`) | from the CPUs and memory |
170
+ | `FHIRSMITH_CACHE_WAIT_MS` | How long a tx-worker waits for the master to build a cache it reads (see [cluster.md](cluster.md#what-runs-where)) | `300000` (5 minutes) |
171
+ | `NODE_ENV` | Node environment. Express and other libraries check it (in development mode, Express's error pages include stack traces). If it isn't set the server sets it to `production`, or to `development` when run with `npm run dev` | `production` |
172
+
173
+ ## Modules
174
+
175
+ Each module has its own entry under `modules`, documented with the module:
176
+
177
+ | Module | Configuration |
178
+ |---|---|
179
+ | `tx` - terminology server | [tx/README.md](tx/README.md#configuration) |
180
+ | `registry` - terminology server registry | [registry/documentation/configuration.md](registry/documentation/configuration.md) |
181
+ | `packages` - package server | [packages/readme.md](packages/readme.md) |
182
+ | `shl` - SMART Health Links | [shl/readme.md](shl/readme.md) |
183
+ | `ips` - IPS documents | [ips/readme.md](ips/readme.md) |
184
+ | `testing` - test reports | [testing/readme.md](testing/readme.md) |
185
+ | `folder` | [folder/readme.md](folder/readme.md) |
186
+ | `npmprojector` | [npmprojector/readme.md](npmprojector/readme.md) |
187
+ | `token` | [token/security_guide.md](token/security_guide.md) |
188
+ | `xig` | [xig/readme.md](xig/readme.md) |
189
+ | `vcl` | [vcl/readme.md](vcl/readme.md) |
190
+ | `publisher` | [publisher/readme.md](publisher/readme.md) |
191
+
192
+ ## Upgrading to 1.0.0
193
+
194
+ 1.0.0 is a big upgrade from 0.17 and 0.18. Most servers won't need to change any settings, but
195
+ every operator should check these. Each item says who it affects; the [CHANGELOG](CHANGELOG.md)
196
+ has the detail.
197
+
198
+ ### Every server
199
+
200
+ - **Node.js 24** or later (`engines` in package.json). After upgrading Node, `npm ci` (there
201
+ are new dependencies) - or `npm rebuild` for the native ones.
202
+ - **Start command**: `npm start` is now `node --expose-gc server.js`. A service that runs
203
+ `node server.js` still works (the memory guard sets the flag itself), but add `--expose-gc`.
204
+ `--port` or `--instance-code` without `--role` now stops the server with an error (they used
205
+ to be ignored): drop them from a single server's command line.
206
+ - **`NODE_ENV`** is `production` when it isn't set (`npm run dev`: `development`). Set it
207
+ yourself if you relied on development mode (Express's error pages with stack traces).
208
+ - **Keep-alive**: idle connections are kept for 65 s (was Node's 5 s), longer than nginx's
209
+ upstream `keepalive_timeout` (60 s by default). If you set nginx's higher, keep it under 65 s.
210
+ - **Memory guard** (new, on by default): above 85% of the memory limit (the cgroup's - systemd
211
+ `MemoryMax` - or else the machine's RAM) new requests get 503, and above 92% running
212
+ operations are aborted (422). If your memory limit is tight, check `server.memory` ([Memory](#memory)).
213
+ - **Request size**: `server.maxBodyMB` (default 50) is now a setting. nginx's
214
+ `client_max_body_size` must be at least as big, or clients get nginx's HTML 413 rather than an
215
+ OperationOutcome.
216
+ - **`config.json` may have comments** (`//` and `/* */`), and `config-template.json` now has
217
+ every module turned off.
218
+ - **New, optional**: `email` and `dailyReport.to` ([the daily report](#email-and-the-daily-report) -
219
+ nothing is sent without both), `npmAudit` (a periodic `npm audit` of the server; set
220
+ `"enabled": false` on a server with no internet access), `accessibility.checkPanel`,
221
+ `stats.sampleSeconds`.
222
+ - **Cluster mode** (`node cluster.js`, [cluster.md](cluster.md)) is new and opt-in: `server.js`
223
+ runs as before.
224
+
225
+ ### tx (terminology server)
226
+
227
+ - **Expansion limits**: the defaults are now `externalLimit` 10000 (was 3000) and `internalLimit`
228
+ 100000 (was 10000), and there's a new `walkLimit` (default 10 x `internalLimit`) for paging.
229
+ The three are checked at startup: one that isn't a whole number above 0, or
230
+ external <= internal <= walk not holding, **stops the server**. If you set any of them, check
231
+ them. Bigger expansions now succeed, so a busy server may use more memory.
232
+ - **Value sets with an exclude or an import are limited** like any other (they used to come
233
+ back whole, whatever their size): a client that expands a big one unpaged now gets a 422
234
+ too-costly, and should page it.
235
+ - **`Tools/Java` is no longer special**: the IG Publisher and the tx-ecosystem test runner get
236
+ the configured limits like any client (they used to be held to 3000/10000).
237
+ - **`x-too-costly-threshold`** can only lower the limit: a value above `internalLimit` is cut
238
+ down to it, and `0` (or anything that isn't a positive whole number) is ignored - it no
239
+ longer means "no limit".
240
+ - **Memory caps** with defaults (see the `Limits:` line in the startup log): `cacheMaxConcepts`,
241
+ `cacheMaxTotalConcepts`, `expansionCacheMaxCodes`, `usageTrackerMaxCodes`,
242
+ `usageTrackerMaxCodeLength`, `languageTagCacheSize`. A client may now get `cache-id-unknown`
243
+ (or 422) for a `$cache-control` cache that grew too big.
244
+ - **New, optional**: `snomed` (load SNOMED CT editions memory-mapped, or prewarmed - see
245
+ tx/README.md), and `ecosystem` (`$validate-code` for code systems held by other servers in
246
+ the terminology ecosystem; off by default).
247
+ - **`pruneTerminologyCache`** (new, default false): delete the databases, caches and packages
248
+ in `terminology-cache` that the library no longer uses. Turn it on for a server that has the
249
+ data directory to itself.
250
+ - **`instanceCode`** is checked at startup, and a cluster master needs one.
251
+ - **VSAC** (`vsacCfg`): the default `baseUrl` is https (an http one is logged as a warning);
252
+ value sets are read from the database when asked for, with a cache sized by `valueSetCacheMB`
253
+ (default 64); a value set version VSAC doesn't have is now not found.
254
+ - **Language packs**: a `lang-pack` or `extension-codes` supplement is used without being
255
+ referenced only when it is marked `codesystem-supplement-global` = `true`. The
256
+ `global-lang-pack` code is gone. If you load language packs, check they are marked.
257
+ - **Behaviour, with no setting**: `regex` filters match the whole value (LOINC regex value sets
258
+ may select fewer codes); ICD-11 codes are entity ids; a resource that both THO and a core
259
+ package provide is available in both versions; for LOINC answer lists, `answers-for` is the
260
+ filter (`LIST` still works, for backwards compatibility only); `$cache-control` caches are
261
+ never carried over a restart.
262
+ - **Rebuilds** (optional, for the new features): SNOMED CT caches for alternate identifiers and
263
+ concrete values, and any built in a time zone ahead of UTC (their effectiveTimes are a day
264
+ early); the LOINC database for its mappings, its Part translations (`PartName` designations,
265
+ from every linguistic variant) and without the blank answer code that externally defined
266
+ answer lists added. RxNorm's new index is added to an existing database at load. Coming from
267
+ 0.16 or earlier, the package value set and concept map caches rebuild themselves once, so
268
+ the first start is slower.
269
+
270
+ ### Other modules
271
+
272
+ - **registry**: `maxResponseMB` (default 100) limits what the crawler reads from a server. New,
273
+ optional: `oauth`, client credentials for servers that need a token to be scanned.
274
+ - **packages**: the crawler refuses packages over its new limits (`maxDownloadMB` 250,
275
+ `maxUnpackedMB` 4096, `maxFileMB` 50, `maxFiles` 250000); raise them only if a real package
276
+ is refused.
277
+ - **packages, signing** (on by default, `npmSigning`): the registry signs every version, so
278
+ `npm audit signatures` works against it. At the first start it makes its key,
279
+ `npm-signing-key.pem` next to the database (`npmSigning.keyFile`) - **back it up**: clients
280
+ check signatures against the published key - and signs the versions it already holds in the
281
+ background (`/packages/-/npm/v1/keys` is a 404 until it's done; progress in `/packages/status`).
282
+ Set `"enabled": false` to opt out.
283
+ - **packages, publishers' signatures**: `crawler.signatures` defaults to `check` (a signature
284
+ that fails is logged and the package stored anyway); `require` refuses unsigned packages and
285
+ doesn't read feeds that list no keys. Feeds' keys are in `package-feeds.json`.
286
+ - **packages, `/packages/feed-check`** (on by default, `feedCheck`): anyone can have the server
287
+ download and check a feed's packages (one run at a time, at most `maxItems` 50 and
288
+ `maxTotalMB` 500). Set `"enabled": false` if you don't want that on your server.
289
+ - **xig**: `POST /xig/update` only works when `updateToken` is set.
290
+ - **ext-tracker**: set `token` (the IG Publisher's) so only the publisher can submit.
291
+ - **token**: the JSON API takes the API key in a header (`Authorization: Bearer` or
292
+ `X-API-Key`), not the URL - clients must change. The session cookie is now `Secure` when the
293
+ request came over https, which behind nginx needs `X-Forwarded-Proto` (`cookieSecure`
294
+ overrides).
295
+ - **publisher**: the administrator's password is now `adminPassword` in the config (set at
296
+ every start). Without it, an administrator that still has the old fixed default password
297
+ (`admin123`) gets a random one at startup, which is logged - set `adminPassword`, or look in
298
+ the log.
299
+ - **ips**: to use a validator jar of your own, set `validator.jar`, rather than replacing
300
+ `bin/validator_cli.jar`. `validator.checkIpsCodes` is gone (validator 7.0.4 refuses it);
301
+ `validator.autoLoadPackages` (default true) lets the validator load the packages that define
302
+ extensions it doesn't know.
303
+
304
+ There are no manual data migrations: the databases that changed migrate themselves at startup.
305
+