fhirsmith 0.12.0 → 0.13.1

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 (57) hide show
  1. package/CHANGELOG.md +323 -4
  2. package/README.md +1 -1
  3. package/config-template.json +13 -0
  4. package/library/languages.js +191 -34
  5. package/library/path-safety.js +51 -0
  6. package/library/request-token.js +56 -0
  7. package/package.json +2 -2
  8. package/packages/packages.js +17 -118
  9. package/publisher/publisher.js +60 -30
  10. package/publisher/validation.js +15 -3
  11. package/server.js +12 -3
  12. package/translations/Messages.properties +1 -0
  13. package/tx/README.md +21 -0
  14. package/tx/cs/cs-api.js +30 -1
  15. package/tx/cs/cs-cs.js +14 -1
  16. package/tx/cs/cs-icd11.js +844 -0
  17. package/tx/cs/cs-lang.js +331 -85
  18. package/tx/cs/cs-loinc.js +45 -4
  19. package/tx/cs/cs-mimetypes.js +492 -21
  20. package/tx/cs/cs-snomed.js +38 -4
  21. package/tx/cs/cs-ucum.js +33 -3
  22. package/tx/cs/icd11-expressions.js +243 -0
  23. package/tx/importers/export-icd11-codesystem.js +823 -0
  24. package/tx/importers/icd11-schema.md +285 -0
  25. package/tx/importers/import-icd11.js +861 -0
  26. package/tx/importers/import-loinc.module.js +14 -7
  27. package/tx/importers/import-sct.module.js +10 -1
  28. package/tx/library/codesystem.js +21 -27
  29. package/tx/library/nesting-limit.js +79 -0
  30. package/tx/library/operation-outcome.js +97 -3
  31. package/tx/library/valueset.js +16 -20
  32. package/tx/library.js +23 -1
  33. package/tx/ocl/cache/cache-utils.cjs +5 -3
  34. package/tx/ocl/cs-ocl.cjs +73 -17
  35. package/tx/ocl/jobs/background-queue.cjs +6 -3
  36. package/tx/ocl/mappers/concept-mapper.cjs +9 -1
  37. package/tx/ocl/shared/patches.cjs +48 -1
  38. package/tx/ocl/vs-ocl.cjs +15 -13
  39. package/tx/provider.js +1 -1
  40. package/tx/sct/expressions.js +194 -17
  41. package/tx/tx.fhir.org.yml +2 -1
  42. package/tx/tx.js +15 -42
  43. package/tx/workers/batch-validate.js +6 -16
  44. package/tx/workers/batch.js +16 -28
  45. package/tx/workers/cache-control.js +6 -12
  46. package/tx/workers/closure.js +3 -8
  47. package/tx/workers/compare.js +6 -34
  48. package/tx/workers/expand.js +36 -45
  49. package/tx/workers/lookup.js +21 -39
  50. package/tx/workers/read.js +6 -40
  51. package/tx/workers/search.js +2 -8
  52. package/tx/workers/subsumes.js +58 -41
  53. package/tx/workers/translate.js +11 -17
  54. package/tx/workers/validate.js +76 -77
  55. package/tx/workers/worker.js +70 -1
  56. package/tx/xml/xml-base.js +243 -76
  57. package/tx/xversion/xv-valueset.js +39 -1
@@ -5,6 +5,8 @@ const validation = require('./validation');
5
5
  const Database = require('sqlite3').Database;
6
6
  const bcrypt = require('bcrypt');
7
7
  const session = require('express-session');
8
+ const rateLimit = require('express-rate-limit');
9
+ const crypto = require('crypto');
8
10
  const folders = require('../library/folder-setup');
9
11
  const escape = require('escape-html');
10
12
  const {Utilities} = require("../library/utilities");
