fhirsmith 0.16.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -0
- package/README.md +105 -30
- package/config-template.json +130 -4
- package/extension-tracker/extension-tracker.js +111 -3
- package/extension-tracker/readme.md +3 -0
- package/ips/ai-client.js +263 -0
- package/ips/composer.js +805 -0
- package/ips/editor-schema.js +289 -0
- package/ips/editor.js +290 -0
- package/ips/fhirpath-ips.js +397 -0
- package/ips/ips-codeable-concepts.md +1742 -0
- package/ips/ips-template.html +125 -0
- package/ips/ips-valuesets.js +159 -0
- package/ips/ips.js +1736 -0
- package/ips/membership.js +179 -0
- package/ips/pipeline.js +1012 -0
- package/ips/presenter.js +974 -0
- package/ips/profile-editor.js +578 -0
- package/ips/profiles.js +506 -0
- package/ips/readme.md +460 -0
- package/ips/render.js +1482 -0
- package/ips/search-params.js +83 -0
- package/ips/search.js +667 -0
- package/ips/share.js +178 -0
- package/ips/store.js +650 -0
- package/ips/terminology.js +395 -0
- package/ips/tx-client.js +202 -0
- package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
- package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
- package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
- package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
- package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
- package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
- package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
- package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
- package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
- package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
- package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
- package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
- package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-medication.json +50 -0
- package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
- package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
- package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
- package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
- package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
- package/ips/valuesets/ValueSet-ips-route.json +46 -0
- package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
- package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
- package/ips/valuesets/elements.json +44 -0
- package/library/daily-report.js +422 -0
- package/library/fhir-search-values.js +138 -0
- package/library/html-server.js +32 -23
- package/library/html.js +9 -0
- package/library/mailer.js +77 -0
- package/library/narrative-check.js +222 -0
- package/library/npm-audit.js +25 -0
- package/library/package-manager.js +4 -2
- package/library/sqlite-session-store.js +29 -1
- package/package.json +8 -2
- package/packages/package-crawler.js +189 -82
- package/packages/packages.js +36 -63
- package/packages/utilities/dedupe-versions.js +22 -8
- package/publisher/publisher.js +29 -0
- package/registry/auth-store.js +366 -0
- package/registry/auth.js +872 -0
- package/registry/crawler.js +48 -1
- package/registry/documentation/api.md +162 -0
- package/registry/documentation/configuration.md +93 -0
- package/registry/documentation/documents.md +74 -0
- package/registry/documentation/internals.md +151 -0
- package/registry/documentation/readme.md +100 -0
- package/registry/documentation/security.md +199 -0
- package/registry/documents.js +309 -0
- package/registry/openapi.yaml +30 -0
- package/registry/registry-template.html +1 -1
- package/registry/registry.js +247 -54
- package/registry/uptime.js +154 -0
- package/release.sh +80 -0
- package/server.js +105 -44
- package/shl/shl.js +64 -9
- package/stats-db.js +93 -0
- package/stats.js +158 -1
- package/testing/readme.md +1 -1
- package/testing/search.js +2 -108
- package/testing/store.js +5 -0
- package/testing/testing.js +20 -0
- package/token/token-template.html +5 -4
- package/token/token.js +17 -10
- package/translations/Messages.properties +9 -0
- package/tx/README.md +52 -0
- package/tx/cm/cm-database.js +62 -0
- package/tx/cm/cm-package.js +20 -3
- package/tx/cs/cs-api.js +90 -14
- package/tx/cs/cs-cs.js +142 -19
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +418 -0
- package/tx/cs/cs-loinc.js +79 -39
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +918 -55
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- package/tx/library.js +107 -6
- package/tx/ocl/README.md +62 -6
- package/tx/ocl/cm-ocl.cjs +166 -44
- package/tx/ocl/cs-ocl.cjs +163 -13
- package/tx/ocl/resolve/public-access.js +143 -0
- package/tx/ocl/resolve/reference-resolver.js +458 -0
- package/tx/ocl/vs-ocl.cjs +167 -4
- package/tx/openapi-operations.js +4 -0
- package/tx/params.js +8 -1
- package/tx/problems.js +7 -5
- package/tx/provider.js +21 -5
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- package/tx/syndication/syndication-feed.js +218 -0
- package/tx/syndication/syndication-store.js +415 -0
- package/tx/tx-html.js +37 -8
- package/tx/tx.fhir.org.yml +2 -3
- package/tx/tx.js +148 -5
- package/tx/vs/vs-database.js +26 -0
- package/tx/vs/vs-package.js +42 -10
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/expand.js +34 -7
- package/tx/workers/lookup.js +61 -6
- package/tx/workers/metadata.js +6 -1
- package/tx/workers/subsumes.js +1 -1
- package/tx/workers/translate.js +166 -15
- package/tx/workers/validate.js +128 -6
- package/utilities/make-sct-test-extension.js +264 -0
- package/utilities/submit-test-report.js +1 -1
- package/xig/xig.js +120 -12
- package/gr.sh +0 -3
- package/registry/readme.md +0 -124
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// library/mailer.js
|
|
2
|
+
// Outbound email for the server, over SMTP (nodemailer). One of these is made from the
|
|
3
|
+
// top-level "email" block of config.json, and anything that wants to send mail asks it
|
|
4
|
+
// whether it's configured first - a server with no "email" block simply doesn't send.
|
|
5
|
+
//
|
|
6
|
+
// "email": {
|
|
7
|
+
// "host": "smtp.example.org",
|
|
8
|
+
// "port": 587, // default 587; 465 implies secure
|
|
9
|
+
// "secure": false, // TLS from the start (true for 465), else STARTTLS
|
|
10
|
+
// "user": "...", "password": "...", // optional - leave out for an open relay
|
|
11
|
+
// "from": "FHIRsmith <fhirsmith@example.org>"
|
|
12
|
+
// }
|
|
13
|
+
//
|
|
14
|
+
// nodemailer is only loaded when a message is actually sent, so a server that doesn't
|
|
15
|
+
// send mail doesn't need it installed.
|
|
16
|
+
|
|
17
|
+
class Mailer {
|
|
18
|
+
|
|
19
|
+
constructor(config, log) {
|
|
20
|
+
this.config = config || {};
|
|
21
|
+
this.log = log || console;
|
|
22
|
+
this.transport = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether enough has been configured to send: a host to send through and an
|
|
27
|
+
* address to send from.
|
|
28
|
+
*/
|
|
29
|
+
configured() {
|
|
30
|
+
return Boolean(this.config.host && this.config.from);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_transport() {
|
|
34
|
+
if (!this.transport) {
|
|
35
|
+
const nodemailer = require('nodemailer');
|
|
36
|
+
const port = this.config.port || 587;
|
|
37
|
+
const options = {
|
|
38
|
+
host: this.config.host,
|
|
39
|
+
port: port,
|
|
40
|
+
secure: this.config.secure ?? (port === 465)
|
|
41
|
+
};
|
|
42
|
+
if (this.config.user) {
|
|
43
|
+
options.auth = {user: this.config.user, pass: this.config.password};
|
|
44
|
+
}
|
|
45
|
+
this.transport = nodemailer.createTransport(options);
|
|
46
|
+
}
|
|
47
|
+
return this.transport;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Send one message. `to` is an address or a list of them; text is the body.
|
|
52
|
+
* Resolves with nodemailer's result, or throws - callers decide what a
|
|
53
|
+
* failure means to them.
|
|
54
|
+
*/
|
|
55
|
+
async send({to, subject, text, html}) {
|
|
56
|
+
if (!this.configured()) {
|
|
57
|
+
throw new Error('Email is not configured (email.host and email.from are needed)');
|
|
58
|
+
}
|
|
59
|
+
if (!to || (Array.isArray(to) && to.length === 0)) {
|
|
60
|
+
throw new Error('No recipient given');
|
|
61
|
+
}
|
|
62
|
+
const message = {
|
|
63
|
+
from: this.config.from,
|
|
64
|
+
to: Array.isArray(to) ? to.join(', ') : to,
|
|
65
|
+
subject: subject,
|
|
66
|
+
text: text
|
|
67
|
+
};
|
|
68
|
+
if (html) {
|
|
69
|
+
message.html = html;
|
|
70
|
+
}
|
|
71
|
+
const result = await this._transport().sendMail(message);
|
|
72
|
+
this.log.info(`Email sent to ${message.to}: ${subject}`);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = Mailer;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026, Health Intersections Pty Ltd (http://www.healthintersections.com.au)
|
|
3
|
+
//
|
|
4
|
+
// Licensed under BSD-3: https://opensource.org/license/bsd-3-clause
|
|
5
|
+
//
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Checks FHIR narrative (and any other XHTML that will be shown in a page) for active
|
|
9
|
+
* content: anything that can run script, submit or navigate on its own, load another
|
|
10
|
+
* document, or restyle the page around it.
|
|
11
|
+
*
|
|
12
|
+
* FHIR's rule is that narrative "SHALL contain only the basic HTML formatting elements ...,
|
|
13
|
+
* <a> elements (either name or href), images and internally contained style attributes",
|
|
14
|
+
* and "SHALL NOT contain a head, a body element, external stylesheet references, deprecated
|
|
15
|
+
* elements, scripts, forms, base/link/xlink, frames, iframes, objects or event related
|
|
16
|
+
* attributes". This checks the active-content part of that, not the whole rule.
|
|
17
|
+
*
|
|
18
|
+
* The XHTML is parsed the way a browser parses it when it is inserted into a page (parse5
|
|
19
|
+
* implements the HTML parsing algorithm), so entity-encoded, mixed-case or malformed markup
|
|
20
|
+
* is checked as the browser would see it, not as it is written.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const parse5 = require('parse5');
|
|
24
|
+
|
|
25
|
+
// Elements that run script, load or embed another document, submit data, or change how
|
|
26
|
+
// the page around them is read. svg and math are here because FHIR narrative has no
|
|
27
|
+
// inline graphics (images are <img>), and both can carry script.
|
|
28
|
+
const ACTIVE_ELEMENTS = new Set([
|
|
29
|
+
'script', 'style', 'link', 'meta', 'base',
|
|
30
|
+
'iframe', 'frame', 'frameset', 'object', 'embed', 'applet', 'portal', 'template',
|
|
31
|
+
'form', 'input', 'button', 'select', 'textarea', 'isindex', 'keygen',
|
|
32
|
+
'svg', 'math', 'head', 'body', 'html'
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// Attributes that hold a URL
|
|
36
|
+
const URL_ATTRIBUTES = new Set([
|
|
37
|
+
'href', 'src', 'action', 'formaction', 'xlink:href', 'background', 'poster', 'srcset',
|
|
38
|
+
'data', 'codebase', 'cite', 'longdesc', 'usemap', 'dynsrc', 'lowsrc', 'ping', 'manifest'
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
// Attributes that are active whatever their value
|
|
42
|
+
const ACTIVE_ATTRIBUTES = new Set(['srcdoc', 'http-equiv', 'formaction']);
|
|
43
|
+
|
|
44
|
+
// CSS in a style attribute that runs script or binds behaviour (old IE and Gecko)
|
|
45
|
+
const ACTIVE_CSS = /expression\(|javascript:|vbscript:|-moz-binding|behavior:|@import/;
|
|
46
|
+
|
|
47
|
+
// The scheme of a URL, as a browser reads it: leading spaces and control characters
|
|
48
|
+
// are ignored, and so are tabs and newlines anywhere
|
|
49
|
+
function urlScheme(url) {
|
|
50
|
+
// eslint-disable-next-line no-control-regex -- control characters are what browsers strip
|
|
51
|
+
const cleaned = String(url).replace(/^[\x00-\x20]+/, '').replace(/[\t\n\r]/g, '');
|
|
52
|
+
const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(cleaned);
|
|
53
|
+
return m ? { scheme: m[1].toLowerCase(), rest: cleaned.substring(m[0].length) } : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Every URL in an attribute value (srcset has several)
|
|
57
|
+
function urlsIn(name, value) {
|
|
58
|
+
if (name === 'srcset') {
|
|
59
|
+
return String(value).split(',').map(s => s.trim().split(/\s+/)[0]).filter(Boolean);
|
|
60
|
+
}
|
|
61
|
+
return [value];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// CSS comments removed by a single pass: the obvious regex, /\/\*[\s\S]*?\*\//g, rescans to
|
|
65
|
+
// the end of the string for every unterminated '/*', which is quadratic - a style attribute
|
|
66
|
+
// of a few hundred KB of '/* ' held the event loop for seconds
|
|
67
|
+
function stripCssComments(css) {
|
|
68
|
+
let out = '';
|
|
69
|
+
let i = 0;
|
|
70
|
+
for (;;) {
|
|
71
|
+
const start = css.indexOf('/*', i);
|
|
72
|
+
if (start < 0) {
|
|
73
|
+
return out + css.substring(i);
|
|
74
|
+
}
|
|
75
|
+
out += css.substring(i, start);
|
|
76
|
+
const end = css.indexOf('*/', start + 2);
|
|
77
|
+
if (end < 0) {
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
i = end + 2;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// CSS with comments removed and escapes decoded, so \65 xpression( is seen as expression(
|
|
85
|
+
function normaliseCss(css) {
|
|
86
|
+
return stripCssComments(String(css))
|
|
87
|
+
.replace(/\\([0-9a-fA-F]{1,6})\s?/g, (_, hex) => {
|
|
88
|
+
const cp = parseInt(hex, 16);
|
|
89
|
+
return cp > 0 && cp <= 0x10ffff ? String.fromCodePoint(cp) : '';
|
|
90
|
+
})
|
|
91
|
+
.replace(/\\(.)/g, '$1')
|
|
92
|
+
.replace(/\s+/g, '')
|
|
93
|
+
.toLowerCase();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function describe(node) {
|
|
97
|
+
return `<${node.tagName}>`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function checkAttribute(node, attr, problems) {
|
|
101
|
+
const name = (attr.prefix ? `${attr.prefix}:${attr.name}` : attr.name).toLowerCase();
|
|
102
|
+
const value = attr.value || '';
|
|
103
|
+
if (name.startsWith('on')) {
|
|
104
|
+
problems.push(`an event handler attribute (${name}) on ${describe(node)}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (ACTIVE_ATTRIBUTES.has(name)) {
|
|
108
|
+
problems.push(`a ${name} attribute on ${describe(node)}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (name === 'style') {
|
|
112
|
+
if (ACTIVE_CSS.test(normaliseCss(value))) {
|
|
113
|
+
problems.push(`script or behaviour in the style attribute of ${describe(node)}`);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (URL_ATTRIBUTES.has(name)) {
|
|
118
|
+
for (const url of urlsIn(name, value)) {
|
|
119
|
+
const s = urlScheme(url);
|
|
120
|
+
if (!s) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (s.scheme === 'javascript' || s.scheme === 'vbscript' || s.scheme === 'livescript') {
|
|
124
|
+
problems.push(`a ${s.scheme}: URL in the ${name} of ${describe(node)}`);
|
|
125
|
+
} else if (s.scheme === 'data') {
|
|
126
|
+
// an inline image is fine: an image can't run script
|
|
127
|
+
const isImage = node.tagName === 'img' && (name === 'src' || name === 'srcset')
|
|
128
|
+
&& /^image\//i.test(s.rest.trim());
|
|
129
|
+
if (!isImage) {
|
|
130
|
+
problems.push(`a data: URL in the ${name} of ${describe(node)}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function walk(node, problems) {
|
|
138
|
+
if (node.tagName) {
|
|
139
|
+
if (ACTIVE_ELEMENTS.has(node.tagName.toLowerCase())) {
|
|
140
|
+
problems.push(`a ${describe(node)} element`);
|
|
141
|
+
}
|
|
142
|
+
for (const attr of node.attrs || []) {
|
|
143
|
+
checkAttribute(node, attr, problems);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// a <template>'s content is a separate fragment
|
|
147
|
+
for (const child of [...(node.childNodes || []), ...(node.content ? [node.content] : [])]) {
|
|
148
|
+
walk(child, problems);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Checks XHTML for active content.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} xhtml - e.g. a narrative's text.div
|
|
156
|
+
* @returns {string|null} what was found (the first few things, if several), or null if
|
|
157
|
+
* there is no active content
|
|
158
|
+
*/
|
|
159
|
+
function checkNarrative(xhtml) {
|
|
160
|
+
if (xhtml == null || xhtml === '') {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (typeof xhtml !== 'string') {
|
|
164
|
+
return 'the narrative is not a string';
|
|
165
|
+
}
|
|
166
|
+
const problems = [];
|
|
167
|
+
walk(parse5.parseFragment(xhtml), problems);
|
|
168
|
+
if (problems.length === 0) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const shown = [...new Set(problems)];
|
|
172
|
+
const more = shown.length > 3 ? ` (and ${shown.length - 3} more)` : '';
|
|
173
|
+
return `active content in the narrative: ${shown.slice(0, 3).join('; ')}${more}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Checks every narrative in a resource: its own, its contained resources', and those of
|
|
178
|
+
* the resources in a Bundle's entries.
|
|
179
|
+
*
|
|
180
|
+
* @param {object} resource - the resource as JSON
|
|
181
|
+
* @returns {string|null} what was found, with where, or null
|
|
182
|
+
*/
|
|
183
|
+
function checkResourceNarratives(resource) {
|
|
184
|
+
const check = (res, where) => {
|
|
185
|
+
if (!res || typeof res !== 'object') {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const found = checkNarrative(res.text?.div);
|
|
189
|
+
if (found) {
|
|
190
|
+
return `${where}: ${found}`;
|
|
191
|
+
}
|
|
192
|
+
for (const [i, c] of (Array.isArray(res.contained) ? res.contained : []).entries()) {
|
|
193
|
+
const r = check(c, `${where}.contained[${i}]${c?.id ? ` (#${c.id})` : ''}`);
|
|
194
|
+
if (r) {
|
|
195
|
+
return r;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const [i, e] of (Array.isArray(res.entry) ? res.entry : []).entries()) {
|
|
199
|
+
const r = check(e?.resource, `${where}.entry[${i}]`);
|
|
200
|
+
if (r) {
|
|
201
|
+
return r;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
};
|
|
206
|
+
return check(resource, resource?.resourceType ? `${resource.resourceType}${resource.id ? '/' + resource.id : ''}` : 'resource');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* As checkResourceNarratives, but throws.
|
|
211
|
+
*
|
|
212
|
+
* @param {object} resource
|
|
213
|
+
* @param {string} [context] - for the message, e.g. the package it came from
|
|
214
|
+
*/
|
|
215
|
+
function assertNoActiveContent(resource, context) {
|
|
216
|
+
const found = checkResourceNarratives(resource);
|
|
217
|
+
if (found) {
|
|
218
|
+
throw new Error(context ? `${context}: ${found}` : found);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = { checkNarrative, checkResourceNarratives, assertNoActiveContent };
|
package/library/npm-audit.js
CHANGED
|
@@ -371,6 +371,31 @@ class NpmAudit {
|
|
|
371
371
|
return this.findings.some(f => f.severity === 'critical' || f.severity === 'high');
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
/**
|
|
375
|
+
* For the daily report: plain text, and only when there's something to act
|
|
376
|
+
* on - packages with known vulnerabilities (and what to do about them), a
|
|
377
|
+
* check that couldn't be run, or a newer FHIRsmith release. '' otherwise.
|
|
378
|
+
*/
|
|
379
|
+
reportText() {
|
|
380
|
+
if (!this.enabled) {
|
|
381
|
+
return '';
|
|
382
|
+
}
|
|
383
|
+
const sentences = [];
|
|
384
|
+
if (this.findings.length > 0) {
|
|
385
|
+
const list = this.findings.map(f => `${f.name} ${f.installed.join('/')} (${f.severity}: ${f.range})`).join(', ');
|
|
386
|
+
sentences.push(`npm audit found ${this.summary()} in the packages this server uses: ${list}. ${this.advice()}`);
|
|
387
|
+
}
|
|
388
|
+
if (this.lastError) {
|
|
389
|
+
sentences.push(this.checkedAt
|
|
390
|
+
? `The latest npm audit check failed: ${this.lastError}.`
|
|
391
|
+
: `The npm audit check could not be run: ${this.lastError}.`);
|
|
392
|
+
}
|
|
393
|
+
if (this.findings.length === 0 && this.installedVersion && compareVersions(this.installedVersion, this.latestVersion) < 0) {
|
|
394
|
+
sentences.push(`FHIRsmith ${this.latestVersion} is available (this server is running ${this.installedVersion}).`);
|
|
395
|
+
}
|
|
396
|
+
return sentences.join(' ');
|
|
397
|
+
}
|
|
398
|
+
|
|
374
399
|
/**
|
|
375
400
|
* Home page: nothing unless there is something to report
|
|
376
401
|
*/
|
|
@@ -520,8 +520,10 @@ class PackageManager {
|
|
|
520
520
|
const packagePath = path.join(this.cacheFolder, packageName);
|
|
521
521
|
|
|
522
522
|
try {
|
|
523
|
-
|
|
524
|
-
|
|
523
|
+
// the folder alone is not enough: a download that failed after the folder was made
|
|
524
|
+
// leaves an empty one, which must not count as the package
|
|
525
|
+
const stats = await fs.stat(path.join(packagePath, 'package', 'package.json'));
|
|
526
|
+
if (stats.isFile()) {
|
|
525
527
|
return packageName;
|
|
526
528
|
}
|
|
527
529
|
} catch (error) {
|
|
@@ -130,4 +130,32 @@ class SqliteSessionStore extends session.Store {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
/**
|
|
134
|
+
* The callback API this store uses (run, get), over a better-sqlite3 database - for the
|
|
135
|
+
* modules that keep their data with better-sqlite3 rather than sqlite3.
|
|
136
|
+
* @param {object} db - an open better-sqlite3 Database
|
|
137
|
+
*/
|
|
138
|
+
function betterSqlite(db) {
|
|
139
|
+
return {
|
|
140
|
+
run(sql, params, cb) {
|
|
141
|
+
let info;
|
|
142
|
+
try {
|
|
143
|
+
info = db.prepare(sql).run(...params);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
return cb(e);
|
|
146
|
+
}
|
|
147
|
+
cb.call(info, null);
|
|
148
|
+
},
|
|
149
|
+
get(sql, params, cb) {
|
|
150
|
+
let row;
|
|
151
|
+
try {
|
|
152
|
+
row = db.prepare(sql).get(...params);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
return cb(e);
|
|
155
|
+
}
|
|
156
|
+
cb(null, row);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { SqliteSessionStore, betterSqlite };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fhirsmith",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"txVersion": "1.9.6",
|
|
5
5
|
"description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem",
|
|
6
6
|
"main": "server.js",
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"test:xig": "jest --testPathPatterns=xig",
|
|
20
20
|
"test:packages": "jest --testPathPatterns=packages",
|
|
21
21
|
"test:token": "jest --testPathPatterns=token",
|
|
22
|
+
"test:testing": "jest --testPathPatterns=tests/testing",
|
|
23
|
+
"test:ips": "jest --testPathPatterns=tests/ips",
|
|
22
24
|
"test:integration": "jest --testPathPatterns=integration",
|
|
23
25
|
"test:unit": "jest --testPathPatterns=unit",
|
|
24
26
|
"lint": "eslint . --ext .js --ignore-path .gitignore",
|
|
@@ -46,21 +48,25 @@
|
|
|
46
48
|
"express-rate-limit": "^7.4.1",
|
|
47
49
|
"express-session": "^1.19.0",
|
|
48
50
|
"fast-xml-parser": "^5.3.4",
|
|
49
|
-
"fhir-validator-wrapper": "1.
|
|
51
|
+
"fhir-validator-wrapper": "1.5.0",
|
|
50
52
|
"fhirpath": "^5.2.1",
|
|
51
53
|
"fs-extra": "^11.3.3",
|
|
52
54
|
"ini": "^6.0.0",
|
|
53
55
|
"inquirer": "^8.2.5",
|
|
56
|
+
"jose": "^5.10.0",
|
|
54
57
|
"liquidjs": "^10.25.5",
|
|
55
58
|
"lusca": "^1.7.0",
|
|
56
59
|
"natural": "^8.1.1",
|
|
57
60
|
"node-cron": "^4.6.0",
|
|
61
|
+
"nodemailer": "^10.2.0",
|
|
58
62
|
"pako": "^2.1.0",
|
|
63
|
+
"parse5": "^7.3.0",
|
|
59
64
|
"passport": "^0.7.0",
|
|
60
65
|
"passport-facebook": "^3.0.0",
|
|
61
66
|
"passport-github2": "^0.1.12",
|
|
62
67
|
"passport-google-oauth20": "^2.0.0",
|
|
63
68
|
"properties-file": "^3.6.4",
|
|
69
|
+
"qrcode-generator": "^2.0.4",
|
|
64
70
|
"re2js": "^2.8.0",
|
|
65
71
|
"rimraf": "^5.0.10",
|
|
66
72
|
"sqlite3": "^6.0.1",
|