fhirsmith 0.18.1 → 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.
- package/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +190 -9
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +331 -33
- package/ips/jurisdictions.js +65 -0
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +131 -17
- package/ips/presenter.js +1 -1
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +170 -20
- package/ips/render.js +769 -109
- package/ips/scripts.js +46 -0
- package/ips/store.js +123 -11
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/api.js +179 -1
- package/registry/crawler.js +86 -108
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/configuration.md +34 -0
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +58 -6
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +158 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +50 -16
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +199 -24
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +193 -67
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +861 -95
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +125 -61
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/tho-precedence.js +40 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +314 -67
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +81 -17
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +175 -15
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +44 -17
- package/tx/vs/vs-vsac.js +649 -101
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
package/registry/api.js
CHANGED
|
@@ -27,6 +27,20 @@ function securityFlags(security) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Code systems that tx.fhir.org handles specially. When the registry doesn't settle where
|
|
31
|
+
// a request involving one of these goes, it goes to the primary server (see chooseServer)
|
|
32
|
+
const SNOMED_CT = 'http://snomed.info/sct';
|
|
33
|
+
const PRIMARY_SERVER_SYSTEMS = ['http://unitsofmeasure.org', 'http://loinc.org', SNOMED_CT,
|
|
34
|
+
'http://www.nlm.nih.gov/research/umls/rxnorm', 'http://hl7.org/fhir/sid/cvx', 'urn:ietf:bcp:13', 'urn:ietf:bcp:47',
|
|
35
|
+
'urn:ietf:rfc:3986', 'http://www.ama-assn.org/go/cpt', 'urn:oid:1.2.36.1.2001.1005.17', 'urn:iso:std:iso:3166',
|
|
36
|
+
'http://varnomen.hgvs.org', 'http://unstats.un.org/unsd/methods/m49/m49.htm', 'urn:iso:std:iso:4217',
|
|
37
|
+
'http://hl7.org/fhir/sid/ndc', 'http://fhir.ohdsi.org/CodeSystem/concepts', 'http://fdasis.nlm.nih.gov', 'https://www.usps.com/'];
|
|
38
|
+
|
|
39
|
+
function isTxFhirOrg(server) {
|
|
40
|
+
const s = server.replace('http://', 'https://');
|
|
41
|
+
return s.startsWith('https://tx.fhir.org/') || s.startsWith('https://tx-dev.fhir.org/');
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
class RegistryAPI {
|
|
31
45
|
constructor(crawler) {
|
|
32
46
|
this.crawler = crawler;
|
|
@@ -592,6 +606,170 @@ class RegistryAPI {
|
|
|
592
606
|
};
|
|
593
607
|
}
|
|
594
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Choose one server for a request that involves a set of code systems.
|
|
611
|
+
*
|
|
612
|
+
* This is a server side copy of the routing logic in the HL7 Java tooling
|
|
613
|
+
* (TerminologyClientManager.chooseServer in org.hl7.fhir.core), for clients that aren't
|
|
614
|
+
* Java. It doesn't replace that code. The Java client does the work that needs its own
|
|
615
|
+
* context before routing, and so must the caller here:
|
|
616
|
+
*
|
|
617
|
+
* - systems: the code systems involved, as url or url|version, from the codings and the
|
|
618
|
+
* value set's includes and excludes, following imported value sets. Versions fixed by
|
|
619
|
+
* expansion parameters (system-version, force-system-version) should already be applied.
|
|
620
|
+
* If the caller couldn't follow an import, it should just use its default server.
|
|
621
|
+
* - language: only for language sensitive requests (see resolveCodeSystem).
|
|
622
|
+
*
|
|
623
|
+
* Differences from the Java code:
|
|
624
|
+
* - 'Does a candidate really have the code system' is answered from the content mode the
|
|
625
|
+
* server reported in its TerminologyCapabilities, not by searching the server. If it
|
|
626
|
+
* didn't report one, the candidate is assumed to be usable.
|
|
627
|
+
* - The 'primary server' for the systems tx.fhir.org handles specially is the default.
|
|
628
|
+
* - The value set's source server is found by resolving the value set here.
|
|
629
|
+
* - Ties are broken by order (the first system, then registration order), not hash order.
|
|
630
|
+
*
|
|
631
|
+
* @returns {{server: string, log: string[]}} the server to use, and why
|
|
632
|
+
*/
|
|
633
|
+
chooseServer(fhirVersion, systems, defaultServer, valueSet = '', usage = '', language = '') {
|
|
634
|
+
if (!fhirVersion) {
|
|
635
|
+
throw new Error('A FHIR version is required');
|
|
636
|
+
}
|
|
637
|
+
if (!defaultServer) {
|
|
638
|
+
throw new Error('A default server is required');
|
|
639
|
+
}
|
|
640
|
+
const log = [];
|
|
641
|
+
const list = [...new Set((systems || []).filter(s => s))];
|
|
642
|
+
if (valueSet) {
|
|
643
|
+
log.push(`Value set: ${valueSet}`);
|
|
644
|
+
}
|
|
645
|
+
if (list.length === 0) {
|
|
646
|
+
log.push('No code systems: using the default server');
|
|
647
|
+
return { server: defaultServer, log };
|
|
648
|
+
}
|
|
649
|
+
const server = this._chooseServer(fhirVersion, list, defaultServer, valueSet, usage, language, log);
|
|
650
|
+
return { server, log };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
_chooseServer(fhirVersion, systems, defaultServer, valueSet, usage, language, log) {
|
|
654
|
+
// what the registry says about each system on its own
|
|
655
|
+
const choices = systems.map(system => {
|
|
656
|
+
const r = this.resolveCodeSystem(fhirVersion, system, false, usage, language).result;
|
|
657
|
+
const choice = { system, authoritative: [], candidates: [], content: {} };
|
|
658
|
+
for (const e of r.authoritative || []) {
|
|
659
|
+
if (!choice.authoritative.includes(e.url)) choice.authoritative.push(e.url);
|
|
660
|
+
}
|
|
661
|
+
for (const e of r.candidates || []) {
|
|
662
|
+
if (!choice.candidates.includes(e.url)) {
|
|
663
|
+
choice.candidates.push(e.url);
|
|
664
|
+
choice.content[e.url] = e.content;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
log.push(`${system}: authoritative [${choice.authoritative.join(', ')}], candidates [${choice.candidates.join(', ')}]`);
|
|
668
|
+
return choice;
|
|
669
|
+
});
|
|
670
|
+
// the registry knows no server at all for the system
|
|
671
|
+
const unresolved = c => c.authoritative.length === 0 && c.candidates.length === 0;
|
|
672
|
+
|
|
673
|
+
// 1. a server that's authoritative for all of them
|
|
674
|
+
for (const c of choices) {
|
|
675
|
+
for (const s of c.authoritative) {
|
|
676
|
+
if (choices.every(t => t.authoritative.includes(s))) {
|
|
677
|
+
log.push(`Found authoritative server ${s}`);
|
|
678
|
+
return s;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// 2. a server that's authoritative for one of them, and a candidate for the others. A
|
|
684
|
+
// system that no server is known for doesn't constrain the choice
|
|
685
|
+
for (const c of choices) {
|
|
686
|
+
for (const s of c.authoritative) {
|
|
687
|
+
if (choices.every(t => unresolved(t) || t.authoritative.includes(s) || t.candidates.includes(s))) {
|
|
688
|
+
log.push(`Found partially authoritative server ${s}`);
|
|
689
|
+
return s;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// 3. code systems with special handling on tx.fhir.org, that might not be resolved above.
|
|
695
|
+
// We don't want them going to secondary servers by accident
|
|
696
|
+
const bases = systems.map(s => s.includes('|') ? s.substring(0, s.indexOf('|')) : s);
|
|
697
|
+
if (systems.length > 1 && bases.includes(SNOMED_CT)) {
|
|
698
|
+
log.push(`Several code systems including SNOMED CT: choosing for SNOMED CT alone`);
|
|
699
|
+
return this._chooseServer(fhirVersion, [SNOMED_CT], defaultServer, valueSet, usage, language, log);
|
|
700
|
+
}
|
|
701
|
+
const special = bases.find(b => PRIMARY_SERVER_SYSTEMS.includes(b));
|
|
702
|
+
if (special) {
|
|
703
|
+
log.push(`Use primary server (the default) for ${special}`);
|
|
704
|
+
return defaultServer;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// 4. a server that's a candidate for all of them (again, systems no server is known for
|
|
708
|
+
// don't constrain the choice), and that has a usable copy of the code systems
|
|
709
|
+
for (const c of choices) {
|
|
710
|
+
for (const s of c.candidates) {
|
|
711
|
+
if (choices.every(t => unresolved(t) || t.candidates.includes(s)) &&
|
|
712
|
+
this._supportsAll(s, choices, unresolved, log)) {
|
|
713
|
+
log.push(`Found candidate server ${s}`);
|
|
714
|
+
return s;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// 5. no agreement - take the one that is most authoritative
|
|
720
|
+
const counts = new Map();
|
|
721
|
+
for (const c of choices) {
|
|
722
|
+
for (const s of c.authoritative) {
|
|
723
|
+
counts.set(s, (counts.get(s) || 0) + 1);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
let max = null;
|
|
727
|
+
for (const [s, n] of counts) {
|
|
728
|
+
if (max === null || n > counts.get(max)) max = s;
|
|
729
|
+
}
|
|
730
|
+
if (max !== null) {
|
|
731
|
+
log.push(`Found most authoritative server ${max}`);
|
|
732
|
+
return max;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// 6. nothing. Use the server the value set comes from, if it comes from one, else the default
|
|
736
|
+
const why = systems.length === 1 ? 'Not handled by any servers' : 'Handled by multiple servers';
|
|
737
|
+
if (valueSet) {
|
|
738
|
+
const r = this.resolveValueSet(fhirVersion, valueSet, false, usage).result;
|
|
739
|
+
let source = (r.authoritative || []).concat(r.candidates || []).map(e => e.url)[0];
|
|
740
|
+
if (source === 'https://vsac.nlm.nih.gov') {
|
|
741
|
+
source = defaultServer;
|
|
742
|
+
}
|
|
743
|
+
if (source) {
|
|
744
|
+
log.push(`${why}. Using source @ '${source}'`);
|
|
745
|
+
return source;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
log.push(`${why}. Using the default server`);
|
|
749
|
+
return defaultServer;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Whether a candidate server has a usable copy (complete or fragment) of each code system
|
|
754
|
+
* it's a candidate for. Systems for which tx.fhir.org is a candidate aren't checked
|
|
755
|
+
*/
|
|
756
|
+
_supportsAll(server, choices, unresolved, log) {
|
|
757
|
+
if (isTxFhirOrg(server)) {
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
760
|
+
for (const t of choices) {
|
|
761
|
+
if (unresolved(t) || !t.candidates.includes(server) || t.candidates.some(isTxFhirOrg)) {
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const content = t.content[server];
|
|
765
|
+
if (content && content !== 'complete' && content !== 'fragment') {
|
|
766
|
+
log.push(`Candidate server ${server} dropped for ${t.system}: its copy of the code system is '${content}'`);
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
|
|
595
773
|
/**
|
|
596
774
|
* The ecosystem Discovery API (GET {root} as JSON): one row per server endpoint.
|
|
597
775
|
*
|
|
@@ -774,7 +952,7 @@ class RegistryAPI {
|
|
|
774
952
|
}
|
|
775
953
|
html += '</td>\n';
|
|
776
954
|
|
|
777
|
-
html += `<td>${row.security}
|
|
955
|
+
html += `<td>${escape(row.security)}</td>\n`;
|
|
778
956
|
|
|
779
957
|
html += '</tr>\n';
|
|
780
958
|
}
|
package/registry/crawler.js
CHANGED
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
// Crawler for gathering server information from terminology servers
|
|
3
3
|
|
|
4
4
|
const axios = require('axios');
|
|
5
|
-
const dns = require('dns');
|
|
6
|
-
const http = require('http');
|
|
7
|
-
const https = require('https');
|
|
8
|
-
const ipaddr = require('ipaddr.js');
|
|
9
5
|
const {
|
|
10
6
|
ServerRegistries,
|
|
11
7
|
ServerRegistry,
|
|
@@ -13,24 +9,11 @@ const {
|
|
|
13
9
|
ServerVersionInformation,
|
|
14
10
|
} = require('./model');
|
|
15
11
|
const { UptimeHistory } = require('./uptime');
|
|
12
|
+
const { ServerCredentials } = require('../library/server-credentials');
|
|
13
|
+
const { createGuardedAgents } = require('../library/ssrf-guard');
|
|
16
14
|
const {Extensions} = require("../tx/library/extensions");
|
|
17
15
|
const {debugLog} = require("../tx/operation-context");
|
|
18
16
|
|
|
19
|
-
// True if an IP literal is anything other than a normal public (unicast) address:
|
|
20
|
-
// loopback, private, link-local (incl. 169.254.169.254 cloud metadata), unique-local,
|
|
21
|
-
// CGNAT, multicast, reserved, etc. IPv4-mapped IPv6 addresses are unwrapped first.
|
|
22
|
-
function isNonPublicAddress(ip) {
|
|
23
|
-
try {
|
|
24
|
-
let addr = ipaddr.parse(ip);
|
|
25
|
-
if (addr.kind() === 'ipv6' && addr.isIPv4MappedAddress()) {
|
|
26
|
-
addr = addr.toIPv4Address();
|
|
27
|
-
}
|
|
28
|
-
return addr.range() !== 'unicast';
|
|
29
|
-
} catch (e) {
|
|
30
|
-
return true; // unparseable - treat as unsafe
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
17
|
const MASTER_URL = 'https://fhir.github.io/ig-registry/tx-servers.json';
|
|
35
18
|
|
|
36
19
|
class RegistryCrawler {
|
|
@@ -42,7 +25,10 @@ class RegistryCrawler {
|
|
|
42
25
|
masterUrl: config.masterUrl || MASTER_URL,
|
|
43
26
|
userAgent: config.userAgent || 'HealthIntersections/FhirServer',
|
|
44
27
|
crawlInterval: config.crawlInterval || 5 * 60 * 1000, // 5 minutes default
|
|
45
|
-
apiKeys: config.apiKeys || {}, //
|
|
28
|
+
apiKeys: config.apiKeys || {}, // server code -> API key (sent as Api-Key)
|
|
29
|
+
// server code -> { tokenEndpoint, clientId, clientSecret, scope }: an OAuth2
|
|
30
|
+
// client_credentials grant; the token is sent as Authorization: Bearer
|
|
31
|
+
oauth: config.oauth || {},
|
|
46
32
|
allowPrivateAddresses: config.allowPrivateAddresses || false, // SSRF opt-out for local test servers
|
|
47
33
|
// the most a server's response can be, once unzipped: the servers are other people's
|
|
48
34
|
maxResponseBytes: (config.maxResponseMB || 100) * 1024 * 1024,
|
|
@@ -61,65 +47,30 @@ class RegistryCrawler {
|
|
|
61
47
|
this.totalBytes = 0;
|
|
62
48
|
this.log = console;
|
|
63
49
|
this.abortController = null;
|
|
50
|
+
// API keys and OAuth tokens for the servers that need them (shared with the tx module's
|
|
51
|
+
// ecosystem resolution: library/server-credentials.js)
|
|
52
|
+
this.credentials = new ServerCredentials({
|
|
53
|
+
apiKeys: this.config.apiKeys,
|
|
54
|
+
oauth: this.config.oauth,
|
|
55
|
+
timeout: this.config.timeout,
|
|
56
|
+
userAgent: this.config.userAgent,
|
|
57
|
+
agents: () => this.guardedAgents(),
|
|
58
|
+
signal: () => this.abortController?.signal,
|
|
59
|
+
log: (level, message) => this.addLogEntry(level, message)
|
|
60
|
+
});
|
|
64
61
|
}
|
|
65
62
|
|
|
66
63
|
useLog(logv) {
|
|
67
64
|
this.log = logv;
|
|
68
65
|
}
|
|
69
66
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// Set config.allowPrivateAddresses = true to disable (e.g. for local test servers).
|
|
75
|
-
ssrfLookup() {
|
|
76
|
-
const allowPrivate = !!(this.config && this.config.allowPrivateAddresses);
|
|
77
|
-
return (hostname, options, callback) => {
|
|
78
|
-
if (typeof options === 'function') {
|
|
79
|
-
callback = options;
|
|
80
|
-
options = {};
|
|
81
|
-
}
|
|
82
|
-
const wantAll = !!(options && options.all);
|
|
83
|
-
dns.lookup(hostname, Object.assign({}, options, { all: true }), (err, addresses) => {
|
|
84
|
-
if (err) {
|
|
85
|
-
callback(err);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (!allowPrivate) {
|
|
89
|
-
for (const a of addresses) {
|
|
90
|
-
if (isNonPublicAddress(a.address)) {
|
|
91
|
-
callback(new Error('Blocked request to non-public address ' + a.address + ' (host ' + hostname + ')'));
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (wantAll) {
|
|
97
|
-
callback(null, addresses);
|
|
98
|
-
} else {
|
|
99
|
-
callback(null, addresses[0].address, addresses[0].family);
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// http/https agents that route every connection through the SSRF lookup.
|
|
67
|
+
// http/https agents that only connect to public addresses: the SSRF guard for outbound
|
|
68
|
+
// fetches (library/ssrf-guard.js). The registry is crawled from server-supplied "next" links,
|
|
69
|
+
// so the target host is not trusted. Set config.allowPrivateAddresses = true to disable (e.g.
|
|
70
|
+
// for local test servers).
|
|
106
71
|
guardedAgents() {
|
|
107
72
|
if (!this._guardedAgents) {
|
|
108
|
-
|
|
109
|
-
class GuardedHttpAgent extends http.Agent {
|
|
110
|
-
createConnection(options, cb) {
|
|
111
|
-
return super.createConnection(Object.assign({}, options, { lookup }), cb);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
class GuardedHttpsAgent extends https.Agent {
|
|
115
|
-
createConnection(options, cb) {
|
|
116
|
-
return super.createConnection(Object.assign({}, options, { lookup }), cb);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
this._guardedAgents = {
|
|
120
|
-
httpAgent: new GuardedHttpAgent({ keepAlive: true }),
|
|
121
|
-
httpsAgent: new GuardedHttpsAgent({ keepAlive: true })
|
|
122
|
-
};
|
|
73
|
+
this._guardedAgents = createGuardedAgents({ allowPrivate: !!this.config.allowPrivateAddresses });
|
|
123
74
|
}
|
|
124
75
|
return this._guardedAgents;
|
|
125
76
|
}
|
|
@@ -160,6 +111,8 @@ class RegistryCrawler {
|
|
|
160
111
|
}
|
|
161
112
|
|
|
162
113
|
newData.doco = masterJson.documentation || '';
|
|
114
|
+
newData.registration = masterJson.registration || '';
|
|
115
|
+
newData.ecosystemDoco = masterJson['ecosystem-documentation'] || '';
|
|
163
116
|
|
|
164
117
|
// Process each registry
|
|
165
118
|
const registries = masterJson.registries || [];
|
|
@@ -300,7 +253,7 @@ class RegistryCrawler {
|
|
|
300
253
|
const version = new ServerVersionInformation();
|
|
301
254
|
version.version = versionConfig.version;
|
|
302
255
|
version.address = versionConfig.url;
|
|
303
|
-
version.security = this.getApiKey(server.code) == null ? "open" : "api-key";
|
|
256
|
+
version.security = this.getOAuthConfig(server.code) ? "oauth" : this.getApiKey(server.code) == null ? "open" : "api-key";
|
|
304
257
|
|
|
305
258
|
if (!version.address) {
|
|
306
259
|
this.addLogEntry('error', `No URL for version ${version.version} of ${server.name}`, server.address);
|
|
@@ -336,7 +289,7 @@ class RegistryCrawler {
|
|
|
336
289
|
version.lastTat = `${Date.now() - startTime}ms`;
|
|
337
290
|
this.uptime.record(version.address, true, this.scanTime || startTime);
|
|
338
291
|
|
|
339
|
-
this.addLogEntry('info', ` Server ${version.address}: ${version.lastTat} for ${version.codeSystems.length} CodeSystems and ${version.valueSets.length} ValueSets`);
|
|
292
|
+
this.addLogEntry('info', ` Server ${version.address}: ${version.lastTat} for ${version.codeSystems.length} CodeSystems and ${version.valueSets.length} ValueSets (${version.security})`);
|
|
340
293
|
|
|
341
294
|
} catch (error) {
|
|
342
295
|
debugLog(error);
|
|
@@ -413,7 +366,7 @@ class RegistryCrawler {
|
|
|
413
366
|
async processServerVersionR3(version, server, exclusions) {
|
|
414
367
|
// Get capability statement
|
|
415
368
|
const capabilityUrl = `${version.address}/metadata`;
|
|
416
|
-
const capability = await this.fetchJson(capabilityUrl, server.
|
|
369
|
+
const capability = await this.fetchJson(capabilityUrl, server.code, version.address);
|
|
417
370
|
|
|
418
371
|
version.version = capability.fhirVersion || '3.0.2';
|
|
419
372
|
version.software = capability.software ? capability.software.name : "unknown";
|
|
@@ -423,7 +376,7 @@ class RegistryCrawler {
|
|
|
423
376
|
// Get terminology capabilities (R3 uses Parameters resource)
|
|
424
377
|
try {
|
|
425
378
|
const termCapUrl = `${version.address}/metadata?mode=terminology`;
|
|
426
|
-
const termCap = await this.fetchJson(termCapUrl, server.
|
|
379
|
+
const termCap = await this.fetchJson(termCapUrl, server.code, version.address);
|
|
427
380
|
|
|
428
381
|
if (termCap.parameter) {
|
|
429
382
|
termCap.parameter.forEach(param => {
|
|
@@ -459,7 +412,7 @@ class RegistryCrawler {
|
|
|
459
412
|
async processServerVersionR4or5(version, server, defVersion, exclusions) {
|
|
460
413
|
// Get capability statement
|
|
461
414
|
const capabilityUrl = `${version.address}/metadata`;
|
|
462
|
-
const capability = await this.fetchJson(capabilityUrl, server.code);
|
|
415
|
+
const capability = await this.fetchJson(capabilityUrl, server.code, version.address);
|
|
463
416
|
|
|
464
417
|
version.version = capability.fhirVersion || defVersion;
|
|
465
418
|
version.software = capability.software ? capability.software.name : "unknown";
|
|
@@ -471,7 +424,7 @@ class RegistryCrawler {
|
|
|
471
424
|
// Get terminology capabilities
|
|
472
425
|
try {
|
|
473
426
|
const termCapUrl = `${version.address}/metadata?mode=terminology`;
|
|
474
|
-
const termCap = await this.fetchJson(termCapUrl, server.code);
|
|
427
|
+
const termCap = await this.fetchJson(termCapUrl, server.code, version.address);
|
|
475
428
|
|
|
476
429
|
if (termCap.codeSystem) {
|
|
477
430
|
termCap.codeSystem.forEach(cs => {
|
|
@@ -528,7 +481,7 @@ class RegistryCrawler {
|
|
|
528
481
|
|
|
529
482
|
if (this.abortController?.signal.aborted) break;
|
|
530
483
|
this.log.debug(`Fetching value sets from ${searchUrl}`);
|
|
531
|
-
const bundle = await this.fetchJson(searchUrl, server.code);
|
|
484
|
+
const bundle = await this.fetchJson(searchUrl, server.code, version.address);
|
|
532
485
|
|
|
533
486
|
// Process entries in this page
|
|
534
487
|
if (bundle.entry) {
|
|
@@ -596,36 +549,27 @@ class RegistryCrawler {
|
|
|
596
549
|
|
|
597
550
|
/**
|
|
598
551
|
* Fetch JSON from a URL
|
|
552
|
+
* @param {string} url
|
|
553
|
+
* @param {string} serverCode - the code the server's credentials (apiKeys, oauth) are under
|
|
554
|
+
* @param {string} [credentialBase] - the server's own address. When given, credentials are
|
|
555
|
+
* sent only to that origin: next links come from the server, and may point anywhere
|
|
599
556
|
*/
|
|
600
|
-
async fetchJson(url,
|
|
557
|
+
async fetchJson(url, serverCode, credentialBase) {
|
|
601
558
|
try {
|
|
602
559
|
// Add timestamp to bypass cache
|
|
603
560
|
const fetchUrl = url.includes('?')
|
|
604
561
|
? `${url}&_ts=${Date.now()}`
|
|
605
562
|
: `${url}?_ts=${Date.now()}`;
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if (apiKey) {
|
|
615
|
-
headers['Api-Key'] = apiKey;
|
|
563
|
+
|
|
564
|
+
const sendCredentials = !credentialBase || this.sameOrigin(url, credentialBase);
|
|
565
|
+
const useOAuth = sendCredentials && !!this.getOAuthConfig(serverCode);
|
|
566
|
+
|
|
567
|
+
let response = await this.getJson(fetchUrl, await this.credentialHeaders(serverCode, sendCredentials, false));
|
|
568
|
+
if (response.status === 401 && useOAuth) {
|
|
569
|
+
// the token was revoked or expired early: get a new one, and try once more
|
|
570
|
+
response = await this.getJson(fetchUrl, await this.credentialHeaders(serverCode, sendCredentials, true));
|
|
616
571
|
}
|
|
617
|
-
|
|
618
|
-
const response = await axios.get(fetchUrl, {
|
|
619
|
-
timeout: this.config.timeout,
|
|
620
|
-
headers: headers,
|
|
621
|
-
signal: this.abortController?.signal,
|
|
622
|
-
validateStatus: (status) => status < 500, // Don't throw on 4xx
|
|
623
|
-
// counted as it is unzipped, so a small response that unzips to a lot is stopped too
|
|
624
|
-
maxContentLength: this.config.maxResponseBytes,
|
|
625
|
-
httpAgent: this.guardedAgents().httpAgent,
|
|
626
|
-
httpsAgent: this.guardedAgents().httpsAgent
|
|
627
|
-
});
|
|
628
|
-
|
|
572
|
+
|
|
629
573
|
if (response.status >= 400) {
|
|
630
574
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
631
575
|
}
|
|
@@ -655,16 +599,50 @@ class RegistryCrawler {
|
|
|
655
599
|
}
|
|
656
600
|
}
|
|
657
601
|
|
|
602
|
+
async getJson(fetchUrl, credentials) {
|
|
603
|
+
return axios.get(fetchUrl, {
|
|
604
|
+
timeout: this.config.timeout,
|
|
605
|
+
headers: Object.assign({
|
|
606
|
+
'Accept': 'application/json, application/fhir+json',
|
|
607
|
+
'User-Agent': this.config.userAgent
|
|
608
|
+
}, credentials),
|
|
609
|
+
signal: this.abortController?.signal,
|
|
610
|
+
validateStatus: (status) => status < 500, // Don't throw on 4xx
|
|
611
|
+
// counted as it is unzipped, so a small response that unzips to a lot is stopped too
|
|
612
|
+
maxContentLength: this.config.maxResponseBytes,
|
|
613
|
+
httpAgent: this.guardedAgents().httpAgent,
|
|
614
|
+
httpsAgent: this.guardedAgents().httpsAgent
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
sameOrigin(url, base) {
|
|
619
|
+
try {
|
|
620
|
+
return new URL(url).origin === new URL(base).origin;
|
|
621
|
+
} catch (e) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
658
626
|
/**
|
|
659
|
-
*
|
|
627
|
+
* The headers that carry a server's credentials (see ServerCredentials)
|
|
660
628
|
*/
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
return this.config.apiKeys[name];
|
|
629
|
+
async credentialHeaders(serverCode, sendCredentials, renewToken) {
|
|
630
|
+
if (!sendCredentials) {
|
|
631
|
+
return {};
|
|
665
632
|
}
|
|
666
|
-
|
|
667
|
-
|
|
633
|
+
return this.credentials.headers(serverCode, renewToken);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
getApiKey(name) {
|
|
637
|
+
return this.credentials.getApiKey(name);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
getOAuthConfig(name) {
|
|
641
|
+
return this.credentials.getOAuthConfig(name);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async getAccessToken(serverCode, renew = false) {
|
|
645
|
+
return this.credentials.getAccessToken(serverCode, renew);
|
|
668
646
|
}
|
|
669
647
|
|
|
670
648
|
/**
|
|
@@ -15,12 +15,13 @@ contract:
|
|
|
15
15
|
Every response carries `Link: </tx-reg/openapi.json>; rel="service-desc"` (RFC 8631), and the
|
|
16
16
|
HTML pages the matching `<link>` element.
|
|
17
17
|
|
|
18
|
-
There are two operations,
|
|
18
|
+
There are two operations from the ecosystem IG, and one extension, all `GET`:
|
|
19
19
|
|
|
20
20
|
| Endpoint | |
|
|
21
21
|
|---|---|
|
|
22
22
|
| `/tx-reg/` | **Discovery** - the server endpoints in the ecosystem |
|
|
23
23
|
| `/tx-reg/resolve` | **Resolution** - which endpoints to use for a code system or value set |
|
|
24
|
+
| `/tx-reg/choose-server` | **Server choice** - which one server to use for a request involving several code systems (JSON only; [below](#server-choice-get-tx-regchoose-server)) |
|
|
24
25
|
|
|
25
26
|
Both answer JSON, or an HTML page when the request's `Accept` header contains `text/html`.
|
|
26
27
|
`/tx-reg/resolve` in a browser without parameters is a form for trying it. Parameters are read
|
|
@@ -143,7 +144,72 @@ when empty, so an empty object means nobody hosts it.
|
|
|
143
144
|
* Resolve takes `version` as an alternative to `url|version`.
|
|
144
145
|
* Resolve omits `authoritative` and `candidates` when they are empty.
|
|
145
146
|
|
|
146
|
-
##
|
|
147
|
+
## Server choice: `GET /tx-reg/choose-server`
|
|
148
|
+
|
|
149
|
+
Not in the ecosystem IG. A request often involves several code systems - a value set that
|
|
150
|
+
includes LOINC and a national code system, a CodeableConcept with codings from three - and
|
|
151
|
+
resolution answers for one at a time. The HL7 Java tooling combines the answers into a choice
|
|
152
|
+
of one server (`TerminologyClientManager.chooseServer` in org.hl7.fhir.core); this makes the
|
|
153
|
+
same choice here for clients that aren't Java. It doesn't replace the Java code, which still
|
|
154
|
+
does its own choosing.
|
|
155
|
+
|
|
156
|
+
| Parameter | |
|
|
157
|
+
|---|---|
|
|
158
|
+
| `fhirVersion` | Required, as for resolve |
|
|
159
|
+
| `system` | A code system the request involves, `url` or `url\|version`. Repeat it for each (`system=a&system=b`) - the one parameter that isn't first-value-only |
|
|
160
|
+
| `default` | Required. The client's default terminology server: the fallback, and the server for the code systems tx.fhir.org handles specially |
|
|
161
|
+
| `valueSet` | The value set the request is about, `url` or `url\|version`, if there is one |
|
|
162
|
+
| `language` | As for resolve, but only for language sensitive requests (an expansion where the display language matters, a validation that checks a display, a lookup that returns designations) |
|
|
163
|
+
| `usage` | As for resolve |
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"server": "https://tx.ontoserver.csiro.au/fhir",
|
|
168
|
+
"log": [
|
|
169
|
+
"https://healthterminologies.gov.au/fhir/CodeSystem/x: authoritative [https://tx.ontoserver.csiro.au/fhir], candidates []",
|
|
170
|
+
"http://terminology.hl7.org/CodeSystem/v2-0203: authoritative [http://tx.fhir.org/r4], candidates [https://tx.ontoserver.csiro.au/fhir]",
|
|
171
|
+
"Found partially authoritative server https://tx.ontoserver.csiro.au/fhir"
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`log` is for people: what resolution says about each code system, then the rule that decided.
|
|
177
|
+
|
|
178
|
+
### What the client does first
|
|
179
|
+
|
|
180
|
+
What the Java tooling works out before it chooses, a client must too:
|
|
181
|
+
|
|
182
|
+
* The code systems: from the codings, and from the value set's includes and excludes,
|
|
183
|
+
following imported value sets. The imports are often only in the client's own context (the
|
|
184
|
+
IG being built), so the registry can't follow them. If the client can't follow one, it
|
|
185
|
+
should use its default server without asking.
|
|
186
|
+
* Versions: those fixed by the expansion parameters (`system-version`,
|
|
187
|
+
`force-system-version`) applied to the systems.
|
|
188
|
+
* Language: for an expansion, the `displayLanguage` of the expansion parameters if no other.
|
|
189
|
+
|
|
190
|
+
### How the choice is made
|
|
191
|
+
|
|
192
|
+
In order:
|
|
193
|
+
|
|
194
|
+
1. A server authoritative for all the code systems.
|
|
195
|
+
2. A server authoritative for one, and authoritative or a candidate for each of the others.
|
|
196
|
+
3. Several code systems including SNOMED CT: the choice for SNOMED CT alone. Otherwise, if a
|
|
197
|
+
code system is one tx.fhir.org handles specially (LOINC, UCUM, RxNorm, CVX, NDC, CPT,
|
|
198
|
+
BCP-13/47, ISO 3166/4217 ...): the default.
|
|
199
|
+
4. A server that is a candidate for all of them, and has a usable copy of each - its
|
|
200
|
+
TerminologyCapabilities says `complete` or `fragment`, or gives no content mode. Not
|
|
201
|
+
checked for a code system tx.fhir.org is also a candidate for.
|
|
202
|
+
5. The server authoritative for the most of them (the first, on a tie).
|
|
203
|
+
6. The server the value set comes from (by resolving `valueSet`), else the default.
|
|
204
|
+
|
|
205
|
+
In 2 and 4, a code system that no server hosts doesn't count against a server.
|
|
206
|
+
|
|
207
|
+
Where this differs from the Java tooling: step 4 uses the content mode the registry scanned
|
|
208
|
+
instead of searching the server, and trusts a server that didn't say; the special handling in
|
|
209
|
+
step 3 sends to `default` rather than to whichever of tx.fhir.org / tx-dev the client has used;
|
|
210
|
+
ties go to the first rather than to hash order. Step 3 is tx.fhir.org specific, and applies to
|
|
211
|
+
any ecosystem for now.
|
|
212
|
+
|
|
147
213
|
|
|
148
214
|
These are HTML, not part of the API, and never secured:
|
|
149
215
|
|
|
@@ -12,6 +12,7 @@ The registry is configured under `modules.registry` in the server's configuratio
|
|
|
12
12
|
"timeout": 30000,
|
|
13
13
|
"userAgent": "YourServer/1.0",
|
|
14
14
|
"apiKeys": {},
|
|
15
|
+
"oauth": {},
|
|
15
16
|
"maxResponseMB": 100,
|
|
16
17
|
"uptimeWindowDays": 30,
|
|
17
18
|
"uptimeRetentionDays": 90,
|
|
@@ -25,6 +26,39 @@ The registry is configured under `modules.registry` in the server's configuratio
|
|
|
25
26
|
|---|---|---|
|
|
26
27
|
| `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
28
|
| `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. |
|
|
29
|
+
| `oauth` | `{}` | OAuth2 client credentials for servers the registry needs a token to scan, by server code - see [below](#oauth). An endpoint scanned with a token is reported as `security: oauth` in the API. |
|
|
30
|
+
|
|
31
|
+
Credentials are only sent to the origin (scheme, host and port) of the server's registered
|
|
32
|
+
address. A `next` link that points somewhere else is still followed, but without them.
|
|
33
|
+
|
|
34
|
+
### OAuth
|
|
35
|
+
|
|
36
|
+
For a server that issues tokens by the OAuth2 `client_credentials` grant, the registry asks
|
|
37
|
+
the token endpoint for a token, sending its client id and secret in the form body
|
|
38
|
+
(`client_secret_post`), and sends the token as `Authorization: Bearer`. The token is reused
|
|
39
|
+
until it is about to expire (`expires_in`; 5 minutes if the endpoint doesn't say), and
|
|
40
|
+
replaced early if the server answers 401. A server with an `oauth` entry is not sent its
|
|
41
|
+
`apiKeys` entry.
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
"oauth": {
|
|
45
|
+
"nzhts": {
|
|
46
|
+
"tokenEndpoint": "https://authenticate.nzhts.digital.health.nz/auth/realms/nzhts/protocol/openid-connect/token",
|
|
47
|
+
"clientId": "hl7",
|
|
48
|
+
"clientSecret": "...",
|
|
49
|
+
"scope": ""
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Property | |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `tokenEndpoint` | The token endpoint. Must be `https:`. Redirects are not followed. |
|
|
57
|
+
| `clientId`, `clientSecret` | The client's credentials, from the server's operator. |
|
|
58
|
+
| `scope` | Optional. Sent if present and not empty. |
|
|
59
|
+
|
|
60
|
+
The field names are those of the HL7 Java tooling's `oauthClientCredentials` settings, so the
|
|
61
|
+
same values can be copied between the two.
|
|
28
62
|
|
|
29
63
|
## Scanning
|
|
30
64
|
|
|
@@ -28,7 +28,10 @@ How the module works, for anyone changing it. The user-facing behaviour is in
|
|
|
28
28
|
`RegistryCrawler.crawl()` runs every `crawlInterval` minutes (first 5 seconds after start),
|
|
29
29
|
and never concurrently with itself. A scan builds a fresh `ServerRegistries` from scratch:
|
|
30
30
|
|
|
31
|
-
1. fetch the master registration file (`formatVersion` must be `"1"`);
|
|
31
|
+
1. fetch the master registration file (`formatVersion` must be `"1"`); its optional
|
|
32
|
+
`registration` and `ecosystem-documentation` URLs are the "Register your own server" and
|
|
33
|
+
"Documentation" links on the home page (each is omitted if the file doesn't give it), since
|
|
34
|
+
they depend on the ecosystem;
|
|
32
35
|
2. for each registry it lists, fetch the registry file and build a `ServerRegistry`;
|
|
33
36
|
3. for each server, a `ServerInformation` with its claims (`authoritative`,
|
|
34
37
|
`authoritative-valuesets`, `languages`, `usage`, `exclusions`), and for each of its
|
|
@@ -27,8 +27,9 @@ it, [api.md](api.md) says so.
|
|
|
27
27
|
|
|
28
28
|
**The registration files.** The ecosystem is declared by a *master registration file* that
|
|
29
29
|
lists *registries*, each of which lists *servers*, each of which lists its endpoints (one per
|
|
30
|
-
FHIR version) and its claims. The format is documented
|
|
31
|
-
|
|
30
|
+
FHIR version) and its claims. The format is documented on the IG's ecosystem page; HL7's own
|
|
31
|
+
ecosystem (its locations, and how to register a server in it) is documented in the ig-registry
|
|
32
|
+
repository's [tx-registry-doco.md](https://github.com/FHIR/ig-registry/blob/master/tx-registry-doco.md).
|
|
32
33
|
HL7's master file is https://fhir.github.io/ig-registry/tx-servers.json, and its registries
|
|
33
34
|
are the per-affiliate `*-tx-servers.json` files in the same repository (HL7 International,
|
|
34
35
|
HL7 Australia, HL7 Europe, and so on). A server's registration carries:
|