@jwilleke/ngdpbase-types 5.20.0 → 5.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/packages/ngdpbase-types/index.d.ts +5 -0
- package/lib/src/mail/MailProvider.d.ts +2 -0
- package/lib/src/managers/AddonsManager.d.ts +3 -8
- package/lib/src/managers/AttachmentManager.d.ts +64 -12
- package/lib/src/managers/AuthManager.d.ts +24 -4
- package/lib/src/managers/BackgroundJobManager.d.ts +71 -1
- package/lib/src/managers/BaseManager.d.ts +20 -0
- package/lib/src/managers/BaseManager.js +9 -0
- package/lib/src/managers/CommentManager.d.ts +10 -1
- package/lib/src/managers/ConfigurationManager.d.ts +16 -1
- package/lib/src/managers/DatabaseManager.d.ts +53 -16
- package/lib/src/managers/DatabaseManager.js +172 -38
- package/lib/src/managers/EmailManager.d.ts +42 -0
- package/lib/src/managers/FootnoteManager.d.ts +10 -1
- package/lib/src/managers/NotificationManager.d.ts +13 -0
- package/lib/src/managers/OrganizationManager.d.ts +40 -6
- package/lib/src/managers/PersonManager.d.ts +106 -9
- package/lib/src/managers/RoleManager.d.ts +19 -1
- package/lib/src/managers/UserManager.d.ts +15 -0
- package/lib/src/providers/BaseAttachmentProvider.d.ts +14 -0
- package/lib/src/providers/BasicAttachmentProvider.d.ts +26 -2
- package/lib/src/providers/SqliteDatabaseProvider.d.ts +12 -0
- package/lib/src/providers/SqliteDatabaseProvider.js +50 -7
- package/lib/src/providers/SqliteJobStateProvider.d.ts +10 -0
- package/lib/src/providers/attachmentFilesBackup.d.ts +52 -0
- package/lib/src/providers/sqliteBackup.d.ts +6 -0
- package/lib/src/providers/sqliteBackup.js +16 -6
- package/lib/src/security/permissions.generated.d.ts +4 -0
- package/lib/src/types/Asset.d.ts +10 -0
- package/lib/src/types/Config.d.ts +11 -0
- package/lib/src/types/Organization.d.ts +7 -0
- package/lib/src/types/Person.d.ts +27 -3
- package/lib/src/types/PersonProvider.d.ts +6 -0
- package/lib/src/types/WikiEngine.d.ts +9 -21
- package/lib/src/utils/addonConfigLayer.d.ts +19 -0
- package/lib/src/utils/addonConfigLayer.js +45 -3
- package/lib/src/utils/addonPageSync.d.ts +24 -0
- package/lib/src/utils/administrators.d.ts +21 -0
- package/lib/src/utils/auditEventNames.generated.d.ts +4 -0
- package/lib/src/utils/auditEventNames.generated.js +4 -0
- package/lib/src/utils/backupCrypto.d.ts +28 -0
- package/lib/src/utils/backupKey.d.ts +23 -0
- package/lib/src/utils/backupKey.js +37 -0
- package/lib/src/utils/configPaths.d.ts +55 -0
- package/lib/src/utils/configPaths.js +133 -0
- package/lib/src/utils/contactAccess.d.ts +58 -0
- package/lib/src/utils/databaseKey.d.ts +7 -0
- package/lib/src/utils/databaseKey.js +7 -0
- package/lib/src/utils/directoryBackup.d.ts +78 -0
- package/lib/src/utils/emailForLog.d.ts +20 -0
- package/lib/src/utils/maintenanceState.d.ts +153 -0
- package/lib/src/utils/pluginFormatters.d.ts +6 -1
- package/lib/src/utils/pluginFormatters.js +5 -2
- package/lib/src/utils/privateStoreTakeout.d.ts +2 -2
- package/lib/src/utils/redactSecrets.js +31 -11
- package/lib/src/utils/sensitiveValues.d.ts +49 -3
- package/lib/src/utils/sensitiveValues.js +139 -11
- package/lib/src/utils/siteRestore.d.ts +110 -0
- package/lib/src/utils/siteRestore.js +309 -0
- package/lib/src/utils/treeArchive.d.ts +44 -0
- package/lib/src/utils/username.d.ts +7 -1
- package/package.json +1 -1
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* (where the uuid matches the v4 segment of `@id`).
|
|
7
7
|
*
|
|
8
8
|
* `@id` uses the urn:uuid:<v4> form because persons typically lack a
|
|
9
|
-
* canonical web URL. `identifier`
|
|
10
|
-
*
|
|
9
|
+
* canonical web URL. `identifier` is the person's account (`User.username`)
|
|
10
|
+
* when they have one, and `person:<uuid>` when they do not (#1930) — a form
|
|
11
|
+
* no username may take (`ACCOUNTLESS_PERSON_PREFIX`, `src/utils/username.ts`).
|
|
11
12
|
*
|
|
12
13
|
* No embedded role-records — those live in the future person-contacts
|
|
13
14
|
* addon (#602) and are folded in at JSON-LD export time.
|
|
@@ -15,6 +16,18 @@
|
|
|
15
16
|
* @see https://schema.org/Person
|
|
16
17
|
*/
|
|
17
18
|
import type { PostalAddress, ContactPoint } from './Organization.js';
|
|
19
|
+
/**
|
|
20
|
+
* One organization a person is affiliated with (#1930): schema.org
|
|
21
|
+
* `affiliation`, with the person's role or title there when one is known.
|
|
22
|
+
* The organization is named by its `@id`; OrganizationManager lists an
|
|
23
|
+
* organization's people by reading these, so the link is stored once, here.
|
|
24
|
+
*/
|
|
25
|
+
export interface PersonAffiliation {
|
|
26
|
+
/** The organization's `@id`. */
|
|
27
|
+
organization: string;
|
|
28
|
+
/** The person's role or title there (schema.org `roleName`), when known. */
|
|
29
|
+
role?: string;
|
|
30
|
+
}
|
|
18
31
|
/**
|
|
19
32
|
* Canonical Person record.
|
|
20
33
|
*
|
|
@@ -26,7 +39,7 @@ export interface Person {
|
|
|
26
39
|
'@type': 'Person';
|
|
27
40
|
/** urn:uuid:<v4> */
|
|
28
41
|
'@id': string;
|
|
29
|
-
/**
|
|
42
|
+
/** The account's username, or `person:<uuid>` for a person with no account (#1930). */
|
|
30
43
|
identifier: string;
|
|
31
44
|
/** Display name */
|
|
32
45
|
name?: string;
|
|
@@ -46,6 +59,8 @@ export interface Person {
|
|
|
46
59
|
image?: string;
|
|
47
60
|
/** Other URLs that identify this person (LinkedIn, GitHub, etc.) */
|
|
48
61
|
sameAs?: string[];
|
|
62
|
+
/** Organizations this person is affiliated with, each with an optional role (#1930). */
|
|
63
|
+
affiliation?: PersonAffiliation[];
|
|
49
64
|
/** Allow extension fields (additionalProperty, etc.) */
|
|
50
65
|
[key: string]: unknown;
|
|
51
66
|
}
|
|
@@ -54,3 +69,12 @@ export interface Person {
|
|
|
54
69
|
* `@context`/`@type` are immutable; `@id` and `identifier` cannot be changed via update.
|
|
55
70
|
*/
|
|
56
71
|
export type PersonUpdate = Partial<Omit<Person, '@context' | '@type' | '@id' | 'identifier'>>;
|
|
72
|
+
/**
|
|
73
|
+
* What a caller gives `PersonManager.create` (#1930): the person's fields.
|
|
74
|
+
* `identifier` is left out for a person with no account (one is generated);
|
|
75
|
+
* UserManager passes the account's username when it creates the person for a
|
|
76
|
+
* new account.
|
|
77
|
+
*/
|
|
78
|
+
export type PersonInput = PersonUpdate & {
|
|
79
|
+
identifier?: string;
|
|
80
|
+
};
|
|
@@ -18,6 +18,12 @@ export interface PersonProvider extends BaseProvider {
|
|
|
18
18
|
getByIdentifier(identifier: string): Promise<Person | null>;
|
|
19
19
|
/** Persist a new Person. Returns the stored record. */
|
|
20
20
|
create(person: Person): Promise<Person>;
|
|
21
|
+
/**
|
|
22
|
+
* Write the whole record in place of the one with the same `@id` (#1930).
|
|
23
|
+
* The only write that may change `identifier`: PersonManager uses it to join
|
|
24
|
+
* a person to an account.
|
|
25
|
+
*/
|
|
26
|
+
replace(person: Person): Promise<Person>;
|
|
21
27
|
/** Apply a partial update by `@id`. Returns the updated record, or null if not found. */
|
|
22
28
|
update(id: string, patch: PersonUpdate): Promise<Person | null>;
|
|
23
29
|
/** Delete by `@id`. Returns true if a record was removed. */
|
|
@@ -8,6 +8,7 @@ import { WikiConfig } from './Config.js';
|
|
|
8
8
|
import type { Logger } from 'winston';
|
|
9
9
|
import type { Application, Request, RequestHandler, Router, RouterOptions } from 'express';
|
|
10
10
|
import type express from 'express';
|
|
11
|
+
import type { MaintenanceState } from '../utils/maintenanceState.js';
|
|
11
12
|
/**
|
|
12
13
|
* All known manager names as a union type
|
|
13
14
|
* Use with getManager() for type-safe access when combined with explicit type parameter
|
|
@@ -95,28 +96,15 @@ export interface WikiEngine {
|
|
|
95
96
|
*/
|
|
96
97
|
shutdown(): Promise<void>;
|
|
97
98
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* Also a site-critical add-on that did not load (#1883, operator
|
|
106
|
-
* 2026-10-18): its fix may lie outside the UI (a key in the instance .env),
|
|
107
|
-
* and the admin notification says so.
|
|
108
|
-
*/
|
|
109
|
-
blockConfiguration(reason: string, options?: {
|
|
110
|
-
publicMessage?: string;
|
|
111
|
-
}): void;
|
|
112
|
-
/** Configuration values that could not be used, in the order found (#1152). */
|
|
113
|
-
getBlockingConditions(): readonly string[];
|
|
114
|
-
/**
|
|
115
|
-
* What visitors see in place of the reasons, when a reason was recorded with
|
|
116
|
-
* a `publicMessage` (#1883: a site-critical add-on that did not load); null
|
|
117
|
-
* when every reason may be shown.
|
|
99
|
+
* Whether the site is in maintenance mode, and why (#1906): the one door.
|
|
100
|
+
* A manager that needs the site closed calls `maintenance.enter(reason,
|
|
101
|
+
* { message?, estimatedDuration? })`, and `maintenance.leave(reason)` when
|
|
102
|
+
* the reason clears. Reasons are for administrators only; visitors get
|
|
103
|
+
* `maintenance.visitorView(configManager)`. Replaces `blockConfiguration`
|
|
104
|
+
* (#1152), still recorded rather than thrown so `initialize()` finishes and
|
|
105
|
+
* `/admin`, `/login` and `/logout` stay reachable.
|
|
118
106
|
*/
|
|
119
|
-
|
|
107
|
+
readonly maintenance: MaintenanceState;
|
|
120
108
|
/**
|
|
121
109
|
* Get all registered manager names
|
|
122
110
|
* @returns Array of manager names
|
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
* addon's own file and from the operator's, which wins either way, and kept
|
|
47
47
|
* apart from the merge so a refused addon's value is still known. A value that
|
|
48
48
|
* is not exactly `true` or `false` refuses the addon and counts as `true`.
|
|
49
|
+
*
|
|
50
|
+
* A fourth, `ngdpbase.config.sensitive-values` (#1889): an enabled addon may
|
|
51
|
+
* classify only its own keys, `ngdpbase.addons.<slug>` and what is under it
|
|
52
|
+
* (paths inside its values included). An entry for any other key refuses the
|
|
53
|
+
* addon, since it could unhide core's or another addon's secret.
|
|
49
54
|
*/
|
|
50
55
|
import { type AddonDatabaseDeclaration } from './addonDatabaseRules.js';
|
|
51
56
|
export declare const ADDONS_PATH_KEY = "ngdpbase.managers.addons-manager.addons-path";
|
|
@@ -100,6 +105,20 @@ export interface AddonDatabaseLayer {
|
|
|
100
105
|
* file: a value there for a declaration's key is ignored, and said so.
|
|
101
106
|
*/
|
|
102
107
|
export declare function addonDatabaseLayer(addons: readonly AddonDefaults[], custom: Record<string, unknown>): AddonDatabaseLayer;
|
|
108
|
+
export interface AddonSensitiveValuesLayer {
|
|
109
|
+
/** Addons with an entry for a key that is not their own, by slug, with why: never loaded. */
|
|
110
|
+
refused: Map<string, string>;
|
|
111
|
+
problems: string[];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Which values an addon may classify (#1889): its own, `ngdpbase.addons.<slug>`
|
|
115
|
+
* and anything under it, top-level keys and paths alike. An entry for core's
|
|
116
|
+
* keys or another addon's could unhide them (`false`, or `sensitive` for a
|
|
117
|
+
* `secret`), so it refuses the addon, as a database declaration under another
|
|
118
|
+
* slug does (#1860): it is not loaded and none of its keys apply. Only an
|
|
119
|
+
* enabled addon's entries reach the merge, so only an enabled addon is checked.
|
|
120
|
+
*/
|
|
121
|
+
export declare function addonSensitiveValuesLayer(addons: readonly AddonDefaults[]): AddonSensitiveValuesLayer;
|
|
103
122
|
/** The configuration key that makes an addon site-critical (#1883). */
|
|
104
123
|
export declare function addonSiteCriticalKey(slug: string): string;
|
|
105
124
|
export interface AddonSiteCriticalLayer {
|
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
* addon's own file and from the operator's, which wins either way, and kept
|
|
47
47
|
* apart from the merge so a refused addon's value is still known. A value that
|
|
48
48
|
* is not exactly `true` or `false` refuses the addon and counts as `true`.
|
|
49
|
+
*
|
|
50
|
+
* A fourth, `ngdpbase.config.sensitive-values` (#1889): an enabled addon may
|
|
51
|
+
* classify only its own keys, `ngdpbase.addons.<slug>` and what is under it
|
|
52
|
+
* (paths inside its values included). An entry for any other key refuses the
|
|
53
|
+
* addon, since it could unhide core's or another addon's secret.
|
|
49
54
|
*/
|
|
50
55
|
import fs from 'fs';
|
|
51
56
|
import path from 'path';
|
|
@@ -53,6 +58,7 @@ import { isDeepStrictEqual } from 'util';
|
|
|
53
58
|
import { configFilePaths, deepMergeConfigs, deepMergeObjects, instanceDataFolder, isPlainObject, readConfigFilesSync } from './configFiles.js';
|
|
54
59
|
import { configuredInstallSlugs, findNodeModulesDir, installedAddonsDir, matchNpmPackageDirs, resolveAddonSlug, splitAddonsPath } from './addonsPathResolver.js';
|
|
55
60
|
import { lockedAddonDirsSync } from './addonLock.js';
|
|
61
|
+
import { LEGACY_SECRET_KEYS_KEY, SENSITIVE_VALUES_KEY } from './sensitiveValues.js';
|
|
56
62
|
import { parsePrivatestore } from './privateStorePath.js';
|
|
57
63
|
import { OWNER_DECIDED_FIELDS, SYSTEM_CATEGORY_KEY, systemCategoryValueProblems } from './systemCategoryRules.js';
|
|
58
64
|
import { addonDatabaseKey, addonDatabaseKeySlug, addonDatabaseProblems } from './addonDatabaseRules.js';
|
|
@@ -272,6 +278,41 @@ export function addonDatabaseLayer(addons, custom) {
|
|
|
272
278
|
}
|
|
273
279
|
return layer;
|
|
274
280
|
}
|
|
281
|
+
/** Every entry an addon's file names in a sensitive-values map or the legacy list, whatever its level. */
|
|
282
|
+
function sensitiveEntriesOf(defaults) {
|
|
283
|
+
const names = [];
|
|
284
|
+
for (const listKey of [SENSITIVE_VALUES_KEY, LEGACY_SECRET_KEYS_KEY]) {
|
|
285
|
+
const declared = defaults[listKey];
|
|
286
|
+
if (Array.isArray(declared))
|
|
287
|
+
names.push(...declared.filter((v) => typeof v === 'string'));
|
|
288
|
+
else if (isPlainObject(declared))
|
|
289
|
+
names.push(...Object.keys(declared));
|
|
290
|
+
}
|
|
291
|
+
return names;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Which values an addon may classify (#1889): its own, `ngdpbase.addons.<slug>`
|
|
295
|
+
* and anything under it, top-level keys and paths alike. An entry for core's
|
|
296
|
+
* keys or another addon's could unhide them (`false`, or `sensitive` for a
|
|
297
|
+
* `secret`), so it refuses the addon, as a database declaration under another
|
|
298
|
+
* slug does (#1860): it is not loaded and none of its keys apply. Only an
|
|
299
|
+
* enabled addon's entries reach the merge, so only an enabled addon is checked.
|
|
300
|
+
*/
|
|
301
|
+
export function addonSensitiveValuesLayer(addons) {
|
|
302
|
+
const layer = { refused: new Map(), problems: [] };
|
|
303
|
+
for (const addon of addons) {
|
|
304
|
+
if (!addon.enabled)
|
|
305
|
+
continue;
|
|
306
|
+
const own = `ngdpbase.addons.${addon.slug}`;
|
|
307
|
+
const foreign = sensitiveEntriesOf(addon.defaults).filter((entry) => entry.trim() !== own && !entry.trim().startsWith(`${own}.`));
|
|
308
|
+
if (foreign.length === 0)
|
|
309
|
+
continue;
|
|
310
|
+
const reason = `its sensitive-values entries ${foreign.map((e) => `'${e}'`).join(', ')} are not its own; an add-on classifies only ${own} and what is under it`;
|
|
311
|
+
layer.refused.set(addon.slug, reason);
|
|
312
|
+
layer.problems.push(`${addonProblemSubject(addon.slug, addon.enabled)}: ${reason}; the add-on is not loaded`);
|
|
313
|
+
}
|
|
314
|
+
return layer;
|
|
315
|
+
}
|
|
275
316
|
/** The configuration key that makes an addon site-critical (#1883). */
|
|
276
317
|
export function addonSiteCriticalKey(slug) {
|
|
277
318
|
return `ngdpbase.addons.${slug}.site-critical`;
|
|
@@ -360,9 +401,10 @@ export function mergeWithAddonLayer(shipped, addons, custom) {
|
|
|
360
401
|
const layer = addonCategoryLayer(shipped[SYSTEM_CATEGORY_KEY], customCategories, addons);
|
|
361
402
|
const databaseLayer = addonDatabaseLayer(addons, custom);
|
|
362
403
|
const criticalLayer = addonSiteCriticalLayer(addons, custom);
|
|
404
|
+
const sensitiveLayer = addonSensitiveValuesLayer(addons);
|
|
363
405
|
const siteCritical = criticalLayer.siteCritical;
|
|
364
406
|
const refused = new Map(layer.refused);
|
|
365
|
-
for (const [slug, reason] of [...databaseLayer.refused, ...criticalLayer.refused])
|
|
407
|
+
for (const [slug, reason] of [...databaseLayer.refused, ...criticalLayer.refused, ...sensitiveLayer.refused])
|
|
366
408
|
refused.set(slug, refused.has(slug) ? `${refused.get(slug)}; ${reason}` : reason);
|
|
367
409
|
const databases = databaseLayer.declarations.map((d) => ({ ...d, enabled: d.enabled && !refused.has(d.declaredBy) }));
|
|
368
410
|
const withAddons = addons
|
|
@@ -376,14 +418,14 @@ export function mergeWithAddonLayer(shipped, addons, custom) {
|
|
|
376
418
|
return deepMergeObjects(acc, rest);
|
|
377
419
|
}, shipped);
|
|
378
420
|
if (Object.keys(layer.entries).length === 0) {
|
|
379
|
-
return { merged: deepMergeConfigs(withAddons, custom), problems: [...layer.problems, ...databaseLayer.problems, ...criticalLayer.problems], refused, databases, siteCritical };
|
|
421
|
+
return { merged: deepMergeConfigs(withAddons, custom), problems: [...layer.problems, ...databaseLayer.problems, ...criticalLayer.problems, ...sensitiveLayer.problems], refused, databases, siteCritical };
|
|
380
422
|
}
|
|
381
423
|
const shippedCategories = isPlainObject(withAddons[SYSTEM_CATEGORY_KEY]) ? withAddons[SYSTEM_CATEGORY_KEY] : {};
|
|
382
424
|
const layered = { ...withAddons, [SYSTEM_CATEGORY_KEY]: deepMergeObjects(shippedCategories, layer.entries) };
|
|
383
425
|
const merged = deepMergeObjects(layered, custom);
|
|
384
426
|
const stood = ownerDeclarationsStand(merged[SYSTEM_CATEGORY_KEY], layer, customCategories);
|
|
385
427
|
merged[SYSTEM_CATEGORY_KEY] = stood.categories;
|
|
386
|
-
return { merged: merged, problems: [...layer.problems, ...stood.problems, ...databaseLayer.problems, ...criticalLayer.problems], refused, databases, siteCritical };
|
|
428
|
+
return { merged: merged, problems: [...layer.problems, ...stood.problems, ...databaseLayer.problems, ...criticalLayer.problems, ...sensitiveLayer.problems], refused, databases, siteCritical };
|
|
387
429
|
}
|
|
388
430
|
/**
|
|
389
431
|
* The full merge for a caller with the two files already read (the manager,
|
|
@@ -31,6 +31,30 @@ export declare function evaluateSeededAddonPage(args: {
|
|
|
31
31
|
liveContent: string;
|
|
32
32
|
storedHash: string | undefined;
|
|
33
33
|
}): SeededAddonPageStatus;
|
|
34
|
+
/**
|
|
35
|
+
* How a shipped item's sync status reads on an admin screen: the status words
|
|
36
|
+
* and the edit protection of Admin → Required Pages, which every screen that
|
|
37
|
+
* syncs shipped content shares (`views/_sync-status.ejs`, `views/_sync-button.ejs`;
|
|
38
|
+
* shipped forms, #1918).
|
|
39
|
+
*
|
|
40
|
+
* - `status` — `modified` when the live copy differs from what ships,
|
|
41
|
+
* `current` when it is the same.
|
|
42
|
+
* - `userModified` — edited on this site: Sync does not replace it; Force Sync
|
|
43
|
+
* does, after a confirm.
|
|
44
|
+
*/
|
|
45
|
+
export interface SyncRowState {
|
|
46
|
+
status: 'current' | 'modified';
|
|
47
|
+
userModified: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The one mapping from `evaluateSeededAddonPage`'s answer to the admin screen's
|
|
51
|
+
* words (#1918). `userModifiedFlag` is a page's explicit `user-modified`
|
|
52
|
+
* frontmatter, which marks it edited even when the hash cannot tell.
|
|
53
|
+
*
|
|
54
|
+
* @param seedStatus - `evaluateSeededAddonPage`'s answer for the live copy
|
|
55
|
+
* @param userModifiedFlag - The live copy is explicitly marked edited
|
|
56
|
+
*/
|
|
57
|
+
export declare function syncRowState(seedStatus: SeededAddonPageStatus, userModifiedFlag?: boolean): SyncRowState;
|
|
34
58
|
/**
|
|
35
59
|
* Default edit access for a page ngdpbase ships (#971, #1411): a `general`
|
|
36
60
|
* page is instance-owned and gets no stamp; any other category — system,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who an administrator notification goes to.
|
|
3
|
+
*
|
|
4
|
+
* __An administrator is whoever holds `admin-system`__, asked per user through
|
|
5
|
+
* the policy (#1431 7b) — never a role name. Used by every report that must
|
|
6
|
+
* reach administrators and nobody else: a damaged page (#1431 step 7) and
|
|
7
|
+
* failing outbound email (#1916).
|
|
8
|
+
*
|
|
9
|
+
* An empty answer means "nobody to tell", and the caller must then log only:
|
|
10
|
+
* an empty `targetUsers` list means ALL users in NotificationManager, which
|
|
11
|
+
* would announce an operator problem to every reader.
|
|
12
|
+
*
|
|
13
|
+
* Null when the managers needed to answer are not registered (tests, a
|
|
14
|
+
* half-booted engine) — the caller treats that like "nobody to tell".
|
|
15
|
+
*/
|
|
16
|
+
type EngineLike = {
|
|
17
|
+
getManager: (name: string) => unknown;
|
|
18
|
+
} | null | undefined;
|
|
19
|
+
/** Usernames of every user who holds `admin-system`; null when it cannot be asked. */
|
|
20
|
+
export declare function administratorUsernames(engine: EngineLike): Promise<string[] | null>;
|
|
21
|
+
export {};
|
|
@@ -36,6 +36,8 @@ export declare const AUDIT_EVENT: {
|
|
|
36
36
|
readonly BACKUP_CREATE: "backup-create";
|
|
37
37
|
/** A restore of a backup's databases started from Admin → Backup: which backup and who asked; recorded before the fresh backup and the staging, which cannot proceed without it (#1723) */
|
|
38
38
|
readonly BACKUP_RESTORE: "backup-restore";
|
|
39
|
+
/** A staged restore (site or databases-only) put in place at start: which backup, when it was taken, who started the restore and which parts were applied; written into the restored audit log, so the log itself says a restore happened (#1876) */
|
|
40
|
+
readonly BACKUP_RESTORE_APPLIED: "backup-restore-applied";
|
|
39
41
|
/** A staged database restore cancelled before the restart applied it: which backup, who staged it and who cancelled; recorded first, which cannot proceed without it (#1723) */
|
|
40
42
|
readonly BACKUP_RESTORE_CANCEL: "backup-restore-cancel";
|
|
41
43
|
/** A staged database restore replaced by another before the restart applied it: which backup replaced which, and who asked; recorded first, which cannot proceed without it (#1723) */
|
|
@@ -48,6 +50,8 @@ export declare const AUDIT_EVENT: {
|
|
|
48
50
|
readonly CONFIG_CHANGE: "config-change";
|
|
49
51
|
/** Every custom configuration value discarded; recorded before the reset, which cannot proceed without it */
|
|
50
52
|
readonly CONFIG_RESET: "config-reset";
|
|
53
|
+
/** Contact created, changed, deleted or linked to an account; metadata names the kind (person or organization), the operation and the fields changed, never their values */
|
|
54
|
+
readonly CONTACT_MANAGE: "contact-manage";
|
|
51
55
|
/** A page's footnote list changed — action add, import, transfer, update or delete; page content written on someone's behalf (#1233) */
|
|
52
56
|
readonly FOOTNOTE_EDIT: "footnote-edit";
|
|
53
57
|
/** A background job finished successfully */
|
|
@@ -43,6 +43,8 @@ export const AUDIT_EVENT = {
|
|
|
43
43
|
BACKUP_CREATE: 'backup-create',
|
|
44
44
|
/** A restore of a backup's databases started from Admin → Backup: which backup and who asked; recorded before the fresh backup and the staging, which cannot proceed without it (#1723) */
|
|
45
45
|
BACKUP_RESTORE: 'backup-restore',
|
|
46
|
+
/** A staged restore (site or databases-only) put in place at start: which backup, when it was taken, who started the restore and which parts were applied; written into the restored audit log, so the log itself says a restore happened (#1876) */
|
|
47
|
+
BACKUP_RESTORE_APPLIED: 'backup-restore-applied',
|
|
46
48
|
/** A staged database restore cancelled before the restart applied it: which backup, who staged it and who cancelled; recorded first, which cannot proceed without it (#1723) */
|
|
47
49
|
BACKUP_RESTORE_CANCEL: 'backup-restore-cancel',
|
|
48
50
|
/** A staged database restore replaced by another before the restart applied it: which backup replaced which, and who asked; recorded first, which cannot proceed without it (#1723) */
|
|
@@ -55,6 +57,8 @@ export const AUDIT_EVENT = {
|
|
|
55
57
|
CONFIG_CHANGE: 'config-change',
|
|
56
58
|
/** Every custom configuration value discarded; recorded before the reset, which cannot proceed without it */
|
|
57
59
|
CONFIG_RESET: 'config-reset',
|
|
60
|
+
/** Contact created, changed, deleted or linked to an account; metadata names the kind (person or organization), the operation and the fields changed, never their values */
|
|
61
|
+
CONTACT_MANAGE: 'contact-manage',
|
|
58
62
|
/** A page's footnote list changed — action add, import, transfer, update or delete; page content written on someone's behalf (#1233) */
|
|
59
63
|
FOOTNOTE_EDIT: 'footnote-edit',
|
|
60
64
|
/** A background job finished successfully */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Transform } from 'node:stream';
|
|
2
|
+
/** The first bytes of every encrypted backup file. */
|
|
3
|
+
export declare const BACKUP_FILE_MAGIC: Buffer<ArrayBuffer>;
|
|
4
|
+
/** What an encrypted backup file's name ends with. */
|
|
5
|
+
export declare const ENCRYPTED_SUFFIX = ".enc";
|
|
6
|
+
/** A backup file made under a different backup key than the one set now. */
|
|
7
|
+
export declare class BackupKeyMismatchError extends Error {
|
|
8
|
+
readonly fileFingerprint: string;
|
|
9
|
+
readonly keyFingerprint: string;
|
|
10
|
+
constructor(fileFingerprint: string, keyFingerprint: string);
|
|
11
|
+
}
|
|
12
|
+
/** Whether these bytes (a file's first) are an encrypted backup file. */
|
|
13
|
+
export declare function isEncryptedBackup(head: Buffer): boolean;
|
|
14
|
+
/** Whether the file on disk is an encrypted backup file, read from its first bytes only. */
|
|
15
|
+
export declare function fileIsEncrypted(file: string): Promise<boolean>;
|
|
16
|
+
/** A stream that encrypts what is written to it under `backupKey`. */
|
|
17
|
+
export declare function encryptStream(backupKey: string): Transform;
|
|
18
|
+
/**
|
|
19
|
+
* A stream that decrypts an encrypted backup file written to it. Fails —
|
|
20
|
+
* naming why — on a file that is not one, a different backup key
|
|
21
|
+
* ({@link BackupKeyMismatchError}), any record that does not verify, and a
|
|
22
|
+
* file cut short or with bytes after its last record.
|
|
23
|
+
*/
|
|
24
|
+
export declare function decryptStream(backupKey: string): Transform;
|
|
25
|
+
/** Encrypt a whole buffer (the backup document, already in memory). */
|
|
26
|
+
export declare function encryptBuffer(data: Buffer, backupKey: string): Promise<Buffer>;
|
|
27
|
+
/** Decrypt a whole buffer. */
|
|
28
|
+
export declare function decryptBuffer(data: Buffer, backupKey: string): Promise<Buffer>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment variable holding the key every backup is encrypted under.
|
|
3
|
+
* Its own secret, not the database key: the copy that travels and the copy
|
|
4
|
+
* that stays should not fall to the same compromise. Never a config key.
|
|
5
|
+
*/
|
|
6
|
+
export declare const BACKUP_KEY_ENV = "NGDPBASE_BACKUP_KEY";
|
|
7
|
+
/**
|
|
8
|
+
* A short fingerprint of a backup key (#1723, D3): the first 16 hex digits of
|
|
9
|
+
* an HMAC of a fixed label under the key. Recorded in each backup so a restore
|
|
10
|
+
* under another key says so plainly; it never reveals the key.
|
|
11
|
+
*/
|
|
12
|
+
export declare function backupKeyFingerprint(key: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* How every part left out of a backup for want of the key says so: the
|
|
15
|
+
* message starts with this. BackupManager recognises such a part by it
|
|
16
|
+
* (`isLeftOutForNoKey`): those are covered by its banner and one-time
|
|
17
|
+
* notice, never a notice per backup (operator, #1912).
|
|
18
|
+
*/
|
|
19
|
+
export declare const LEFT_OUT_FOR_NO_KEY = "NGDPBASE_BACKUP_KEY is not set, so ";
|
|
20
|
+
/** Whether a backup warning or error says a part was left out only because there is no backup key. */
|
|
21
|
+
export declare function isLeftOutForNoKey(message: string): boolean;
|
|
22
|
+
/** The backup key as set now, trimmed; '' when there is none. Read at use, never cached. */
|
|
23
|
+
export declare function currentBackupKey(): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The backup key (#1723, #1561, #1876): the one secret every encrypted part of
|
|
3
|
+
* a backup is made under — the database files (SQLCipher, `sqliteBackup.ts`),
|
|
4
|
+
* and the backup document, the file archives and the credentials store
|
|
5
|
+
* (`backupCrypto.ts`). One declaration here; DatabaseManager and the file
|
|
6
|
+
* parts read it from this module.
|
|
7
|
+
*/
|
|
8
|
+
import { createHmac } from 'node:crypto';
|
|
9
|
+
/**
|
|
10
|
+
* The environment variable holding the key every backup is encrypted under.
|
|
11
|
+
* Its own secret, not the database key: the copy that travels and the copy
|
|
12
|
+
* that stays should not fall to the same compromise. Never a config key.
|
|
13
|
+
*/
|
|
14
|
+
export const BACKUP_KEY_ENV = 'NGDPBASE_BACKUP_KEY';
|
|
15
|
+
/**
|
|
16
|
+
* A short fingerprint of a backup key (#1723, D3): the first 16 hex digits of
|
|
17
|
+
* an HMAC of a fixed label under the key. Recorded in each backup so a restore
|
|
18
|
+
* under another key says so plainly; it never reveals the key.
|
|
19
|
+
*/
|
|
20
|
+
export function backupKeyFingerprint(key) {
|
|
21
|
+
return createHmac('sha256', key).update('ngdpbase backup key fingerprint v1').digest('hex').slice(0, 16);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* How every part left out of a backup for want of the key says so: the
|
|
25
|
+
* message starts with this. BackupManager recognises such a part by it
|
|
26
|
+
* (`isLeftOutForNoKey`): those are covered by its banner and one-time
|
|
27
|
+
* notice, never a notice per backup (operator, #1912).
|
|
28
|
+
*/
|
|
29
|
+
export const LEFT_OUT_FOR_NO_KEY = `${BACKUP_KEY_ENV} is not set, so `;
|
|
30
|
+
/** Whether a backup warning or error says a part was left out only because there is no backup key. */
|
|
31
|
+
export function isLeftOutForNoKey(message) {
|
|
32
|
+
return message.startsWith(LEFT_OUT_FOR_NO_KEY);
|
|
33
|
+
}
|
|
34
|
+
/** The backup key as set now, trimmed; '' when there is none. Read at use, never cached. */
|
|
35
|
+
export function currentBackupKey() {
|
|
36
|
+
return (process.env[BACKUP_KEY_ENV] ?? '').trim();
|
|
37
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paths inside a configuration value (#1889).
|
|
3
|
+
*
|
|
4
|
+
* A configuration key names a value; when the value is a map or a list, each
|
|
5
|
+
* thing inside it has a path below the key: its map keys and list indices, in
|
|
6
|
+
* order. `ngdpbase.addons.accounting.accounts` → `checking` → `account-number`
|
|
7
|
+
* is written `ngdpbase.addons.accounting.accounts.checking.account-number`.
|
|
8
|
+
*
|
|
9
|
+
* Two readers walk these paths, and they walk them here, the same way:
|
|
10
|
+
*
|
|
11
|
+
* - `ConfigurationManager` resolves a bare `$NAME` environment reference
|
|
12
|
+
* wherever it sits in a value, naming the full path when the variable is
|
|
13
|
+
* unset;
|
|
14
|
+
* - `sensitiveValues` hides what `ngdpbase.config.sensitive-values` names by
|
|
15
|
+
* a path, where `*` stands for any one map key (or list index).
|
|
16
|
+
*
|
|
17
|
+
* @module utils/configPaths
|
|
18
|
+
*/
|
|
19
|
+
/** A bare, whole-string environment reference: `$NAME`, upper case, digits and `_`. */
|
|
20
|
+
export declare const ENV_REF_PATTERN: RegExp;
|
|
21
|
+
/** The variable a bare `$NAME` reference names, or null when the value is not one. */
|
|
22
|
+
export declare function envRefName(value: unknown): string | null;
|
|
23
|
+
/** One path segment that stands for any map key or list index. */
|
|
24
|
+
export declare const WILDCARD = "*";
|
|
25
|
+
/** A key and the path below it, written as configuration keys are: dot-joined. */
|
|
26
|
+
export declare function pathString(key: string, below: readonly string[]): string;
|
|
27
|
+
/**
|
|
28
|
+
* Visit a value and everything inside it, top first. `visit` returns a
|
|
29
|
+
* replacement for the node, or `undefined` to keep it and go inside. Copy on
|
|
30
|
+
* write: a map or list nothing inside changed is returned as itself, so a read
|
|
31
|
+
* of a value with nothing to replace allocates nothing.
|
|
32
|
+
*/
|
|
33
|
+
export declare function mapConfigValue(value: unknown, visit: (node: unknown, below: readonly string[]) => unknown, below?: string[]): unknown;
|
|
34
|
+
/**
|
|
35
|
+
* The segments of a path entry, or null when it is not a well-formed one: no
|
|
36
|
+
* empty segment, and `*` only as a whole segment with at least two written-out
|
|
37
|
+
* segments before the first one (`ngdpbase.<category>`), since a key is never
|
|
38
|
+
* a wildcard.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parsePathEntry(entry: string): string[] | null;
|
|
41
|
+
/**
|
|
42
|
+
* Whether `entry` names something inside `key`'s value, and the path pattern
|
|
43
|
+
* below the key if so. The key is matched literally: a `*` stands only for a
|
|
44
|
+
* segment below it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function patternBelow(entry: string, key: string): string[] | null;
|
|
47
|
+
/** Whether a path below a key matches a pattern below it: same length, `*` any one segment. */
|
|
48
|
+
export declare function pathMatches(pattern: readonly string[], below: readonly string[]): boolean;
|
|
49
|
+
/** What sits at a path below a value, or undefined. */
|
|
50
|
+
export declare function valueAt(value: unknown, below: readonly string[]): unknown;
|
|
51
|
+
/**
|
|
52
|
+
* The configuration key a path entry lies inside: the longest prefix, ending
|
|
53
|
+
* before the first `*`, for which `has` is true. Null when none is set.
|
|
54
|
+
*/
|
|
55
|
+
export declare function keyOfEntry(entry: string, has: (key: string) => boolean): string | null;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paths inside a configuration value (#1889).
|
|
3
|
+
*
|
|
4
|
+
* A configuration key names a value; when the value is a map or a list, each
|
|
5
|
+
* thing inside it has a path below the key: its map keys and list indices, in
|
|
6
|
+
* order. `ngdpbase.addons.accounting.accounts` → `checking` → `account-number`
|
|
7
|
+
* is written `ngdpbase.addons.accounting.accounts.checking.account-number`.
|
|
8
|
+
*
|
|
9
|
+
* Two readers walk these paths, and they walk them here, the same way:
|
|
10
|
+
*
|
|
11
|
+
* - `ConfigurationManager` resolves a bare `$NAME` environment reference
|
|
12
|
+
* wherever it sits in a value, naming the full path when the variable is
|
|
13
|
+
* unset;
|
|
14
|
+
* - `sensitiveValues` hides what `ngdpbase.config.sensitive-values` names by
|
|
15
|
+
* a path, where `*` stands for any one map key (or list index).
|
|
16
|
+
*
|
|
17
|
+
* @module utils/configPaths
|
|
18
|
+
*/
|
|
19
|
+
/** A bare, whole-string environment reference: `$NAME`, upper case, digits and `_`. */
|
|
20
|
+
export const ENV_REF_PATTERN = /^\$([A-Z_][A-Z0-9_]*)$/;
|
|
21
|
+
/** The variable a bare `$NAME` reference names, or null when the value is not one. */
|
|
22
|
+
export function envRefName(value) {
|
|
23
|
+
return typeof value === 'string' ? (ENV_REF_PATTERN.exec(value)?.[1] ?? null) : null;
|
|
24
|
+
}
|
|
25
|
+
/** One path segment that stands for any map key or list index. */
|
|
26
|
+
export const WILDCARD = '*';
|
|
27
|
+
/** A key and the path below it, written as configuration keys are: dot-joined. */
|
|
28
|
+
export function pathString(key, below) {
|
|
29
|
+
return below.length === 0 ? key : `${key}.${below.join('.')}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Visit a value and everything inside it, top first. `visit` returns a
|
|
33
|
+
* replacement for the node, or `undefined` to keep it and go inside. Copy on
|
|
34
|
+
* write: a map or list nothing inside changed is returned as itself, so a read
|
|
35
|
+
* of a value with nothing to replace allocates nothing.
|
|
36
|
+
*/
|
|
37
|
+
export function mapConfigValue(value, visit, below = []) {
|
|
38
|
+
const replaced = visit(value, below);
|
|
39
|
+
if (replaced !== undefined)
|
|
40
|
+
return replaced;
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
let out = null;
|
|
43
|
+
for (let i = 0; i < value.length; i++) {
|
|
44
|
+
below.push(String(i));
|
|
45
|
+
const next = mapConfigValue(value[i], visit, below);
|
|
46
|
+
below.pop();
|
|
47
|
+
if (next !== value[i]) {
|
|
48
|
+
out ??= value.slice();
|
|
49
|
+
out[i] = next;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out ?? value;
|
|
53
|
+
}
|
|
54
|
+
if (value !== null && typeof value === 'object') {
|
|
55
|
+
let out = null;
|
|
56
|
+
for (const [k, v] of Object.entries(value)) {
|
|
57
|
+
below.push(k);
|
|
58
|
+
const next = mapConfigValue(v, visit, below);
|
|
59
|
+
below.pop();
|
|
60
|
+
if (next !== v) {
|
|
61
|
+
out ??= { ...value };
|
|
62
|
+
out[k] = next;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out ?? value;
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The segments of a path entry, or null when it is not a well-formed one: no
|
|
71
|
+
* empty segment, and `*` only as a whole segment with at least two written-out
|
|
72
|
+
* segments before the first one (`ngdpbase.<category>`), since a key is never
|
|
73
|
+
* a wildcard.
|
|
74
|
+
*/
|
|
75
|
+
export function parsePathEntry(entry) {
|
|
76
|
+
if (typeof entry !== 'string' || entry === '')
|
|
77
|
+
return null;
|
|
78
|
+
const segments = entry.split('.');
|
|
79
|
+
if (segments.some((s) => s === '' || (s.includes(WILDCARD) && s !== WILDCARD)))
|
|
80
|
+
return null;
|
|
81
|
+
const wild = segments.indexOf(WILDCARD);
|
|
82
|
+
if (wild !== -1 && wild < 2)
|
|
83
|
+
return null;
|
|
84
|
+
return segments;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether `entry` names something inside `key`'s value, and the path pattern
|
|
88
|
+
* below the key if so. The key is matched literally: a `*` stands only for a
|
|
89
|
+
* segment below it.
|
|
90
|
+
*/
|
|
91
|
+
export function patternBelow(entry, key) {
|
|
92
|
+
if (!entry.startsWith(`${key}.`))
|
|
93
|
+
return null;
|
|
94
|
+
const segments = parsePathEntry(entry);
|
|
95
|
+
return segments ? segments.slice(key.split('.').length) : null;
|
|
96
|
+
}
|
|
97
|
+
/** Whether a path below a key matches a pattern below it: same length, `*` any one segment. */
|
|
98
|
+
export function pathMatches(pattern, below) {
|
|
99
|
+
if (pattern.length !== below.length)
|
|
100
|
+
return false;
|
|
101
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
102
|
+
if (pattern[i] !== WILDCARD && pattern[i] !== below[i])
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
/** What sits at a path below a value, or undefined. */
|
|
108
|
+
export function valueAt(value, below) {
|
|
109
|
+
let node = value;
|
|
110
|
+
for (const segment of below) {
|
|
111
|
+
if (node === null || typeof node !== 'object')
|
|
112
|
+
return undefined;
|
|
113
|
+
node = node[segment];
|
|
114
|
+
}
|
|
115
|
+
return node;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The configuration key a path entry lies inside: the longest prefix, ending
|
|
119
|
+
* before the first `*`, for which `has` is true. Null when none is set.
|
|
120
|
+
*/
|
|
121
|
+
export function keyOfEntry(entry, has) {
|
|
122
|
+
const segments = parsePathEntry(entry);
|
|
123
|
+
if (!segments)
|
|
124
|
+
return null;
|
|
125
|
+
const wild = segments.indexOf(WILDCARD);
|
|
126
|
+
const longest = wild === -1 ? segments.length - 1 : wild;
|
|
127
|
+
for (let n = longest; n >= 2; n--) {
|
|
128
|
+
const key = segments.slice(0, n).join('.');
|
|
129
|
+
if (has(key))
|
|
130
|
+
return key;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|