@@ -35,11 +37,28 @@ class PublisherModule {
35
37
  await this.initializeDatabase();
36
38
 
37
39
  // Set up session middleware - use in-memory sessions or separate session db
40
+ if (!this.config.sessionSecret) {
41
+ // Never fall back to a constant. This project is open source, so a fixed default
42
+ // secret is a published signing key: anyone could mint themselves an admin session
43
+ // on any deployment that forgot the setting, and nothing about the server would
44
+ // look wrong. A random secret fails safe instead - sessions simply do not survive
45
+ // a restart, which is visible and harmless. token.js does the same.
46
+ this.logger.warn('publisher: no sessionSecret configured - using a random one, so ' +
47
+ 'logins will not survive a restart. Set modules.publisher.sessionSecret.');
48
+ }
38
49
  this.router.use(session({
39
- secret: this.config.sessionSecret || 'your-secret-key-change-this',
50
+ secret: this.config.sessionSecret || crypto.randomBytes(64).toString('hex'),
40
51
  resave: false,
41
52
  saveUninitialized: false,
42
- cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 } // 24 hours
53
+ cookie: {
54
+ // Secure by default: the cookie is then only ever sent over HTTPS. Express is
55
+ // configured to trust the proxy, so this works behind an NGINX TLS terminator
56
+ // reporting X-Forwarded-Proto. Set cookieSecure false to run the publisher over
57
+ // plain HTTP - without it the browser will not send the cookie back and login
58
+ // appears to succeed and then silently do nothing.
59
+ secure: this.config.cookieSecure ?? true,
60
+ maxAge: 24 * 60 * 60 * 1000 // 24 hours
61
+ }
43
62
  // Not using SQLiteStore to avoid the database conflict
44
63
  }));
45
64
 
