fhirsmith 0.17.0 → 0.18.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 (121) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +80 -13
  3. package/config-template.json +106 -1
  4. package/ips/ai-client.js +263 -0
  5. package/ips/composer.js +805 -0
  6. package/ips/editor-schema.js +289 -0
  7. package/ips/editor.js +290 -0
  8. package/ips/fhirpath-ips.js +397 -0
  9. package/ips/ips-codeable-concepts.md +1742 -0
  10. package/ips/ips-template.html +125 -0
  11. package/ips/ips-valuesets.js +159 -0
  12. package/ips/ips.js +1736 -0
  13. package/ips/membership.js +179 -0
  14. package/ips/pipeline.js +1012 -0
  15. package/ips/presenter.js +974 -0
  16. package/ips/profile-editor.js +578 -0
  17. package/ips/profiles.js +506 -0
  18. package/ips/readme.md +460 -0
  19. package/ips/render.js +1482 -0
  20. package/ips/search-params.js +83 -0
  21. package/ips/search.js +667 -0
  22. package/ips/share.js +178 -0
  23. package/ips/store.js +650 -0
  24. package/ips/terminology.js +395 -0
  25. package/ips/tx-client.js +202 -0
  26. package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
  27. package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
  28. package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
  29. package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
  30. package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
  31. package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
  32. package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
  33. package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
  34. package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
  35. package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
  36. package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
  37. package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
  38. package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
  39. package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
  40. package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
  41. package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
  42. package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
  43. package/ips/valuesets/ValueSet-ips-medication.json +50 -0
  44. package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
  45. package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
  46. package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
  47. package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
  48. package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
  49. package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
  50. package/ips/valuesets/ValueSet-ips-route.json +46 -0
  51. package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
  52. package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
  53. package/ips/valuesets/elements.json +44 -0
  54. package/library/daily-report.js +422 -0
  55. package/library/fhir-search-values.js +138 -0
  56. package/library/html-server.js +32 -23
  57. package/library/mailer.js +77 -0
  58. package/library/narrative-check.js +21 -2
  59. package/library/npm-audit.js +25 -0
  60. package/library/package-manager.js +4 -2
  61. package/library/sqlite-session-store.js +29 -1
  62. package/package.json +7 -2
  63. package/packages/packages.js +34 -3
  64. package/publisher/publisher.js +29 -0
  65. package/registry/auth-store.js +366 -0
  66. package/registry/auth.js +872 -0
  67. package/registry/crawler.js +41 -1
  68. package/registry/documentation/api.md +162 -0
  69. package/registry/documentation/configuration.md +93 -0
  70. package/registry/documentation/documents.md +74 -0
  71. package/registry/documentation/internals.md +151 -0
  72. package/registry/documentation/readme.md +100 -0
  73. package/registry/documentation/security.md +199 -0
  74. package/registry/documents.js +309 -0
  75. package/registry/openapi.yaml +30 -0
  76. package/registry/registry-template.html +1 -1
  77. package/registry/registry.js +230 -44
  78. package/registry/uptime.js +154 -0
  79. package/server.js +105 -44
  80. package/shl/shl.js +64 -9
  81. package/stats-db.js +93 -0
  82. package/stats.js +158 -1
  83. package/testing/search.js +2 -108
  84. package/testing/store.js +5 -0
  85. package/testing/testing.js +20 -0
  86. package/token/token-template.html +5 -4
  87. package/token/token.js +17 -10
  88. package/translations/Messages.properties +9 -0
  89. package/tx/README.md +52 -0
  90. package/tx/cs/cs-api.js +90 -14
  91. package/tx/cs/cs-cs.js +56 -1
  92. package/tx/cs/cs-icd11.js +366 -112
  93. package/tx/cs/cs-icf.js +418 -0
  94. package/tx/cs/cs-loinc.js +47 -25
  95. package/tx/cs/cs-omop.js +10 -3
  96. package/tx/cs/cs-snomed.js +918 -55
  97. package/tx/cs/cs-ucum.js +6 -0
  98. package/tx/cs/icd11-expressions.js +191 -78
  99. package/tx/importers/export-icd11-codesystem.js +58 -12
  100. package/tx/importers/icd11-schema.md +9 -5
  101. package/tx/library.js +98 -4
  102. package/tx/openapi-operations.js +4 -0
  103. package/tx/params.js +8 -1
  104. package/tx/provider.js +16 -4
  105. package/tx/sct/documentation.md +251 -0
  106. package/tx/sct/ecl.js +758 -46
  107. package/tx/sct/expressions.js +113 -52
  108. package/tx/syndication/syndication-feed.js +218 -0
  109. package/tx/syndication/syndication-store.js +415 -0
  110. package/tx/tx-html.js +15 -1
  111. package/tx/tx.fhir.org.yml +2 -3
  112. package/tx/tx.js +144 -2
  113. package/tx/vs/vs-vsac.js +25 -6
  114. package/tx/workers/expand.js +33 -6
  115. package/tx/workers/lookup.js +61 -6
  116. package/tx/workers/subsumes.js +1 -1
  117. package/tx/workers/translate.js +166 -15
  118. package/tx/workers/validate.js +128 -6
  119. package/utilities/make-sct-test-extension.js +264 -0
  120. package/xig/xig.js +32 -0
  121. package/registry/readme.md +0 -124
