@jwilleke/ngdpbase-types 5.19.0 → 5.21.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/src/managers/AddonsManager.d.ts +26 -0
- package/lib/src/managers/AuthManager.d.ts +10 -1
- package/lib/src/managers/BackgroundJobManager.d.ts +6 -0
- package/lib/src/managers/BaseManager.d.ts +6 -0
- package/lib/src/managers/ConfigurationManager.d.ts +27 -2
- package/lib/src/managers/DatabaseManager.d.ts +31 -1
- package/lib/src/managers/DatabaseManager.js +108 -17
- package/lib/src/providers/SqliteDatabaseProvider.d.ts +5 -0
- package/lib/src/providers/SqliteDatabaseProvider.js +34 -7
- package/lib/src/types/WikiEngine.d.ts +12 -1
- package/lib/src/utils/addonConfigLayer.d.ts +51 -1
- package/lib/src/utils/addonConfigLayer.js +104 -8
- package/lib/src/utils/configPaths.d.ts +55 -0
- package/lib/src/utils/configPaths.js +133 -0
- package/lib/src/utils/pluginFormatters.d.ts +6 -1
- package/lib/src/utils/pluginFormatters.js +5 -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/startupState.d.ts +65 -0
- package/lib/src/utils/startupState.js +115 -0
- package/package.json +1 -1
|
@@ -15,12 +15,20 @@
|
|
|
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
|
*
|
|
21
28
|
* @module utils/sensitiveValues
|
|
22
29
|
*/
|
|
23
30
|
import { enabledEntries } from './configFiles.js';
|
|
31
|
+
import { envRefName, mapConfigValue, parsePathEntry, pathMatches, pathString, patternBelow, valueAt } from './configPaths.js';
|
|
24
32
|
export const SENSITIVE_VALUES_KEY = 'ngdpbase.config.sensitive-values';
|
|
25
33
|
/** The list's name before #1750; still read, with a boot warning to rename it. */
|
|
26
34
|
export const LEGACY_SECRET_KEYS_KEY = 'ngdpbase.config.secret-keys';
|
|
@@ -30,34 +38,83 @@ const SECRET_NAME_PATTERN = /secret|passw(or)?d|token|credential|api[-_]?key|pri
|
|
|
30
38
|
export function looksSecret(name) {
|
|
31
39
|
return SECRET_NAME_PATTERN.test(name);
|
|
32
40
|
}
|
|
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 const NO_SECRETS = Object.freeze({ sensitive: true, secret: false });
|
|
33
47
|
/** What a hidden value is shown as. */
|
|
34
48
|
export const HIDDEN = '(hidden)';
|
|
35
49
|
/**
|
|
36
50
|
* The declared levels: the legacy list's keys as `secret`, then the new map
|
|
37
51
|
* (`"secret"`, `"sensitive"`, `true` = secret; `false` / `null` removes an
|
|
38
|
-
* entry, so an instance can override a shipped one).
|
|
52
|
+
* entry, so an instance can override a shipped one). An entry that is not a
|
|
53
|
+
* well-formed key or path (#1889: an empty segment, a `*` inside a segment, or
|
|
54
|
+
* a `*` before `ngdpbase.<category>`) is no entry, as a level that is none of
|
|
55
|
+
* these is no entry; {@link sensitiveValueProblems} names both.
|
|
39
56
|
*/
|
|
40
57
|
export function valueLevels(read) {
|
|
41
58
|
const levels = new Map();
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
const entry = (key) => (parsePathEntry(key.trim()) ? key.trim() : null);
|
|
60
|
+
for (const key of enabledEntries(read(LEGACY_SECRET_KEYS_KEY, {}))) {
|
|
61
|
+
const k = entry(key);
|
|
62
|
+
if (k)
|
|
63
|
+
levels.set(k, 'secret');
|
|
64
|
+
}
|
|
44
65
|
const declared = read(SENSITIVE_VALUES_KEY, {});
|
|
45
66
|
if (declared && typeof declared === 'object' && !Array.isArray(declared)) {
|
|
46
67
|
for (const [key, level] of Object.entries(declared)) {
|
|
68
|
+
const k = entry(key);
|
|
69
|
+
if (!k)
|
|
70
|
+
continue;
|
|
47
71
|
if (level === 'secret' || level === true)
|
|
48
|
-
levels.set(
|
|
72
|
+
levels.set(k, 'secret');
|
|
49
73
|
else if (level === 'sensitive')
|
|
50
|
-
levels.set(
|
|
74
|
+
levels.set(k, 'sensitive');
|
|
51
75
|
else
|
|
52
|
-
levels.delete(
|
|
76
|
+
levels.delete(k);
|
|
53
77
|
}
|
|
54
78
|
}
|
|
55
79
|
else {
|
|
56
|
-
for (const key of enabledEntries(declared))
|
|
57
|
-
|
|
80
|
+
for (const key of enabledEntries(declared)) {
|
|
81
|
+
const k = entry(key);
|
|
82
|
+
if (k)
|
|
83
|
+
levels.set(k, 'secret');
|
|
84
|
+
}
|
|
58
85
|
}
|
|
59
86
|
return levels;
|
|
60
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* What is wrong with the declared entries, one line each (#1889): an entry
|
|
90
|
+
* that is not a well-formed key or path, or a level that is none of
|
|
91
|
+
* `"secret"`, `"sensitive"`, `true`, `false`, `null`. {@link valueLevels}
|
|
92
|
+
* drops each such entry; the engine warns at boot.
|
|
93
|
+
*/
|
|
94
|
+
export function sensitiveValueProblems(read) {
|
|
95
|
+
const problems = [];
|
|
96
|
+
const named = (list, key) => {
|
|
97
|
+
if (!parsePathEntry(key.trim())) {
|
|
98
|
+
problems.push(`${list}: '${key}' is not a configuration key or path (an empty segment, or a * that is not a whole segment after ngdpbase.<category>); it is ignored`);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
for (const key of enabledEntries(read(LEGACY_SECRET_KEYS_KEY, {})))
|
|
102
|
+
named(LEGACY_SECRET_KEYS_KEY, key);
|
|
103
|
+
const declared = read(SENSITIVE_VALUES_KEY, {});
|
|
104
|
+
if (declared && typeof declared === 'object' && !Array.isArray(declared)) {
|
|
105
|
+
for (const [key, level] of Object.entries(declared)) {
|
|
106
|
+
named(SENSITIVE_VALUES_KEY, key);
|
|
107
|
+
if (!(level === 'secret' || level === 'sensitive' || level === true || level === false || level === null)) {
|
|
108
|
+
problems.push(`${SENSITIVE_VALUES_KEY}: '${key}' has level ${JSON.stringify(level)}, which is not "secret" or "sensitive"; the entry is ignored`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
for (const key of enabledEntries(declared))
|
|
114
|
+
named(SENSITIVE_VALUES_KEY, key);
|
|
115
|
+
}
|
|
116
|
+
return problems;
|
|
117
|
+
}
|
|
61
118
|
/** The keys declared `secret`: what logs, the audit log and the admin screen never print. */
|
|
62
119
|
export function secretKeys(read) {
|
|
63
120
|
return [...valueLevels(read)].filter(([, level]) => level === 'secret').map(([key]) => key);
|
|
@@ -91,9 +148,79 @@ function hideNestedSecrets(value) {
|
|
|
91
148
|
}
|
|
92
149
|
return value;
|
|
93
150
|
}
|
|
151
|
+
/** The path entries inside a key's value, as patterns below the key, with their levels. */
|
|
152
|
+
function entriesBelow(key, levels) {
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const [entry, level] of levels) {
|
|
155
|
+
const pattern = patternBelow(entry, key);
|
|
156
|
+
if (pattern && pattern.length > 0)
|
|
157
|
+
out.push([pattern, level]);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
/** Whether a viewer may see a value at this level. */
|
|
162
|
+
function maySee(level, viewer) {
|
|
163
|
+
return level === 'secret' ? viewer.secret : viewer.sensitive;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* A key's value with what its path entries name hidden from this viewer
|
|
167
|
+
* (#1889). An empty value, and a bare `$NAME` reference, are kept: neither is
|
|
168
|
+
* the secret. The key's own level is {@link shownValue}'s business.
|
|
169
|
+
*/
|
|
170
|
+
export function hideDeclaredPaths(key, value, levels, viewer) {
|
|
171
|
+
const hidden = entriesBelow(key, levels).filter(([, level]) => !maySee(level, viewer)).map(([pattern]) => pattern);
|
|
172
|
+
if (hidden.length === 0)
|
|
173
|
+
return value;
|
|
174
|
+
return mapConfigValue(value, (node, below) => {
|
|
175
|
+
if (below.length === 0 || !hidden.some((pattern) => pathMatches(pattern, below)))
|
|
176
|
+
return undefined;
|
|
177
|
+
return node === undefined || node === null || node === '' || envRefName(node) ? node : HIDDEN;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The strings (and numbers, as text) a key's secret path entries name inside
|
|
182
|
+
* its value, each with its full path (#1889): what log redaction strikes.
|
|
183
|
+
* Everything under a named map or list counts.
|
|
184
|
+
*/
|
|
185
|
+
export function secretValuesBelow(key, value, levels) {
|
|
186
|
+
const secret = entriesBelow(key, levels).filter(([, level]) => level === 'secret').map(([pattern]) => pattern);
|
|
187
|
+
const found = [];
|
|
188
|
+
if (secret.length === 0)
|
|
189
|
+
return found;
|
|
190
|
+
mapConfigValue(value, (node, below) => {
|
|
191
|
+
if (below.length === 0 || !secret.some((pattern) => pathMatches(pattern, below)))
|
|
192
|
+
return undefined;
|
|
193
|
+
const at = [...below];
|
|
194
|
+
mapConfigValue(node, (leaf, under) => {
|
|
195
|
+
if (typeof leaf === 'string' || typeof leaf === 'number')
|
|
196
|
+
found.push({ path: pathString(key, [...at, ...under]), value: String(leaf) });
|
|
197
|
+
return undefined;
|
|
198
|
+
});
|
|
199
|
+
return node;
|
|
200
|
+
});
|
|
201
|
+
return found;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* A value submitted from a screen that showed it with paths hidden, with each
|
|
205
|
+
* `HIDDEN` placeholder at a declared path put back to the stored value there
|
|
206
|
+
* (#1889). Without this, saving an edit to an accounts map would write the
|
|
207
|
+
* placeholder over every account number.
|
|
208
|
+
*/
|
|
209
|
+
export function keepHiddenValues(key, submitted, stored, levels) {
|
|
210
|
+
const patterns = entriesBelow(key, levels).map(([pattern]) => pattern);
|
|
211
|
+
if (patterns.length === 0)
|
|
212
|
+
return submitted;
|
|
213
|
+
return mapConfigValue(submitted, (node, below) => {
|
|
214
|
+
if (node !== HIDDEN || !patterns.some((pattern) => pathMatches(pattern, below)))
|
|
215
|
+
return undefined;
|
|
216
|
+
const kept = valueAt(stored, below);
|
|
217
|
+
return kept === undefined ? node : kept;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
94
220
|
/**
|
|
95
|
-
* The value a viewer may see for a key: itself, or `HIDDEN
|
|
96
|
-
* is
|
|
221
|
+
* The value a viewer may see for a key: itself, or `HIDDEN`; inside it, what
|
|
222
|
+
* its path entries name is hidden as well (#1889). An empty value is shown as
|
|
223
|
+
* it is: "not set" reveals nothing.
|
|
97
224
|
*/
|
|
98
225
|
export function shownValue(key, value, levels, viewer) {
|
|
99
226
|
const empty = value === undefined || value === null || value === '';
|
|
@@ -102,5 +229,6 @@ export function shownValue(key, value, levels, viewer) {
|
|
|
102
229
|
return empty ? value : HIDDEN;
|
|
103
230
|
if (level === 'sensitive' && !viewer.sensitive)
|
|
104
231
|
return empty ? value : HIDDEN;
|
|
105
|
-
|
|
232
|
+
const shown = hideDeclaredPaths(key, value, levels, viewer);
|
|
233
|
+
return viewer.secret ? shown : hideNestedSecrets(shown);
|
|
106
234
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the initialisation gate serves, and when (#1152).
|
|
3
|
+
*
|
|
4
|
+
* `app.ts` used to treat every initialisation failure the same way:
|
|
5
|
+
* `process.exit(1)`. A mistyped CIDR and an unreadable data directory produced
|
|
6
|
+
* the identical outcome — a process that is gone, and an operator whose only
|
|
7
|
+
* route back is the filesystem. Under a supervisor it is worse than useless: a
|
|
8
|
+
* process that exits on a bad config value restarts, fails identically, and
|
|
9
|
+
* restarts again, and nothing in that loop ever reaches the admin UI.
|
|
10
|
+
*
|
|
11
|
+
* __The test is not severity.__ A malformed deny rule is serious, and the
|
|
12
|
+
* instance should stop serving because of it. The question is whether stopping
|
|
13
|
+
* the *process* helps, and it only helps when the process cannot offer a way
|
|
14
|
+
* out.
|
|
15
|
+
*
|
|
16
|
+
* See D9 to D13 of docs/security-posture.md.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Paths served in every state, so the maintenance page can render itself.
|
|
20
|
+
*
|
|
21
|
+
* Assets only. Adding a route here would open it during `starting` too, when
|
|
22
|
+
* the managers behind it are not up.
|
|
23
|
+
*/
|
|
24
|
+
export declare const STARTUP_BYPASS_PATHS: readonly ["/css", "/js", "/images", "/themes", "/addons"];
|
|
25
|
+
export type GateDecision = 'serve' | 'block';
|
|
26
|
+
/**
|
|
27
|
+
* What the initialisation gate should do with a request.
|
|
28
|
+
*
|
|
29
|
+
* The server either is `engineReady` or it is not. What decides the third
|
|
30
|
+
* behaviour is not a third readiness value but a second, independent fact:
|
|
31
|
+
* whether the engine finished and a configuration value is unusable. Both are
|
|
32
|
+
* passed in, so the impossible combination — ready AND blocked — simply never
|
|
33
|
+
* arises at the call site rather than being modelled away.
|
|
34
|
+
*/
|
|
35
|
+
export declare function gateDecision(engineReady: boolean, blocked: boolean, path: string): GateDecision;
|
|
36
|
+
/**
|
|
37
|
+
* The message shown on the maintenance page and logged at boot.
|
|
38
|
+
*
|
|
39
|
+
* Every reason at once, rather than the first: an operator who fixes one and
|
|
40
|
+
* restarts into the next has been made to do the work twice.
|
|
41
|
+
*/
|
|
42
|
+
export declare function describeBlocked(reasons: readonly string[]): string;
|
|
43
|
+
/**
|
|
44
|
+
* What a visitor is told about a blocked instance: on the maintenance page and
|
|
45
|
+
* in `/health/readiness`, both served without sign-in (#1883).
|
|
46
|
+
*
|
|
47
|
+
* Every reason, as {@link describeBlocked} words them, unless one was recorded
|
|
48
|
+
* for administrators only (a site-critical add-on that did not load): then
|
|
49
|
+
* only `publicMessage`, and no reason at all — not even the ones that could be
|
|
50
|
+
* shown, since one detail beside a vague notice still says what failed.
|
|
51
|
+
* Administrators get the reasons in the log and the admin notification.
|
|
52
|
+
*/
|
|
53
|
+
export declare function blockedForVisitors(reasons: readonly string[], publicMessage: string | null): {
|
|
54
|
+
message: string;
|
|
55
|
+
detail: readonly string[];
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* #1883: what visitors see while a failure for administrators only holds the
|
|
59
|
+
* site in maintenance mode — a site-critical add-on that did not load, or an
|
|
60
|
+
* application database that cannot be opened safely (#1892). Empty leaves
|
|
61
|
+
* only the page's heading.
|
|
62
|
+
*/
|
|
63
|
+
export declare const SITE_CRITICAL_PUBLIC_MESSAGE_KEY = "ngdpbase.managers.addons-manager.site-critical.public-message";
|
|
64
|
+
/** The configured public message ({@link SITE_CRITICAL_PUBLIC_MESSAGE_KEY}); '' when unset or not a string. */
|
|
65
|
+
export declare function sitePublicMessage(getProperty: ((key: string, fallback: unknown) => unknown) | undefined): string;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the initialisation gate serves, and when (#1152).
|
|
3
|
+
*
|
|
4
|
+
* `app.ts` used to treat every initialisation failure the same way:
|
|
5
|
+
* `process.exit(1)`. A mistyped CIDR and an unreadable data directory produced
|
|
6
|
+
* the identical outcome — a process that is gone, and an operator whose only
|
|
7
|
+
* route back is the filesystem. Under a supervisor it is worse than useless: a
|
|
8
|
+
* process that exits on a bad config value restarts, fails identically, and
|
|
9
|
+
* restarts again, and nothing in that loop ever reaches the admin UI.
|
|
10
|
+
*
|
|
11
|
+
* __The test is not severity.__ A malformed deny rule is serious, and the
|
|
12
|
+
* instance should stop serving because of it. The question is whether stopping
|
|
13
|
+
* the *process* helps, and it only helps when the process cannot offer a way
|
|
14
|
+
* out.
|
|
15
|
+
*
|
|
16
|
+
* See D9 to D13 of docs/security-posture.md.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Paths served in every state, so the maintenance page can render itself.
|
|
20
|
+
*
|
|
21
|
+
* Assets only. Adding a route here would open it during `starting` too, when
|
|
22
|
+
* the managers behind it are not up.
|
|
23
|
+
*/
|
|
24
|
+
export const STARTUP_BYPASS_PATHS = ['/css', '/js', '/images', '/themes', '/addons'];
|
|
25
|
+
/** Exact paths served in every state. */
|
|
26
|
+
const BYPASS_FILES = ['/favicon.ico', '/favicon.svg'];
|
|
27
|
+
/**
|
|
28
|
+
* Paths served when the engine finished and a configuration value is unusable,
|
|
29
|
+
* but NOT while the engine is merely starting.
|
|
30
|
+
*
|
|
31
|
+
* The difference is whether the screens work. While the engine is initialising
|
|
32
|
+
* the managers behind `/admin` are not up, so letting a request through fails
|
|
33
|
+
* in a worse way than a 503. Once it has finished and only a value is wrong,
|
|
34
|
+
* they are exactly what the operator needs.
|
|
35
|
+
*/
|
|
36
|
+
const REPAIR_PATHS = ['/admin', '/login', '/logout'];
|
|
37
|
+
/**
|
|
38
|
+
* Does `path` sit under `prefix` as a route, rather than merely starting with
|
|
39
|
+
* its characters?
|
|
40
|
+
*
|
|
41
|
+
* `/adminish` is an ordinary page and must not reach the admin bypass. Prefix
|
|
42
|
+
* matching without a boundary is how a gate springs a leak.
|
|
43
|
+
*/
|
|
44
|
+
function isUnder(path, prefix) {
|
|
45
|
+
return path === prefix || path.startsWith(`${prefix}/`) || path.startsWith(`${prefix}?`);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* What the initialisation gate should do with a request.
|
|
49
|
+
*
|
|
50
|
+
* The server either is `engineReady` or it is not. What decides the third
|
|
51
|
+
* behaviour is not a third readiness value but a second, independent fact:
|
|
52
|
+
* whether the engine finished and a configuration value is unusable. Both are
|
|
53
|
+
* passed in, so the impossible combination — ready AND blocked — simply never
|
|
54
|
+
* arises at the call site rather than being modelled away.
|
|
55
|
+
*/
|
|
56
|
+
export function gateDecision(engineReady, blocked, path) {
|
|
57
|
+
if (engineReady)
|
|
58
|
+
return 'serve';
|
|
59
|
+
if (BYPASS_FILES.includes(path))
|
|
60
|
+
return 'serve';
|
|
61
|
+
if (STARTUP_BYPASS_PATHS.some((prefix) => isUnder(path, prefix)))
|
|
62
|
+
return 'serve';
|
|
63
|
+
// Blocked means the engine FINISHED and a value is wrong, so these screens
|
|
64
|
+
// work and are the only repair path without filesystem access. While merely
|
|
65
|
+
// starting they stay closed, because the managers behind them are not up.
|
|
66
|
+
if (blocked && REPAIR_PATHS.some((prefix) => isUnder(path, prefix)))
|
|
67
|
+
return 'serve';
|
|
68
|
+
return 'block';
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The message shown on the maintenance page and logged at boot.
|
|
72
|
+
*
|
|
73
|
+
* Every reason at once, rather than the first: an operator who fixes one and
|
|
74
|
+
* restarts into the next has been made to do the work twice.
|
|
75
|
+
*/
|
|
76
|
+
export function describeBlocked(reasons) {
|
|
77
|
+
if (reasons.length === 0) {
|
|
78
|
+
// Blocked with no reason is an internal inconsistency: nothing sets the
|
|
79
|
+
// blocked state without recording why. Say "unknown" rather than render an
|
|
80
|
+
// empty page — and note that the repair path stays OPEN in this case, so
|
|
81
|
+
// an operator can still sign in and look, which is the recoverable answer.
|
|
82
|
+
return 'The instance is not serving because its configuration could not be used. '
|
|
83
|
+
+ 'The reason is UNKNOWN, which is itself a bug — nothing should block the instance without recording why. '
|
|
84
|
+
+ 'Please report it, and check the startup log.';
|
|
85
|
+
}
|
|
86
|
+
const list = reasons.map((r) => `• ${r}`).join('\n');
|
|
87
|
+
return `The instance is not serving because its configuration could not be used:\n${list}`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* What a visitor is told about a blocked instance: on the maintenance page and
|
|
91
|
+
* in `/health/readiness`, both served without sign-in (#1883).
|
|
92
|
+
*
|
|
93
|
+
* Every reason, as {@link describeBlocked} words them, unless one was recorded
|
|
94
|
+
* for administrators only (a site-critical add-on that did not load): then
|
|
95
|
+
* only `publicMessage`, and no reason at all — not even the ones that could be
|
|
96
|
+
* shown, since one detail beside a vague notice still says what failed.
|
|
97
|
+
* Administrators get the reasons in the log and the admin notification.
|
|
98
|
+
*/
|
|
99
|
+
export function blockedForVisitors(reasons, publicMessage) {
|
|
100
|
+
if (publicMessage !== null)
|
|
101
|
+
return { message: publicMessage, detail: [publicMessage] };
|
|
102
|
+
return { message: describeBlocked(reasons), detail: reasons };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* #1883: what visitors see while a failure for administrators only holds the
|
|
106
|
+
* site in maintenance mode — a site-critical add-on that did not load, or an
|
|
107
|
+
* application database that cannot be opened safely (#1892). Empty leaves
|
|
108
|
+
* only the page's heading.
|
|
109
|
+
*/
|
|
110
|
+
export const SITE_CRITICAL_PUBLIC_MESSAGE_KEY = 'ngdpbase.managers.addons-manager.site-critical.public-message';
|
|
111
|
+
/** The configured public message ({@link SITE_CRITICAL_PUBLIC_MESSAGE_KEY}); '' when unset or not a string. */
|
|
112
|
+
export function sitePublicMessage(getProperty) {
|
|
113
|
+
const value = getProperty?.(SITE_CRITICAL_PUBLIC_MESSAGE_KEY, '');
|
|
114
|
+
return typeof value === 'string' ? value : '';
|
|
115
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jwilleke/ngdpbase-types",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.21.0",
|
|
4
4
|
"description": "TypeScript types for ngdpbase add-ons, and a test kit of core's own values. A dev dependency, versioned with ngdpbase.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Jim Willeke",
|