@jwilleke/ngdpbase-types 5.18.0 → 5.20.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.
@@ -17,6 +17,11 @@ import type { WikiEngine } from '../types/WikiEngine.js';
17
17
  import type { User } from '../types/User.js';
18
18
  import { type StoreOwnerState } from '../utils/privateStoreDoor.js';
19
19
  import { type AddonHost } from '../utils/addonHost.js';
20
+ /**
21
+ * #1883: what visitors see while a site-critical add-on that did not load
22
+ * holds the site in maintenance mode. Empty leaves only the page's heading.
23
+ */
24
+ export declare const SITE_CRITICAL_PUBLIC_MESSAGE_KEY = "ngdpbase.managers.addons-manager.site-critical.public-message";
20
25
  /**
21
26
  * Type passed to `AddonModule.profileSection()` (#534). Matches what
22
27
  * `UserManager.getUser()` returns at runtime — `User` minus the password
@@ -280,6 +285,8 @@ declare class AddonsManager extends BaseManager {
280
285
  private dashboardCards;
281
286
  /** Name of the first domain addon loaded — only one is permitted */
282
287
  private domainAddonName;
288
+ /** #1883: the enabled add-ons that did not load this start, with why — each reported once. */
289
+ private readonly loadFailures;
283
290
  constructor(engine: WikiEngine);
284
291
  /**
285
292
  * Initialize the AddonsManager
@@ -351,6 +358,30 @@ declare class AddonsManager extends BaseManager {
351
358
  * bundled/drop-in addon wins.
352
359
  */
353
360
  private registerAddonFromDir;
361
+ /**
362
+ * Register an add-on that will not be loaded, so Admin → Add-ons shows it
363
+ * with the reason, and report it when it is enabled (#1883). The first
364
+ * add-on of a slug wins, as for a loadable one.
365
+ */
366
+ private refuseAddon;
367
+ /**
368
+ * #1883 (operator, 2026-10-18): the one rule for an enabled add-on that does
369
+ * not load, whatever the reason. Admins get a notification at the most
370
+ * severe level naming it and the reason, which says how to fix it where the
371
+ * reason does. A site-critical add-on (`ngdpbase.addons.<slug>.site-critical`)
372
+ * also holds the whole site in maintenance mode through core's survivable
373
+ * path (#1152): visitors see only the configured public message, with no
374
+ * details, and administrators can still sign in and reach /admin. Once per
375
+ * add-on per start. Nothing is opened in its place.
376
+ */
377
+ private recordLoadFailure;
378
+ /**
379
+ * #1883: an enabled add-on that discovery never found — its install failed
380
+ * (#1830), or only a stray folder answers it — did not load either.
381
+ */
382
+ private reportUndiscoveredAddons;
383
+ /** A notification to administrators; best-effort, like recordSeedFailure. */
384
+ private notifyAdmins;
354
385
  /**
355
386
  * Why an addon cannot run on this core (#1757): its manifest's `minVersion`
356
387
  * is newer than this release. Null when it can, or when it states none.
@@ -1052,6 +1052,15 @@ declare class AuthManager extends BaseManager {
1052
1052
  unlockVaults(username: string, password: string): Promise<string | undefined>;
1053
1053
  private vaultUnlocker;
1054
1054
  private warnedNoUnlocker;
1055
+ /**
1056
+ * The stored spelling of a username a sign-in brought (#1882), or null when
1057
+ * no account has it. Lookups compare normalized (#1436), so "ADMIN" finds
1058
+ * admin's record; the session, the audit record, the roles and the vault
1059
+ * paths must carry the stored name, never the typed variant, which is a
1060
+ * roleless twin. The one rule, asked by every door a sign-in passes:
1061
+ * `authenticate`, `beginTwoStep` and `completeSignIn`.
1062
+ */
1063
+ private storedUsername;
1055
1064
  /** A fresh session for a sign-in (#1043); the one already on it ends first (#1626, #1670). */
1056
1065
  private regenerate;
1057
1066
  /**
@@ -1109,7 +1118,7 @@ declare class AuthManager extends BaseManager {
1109
1118
  ip?: string;
1110
1119
  userAgent?: string;
1111
1120
  privateStoreHandle?: string;
1112
- }): string;
1121
+ }): Promise<string>;
1113
1122
  /** The pending sign-in, for the browser that started it; null when gone, expired, or another browser. */
