@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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contacts door's one decision and its one record (#1930).
|
|
3
|
+
*
|
|
4
|
+
* PersonManager and OrganizationManager are the enforcement points for
|
|
5
|
+
* contacts. Both ask the PDP the same two questions with the caller's
|
|
6
|
+
* context: `contact-read` to list, search or view, `contact-manage` to
|
|
7
|
+
* create, change, delete or link; and both record each change as the
|
|
8
|
+
* `contact-manage` audit event. That rule lives here once, so the two doors
|
|
9
|
+
* cannot drift (operator, 2026-10-20).
|
|
10
|
+
*
|
|
11
|
+
* A refusal is thrown as {@link ContactRefusedError}, carrying the decision.
|
|
12
|
+
* A route turns it into the answer with `ApiContext.enforceDeny(err.permission,
|
|
13
|
+
* err.decision)`, the one place a refusal is shaped (#1431); the door never
|
|
14
|
+
* builds an HTTP answer itself.
|
|
15
|
+
*/
|
|
16
|
+
import { type ActorContext } from '../context/ActorContext.js';
|
|
17
|
+
import type { CorePermission } from '../security/permissions.generated.js';
|
|
18
|
+
import type { Decision } from '../types/Policy.js';
|
|
19
|
+
type EngineLike = {
|
|
20
|
+
getManager: <T = unknown>(name: string) => T | null | undefined;
|
|
21
|
+
};
|
|
22
|
+
/** What a contacts door asks: `read` to list, search or view; `manage` to change. */
|
|
23
|
+
export declare const CONTACT_PERMISSION: {
|
|
24
|
+
readonly read: "contact-read";
|
|
25
|
+
readonly manage: "contact-manage";
|
|
26
|
+
};
|
|
27
|
+
/** A contacts door said no. `permission` is what was refused; `decision` is the PDP's answer. */
|
|
28
|
+
export declare class ContactRefusedError extends Error {
|
|
29
|
+
readonly permission: string;
|
|
30
|
+
readonly decision: Decision;
|
|
31
|
+
constructor(permission: string, decision: Decision);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Throws {@link ContactRefusedError} unless `ctx` may do `kind` (`read` asks
|
|
35
|
+
* `contact-read`, `manage` asks `contact-manage`), and each of `also` (a link
|
|
36
|
+
* to an account also asks `user-edit`). A missing context or decision point
|
|
37
|
+
* refuses: closed, never open.
|
|
38
|
+
*/
|
|
39
|
+
export declare function requireContactPermission(engine: EngineLike, ctx: ActorContext, kind: keyof typeof CONTACT_PERMISSION, also?: readonly CorePermission[]): Promise<void>;
|
|
40
|
+
/** What a contacts record says was done. Names fields, never their values. */
|
|
41
|
+
export interface ContactChange {
|
|
42
|
+
kind: 'person' | 'organization';
|
|
43
|
+
operation: 'create' | 'update' | 'delete' | 'link';
|
|
44
|
+
id: string;
|
|
45
|
+
fields?: string[];
|
|
46
|
+
/** For a link: the account joined to the person. */
|
|
47
|
+
account?: string;
|
|
48
|
+
/** For a link: how it was confirmed, `admin` or `email`. */
|
|
49
|
+
via?: 'admin' | 'email';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Record a contacts change as `contact-manage`, under the caller's context.
|
|
53
|
+
* `continue`: a lost record is counted, the change stands. A system context
|
|
54
|
+
* goes through `recordSystemAction`, so the bootstrap admin's person, made
|
|
55
|
+
* before the audit sink is up, is recorded once it is.
|
|
56
|
+
*/
|
|
57
|
+
export declare function recordContactChange(engine: EngineLike, ctx: ActorContext, change: ContactChange): Promise<void>;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment variable holding the SQLCipher key (#1536). Never a config
|
|
3
|
+
* key: config is readable from the admin screens. Declared here, with no
|
|
4
|
+
* dependencies, so the boot (`bootstrap-env.ts`) can name it without loading
|
|
5
|
+
* DatabaseManager; DatabaseManager re-exports it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DATABASE_KEY_ENV = "NGDPBASE_DATABASE_KEY";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment variable holding the SQLCipher key (#1536). Never a config
|
|
3
|
+
* key: config is readable from the admin screens. Declared here, with no
|
|
4
|
+
* dependencies, so the boot (`bootstrap-env.ts`) can name it without loading
|
|
5
|
+
* DatabaseManager; DatabaseManager re-exports it.
|
|
6
|
+
*/
|
|
7
|
+
export const DATABASE_KEY_ENV = 'NGDPBASE_DATABASE_KEY';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { BackupData, ManagerBackupOptions, ManagerRestoreOptions } from '../managers/BaseManager.js';
|
|
2
|
+
import { siteRestoreFile, type PartOutcome } from './siteRestore.js';
|
|
3
|
+
/** A folder part's `data` in the backup document. */
|
|
4
|
+
export interface DirectoryBackupData {
|
|
5
|
+
format: 1;
|
|
6
|
+
/** The archive beside the document: `<stem>.<part>.tar`, or `.tar.enc` when encrypted. */
|
|
7
|
+
archive: string;
|
|
8
|
+
files: number;
|
|
9
|
+
bytes: number;
|
|
10
|
+
/** `backupKeyFingerprint()` of the key the archive is encrypted under; absent when it is not encrypted. */
|
|
11
|
+
keyFingerprint?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Whether `name` is a bare archive name in the backup directory, never a path. */
|
|
14
|
+
export declare function isArchiveName(name: unknown): name is string;
|
|
15
|
+
/**
|
|
16
|
+
* The name and key for one archive of a backup: encrypted under the backup
|
|
17
|
+
* key when it is set (#1561), plain otherwise. BackupManager says, once per
|
|
18
|
+
* backup, that a backup without the key is not encrypted.
|
|
19
|
+
*/
|
|
20
|
+
export declare function archiveTarget(stem: string, part: string): {
|
|
21
|
+
archive: string;
|
|
22
|
+
backupKey?: string;
|
|
23
|
+
keyFingerprint?: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The key to read an archive of a backup with, checked against the
|
|
27
|
+
* fingerprint the backup recorded: a backup made under another key is
|
|
28
|
+
* refused before anything is staged. None for a backup that was not
|
|
29
|
+
* encrypted, which is then read as the plain archive it says it is.
|
|
30
|
+
*/
|
|
31
|
+
export declare function archiveReadKey(manager: string, data: {
|
|
32
|
+
keyFingerprint?: string;
|
|
33
|
+
}): string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Back the folder up beside the document BackupManager is writing. Throws
|
|
36
|
+
* outside BackupManager: the files go beside a document, never into it.
|
|
37
|
+
* `exclude` leaves out a path relative to the folder (posix form).
|
|
38
|
+
*/
|
|
39
|
+
export declare function backupDirectoryPart(manager: string, part: string, folder: string | null, options?: ManagerBackupOptions, extra?: {
|
|
40
|
+
exclude?: (relative: string) => boolean;
|
|
41
|
+
}): Promise<BackupData>;
|
|
42
|
+
/**
|
|
43
|
+
* Stage the folder from a backup for the next start: the archive is
|
|
44
|
+
* extracted to `<folder>.staged`, and the swap recorded in the site restore.
|
|
45
|
+
* Throws — staging nothing — outside a site restore, for a backup without
|
|
46
|
+
* this part, or for an archive that is missing, unsafe, or under another key.
|
|
47
|
+
*/
|
|
48
|
+
export declare function stageDirectoryPart(manager: string, folder: string | null, backupData: BackupData, options?: ManagerRestoreOptions): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Put a staged restore of the folder in place, at start, before the manager
|
|
51
|
+
* reads it. Loud when something was staged but not applied.
|
|
52
|
+
*/
|
|
53
|
+
export declare function applyDirectoryPartAtStart(manager: string, folder: string, configManager: Parameters<typeof siteRestoreFile>[0]): PartOutcome | null;
|
|
54
|
+
/**
|
|
55
|
+
* Back one file up beside the document, encrypted under the backup key, as
|
|
56
|
+
* `<stem>.<part>.tar.enc`. With `requireKey` and no key the file is left out:
|
|
57
|
+
* `data` is null and a warning says why, so BackupManager reports the backup
|
|
58
|
+
* incomplete (#1876: the credentials store is never in a backup unencrypted).
|
|
59
|
+
*/
|
|
60
|
+
export declare function backupFilePart(manager: string, part: string, file: string | null, options: ManagerBackupOptions | undefined, { requireKey, what }: {
|
|
61
|
+
requireKey: boolean;
|
|
62
|
+
what: string;
|
|
63
|
+
}): Promise<{
|
|
64
|
+
data: DirectoryBackupData | null;
|
|
65
|
+
warnings: string[];
|
|
66
|
+
note?: string;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Stage one file from a backup as `<file>.staged`, and record the swap. When
|
|
70
|
+
* the file was absent at backup time the archive holds nothing, and
|
|
71
|
+
* `whenAbsent` is staged in its place (an empty store), so the restore still
|
|
72
|
+
* puts the site back as it was.
|
|
73
|
+
*/
|
|
74
|
+
export declare function stageFilePart(manager: string, file: string, data: DirectoryBackupData, options: ManagerRestoreOptions | undefined, { whenAbsent }: {
|
|
75
|
+
whenAbsent: string;
|
|
76
|
+
}): Promise<void>;
|
|
77
|
+
/** Put a staged restore of one file in place, at start, before the manager reads it. */
|
|
78
|
+
export declare function applyFilePartAtStart(manager: string, file: string, configManager: Parameters<typeof siteRestoreFile>[0]): PartOutcome | null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* emailForLog — the one way an email address reaches the server log (#1916).
|
|
3
|
+
*
|
|
4
|
+
* Rule (operator, 2026-10-20): a full email address never reaches the server
|
|
5
|
+
* log. A log line names the address's domain only. Audit records are not the
|
|
6
|
+
* server log and keep full addresses; do not use these helpers on them.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The domain(s) of one address or a comma-separated list, lower-cased:
|
|
10
|
+
* `"Bob <bob@Example.org>, ann@x.io"` → `"example.org,x.io"`. A value
|
|
11
|
+
* without an `@` (or an empty one) reads as `unknown`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function emailDomain(to: string | null | undefined): string;
|
|
14
|
+
/**
|
|
15
|
+
* Any address inside free text (an SMTP error often quotes the rejected one)
|
|
16
|
+
* cut to its domain: `bob@example.org` → `…@example.org`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function redactEmailAddresses(text: string): string;
|
|
19
|
+
/** An error's message, with any address in it cut to its domain. */
|
|
20
|
+
export declare function errorForLog(err: unknown): string;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maintenance mode: the one owner (#1906), and the settings it reads (#1147).
|
|
3
|
+
*
|
|
4
|
+
* {@link MaintenanceState} (at the end of this file) is the door every manager
|
|
5
|
+
* uses to put the site in maintenance mode. {@link resolveMaintenanceState}
|
|
6
|
+
* reads the administrator's settings for it from configuration.
|
|
7
|
+
*
|
|
8
|
+
* #1147, the history behind the settings reader:
|
|
9
|
+
*
|
|
10
|
+
* One switch, one state. Before this module, maintenance mode had two readers
|
|
11
|
+
* on two different sources and the admin toggle wrote to only one of them:
|
|
12
|
+
*
|
|
13
|
+
* - the gate middleware read `engine.config.features.maintenance`, an
|
|
14
|
+
* in-memory object that configuration never populates — `Engine.initialize`
|
|
15
|
+
* will not overwrite `this.config` once set (`core/Engine.ts:63`), and the
|
|
16
|
+
* constructor sets it to `{}`, so it stayed empty for the process lifetime
|
|
17
|
+
* - `PolicyInformationPoint.checkContextRestrictions` read the documented keys through
|
|
18
|
+
* `ConfigurationManager`
|
|
19
|
+
*
|
|
20
|
+
* So the shipped `ngdpbase.features.maintenance.enabled` key did not gate the
|
|
21
|
+
* site, the toggle did not survive a restart, and the two halves could
|
|
22
|
+
* disagree at runtime — the gate serving traffic normally while the ACL
|
|
23
|
+
* evaluator denied on maintenance grounds, or the reverse.
|
|
24
|
+
*
|
|
25
|
+
* Every reader now resolves through here, which is what makes "one switch, one
|
|
26
|
+
* state" provable rather than asserted.
|
|
27
|
+
*/
|
|
28
|
+
/** Configuration read function, matching `ConfigurationManager.getProperty`. */
|
|
29
|
+
export type ReadProperty = (key: string, fallback: unknown) => unknown;
|
|
30
|
+
/** The administrator's maintenance settings, as configured. */
|
|
31
|
+
export interface MaintenanceSettings {
|
|
32
|
+
/** Whether the admin switch was left on: the persisted record of {@link ADMIN_SWITCH_REASON}. */
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
/** Whether administrators may still reach the instance while it is closed. */
|
|
35
|
+
allowAdmins: boolean;
|
|
36
|
+
/** Shown on the maintenance page. Never empty. */
|
|
37
|
+
message: string;
|
|
38
|
+
/** Operator-supplied estimate, or null when none is configured. */
|
|
39
|
+
estimatedDuration: string | null;
|
|
40
|
+
}
|
|
41
|
+
export declare const MAINTENANCE_ENABLED_KEY = "ngdpbase.features.maintenance.enabled";
|
|
42
|
+
export declare const MAINTENANCE_ALLOW_ADMINS_KEY = "ngdpbase.features.maintenance.allow-admins";
|
|
43
|
+
export declare const MAINTENANCE_MESSAGE_KEY = "ngdpbase.features.maintenance.message";
|
|
44
|
+
export declare const MAINTENANCE_DURATION_KEY = "ngdpbase.features.maintenance.estimated-duration";
|
|
45
|
+
/**
|
|
46
|
+
* Read the administrator's maintenance settings from configuration.
|
|
47
|
+
*
|
|
48
|
+
* Read when asked rather than cached at boot, so a changed message, estimate
|
|
49
|
+
* or allow-admins applies at once. Whether the site IS in maintenance mode is
|
|
50
|
+
* {@link MaintenanceState}'s, not this: `enabled` here is only the admin
|
|
51
|
+
* switch's persisted record (#1906).
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveMaintenanceState(get: ReadProperty): MaintenanceSettings;
|
|
54
|
+
/**
|
|
55
|
+
* #1883's key, retired by #1906. Still read when a site set it, so an instance
|
|
56
|
+
* that wrote its own outage wording for v5.20.0 keeps it — but only while the
|
|
57
|
+
* site has not set {@link MAINTENANCE_MESSAGE_KEY} itself. No shipped default.
|
|
58
|
+
*/
|
|
59
|
+
export declare const LEGACY_MESSAGE_KEY = "ngdpbase.managers.addons-manager.site-critical.public-message";
|
|
60
|
+
/**
|
|
61
|
+
* Why the admin switch holds the site in maintenance mode: the one reason an
|
|
62
|
+
* administrator enters and leaves, through the toggle on the admin dashboard.
|
|
63
|
+
*/
|
|
64
|
+
export declare const ADMIN_SWITCH_REASON = "An administrator turned maintenance mode on (Admin Dashboard \u2192 Enable Maintenance; ngdpbase.features.maintenance.enabled).";
|
|
65
|
+
/** What a manager may say when it puts the site in maintenance mode. */
|
|
66
|
+
export interface EnterOptions {
|
|
67
|
+
/** What visitors are told, in place of the configured message. */
|
|
68
|
+
message?: string;
|
|
69
|
+
/** What visitors are told about how long, in place of the configured estimate. */
|
|
70
|
+
estimatedDuration?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Whether administrators may use the whole site while this reason holds.
|
|
73
|
+
* False (the default) for a failure: everyone, administrators included, gets
|
|
74
|
+
* only `/admin`, `/login` and `/logout`, because the rest of the site cannot
|
|
75
|
+
* be trusted to work. True only for the admin switch, where nothing is broken.
|
|
76
|
+
*/
|
|
77
|
+
adminsMayEnter?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The configuration reads the visitor view needs — `ConfigurationManager`'s
|
|
81
|
+
* own methods, so callers pass the manager itself. `getCustomProperty` tells
|
|
82
|
+
* "the site set this" from "this is the shipped default", which decides
|
|
83
|
+
* whether the legacy key may still speak.
|
|
84
|
+
*/
|
|
85
|
+
export interface MaintenanceConfig {
|
|
86
|
+
getProperty(key: string, fallback: unknown): unknown;
|
|
87
|
+
getCustomProperty?(key: string): unknown;
|
|
88
|
+
}
|
|
89
|
+
/** What a visitor is shown while the site is in maintenance mode. It carries no reason. */
|
|
90
|
+
export interface MaintenanceVisitorView {
|
|
91
|
+
message: string;
|
|
92
|
+
estimatedDuration: string | null;
|
|
93
|
+
/** Whether a signed-in administrator may use the whole site. */
|
|
94
|
+
adminsMayEnter: boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Whether the site is in maintenance mode, and why (#1906, operator 2026-10-19).
|
|
98
|
+
*
|
|
99
|
+
* The one door. Whatever needs the site closed calls {@link enter} with a
|
|
100
|
+
* reason for administrators and, if it has one, its own message for visitors;
|
|
101
|
+
* {@link leave} reopens the site when that reason clears. AuditManager
|
|
102
|
+
* (refuse-boot), AddonsManager (a site-critical add-on that did not load),
|
|
103
|
+
* DatabaseManager (the application database cannot be opened), the boot-time
|
|
104
|
+
* configuration checks in `app.ts` (TLS files, egress ranges) and the admin
|
|
105
|
+
* toggle all go through here. `engine.blockConfiguration` is retired.
|
|
106
|
+
*
|
|
107
|
+
* __Where the state lives.__ One instance per Engine, created in its
|
|
108
|
+
* constructor (`engine.maintenance`), in memory. Not in configuration: the
|
|
109
|
+
* boot paths call `enter` while managers are still initialising —
|
|
110
|
+
* DatabaseManager before most of the others, AuditManager before the
|
|
111
|
+
* notification layer — and a failure that is only true for this run must not
|
|
112
|
+
* be written into the operator's file, where it would outlive the failure.
|
|
113
|
+
* Not a module global: each Engine, and so each test, has its own.
|
|
114
|
+
*
|
|
115
|
+
* The admin switch is the one reason that must survive a restart (#1147), so
|
|
116
|
+
* the toggle also persists `ngdpbase.features.maintenance.enabled`, and
|
|
117
|
+
* {@link restoreSwitch} enters it again at boot. That key is the reason's
|
|
118
|
+
* record; nothing reads it to decide whether the site is open.
|
|
119
|
+
*
|
|
120
|
+
* Reasons are for administrators only: the log, the admin dashboard, and the
|
|
121
|
+
* notifications the managers send. Visitors, and `/health/readiness`, get
|
|
122
|
+
* {@link visitorView}, which carries none.
|
|
123
|
+
*/
|
|
124
|
+
export declare class MaintenanceState {
|
|
125
|
+
private readonly entries;
|
|
126
|
+
/** Put the site in maintenance mode for `reason`. Entering a held reason again replaces its options. */
|
|
127
|
+
enter(reason: string, options?: EnterOptions): void;
|
|
128
|
+
/** Clear `reason`. The site reopens when no reason is left. False when it was not held. */
|
|
129
|
+
leave(reason: string): boolean;
|
|
130
|
+
/** Whether any reason holds the site in maintenance mode. */
|
|
131
|
+
isOn(): boolean;
|
|
132
|
+
/** Whether `reason` currently holds it. */
|
|
133
|
+
has(reason: string): boolean;
|
|
134
|
+
/** Every reason, in the order entered. For administrators only. */
|
|
135
|
+
reasons(): readonly string[];
|
|
136
|
+
/**
|
|
137
|
+
* Whether a reason holds that keeps everyone, administrators included, to
|
|
138
|
+
* `/admin`, `/login` and `/logout`: a failure, rather than the admin switch.
|
|
139
|
+
*/
|
|
140
|
+
isClosedToAdmins(): boolean;
|
|
141
|
+
/** Enter the admin switch again when the configuration says it was left on (#1147). */
|
|
142
|
+
restoreSwitch(config: MaintenanceConfig | null | undefined): void;
|
|
143
|
+
/**
|
|
144
|
+
* What visitors see; null when the site is open.
|
|
145
|
+
*
|
|
146
|
+
* The message is the first reason's own, when a manager passed one; else
|
|
147
|
+
* `ngdpbase.features.maintenance.message` as the site set it; else the
|
|
148
|
+
* retired {@link LEGACY_MESSAGE_KEY}, when the site set that; else the
|
|
149
|
+
* shipped message. The estimate: the first reason's own, else
|
|
150
|
+
* `ngdpbase.features.maintenance.estimated-duration`, else none.
|
|
151
|
+
*/
|
|
152
|
+
visitorView(config: MaintenanceConfig | null | undefined): MaintenanceVisitorView | null;
|
|
153
|
+
}
|
|
@@ -416,10 +416,15 @@ export declare function resolveManagerFetch(spec: string | undefined, context: {
|
|
|
416
416
|
};
|
|
417
417
|
userContext?: unknown;
|
|
418
418
|
}): Promise<ManagerFetchResult>;
|
|
419
|
-
/**
|
|
419
|
+
/**
|
|
420
|
+
* One choice in a form's dropdown or checklist: the value submitted, the text
|
|
421
|
+
* shown, and optionally what it is worth in whole minor units (cents), which a
|
|
422
|
+
* checklist adds up as boxes are ticked (#1904).
|
|
423
|
+
*/
|
|
420
424
|
export interface FormOption {
|
|
421
425
|
value: string;
|
|
422
426
|
label: string;
|
|
427
|
+
amount?: number;
|
|
423
428
|
}
|
|
424
429
|
/** Outcome of resolveManagerOptions; the statuses mean what they mean for resolveManagerFetch. */
|
|
425
430
|
export type ManagerOptionsResult = {
|
|
@@ -676,9 +676,12 @@ function toFormOptionList(value) {
|
|
|
676
676
|
options.push({ value: item, label: item });
|
|
677
677
|
}
|
|
678
678
|
else if (item && typeof item === 'object') {
|
|
679
|
-
const { value: v, label } = item;
|
|
679
|
+
const { value: v, label, amount } = item;
|
|
680
680
|
if ((typeof v === 'string' || typeof v === 'number') && String(v) !== '') {
|
|
681
|
-
|
|
681
|
+
const option = { value: String(v), label: typeof label === 'string' && label !== '' ? label : String(v) };
|
|
682
|
+
if (typeof amount === 'number' && Number.isSafeInteger(amount))
|
|
683
|
+
option.amount = amount;
|
|
684
|
+
options.push(option);
|
|
682
685
|
}
|
|
683
686
|
}
|
|
684
687
|
}
|
|
@@ -55,7 +55,7 @@ export type StoreTakeout = {
|
|
|
55
55
|
* Returns an empty file list when the store does not exist, so a caller can ask
|
|
56
56
|
* about a store without first proving it is there.
|
|
57
57
|
*/
|
|
58
|
-
export declare function collectStore(pagesDirectory: string, owner: string, store: string, layout?: PrivateStoreLayoutOverrides): Promise<StoreTakeout>;
|
|
58
|
+
export declare function collectStore(pagesDirectory: string, owner: string, store: string, layout?: PrivateStoreLayoutOverrides, skip?: (relativeToStore: string) => boolean): Promise<StoreTakeout>;
|
|
59
59
|
/** The store ids a user has on disk, in directory order. */
|
|
60
60
|
export declare function listStoreIds(pagesDirectory: string, owner: string, layout?: PrivateStoreLayoutOverrides): Promise<string[]>;
|
|
61
61
|
/** The users who have a private directory on disk, in directory order. */
|
|
@@ -68,7 +68,7 @@ export declare function listPrivateOwners(pagesDirectory: string, layout?: Priva
|
|
|
68
68
|
* This is the unit both callers work in — an instance backup asks for it once
|
|
69
69
|
* per owner, and a user's own download asks for it once, for themselves.
|
|
70
70
|
*/
|
|
71
|
-
export declare function collectUser(pagesDirectory: string, owner: string, layout?: PrivateStoreLayoutOverrides): Promise<{
|
|
71
|
+
export declare function collectUser(pagesDirectory: string, owner: string, layout?: PrivateStoreLayoutOverrides, skip?: (relativeToStore: string) => boolean): Promise<{
|
|
72
72
|
owner: string;
|
|
73
73
|
userFiles: StoreFile[];
|
|
74
74
|
stores: StoreTakeout[];
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
* {@link refreshRedactedSecrets} and log them themselves.
|
|
38
38
|
*/
|
|
39
39
|
import { format } from 'winston';
|
|
40
|
-
import { secretKeys } from './sensitiveValues.js';
|
|
40
|
+
import { secretKeys, secretValuesBelow, valueLevels } from './sensitiveValues.js';
|
|
41
|
+
import { keyOfEntry } from './configPaths.js';
|
|
41
42
|
/**
|
|
42
43
|
* Values shorter than this are never redacted.
|
|
43
44
|
*
|
|
@@ -88,27 +89,46 @@ export function refreshRedactedSecrets(configManager) {
|
|
|
88
89
|
redactions = [];
|
|
89
90
|
return { active: 0, skipped };
|
|
90
91
|
}
|
|
91
|
-
const
|
|
92
|
+
const read = (k, d) => configManager.getProperty(k, d);
|
|
93
|
+
const keys = secretKeys(read);
|
|
92
94
|
const seen = new Set();
|
|
93
95
|
const next = [];
|
|
94
|
-
|
|
95
|
-
if (typeof rawKey !== 'string')
|
|
96
|
-
continue;
|
|
97
|
-
const key = rawKey.trim();
|
|
98
|
-
if (key === '')
|
|
99
|
-
continue;
|
|
100
|
-
const value = configManager.getProperty(key, undefined);
|
|
96
|
+
const take = (key, value) => {
|
|
101
97
|
const reason = rejectionReason(value);
|
|
102
98
|
if (reason) {
|
|
103
99
|
skipped.push({ key, reason });
|
|
104
|
-
|
|
100
|
+
return;
|
|
105
101
|
}
|
|
106
102
|
const literal = value.trim();
|
|
107
103
|
// Two keys holding the same value redact once; the first key names it.
|
|
108
104
|
if (seen.has(literal))
|
|
109
|
-
|
|
105
|
+
return;
|
|
110
106
|
seen.add(literal);
|
|
111
107
|
next.push({ key, value: literal });
|
|
108
|
+
};
|
|
109
|
+
// #1889: an entry that names a path inside a key's value (with or without
|
|
110
|
+
// a `*`) is redacted by each value it finds there, named by its full path.
|
|
111
|
+
const inside = new Set();
|
|
112
|
+
for (const rawKey of keys) {
|
|
113
|
+
if (typeof rawKey !== 'string')
|
|
114
|
+
continue;
|
|
115
|
+
const key = rawKey.trim();
|
|
116
|
+
if (key === '')
|
|
117
|
+
continue;
|
|
118
|
+
const value = configManager.getProperty(key, undefined);
|
|
119
|
+
if (value === undefined) {
|
|
120
|
+
const parent = keyOfEntry(key, (k) => configManager.getProperty(k, undefined) !== undefined);
|
|
121
|
+
if (parent) {
|
|
122
|
+
inside.add(parent);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
take(key, value);
|
|
127
|
+
}
|
|
128
|
+
const levels = valueLevels(read);
|
|
129
|
+
for (const parent of inside) {
|
|
130
|
+
for (const { path, value } of secretValuesBelow(parent, configManager.getProperty(parent, undefined), levels))
|
|
131
|
+
take(path, value);
|
|
112
132
|
}
|
|
113
133
|
// Longest first: where one secret contains another, the longer must be
|
|
114
134
|
// struck first or the shorter leaves a mangled remainder of the longer.
|
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
* hides secret-looking fields nested inside an object value (an OIDC client's
|
|
16
16
|
* `client_secret` inside `oidc-auth-server.clients`).
|
|
17
17
|
*
|
|
18
|
+
* An entry may also name something inside a key's value by its path (#1889):
|
|
19
|
+
* `ngdpbase.addons.accounting.accounts.*.account-number`, where `*` stands for
|
|
20
|
+
* any one map key (or list index). It hides what sits there, at the entry's
|
|
21
|
+
* level, wherever a value is shown, except a bare `$NAME` reference, which is
|
|
22
|
+
* a pointer to the environment, not the secret. The paths are walked by
|
|
23
|
+
* `utils/configPaths.ts`, the same walk that resolves `$NAME` inside a value.
|
|
24
|
+
*
|
|
18
25
|
* Hidden values are replaced on the server, before any HTML is built: they
|
|
19
26
|
* never reach a page, its source or the parse cache.
|
|
20
27
|
*
|
|
@@ -31,15 +38,31 @@ export interface ValueViewer {
|
|
|
31
38
|
}
|
|
32
39
|
/** Whether a key or field name looks like a secret (the backstop's test; also `npm run lint:sensitive`). */
|
|
33
40
|
export declare function looksSecret(name: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* What an administrator's screen, a log line and an audit record may hold:
|
|
43
|
+
* sensitive values, never secrets. Secrets there are revealed one key at a
|
|
44
|
+
* time, on demand (`secret-reveal`).
|
|
45
|
+
*/
|
|
46
|
+
export declare const NO_SECRETS: ValueViewer;
|
|
34
47
|
/** What a hidden value is shown as. */
|
|
35
48
|
export declare const HIDDEN = "(hidden)";
|
|
36
49
|
type Read = (key: string, fallback: unknown) => unknown;
|
|
37
50
|
/**
|
|
38
51
|
* The declared levels: the legacy list's keys as `secret`, then the new map
|
|
39
52
|
* (`"secret"`, `"sensitive"`, `true` = secret; `false` / `null` removes an
|
|
40
|
-
* entry, so an instance can override a shipped one).
|
|
53
|
+
* entry, so an instance can override a shipped one). An entry that is not a
|
|
54
|
+
* well-formed key or path (#1889: an empty segment, a `*` inside a segment, or
|
|
55
|
+
* a `*` before `ngdpbase.<category>`) is no entry, as a level that is none of
|
|
56
|
+
* these is no entry; {@link sensitiveValueProblems} names both.
|
|
41
57
|
*/
|
|
42
58
|
export declare function valueLevels(read: Read): Map<string, ValueLevel>;
|
|
59
|
+
/**
|
|
60
|
+
* What is wrong with the declared entries, one line each (#1889): an entry
|
|
61
|
+
* that is not a well-formed key or path, or a level that is none of
|
|
62
|
+
* `"secret"`, `"sensitive"`, `true`, `false`, `null`. {@link valueLevels}
|
|
63
|
+
* drops each such entry; the engine warns at boot.
|
|
64
|
+
*/
|
|
65
|
+
export declare function sensitiveValueProblems(read: Read): string[];
|
|
43
66
|
/** The keys declared `secret`: what logs, the audit log and the admin screen never print. */
|
|
44
67
|
export declare function secretKeys(read: Read): string[];
|
|
45
68
|
/**
|
|
@@ -52,8 +75,31 @@ export declare function levelOf(key: string, levels: Map<string, ValueLevel>, va
|
|
|
52
75
|
/** Whether the legacy list is still set (to warn at boot). */
|
|
53
76
|
export declare function usesLegacyName(read: Read): boolean;
|
|
54
77
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
78
|
+
* A key's value with what its path entries name hidden from this viewer
|
|
79
|
+
* (#1889). An empty value, and a bare `$NAME` reference, are kept: neither is
|
|
80
|
+
* the secret. The key's own level is {@link shownValue}'s business.
|
|
81
|
+
*/
|
|
82
|
+
export declare function hideDeclaredPaths(key: string, value: unknown, levels: Map<string, ValueLevel>, viewer: ValueViewer): unknown;
|
|
83
|
+
/**
|
|
84
|
+
* The strings (and numbers, as text) a key's secret path entries name inside
|
|
85
|
+
* its value, each with its full path (#1889): what log redaction strikes.
|
|
86
|
+
* Everything under a named map or list counts.
|
|
87
|
+
*/
|
|
88
|
+
export declare function secretValuesBelow(key: string, value: unknown, levels: Map<string, ValueLevel>): Array<{
|
|
89
|
+
path: string;
|
|
90
|
+
value: string;
|
|
91
|
+
}>;
|
|
92
|
+
/**
|
|
93
|
+
* A value submitted from a screen that showed it with paths hidden, with each
|
|
94
|
+
* `HIDDEN` placeholder at a declared path put back to the stored value there
|
|
95
|
+
* (#1889). Without this, saving an edit to an accounts map would write the
|
|
96
|
+
* placeholder over every account number.
|
|
97
|
+
*/
|
|
98
|
+
export declare function keepHiddenValues(key: string, submitted: unknown, stored: unknown, levels: Map<string, ValueLevel>): unknown;
|
|
99
|
+
/**
|
|
100
|
+
* The value a viewer may see for a key: itself, or `HIDDEN`; inside it, what
|
|
101
|
+
* its path entries name is hidden as well (#1889). An empty value is shown as
|
|
102
|
+
* it is: "not set" reveals nothing.
|
|
57
103
|
*/
|
|
58
104
|
export declare function shownValue(key: string, value: unknown, levels: Map<string, ValueLevel>, viewer: ValueViewer): unknown;
|
|
59
105
|
export {};
|