fhirsmith 0.18.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +149 -0
  3. package/CONFIGURATION.md +305 -0
  4. package/LICENSE +29 -0
  5. package/README.md +29 -122
  6. package/cluster.js +1220 -0
  7. package/cluster.md +707 -0
  8. package/config-template.json +77 -8
  9. package/extension-tracker/extension-tracker-template.html +5 -8
  10. package/extension-tracker/extension-tracker.js +0 -2
  11. package/folder/folder.js +7 -6
  12. package/ips/analysis.js +560 -0
  13. package/ips/ips-template.html +5 -8
  14. package/ips/ips.js +132 -59
  15. package/ips/nationality.js +120 -0
  16. package/ips/pipeline.js +74 -21
  17. package/ips/presenter.js +1 -1
  18. package/ips/readme.md +132 -25
  19. package/ips/render.js +466 -77
  20. package/ips/store.js +79 -22
  21. package/ips/structure.js +247 -0
  22. package/ips/terminology.js +1 -1
  23. package/library/body-limit.js +22 -0
  24. package/library/cache-generation.js +15 -0
  25. package/library/cgroup-memory.js +133 -1
  26. package/library/cluster-counts.js +121 -0
  27. package/library/cluster-events.js +153 -0
  28. package/library/cluster-metrics.js +193 -0
  29. package/library/cluster-status.js +468 -0
  30. package/library/config-reader.js +96 -0
  31. package/library/daily-report.js +100 -2
  32. package/library/html-server.js +27 -1
  33. package/library/i18nsupport.js +23 -1
  34. package/library/keep-alive.js +18 -0
  35. package/library/languages.js +17 -3
  36. package/library/logger.js +31 -0
  37. package/library/memory-guard.js +428 -0
  38. package/library/native-memory.js +52 -0
  39. package/library/npm-audit.js +51 -25
  40. package/library/openapi-doc.js +7 -5
  41. package/library/package-manager.js +8 -0
  42. package/library/role.js +197 -0
  43. package/library/server-credentials.js +169 -0
  44. package/library/sqlite-utilities.js +59 -0
  45. package/library/ssrf-guard.js +100 -0
  46. package/library/static-response-stats.js +16 -0
  47. package/library/version-utilities.js +50 -0
  48. package/library/worker-count.js +192 -0
  49. package/nginx.md +30 -11
  50. package/npmprojector/indexer.js +18 -10
  51. package/package.json +11 -8
  52. package/packages/feed-check-page.js +117 -0
  53. package/packages/feed-check.js +279 -0
  54. package/packages/npm-check.js +57 -3
  55. package/packages/npm-signing.js +219 -0
  56. package/packages/openapi.yaml +207 -1
  57. package/packages/package-crawler.js +322 -41
  58. package/packages/packages-template.html +6 -8
  59. package/packages/packages.js +604 -57
  60. package/packages/readme.md +113 -1
  61. package/publisher/publisher-template.html +2 -5
  62. package/publisher/publisher.js +121 -110
  63. package/publisher/readme.md +75 -0
  64. package/registry/crawler.js +84 -108
  65. package/registry/documentation/configuration.md +34 -0
  66. package/registry/registry-template.html +5 -8
  67. package/registry/registry.js +5 -4
  68. package/root-bare-template.html +7 -4
  69. package/root-template.html +5 -8
  70. package/server.js +398 -198
  71. package/shl/shl.js +38 -37
  72. package/static/a11y/a11y-check.js +832 -0
  73. package/static/a11y/axe.min.js +12 -0
  74. package/static/assets/css/bootstrap-fhir.css +15 -15
  75. package/static/assets/images/flags/au.svg +18 -0
  76. package/static/assets/images/flags/unknown.svg +11 -0
  77. package/static/assets/images/flags/us.svg +63 -0
  78. package/static/assets/images/icon_audit.png +0 -0
  79. package/static/assets/images/icon_clinical.png +0 -0
  80. package/static/assets/images/icon_document.png +0 -0
  81. package/static/assets/images/icon_external.png +0 -0
  82. package/static/assets/images/icon_medication.png +0 -0
  83. package/static/assets/images/icon_organization.png +0 -0
  84. package/static/assets/images/icon_other.png +0 -0
  85. package/static/assets/images/icon_person.png +0 -0
  86. package/static/assets/images/icon_section.png +0 -0
  87. package/stats-db.js +45 -0
  88. package/stats.js +197 -10
  89. package/testing/testing-template.html +5 -8
  90. package/token/nginx_passport_setup.md +1 -1
  91. package/token/security_guide.md +1 -1
  92. package/token/token-template.html +2 -5
  93. package/token/token.js +40 -36
  94. package/translations/Messages.properties +19 -0
  95. package/tx/README.md +154 -4
  96. package/tx/closure/closure-store.js +126 -3
  97. package/tx/cm/cm-database.js +69 -57
  98. package/tx/cm/cm-package.js +29 -1
  99. package/tx/cs/cs-api.js +284 -19
  100. package/tx/cs/cs-api.md +41 -4
  101. package/tx/cs/cs-areacode.js +1 -1
  102. package/tx/cs/cs-country.js +17 -5
  103. package/tx/cs/cs-cpt.js +33 -16
  104. package/tx/cs/cs-cs.js +148 -14
  105. package/tx/cs/cs-currency.js +1 -1
  106. package/tx/cs/cs-icd11.js +21 -13
  107. package/tx/cs/cs-icf.js +22 -3
  108. package/tx/cs/cs-lang.js +2 -2
  109. package/tx/cs/cs-loinc.js +185 -66
  110. package/tx/cs/cs-mimetypes.js +12 -2
  111. package/tx/cs/cs-ndc.js +40 -25
  112. package/tx/cs/cs-omop.js +54 -29
  113. package/tx/cs/cs-rxnorm.js +300 -196
  114. package/tx/cs/cs-snomed.js +608 -64
  115. package/tx/cs/cs-ucum.js +14 -4
  116. package/tx/cs/cs-unii.js +18 -14
  117. package/tx/cs/cs-virtual.js +210 -0
  118. package/tx/ecosystem-resolver.js +413 -0
  119. package/tx/html/cluster-dash-metrics.liquid +26 -0
  120. package/tx/html/cluster-home-metrics.liquid +31 -0
  121. package/tx/html/cluster-metrics-script.liquid +251 -0
  122. package/tx/html/codesystem-operations.liquid +3 -3
  123. package/tx/html/ecl-panel.liquid +1 -1
  124. package/tx/html/external-metrics.liquid +17 -0
  125. package/tx/html/operations-form.liquid +17 -17
  126. package/tx/html/search-form.liquid +11 -11
  127. package/tx/html/status-line.liquid +58 -0
  128. package/tx/html/status-panel.liquid +140 -0
  129. package/tx/html/status-tasks.liquid +25 -0
  130. package/tx/html/tx-template.html +2 -5
  131. package/tx/importers/atc-to-fhir.js +17 -16
  132. package/tx/importers/export-icd11-codesystem.js +3 -2
  133. package/tx/importers/import-icd11.js +4 -3
  134. package/tx/importers/import-loinc.module.js +118 -56
  135. package/tx/importers/import-ndc.module.js +56 -44
  136. package/tx/importers/import-rxnorm.module.js +118 -79
  137. package/tx/importers/import-sct.module.js +57 -58
  138. package/tx/importers/import-unii.module.js +37 -36
  139. package/tx/importers/subset-loinc.module.js +49 -48
  140. package/tx/importers/subset-rxnorm.module.js +29 -28
  141. package/tx/importers/terminal.js +18 -0
  142. package/tx/importers/tx-import-base.js +13 -12
  143. package/tx/importers/tx-import-settings.js +5 -4
  144. package/tx/importers/tx-import.js +53 -52
  145. package/tx/library/canonical-resource.js +49 -0
  146. package/tx/library/codesystem.js +94 -5
  147. package/tx/library/designations.js +45 -2
  148. package/tx/library/extensions.js +13 -7
  149. package/tx/library/implicit-valuesets.js +66 -0
  150. package/tx/library/renderer.js +77 -21
  151. package/tx/library/sqlite-utilities.js +2 -0
  152. package/tx/library/ucum-parsers.js +11 -1
  153. package/tx/library/wait-for-cache.js +43 -0
  154. package/tx/library.js +321 -22
  155. package/tx/openapi-operations.js +6 -4
  156. package/tx/operation-context.js +460 -99
  157. package/tx/params.js +25 -0
  158. package/tx/problems.js +3 -5
  159. package/tx/provider.js +278 -42
  160. package/tx/resource-cache-sqlite.js +1050 -0
  161. package/tx/sct/ecl.js +47 -6
  162. package/tx/sct/editions.js +39 -0
  163. package/tx/sct/expressions.js +10 -1
  164. package/tx/sct/load-config.js +107 -0
  165. package/tx/sct/mmap.js +109 -0
  166. package/tx/sct/structures.js +166 -13
  167. package/tx/sct/text-index.js +5 -1
  168. package/tx/syndication/syndication-feed.js +2 -2
  169. package/tx/tx-html.js +44 -8
  170. package/tx/tx.fhir.org.yml +8 -8
  171. package/tx/tx.js +282 -80
  172. package/tx/usage-tracker.js +39 -11
  173. package/tx/vs/vs-api.js +19 -0
  174. package/tx/vs/vs-database.js +386 -62
  175. package/tx/vs/vs-package.js +27 -4
  176. package/tx/vs/vs-vsac.js +648 -100
  177. package/tx/workers/batch-validate.js +37 -10
  178. package/tx/workers/batch.js +6 -1
  179. package/tx/workers/cache-control.js +15 -4
  180. package/tx/workers/closure.js +156 -13
  181. package/tx/workers/compare.js +8 -5
  182. package/tx/workers/expand.js +1173 -289
  183. package/tx/workers/metadata.js +35 -0
  184. package/tx/workers/search.js +53 -1
  185. package/tx/workers/supplement-filters.js +145 -0
  186. package/tx/workers/translate.js +169 -4
  187. package/tx/workers/validate.js +223 -119
  188. package/tx/workers/worker.js +123 -31
  189. package/tx/xversion/xv-valueset.js +7 -4
  190. package/utilities/submit-test-report.js +3 -1
  191. package/utilities/tx-stress-test.js +482 -0
  192. package/vcl/readme.md +37 -0
  193. package/vcl/vcl-parser.js +15 -0
  194. package/xig/readme.md +52 -0
  195. package/xig/xig-template.html +5 -8
  196. package/xig/xig.js +238 -422
  197. package/changelog-template.md +0 -22
  198. package/registry/test-runner.js +0 -237
  199. package/release.sh +0 -80
  200. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  201. package/tsconfig.eslint.json +0 -9
  202. package/tx/html/dash-metrics.liquid +0 -147
  203. package/tx/html/home-metrics.liquid +0 -225