@@ -277,9 +296,20 @@ class PublisherModule {
277
296
  // Main dashboard
278
297
  this.router.get('/', this.renderDashboard.bind(this));
279
298
 
280
- // Authentication
299
+ // Authentication. The login post is rate limited: besides slowing down guessing, it
300
+ // is the one unauthenticated route that runs bcrypt, and bcrypt.compare holds a libuv
301
+ // threadpool thread (4 by default) for ~100ms - so without a limit a handful of
302
+ // concurrent attempts stall every file operation the server wants to do.
303
+ const loginLimiter = rateLimit({
304
+ windowMs: 15 * 60 * 1000,
305
+ max: this.config.loginAttemptsPerWindow ?? 20,
306
+ message: 'Too many login attempts, please try again later.',
307
+ standardHeaders: true,
308
+ legacyHeaders: false
309
+ });
310
+
281
311
  this.router.get('/login', this.renderLogin.bind(this));
282
- this.router.post('/login', this.handleLogin.bind(this));
312
+ this.router.post('/login', loginLimiter, this.handleLogin.bind(this));
283
313
  this.router.post('/logout', this.handleLogout.bind(this));
284
314
 
285
315
  // Tasks
@@ -1320,7 +1350,7 @@ class PublisherModule {
1320
1350
  content += '<div class="col-12">';
1321
1351
 
1322
1352
  if (req.session.userId) {
1323
- content += '<p>Welcome, ' + req.session.userName + '!</p>';
1353
+ content += '<p>Welcome, ' + escape(req.session.userName) + '!</p>';
1324
1354
  content += '<div class="mb-3">';
1325
1355
  content += '<a href="/publisher/tasks" class="btn btn-primary me-2">View All Tasks</a>';
1326
1356
  if (req.session.isAdmin) {
@@ -1348,11 +1378,11 @@ class PublisherModule {
1348
1378
  tasks.forEach(task => {
1349
1379
  content += '<tr>';
1350
1380
  content += '<td><strong>#' + task.id + '</strong></td>';
1351
- content += '<td>' + task.npm_package_id + '</td>';
1352
- content += '<td>' + task.version + '</td>';
1353
- content += '<td><span class="badge bg-' + this.getStatusColor(task.status) + '">' + task.status + '</span></td>';
1381
+ content += '<td>' + escape(task.npm_package_id) + '</td>';
1382
+ content += '<td>' + escape(task.version) + '</td>';
1383
+ content += '<td><span class="badge bg-' + this.getStatusColor(task.status) + '">' + escape(task.status) + '</span></td>';
1354
1384
  content += '<td>' + new Date(task.queued_at).toLocaleString() + '</td>';
1355
- content += '<td>' + task.user_name + '</td>';
1385
+ content += '<td>' + escape(task.user_name) + '</td>';
1356
1386
  content += '</tr>';
1357
1387
  });
1358
1388
 
@@ -1502,23 +1532,23 @@ class PublisherModule {
1502
1532
  content += '</div>';
1503
1533
  content += '<div class="col-md-3">';
1504
1534
  content += '<label for="github_org" class="form-label">GitHub Org</label>';
1505
- content += '<input type="text" class="form-control" id="github_org" name="github_org" required maxlength="39" pattern="' + validation.HTML_PATTERNS.github_org + '" title="Letters, digits and single hyphens" placeholder="hl7">';
1535
+ content += '<input type="text" class="form-control" id="github_org" name="github_org" required maxlength="39" pattern="' + escape(validation.HTML_PATTERNS.github_org) + '" title="Letters, digits and single hyphens" placeholder="hl7">';
1506
1536
  content += '</div>';
1507
1537
  content += '<div class="col-md-3">';
1508
1538
  content += '<label for="github_repo" class="form-label">GitHub Repo</label>';
1509
- content += '<input type="text" class="form-control" id="github_repo" name="github_repo" required maxlength="100" pattern="' + validation.HTML_PATTERNS.github_repo + '" title="Letters, digits, dots, hyphens and underscores" placeholder="fhir-us-core">';
1539
+ content += '<input type="text" class="form-control" id="github_repo" name="github_repo" required maxlength="100" pattern="' + escape(validation.HTML_PATTERNS.github_repo) + '" title="Letters, digits, dots, hyphens and underscores" placeholder="fhir-us-core">';
1510
1540
  content += '</div>';
1511
1541
  content += '<div class="col-md-3">';
1512
1542
  content += '<label for="git_branch" class="form-label">Branch</label>';
1513
- content += '<input type="text" class="form-control" id="git_branch" name="git_branch" required maxlength="255" pattern="' + validation.HTML_PATTERNS.git_branch + '" title="A git branch name - slashes are fine, but not spaces, backslashes or any of ~ ^ : ? * [" placeholder="main">';
1543
+ content += '<input type="text" class="form-control" id="git_branch" name="git_branch" required maxlength="255" pattern="' + escape(validation.HTML_PATTERNS.git_branch) + '" title="A git branch name - slashes are fine, but not spaces, backslashes, markup characters, or any of ~ ^ : ? * [" placeholder="main">';
1514
1544
  content += '</div>';
1515
1545
  content += '<div class="col-md-4">';
1516
1546
  content += '<label for="npm_package_id" class="form-label">NPM Package ID</label>';
1517
- content += '<input type="text" class="form-control" id="npm_package_id" name="npm_package_id" required maxlength="128" pattern="' + validation.HTML_PATTERNS.npm_package_id + '" title="Letters, digits, dots, hyphens and underscores" placeholder="hl7.fhir.us.core">';
1547
+ content += '<input type="text" class="form-control" id="npm_package_id" name="npm_package_id" required maxlength="128" pattern="' + escape(validation.HTML_PATTERNS.npm_package_id) + '" title="Letters, digits, dots, hyphens and underscores" placeholder="hl7.fhir.us.core">';
1518
1548
  content += '</div>';
1519
1549
  content += '<div class="col-md-4">';
1520
1550
  content += '<label for="version" class="form-label">Version</label>';
1521
- content += '<input type="text" class="form-control" id="version" name="version" required maxlength="64" pattern="' + validation.HTML_PATTERNS.version + '" title="Letters, digits, dots, hyphens and plus signs" placeholder="6.0.0">';
1551
+ content += '<input type="text" class="form-control" id="version" name="version" required maxlength="64" pattern="' + escape(validation.HTML_PATTERNS.version) + '" title="Letters, digits, dots, hyphens and plus signs" placeholder="6.0.0">';
1522
1552
  content += '</div>';
1523
1553
  content += '<div class="col-md-4 d-flex align-items-end">';
1524
1554
  content += '<button type="submit" class="btn btn-primary">Create Task</button>';
@@ -1554,13 +1584,13 @@ class PublisherModule {
1554
1584
 
1555
1585
  content += '<tr>';
1556
1586
  content += '<td><strong>#' + task.id + '</strong></td>';
1557
- content += '<td><code>' + task.npm_package_id + '</code></td>';
1558
- content += '<td>' + task.version + '</td>';
1559
- content += '<td>' + task.website_name + '</td>';
1560
- content += '<td><span class="badge bg-' + this.getStatusColor(task.status) + '">' + task.status + '</span></td>';
1587
+ content += '<td><code>' + escape(task.npm_package_id) + '</code></td>';
1588
+ content += '<td>' + escape(task.version) + '</td>';
1589
+ content += '<td>' + escape(task.website_name) + '</td>';
1590
+ content += '<td><span class="badge bg-' + this.getStatusColor(task.status) + '">' + escape(task.status) + '</span></td>';
1561
1591
  content += '<td>' + (task.publisher_version ? '<code>' + escape(task.publisher_version) + '</code>' : '<span class="text-muted">—</span>') + '</td>';
1562
1592
  content += '<td>' + new Date(task.queued_at).toLocaleString() + '</td>';
1563
- content += '<td>' + task.user_name + '</td>';
1593
+ content += '<td>' + escape(task.user_name) + '</td>';
1564
1594
  content += '<td class="task-actions">';
1565
1595
  content += '<a href="/publisher/tasks/' + task.id + '/history" class="btn btn-sm btn-outline-secondary me-1">History</a>';
1566
1596
 
@@ -1868,20 +1898,20 @@ class PublisherModule {
1868
1898
 
1869
1899
  if (req.headers.accept && req.headers.accept.includes('text/html')) {
1870
1900
  const htmlServer = require('../library/html-server');
1871
- let content = '<h3>Task Output: #' + task.id + ' - ' + task.npm_package_id + '#' + task.version + '</h3>';
1872
- content += '<p><strong>Status:</strong> <span class="badge bg-' + this.getStatusColor(task.status) + '">' + task.status + '</span></p>';
1873
- content += '<p><strong>GitHub:</strong> ' + task.github_org + '/' + task.github_repo + ' (' + task.git_branch + ')</p>';
1901
+ let content = '<h3>Task Output: #' + task.id + ' - ' + escape(task.npm_package_id) + '#' + escape(task.version) + '</h3>';
1902
+ content += '<p><strong>Status:</strong> <span class="badge bg-' + this.getStatusColor(task.status) + '">' + escape(task.status) + '</span></p>';
1903
+ content += '<p><strong>GitHub:</strong> ' + escape(task.github_org) + '/' + escape(task.github_repo) + ' (' + escape(task.git_branch) + ')</p>';
1874
1904
 
1875
1905
  if (task.publisher_version) {
1876
1906
  content += '<p><strong>IG Publisher:</strong> <code>' + escape(task.publisher_version) + '</code></p>';
1877
1907
  }
1878
1908
 
1879
1909
  if (task.local_folder) {
1880
- content += '<p><strong>Local Folder:</strong> <code>' + task.local_folder + '</code></p>';
1910
+ content += '<p><strong>Local Folder:</strong> <code>' + escape(task.local_folder) + '</code></p>';
1881
1911
  }
1882
1912
 
1883
1913
  if (task.failure_reason) {
1884
- content += '<div class="alert alert-danger"><strong>Failure Reason:</strong> ' + task.failure_reason + '</div>';
1914
+ content += '<div class="alert alert-danger"><strong>Failure Reason:</strong> ' + escape(task.failure_reason) + '</div>';
1885
1915
  }
1886
1916
 
1887
1917
  // Task logs section
@@ -1893,7 +1923,7 @@ class PublisherModule {
1893
1923
  logs.forEach(log => {
1894
1924
  const timestamp = new Date(log.timestamp).toLocaleString();
1895
1925
  const levelClass = log.level === 'error' ? 'text-danger' : (log.level === 'warn' ? 'text-warning' : '');
1896
- content += '<div class="' + levelClass + '">[' + timestamp + '] [' + log.level.toUpperCase() + '] ' + log.message + '</div>';
1926
+ content += '<div class="' + levelClass + '">[' + timestamp + '] [' + escape(log.level.toUpperCase()) + '] ' + escape(log.message) + '</div>';
1897
1927
  });
1898
1928
  content += '</div>';
1899
1929
  }
@@ -2009,7 +2039,7 @@ class PublisherModule {
2009
2039
  content += '<div class="card mb-4"><div class="card-body">';
2010
2040
  content += '<div class="row">';
2011
2041
  content += '<div class="col-md-6">';
2012
- content += '<p><strong>Status:</strong> <span class="badge bg-' + this.getStatusColor(task.status) + '">' + task.status + '</span></p>';
2042
+ content += '<p><strong>Status:</strong> <span class="badge bg-' + this.getStatusColor(task.status) + '">' + escape(task.status) + '</span></p>';
2013
2043
  content += '<p><strong>Package:</strong> <code>' + escape(task.npm_package_id) + '</code></p>';
2014
2044
  content += '<p><strong>Version:</strong> ' + escape(task.version) + '</p>';
2015
2045
  content += '<p><strong>Website:</strong> ' + escape(task.website_name) + '</p>';
@@ -2085,7 +2115,7 @@ class PublisherModule {
2085
2115
  if (action.action === 'create_task') actionLabel = 'Created task';
2086
2116
  else if (action.action === 'approve_task') actionLabel = 'Approved task';
2087
2117
  else if (action.action === 'delete_task') actionLabel = 'Deleted task';
2088
- else actionLabel = action.action.replace(/_/g, ' ');
2118
+ else actionLabel = escape(action.action.replace(/_/g, ' '));
2089
2119
 
2090
2120
  events.push({
2091
2121
  timestamp: action.timestamp,
@@ -2306,7 +2336,7 @@ class PublisherModule {
2306
2336
 
2307
2337
  websites.forEach(website => {
2308
2338
  content += '<tr>';
2309
- content += '<td>' + website.name + '</td>';
2339
+ content += '<td>' + escape(website.name) + '</td>';
2310
2340
  content += '<td><code>' + escape(website.local_folder) + '</code></td>';
2311
2341
  content += '<td><code>' + escape(website.git_root || '') + '</code></td>';
2312
2342
  content += '<td><code>' + escape(website.history_templates) + '</code></td>';
@@ -2427,7 +2457,7 @@ class PublisherModule {
2427
2457
 
2428
2458
  content += '<div class="card mb-3">';
2429
2459
  content += '<div class="card-header">';
2430
- content += '<h5>' + user.name + ' (' + user.login + ') ' + (user.is_admin ? '<span class="badge bg-warning">Admin</span>' : '') + '</h5>';
2460
+ content += '<h5>' + escape(user.name) + ' (' + escape(user.login) + ') ' + (user.is_admin ? '<span class="badge bg-warning">Admin</span>' : '') + '</h5>';
2431
2461
  content += '</div>';
2432
2462
  content += '<div class="card-body">';
2433
2463
 
@@ -2443,7 +2473,7 @@ class PublisherModule {
2443
2473
 
2444
2474
  websites.forEach(website => {
2445
2475
  const perm = permissions.find(p => p.website_id === website.id) || {};
2446
- content += '<div>' + website.name + '</div>';
2476
+ content += '<div>' + escape(website.name) + '</div>';
2447
2477
  content += '<div>';
2448
2478
  content += '<input type="checkbox" name="queue_' + website.id + '"' + (perm.can_queue ? ' checked' : '') + '>';
2449
2479
  content += '</div>';
@@ -6,9 +6,15 @@
6
6
  * lines, in URLs, and in file names, so they're checked at the boundary rather
7
7
  * than trusted to be harmless everywhere they're later used.
8
8
  *
9
- * The rules are the ones GitHub and git themselves enforce (see
9
+ * The rules are mostly the ones GitHub and git themselves enforce (see
10
10
  * git-check-ref-format(1) for the branch rules): anything rejected here would
11
- * have failed at clone time anyway, with a much worse error.
11
+ * have failed at clone time anyway, with a much worse error. The exception is
12
+ * the markup characters in a branch name, which git permits and we do not - see
13
+ * validateGitBranch.
14
+ *
15
+ * These checks are NOT the defence against markup in the pages that display
16
+ * these values. Output escaping is, and it has to stay there whatever this file
17
+ * allows; the branch rule below is a second line, not the first.
12
18
  */
13
19
 
14
20
  // GitHub owner names: letters, digits and single hyphens, no hyphen at either
@@ -84,6 +90,12 @@ function validateGitBranch(value) {
84
90
  if (/[~^:?*[\\]/.test(value)) {
85
91
  return 'Branch may not contain any of ~ ^ : ? * [ \\';
86
92
  }
93
+ // git itself allows these, but a branch name is displayed on the task pages, and a value that
94
+ // has to be escaped everywhere it is shown is one escape away from being a stored XSS. Nobody
95
+ // needs a branch called <script>, so refuse it at the boundary as well as escaping on output
96
+ if (/[<>"'&]/.test(value)) {
97
+ return 'Branch may not contain any of < > " \' &';
98
+ }
87
99
  if (value.includes('..')) {
88
100
  return 'Branch may not contain ..';
89
101
  }
@@ -182,7 +194,7 @@ function validateTaskInput(input) {
182
194
  const HTML_PATTERNS = {
183
195
  github_org: '[A-Za-z0-9](-?[A-Za-z0-9])*',
184
196
  github_repo: '[A-Za-z0-9._-]+',
185
- git_branch: '[^\\\\ ~^:?*\\[]+',
197
+ git_branch: '[^\\\\ ~^:?*\\[<>"\'&]+',
186
198
  npm_package_id: '[A-Za-z0-9][A-Za-z0-9._-]*',
187
199
  version: '[A-Za-z0-9][A-Za-z0-9.+-]*'
188
200
  };
package/server.js CHANGED
@@ -15,6 +15,7 @@ const v8 = require('v8');
15
15
  const folders = require('./library/folder-setup'); // <-- ADD: load early
16
16
  const { statSync, readdirSync } = require('fs');
17
17
  const escape = require('escape-html');
18
+ const { resolveWithin } = require('./library/path-safety');
18
19
 
19
20
  // Load configuration BEFORE logger
20
21
  let config;
@@ -540,10 +541,18 @@ app.use((req, res, next) => {
540
541
  });
541
542
  if (config.server?.webBase) {
542
543
  const overrideDir = path.resolve(config.server.webBase);
544
+ // Anything under overrideDir may be served; nothing outside it may - see
545
+ // library/path-safety.js for why path.join is not enough on its own.
543
546
  app.use((req, res, next) => {
544
- const filePath = path.join(overrideDir, req.path);
545
- fs.access(filePath, fs.constants.F_OK, (err) => {
546
- if (!err) {
547
+ const filePath = resolveWithin(overrideDir, req.path);
548
+ if (filePath === null) {
549
+ return next();
550
+ }
551
+ // stat rather than access: a directory passes an existence check and then makes
552
+ // sendFile fail with EISDIR, where falling through to express.static serves its
553
+ // index.html instead.
554
+ fs.stat(filePath, (err, stat) => {
555
+ if (!err && stat.isFile()) {
547
556
  res.sendFile(filePath);
548
557
  } else {
549
558
  next();
@@ -1125,6 +1125,7 @@ NO_CODE_PROVIDED_VERSION = No code provided for CodeSystem ''{0}'' version ''{1}
1125
1125
  UNKNOWN_CODESYSTEM = A definition for CodeSystem ''{0}'' could not be found, so the code cannot be validated
1126
1126
  UNKNOWN_CODESYSTEM_VERSION = A definition for CodeSystem ''{0}'' version ''{1}'' could not be found, so the code cannot be validated. Valid versions: {2}
1127
1127
  UNKNOWN_CODESYSTEM_VERSION_NONE = A definition for CodeSystem ''{0}'' version ''{1}'' could not be found, so the code cannot be validated. No versions of this code system are known
1128
+ UNKNOWN_CODESYSTEM_CODING_NOT_CHECKED = A definition for the code system ''{0}'' could not be found, so this coding was not checked. The CodeableConcept is still valid: another coding is in the value set
1128
1129
  UNKNOWN_CODESYSTEM_EXP = A definition for CodeSystem ''{0}'' could not be found, so the value set cannot be expanded
1129
1130
  UNKNOWN_CODESYSTEM_VERSION_EXP = A definition for CodeSystem ''{0}'' version ''{1}'' could not be found, so the value set cannot be expanded. Valid versions: {2}
1130
1131
  UNKNOWN_CODESYSTEM_VERSION_EXP_NONE = A definition for CodeSystem ''{0}'' version ''{1}'' could not be found, so the value set cannot be expanded. No versions of this code system are known
package/tx/README.md CHANGED
@@ -278,6 +278,27 @@ Loads OMOP (Observational Medical Outcomes Partnership) vocabulary mappings from
278
278
  ```
279
279
  The file is built by importing OMOP (see [documentation](importers/readme.md))
280
280
 
281
+ #### `icd11` - ICD-11
282
+
283
+ Loads ICD-11 from a SQLite database built by importing from the WHO ICD-API.
284
+
285
+ ```yaml
286
+ - icd11:icd11-2026-01.db
287
+ ```
288
+
289
+ One database holds three code systems - the MMS and ICF linearizations and the Foundation -
290
+ so a single source line registers a provider for each one it finds:
291
+
292
+ | system | url |
293
+ |---|---|
294
+ | MMS | `http://id.who.int/icd/release/11/mms` |
295
+ | ICF | `http://id.who.int/icd/release/11/icf` |
296
+ | Foundation | `http://id.who.int/icd/entity` |
297
+
298
+ The file is built by importing ICD-11 (see [documentation](importers/icd11-schema.md)). Note
299
+ that only the WHO native API can enumerate the classification; the FHIR endpoint cannot, which
300
+ is why the importer reads from the native one.
301
+
281
302
  #### `npm` - FHIR NPM Packages
282
303
 
283
304
  Loads CodeSystem, ValueSet, and ConceptMap resources from FHIR NPM packages. Packages are fetched from the FHIR package registry (packages2.fhir.org).
package/tx/cs/cs-api.js CHANGED
@@ -993,9 +993,38 @@ class CodeSystemFactoryProvider {
993
993
  }
994
994
  }
995
995
 
996
+ /**
997
+ * The error a code system provider raises when it cannot work out the subsumption
998
+ * relationship between two codes.
999
+ *
1000
+ * $subsumes has four outcome codes - equivalent, subsumes, subsumed-by, not-subsumed -
1001
+ * and none of them means "unknown", so a server that cannot decide has no honest answer
1002
+ * to give and must report an error instead. Answering `not-subsumed` would assert
1003
+ * something the server has not established, in the direction that wrongly drops a code
1004
+ * out of a cohort. See FHIR-58748.
1005
+ *
1006
+ * This is a real condition in more than one code system: SNOMED CT expressions that no
1007
+ * classifier has seen, and media types whose difference lies in a parameter the server
1008
+ * does not understand.
1009
+ *
1010
+ * 422 rather than 400 because the request is well formed and both codes are valid - the
1011
+ * server simply cannot answer it.
1012
+ *
1013
+ * @param {string} message - what could not be determined, and why
1014
+ * @returns {Error} tagged with statusCode, issueCode and txIssueType
1015
+ */
1016
+ function cannotDetermineSubsumption(message) {
1017
+ const error = new Error(message);
1018
+ error.statusCode = 422;
1019
+ error.issueCode = 'not-supported';
1020
+ error.txIssueType = 'cannot-determine';
1021
+ return error;
1022
+ }
1023
+
996
1024
  module.exports = {
997
1025
  FilterExecutionContext,
998
1026
  CodeSystemProvider,
999
1027
  CodeSystemContentMode,
1000
- CodeSystemFactoryProvider
1028
+ CodeSystemFactoryProvider,
1029
+ cannotDetermineSubsumption
1001
1030
  };
package/tx/cs/cs-cs.js CHANGED
@@ -1004,7 +1004,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1004
1004
  }
1005
1005
  // Supported hierarchy filters
1006
1006
  if ((prop === 'concept' || prop === 'code') &&
1007
- ['is-a', 'descendent-of', 'is-not-a', 'in', '=', 'regex'].includes(op)) {
1007
+ ['is-a', 'descendent-of', 'is-not-a', 'child-of', 'in', '=', 'regex'].includes(op)) {
1008
1008
  return true;
1009
1009
  }
1010
1010
 
@@ -1695,6 +1695,19 @@ class FhirCodeSystemFactory extends CodeSystemFactoryProvider {
1695
1695
  return "cs";
1696
1696
  }
1697
1697
 
1698
+ /**
1699
+ * This factory wraps whatever CodeSystem it is handed at build() time, so it has no
1700
+ * system or version of its own. It is never registered in codeSystemFactories (which
1701
+ * is keyed on system()); these answer for the abstract contract, not for a lookup.
1702
+ */
1703
+ system() {
1704
+ return null;
1705
+ }
1706
+
1707
+ version() {
1708
+ return null;
1709
+ }
1710
+
1698
1711
  }
1699
1712
 
1700
1713
  module.exports = {