@jwilleke/ngdpbase-types 5.25.0 → 5.26.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 (59) hide show
  1. package/README.md +1 -1
  2. package/lib/packages/ngdpbase-types/index.d.ts +4 -0
  3. package/lib/src/http/egressPolicy.js +61 -0
  4. package/lib/src/http/guardedElasticsearch.d.ts +27 -0
  5. package/lib/src/http/guardedElasticsearch.js +99 -0
  6. package/lib/src/managers/AddonsManager.d.ts +37 -1
  7. package/lib/src/managers/AttachmentManager.d.ts +4 -5
  8. package/lib/src/managers/AuthManager.d.ts +150 -32
  9. package/lib/src/managers/BackgroundJobManager.d.ts +22 -12
  10. package/lib/src/managers/BackupManager.d.ts +779 -0
  11. package/lib/src/managers/BackupManager.js +2042 -0
  12. package/lib/src/managers/BaseManager.d.ts +156 -6
  13. package/lib/src/managers/BaseManager.js +64 -2
  14. package/lib/src/managers/CommentManager.d.ts +6 -6
  15. package/lib/src/managers/ConsentManager.d.ts +130 -0
  16. package/lib/src/managers/DatabaseManager.d.ts +6 -0
  17. package/lib/src/managers/DatabaseManager.js +25 -15
  18. package/lib/src/managers/FootnoteManager.d.ts +6 -6
  19. package/lib/src/managers/OrganizationManager.d.ts +8 -6
  20. package/lib/src/managers/PageManager.d.ts +16 -0
  21. package/lib/src/managers/PersonManager.d.ts +8 -6
  22. package/lib/src/managers/RoleManager.d.ts +6 -8
  23. package/lib/src/providers/BaseAttachmentProvider.d.ts +4 -4
  24. package/lib/src/providers/BaseBackupProvider.d.ts +77 -0
  25. package/lib/src/providers/BaseBackupProvider.js +52 -0
  26. package/lib/src/providers/BasicAttachmentProvider.d.ts +4 -4
  27. package/lib/src/providers/FileBackupProvider.d.ts +34 -0
  28. package/lib/src/providers/FileBackupProvider.js +116 -0
  29. package/lib/src/providers/FileConsentProvider.d.ts +18 -0
  30. package/lib/src/providers/SqliteDatabaseProvider.d.ts +2 -2
  31. package/lib/src/providers/SqliteDatabaseProvider.js +12 -11
  32. package/lib/src/providers/attachmentFilesBackup.d.ts +5 -4
  33. package/lib/src/providers/databaseMigrations.js +45 -0
  34. package/lib/src/security/PolicyDecisionPoint.d.ts +14 -0
  35. package/lib/src/types/Consent.d.ts +90 -0
  36. package/lib/src/types/express.d.ts +6 -0
  37. package/lib/src/utils/addonHost.d.ts +22 -0
  38. package/lib/src/utils/addonHost.js +14 -1
  39. package/lib/src/utils/auditEventNames.generated.d.ts +20 -0
  40. package/lib/src/utils/auditEventNames.generated.js +20 -0
  41. package/lib/src/utils/backupCrypto.js +275 -0
  42. package/lib/src/utils/backupDocument.d.ts +36 -0
  43. package/lib/src/utils/backupDocument.js +99 -0
  44. package/lib/src/utils/browserDescription.d.ts +11 -0
  45. package/lib/src/utils/directoryBackup.d.ts +48 -48
  46. package/lib/src/utils/directoryBackup.js +291 -0
  47. package/lib/src/utils/instanceEnvBackup.d.ts +46 -0
  48. package/lib/src/utils/instanceEnvBackup.js +120 -0
  49. package/lib/src/utils/instanceEnvSecret.js +79 -0
  50. package/lib/src/utils/ipLocation.d.ts +11 -0
  51. package/lib/src/utils/schedule.js +171 -0
  52. package/lib/src/utils/secretFileMode.js +78 -0
  53. package/lib/src/utils/siteRestore.d.ts +13 -16
  54. package/lib/src/utils/siteRestore.js +33 -6
  55. package/lib/src/utils/treeArchive.d.ts +2 -0
  56. package/lib/src/utils/treeArchive.js +242 -0
  57. package/lib/src/utils/version.js +408 -0
  58. package/package.json +6 -1
  59. package/lib/src/utils/administrators.d.ts +0 -21