1114
1123
  pendingSignIn(handle: string | undefined, binding: string | undefined): PendingSignIn | null;
1115
1124
  /**
@@ -18,6 +18,7 @@ import { type PathPreflightResult, type PathPreflightOptions } from '../utils/Pa
18
18
  import type { WikiEngine } from '../types/WikiEngine.js';
19
19
  import type { ManagerFetchOptions } from '../utils/managerUtils.js';
20
20
  import type { FormOption } from '../utils/pluginFormatters.js';
21
+ import type { ActorContext } from '../context/ActorContext.js';
21
22
  /**
22
23
  * Backup data structure returned by backup() method
23
24
  */
@@ -34,9 +35,44 @@ export interface BackupData {
34
35
  providerBackup?: unknown;
35
36
  /** Optional note about the backup */
36
37
  note?: string;
38
+ /**
39
+ * Parts of this manager's backup that failed while the rest was written
40
+ * (#1723) — a database that could not be copied, say. BackupManager tells the
41
+ * admins about each one: a backup is never silently partial.
42
+ */
43
+ warnings?: string[];
37
44
  /** Allow additional properties */
38
45
  [key: string]: unknown;
39
46
  }
47
+ /**
48
+ * What BackupManager hands each manager's `backup()` (#1723): who asked, and
49
+ * where the backup document is being written. A manager whose data is too
50
+ * large for the JSON document (the databases) writes its own files into
51
+ * `directory`, each named `<stem>.<something>`, and references them from its
52
+ * entry; BackupManager prunes them with the document.
53
+ */
54
+ export interface ManagerBackupOptions {
55
+ ctx: ActorContext;
56
+ /** The backup directory the document is written to. */
57
+ directory: string;
58
+ /** The document's filename without `.json.gz` / `.json`. */
59
+ stem: string;
60
+ }
61
+ /** What BackupManager hands each manager's `restore()` (#1723): who asked, and the directory the backup document was read from. */
62
+ export interface ManagerRestoreOptions {
63
+ ctx: ActorContext;
64
+ directory: string;
65
+ /** The backup document's filename. */
66
+ source?: string;
67
+ /**
68
+ * Replace what an earlier restore staged and the restart has not applied
69
+ * yet (#1723, D5). Without it a manager that stages refuses, so a staged
70
+ * restore is never replaced silently.
71
+ */
72
+ replaceStaged?: boolean;
73
+ /** The backup of the live data BackupManager took just before this restore (#1723, D6), by filename. */
74
+ safetyBackup?: string;
75
+ }
40
76
  /**
41
77
  * Base class for all managers
42
78
  *
@@ -279,7 +315,7 @@ declare abstract class BaseManager {
279
315
  * };
280
316
  * }
281
317
  */
282
- backup(): Promise<BackupData>;
318
+ backup(_options?: ManagerBackupOptions): Promise<BackupData>;
283
319
  /**
284
320
  * Restore manager data from backup
285
321
  *
@@ -298,7 +334,7 @@ declare abstract class BaseManager {
298
334
  * this.settings = backupData.data.settings;
299
335
  * }
300
336
  */
301
- restore(backupData: BackupData): Promise<void>;
337
+ restore(backupData: BackupData, _options?: ManagerRestoreOptions): Promise<void>;
302
338
  /**
303
339
  * Return a plain-text string suitable for use as MarqueePlugin banner text.
304
340
  *
@@ -265,7 +265,7 @@ class BaseManager {
265
265
  * };
266
266
  * }
267
267
  */