@@ -12,6 +12,7 @@ const {
12
12
  ServerInformation,
13
13
  ServerVersionInformation,
14
14
  } = require('./model');
15
+ const { UptimeHistory } = require('./uptime');
15
16
  const {Extensions} = require("../tx/library/extensions");
16
17
  const {debugLog} = require("../tx/operation-context");
17
18
 
@@ -44,11 +45,16 @@ class RegistryCrawler {
44
45
  apiKeys: config.apiKeys || {}, // Map of server URL or code to API key
45
46
  allowPrivateAddresses: config.allowPrivateAddresses || false, // SSRF opt-out for local test servers
46
47
  // the most a server's response can be, once unzipped: the servers are other people's
47
- maxResponseBytes: (config.maxResponseMB || 100) * 1024 * 1024
48
+ maxResponseBytes: (config.maxResponseMB || 100) * 1024 * 1024,
49
+ uptimeRetentionDays: config.uptimeRetentionDays,
50
+ uptimeWindowDays: config.uptimeWindowDays
48
51
  };
49
52
  this.stats = stats;
50
53
 
51
54
  this.currentData = new ServerRegistries();
55
+ // one record per endpoint per scan: whether it answered. Reported on the software page
56
+ this.uptime = new UptimeHistory({ retentionDays: config.uptimeRetentionDays, windowDays: config.uptimeWindowDays });
57
+ this.scanTime = null; // when the scan in progress started (ms)
52
58
  this.crawlTimer = null;
53
59
  this.isCrawling = false;
54
60
  this.errors = [];
