@jwilleke/ngdpbase-types 5.25.0 → 5.27.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/README.md +1 -1
- package/lib/packages/ngdpbase-types/index.d.ts +9 -2
- package/lib/packages/ngdpbase-types/testing.d.ts +7 -3
- package/lib/packages/ngdpbase-types/testing.js +7 -3
- package/lib/src/converters/DocxConverter.d.ts +30 -0
- package/lib/src/converters/MarkdownConverter.d.ts +31 -0
- package/lib/src/export/ChromeServicePdfProvider.d.ts +52 -0
- package/lib/src/export/ChromeServicePdfProvider.js +153 -0
- package/lib/src/export/LocalChromePdfProvider.d.ts +25 -0
- package/lib/src/export/LocalChromePdfProvider.js +33 -0
- package/lib/src/export/PdfProvider.d.ts +51 -0
- package/lib/src/export/PdfProvider.js +37 -0
- package/lib/src/export/pdfEngine.d.ts +113 -0
- package/lib/src/export/pdfEngine.js +195 -0
- package/lib/src/http/egressPolicy.js +61 -0
- package/lib/src/http/guardedElasticsearch.d.ts +27 -0
- package/lib/src/http/guardedElasticsearch.js +99 -0
- package/lib/src/ice/converterShim.d.ts +5 -0
- package/lib/src/ice/fileHandling.d.ts +30 -0
- package/lib/src/ice/ledger.d.ts +37 -0
- package/lib/src/ice/runImport.d.ts +51 -0
- package/lib/src/ice/types.d.ts +262 -0
- package/lib/src/mail/MailProvider.d.ts +15 -0
- package/lib/src/mail/NodemailerMailProvider.d.ts +1 -1
- package/lib/src/managers/AddonsManager.d.ts +55 -3
- package/lib/src/managers/AttachmentManager.d.ts +4 -5
- package/lib/src/managers/AuthManager.d.ts +150 -32
- package/lib/src/managers/BackgroundJobManager.d.ts +22 -12
- package/lib/src/managers/BackupManager.d.ts +779 -0
- package/lib/src/managers/BackupManager.js +2042 -0
- package/lib/src/managers/BaseManager.d.ts +156 -6
- package/lib/src/managers/BaseManager.js +64 -2
- package/lib/src/managers/CommentManager.d.ts +6 -6
- package/lib/src/managers/ConsentManager.d.ts +130 -0
- package/lib/src/managers/DatabaseManager.d.ts +6 -0
- package/lib/src/managers/DatabaseManager.js +25 -15
- package/lib/src/managers/EmailManager.d.ts +32 -5
- package/lib/src/managers/ExportManager.d.ts +62 -0
- package/lib/src/managers/ExportManager.js +147 -2
- package/lib/src/managers/FootnoteManager.d.ts +6 -6
- package/lib/src/managers/ImportManager.d.ts +590 -0
- package/lib/src/managers/OrganizationManager.d.ts +8 -6
- package/lib/src/managers/PageManager.d.ts +61 -1
- package/lib/src/managers/PersonManager.d.ts +8 -6
- package/lib/src/managers/RoleManager.d.ts +6 -8
- package/lib/src/providers/BaseAttachmentProvider.d.ts +4 -4
- package/lib/src/providers/BaseBackupProvider.d.ts +77 -0
- package/lib/src/providers/BaseBackupProvider.js +52 -0
- package/lib/src/providers/BasicAttachmentProvider.d.ts +4 -4
- package/lib/src/providers/FileBackupProvider.d.ts +34 -0
- package/lib/src/providers/FileBackupProvider.js +116 -0
- package/lib/src/providers/FileConsentProvider.d.ts +18 -0
- package/lib/src/providers/SqliteDatabaseProvider.d.ts +2 -2
- package/lib/src/providers/SqliteDatabaseProvider.js +12 -11
- package/lib/src/providers/attachmentFilesBackup.d.ts +5 -4
- package/lib/src/providers/databaseMigrations.js +45 -0
- package/lib/src/security/PolicyDecisionPoint.d.ts +14 -0
- package/lib/src/security/PolicyInformationPoint.d.ts +25 -2
- package/lib/src/types/Config.d.ts +2 -0
- package/lib/src/types/Consent.d.ts +90 -0
- package/lib/src/types/express.d.ts +6 -0
- package/lib/src/utils/addonHost.d.ts +45 -2
- package/lib/src/utils/addonHost.js +39 -3
- package/lib/src/utils/auditEventNames.generated.d.ts +20 -0
- package/lib/src/utils/auditEventNames.generated.js +20 -0
- package/lib/src/utils/backupCrypto.js +275 -0
- package/lib/src/utils/backupDocument.d.ts +36 -0
- package/lib/src/utils/backupDocument.js +99 -0
- package/lib/src/utils/browserDescription.d.ts +11 -0
- package/lib/src/utils/directoryBackup.d.ts +48 -48
- package/lib/src/utils/directoryBackup.js +291 -0
- package/lib/src/utils/frontmatterAccess.d.ts +54 -11
- package/lib/src/utils/importRunSummary.d.ts +64 -0
- package/lib/src/utils/instanceEnvBackup.d.ts +46 -0
- package/lib/src/utils/instanceEnvBackup.js +120 -0
- package/lib/src/utils/instanceEnvSecret.js +79 -0
- package/lib/src/utils/ipLocation.d.ts +11 -0
- package/lib/src/utils/ncmNotify.d.ts +30 -0
- package/lib/src/utils/privateStoreImport.d.ts +58 -0
- package/lib/src/utils/schedule.js +171 -0
- package/lib/src/utils/secretFileMode.js +78 -0
- package/lib/src/utils/siteRestore.d.ts +13 -16
- package/lib/src/utils/siteRestore.js +33 -6
- package/lib/src/utils/treeArchive.d.ts +2 -0
- package/lib/src/utils/treeArchive.js +242 -0
- package/lib/src/utils/version.js +408 -0
- package/lib/src/utils/zipArchive.d.ts +71 -0
- package/package.json +6 -1
- package/lib/src/utils/administrators.d.ts +0 -21
|
@@ -63,6 +63,114 @@ export interface ManagerBackupOptions {
|
|
|
63
63
|
* failed: the restore keeps its file beside the restored one instead.
|
|
64
64
|
*/
|
|
65
65
|
safetyBackup?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* One more archive beside the document, named and keyed by BackupManager
|
|
68
|
+
* (#1985), for a manager whose files are not a declared folder. A declared
|
|
69
|
+
* folder or file (`backupFolders()`) needs none of this.
|
|
70
|
+
*/
|
|
71
|
+
archive?: (part: string) => BackupArchiveTarget;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A folder or file part's record in a manager's entry of the backup document
|
|
75
|
+
* (#1876, #1985): the archive beside the document and what it holds. The
|
|
76
|
+
* shape every backup since v5.21 writes; a restore reads it as it is.
|
|
77
|
+
*/
|
|
78
|
+
export interface BackupPartRecord {
|
|
79
|
+
format: 1;
|
|
80
|
+
/** The archive beside the document: `<stem>.<part>.tar`, or `.tar.enc` when encrypted. */
|
|
81
|
+
archive: string;
|
|
82
|
+
files: number;
|
|
83
|
+
bytes: number;
|
|
84
|
+
/** `backupKeyFingerprint()` of the key the archive is encrypted under; absent when it is not encrypted. */
|
|
85
|
+
keyFingerprint?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A folder a manager keeps its data in (#1985): BackupManager archives it
|
|
89
|
+
* beside the backup document, stages it in a site restore, and puts the
|
|
90
|
+
* staged copy in place at the next start before the manager reads it.
|
|
91
|
+
*/
|
|
92
|
+
export interface BackupFolderPart {
|
|
93
|
+
/** The archive's name: `<stem>.<part>.tar` (`.tar.enc` under the backup key). Never change it: old backups are matched by it. */
|
|
94
|
+
part: string;
|
|
95
|
+
/** The folder, resolved; null when this site has none configured. */
|
|
96
|
+
folder: string | null;
|
|
97
|
+
/** Leave out a path relative to the folder, in posix form (job state's slot locks). */
|
|
98
|
+
exclude?: (relative: string) => boolean;
|
|
99
|
+
/**
|
|
100
|
+
* A backup with no record of this part stages nothing for it, and the
|
|
101
|
+
* restore names it as not restored. Without it such a backup refuses the
|
|
102
|
+
* whole site restore (one taken before the part was backed up).
|
|
103
|
+
*/
|
|
104
|
+
optional?: boolean;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* One file a manager keeps its data in (#1985): the credentials store. As a
|
|
108
|
+
* folder, but the archive holds the one file.
|
|
109
|
+
*/
|
|
110
|
+
export interface BackupFilePart {
|
|
111
|
+
part: string;
|
|
112
|
+
/** The file, resolved; null when this site has none configured. */
|
|
113
|
+
file: string | null;
|
|
114
|
+
/** How the backup and the log name it ("credentials store"). */
|
|
115
|
+
what: string;
|
|
116
|
+
/** Only ever backed up encrypted: without the backup key it is left out, with a warning (#1876). */
|
|
117
|
+
requireKey?: boolean;
|
|
118
|
+
/** Staged in its place when the file was absent at backup time, so the restore still puts the site back as it was. */
|
|
119
|
+
whenAbsent: string;
|
|
120
|
+
optional?: boolean;
|
|
121
|
+
}
|
|
122
|
+
/** What a manager declares as its data (#1985). */
|
|
123
|
+
export type BackupFolder = BackupFolderPart | BackupFilePart;
|
|
124
|
+
/**
|
|
125
|
+
* One more archive beside the backup document, named and keyed by
|
|
126
|
+
* BackupManager (#1985): for a manager whose files are not one declared
|
|
127
|
+
* folder (the attachment pool and vaults, the audit log among the
|
|
128
|
+
* application logs). Encrypted under the backup key when it is set.
|
|
129
|
+
*/
|
|
130
|
+
export interface BackupArchiveTarget {
|
|
131
|
+
/** Its file name, `<stem>.<part>.tar` or `.tar.enc`: what the entry records. */
|
|
132
|
+
archive: string;
|
|
133
|
+
/** Where to write it. */
|
|
134
|
+
path: string;
|
|
135
|
+
backupKey?: string;
|
|
136
|
+
keyFingerprint?: string;
|
|
137
|
+
}
|
|
138
|
+
/** One live path and the staged copy that replaces it at the next start (#1876). */
|
|
139
|
+
export interface StagedSwap {
|
|
140
|
+
live: string;
|
|
141
|
+
staged: string;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* A whole-site restore being staged (#1876), handed to a manager's
|
|
145
|
+
* `restore()` by BackupManager, which keeps the record that binds the parts
|
|
146
|
+
* together (#1986). A manager whose restore is staged
|
|
147
|
+
* (`restoreIsStaged()`) stages its files beside its live ones and records
|
|
148
|
+
* its part here; nothing else writes that record.
|
|
149
|
+
*/
|
|
150
|
+
export interface SiteRestoreStaging {
|
|
151
|
+
/** Where the staged copy of a live path goes: beside it, `<live>.staged`. The next start applies nothing named otherwise. */
|
|
152
|
+
stagedPathFor(live: string): string;
|
|
153
|
+
/**
|
|
154
|
+
* Record this manager's part: the pairs the next start swaps in, all
|
|
155
|
+
* together with every other part or not at all. `discard` names further
|
|
156
|
+
* files to remove if the restore is thrown away (DatabaseManager's own
|
|
157
|
+
* manifest).
|
|
158
|
+
*/
|
|
159
|
+
record(swaps: StagedSwap[], options?: {
|
|
160
|
+
discard?: string[];
|
|
161
|
+
}): void;
|
|
162
|
+
}
|
|
163
|
+
/** What a staged restore of a manager's part did at this start (#1985, #1986). */
|
|
164
|
+
export interface StagedRestoreAtStart {
|
|
165
|
+
/** Each live path now restored, with where its previous content was kept. */
|
|
166
|
+
applied: Array<{
|
|
167
|
+
live: string;
|
|
168
|
+
kept: string;
|
|
169
|
+
}>;
|
|
170
|
+
/** Why nothing (or, after a failed rename, not everything) was applied. */
|
|
171
|
+
refused?: string;
|
|
172
|
+
/** When the backup it came from was taken. */
|
|
173
|
+
backupTakenAt?: string;
|
|
66
174
|
}
|
|
67
175
|
/** What BackupManager hands each manager's `restore()` (#1723): who asked, and the directory the backup document was read from. */
|
|
68
176
|
export interface ManagerRestoreOptions {
|
|
@@ -79,12 +187,23 @@ export interface ManagerRestoreOptions {
|
|
|
79
187
|
/** The backup of the live data BackupManager took just before this restore (#1723, D6), by filename. */
|
|
80
188
|
safetyBackup?: string;
|
|
81
189
|
/**
|
|
82
|
-
*
|
|
83
|
-
* manager whose restore is staged (`restoreIsStaged()`) stages
|
|
84
|
-
* beside its live ones and records its part
|
|
85
|
-
* every part or none
|
|
190
|
+
* Present when this restore is staged as part of a whole-site restore
|
|
191
|
+
* (#1876): a manager whose restore is staged (`restoreIsStaged()`) stages
|
|
192
|
+
* its files beside its live ones and records its part through it; the
|
|
193
|
+
* next start applies every part or none. BackupManager keeps the record
|
|
194
|
+
* (#1986).
|
|
86
195
|
*/
|
|
87
|
-
siteRestore?:
|
|
196
|
+
siteRestore?: SiteRestoreStaging;
|
|
197
|
+
/**
|
|
198
|
+
* An archive the backup names, checked by BackupManager (#1985): a bare
|
|
199
|
+
* file name in the backup directory, read under the key its fingerprint
|
|
200
|
+
* names. Throws, naming `manager`, for anything else. For a manager that
|
|
201
|
+
* stages its own files (attachments, the audit log).
|
|
202
|
+
*/
|
|
203
|
+
readArchive?: (manager: string, archive: unknown, keyFingerprint: string | undefined) => {
|
|
204
|
+
path: string;
|
|
205
|
+
backupKey?: string;
|
|
206
|
+
};
|
|
88
207
|
}
|
|
89
208
|
/**
|
|
90
209
|
* Base class for all managers
|
|
@@ -209,6 +328,35 @@ declare abstract class BaseManager {
|
|
|
209
328
|
* }
|
|
210
329
|
*/
|
|
211
330
|
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
331
|
+
/**
|
|
332
|
+
* Where this manager keeps its data as folders or files (#1985): declared,
|
|
333
|
+
* never backed up by hand. BackupManager archives each beside the backup
|
|
334
|
+
* document (`<stem>.<part>.tar`), stages it in a site restore, and puts the
|
|
335
|
+
* staged copy in place at the next start, in `initialize()` above, before
|
|
336
|
+
* the subclass reads it. Resolved from the configuration, so it can be
|
|
337
|
+
* answered before the subclass's own `initialize()` has run. Default none.
|
|
338
|
+
*
|
|
339
|
+
* A manager with data in the document as well keeps its `backup()`; a
|
|
340
|
+
* declared part's record goes beside that data (`data[part]`), or is the
|
|
341
|
+
* entry's `data` when the manager has none and declares one part.
|
|
342
|
+
*/
|
|
343
|
+
backupFolders(): BackupFolder[];
|
|
344
|
+
/** What putting a staged restore of the declared parts in place did at this start, or null (#1985). */
|
|
345
|
+
private stagedRestore;
|
|
346
|
+
/** What this start did with a staged restore of this manager's declared parts (#1985): null when there was none. */
|
|
347
|
+
protected stagedRestoreAtStart(): StagedRestoreAtStart | null;
|
|
348
|
+
/**
|
|
349
|
+
* The name this manager is registered under: a staged part is recorded by
|
|
350
|
+
* it. Its class name when it is not registered (a test).
|
|
351
|
+
*/
|
|
352
|
+
private registeredName;
|
|
353
|
+
/**
|
|
354
|
+
* #1985: a staged site restore of the declared parts goes in before the
|
|
355
|
+
* subclass reads them. BackupManager's own step, reached through its
|
|
356
|
+
* module (BackupManager starts after most managers); loaded only when there
|
|
357
|
+
* is something declared. A part not applied is said on the dashboard.
|
|
358
|
+
*/
|
|
359
|
+
private applyStagedBackupParts;
|
|
212
360
|
/**
|
|
213
361
|
* Check if manager has been initialized
|
|
214
362
|
*
|
|
@@ -375,7 +523,9 @@ declare abstract class BaseManager {
|
|
|
375
523
|
* Whether this manager's `restore()` stages for the next start instead of
|
|
376
524
|
* writing into the running site (#1723, #1876). BackupManager stages every
|
|
377
525
|
* such manager in a site restore together, all or nothing, after a backup
|
|
378
|
-
* of the live data. The default
|
|
526
|
+
* of the live data. The default: true when it declares folders or files
|
|
527
|
+
* (#1985), which BackupManager stages; otherwise false, the restore writes
|
|
528
|
+
* live.
|
|
379
529
|
*/
|
|
380
530
|
restoreIsStaged(): boolean;
|
|
381
531
|
/**
|
|
@@ -67,8 +67,63 @@ class BaseManager {
|
|
|
67
67
|
*/
|
|
68
68
|
async initialize(config = {}) {
|
|
69
69
|
this.config = config;
|
|
70
|
+
// #1985: before anything is read, and before the manager counts as started.
|
|
71
|
+
await this.applyStagedBackupParts();
|
|
70
72
|
this.initialized = true;
|
|
71
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Where this manager keeps its data as folders or files (#1985): declared,
|
|
76
|
+
* never backed up by hand. BackupManager archives each beside the backup
|
|
77
|
+
* document (`<stem>.<part>.tar`), stages it in a site restore, and puts the
|
|
78
|
+
* staged copy in place at the next start, in `initialize()` above, before
|
|
79
|
+
* the subclass reads it. Resolved from the configuration, so it can be
|
|
80
|
+
* answered before the subclass's own `initialize()` has run. Default none.
|
|
81
|
+
*
|
|
82
|
+
* A manager with data in the document as well keeps its `backup()`; a
|
|
83
|
+
* declared part's record goes beside that data (`data[part]`), or is the
|
|
84
|
+
* entry's `data` when the manager has none and declares one part.
|
|
85
|
+
*/
|
|
86
|
+
backupFolders() {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
/** What putting a staged restore of the declared parts in place did at this start, or null (#1985). */
|
|
90
|
+
stagedRestore = null;
|
|
91
|
+
/** What this start did with a staged restore of this manager's declared parts (#1985): null when there was none. */
|
|
92
|
+
stagedRestoreAtStart() {
|
|
93
|
+
return this.stagedRestore;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The name this manager is registered under: a staged part is recorded by
|
|
97
|
+
* it. Its class name when it is not registered (a test).
|
|
98
|
+
*/
|
|
99
|
+
registeredName() {
|
|
100
|
+
const engine = this.engine;
|
|
101
|
+
const names = typeof engine?.getRegisteredManagers === 'function' ? engine.getRegisteredManagers() : [];
|
|
102
|
+
return names.find((n) => engine?.getManager?.(n) === this) ?? this.constructor.name;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* #1985: a staged site restore of the declared parts goes in before the
|
|
106
|
+
* subclass reads them. BackupManager's own step, reached through its
|
|
107
|
+
* module (BackupManager starts after most managers); loaded only when there
|
|
108
|
+
* is something declared. A part not applied is said on the dashboard.
|
|
109
|
+
*/
|
|
110
|
+
async applyStagedBackupParts() {
|
|
111
|
+
let declared;
|
|
112
|
+
try {
|
|
113
|
+
declared = this.backupFolders();
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
logger.error(`[${this.constructor.name}] Its backup folders could not be read, so a staged restore of them was not checked: ${err.message}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (declared.length === 0)
|
|
120
|
+
return;
|
|
121
|
+
const { applyStagedPartsAtStart } = await import('./BackupManager.js');
|
|
122
|
+
this.stagedRestore = applyStagedPartsAtStart(this.engine, this.registeredName(), declared);
|
|
123
|
+
if (this.stagedRestore?.refused) {
|
|
124
|
+
this.markNeedsAttention(`A staged restore of ${declared.map((d) => d.part).join(', ')} was not applied: ${this.stagedRestore.refused}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
72
127
|
/**
|
|
73
128
|
* Check if manager has been initialized
|
|
74
129
|
*
|
|
@@ -312,10 +367,17 @@ class BaseManager {
|
|
|
312
367
|
* Whether this manager's `restore()` stages for the next start instead of
|
|
313
368
|
* writing into the running site (#1723, #1876). BackupManager stages every
|
|
314
369
|
* such manager in a site restore together, all or nothing, after a backup
|
|
315
|
-
* of the live data. The default
|
|
370
|
+
* of the live data. The default: true when it declares folders or files
|
|
371
|
+
* (#1985), which BackupManager stages; otherwise false, the restore writes
|
|
372
|
+
* live.
|
|
316
373
|
*/
|
|
317
374
|
restoreIsStaged() {
|
|
318
|
-
|
|
375
|
+
try {
|
|
376
|
+
return this.backupFolders().length > 0;
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
319
381
|
}
|
|
320
382
|
/**
|
|
321
383
|
* Return a plain-text string suitable for use as MarqueePlugin banner text.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import BaseManager, { type
|
|
1
|
+
import BaseManager, { type BackupFolder } from './BaseManager.js';
|
|
2
2
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
3
3
|
import type { PageComment } from '../types/Comment.js';
|
|
4
4
|
import { type ActorContext } from '../context/ActorContext.js';
|
|
@@ -10,15 +10,15 @@ export default class CommentManager extends BaseManager {
|
|
|
10
10
|
private readonly outsideCheck;
|
|
11
11
|
constructor(engine: WikiEngine);
|
|
12
12
|
initialize(): Promise<void>;
|
|
13
|
+
/** `ngdpbase.comments.storagedir`, resolved, or the default when there is no configuration (a test). */
|
|
14
|
+
private configuredDir;
|
|
13
15
|
/**
|
|
14
16
|
* The public folder is in the backup (#1876), as `<stem>.comments.tar` beside
|
|
15
17
|
* the document: the public pages' comments. A vault page's are in its vault, which
|
|
16
|
-
* PageManager backs up with the vault, sealed as stored.
|
|
18
|
+
* PageManager backs up with the vault, sealed as stored. BackupManager backs
|
|
19
|
+
* it up, stages it, and puts it in place at the next start (#1985).
|
|
17
20
|
*/
|
|
18
|
-
|
|
19
|
-
/** Staged for the next start with the rest of a site restore (#1876); the live folder is untouched. */
|
|
20
|
-
restore(backupData: BackupData, options?: ManagerRestoreOptions): Promise<void>;
|
|
21
|
-
restoreIsStaged(): boolean;
|
|
21
|
+
backupFolders(): BackupFolder[];
|
|
22
22
|
isEnabled(): boolean;
|
|
23
23
|
/** The public folder and the vaults, as configured now (#1839). */
|
|
24
24
|
private roots;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConsentManager — what each person has accepted, and asking them (#1603).
|
|
3
|
+
*
|
|
4
|
+
* It owns two things and asks for everything else:
|
|
5
|
+
*
|
|
6
|
+
* - __The records.__ One append-only file per account
|
|
7
|
+
* (`ngdpbase.consent.storagedir`, `FileConsentProvider`), owner-only: which
|
|
8
|
+
* document, which version, its effective date, when, and who and how.
|
|
9
|
+
* - __The prompts.__ Which documents a person still has to accept: core's
|
|
10
|
+
* (`ngdpbase.consent.documents`, asked at sign-up and at an interactive
|
|
11
|
+
* sign-in) and each add-on's (asked before its per-person switch turns on,
|
|
12
|
+
* and again on a version change when the person next uses it).
|
|
13
|
+
*
|
|
14
|
+
* The documents themselves are pages, read when asked (`version:` and
|
|
15
|
+
* `effective:` front matter), never copied. Whether a person has accepted
|
|
16
|
+
* the current version is derived each time from their records and the page,
|
|
17
|
+
* so a version change "clears" everyone's acceptance without touching a
|
|
18
|
+
* record. Per-use consents — apps approved through `/oidc`, share links —
|
|
19
|
+
* stay with their owners (OidcManager, ShareManager).
|
|
20
|
+
*
|
|
21
|
+
* Only the person accepts, for themselves: never a token, a share or a proxy
|
|
22
|
+
* acting for them, and never an administrator on their behalf. Each
|
|
23
|
+
* acceptance is recorded in the audit log (`consent-grant`, on-failure
|
|
24
|
+
* refuse) before it is stored.
|
|
25
|
+
*/
|
|
26
|
+
import BaseManager, { type BackupFolder } from './BaseManager.js';
|
|
27
|
+
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
28
|
+
import { type ActorContext } from '../context/ActorContext.js';
|
|
29
|
+
import { type AddonConsentRegistration, type ConsentDocument, type ConsentRecord, type ConsentScope, type ConsentSeen, type ConsentStatus } from '../types/Consent.js';
|
|
30
|
+
export declare const CONSENT_DOCUMENTS_KEY = "ngdpbase.consent.documents";
|
|
31
|
+
/** Refused: not this person, not signed in themselves, or not allowed. Nothing was recorded. */
|
|
32
|
+
export declare class ConsentRefusedError extends Error {
|
|
33
|
+
readonly name = "ConsentRefusedError";
|
|
34
|
+
}
|
|
35
|
+
/** A document changed (or appeared) after the person was shown it; show it again. Nothing was recorded. */
|
|
36
|
+
export declare class ConsentChangedError extends Error {
|
|
37
|
+
readonly name = "ConsentChangedError";
|
|
38
|
+
}
|
|
39
|
+
/** The documents of one scope with this person's state, for "Your consents". */
|
|
40
|
+
export interface ConsentScopeStatus {
|
|
41
|
+
scope: ConsentScope;
|
|
42
|
+
statuses: ConsentStatus[];
|
|
43
|
+
}
|
|
44
|
+
declare class ConsentManager extends BaseManager {
|
|
45
|
+
readonly description = "What each person has accepted \u2014 core's terms and add-ons' documents \u2014 and asking them (#1603)";
|
|
46
|
+
private provider;
|
|
47
|
+
private storageDir;
|
|
48
|
+
/** Add-on slug → its documents and the paths it is used at, registered at load. */
|
|
49
|
+
private readonly addons;
|
|
50
|
+
/** Username → their records, read once and dropped on every write. */
|
|
51
|
+
private readonly cache;
|
|
52
|
+
/** Document id → why it cannot be asked for; the needs-attention reason is made from these. */
|
|
53
|
+
private readonly problems;
|
|
54
|
+
private attentionReason;
|
|
55
|
+
constructor(engine: WikiEngine);
|
|
56
|
+
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
57
|
+
private configuredStorageDir;
|
|
58
|
+
/** The consent records folder is in the backup (#1985); a backup from before #1603 has none, so it is optional. */
|
|
59
|
+
backupFolders(): BackupFolder[];
|
|
60
|
+
/**
|
|
61
|
+
* An add-on's documents (#1603), registered from its `register()`. Each id
|
|
62
|
+
* must start `<slug>-`, so an add-on can never register core's documents or
|
|
63
|
+
* another add-on's; the add-on must be per-person (`perPerson: true`),
|
|
64
|
+
* since its documents are accepted when a person turns it on. Registering
|
|
65
|
+
* twice replaces the first.
|
|
66
|
+
*/
|
|
67
|
+
registerAddonDocuments(slug: string, registration: AddonConsentRegistration): void;
|
|
68
|
+
/** The add-ons that registered documents, in registration order. */
|
|
69
|
+
registeredAddons(): string[];
|
|
70
|
+
/** The add-on whose registered path `path` is under, or null. */
|
|
71
|
+
addonForPath(path: string): string | null;
|
|
72
|
+
/** Every document id a person may be asked to accept, core's and every add-on's: the pages that stay readable while they are asked. */
|
|
73
|
+
allDocumentIds(): string[];
|
|
74
|
+
private documentIds;
|
|
75
|
+
/**
|
|
76
|
+
* The documents of `scope` as they stand now. A page that is missing, or
|
|
77
|
+
* has no `version:`, is left out — nobody is asked for it, nothing is
|
|
78
|
+
* blocked — and ConsentManager needs attention until it is back.
|
|
79
|
+
*/
|
|
80
|
+
documents(scope?: ConsentScope): Promise<ConsentDocument[]>;
|
|
81
|
+
private readDocument;
|
|
82
|
+
/** Needs attention while any document cannot be asked for; ready again once none is (warn, never force). */
|
|
83
|
+
private reportProblems;
|
|
84
|
+
/** Read every document once, so a missing one shows on the dashboard from start-up. Called after the required pages are seeded. */
|
|
85
|
+
checkDocuments(): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* `username`'s records on their current account, oldest first. A record
|
|
88
|
+
* made on an earlier account of the same name is never theirs (#1937).
|
|
89
|
+
*/
|
|
90
|
+
private recordsOf;
|
|
91
|
+
private accountOf;
|
|
92
|
+
/**
|
|
93
|
+
* The documents of `scope` that `username` has not accepted at their
|
|
94
|
+
* current version — what they are to be asked. Empty when there is
|
|
95
|
+
* nothing to ask (no documents, or no consent folder).
|
|
96
|
+
*/
|
|
97
|
+
pendingFor(username: string, scope?: ConsentScope): Promise<ConsentDocument[]>;
|
|
98
|
+
/**
|
|
99
|
+
* "Your consents" for the person `ctx` acts as: each document of core's and
|
|
100
|
+
* of each registered add-on, with their acceptance of the current version
|
|
101
|
+
* (or none) and their latest of any version.
|
|
102
|
+
*/
|
|
103
|
+
statusFor(ctx: ActorContext): Promise<ConsentScopeStatus[]>;
|
|
104
|
+
/**
|
|
105
|
+
* Record that the person `ctx` acts as accepted what they were shown:
|
|
106
|
+
* every document of `scope` they have not accepted at its current version.
|
|
107
|
+
*
|
|
108
|
+
* - Only the person, for themselves: a token, share or proxy is refused, and
|
|
109
|
+
* there is no way to name anyone else. The person must hold
|
|
110
|
+
* `profile-manage`, the permission for managing their own account.
|
|
111
|
+
* - `seen` is what they were shown. A pending document missing from it, or
|
|
112
|
+
* shown at another version, refuses with `ConsentChangedError`, so nobody
|
|
113
|
+
* is recorded as accepting a version they never saw.
|
|
114
|
+
* - Each acceptance is recorded as `consent-grant` (on-failure refuse)
|
|
115
|
+
* before it is stored: a failed audit record refuses the acceptance.
|
|
116
|
+
*
|
|
117
|
+
* @param how How they were asked: `sign-up`, `sign-in` or `add-on`
|
|
118
|
+
* @returns The records written; empty when there was nothing to accept
|
|
119
|
+
*/
|
|
120
|
+
accept(ctx: ActorContext, scope: ConsentScope, seen: ConsentSeen[], how: string): Promise<ConsentRecord[]>;
|
|
121
|
+
/**
|
|
122
|
+
* The account `username` was deleted (`AuthManager.closeAccess`): its
|
|
123
|
+
* records go with it, after `consent-withdraw` names what it had accepted
|
|
124
|
+
* (on-failure refuse: the records stay, and a later account of that name
|
|
125
|
+
* still never inherits them). The audit log keeps the events for its
|
|
126
|
+
* retention window.
|
|
127
|
+
*/
|
|
128
|
+
removeAccount(username: string, ctx: ActorContext): Promise<void>;
|
|
129
|
+
}
|
|
130
|
+
export default ConsentManager;
|
|
@@ -92,6 +92,12 @@ declare class DatabaseManager extends BaseManager {
|
|
|
92
92
|
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
93
93
|
/** Whether an application database is open. */
|
|
94
94
|
isEnabled(): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Whether the open application database is encrypted at rest (SQLCipher,
|
|
97
|
+
* `NGDPBASE_DATABASE_KEY`); false when none is open. Asked by a provider
|
|
98
|
+
* that reports what its records are stored under (#1537).
|
|
99
|
+
*/
|
|
100
|
+
isEncrypted(): boolean;
|
|
95
101
|
/**
|
|
96
102
|
* Why the configured application database is not open (#1892): it could not
|
|
97
103
|
* be opened safely, and the site is in maintenance mode. Null when it is
|
|
@@ -47,7 +47,6 @@ import { AUDIT_EVENT } from '../utils/auditEventNames.js';
|
|
|
47
47
|
import logger from '../utils/logger.js';
|
|
48
48
|
import { ADDON_SLUG, ledgerPrefix } from '../utils/addonDatabaseRules.js';
|
|
49
49
|
import { applyStagedRestore, manifestPath, runOffThread, setAsideStagedRestore, stagedPath, writeManifest } from '../providers/sqliteBackup.js';
|
|
50
|
-
import { gatePart, markPart, recordStagedPart, siteRestoreFile } from '../utils/siteRestore.js';
|
|
51
50
|
import { BACKUP_KEY_ENV, backupKeyFingerprint, currentBackupKey, LEFT_OUT_FOR_NO_KEY } from '../utils/backupKey.js';
|
|
52
51
|
// The environment variable holding the SQLCipher key: one declaration, `src/utils/databaseKey.ts`.
|
|
53
52
|
import { DATABASE_KEY_ENV } from '../utils/databaseKey.js';
|
|
@@ -134,7 +133,7 @@ class DatabaseManager extends BaseManager {
|
|
|
134
133
|
this.databaseFile = file;
|
|
135
134
|
// #1723: a staged restore goes in now, before any database opens.
|
|
136
135
|
try {
|
|
137
|
-
this.applyRestoreAtStart(file);
|
|
136
|
+
await this.applyRestoreAtStart(file);
|
|
138
137
|
}
|
|
139
138
|
catch (err) {
|
|
140
139
|
this.markUnavailable(`A staged database restore could not be put in place: ${err.message}. ` +
|
|
@@ -172,6 +171,14 @@ class DatabaseManager extends BaseManager {
|
|
|
172
171
|
isEnabled() {
|
|
173
172
|
return this.provider !== null;
|
|
174
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Whether the open application database is encrypted at rest (SQLCipher,
|
|
176
|
+
* `NGDPBASE_DATABASE_KEY`); false when none is open. Asked by a provider
|
|
177
|
+
* that reports what its records are stored under (#1537).
|
|
178
|
+
*/
|
|
179
|
+
isEncrypted() {
|
|
180
|
+
return this.provider?.storage().encrypted ?? false;
|
|
181
|
+
}
|
|
175
182
|
/**
|
|
176
183
|
* Why the configured application database is not open (#1892): it could not
|
|
177
184
|
* be opened safely, and the site is in maintenance mode. Null when it is
|
|
@@ -361,6 +368,15 @@ class DatabaseManager extends BaseManager {
|
|
|
361
368
|
else {
|
|
362
369
|
this.clearFault('backup-key');
|
|
363
370
|
}
|
|
371
|
+
// #1537: the database audit provider buffers `continue` records; written
|
|
372
|
+
// first, so every record accepted before the backup is in the copy. A
|
|
373
|
+
// flush that fails leaves them queued and is logged; the backup goes on.
|
|
374
|
+
try {
|
|
375
|
+
await this.engine.getManager('AuditManager')?.flushAuditQueue?.();
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
logger.warn('[DatabaseManager] The audit queue could not be written before the backup; records still queued are not in it (#1537):', err);
|
|
379
|
+
}
|
|
364
380
|
const sources = this.backupSources(this.databaseFile);
|
|
365
381
|
const absent = sources.filter((s) => !existsSync(s.file)).map((s) => s.name);
|
|
366
382
|
const exports = sources
|
|
@@ -494,13 +510,7 @@ class DatabaseManager extends BaseManager {
|
|
|
494
510
|
};
|
|
495
511
|
writeManifest(manifestFile, manifest);
|
|
496
512
|
// #1876: part of a site restore — the databases go in with the other parts, or not at all.
|
|
497
|
-
|
|
498
|
-
recordStagedPart(options.siteRestore, {
|
|
499
|
-
manager: 'DatabaseManager',
|
|
500
|
-
swaps: manifest.databases.map((d) => ({ live: d.live, staged: d.staged })),
|
|
501
|
-
discard: [manifestFile]
|
|
502
|
-
});
|
|
503
|
-
}
|
|
513
|
+
options.siteRestore?.record(manifest.databases.map((d) => ({ live: d.live, staged: d.staged })), { discard: [manifestFile] });
|
|
504
514
|
const names = stages.map((s) => s.name).join(', ');
|
|
505
515
|
logger.warn(`[DatabaseManager] Restore staged for ${names} — restart to apply it (#1723)`);
|
|
506
516
|
const unopened = this.unavailable
|
|
@@ -611,7 +621,7 @@ class DatabaseManager extends BaseManager {
|
|
|
611
621
|
return sources;
|
|
612
622
|
}
|
|
613
623
|
/** Put a staged restore in place, before any database opens; loud when it cannot. */
|
|
614
|
-
applyRestoreAtStart(file) {
|
|
624
|
+
async applyRestoreAtStart(file) {
|
|
615
625
|
const dir = path.resolve(path.dirname(file));
|
|
616
626
|
const allowed = (live) => {
|
|
617
627
|
const resolved = path.resolve(live);
|
|
@@ -622,19 +632,19 @@ class DatabaseManager extends BaseManager {
|
|
|
622
632
|
};
|
|
623
633
|
// #1876: a database restore staged as part of a site restore goes in only
|
|
624
634
|
// when the whole site restore does; otherwise it is set aside with it.
|
|
625
|
-
|
|
635
|
+
// BackupManager keeps that record and decides (#1986); a module function, as DatabaseManager starts first.
|
|
636
|
+
const { siteRestorePartAtStart } = await import('./BackupManager.js');
|
|
626
637
|
const stagedBackupAt = this.getStagedRestore()?.backup ?? '';
|
|
627
|
-
const gate =
|
|
638
|
+
const gate = siteRestorePartAtStart(this.engine, 'DatabaseManager');
|
|
628
639
|
let outcome;
|
|
629
640
|
if (gate.decision === 'set-aside') {
|
|
630
641
|
outcome = setAsideStagedRestore(file, `the site restore it belongs to was not applied: ${gate.reason}`);
|
|
631
|
-
|
|
632
|
-
markPart(siteFile, 'DatabaseManager', 'set-aside', gate.reason);
|
|
642
|
+
gate.settle('set-aside', gate.reason);
|
|
633
643
|
}
|
|
634
644
|
else {
|
|
635
645
|
outcome = applyStagedRestore(file, allowed);
|
|
636
646
|
if (gate.decision === 'apply') {
|
|
637
|
-
|
|
647
|
+
gate.settle(outcome && !outcome.refused ? 'applied' : 'set-aside', outcome?.refused ?? (outcome ? undefined : 'no staged database restore was found'));
|
|
638
648
|
}
|
|
639
649
|
}
|
|
640
650
|
if (!outcome)
|
|
@@ -20,17 +20,34 @@
|
|
|
20
20
|
* running count, not one per email. `send()` still rejects, so a caller with
|
|
21
21
|
* a person waiting can tell them.
|
|
22
22
|
*
|
|
23
|
+
* `sendTo(ctx, …, { attachments })` attaches files, a PDF from
|
|
24
|
+
* `ExportManager.htmlToPdf` for one (#1974), up to
|
|
25
|
+
* `ngdpbase.mail.attachments.max-bytes` in total. A log line names how many
|
|
26
|
+
* files and bytes, never their names or content.
|
|
27
|
+
*
|
|
23
28
|
* @see docs/admin/email-setup.md for operator setup instructions
|
|
24
29
|
* @see {@link https://github.com/jwilleke/ngdpbase/issues/456}
|
|
25
30
|
*/
|
|
26
31
|
import BaseManager from './BaseManager.js';
|
|
27
32
|
import type { BackupData } from './BaseManager.js';
|
|
28
33
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
29
|
-
import type { MailMessage, MailProvider } from '../mail/MailProvider.js';
|
|
34
|
+
import type { MailAttachment, MailMessage, MailProvider } from '../mail/MailProvider.js';
|
|
35
|
+
import type { ActorContext } from '../context/ActorContext.js';
|
|
36
|
+
export type { MailAttachment } from '../mail/MailProvider.js';
|
|
30
37
|
/** The degraded reason while sends fail; cleared by the next success. */
|
|
31
38
|
export declare const SEND_FAILING_REASON = "outbound email is failing \u2014 see the server log and the administrator notification";
|
|
32
39
|
/** Title of the administrator notice. */
|
|
33
40
|
export declare const SEND_FAILING_TITLE = "Email could not be sent";
|
|
41
|
+
/** An attachment the message cannot carry (#1974): refused before sending, so it is not a delivery failure. */
|
|
42
|
+
export declare class AttachmentRefusedError extends Error {
|
|
43
|
+
readonly code = "ATTACHMENT_REFUSED";
|
|
44
|
+
constructor(message: string);
|
|
45
|
+
}
|
|
46
|
+
/** What a caller may add after the body (#1974). */
|
|
47
|
+
export interface SendOptions {
|
|
48
|
+
/** Files to attach, e.g. a PDF from `ExportManager.htmlToPdf`. */
|
|
49
|
+
attachments?: MailAttachment[];
|
|
50
|
+
}
|
|
34
51
|
declare class EmailManager extends BaseManager implements MailProvider {
|
|
35
52
|
private provider;
|
|
36
53
|
private providerName;
|
|
@@ -39,6 +56,7 @@ declare class EmailManager extends BaseManager implements MailProvider {
|
|
|
39
56
|
private noticeWindowMs;
|
|
40
57
|
private notice;
|
|
41
58
|
private failuresSinceSuccess;
|
|
59
|
+
private attachmentMaxBytes;
|
|
42
60
|
constructor(engine: WikiEngine);
|
|
43
61
|
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
44
62
|
/**
|
|
@@ -49,16 +67,25 @@ declare class EmailManager extends BaseManager implements MailProvider {
|
|
|
49
67
|
*/
|
|
50
68
|
send(message: MailMessage): Promise<void>;
|
|
51
69
|
/**
|
|
52
|
-
*
|
|
70
|
+
* Build and send a message in one call.
|
|
71
|
+
*
|
|
72
|
+
* @param ctx - Who is sending (security-posture P1); named in the log line when the message carries attachments
|
|
73
|
+
* @param to - Recipient address
|
|
74
|
+
* @param subject - Subject line
|
|
75
|
+
* @param text - Plain-text body
|
|
76
|
+
* @param html - Optional HTML body
|
|
77
|
+
* @param options - Attachments (#1974); their total is capped by `ngdpbase.mail.attachments.max-bytes`
|
|
53
78
|
*/
|
|
54
|
-
sendTo(to: string, subject: string, text: string, html?: string): Promise<void>;
|
|
79
|
+
sendTo(ctx: ActorContext, to: string, subject: string, text: string, html?: string, options?: SendOptions): Promise<void>;
|
|
55
80
|
/**
|
|
56
81
|
* Send without waiting, for a sender nobody is waiting on (a form's copy, a
|
|
57
82
|
* reservation notice). Never rejects: a failure is already logged and
|
|
58
83
|
* reported to administrators by `send()` (#1916), so there is nothing left
|
|
59
|
-
* for the caller to do with it.
|
|
84
|
+
* for the caller to do with it. An attachment over the limit is logged here.
|
|
60
85
|
*/
|
|
61
|
-
sendInBackground(to: string, subject: string, text: string, html?: string): void;
|
|
86
|
+
sendInBackground(ctx: ActorContext, to: string, subject: string, text: string, html?: string, options?: SendOptions): void;
|
|
87
|
+
/** Refuse attachments a mail relay would choke on, or that are not what they say. */
|
|
88
|
+
private checkAttachments;
|
|
62
89
|
/** Whether sends are failing right now: degraded by a failed send, not yet cleared by a success. */
|
|
63
90
|
isDeliveryFailing(): boolean;
|
|
64
91
|
private reportFailure;
|