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.
- package/CHANGELOG.md +83 -0
- package/README.md +80 -13
- package/config-template.json +106 -1
- package/ips/ai-client.js +263 -0
- package/ips/composer.js +805 -0
- package/ips/editor-schema.js +289 -0
- package/ips/editor.js +290 -0
- package/ips/fhirpath-ips.js +397 -0
- package/ips/ips-codeable-concepts.md +1742 -0
- package/ips/ips-template.html +125 -0
- package/ips/ips-valuesets.js +159 -0
- package/ips/ips.js +1736 -0
- package/ips/membership.js +179 -0
- package/ips/pipeline.js +1012 -0
- package/ips/presenter.js +974 -0
- package/ips/profile-editor.js +578 -0
- package/ips/profiles.js +506 -0
- package/ips/readme.md +460 -0
- package/ips/render.js +1482 -0
- package/ips/search-params.js +83 -0
- package/ips/search.js +667 -0
- package/ips/share.js +178 -0
- package/ips/store.js +650 -0
- package/ips/terminology.js +395 -0
- package/ips/tx-client.js +202 -0
- package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
- package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
- package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
- package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
- package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
- package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
- package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
- package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
- package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
- package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
- package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
- package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
- package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-medication.json +50 -0
- package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
- package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
- package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
- package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
- package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
- package/ips/valuesets/ValueSet-ips-route.json +46 -0
- package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
- package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
- package/ips/valuesets/elements.json +44 -0
- package/library/daily-report.js +422 -0
- package/library/fhir-search-values.js +138 -0
- package/library/html-server.js +32 -23
- package/library/mailer.js +77 -0
- package/library/narrative-check.js +21 -2
- package/library/npm-audit.js +25 -0
- package/library/package-manager.js +4 -2
- package/library/sqlite-session-store.js +29 -1
- package/package.json +7 -2
- package/packages/packages.js +34 -3
- package/publisher/publisher.js +29 -0
- package/registry/auth-store.js +366 -0
- package/registry/auth.js +872 -0
- package/registry/crawler.js +41 -1
- package/registry/documentation/api.md +162 -0
- package/registry/documentation/configuration.md +93 -0
- package/registry/documentation/documents.md +74 -0
- package/registry/documentation/internals.md +151 -0
- package/registry/documentation/readme.md +100 -0
- package/registry/documentation/security.md +199 -0
- package/registry/documents.js +309 -0
- package/registry/openapi.yaml +30 -0
- package/registry/registry-template.html +1 -1
- package/registry/registry.js +230 -44
- package/registry/uptime.js +154 -0
- package/server.js +105 -44
- package/shl/shl.js +64 -9
- package/stats-db.js +93 -0
- package/stats.js +158 -1
- package/testing/search.js +2 -108
- package/testing/store.js +5 -0
- package/testing/testing.js +20 -0
- package/token/token-template.html +5 -4
- package/token/token.js +17 -10
- package/translations/Messages.properties +9 -0
- package/tx/README.md +52 -0
- package/tx/cs/cs-api.js +90 -14
- package/tx/cs/cs-cs.js +56 -1
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +418 -0
- package/tx/cs/cs-loinc.js +47 -25
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +918 -55
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- package/tx/library.js +98 -4
- package/tx/openapi-operations.js +4 -0
- package/tx/params.js +8 -1
- package/tx/provider.js +16 -4
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- package/tx/syndication/syndication-feed.js +218 -0
- package/tx/syndication/syndication-store.js +415 -0
- package/tx/tx-html.js +15 -1
- package/tx/tx.fhir.org.yml +2 -3
- package/tx/tx.js +144 -2
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/expand.js +33 -6
- package/tx/workers/lookup.js +61 -6
- package/tx/workers/subsumes.js +1 -1
- package/tx/workers/translate.js +166 -15
- package/tx/workers/validate.js +128 -6
- package/utilities/make-sct-test-extension.js +264 -0
- package/xig/xig.js +32 -0
- package/registry/readme.md +0 -124
package/registry/crawler.js
CHANGED
|
@@ -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.
|