@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.
Files changed (89) hide show
  1. package/README.md +1 -1
  2. package/lib/packages/ngdpbase-types/index.d.ts +9 -2
  3. package/lib/packages/ngdpbase-types/testing.d.ts +7 -3
  4. package/lib/packages/ngdpbase-types/testing.js +7 -3
  5. package/lib/src/converters/DocxConverter.d.ts +30 -0
  6. package/lib/src/converters/MarkdownConverter.d.ts +31 -0
  7. package/lib/src/export/ChromeServicePdfProvider.d.ts +52 -0
  8. package/lib/src/export/ChromeServicePdfProvider.js +153 -0
  9. package/lib/src/export/LocalChromePdfProvider.d.ts +25 -0
  10. package/lib/src/export/LocalChromePdfProvider.js +33 -0
  11. package/lib/src/export/PdfProvider.d.ts +51 -0
  12. package/lib/src/export/PdfProvider.js +37 -0
  13. package/lib/src/export/pdfEngine.d.ts +113 -0
  14. package/lib/src/export/pdfEngine.js +195 -0
  15. package/lib/src/http/egressPolicy.js +61 -0
  16. package/lib/src/http/guardedElasticsearch.d.ts +27 -0
  17. package/lib/src/http/guardedElasticsearch.js +99 -0
  18. package/lib/src/ice/converterShim.d.ts +5 -0
  19. package/lib/src/ice/fileHandling.d.ts +30 -0
  20. package/lib/src/ice/ledger.d.ts +37 -0
  21. package/lib/src/ice/runImport.d.ts +51 -0
  22. package/lib/src/ice/types.d.ts +262 -0
  23. package/lib/src/mail/MailProvider.d.ts +15 -0
  24. package/lib/src/mail/NodemailerMailProvider.d.ts +1 -1
  25. package/lib/src/managers/AddonsManager.d.ts +55 -3
  26. package/lib/src/managers/AttachmentManager.d.ts +4 -5
  27. package/lib/src/managers/AuthManager.d.ts +150 -32
  28. package/lib/src/managers/BackgroundJobManager.d.ts +22 -12
  29. package/lib/src/managers/BackupManager.d.ts +779 -0
  30. package/lib/src/managers/BackupManager.js +2042 -0
  31. package/lib/src/managers/BaseManager.d.ts +156 -6
  32. package/lib/src/managers/BaseManager.js +64 -2
  33. package/lib/src/managers/CommentManager.d.ts +6 -6
  34. package/lib/src/managers/ConsentManager.d.ts +130 -0
  35. package/lib/src/managers/DatabaseManager.d.ts +6 -0
  36. package/lib/src/managers/DatabaseManager.js +25 -15
  37. package/lib/src/managers/EmailManager.d.ts +32 -5
  38. package/lib/src/managers/ExportManager.d.ts +62 -0
  39. package/lib/src/managers/ExportManager.js +147 -2
  40. package/lib/src/managers/FootnoteManager.d.ts +6 -6
  41. package/lib/src/managers/ImportManager.d.ts +590 -0
  42. package/lib/src/managers/OrganizationManager.d.ts +8 -6
  43. package/lib/src/managers/PageManager.d.ts +61 -1
  44. package/lib/src/managers/PersonManager.d.ts +8 -6
  45. package/lib/src/managers/RoleManager.d.ts +6 -8
  46. package/lib/src/providers/BaseAttachmentProvider.d.ts +4 -4
  47. package/lib/src/providers/BaseBackupProvider.d.ts +77 -0
  48. package/lib/src/providers/BaseBackupProvider.js +52 -0
  49. package/lib/src/providers/BasicAttachmentProvider.d.ts +4 -4
  50. package/lib/src/providers/FileBackupProvider.d.ts +34 -0
  51. package/lib/src/providers/FileBackupProvider.js +116 -0
  52. package/lib/src/providers/FileConsentProvider.d.ts +18 -0
  53. package/lib/src/providers/SqliteDatabaseProvider.d.ts +2 -2
  54. package/lib/src/providers/SqliteDatabaseProvider.js +12 -11
  55. package/lib/src/providers/attachmentFilesBackup.d.ts +5 -4
  56. package/lib/src/providers/databaseMigrations.js +45 -0
  57. package/lib/src/security/PolicyDecisionPoint.d.ts +14 -0
  58. package/lib/src/security/PolicyInformationPoint.d.ts +25 -2
  59. package/lib/src/types/Config.d.ts +2 -0
  60. package/lib/src/types/Consent.d.ts +90 -0
  61. package/lib/src/types/express.d.ts +6 -0
  62. package/lib/src/utils/addonHost.d.ts +45 -2
  63. package/lib/src/utils/addonHost.js +39 -3
  64. package/lib/src/utils/auditEventNames.generated.d.ts +20 -0
  65. package/lib/src/utils/auditEventNames.generated.js +20 -0
  66. package/lib/src/utils/backupCrypto.js +275 -0
  67. package/lib/src/utils/backupDocument.d.ts +36 -0
  68. package/lib/src/utils/backupDocument.js +99 -0
  69. package/lib/src/utils/browserDescription.d.ts +11 -0
  70. package/lib/src/utils/directoryBackup.d.ts +48 -48
  71. package/lib/src/utils/directoryBackup.js +291 -0
  72. package/lib/src/utils/frontmatterAccess.d.ts +54 -11
  73. package/lib/src/utils/importRunSummary.d.ts +64 -0
  74. package/lib/src/utils/instanceEnvBackup.d.ts +46 -0
  75. package/lib/src/utils/instanceEnvBackup.js +120 -0
  76. package/lib/src/utils/instanceEnvSecret.js +79 -0
  77. package/lib/src/utils/ipLocation.d.ts +11 -0
  78. package/lib/src/utils/ncmNotify.d.ts +30 -0
  79. package/lib/src/utils/privateStoreImport.d.ts +58 -0
  80. package/lib/src/utils/schedule.js +171 -0
  81. package/lib/src/utils/secretFileMode.js +78 -0
  82. package/lib/src/utils/siteRestore.d.ts +13 -16
  83. package/lib/src/utils/siteRestore.js +33 -6
  84. package/lib/src/utils/treeArchive.d.ts +2 -0
  85. package/lib/src/utils/treeArchive.js +242 -0
  86. package/lib/src/utils/version.js +408 -0
  87. package/lib/src/utils/zipArchive.d.ts +71 -0
  88. package/package.json +6 -1
  89. 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
- * The site restore manifest this restore is staged under (#1876). A
83
- * manager whose restore is staged (`restoreIsStaged()`) stages its files
84
- * beside its live ones and records its part there; the next start applies
85
- * every part or none (`src/utils/siteRestore.ts`).
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?: string;
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 is false: the restore writes live.
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 is false: the restore writes live.
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
- return false;
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 BackupData, type ManagerBackupOptions, type ManagerRestoreOptions } from './BaseManager.js';
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
- backup(options?: ManagerBackupOptions): Promise<BackupData>;
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
- if (options.siteRestore) {
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
- const siteFile = siteRestoreFile(this.engine.getManager('ConfigurationManager'));
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 = gatePart(siteFile, 'DatabaseManager');
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
- if (gate.part)
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
- markPart(siteFile, 'DatabaseManager', outcome && !outcome.refused ? 'applied' : 'set-aside', outcome?.refused ?? (outcome ? undefined : 'no staged database restore was found'));
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
- * Convenience wrapper — build and send a message in one call.
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;