@@ -0,0 +1,779 @@
1
+ import type { Readable } from 'stream';
2
+ import BaseManager, { type BackupData as BaseBackupData, type BackupFolder, type StagedRestoreAtStart, type StagedSwap } from './BaseManager.js';
3
+ import { type ActorContext } from '../context/ActorContext.js';
4
+ import type { WikiEngine } from '../types/WikiEngine.js';
5
+ import { type Schedule, type ScheduleInput } from '../utils/schedule.js';
6
+ import { type SiteRestoreManifest } from '../utils/siteRestore.js';
7
+ import { type BackupDocument } from '../utils/backupDocument.js';
8
+ import type { JobRunSince } from './BackgroundJobManager.js';
9
+ /**
10
+ * Backup options
11
+ */
12
+ export interface BackupOptions {
13
+ filename?: string;
14
+ compress?: boolean;
15
+ /**
16
+ * Leave retention for the next backup (#1723). The backup a restore takes
17
+ * of the live data first sets it, so that backup can never prune the very
18
+ * backup being restored.
19
+ */
20
+ skipCleanup?: boolean;
21
+ /** Throw when any part of the backup failed, after saying so (#1723): a restore stages nothing without a whole copy of the live data. */
22
+ requireComplete?: boolean;
23
+ /** This is a restore's backup of the live data (#1892 D2); handed to every manager as `safetyBackup`. */
24
+ safetyBackup?: boolean;
25
+ }
26
+ /**
27
+ * Restore options
28
+ */
29
+ /** What a restore started from Admin → Backup puts back (#1945): the whole site, or the databases only. */
30
+ export type RestoreScope = 'site' | 'databases';
31
+ export interface RestoreOptions {
32
+ skipValidation?: boolean;
33
+ managerFilter?: string[];
34
+ /** Replace a database restore staged earlier and not applied yet (#1723, D5); refused without it. */
35
+ replaceStaged?: boolean;
36
+ }
37
+ /** A backup in the backup directory, as Admin → Backup lists it for download and restore (#1723). */
38
+ export interface StoredBackup extends BackupFileInfo {
39
+ /** The database files beside the document, by name (`application`, `addon-<owner>`). */
40
+ databases: string[];
41
+ /** The file archives beside the document, by part (`attachments`, `roles`, …) (#1876). */
42
+ parts: string[];
43
+ /** Every file beside the document, by filename: what a download carries with it. */
44
+ files: string[];
45
+ /** Whether the document is encrypted under the backup key (#1561): its name ends `.enc`. */
46
+ encrypted: boolean;
47
+ /** The document and every file beside it together, in bytes. */
48
+ totalSize: number;
49
+ }
50
+ /** Backup data structure: the document (`src/utils/backupDocument.ts`). */
51
+ export type BackupData = BackupDocument;
52
+ /**
53
+ * Restore results
54
+ */
55
+ export interface RestoreResults {
56
+ success: string[];
57
+ failed: Array<{
58
+ manager: string;
59
+ error: string;
60
+ }>;
61
+ skipped: string[];
62
+ }
63
+ /**
64
+ * Backup file information
65
+ */
66
+ export interface BackupFileInfo {
67
+ filename: string;
68
+ path: string;
69
+ size: number;
70
+ created: Date;
71
+ modified: Date;
72
+ }
73
+ /**
74
+ * BackupManager - Coordinates backup and restore operations across all managers
75
+ *
76
+ * Orchestrates system-wide backup and restore by calling backup()/restore()
77
+ * on all registered managers and aggregating their data into compressed archives.
78
+ *
79
+ * Responsibilities:
80
+ * - Call backup() on all registered managers
81
+ * - Aggregate backup data into a single .gz file
82
+ * - Restore from .gz backup file
83
+ * - Call restore() on all registered managers
84
+ *
85
+ * Architecture:
86
+ * - Each manager implements backup() to return its state
87
+ * - BackupManager collects all states into one object
88
+ * - Serializes to JSON and compresses with gzip
89
+ * - Stores as single .gz file
90
+ *
91
+ * @class BackupManager
92
+ * @extends BaseManager
93
+ *
94
+ * @property {string|null} backupDirectory - Directory for backup files
95
+ * @property {number} maxBackups - Maximum number of backups to retain
96
+ *
97
+ * @see {@link BaseManager} for base functionality and backup() pattern
98
+ *
99
+ * @example
100
+ * const backupManager = engine.getManager('BackupManager');
101
+ * const backupPath = await backupManager.createBackup(wikiContext.userContext);
102
+ * console.log('Backup created:', backupPath);
103
+ */
104
+ /** Auto-backup schedule configuration */
105
+ export interface AutoBackupConfig {
106
+ enabled: boolean;
107
+ time: string;
108
+ days: string;
109
+ maxBackups: number;
110
+ directory: string;
111
+ }
112
+ /**
113
+ * A backup document's filename without `.json.gz` / `.json` (#1723). A
114
+ * manager's own files beside the document are named `<stem>.<name>.db` (a
115
+ * database, #1723) or `<stem>.<part>.tar` (a folder of files, #1876), and are
116
+ * pruned and downloaded with it.
117
+ */
118
+ export declare function backupStem(filename: string): string;
119
+ /** Whether `filename` is one of the files a manager wrote beside the document named `documentFilename` (#1723); a file archive may be encrypted, `.tar.enc` (#1561). */
120
+ export declare function isCompanionOf(filename: string, documentFilename: string): boolean;
121
+ /**
122
+ * What no backup holds, so no restore brings back (#1876), in the words
123
+ * admins read: these stay as they are on the site. The backup-restore guide
124
+ * and the Backup help page list the same.
125
+ */
126
+ export declare const NEVER_IN_A_BACKUP: readonly string[];
127
+ /** The backup document entry, and the site restore part, an add-on's files are recorded under (#1985). */
128
+ export declare function addonEntryName(slug: string): string;
129
+ /**
130
+ * Put a staged restore of a manager's declared folders and files in place,
131
+ * at start, before it reads them (#1985). BackupManager's step: a manager
132
+ * reaches it through `BaseManager.initialize()`, an add-on's folders through
133
+ * AddonsManager before the add-on's `register()`. A module function, not a
134
+ * method, because most managers start before BackupManager does; it needs
135
+ * only the configuration, for the site restore manifest.
136
+ */
137
+ export declare function applyStagedPartsAtStart(engine: WikiEngine, manager: string, declared: BackupFolder[]): StagedRestoreAtStart | null;
138
+ /** How a manager whose restore is staged (`restoreIsStaged()`) puts its part in place at start (#1986). */
139
+ export interface StagedPartAtStartOptions {
140
+ /** Whether this manager may replace that live path: a manifest naming any other is refused, whatever it says. */
141
+ allowed: (swap: StagedSwap) => boolean;
142
+ /** The live paths of this manager's that may have a staged copy beside them: one no committed restore names (an interrupted staging) is set aside. */
143
+ mayBeStaged?: () => string[];
144
+ /**
145
+ * Put one swap in place yourself, instead of the rename: for a part whose
146
+ * live path is not a file or folder of its own (the audit files share the
147
+ * log folder). Move the live content to `kept` and the staged one in;
148
+ * throw to refuse.
149
+ */
150
+ put?: (swap: StagedSwap, kept: string) => void;
151
+ }
152
+ /**
153
+ * Put `manager`'s part of a staged site restore in place, at start, before
154
+ * it reads its data (#1876, #1986): when the whole restore is there to
155
+ * apply, each live path steps aside as `<live>.pre-restore` and the staged
156
+ * copy takes its name; otherwise its staged files are set aside as
157
+ * `*.incomplete`. The part is marked in BackupManager's record either way.
158
+ * For a manager whose restore is staged by its own `restore()` (the
159
+ * attachments, the audit log); declared folders and files go through
160
+ * `applyStagedPartsAtStart`. A module function: most managers start before
161
+ * BackupManager. Null when there was nothing to do.
162
+ */
163
+ export declare function applyStagedPartAtStart(engine: WikiEngine, manager: string, options: StagedPartAtStartOptions): StagedRestoreAtStart | null;
164
+ /**
165
+ * BackupManager's answer, at start, for a manager that applies a staged
166
+ * restore of its own kind (the databases, #1723): whether its part of a
167
+ * staged site restore is to go in now (#1876, #1986).
168
+ *
169
+ * - `none` — no site restore names it: the manager does as it would alone.
170
+ * - `apply` — the whole restore is there: put it in, then `settle` it.
171
+ * - `set-aside` — it isn't (`reason`): set the staged files aside, then `settle` it.
172
+ */
173
+ export interface SiteRestorePartAtStart {
174
+ decision: 'none' | 'apply' | 'set-aside';
175
+ reason?: string;
176
+ /** Record what became of the part. Nothing to record for `none`, or a set-aside with no part of this manager's. */
177
+ settle(status: 'applied' | 'set-aside', note?: string): void;
178
+ }
179
+ /** BackupManager's start-time gate on `manager`'s part of a staged site restore (#1986): see {@link SiteRestorePartAtStart}. */
180
+ export declare function siteRestorePartAtStart(engine: WikiEngine, manager: string): SiteRestorePartAtStart;
181
+ /** A manager's data as the "not restored" notification names it. */
182
+ export declare function restoreLabel(manager: string): string;
183
+ /**
184
+ * What a staged restore will not bring back, written when it is staged (#1876)
185
+ * and read once it has been applied, for the critical notification: kept by
186
+ * BackupManager beside the site restore manifest, as `<manifest>.report.json`.
187
+ */
188
+ export interface RestoreReport {
189
+ version: 1;
190
+ source: string;
191
+ /** When the backup was taken. */
192
+ backup: string;
193
+ /** The backup's parts that were not staged, by manager. */
194
+ notRestored: string[];
195
+ /** The job runs since the backup, read from the live job state when the restore was staged. */
196
+ jobRuns: JobRunSince[];
197
+ /** Who started the restore. */
198
+ stagedBy?: string;
199
+ }
200
+ /** BackupManager's fault, and the dashboard banner, while there is no backup key (#1561, #1912). */
201
+ export declare const BACKUP_KEY_MISSING = "NGDPBASE_BACKUP_KEY is not set, so backups are NOT encrypted: the document (password hashes, settings with their secrets) and the file archives beside it are in the clear, and the credentials store, the databases and the instance .env are left out";
202
+ /** The title of the one notification about it. */
203
+ export declare const BACKUP_KEY_MISSING_TITLE = "Backups are not encrypted";
204
+ /**
205
+ * The config key that records an admin's confirmation that the backup key is
206
+ * saved off the server (#1876): the fingerprint of the key they saved. A
207
+ * different key (a new one made, or one replaced) asks again.
208
+ */
209
+ export declare const BACKUP_KEY_SAVED_KEY = "ngdpbase.backup.key-saved-fingerprint";
210
+ /** The banner while no admin has confirmed the backup key is saved off the server (#1876). */
211
+ export declare function backupKeyNotSaved(fingerprint: string): string;
212
+ /**
213
+ * The config key that records the names (never the values) the instance
214
+ * `.env` set when an admin last confirmed it is saved off the server (#1976),
215
+ * sorted. Null until the first check records the baseline. The names, not
216
+ * only their fingerprint, so the reminder can say which keys were added or
217
+ * removed.
218
+ */
219
+ export declare const ENV_KEYS_SAVED_KEY = "ngdpbase.backup.env-keys-saved";
220
+ /** The title of the one notification per change of the `.env` key set (#1976). */
221
+ export declare const ENV_KEYS_CHANGED_TITLE = "Save a copy of .env";
222
+ /** The banner while the instance `.env` sets other names than the ones an admin last confirmed saved (#1976): names only, never a value. */
223
+ export declare function envKeysChanged(change: {
224
+ added: readonly string[];
225
+ removed: readonly string[];
226
+ }): string;
227
+ /** What Admin → Backup shows about the instance `.env` key set (#1976): names only, never a value. */
228
+ export interface EnvKeysInfo {
229
+ /** The names the file sets now, sorted. */
230
+ names: string[];
231
+ /** Their fingerprint, posted back with the confirmation so a stale page is refused. */
232
+ fingerprint: string;
233
+ /** Whether a set has been recorded at all: false only before the first check. */
234
+ recorded: boolean;
235
+ /** Names set now that were not when an admin last confirmed. */
236
+ added: string[];
237
+ /** Names an admin confirmed that the file no longer sets. */
238
+ removed: string[];
239
+ /** The file holds this site's current backup key, so a saved copy is a saved key too. */
240
+ holdsBackupKey: boolean;
241
+ }
242
+ /** What Admin → Backup shows about the backup key: never the key itself. */
243
+ export interface BackupKeyInfo {
244
+ set: boolean;
245
+ fingerprint: string | null;
246
+ /** An admin confirmed this key is saved off the server. */
247
+ saved: boolean;
248
+ }
249
+ /** The scheduled job automatic backups run as (#1720). */
250
+ export declare const AUTO_BACKUP_JOB_ID = "backup.auto";
251
+ /** The routine check that backups are still happening (#1563). */
252
+ export declare const BACKUP_HEALTH_JOB_ID = "backup.health";
253
+ /** The title of the notification sent when backups stop working (#1563): a backup failed, or none is recent enough. */
254
+ export declare const BACKUPS_NOT_WORKING_TITLE = "Backups are not working";
255
+ /** A backup that failed (#1563), as kept in the health record until a backup succeeds. */
256
+ export interface BackupFailure {
257
+ /** When it failed, ISO 8601. */
258
+ at: string;
259
+ /** Why, in words an admin can act on. */
260
+ reason: string;
261
+ /** The document that was written anyway (a backup with a part missing), so it is not counted as good. */
262
+ filename?: string;
263
+ }
264
+ /** Whether backups are working, as Admin → Backup and the /admin dashboard show it (#1563). */
265
+ export interface BackupHealth {
266
+ /** The newest backup in the backup directory that is not a failed one: when, how big (with the files beside it), where. */
267
+ lastGood: {
268
+ filename: string;
269
+ at: string;
270
+ size: number;
271
+ location: string;
272
+ } | null;
273
+ /** Automatic backups are on and scheduled. */
274
+ autoBackup: boolean;
275
+ /** The next automatic backup, ISO 8601, or null. */
276
+ nextRun: string | null;
277
+ /** Free bytes at the backup destination, or null when it can't be told. */
278
+ freeBytes: number | null;
279
+ /** The backup that failed, until one succeeds. */
280
+ failure: BackupFailure | null;
281
+ /** How long the site may go without a good backup before that is an outage, in ms. */
282
+ staleAfterMs: number;
283
+ /** No good backup within `staleAfterMs`, or none at all. */
284
+ stale: boolean;
285
+ /** The outage, in one line, while backups are not working; null when they are. */
286
+ fault: string | null;
287
+ }
288
+ /** A number of bytes as admins read it. */
289
+ export declare function formatBytes(bytes: number): string;
290
+ /** Why a backup could not be written, in words an admin can act on: the system's error codes named (#1563). */
291
+ export declare function describeBackupError(err: unknown): string;
292
+ /**
293
+ * The longest gap between two runs of a schedule (#1563): what "the
294
+ * auto-backup interval" is for a schedule that is not evenly spaced (Monday,
295
+ * Wednesday and Friday is three days at its longest; monthly is 31). Counted
296
+ * over the next year's runs, at most 400 of them.
297
+ */
298
+ export declare function longestGapMs(schedule: Schedule, from: Date): number;
299
+ /**
300
+ * The automatic-backup settings as a schedule (#1720): `ngdpbase.backup.auto-backup-time`
301
+ * (HH:MM) and `ngdpbase.backup.auto-backup-days` — `daily`, `monthly` (the 1st),
302
+ * `weekdays` (Monday to Friday), or day names such as `Mon,Wed,Fri`.
303
+ *
304
+ * In `timeZone`: the server's own, because the timer this replaces compared
305
+ * the server's local clock, so an existing setting keeps its time.
306
+ *
307
+ * Throws, naming the setting, for a time or a day it does not understand. The
308
+ * timer this replaces matched neither, so such a setting never backed up, and
309
+ * said nothing.
310
+ */
311
+ export declare function autoBackupSchedule(time: string, days: string, timeZone: string): ScheduleInput;
312
+ /**
313
+ * The text of the critical "not restored" notification (#1876): what the
314
+ * restore put back, what it did not, and per job the runs since the backup —
315
+ * its name, when it last ran and how that ended — newest first.
316
+ */
317
+ export declare function describeNotRestored(input: {
318
+ source: string;
319
+ backup: string;
320
+ applied: string[];
321
+ notRestored: string[];
322
+ jobRuns: JobRunSince[];
323
+ nameOf: (jobId: string) => string;
324
+ missingReport?: boolean;
325
+ }): string;
326
+ declare class BackupManager extends BaseManager {
327
+ private backupDirectory;
328
+ private maxBackups;
329
+ private autoBackupEnabled;
330
+ private autoBackupTime;
331
+ private autoBackupDays;
332
+ private provider;
333
+ private providerClass;
334
+ /** A restore started from Admin → Backup, while it runs (#1723). */
335
+ private restoreRunning;
336
+ /** The outage this manager set because backups are not working (#1563), so only that one is cleared when they work again. */
337
+ private healthFault;
338
+ /**
339
+ * The end of the queue of work that reads or changes the backup directory
340
+ * one at a time (#1563): a backup with its pruning, a restore's safety
341
+ * backup and staging, and the routine health check (`exclusive`).
342
+ */
343
+ private exclusiveTail;
344
+ /**
345
+ * Creates a new BackupManager instance
346
+ *
347
+ * @constructor
348
+ * @param {any} engine - The wiki engine instance
349
+ */
350
+ constructor(engine: WikiEngine);
351
+ /** Normalize a config provider name to its PascalCase class name. */
352
+ private normalizeProviderName;
353
+ /**
354
+ * Load the configured backup storage provider (#170). Mirrors the
355
+ * dynamic-import + fallback pattern used by AuditManager / SearchManager.
356
+ * Falls back to FileBackupProvider on any load error so backups never
357
+ * silently lose their storage target.
358
+ */
359
+ private loadProvider;
360
+ /**
361
+ * Initialize BackupManager
362
+ *
363
+ * @async
364
+ * @param {Record<string, unknown>} [config={}] - Configuration object (unused, reads from ConfigurationManager)
365
+ * @returns {Promise<void>}
366
+ * @throws {Error} If ConfigurationManager is not available
367
+ */
368
+ initialize(config?: Record<string, unknown>): Promise<void>;
369
+ /**
370
+ * Backup BackupManager's own state (conforms to BaseManager interface)
371
+ *
372
+ * @returns {Promise<BaseBackupData>} Backup data for this manager
373
+ */
374
+ backup(): Promise<BaseBackupData>;
375
+ /**
376
+ * Restore BackupManager's own state (conforms to BaseManager interface)
377
+ *
378
+ * @param {BaseBackupData} backupData - Backup data from backup()
379
+ * @returns {Promise<void>}
380
+ */
381
+ restoreState(backupData: BaseBackupData): Promise<void>;
382
+ /**
383
+ * Perform a complete backup of all managers to a file
384
+ *
385
+ * Process:
386
+ * 1. Get all registered managers from engine
387
+ * 2. Call backup() on each manager
388
+ * 3. Aggregate all backup data
389
+ * 4. Compress to .gz file
390
+ * 5. Save with timestamp
391
+ * 6. Clean up old backups
392
+ *
393
+ * @param {BackupOptions} options - Backup options
394
+ * @param {string} options.filename - Custom filename (optional)
395
+ * @param {boolean} options.compress - Whether to compress (default: true)
396
+ * @param ctx - Who asked (#1179): the request's subject, or the schedule's JobContext. First and mandatory — a full copy of the instance is made on someone's behalf.
397
+ * @returns {Promise<string>} Path to created backup file
398
+ */
399
+ createBackup(ctx: ActorContext, options?: BackupOptions): Promise<string>;
400
+ /**
401
+ * One at a time (#1563): `work` starts once everything queued before it
402
+ * has finished, whether that succeeded or failed, and is never run beside
403
+ * it. A second backup asked for while one runs waits for it and is then
404
+ * taken in its own right: its own document, its own options, its own
405
+ * result. So a backup's pruning never deletes files another backup, a
406
+ * restore or the health check is reading. In-process only: one server
407
+ * process owns a backup directory.
408
+ */
409
+ private exclusive;
410
+ /** `createBackup` itself, for a caller already holding the queue (`exclusive`): a restore's safety backup. */
411
+ private createBackupNow;
412
+ /**
413
+ * The instance `.env` beside the document (#1876): `<stem>.env.tar.enc`,
414
+ * only ever encrypted, and never with the backup key's own line. Without
415
+ * the key it is left out (`LEFT_OUT_FOR_NO_KEY`). BackupManager owns this
416
+ * part: the file belongs to no manager, and the backup key is this one's.
417
+ */
418
+ private backupInstanceEnv;
419
+ /** `<FAST_STORAGE>/.env`, or null when the instance data folder is not known. */
420
+ private instanceEnvFile;
421
+ /** The files the managers wrote beside one backup document (#1723). */
422
+ private companionsOf;
423
+ /** Remove the files the managers wrote beside one backup document (#1723), except those in `keep`. */
424
+ private removeCompanions;
425
+ /**
426
+ * Without `NGDPBASE_BACKUP_KEY` backups are still taken, unencrypted, with
427
+ * the credentials store and the databases left out (#1561, #1876). That is
428
+ * said twice, never once per backup (operator, #1912): BackupManager is
429
+ * degraded for as long as the key is missing — the persistent banner on the
430
+ * dashboard — and admins get ONE notification the first time. It is
431
+ * deduplicated the way other one-time notices are (`pageMetadataMissing`):
432
+ * by its title among the stored notifications, which persist across
433
+ * restarts and keep a dismissed one. With the key set the fault clears.
434
+ * Read at start and at each backup.
435
+ */
436
+ private checkBackupKey;
437
+ /**
438
+ * Whether a notification with this title (and message, when given) is
439
+ * among the stored ones, which persist across restarts and keep a dismissed
440
+ * one: how a one-time notice is deduplicated (`pageMetadataMissing`).
441
+ */
442
+ private alreadyNotified;
443
+ /**
444
+ * The instance `.env` key set against the one an admin last confirmed saved
445
+ * off the server (#1976): the reminder while they differ, else null. Names
446
+ * only — a value is never kept, logged or shown.
447
+ *
448
+ * The first check on a site with nothing recorded (a new site, or one
449
+ * upgraded to this release) records the names it finds as the baseline,
450
+ * silently: an upgraded site's keys are already in every encrypted backup,
451
+ * and a new site is already asked to save its backup key, so asking again at
452
+ * that moment would be noise. Every change after it asks. One notification
453
+ * per change, deduplicated by its title and words among the stored
454
+ * notifications, so a restart never repeats it.
455
+ */
456
+ private checkInstanceEnvKeys;
457
+ /** The instance `.env` key set as Admin → Backup shows it (#1976), or null when the site has no instance data folder or the file can't be read. Never a value. */
458
+ envKeysInfo(): EnvKeysInfo | null;
459
+ /** The backup key as Admin → Backup shows it: set or not, its fingerprint, and whether an admin confirmed it is saved. Never the key. */
460
+ backupKeyInfo(): BackupKeyInfo;
461
+ /**
462
+ * The backup key itself, for the one page that asks an admin to copy it
463
+ * off the server (#1876). The route asks `config-manage`, with its fresh
464
+ * sign-in; nothing else shows, logs or returns it.
465
+ */
466
+ revealBackupKey(): string | null;
467
+ /**
468
+ * The instance `.env` exactly as it is on disk, backup key line included,
469
+ * for an admin to keep a copy off the server (#1938). The route asks
470
+ * `config-manage`, with its fresh sign-in. Recorded as `secret-reveal`:
471
+ * the file, never its contents. Null when the site has no instance `.env`.
472
+ */
473
+ revealInstanceEnv(ctx: ActorContext): Promise<Buffer | null>;
474
+ /**
475
+ * An admin confirms the backup key with this fingerprint is saved off the
476
+ * server (#1876). Recorded in the configuration (`ngdpbase.backup.key-saved-fingerprint`,
477
+ * written through ConfigurationManager, which records the change), so it
478
+ * survives restarts; a different key asks again. Refused when the
479
+ * fingerprint is not the current key's: the page they read was stale.
480
+ *
481
+ * With `envFingerprint` (#1976) the admin confirms the whole `.env` is
482
+ * saved, the one confirmation for both reminders: the names it sets are
483
+ * recorded (`ngdpbase.backup.env-keys-saved`), and so is the backup key
484
+ * when the file holds it. Refused when the names changed since the page
485
+ * was loaded.
486
+ */
487
+ confirmBackupKeySaved(ctx: ActorContext, fingerprint: string, envFingerprint?: string): Promise<void>;
488
+ private configManager;
489
+ private setting;
490
+ /** `ngdpbase.backup.health-file`: the failure kept across restarts. */
491
+ private healthFile;
492
+ private readHealthRecord;
493
+ private writeHealthRecord;
494
+ /**
495
+ * A backup failed (#1563): kept until one succeeds, across restarts, and
496
+ * said once — admins get one "Backups are not working" notification per
497
+ * reason, not one per failed attempt. `notify: false` where another
498
+ * notification already said it ("A backup is incomplete").
499
+ */
500
+ private recordBackupFailure;
501
+ /** A backup succeeded (#1563): the failure kept since the last one is cleared. */
502
+ private clearBackupFailure;
503
+ /** The newest backup in the backup directory that is not the failed one: read from the directory, never remembered. */
504
+ private lastGoodBackup;
505
+ /** Automatic backups as they run (#1563): on only when scheduled and not paused, with the schedule in force and the next run. */
506
+ private autoBackupRun;
507
+ /**
508
+ * Whether backups are working (#1563), from what is on disk: the newest
509
+ * good backup in the backup directory, the failure kept in the health
510
+ * record, the automatic-backup schedule, and free space at the destination.
511
+ *
512
+ * No good backup within the window is an outage. The window is
513
+ * `ngdpbase.backup.stale-interval-multiplier` (default 2) times the longest
514
+ * gap between automatic backups when they are on, and
515
+ * `ngdpbase.backup.stale-fallback-days` (default 7) when they are off.
516
+ */
517
+ getBackupHealth(): Promise<BackupHealth>;
518
+ /**
519
+ * Bring the dashboard in line with whether backups work (#1563): an outage
520
+ * (`markDegraded`) while a backup has failed or none is recent enough,
521
+ * cleared — only that one — once they work, then the backup key's reminder
522
+ * (`checkBackupKey`), which never hides an outage. A stretch with no recent
523
+ * backup notifies admins once, kept across restarts. Run at start, on the
524
+ * `backup.health` timer, and after every backup.
525
+ */
526
+ refreshBackupHealth(): Promise<BackupHealth | null>;
527
+ /** `refreshBackupHealth` itself, for a caller already holding the queue: a backup that just ended. */
528
+ private refreshBackupHealthNow;
529
+ /** One notification per stretch without a recent good backup (#1563), kept across restarts by the health record. */
530
+ private notifyStaleOnce;
531
+ /**
532
+ * The routine check (#1563): `backup.health`, every
533
+ * `ngdpbase.backup.health-check-interval` ms (default one hour), as the
534
+ * system principal (BackgroundJobManager's routine maintenance). It is how
535
+ * a backup that never comes is noticed.
536
+ */
537
+ private registerHealthCheck;
538
+ /**
539
+ * Refuse a backup there is no room for (#1563), before anything is
540
+ * written: free space at the destination against the last backup's size
541
+ * (the document and every file beside it) times
542
+ * `ngdpbase.backup.space-margin` (default 1.5). Skipped when there is no
543
+ * earlier backup or the free space can't be told; a write that then runs
544
+ * out still fails, and says so.
545
+ */
546
+ private checkFreeSpace;
547
+ /**
548
+ * Read a backup just written back (#1563): decrypt it (every record's GCM
549
+ * tag), decompress it, parse it and check its shape, on a worker thread.
550
+ * A document that does not read back is removed — it is not a backup — and
551
+ * the backup fails, naming why.
552
+ */
553
+ private verifyWrittenBackup;
554
+ private notifyAdmins;
555
+ /**
556
+ * Restore all managers from a backup file
557
+ *
558
+ * Process:
559
+ * 1. Read and decompress backup file
560
+ * 2. Parse JSON data
561
+ * 3. Validate backup structure
562
+ * 4. Call restore() on each manager with its data
563
+ *
564
+ * The databases (#1723) go first, and are staged for the next start rather
565
+ * than restored in place: a fresh backup of the live data is taken, then
566
+ * DatabaseManager stages every database in the backup. If either fails,
567
+ * nothing at all is restored.
568
+ *
569
+ * @param ctx - Who asked (#1179): a restore takes a backup and replaces the instance's data on someone's behalf.
570
+ * @param {string} backupPath - Path to backup file
571
+ * @param {RestoreOptions} options - Restore options
572
+ * @param {boolean} options.skipValidation - Skip validation (default: false)
573
+ * @param {string[]} options.managerFilter - Only restore specific managers
574
+ * @returns {Promise<RestoreResults>} Restore results
575
+ */
576
+ restoreFromFile(ctx: ActorContext, backupPath: string, options?: RestoreOptions): Promise<RestoreResults>;
577
+ private restoreFromFileNow;
578
+ /** Whether this site's manager of that name stages its restore (#1876), or its entry is declared folders (#1985). */
579
+ private restoresStaged;
580
+ /**
581
+ * Each loaded add-on's folders (#1985): its `dataPath`, which counts for it
582
+ * whatever it declares, and any further folders its module declares; under
583
+ * the entry its files have always had (`addonEntryName`). For a backup, a
584
+ * `dataPath` that does not exist is left out: that add-on keeps no files.
585
+ */
586
+ private addonParts;
587
+ /** What a manager declares, and an add-on's folders recorded under the same entry (#1985). */
588
+ private declaredFor;
589
+ /** Every entry with declared folders or files, in registration order, add-ons' after (#1985). */
590
+ private declaredParts;
591
+ /**
592
+ * Put a staged restore of an add-on's folders in place before its
593
+ * `register()` runs, so before any of its managers reads them (#1985).
594
+ * AddonsManager calls it as it loads each add-on.
595
+ */
596
+ applyAddonPartsAtStart(slug: string, folders: BackupFolder[]): StagedRestoreAtStart | null;
597
+ /** The site restore manifest, as configured (#1876). */
598
+ private siteRestorePath;
599
+ /** The site restore staged and waiting for the next start, or null (#1876). Throws for a record that can't be read. */
600
+ getStagedSiteRestore(): SiteRestoreManifest | null;
601
+ /** A databases-only restore is refused while a site restore is staged: it would take the site restore's databases away from it (#1876). */
602
+ private refuseIfSiteRestoreStaged;
603
+ /**
604
+ * Stage a whole-site restore (#1876): every manager in `names` stages its
605
+ * part for the next start, after a complete backup of the live data, under
606
+ * one site restore manifest. All or nothing: a part that can't be staged
607
+ * discards every part staged before it, and the live data is never
608
+ * touched. The next start applies every part or none
609
+ * (`src/utils/siteRestore.ts`).
610
+ */
611
+ private stageSiteRestore;
612
+ /**
613
+ * Throw away a staged site restore before the restart applies it (#1876):
614
+ * every staged file and the record are removed, the live data is
615
+ * untouched. Recorded first (`backup-restore-cancel`, refuse).
616
+ */
617
+ cancelStagedSiteRestore(ctx: ActorContext): Promise<SiteRestoreManifest>;
618
+ /** Where this manager keeps the report of a staged restore: beside the site restore manifest (#1876). */
619
+ private restoreReportPath;
620
+ private removeRestoreReport;
621
+ /**
622
+ * Record, when a restore is staged, what it will not bring back (#1876):
623
+ * the parts of the backup not in it, and the job runs since the backup,
624
+ * read from the live job state now, before the restore replaces it.
625
+ */
626
+ private writeRestoreReport;
627
+ /**
628
+ * Tell admins what this start did with a staged restore (#1876). Called
629
+ * once every manager and add-on has started (`WikiEngine.initializeAddons`):
630
+ * each owner applied (or set aside) its part as it started, add-ons last.
631
+ * After any restore that was applied, a critical notification lists what it
632
+ * did not bring back, and the job runs between the backup and the restore.
633
+ */
634
+ reportRestoreAtBoot(): Promise<void>;
635
+ /**
636
+ * The critical notification after a restore was applied (#1876): every
637
+ * part of the site it did not bring back, and the job runs between the
638
+ * backup and the restore (name, last run, result), which are not in the
639
+ * restored job history. Jobs are never run again because of a restore.
640
+ */
641
+ private reportNotRestored;
642
+ /** The report written when the restore was staged, or null. */
643
+ private readRestoreReport;
644
+ /**
645
+ * `backup-restore-applied` (#1876, operator-approved): written at the start
646
+ * that applied a restore, so into the restored audit log — which otherwise
647
+ * would not say a restore happened (the `backup-restore` record of the
648
+ * staging is in the log it replaced). Names the backup, when it was taken,
649
+ * who started the restore and what was put back. on-failure: continue — the
650
+ * restore is already in place.
651
+ */
652
+ private recordRestoreApplied;
653
+ /** Tell admins what the start did with a staged site restore (#1876). */
654
+ private reportSiteRestoreOutcome;
655
+ /**
656
+ * The backups in the backup directory with the database files beside each
657
+ * (#1723): what Admin → Backup offers to download and restore. Read from
658
+ * the file names, so no document is opened to list them.
659
+ */
660
+ listStoredBackups(): Promise<StoredBackup[]>;
661
+ /** A backup in the backup directory by its bare filename, or null: never a path the caller made up. */
662
+ findStoredBackup(filename: string): Promise<StoredBackup | null>;
663
+ /**
664
+ * One backup as a `.tar` stream (#1723, D2): the document and every file
665
+ * beside it — the databases, and the archives of attachment files, role
666
+ * membership, persons, organizations, footnotes and comments (#1876) —
667
+ * read from disk as the stream is consumed, never built in memory. With
668
+ * the backup key set every file in it is encrypted under that key, which
669
+ * is never in the archive (#1723, #1561).
670
+ */
671
+ backupArchive(filename: string): Promise<{
672
+ name: string;
673
+ stream: Readable;
674
+ }>;
675
+ /** Whether a restore started from Admin → Backup is still taking its fresh backup or staging. */
676
+ isRestoreRunning(): boolean;
677
+ /** The managers on this site whose restore is staged (#1876), in registration order: what a site restore stages. */
678
+ private stagedManagerNames;
679
+ /**
680
+ * What a whole-site restore on this site puts back and what it does not
681
+ * (#1945), in the words admins read: the labels of the "not restored"
682
+ * notification (`restoreLabel`, `NEVER_IN_A_BACKUP`), so the restore card
683
+ * and the notification after the restart say the same. Read from the
684
+ * managers on this site, not from any backup: a backup can hold less (the
685
+ * credentials store of one taken without the backup key), never more.
686
+ */
687
+ siteRestoreScope(): {
688
+ restored: string[];
689
+ notRestored: string[];
690
+ };
691
+ /**
692
+ * The restore waiting for the next start, whichever it is (#1945): a site
693
+ * restore, or a databases-only one. A site restore stages the databases
694
+ * too, so it is the one named while both records are there.
695
+ */
696
+ getStagedRestore(): {
697
+ scope: RestoreScope;
698
+ source: string;
699
+ stagedBy: string;
700
+ } | null;
701
+ /**
702
+ * Throw away the restore waiting for the next start, whichever it is
703
+ * (#1945): a staged site restore through `cancelStagedSiteRestore`, a
704
+ * databases-only one through `DatabaseManager.cancelStagedRestore`. Each
705
+ * records `backup-restore-cancel` first (refuse).
706
+ */
707
+ cancelStagedRestore(ctx: ActorContext): Promise<{
708
+ scope: RestoreScope;
709
+ source: string;
710
+ }>;
711
+ /**
712
+ * Start a restore of a stored backup from Admin → Backup: recorded, then a
713
+ * fresh backup of the live data and the staging run in the background, and
714
+ * admins are told when it is staged or why it was not. Nothing changes
715
+ * until the next restart, which the site never does by itself. Refused
716
+ * while another is being prepared, and when a restore is already staged
717
+ * unless `replace` says to replace it.
718
+ *
719
+ * - `scope: 'databases'` (the default; #1723, D1): the databases only.
720
+ * Refused while a site restore is staged, since that includes them.
721
+ * - `scope: 'site'` (#1945): a whole-site restore, every manager whose
722
+ * restore is staged and the instance .env, all or nothing
723
+ * (`stageSiteRestore`), from any stored backup. It replaces a staged
724
+ * restore of either kind only with `replace`.
725
+ *
726
+ * @returns the running restore, for a caller (a test) that waits on it.
727
+ */
728
+ startRestore(ctx: ActorContext, filename: string, options?: {
729
+ replace?: boolean;
730
+ scope?: RestoreScope;
731
+ }): Promise<{
732
+ done: Promise<RestoreResults | null>;
733
+ }>;
734
+ /**
735
+ * List all available backups
736
+ * @returns {Promise<BackupFileInfo[]>} List of backup files with metadata
737
+ */
738
+ listBackups(): Promise<BackupFileInfo[]>;
739
+ /**
740
+ * Clean up old backups, keeping only maxBackups most recent
741
+ * @private
742
+ */
743
+ private cleanupOldBackups;
744
+ /**
745
+ * Get the most recent backup file path
746
+ * @returns {Promise<string | null>} Path to most recent backup, or null if none exist
747
+ */
748
+ getLatestBackup(): Promise<string | null>;
749
+ /**
750
+ * Return current auto-backup configuration and last backup info for the admin UI.
751
+ */
752
+ getAutoBackupStatus(): Promise<{
753
+ config: AutoBackupConfig;
754
+ lastBackup: Date | null;
755
+ }>;
756
+ /**
757
+ * Update auto-backup configuration and persist to config store.
758
+ * Restarts the scheduler with the new settings.
759
+ */
760
+ updateAutoBackupConfig(config: Partial<AutoBackupConfig>, ctx: ActorContext): Promise<void>;
761
+ /**
762
+ * Automatic backups as a scheduled job (#1720, epic #1611).
763
+ *
764
+ * They ran from a timer that checked every minute for an exact match of the
765
+ * clock, so a server that was down at the backup time — or a tick that
766
+ * landed a minute late — skipped that backup and left no record that it had
767
+ * (operator: backups are never silent, #1563). As a scheduled job a backup
768
+ * missed while the server was down runs once when it is back (`catchUp:
769
+ * 'latest'`), a failed one is tried again and then reported, and the run
770
+ * survives a restart. A backup still running when the next one comes due is
771
+ * not doubled (`overlap: 'skip'`).
772
+ *
773
+ * Called at start and whenever the settings change. With automatic backups
774
+ * off, or a setting that cannot be read, the job is registered without a
775
+ * schedule: it can still be run by hand, and nothing runs by itself.
776
+ */
777
+ private scheduleAutoBackup;
778
+ }
779
+ export default BackupManager;