@@ -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 || {}, // Map of server URL or code to API key
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
- // A DNS lookup wrapper that rejects any host resolving to a non-public address.
71
- // Enforced at connection time (so it also covers redirect targets and defeats
72
- // DNS-rebinding), this is the SSRF guard for outbound fetches. The registry is
73
- // crawled from server-supplied "next" links, so the target host is not trusted.
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
- const lookup = this.ssrfLookup();
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
  }
@@ -302,7 +253,7 @@ class RegistryCrawler {
302
253
  const version = new ServerVersionInformation();
303
254
  version.version = versionConfig.version;
304
255
  version.address = versionConfig.url;
305
- 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";
306
257
 
307
258
  if (!version.address) {
308
259
  this.addLogEntry('error', `No URL for version ${version.version} of ${server.name}`, server.address);
@@ -338,7 +289,7 @@ class RegistryCrawler {
338
289
  version.lastTat = `${Date.now() - startTime}ms`;
339
290
  this.uptime.record(version.address, true, this.scanTime || startTime);
340
291
 
341
- 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})`);
342
293
 
343
294
  } catch (error) {
344
295
  debugLog(error);
@@ -415,7 +366,7 @@ class RegistryCrawler {
415
366
  async processServerVersionR3(version, server, exclusions) {
416
367
  // Get capability statement
417
368
  const capabilityUrl = `${version.address}/metadata`;
418
- const capability = await this.fetchJson(capabilityUrl, server.name);
369
+ const capability = await this.fetchJson(capabilityUrl, server.code, version.address);
419
370
 
420
371
  version.version = capability.fhirVersion || '3.0.2';
421
372
  version.software = capability.software ? capability.software.name : "unknown";
@@ -425,7 +376,7 @@ class RegistryCrawler {
425
376
  // Get terminology capabilities (R3 uses Parameters resource)
426
377
  try {
427
378
  const termCapUrl = `${version.address}/metadata?mode=terminology`;
428
- const termCap = await this.fetchJson(termCapUrl, server.name);
379
+ const termCap = await this.fetchJson(termCapUrl, server.code, version.address);
429
380
 
430
381
  if (termCap.parameter) {
431
382
  termCap.parameter.forEach(param => {
@@ -461,7 +412,7 @@ class RegistryCrawler {
461
412
  async processServerVersionR4or5(version, server, defVersion, exclusions) {
462
413
  // Get capability statement
463
414
  const capabilityUrl = `${version.address}/metadata`;
464
- const capability = await this.fetchJson(capabilityUrl, server.code);
415
+ const capability = await this.fetchJson(capabilityUrl, server.code, version.address);
465
416
 
466
417
  version.version = capability.fhirVersion || defVersion;
467
418
  version.software = capability.software ? capability.software.name : "unknown";
@@ -473,7 +424,7 @@ class RegistryCrawler {
473
424
  // Get terminology capabilities
474
425
  try {
475
426
  const termCapUrl = `${version.address}/metadata?mode=terminology`;
476
- const termCap = await this.fetchJson(termCapUrl, server.code);
427
+ const termCap = await this.fetchJson(termCapUrl, server.code, version.address);
477
428
 
478
429
  if (termCap.codeSystem) {
479
430
  termCap.codeSystem.forEach(cs => {
@@ -530,7 +481,7 @@ class RegistryCrawler {
530
481
 
531
482
  if (this.abortController?.signal.aborted) break;
532
483
  this.log.debug(`Fetching value sets from ${searchUrl}`);
533
- const bundle = await this.fetchJson(searchUrl, server.code);
484
+ const bundle = await this.fetchJson(searchUrl, server.code, version.address);
534
485
 
535
486
  // Process entries in this page
536
487
  if (bundle.entry) {
@@ -598,36 +549,27 @@ class RegistryCrawler {
598
549
 
599
550
  /**
600
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
601
556
  */
602
- async fetchJson(url, serverName) {
557
+ async fetchJson(url, serverCode, credentialBase) {
603
558
  try {
604
559
  // Add timestamp to bypass cache
605
560
  const fetchUrl = url.includes('?')
606
561
  ? `${url}&_ts=${Date.now()}`
607
562
  : `${url}?_ts=${Date.now()}`;
608
-
609
- // Get API key if configured
610
- const apiKey = this.getApiKey(serverName);
611
- const headers = {
612
- 'Accept': 'application/json, application/fhir+json',
613
- 'User-Agent': this.config.userAgent
614
- };
615
-
616
- if (apiKey) {
617
- 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));
618
571
  }
619
-
620
- const response = await axios.get(fetchUrl, {
621
- timeout: this.config.timeout,
622
- headers: headers,
623
- signal: this.abortController?.signal,
624
- validateStatus: (status) => status < 500, // Don't throw on 4xx
625
- // counted as it is unzipped, so a small response that unzips to a lot is stopped too
626
- maxContentLength: this.config.maxResponseBytes,
627
- httpAgent: this.guardedAgents().httpAgent,
628
- httpsAgent: this.guardedAgents().httpsAgent
629
- });
630
-
572
+
631
573
  if (response.status >= 400) {
632
574
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
633
575
  }
@@ -657,16 +599,50 @@ class RegistryCrawler {
657
599
  }
658
600
  }
659
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
+
660
626
  /**
661
- * Get API key for a given URL
627
+ * The headers that carry a server's credentials (see ServerCredentials)
662
628
  */
663
- getApiKey(name) {
664
- // Check for exact URL match
665
- if (this.config.apiKeys[name]) {
666
- return this.config.apiKeys[name];
629
+ async credentialHeaders(serverCode, sendCredentials, renewToken) {
630
+ if (!sendCredentials) {
631
+ return {};
667
632
  }
668
-
669
- return null;
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);
670
646
  }
671
647
 
672
648
  /**
@@ -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
 
@@ -55,8 +55,6 @@
55
55
  <div class="container"> <!-- container -->
56
56
  <div style="background-color: #ad1f2f; padding: 6px; color: white;"> <!-- container -->
57
57
 
58
- <a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> &copy; HL7.org &nbsp;|&nbsp;
59
- <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &nbsp;|&nbsp;
60
58
  <a href="/" style="color: gold"> Server Home</a> &nbsp;|&nbsp;
61
59
  <a href="/tx-reg" style="color: gold">Registry Home</a> &nbsp;|&nbsp;
62
60
  <a href="/tx-reg/resolve" style="color: gold">Resolve</a> &nbsp;|&nbsp;
@@ -75,7 +73,7 @@
75
73
  <div class="inner-wrapper">
76
74
  <div class="col-9">
77
75
 
78
- <h2><img border="0" src="../FHIRsmith32.png" style="vertical-align: text-bottom"/> [%title%] </h2>
76
+ <h2><a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" title="FHIRsmith [%ver%]"><img border="0" src="/FHIRsmith32.png" alt="FHIRsmith Logo" style="vertical-align: text-bottom"/></a> [%title%] </h2>
79
77
 
80
78
  [%content%]
81
79
 
@@ -93,8 +91,7 @@
93
91
  <div class="container"> <!-- container -->
94
92
  <div class="inner-wrapper">
95
93
  <p>
96
- <a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> &copy; HL7.org 2011+. &nbsp;|&nbsp;
97
- <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &copy; HealthIntersections.com.au 2023+ &nbsp;|&nbsp;
94
+ <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" alt="FHIRsmith Logo" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &copy; HealthIntersections.com.au 2023+ &nbsp;|&nbsp;
98
95
  Terminology Registry [%crawler-status%] &nbsp;|
99
96
  &nbsp; ([%ms%] ms)
100
97
  [%sponsorMessage%]
@@ -118,10 +115,10 @@
118
115
  ================================================== -->
119
116
  <!-- Placed at the end of the document so the pages load faster -->
120
117
  <!-- <script src="/assets/js/jquery.js"/> -->
121
- <script src="/assets/js/bootstrap.min.js"/>
122
- <script src="/assets/js/respond.min.js"/>
118
+ <script src="/assets/js/bootstrap.min.js"></script>
119
+ <script src="/assets/js/respond.min.js"></script>
123
120
 
124
- <script src="/assets/js/fhir.js"/>
121
+ <script src="/assets/js/fhir.js"></script>
125
122
 
126
123
  <!-- Analytics Below
127
124
  ================================================== -->
@@ -91,6 +91,7 @@ class RegistryModule {
91
91
  timeout: config.timeout || 30000,
92
92
  userAgent: config.userAgent || 'FHIRRegistryServer/1.0',
93
93
  apiKeys: config.apiKeys || {},
94
+ oauth: config.oauth || {},
94
95
  maxResponseMB: config.maxResponseMB,
95
96
  uptimeWindowDays: config.uptimeWindowDays,
96
97
  uptimeRetentionDays: config.uptimeRetentionDays
@@ -1559,7 +1560,7 @@ class RegistryModule {
1559
1560
  // Query information section
1560
1561
  html += '<div class="card mb-4">';
1561
1562
  html += '<div class="card-header">';
1562
- html += '<h2 class="card-title">Query Information</h2>';
1563
+ html += '<h3 class="card-title">Query Information</h3>';
1563
1564
  html += '</div>';
1564
1565
  html += '<div class="card-body">';
1565
1566
  html += `<p><strong>FHIR Version:</strong> ${escape(fhirVersion)}</p>`;
@@ -1577,7 +1578,7 @@ class RegistryModule {
1577
1578
  // Authoritative servers section
1578
1579
  html += '<div class="card mb-4">';
1579
1580
  html += '<div class="card-header">';
1580
- html += '<h2 class="card-title">Authoritative Servers</h2>';
1581
+ html += '<h3 class="card-title">Authoritative Servers</h3>';
1581
1582
  html += '</div>';
1582
1583
  html += '<div class="card-body">';
1583
1584
 
@@ -1616,7 +1617,7 @@ class RegistryModule {
1616
1617
  // Candidate servers section
1617
1618
  html += '<div class="card mb-4">';
1618
1619
  html += '<div class="card-header">';
1619
- html += '<h2 class="card-title">Candidate Servers</h2>';
1620
+ html += '<h3 class="card-title">Candidate Servers</h3>';
1620
1621
  html += '</div>';
1621
1622
  html += '<div class="card-body">';
1622
1623
 
@@ -1867,7 +1868,7 @@ class RegistryModule {
1867
1868
  case 'error': levelStyle = 'color: #d9534f; font-weight: bold;'; break;
1868
1869
  case 'warn': levelStyle = 'color: #f0ad4e;'; break;
1869
1870
  case 'debug': levelStyle = 'color: #5cb85c;'; break;
1870
- default: levelStyle = 'color: #0275d8;'; // info
1871
+ default: levelStyle = 'color: #0056b3;'; // info
1871
1872
  }
1872
1873
 
1873
1874
  // Format: [time] [LEVEL] message
@@ -71,9 +71,12 @@
71
71
 
72
72
  <body style="margin-left: 10px">
73
73
 
74
- <h2>[%title%] </h2>
74
+ <!-- the page's one h1, the size of an h2 as before -->
75
+ <h1 style="font-size:1.5em;margin:0.83em 0">[%title%] </h1>
75
76
 
77
+ <main>
76
78
  [%content%]
79
+ </main>
77
80
 
78
81
 
79
82
  <!-- JS and analytics only. -->
@@ -81,10 +84,10 @@
81
84
  ================================================== -->
82
85
  <!-- Placed at the end of the document so the pages load faster -->
83
86
  <!-- <script src="/assets/js/jquery.js"/> -->
84
- <script src="/assets/js/bootstrap.min.js"/>
85
- <script src="/assets/js/respond.min.js"/>
87
+ <script src="/assets/js/bootstrap.min.js"></script>
88
+ <script src="/assets/js/respond.min.js"></script>
86
89
 
87
- <script src="/assets/js/fhir.js"/>
90
+ <script src="/assets/js/fhir.js"></script>
88
91
 
89
92
  <!-- Analytics Below
90
93
  ================================================== -->
@@ -53,8 +53,6 @@
53
53
  <div id="stripe"> </div>
54
54
  <div class="container"> <!-- container -->
55
55
  <div style="background-color: #ad1f2f; padding: 6px; color: white;"> <!-- container -->
56
- <a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> &copy; HL7.org &nbsp;|&nbsp;
57
- <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &nbsp;|&nbsp;
58
56
  <a href="/" style="color: gold"> Server Home</a>
59
57
  &nbsp;
60
58
  &nbsp;
@@ -70,7 +68,7 @@
70
68
  <div class="inner-wrapper">
71
69
  <div class="col-9">
72
70
 
73
- <h2><img border="0" src="/FHIRsmith32.png" style="vertical-align: text-bottom"/> FHIRsmith: [%title%] </h2>
71
+ <h2><a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" title="FHIRsmith [%ver%]"><img border="0" src="/FHIRsmith32.png" alt="FHIRsmith Logo" style="vertical-align: text-bottom"/></a> FHIRsmith: [%title%] </h2>
74
72
 
75
73
  [%content%]
76
74
 
@@ -89,8 +87,7 @@
89
87
  <div class="container"> <!-- container -->
90
88
  <div class="inner-wrapper">
91
89
  <p>
92
- <a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> &copy; HL7.org 2011+. &nbsp;|&nbsp;
93
- <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &copy; HealthIntersections.com.au 2023+ &nbsp;|
90
+ <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" alt="FHIRsmith Logo" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &copy; HealthIntersections.com.au 2023+ &nbsp;|
94
91
  &nbsp; ([%ms%] ms)
95
92
  [%sponsorMessage%]
96
93
  </p>
@@ -113,10 +110,10 @@
113
110
  ================================================== -->
114
111
  <!-- Placed at the end of the document so the pages load faster -->
115
112
  <!-- <script src="/assets/js/jquery.js"/> -->
116
- <script src="/assets/js/bootstrap.min.js"/>
117
- <script src="/assets/js/respond.min.js"/>
113
+ <script src="/assets/js/bootstrap.min.js"></script>
114
+ <script src="/assets/js/respond.min.js"></script>
118
115
 
119
- <script src="/assets/js/fhir.js"/>
116
+ <script src="/assets/js/fhir.js"></script>
120
117
 
121
118
  <!-- Analytics Below
122
119
  ================================================== -->