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,366 @@
1
+ // registry/auth-store.js
2
+ // The registry's users and their API tokens, in SQLite (better-sqlite3: synchronous, like
3
+ // stats-db.js). A token is stored only as its SHA-256 hash: tokens are 256 bits of
4
+ // randomness, so a fast hash is enough to make the stored form useless to a thief, and a
5
+ // lookup by hash is one indexed read per request. Usage is counted per principal - a
6
+ // user's token, the configured API token, or an OIDC client - and flushed in batches.
7
+
8
+ const crypto = require('crypto');
9
+ const Database = require('better-sqlite3');
10
+ const bcrypt = require('bcrypt');
11
+
12
+ const TOKEN_PREFIX = 'txr_';
13
+
14
+ function hashToken(token) {
15
+ return crypto.createHash('sha256').update(token, 'utf8').digest('hex');
16
+ }
17
+
18
+ /**
19
+ * Make a new token: txr_ + 32 random bytes, base64url. The first 12 characters are kept
20
+ * in clear as the token's name on the pages
21
+ */
22
+ function makeToken() {
23
+ const token = TOKEN_PREFIX + crypto.randomBytes(32).toString('base64url');
24
+ return { token, prefix: token.substring(0, 12), hash: hashToken(token) };
25
+ }
26
+
27
+ class AuthStore {
28
+ /**
29
+ * @param {string} dbPath file path, or ':memory:'
30
+ */
31
+ constructor(dbPath) {
32
+ this.db = new Database(dbPath);
33
+ if (dbPath !== ':memory:') {
34
+ this.db.pragma('journal_mode = WAL');
35
+ }
36
+ this.db.exec(`
37
+ CREATE TABLE IF NOT EXISTS users (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ login TEXT UNIQUE NOT NULL,
40
+ name TEXT NOT NULL DEFAULT '',
41
+ password_hash TEXT,
42
+ enabled INTEGER NOT NULL DEFAULT 1,
43
+ notes TEXT NOT NULL DEFAULT '',
44
+ created_at TEXT NOT NULL
45
+ );
46
+ CREATE TABLE IF NOT EXISTS tokens (
47
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
48
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
49
+ prefix TEXT NOT NULL,
50
+ hash TEXT UNIQUE NOT NULL,
51
+ label TEXT NOT NULL DEFAULT '',
52
+ created_at TEXT NOT NULL,
53
+ expires_at TEXT,
54
+ revoked_at TEXT
55
+ );
56
+ CREATE INDEX IF NOT EXISTS tokens_user ON tokens(user_id);
57
+ CREATE TABLE IF NOT EXISTS usage (
58
+ principal TEXT PRIMARY KEY,
59
+ kind TEXT NOT NULL,
60
+ requests INTEGER NOT NULL DEFAULT 0,
61
+ first_seen TEXT NOT NULL,
62
+ last_seen TEXT NOT NULL
63
+ );
64
+ CREATE TABLE IF NOT EXISTS documents (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ name TEXT UNIQUE NOT NULL,
67
+ type TEXT NOT NULL,
68
+ public INTEGER NOT NULL DEFAULT 0,
69
+ content TEXT NOT NULL,
70
+ updated_at TEXT NOT NULL,
71
+ updated_by TEXT NOT NULL
72
+ );
73
+ CREATE TABLE IF NOT EXISTS document_log (
74
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
75
+ doc_id INTEGER NOT NULL,
76
+ action TEXT NOT NULL,
77
+ name TEXT NOT NULL,
78
+ type TEXT NOT NULL,
79
+ public INTEGER NOT NULL,
80
+ content TEXT NOT NULL,
81
+ at TEXT NOT NULL,
82
+ by TEXT NOT NULL
83
+ );
84
+ CREATE INDEX IF NOT EXISTS document_log_doc ON document_log(doc_id);
85
+ `);
86
+ // added after the first release of the table
87
+ if (!this.db.prepare("SELECT 1 FROM pragma_table_info('users') WHERE name = 'admin'").get()) {
88
+ this.db.exec('ALTER TABLE users ADD COLUMN admin INTEGER NOT NULL DEFAULT 0');
89
+ }
90
+ this.db.pragma('foreign_keys = ON');
91
+ this.pending = new Map(); // principal -> { kind, count, last }
92
+ this.stmts = {
93
+ userByLogin: this.db.prepare('SELECT * FROM users WHERE login = ?'),
94
+ userById: this.db.prepare('SELECT * FROM users WHERE id = ?'),
95
+ users: this.db.prepare('SELECT * FROM users ORDER BY login'),
96
+ insertUser: this.db.prepare('INSERT INTO users (login, name, password_hash, enabled, notes, admin, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)'),
97
+ updateUser: this.db.prepare('UPDATE users SET name = ?, enabled = ?, notes = ?, admin = ? WHERE id = ?'),
98
+ setPassword: this.db.prepare('UPDATE users SET password_hash = ? WHERE id = ?'),
99
+ deleteUser: this.db.prepare('DELETE FROM users WHERE id = ?'),
100
+ tokenByHash: this.db.prepare('SELECT t.*, u.login, u.enabled AS user_enabled FROM tokens t JOIN users u ON u.id = t.user_id WHERE t.hash = ?'),
101
+ tokensForUser: this.db.prepare('SELECT * FROM tokens WHERE user_id = ? ORDER BY created_at DESC'),
102
+ tokenById: this.db.prepare('SELECT * FROM tokens WHERE id = ?'),
103
+ insertToken: this.db.prepare('INSERT INTO tokens (user_id, prefix, hash, label, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?)'),
104
+ revokeToken: this.db.prepare('UPDATE tokens SET revoked_at = ? WHERE id = ? AND user_id = ? AND revoked_at IS NULL'),
105
+ usage: this.db.prepare('SELECT * FROM usage WHERE principal = ?'),
106
+ allUsage: this.db.prepare('SELECT * FROM usage ORDER BY last_seen DESC'),
107
+ upsertUsage: this.db.prepare(`INSERT INTO usage (principal, kind, requests, first_seen, last_seen) VALUES (?, ?, ?, ?, ?)
108
+ ON CONFLICT(principal) DO UPDATE SET requests = requests + excluded.requests, last_seen = excluded.last_seen`),
109
+ documents: this.db.prepare('SELECT id, name, type, public, updated_at, updated_by, length(content) AS size FROM documents ORDER BY name'),
110
+ documentByName: this.db.prepare('SELECT * FROM documents WHERE name = ?'),
111
+ documentById: this.db.prepare('SELECT * FROM documents WHERE id = ?'),
112
+ insertDocument: this.db.prepare('INSERT INTO documents (name, type, public, content, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?)'),
113
+ updateDocument: this.db.prepare('UPDATE documents SET name = ?, type = ?, public = ?, content = ?, updated_at = ?, updated_by = ? WHERE id = ?'),
114
+ deleteDocument: this.db.prepare('DELETE FROM documents WHERE id = ?'),
115
+ logDocument: this.db.prepare('INSERT INTO document_log (doc_id, action, name, type, public, content, at, by) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'),
116
+ documentLog: this.db.prepare('SELECT id, action, name, type, public, at, by, length(content) AS size FROM document_log WHERE doc_id = ? ORDER BY id DESC'),
117
+ documentLogEntry: this.db.prepare('SELECT * FROM document_log WHERE id = ? AND doc_id = ?')
118
+ };
119
+ }
120
+
121
+ // ---- users ----
122
+
123
+ users() {
124
+ return this.stmts.users.all().map(u => this._user(u));
125
+ }
126
+
127
+ userById(id) {
128
+ const u = this.stmts.userById.get(id);
129
+ return u ? this._user(u) : null;
130
+ }
131
+
132
+ userByLogin(login) {
133
+ const u = this.stmts.userByLogin.get(login);
134
+ return u ? this._user(u) : null;
135
+ }
136
+
137
+ _user(row) {
138
+ return { ...row, enabled: !!row.enabled, admin: !!row.admin, hasPassword: !!row.password_hash };
139
+ }
140
+
141
+ /**
142
+ * @returns {number} the new user's id
143
+ * @throws if the login is taken
144
+ */
145
+ async createUser({ login, name = '', password = null, notes = '', enabled = true, admin = false }) {
146
+ const hash = password ? await bcrypt.hash(password, 10) : null;
147
+ const r = this.stmts.insertUser.run(login, name, hash, enabled ? 1 : 0, notes, admin ? 1 : 0, new Date().toISOString());
148
+ return Number(r.lastInsertRowid);
149
+ }
150
+
151
+ updateUser(id, { name, enabled, notes, admin }) {
152
+ const u = this.stmts.userById.get(id);
153
+ if (!u) {
154
+ return false;
155
+ }
156
+ this.stmts.updateUser.run(name ?? u.name, (enabled ?? !!u.enabled) ? 1 : 0, notes ?? u.notes, (admin ?? !!u.admin) ? 1 : 0, id);
157
+ return true;
158
+ }
159
+
160
+ async setPassword(id, password) {
161
+ const hash = password ? await bcrypt.hash(password, 10) : null;
162
+ return this.stmts.setPassword.run(hash, id).changes > 0;
163
+ }
164
+
165
+ async checkPassword(user, password) {
166
+ if (!user || !user.password_hash) {
167
+ return false;
168
+ }
169
+ return bcrypt.compare(password, user.password_hash);
170
+ }
171
+
172
+ deleteUser(id) {
173
+ return this.stmts.deleteUser.run(id).changes > 0;
174
+ }
175
+
176
+ // ---- tokens ----
177
+
178
+ /**
179
+ * Issue a token for a user. The clear token is returned once, here, and never stored
180
+ * @param {number} userId
181
+ * @param {object} [options]
182
+ * @param {string} [options.label]
183
+ * @param {number} [options.expiresInDays]
184
+ * @returns {{id: number, token: string, prefix: string}}
185
+ */
186
+ issueToken(userId, { label = '', expiresInDays = null } = {}) {
187
+ const t = makeToken();
188
+ const now = Date.now();
189
+ const expires = expiresInDays && expiresInDays > 0 ? new Date(now + expiresInDays * 24 * 60 * 60 * 1000).toISOString() : null;
190
+ const r = this.stmts.insertToken.run(userId, t.prefix, t.hash, label, new Date(now).toISOString(), expires);
191
+ return { id: Number(r.lastInsertRowid), token: t.token, prefix: t.prefix };
192
+ }
193
+
194
+ tokensForUser(userId) {
195
+ const now = Date.now();
196
+ return this.stmts.tokensForUser.all(userId).map(t => ({
197
+ ...t,
198
+ status: t.revoked_at ? 'revoked' : (t.expires_at && new Date(t.expires_at).getTime() < now ? 'expired' : 'active'),
199
+ usage: this.usageOf(`token:${t.id}`)
200
+ }));
201
+ }
202
+
203
+ revokeToken(userId, tokenId) {
204
+ return this.stmts.revokeToken.run(new Date().toISOString(), tokenId, userId).changes > 0;
205
+ }
206
+
207
+ /**
208
+ * The user behind a presented token, if it is a live token of an enabled user
209
+ * @returns {{userId: number, login: string, tokenId: number}|null}
210
+ */
211
+ authenticateToken(token, now = Date.now()) {
212
+ if (typeof token !== 'string' || !token.startsWith(TOKEN_PREFIX)) {
213
+ return null;
214
+ }
215
+ const t = this.stmts.tokenByHash.get(hashToken(token));
216
+ if (!t || t.revoked_at || !t.user_enabled) {
217
+ return null;
218
+ }
219
+ if (t.expires_at && new Date(t.expires_at).getTime() < now) {
220
+ return null;
221
+ }
222
+ return { userId: t.user_id, login: t.login, tokenId: t.id };
223
+ }
224
+
225
+ // ---- usage ----
226
+
227
+ /**
228
+ * Count a request by a principal. Counts are kept in memory and written by flush()
229
+ * @param {string} principal e.g. token:12, api-token, oidc:{iss}|{sub}
230
+ * @param {string} kind token | api-token | oidc
231
+ */
232
+ countRequest(principal, kind, now = Date.now()) {
233
+ const p = this.pending.get(principal);
234
+ if (p) {
235
+ p.count++;
236
+ p.last = now;
237
+ } else {
238
+ this.pending.set(principal, { kind, count: 1, last: now });
239
+ }
240
+ }
241
+
242
+ flush() {
243
+ if (this.pending.size === 0) {
244
+ return 0;
245
+ }
246
+ const entries = [...this.pending.entries()];
247
+ const write = this.db.transaction(() => {
248
+ for (const [principal, p] of entries) {
249
+ const at = new Date(p.last).toISOString();
250
+ this.stmts.upsertUsage.run(principal, p.kind, p.count, at, at);
251
+ }
252
+ });
253
+ // a transaction: all written, or none, and then the counts are still pending for the next try
254
+ write();
255
+ // what was written is taken off; counts that came in meanwhile stay
256
+ for (const [principal, p] of entries) {
257
+ if (this.pending.get(principal) === p) {
258
+ this.pending.delete(principal);
259
+ }
260
+ }
261
+ return entries.length;
262
+ }
263
+
264
+ usageOf(principal) {
265
+ const row = this.stmts.usage.get(principal);
266
+ const p = this.pending.get(principal);
267
+ if (!row && !p) {
268
+ return null;
269
+ }
270
+ return {
271
+ requests: (row ? row.requests : 0) + (p ? p.count : 0),
272
+ firstSeen: row ? row.first_seen : new Date(p.last).toISOString(),
273
+ lastSeen: p ? new Date(p.last).toISOString() : row.last_seen
274
+ };
275
+ }
276
+
277
+ /** Usage of principals other than users' tokens (the API token, OIDC clients) */
278
+ otherUsage() {
279
+ this.flush();
280
+ return this.stmts.allUsage.all().filter(u => u.kind !== 'token');
281
+ }
282
+
283
+ // ---- documents ----
284
+
285
+ _document(row) {
286
+ return row ? { ...row, public: !!row.public } : null;
287
+ }
288
+
289
+ /** The documents' metadata (no content), by name */
290
+ documents() {
291
+ return this.stmts.documents.all().map(d => this._document(d));
292
+ }
293
+
294
+ documentByName(name) {
295
+ return this._document(this.stmts.documentByName.get(name));
296
+ }
297
+
298
+ documentById(id) {
299
+ return this._document(this.stmts.documentById.get(id));
300
+ }
301
+
302
+ /**
303
+ * Create a document; every change is also written to the log
304
+ * @returns {number} the document's id
305
+ * @throws if the name is taken
306
+ */
307
+ createDocument({ name, type, public: isPublic = false, content }, by, now = new Date()) {
308
+ const at = now.toISOString();
309
+ return this.db.transaction(() => {
310
+ const r = this.stmts.insertDocument.run(name, type, isPublic ? 1 : 0, content, at, by);
311
+ const id = Number(r.lastInsertRowid);
312
+ this.stmts.logDocument.run(id, 'created', name, type, isPublic ? 1 : 0, content, at, by);
313
+ return id;
314
+ })();
315
+ }
316
+
317
+ updateDocument(id, { name, type, public: isPublic, content }, by, now = new Date()) {
318
+ const d = this.stmts.documentById.get(id);
319
+ if (!d) {
320
+ return false;
321
+ }
322
+ const at = now.toISOString();
323
+ const next = {
324
+ name: name ?? d.name, type: type ?? d.type,
325
+ public: (isPublic ?? !!d.public) ? 1 : 0, content: content ?? d.content
326
+ };
327
+ this.db.transaction(() => {
328
+ this.stmts.updateDocument.run(next.name, next.type, next.public, next.content, at, by, id);
329
+ this.stmts.logDocument.run(id, next.name !== d.name ? 'renamed' : 'edited', next.name, next.type, next.public, next.content, at, by);
330
+ })();
331
+ return true;
332
+ }
333
+
334
+ deleteDocument(id, by, now = new Date()) {
335
+ const d = this.stmts.documentById.get(id);
336
+ if (!d) {
337
+ return false;
338
+ }
339
+ this.db.transaction(() => {
340
+ this.stmts.logDocument.run(id, 'deleted', d.name, d.type, d.public, d.content, now.toISOString(), by);
341
+ this.stmts.deleteDocument.run(id);
342
+ })();
343
+ return true;
344
+ }
345
+
346
+ /** A document's log, newest first, without the content */
347
+ documentLog(docId) {
348
+ return this.stmts.documentLog.all(docId).map(e => ({ ...e, public: !!e.public }));
349
+ }
350
+
351
+ /** One log entry, with the content as it was */
352
+ documentLogEntry(docId, entryId) {
353
+ const e = this.stmts.documentLogEntry.get(entryId, docId);
354
+ return e ? { ...e, public: !!e.public } : null;
355
+ }
356
+
357
+ close() {
358
+ try {
359
+ this.flush();
360
+ } finally {
361
+ this.db.close();
362
+ }
363
+ }
364
+ }
365
+
366
+ module.exports = { AuthStore, makeToken, hashToken, TOKEN_PREFIX };