@@ -133,6 +139,7 @@ class RegistryCrawler {
133
139
 
134
140
  this.isCrawling = true;
135
141
  const startTime = new Date();
142
+ this.scanTime = startTime.getTime();
136
143
  this.errors = [];
137
144
  this.totalBytes = 0;
138
145
 
@@ -168,6 +175,7 @@ class RegistryCrawler {
168
175
 
169
176
  // Update the current data
170
177
  this.currentData = newData;
178
+ this.uptime.retain(this.endpointAddresses(newData), this.scanTime);
171
179
  } catch (error) {
172
180
  debugLog(error);
173
181
  this.addLogEntry('error', 'Exception Scanning:', error);
@@ -326,6 +334,7 @@ class RegistryCrawler {
326
334
  version.valueSets = [...new Set(version.valueSets)].sort();
327
335
  version.lastSuccess = new Date();
328
336
  version.lastTat = `${Date.now() - startTime}ms`;
337
+ this.uptime.record(version.address, true, this.scanTime || startTime);
329
338
 
330
339
  this.addLogEntry('info', ` Server ${version.address}: ${version.lastTat} for ${version.codeSystems.length} CodeSystems and ${version.valueSets.length} ValueSets`);
331
340
 
@@ -335,12 +344,33 @@ class RegistryCrawler {
335
344
  this.addLogEntry('error', `Server ${version.address}: Error after ${elapsed}ms: ${error.message}`);
336
345
  version.error = error.message;
337
346
  version.lastTat = `${elapsed}ms`;
347
+ // a scan cut short (the server shutting down) says nothing about the endpoint
348
+ if (!this.abortController?.signal.aborted) {
349
+ this.uptime.record(version.address, false, this.scanTime || startTime);
350
+ }
338
351
  this.carryForwardSoftware(version);
339
352
  }
340
353
 
341
354
  return version;
342
355
  }
343
356
 
357
+ /**
358
+ * Every endpoint address in a scan's data
359
+ */
360
+ endpointAddresses(data) {
361
+ const result = [];
362
+ for (const registry of (data && data.registries) || []) {
363
+ for (const server of registry.servers || []) {
364
+ for (const version of server.versions || []) {
365
+ if (version.address) {
366
+ result.push(version.address);
367
+ }
368
+ }
369
+ }
370
+ }
371
+ return result;
372
+ }
373
+
344
374
  /**
345
375
  * CapabilityStatement.software.releaseDate, if it means anything. A date within a few
346
376
  * minutes of now is the server's clock, not a release date: FHIRsmith up to 0.14.2 filled
@@ -710,6 +740,16 @@ class RegistryCrawler {
710
740
  return this.currentData.toJSON();
711
741
  }
712
742
 
743
+ loadUptime(json) {
744
+ this.uptime = UptimeHistory.fromJSON(json, {
745
+ retentionDays: this.config.uptimeRetentionDays, windowDays: this.config.uptimeWindowDays
746
+ });
747
+ }
748
+
749
+ saveUptime() {
750
+ return this.uptime.toJSON();
751
+ }
752
+
713
753
  /**
714
754
  * Add log entry to the crawler's log history
715
755
  * @param {string} level - Log level (info, error, warn, debug)
@@ -0,0 +1,162 @@
1
+ # Registry API
2
+
3
+ The normative description of the coordination server API is the
4
+ [ecosystem page](https://build.fhir.org/ig/HL7/fhir-tx-ecosystem-ig/ecosystem.html) of the
5
+ terminology ecosystem IG. This page describes what this registry implements, including where
6
+ it extends the IG. The machine-readable description is the OpenAPI 3.1 document, which is the
7
+ contract:
8
+
9
+ | URL | |
10
+ |---|---|
11
+ | `/tx-reg/openapi` | Browsable reference (HTML), with a "try it" form for each operation; the JSON for other clients |
12
+ | `/tx-reg/openapi.json` | The description as JSON |
13
+ | `/tx-reg/openapi.yaml` | The description as YAML - the source file, [openapi.yaml](../openapi.yaml) |
14
+
15
+ Every response carries `Link: </tx-reg/openapi.json>; rel="service-desc"` (RFC 8631), and the
16
+ HTML pages the matching `<link>` element.
17
+
18
+ There are two operations, both `GET`:
19
+
20
+ | Endpoint | |
21
+ |---|---|
22
+ | `/tx-reg/` | **Discovery** - the server endpoints in the ecosystem |
23
+ | `/tx-reg/resolve` | **Resolution** - which endpoints to use for a code system or value set |
24
+
25
+ Both answer JSON, or an HTML page when the request's `Accept` header contains `text/html`.
26
+ `/tx-reg/resolve` in a browser without parameters is a form for trying it. Parameters are read
27
+ leniently: unknown parameters are ignored, and a repeated parameter takes its first value
28
+ (existing ecosystem clients depend on this).
29
+
30
+ When the registry is secured ([security.md](security.md)) the JSON forms of both require a
31
+ token, sent as `Authorization: Bearer {token}` or `Api-Key: {token}`; without one they answer
32
+ `401` with `WWW-Authenticate: Bearer realm="tx-reg"`. The HTML pages and the OpenAPI
33
+ description are always open.
34
+
35
+ ## Discovery: `GET /tx-reg/`
36
+
37
+ Lists the endpoints in the ecosystem, one entry per server per FHIR version.
38
+
39
+ | Parameter | |
40
+ |---|---|
41
+ | `registry` | Only endpoints from this registry (its code in the master file) |
42
+ | `server` | Only endpoints of this server (its code in its registry) |
43
+ | `fhirVersion` | Only endpoints for this FHIR version: `R4`, `R4B`, `R5` ... or `4.0.1`, `4.0` |
44
+ | `url` | Only endpoints that host this code system (`url` or `url\|version`), the authoritative ones first |
45
+ | `authoritativeOnly` | With `url`: only endpoints authoritative for it |
46
+ | `language` | With `url`: prefer endpoints with a language specific claim for it |
47
+
48
+ ```json
49
+ {
50
+ "last-update": "2026-10-21T22:30:11.412Z",
51
+ "master-url": "https://fhir.github.io/ig-registry/tx-servers.json",
52
+ "results": [
53
+ {
54
+ "server-name": "tx.fhir.org", "server-code": "tx.fhir.org",
55
+ "registry-name": "HL7 Terminology Services", "registry-code": "hl7",
56
+ "registry-url": "https://raw.githubusercontent.com/FHIR/ig-registry/master/hl7-tx-servers.json",
57
+ "url": "http://tx.fhir.org/r4", "fhirVersion": "4.0.1",
58
+ "error": null, "last-success": 184233, "systems": 2311,
59
+ "authoritative": ["http://hl7.org/fhir/*", "http://terminology.hl7.org/*"],
60
+ "authoritative-valuesets": ["http://hl7.org/fhir/ValueSet/*"],
61
+ "security": "open", "open": true
62
+ }
63
+ ]
64
+ }
65
+ ```
66
+
67
+ `error` is what went wrong the last time the endpoint was scanned (null if it worked), and
68
+ `last-success` is how many milliseconds ago it was last scanned successfully. Without `url`,
69
+ `authoritative` and `authoritative-valuesets` are the server's claim masks as registered, and
70
+ there are no candidate lists - listing everything every server hosts would be enormous. With
71
+ `url`, an entry has `candidate: [url]` when the endpoint hosts the code system without
72
+ claiming authority for it, and with `language` too, `languages` gives the tag of the claim the
73
+ endpoint matched on.
74
+
75
+ ## Resolution: `GET /tx-reg/resolve`
76
+
77
+ Returns the endpoints to use for one code system or value set at one FHIR version: those
78
+ authoritative for it, then the other candidates that host it. Choosing between several is the
79
+ client's business (the Java tooling takes the first).
80
+
81
+ | Parameter | |
82
+ |---|---|
83
+ | `fhirVersion` | Required. `R4`, `R4B`, `R5` ... or `4.0.1`, `4.0` |
84
+ | `url` | The code system, `url` or `url\|version`. One of `url` and `valueSet` is required |
85
+ | `version` | A version for `url`, instead of `url\|version` (an extension to the IG; ignored if `url` has one) |
86
+ | `valueSet` | The value set, `url` or `url\|version` |
87
+ | `authoritativeOnly` | Only authoritative endpoints, no candidates |
88
+ | `language` | The language the client needs (BCP-47) |
89
+ | `usage` | What the client is doing - `publication`, `validation`, `code-generation` - for servers that restrict their use |
90
+
91
+ ```json
92
+ {
93
+ "formatVersion": "1",
94
+ "registry-url": "https://fhir.github.io/ig-registry/tx-servers.json",
95
+ "authoritative": [
96
+ { "server-name": "tx.fhir.org", "url": "http://tx.fhir.org/r4", "fhirVersion": "4.0.1",
97
+ "security": "open", "open": true }
98
+ ],
99
+ "candidates": [
100
+ { "server-name": "HL7 Europe Terminology Server", "url": "http://tx.hl7europe.eu/r4",
101
+ "fhirVersion": "4.0.1", "content": "complete", "security": "open", "open": true }
102
+ ]
103
+ }
104
+ ```
105
+
106
+ `access_info`, when present, is the server's own (markdown) note on how to get access.
107
+ `content` on a candidate is the content mode the server reports for the code system
108
+ (`complete`, `fragment` ...), if it reports one. `authoritative` and `candidates` are omitted
109
+ when empty, so an empty object means nobody hosts it.
110
+
111
+ ### How resolution decides
112
+
113
+ * An endpoint is only ever returned for content it actually hosts, as learned by scanning:
114
+ code systems from its TerminologyCapabilities, value sets from its ValueSet search. A claim
115
+ without the content gets nothing.
116
+ * Code system versions: a SNOMED CT edition (`http://snomed.info/sct|http://snomed.info/sct/{edition}`)
117
+ is hosted by any endpoint that hosts a version of that edition; a dated SNOMED CT version
118
+ must match exactly. For every other code system, an endpoint that hosts the code system is
119
+ taken to host all its versions - servers rarely declare versions fully, and the server will
120
+ say if it hasn't got one.
121
+ * Endpoints whose server claims authority for the URL (a mask in `authoritative`, or
122
+ `authoritative-valuesets`) are listed as `authoritative`; the others that host it as
123
+ `candidates`. An endpoint is never in both.
124
+ * `language`: servers with a language specific claim (`languages` in the registration)
125
+ matching the request come first among the authoritative, the most specific tag first
126
+ (`de-CH` before `de`); a server with a claim for the code system but not the language is
127
+ still authoritative, after them; candidates are marked `language-support: unknown`. Value
128
+ set resolution has no language dimension.
129
+ * `usage`: a server whose registration has a `usage` list is only returned when the request's
130
+ `usage` is in it. A request without `usage` gets only servers without the restriction.
131
+ * `exclusions`: content a server's registration excludes is hidden from that server
132
+ entirely - it is neither authoritative nor a candidate for it.
133
+ * The `url` of a FHIR-defined code system or value set may be given with or without the
134
+ `|version`; the FHIR version itself is taken from `fhirVersion`.
135
+
136
+ ### Differences from the ecosystem IG
137
+
138
+ * Discovery without `url` returns claim masks, not candidate lists (above).
139
+ * Entries carry the IG's security flags (`open`, `token`, `oauth` ...) and also a `security`
140
+ string (`open` or `api-key`), kept for existing clients. Both record how the *registry*
141
+ reaches the server: an endpoint is `api-key`/`token` when the registry is configured with a
142
+ key for it, and `open` otherwise. They say nothing about how the client will be let in.
143
+ * Resolve takes `version` as an alternative to `url|version`.
144
+ * Resolve omits `authoritative` and `candidates` when they are empty.
145
+
146
+ ## Operational pages
147
+
148
+ These are HTML, not part of the API, and never secured:
149
+
150
+ | URL | |
151
+ |---|---|
152
+ | `/tx-reg/software` | What each server runs, how old it is, and its uptime ([internals.md](internals.md#the-software-page)) |
153
+ | `/tx-reg/log` | The scanner's log |
154
+ | `/tx-reg/login`, `/tx-reg/account`, `/tx-reg/admin/users` | Login and user administration, when users are configured ([security.md](security.md)) |
155
+
156
+ ## Keeping the description honest
157
+
158
+ [openapi.yaml](../openapi.yaml) is written by hand. `tests/registry/openapi.test.js` fails if
159
+ the router has a route the description doesn't (unless the test's `EXCLUDED` table names it as
160
+ operational), if the description has a route the router hasn't, or if the documented query
161
+ parameters differ from `DISCOVERY_PARAMS` and `RESOLVE_PARAMS` in `registry.js`. Change the
162
+ description with the code.
@@ -0,0 +1,93 @@
1
+ # Registry configuration
2
+
3
+ The registry is configured under `modules.registry` in the server's configuration file (see
4
+ [config-template.json](../../config-template.json) for the whole file). The minimum is
5
+ `"enabled": true`; everything else has a default.
6
+
7
+ ```json
8
+ "registry": {
9
+ "enabled": true,
10
+ "masterUrl": "https://fhir.github.io/ig-registry/tx-servers.json",
11
+ "crawlInterval": 30,
12
+ "timeout": 30000,
13
+ "userAgent": "YourServer/1.0",
14
+ "apiKeys": {},
15
+ "maxResponseMB": 100,
16
+ "uptimeWindowDays": 30,
17
+ "uptimeRetentionDays": 90,
18
+ "security": { }
19
+ }
20
+ ```
21
+
22
+ ## The ecosystem
23
+
24
+ | Setting | Default | |
25
+ |---|---|---|
26
+ | `masterUrl` | HL7's `tx-servers.json` | The ecosystem's master registration file ([readme.md](readme.md#the-specifications)). Point this at your own file to run a registry for your own ecosystem. |
27
+ | `apiKeys` | `{}` | API keys for servers the registry needs one to scan, by server code (as in the server's registration). Sent as an `Api-Key` header. An endpoint scanned with a key is reported as `security: api-key` in the API. |
28
+
29
+ ## Scanning
30
+
31
+ | Setting | Default | |
32
+ |---|---|---|
33
+ | `crawlInterval` | none | Minutes between scans. 0 or absent: the registry never scans, and answers from the data it last saved - useful for a read-only replica or a test. |
34
+ | `timeout` | 30000 | Per-request timeout when scanning, in milliseconds. |
35
+ | `userAgent` | `FHIRRegistryServer/1.0` | The `User-Agent` the scanner sends. Server operators see this in their logs, so name your registry. |
36
+ | `maxResponseMB` | 100 | The most a scanned server's response may be once decompressed. The servers are other people's; a misbehaving one must not exhaust the registry's memory. |
37
+ | `allowPrivateAddresses` | false | The scanner refuses to fetch from private, loopback and link-local addresses (SSRF protection - the URLs it follows come from registration files and servers it does not control). Set true only for a test registry whose servers are on localhost. |
38
+ | `releasesUrl` | GitHub's FHIRsmith releases API | Where the software page learns FHIRsmith's release dates. `""` turns the lookup off (the dates in the shipped `CHANGELOG.md` are used instead). |
39
+
40
+ ## Uptime
41
+
42
+ | Setting | Default | |
43
+ |---|---|---|
44
+ | `uptimeWindowDays` | 30 | The period the software page reports each server's uptime over. |
45
+ | `uptimeRetentionDays` | 90 | How long per-endpoint scan outcomes are kept. |
46
+
47
+ ## Security
48
+
49
+ `security` closes the JSON API. Absent, the registry is open. The block and its three methods
50
+ are described in [security.md](security.md); in brief:
51
+
52
+ ```json
53
+ "security": {
54
+ "apiToken": "one token every client sends",
55
+ "failedAuthPerWindow": 60,
56
+ "users": {
57
+ "database": "registry-auth.db",
58
+ "adminUsername": "admin",
59
+ "adminPassword": "...",
60
+ "sessionSecret": "...",
61
+ "cookieSecure": true,
62
+ "loginAttemptsPerWindow": 20
63
+ },
64
+ "oidc": {
65
+ "issuer": "https://login.microsoftonline.com/{tenant}/v2.0",
66
+ "audience": "api://ecosystem.hl7.org",
67
+ "requiredScope": "registry.read",
68
+ "jwksUri": "https://.../keys",
69
+ "algorithms": ["RS256", "ES256"]
70
+ }
71
+ }
72
+ ```
73
+
74
+ ## Files
75
+
76
+ The registry keeps its state in the server's data folder (`FHIRSMITH_DATA_DIR`, else `./data`):
77
+
78
+ | File | |
79
+ |---|---|
80
+ | `registry/registry-data.json` | The result of the last scan - what every endpoint hosts. Loaded at startup, so the registry answers immediately after a restart. |
81
+ | `registry/registry-uptime.json` | Per-endpoint scan outcomes, for the uptime figures. |
82
+ | `databases/registry-auth.db` | Users and tokens, when `security.users` is configured (`database` names it; an absolute path or `:memory:` also works). |
83
+
84
+ ## Behind a proxy
85
+
86
+ The registry is normally behind nginx (or similar) terminating TLS. Two things matter:
87
+
88
+ * the proxy must pass `X-Forwarded-Proto`, and the server must trust it (`trustProxy` in the
89
+ server configuration), or the login cookie - `Secure` by default - is never set and logins
90
+ bounce straight back to the login page (`cookieSecure: false` is the escape for a registry
91
+ that really is served over plain HTTP);
92
+ * the proxy must pass `Host` (or `X-Forwarded-Host`), or every form post is refused by the
93
+ same-origin check.
@@ -0,0 +1,74 @@
1
+ # Documents
2
+
3
+ A registry run for an ecosystem is a natural place to publish the ecosystem's own
4
+ documentation: its policies, the configuration its clients should use, the terms under which
5
+ servers are listed. The registry stores and serves such documents itself, so an administrator
6
+ can keep them next to the registry without a separate website.
7
+
8
+ Documents are available when `security.users` is configured ([security.md](security.md)):
9
+ they live in the same database as the users, and who may read and edit them is decided by the
10
+ same logins.
11
+
12
+ ## What a document is
13
+
14
+ | | |
15
+ |---|---|
16
+ | **name** | Its address: `/tx-reg/docs/{name}`. Letters, digits, dots, hyphens and underscores, up to 100 characters, unique. Can be changed (the old address stops working). |
17
+ | **type** | `md` (Markdown) or `json`. |
18
+ | **public** | Whether anyone can read it. If not, only logged in users can. |
19
+ | **last edited** | When, and by whom. |
20
+ | **content** | The text, up to 2 MB. JSON must parse to be saved. |
21
+
22
+ ## Who sees what
23
+
24
+ * A **public** document is readable by anyone: on the page, and as the raw file.
25
+ * A document that is **not public** is readable by anyone who is logged in - any user, not
26
+ just administrators. This is for enterprise configuration and policy that should not be on
27
+ the open web. A logged in session is required: an API token, the configured API token or an
28
+ OIDC JWT does *not* give access to documents (they authenticate software to the API; the
29
+ documents are for people). To a stranger such a document does not exist: a browser is sent
30
+ to the login page, anything else gets 404, so names are not disclosed.
31
+ * **Administrators** - the configured one, and any user with the administrator flag - create,
32
+ edit, rename and delete documents. An administrator user can also manage users.
33
+
34
+ ## Reading
35
+
36
+ `GET /tx-reg/docs` lists the documents the reader may see. `GET /tx-reg/docs/{name}` returns
37
+ the document: in a browser (`Accept: text/html`), rendered in the registry's pages - Markdown
38
+ as HTML, JSON pretty-printed - with its metadata and links to its history and, for an
39
+ administrator, to edit it; otherwise the raw content, as `text/markdown` or
40
+ `application/json`, with `Last-Modified`, so a client such as `curl` or a script gets the
41
+ file itself:
42
+
43
+ ```
44
+ curl https://ecosystem.example.org/tx-reg/docs/client-configuration
45
+ ```
46
+
47
+ Markdown is rendered without raw HTML: inline and block HTML in a document is left out (an
48
+ HTML comment marks where), and links to `javascript:` and the like are not made. So a
49
+ document can't put markup or script on a page that shares its origin with other modules.
50
+ JSON documents are shown escaped.
51
+
52
+ ## Editing
53
+
54
+ Editing is a text area with the four metadata fields above it - `/tx-reg/docs/new` to create,
55
+ `/tx-reg/docs/{name}/edit` to change, and a delete button on the edit page. There is no rich
56
+ editor: a document that needs one is written elsewhere and pasted in. Line endings are
57
+ normalised to `\n`.
58
+
59
+ ## History
60
+
61
+ Every change - creation, edit, rename, deletion - is logged with who made it, when, and the
62
+ complete document as it then was. `/tx-reg/docs/{name}/history` lists the log, and each entry
63
+ opens that version, rendered. Each version keeps the visibility it had then: a version from
64
+ when the document was private is only for logged in users, even after the document has been
65
+ made public. Deleting a
66
+ document keeps its log, but the log is reached by the document's id, so it is no longer
67
+ browsable once the document is gone; it remains in the database (`document_log`) for the
68
+ administrator who needs it. A deleted name may be reused; the new document starts a new log.
69
+
70
+ ## Storage
71
+
72
+ Two tables in the users' database (`databases/registry-auth.db`): `documents` (the current
73
+ state) and `document_log` (every version). Nothing is cached; a document is read on each
74
+ request.
@@ -0,0 +1,151 @@
1
+ # Registry internals
2
+
3
+ How the module works, for anyone changing it. The user-facing behaviour is in
4
+ [api.md](api.md) and [security.md](security.md).
5
+
6
+ ## Code
7
+
8
+ | File | |
9
+ |---|---|
10
+ | `registry.js` | `RegistryModule`: initialisation, routes, the HTML pages, scan scheduling, saving and loading state |
11
+ | `api.js` | `RegistryAPI`: discovery and resolution over the scanned data, and the JSON-to-HTML rendering of their results |
12
+ | `crawler.js` | `RegistryCrawler`: scanning the ecosystem |
13
+ | `model.js` | The data model (`ServerRegistries` → `ServerRegistry` → `ServerInformation` → `ServerVersionInformation`), its JSON form, and `ServerRegistryUtilities` - mask matching and version matching |
14
+ | `uptime.js` | `UptimeHistory`: per-endpoint scan outcomes, and the uptime calculation |
15
+ | `fhirsmith-releases.js` | FHIRsmith's release list (GitHub, falling back to `CHANGELOG.md`), for dating servers on the software page |
16
+ | `auth.js`, `auth-store.js` | `RegistryAuth` (API guard, OIDC verification, login/admin/account pages) and `AuthStore` (users, tokens, usage and documents in SQLite) |
17
+ | `documents.js` | `RegistryDocuments`: the document pages, raw serving, editing and history ([documents.md](documents.md)) |
18
+ | `openapi.yaml`, `openapi.js` | The API description, and the module that serves it |
19
+ | `registry-template.html` | The page template (`[%title%]`, `[%content%]`, `[%account%]` for the login link, and the footer placeholders html-server fills) |
20
+
21
+ `test-runner.js` is a leftover and does not run.
22
+
23
+ `server.js` creates the module when `modules.registry.enabled` is set, calls
24
+ `initialize(config)` and mounts `router` at `/tx-reg`. Nothing else in FHIRsmith depends on it.
25
+
26
+ ## Scanning
27
+
28
+ `RegistryCrawler.crawl()` runs every `crawlInterval` minutes (first 5 seconds after start),
29
+ and never concurrently with itself. A scan builds a fresh `ServerRegistries` from scratch:
30
+
31
+ 1. fetch the master registration file (`formatVersion` must be `"1"`);
32
+ 2. for each registry it lists, fetch the registry file and build a `ServerRegistry`;
33
+ 3. for each server, a `ServerInformation` with its claims (`authoritative`,
34
+ `authoritative-valuesets`, `languages`, `usage`, `exclusions`), and for each of its
35
+ `fhirVersions` a `ServerVersionInformation` - the *endpoint*;
36
+ 4. for each endpoint, by the major FHIR version in its registration: `/metadata` (software name,
37
+ version and release date), `/metadata?mode=terminology` (code systems and versions, sorted
38
+ and de-duplicated), `/ValueSet?_elements=url,version` (value set URLs, following `next`
39
+ links). R3 is handled separately from R4/R5 because it has no TerminologyCapabilities
40
+ resource: its `?mode=terminology` answer is a Parameters-shaped structure.
41
+
42
+ If the whole scan fails (the master file can't be read) the previous data is kept and the
43
+ outcome recorded. If an endpoint fails, the error is recorded on it, `lastSuccess` is left
44
+ from the previous scan, and the software fields are carried forward from the previous data
45
+ (`carryForwardSoftware`), so the software page does not lose track of a server that is down.
46
+ The endpoint's code systems and value sets are *not* carried forward: a down server resolves
47
+ nothing until it is back - but see `ServerVersionInformation.update()`, which is how a
48
+ loaded-from-disk record is merged if that path is ever used.
49
+
50
+ Every fetch goes through `fetchJson`, which adds a cache-busting `_ts`, sends the configured
51
+ `User-Agent` and the server's `Api-Key` if there is one, limits the decompressed response to
52
+ `maxResponseMB`, and resolves host names through `ssrfLookup`: an address that is not a public
53
+ unicast address (loopback, private, link-local including the cloud metadata address, CGNAT,
54
+ multicast ...) is refused, before and after DNS, so a hostile registration file cannot make the
55
+ registry probe its own network. `allowPrivateAddresses` turns that off for local testing.
56
+
57
+ Each endpoint probe also records its outcome in `UptimeHistory` with the scan's start time, and
58
+ after a successful scan the history is trimmed to the endpoints that still exist.
59
+
60
+ ## State
61
+
62
+ Two JSON files in the data folder's `registry` directory, written after every scan and read
63
+ at startup:
64
+
65
+ * `registry-data.json` - `ServerRegistries.toJSON()`: the ecosystem as last scanned, with
66
+ `"version": "1"`. Loading it is what lets a restarted registry answer at once.
67
+ * `registry-uptime.json` - `UptimeHistory.toJSON()`: `{ version: "1", endpoints: { url:
68
+ [[timeMs, 1|0], ...] } }`, kept to `uptimeRetentionDays`.
69
+
70
+ Users, tokens, usage and documents live in `databases/registry-auth.db` (better-sqlite3, WAL;
71
+ the `admin` column on `users` is added on open if an older database lacks it). Request
72
+ counts are accumulated in memory and flushed once a minute and at shutdown, so a busy registry
73
+ is not writing SQLite on every API call.
74
+
75
+ A scan builds a new tree and swaps it in whole (`RegistryModule.updateData()` is where a
76
+ lock would go if one were ever needed; in Node's single thread the swap is atomic), so a
77
+ request sees either the old ecosystem or the new, never a mixture.
78
+
79
+ ## Resolution
80
+
81
+ `RegistryAPI.resolveCodeSystem()` and `resolveValueSet()` walk every endpoint of every server
82
+ in every registry and keep those that (a) match the FHIR version
83
+ (`ServerRegistryUtilities.versionMatches`, which accepts release codes and numbers), (b) are
84
+ not excluded for the URL, (c) pass the `usage` test, and (d) actually host the URL
85
+ (`hasMatchingCodeSystem`, with the SNOMED CT edition rule, or a value set lookup). Those whose
86
+ server matches an authority claim (`ServerInformation.matchAuthCS`, which scores language
87
+ specific claims so the most specific tag sorts first) go in `authoritative`; the rest in
88
+ `candidates`, unless `authoritativeOnly`. Masks are matched by `ServerRegistryUtilities`, with
89
+ `*` as the wildcard; actual content is matched without wildcards.
90
+
91
+ `discover()` is the same walk without the hosting test (or with it, when `url` is given),
92
+ producing one row per endpoint.
93
+
94
+ ## The pages
95
+
96
+ All pages go through `RegistryModule.renderPage()`, which applies the template via
97
+ `library/html-server` and fills `[%account%]` with the login, account or users link from
98
+ `RegistryAuth.navLinks()`. Content that came from registration files or scanned servers -
99
+ names, URLs, errors, software names - is other people's input, and is escaped on output; URLs
100
+ are only linked when they are http(s) (`safeHref`). `tests/registry/registry-hostile-content.test.js`
101
+ feeds script-laden registration data through every page to keep it that way.
102
+
103
+ ### The software page
104
+
105
+ `buildSoftwareContent()` groups each server's endpoints by software name and version
106
+ (`softwareRows`), so a server's R3/R4/R5 endpoints are one row under their common base URL
107
+ (`commonBaseUrl`: the shared prefix of the endpoint URLs, backed up to a path boundary) unless
108
+ they run different software. For FHIRsmith servers, `FhirsmithReleases.describe()` dates the
109
+ version against the release list and says how many releases behind it is; other software is
110
+ dated by the `software.releaseDate` it reports, as the UTC day (servers running the same
111
+ release report it in different time zones). Uptime per row is `UptimeHistory.uptime()` over
112
+ all the row's endpoints: the share of probes in the window that succeeded.
113
+
114
+ ### Login and administration
115
+
116
+ `RegistryAuth` adds its routes only when `security.users` is configured. Sessions
117
+ (`express-session`, cookie `txreg.sid`), CSRF (`lusca`) and the same-origin check
118
+ (`library/same-origin`) are written out on every route rather than composed, so that static
119
+ analysis can see the CSRF middleware on each cookie-using route - the convention the other
120
+ modules follow. The API guard (`apiGuard()`) is a separate middleware on `/` and `/resolve`,
121
+ applied only when the request does not accept HTML.
122
+
123
+ ## Security notes for maintainers
124
+
125
+ * The registry fetches URLs it does not control. Keep every outbound request on `fetchJson`
126
+ (SSRF guard, size limit, timeout). The OIDC fetches (`fetch` for discovery, `jose` for keys)
127
+ go only to administrator-configured https URLs.
128
+ * Everything rendered from scanned data or the database is escaped; `safeHref` for links.
129
+ * API tokens are compared with `library/request-token` (constant time); user tokens by
130
+ SHA-256 lookup; passwords with bcrypt, and a login for an unknown user still runs a bcrypt
131
+ comparison so the timing does not reveal which users exist.
132
+ * A user's own token operations are scoped by the user id from the session
133
+ (`revokeToken(userId, tokenId)`), never by the token id alone.
134
+
135
+ ## Tests
136
+
137
+ `tests/registry`, run with `npx jest tests/registry`:
138
+
139
+ | File | |
140
+ |---|---|
141
+ | `registry.test.js`, `registry-resolve.test.js` | Discovery and resolution against `test-data.json` |
142
+ | `registry-language.test.js`, `registry-exclusions.test.js` | Language specific claims; exclusions |
143
+ | `registry-software.test.js` | Release dating, the collapsed software rows, uptime history |
144
+ | `registry-auth.test.js` | The three security methods end to end (supertest), including an OIDC provider stood up in the test with a generated key pair |
145
+ | `registry-documents.test.js` | Documents: visibility, editing, history, validation; administrator users |
146
+ | `registry-hostile-content.test.js` | Script injection through registration data, on every page |
147
+ | `registry-footer.test.js` | The "last updated" footer |
148
+ | `openapi.test.js` | The OpenAPI description agrees with the router and the parameter lists |
149
+
150
+ `test-data.json` is a saved `registry-data.json` for a small fictional ecosystem; tests load it
151
+ into a crawler with `loadData()` rather than scanning anything.