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.
Files changed (142) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +105 -30
  3. package/config-template.json +130 -4
  4. package/extension-tracker/extension-tracker.js +111 -3
  5. package/extension-tracker/readme.md +3 -0
  6. package/ips/ai-client.js +263 -0
  7. package/ips/composer.js +805 -0
  8. package/ips/editor-schema.js +289 -0
  9. package/ips/editor.js +290 -0
  10. package/ips/fhirpath-ips.js +397 -0
  11. package/ips/ips-codeable-concepts.md +1742 -0
  12. package/ips/ips-template.html +125 -0
  13. package/ips/ips-valuesets.js +159 -0
  14. package/ips/ips.js +1736 -0
  15. package/ips/membership.js +179 -0
  16. package/ips/pipeline.js +1012 -0
  17. package/ips/presenter.js +974 -0
  18. package/ips/profile-editor.js +578 -0
  19. package/ips/profiles.js +506 -0
  20. package/ips/readme.md +460 -0
  21. package/ips/render.js +1482 -0
  22. package/ips/search-params.js +83 -0
  23. package/ips/search.js +667 -0
  24. package/ips/share.js +178 -0
  25. package/ips/store.js +650 -0
  26. package/ips/terminology.js +395 -0
  27. package/ips/tx-client.js +202 -0
  28. package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
  29. package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
  30. package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
  31. package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
  32. package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
  33. package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
  34. package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
  35. package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
  36. package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
  37. package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
  38. package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
  39. package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
  40. package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
  41. package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
  42. package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
  43. package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
  44. package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
  45. package/ips/valuesets/ValueSet-ips-medication.json +50 -0
  46. package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
  47. package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
  48. package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
  49. package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
  50. package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
  51. package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
  52. package/ips/valuesets/ValueSet-ips-route.json +46 -0
  53. package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
  54. package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
  55. package/ips/valuesets/elements.json +44 -0
  56. package/library/daily-report.js +422 -0
  57. package/library/fhir-search-values.js +138 -0
  58. package/library/html-server.js +32 -23
  59. package/library/html.js +9 -0
  60. package/library/mailer.js +77 -0
  61. package/library/narrative-check.js +222 -0
  62. package/library/npm-audit.js +25 -0
  63. package/library/package-manager.js +4 -2
  64. package/library/sqlite-session-store.js +29 -1
  65. package/package.json +8 -2
  66. package/packages/package-crawler.js +189 -82
  67. package/packages/packages.js +36 -63
  68. package/packages/utilities/dedupe-versions.js +22 -8
  69. package/publisher/publisher.js +29 -0
  70. package/registry/auth-store.js +366 -0
  71. package/registry/auth.js +872 -0
  72. package/registry/crawler.js +48 -1
  73. package/registry/documentation/api.md +162 -0
  74. package/registry/documentation/configuration.md +93 -0
  75. package/registry/documentation/documents.md +74 -0
  76. package/registry/documentation/internals.md +151 -0
  77. package/registry/documentation/readme.md +100 -0
  78. package/registry/documentation/security.md +199 -0
  79. package/registry/documents.js +309 -0
  80. package/registry/openapi.yaml +30 -0
  81. package/registry/registry-template.html +1 -1
  82. package/registry/registry.js +247 -54
  83. package/registry/uptime.js +154 -0
  84. package/release.sh +80 -0
  85. package/server.js +105 -44
  86. package/shl/shl.js +64 -9
  87. package/stats-db.js +93 -0
  88. package/stats.js +158 -1
  89. package/testing/readme.md +1 -1
  90. package/testing/search.js +2 -108
  91. package/testing/store.js +5 -0
  92. package/testing/testing.js +20 -0
  93. package/token/token-template.html +5 -4
  94. package/token/token.js +17 -10
  95. package/translations/Messages.properties +9 -0
  96. package/tx/README.md +52 -0
  97. package/tx/cm/cm-database.js +62 -0
  98. package/tx/cm/cm-package.js +20 -3
  99. package/tx/cs/cs-api.js +90 -14
  100. package/tx/cs/cs-cs.js +142 -19
  101. package/tx/cs/cs-icd11.js +366 -112
  102. package/tx/cs/cs-icf.js +418 -0
  103. package/tx/cs/cs-loinc.js +79 -39
  104. package/tx/cs/cs-omop.js +10 -3
  105. package/tx/cs/cs-snomed.js +918 -55
  106. package/tx/cs/cs-ucum.js +6 -0
  107. package/tx/cs/icd11-expressions.js +191 -78
  108. package/tx/importers/export-icd11-codesystem.js +58 -12
  109. package/tx/importers/icd11-schema.md +9 -5
  110. package/tx/library.js +107 -6
  111. package/tx/ocl/README.md +62 -6
  112. package/tx/ocl/cm-ocl.cjs +166 -44
  113. package/tx/ocl/cs-ocl.cjs +163 -13
  114. package/tx/ocl/resolve/public-access.js +143 -0
  115. package/tx/ocl/resolve/reference-resolver.js +458 -0
  116. package/tx/ocl/vs-ocl.cjs +167 -4
  117. package/tx/openapi-operations.js +4 -0
  118. package/tx/params.js +8 -1
  119. package/tx/problems.js +7 -5
  120. package/tx/provider.js +21 -5
  121. package/tx/sct/documentation.md +251 -0
  122. package/tx/sct/ecl.js +758 -46
  123. package/tx/sct/expressions.js +113 -52
  124. package/tx/syndication/syndication-feed.js +218 -0
  125. package/tx/syndication/syndication-store.js +415 -0
  126. package/tx/tx-html.js +37 -8
  127. package/tx/tx.fhir.org.yml +2 -3
  128. package/tx/tx.js +148 -5
  129. package/tx/vs/vs-database.js +26 -0
  130. package/tx/vs/vs-package.js +42 -10
  131. package/tx/vs/vs-vsac.js +25 -6
  132. package/tx/workers/expand.js +34 -7
  133. package/tx/workers/lookup.js +61 -6
  134. package/tx/workers/metadata.js +6 -1
  135. package/tx/workers/subsumes.js +1 -1
  136. package/tx/workers/translate.js +166 -15
  137. package/tx/workers/validate.js +128 -6
  138. package/utilities/make-sct-test-extension.js +264 -0
  139. package/utilities/submit-test-report.js +1 -1
  140. package/xig/xig.js +120 -12
  141. package/gr.sh +0 -3
  142. 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 };
@@ -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
- const stats = await fs.stat(packagePath);
524
- if (stats.isDirectory()) {
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
- module.exports = { SqliteSessionStore };
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.16.1",
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.4.0",
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",