268
- async backup() {
268
+ async backup(_options) {
269
269
  // Default implementation returns empty object
270
270
  // Managers with data MUST override this method
271
271
  return {
@@ -292,7 +292,7 @@ class BaseManager {
292
292
  * this.settings = backupData.data.settings;
293
293
  * }
294
294
  */
295
- async restore(backupData) {
295
+ async restore(backupData, _options) {
296
296
  // Default implementation does nothing
297
297
  // Managers with data MUST override this method
298
298
  if (!backupData) {
@@ -60,10 +60,12 @@ declare class ConfigurationManager extends BaseManager {
60
60
  private addonLayers;
61
61
  /** #1559: add-on system-categories refused, and operator values an owner's declaration overrode; #1860: database declarations refused or overridden. */
62
62
  private systemCategoryProblems;
63
- /** #1559: add-ons that must not load because an entry they declare was refused, by slug, with why; #1860: or their database declaration. */
63
+ /** #1559: add-ons that must not load because an entry they declare was refused, by slug, with why; #1860: or their database declaration; #1883: or a site-critical value that is not a boolean. */
64
64
  private refusedAddons;
65
65
  /** #1860: every installed add-on's sound database declaration, on or off. */
66
66
  private addonDatabases;
67
+ /** #1883: which add-ons put the site in maintenance mode when they fail to load, by slug. */
68
+ private siteCriticalAddons;
67
69
  private environment;
68
70
  private defaultConfigPath;
69
71
  private customConfigPath;
@@ -282,6 +284,14 @@ declare class ConfigurationManager extends BaseManager {
282
284
  * ({@link getAddonRefusal}). DatabaseManager is the reader.
283
285
  */
284
286
  getAddonDatabaseDeclarations(): readonly AddonDatabaseDeclaration[];
287
+ /**
288
+ * #1883: whether this add-on's failure to load puts the whole site in
289
+ * maintenance mode — `ngdpbase.addons.<slug>.site-critical`, from the
290
+ * operator's file or else the add-on's own, read even when the add-on was
291
+ * refused. A value that is not exactly true or false counts as true (and
292
+ * refuses the add-on, {@link getAddonRefusal}).
293
+ */
294
+ isAddonSiteCritical(slug: string): boolean;
285
295
  /**
286
296
  * #1813: what one enabled addon's own `config/default-config.json` declares,
287
297
  * exactly as it was folded into the merge — not the merged view, so the
@@ -1,9 +1,52 @@
1
1
  import BaseManager from './BaseManager.js';
2
2
  import type { WikiEngine } from '../types/WikiEngine.js';
3
3
  import type { Migration } from '../providers/sqliteMigrations.js';
4
+ import { type ActorContext } from '../context/ActorContext.js';
5
+ import type { BackupData, ManagerBackupOptions, ManagerRestoreOptions } from './BaseManager.js';
4
6
  import { type AddonDatabaseDeclaration } from '../utils/addonDatabaseRules.js';
7
+ import { type StagedRestoreManifest } from '../providers/sqliteBackup.js';
5
8
  /** The environment variable holding the SQLCipher key. Never a config key: config is readable from the admin screens. */
6
9
  export declare const DATABASE_KEY_ENV = "NGDPBASE_DATABASE_KEY";
10
+ /**
11
+ * The environment variable holding the key every database backup is encrypted
12
+ * under (#1723). Its own secret, not the database key: the copy that travels
13
+ * and the copy that stays should not fall to the same compromise. Never a
14
+ * config key. Unset, no database is backed up, and admins are told.
15
+ */
16
+ export declare const BACKUP_KEY_ENV = "NGDPBASE_BACKUP_KEY";
17
+ /** How long a database backup or restore staging may run on its worker before it is stopped, when not configured. */
18
+ export declare const DEFAULT_BACKUP_WORKER_TIMEOUT_MS = 3600000;
19
+ /**
20
+ * A short fingerprint of a backup key (#1723, D3): the first 16 hex digits of
21
+ * an HMAC of a fixed label under the key. Recorded in each backup so a restore
22
+ * under another key says so plainly; it never reveals the key.
23
+ */
24
+ export declare function backupKeyFingerprint(key: string): string;
25
+ /** One database in a backup, as DatabaseManager's entry in the backup document lists it. */
26
+ export interface DatabaseBackupEntry {
27
+ /** `application`, or `addon-<owner>`. */
28
+ name: string;
29
+ kind: 'application' | 'addon';
30
+ owner?: string;
31
+ /** The encrypted file beside the backup document: `<stem>.<name>.db`. */
32
+ file: string;
33
+ sizeBytes: number;
34
+ tables: number;
35
+ }
36
+ /** DatabaseManager's `data` in a backup document. */
37
+ export interface DatabaseBackupData {
38
+ format: 1;
39
+ /** `backupKeyFingerprint()` of the key the files are encrypted under. */
40
+ keyFingerprint?: string;
41
+ databases: DatabaseBackupEntry[];
42
+ /** Declared databases with no file yet (an add-on that never opened it). */
43
+ absent: string[];
44
+ /** Databases that could not be copied; the backup carries the others. */
45
+ failed: Array<{
46
+ name: string;
47
+ error: string;
48
+ }>;
49
+ }
7
50
  /** What an add-on hands over to have its database opened and migrated. */
8
51
  export interface AddonDatabaseOptions {
9
52
  /** Dated migrations, strictly ascending by id — the same rules as core's ledger. */
@@ -37,6 +80,10 @@ declare class DatabaseManager extends BaseManager {
37
80
  private databaseFile;
38
81
  private busyTimeoutMs;
39
82
  private readonly addonDatabases;
83
+ /** What the start did with a staged restore (#1723), reported once the notifications exist. */
84
+ private restoreAtStart;
85
+ /** Why this manager is degraded, one entry per cause, so one cause clearing leaves the others. */
86
+ private readonly faults;
40
87
  constructor(engine: WikiEngine);
41
88
  initialize(config?: Record<string, unknown>): Promise<void>;
42
89
  /** Whether an application database is open. */
@@ -81,6 +128,58 @@ declare class DatabaseManager extends BaseManager {
81
128
  private declarations;
82
129
  /** The one rule: a database declared `encrypt: "required"` is never opened without the key. */
83
130
  private static keyRefusal;
131
+ /**
132
+ * Copy every database into the backup BackupManager is writing: the
133
+ * application database and every declared add-on database, on or off, at
134
+ * one point in time, on a worker thread. Each goes into its own file,
135
+ * `<stem>.<name>.db`, beside the document, encrypted under
136
+ * `NGDPBASE_BACKUP_KEY`; the document references them. A database that
137
+ * fails is named in `warnings`, and the others are still written.
138
+ *
139
+ * Throws when there is no backup key: nothing is ever written unencrypted.
140
+ */
141
+ backup(options?: ManagerBackupOptions): Promise<BackupData>;
142
+ /**
143
+ * Stage a restore of the databases in a backup, for the next start. Each
144
+ * backed-up database is opened under the backup key, integrity-checked, and
145
+ * copied into `<live>.staged` under this site's database key. All or
146
+ * nothing: if any fails, none is staged. A database the backup does not
147
+ * contain is left as it is. Live files are never touched here.
148
+ *
149
+ * BackupManager takes a fresh backup of the live data before calling this.
150
+ * A backed-up add-on database that no add-on on this site declares is not
151
+ * staged, and admins are told.
152
+ */
153
+ restore(backupData: BackupData, options?: ManagerRestoreOptions): Promise<void>;
154
+ /** The restore staged and waiting for the next start, or null. */
155
+ getStagedRestore(): StagedRestoreManifest | null;
156
+ /**
157
+ * Throw away a staged restore before the restart applies it (#1723, D5):
158
+ * the staged files and the manifest are removed, the live databases are
159
+ * untouched. Recorded first (`backup-restore-cancel`, refuse): a cancel that
160
+ * cannot be recorded does not happen.
161
+ */
162
+ cancelStagedRestore(ctx: ActorContext): Promise<StagedRestoreManifest>;
163
+ /**
164
+ * Tell admins what the start found (#1723): what it did with a staged
165
+ * restore, and a backup key that is missing or the database key. Called by
166
+ * the engine once every manager exists: DatabaseManager starts before the
167
+ * notifications do.
168
+ */
169
+ reportAtBoot(): Promise<void>;
170
+ /** The application database and every declared add-on database (one per owner), on or off. */
171
+ private backupSources;
172
+ /** Put a staged restore in place, before any database opens; loud when it cannot. */
173
+ private applyRestoreAtStart;
174
+ private backupKey;
175
+ /** What is wrong with the backup key: missing, or the database key itself (D3). Null when sound. */
176
+ private backupKeyProblem;
177
+ /** `ngdpbase.database.backup.worker-timeout-ms`, read at use. */
178
+ private workerTimeoutMs;
179
+ /** Degrade for one cause; the dashboard shows every cause still standing. */
180
+ private fault;
181
+ private clearFault;
182
+ private notify;
84
183
  /** Whether every open database passes SQLite's quick check; true when none is configured. */
85
184
  integrityOk(): boolean;
86
185
  /** Closes the add-on databases, then the application database. */