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.
- package/CHANGELOG.md +323 -4
- package/README.md +1 -1
- package/config-template.json +13 -0
- package/library/languages.js +191 -34
- package/library/path-safety.js +51 -0
- package/library/request-token.js +56 -0
- package/package.json +2 -2
- package/packages/packages.js +17 -118
- package/publisher/publisher.js +60 -30
- package/publisher/validation.js +15 -3
- package/server.js +12 -3
- package/translations/Messages.properties +1 -0
- package/tx/README.md +21 -0
- package/tx/cs/cs-api.js +30 -1
- package/tx/cs/cs-cs.js +14 -1
- package/tx/cs/cs-icd11.js +844 -0
- package/tx/cs/cs-lang.js +331 -85
- package/tx/cs/cs-loinc.js +45 -4
- package/tx/cs/cs-mimetypes.js +492 -21
- package/tx/cs/cs-snomed.js +38 -4
- package/tx/cs/cs-ucum.js +33 -3
- package/tx/cs/icd11-expressions.js +243 -0
- package/tx/importers/export-icd11-codesystem.js +823 -0
- package/tx/importers/icd11-schema.md +285 -0
- package/tx/importers/import-icd11.js +861 -0
- package/tx/importers/import-loinc.module.js +14 -7
- package/tx/importers/import-sct.module.js +10 -1
- package/tx/library/codesystem.js +21 -27
- package/tx/library/nesting-limit.js +79 -0
- package/tx/library/operation-outcome.js +97 -3
- package/tx/library/valueset.js +16 -20
- package/tx/library.js +23 -1
- package/tx/ocl/cache/cache-utils.cjs +5 -3
- package/tx/ocl/cs-ocl.cjs +73 -17
- package/tx/ocl/jobs/background-queue.cjs +6 -3
- package/tx/ocl/mappers/concept-mapper.cjs +9 -1
- package/tx/ocl/shared/patches.cjs +48 -1
- package/tx/ocl/vs-ocl.cjs +15 -13
- package/tx/provider.js +1 -1
- package/tx/sct/expressions.js +194 -17
- package/tx/tx.fhir.org.yml +2 -1
- package/tx/tx.js +15 -42
- package/tx/workers/batch-validate.js +6 -16
- package/tx/workers/batch.js +16 -28
- package/tx/workers/cache-control.js +6 -12
- package/tx/workers/closure.js +3 -8
- package/tx/workers/compare.js +6 -34
- package/tx/workers/expand.js +36 -45
- package/tx/workers/lookup.js +21 -39
- package/tx/workers/read.js +6 -40
- package/tx/workers/search.js +2 -8
- package/tx/workers/subsumes.js +58 -41
- package/tx/workers/translate.js +11 -17
- package/tx/workers/validate.js +76 -77
- package/tx/workers/worker.js +70 -1
- package/tx/xml/xml-base.js +243 -76
- package/tx/xversion/xv-valueset.js +39 -1
package/publisher/publisher.js
CHANGED
|
@@ -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 || '
|
|
50
|
+
secret: this.config.sessionSecret || crypto.randomBytes(64).toString('hex'),
|
|
40
51
|
resave: false,
|
|
41
52
|
saveUninitialized: false,
|
|
42
|
-
cookie: {
|
|
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>';
|
package/publisher/validation.js
CHANGED
|
@@ -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 =
|
|
545
|
-
|
|
546
|
-
|
|
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 = {
|