@jwilleke/ngdpbase-types 5.20.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.
@@ -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
+ }
@@ -416,10 +416,15 @@ export declare function resolveManagerFetch(spec: string | undefined, context: {
416
416
  };
417
417
  userContext?: unknown;
418
418
  }): Promise<ManagerFetchResult>;
419
- /** One choice in a form's dropdown: the value submitted, and the text shown. */
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
- options.push({ value: String(v), label: typeof label === 'string' && label !== '' ? label : String(v) });
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
  }
@@ -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 keys = secretKeys((k, d) => configManager.getProperty(k, d));
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
- for (const rawKey of keys) {
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
- continue;
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
- continue;
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
- * The value a viewer may see for a key: itself, or `HIDDEN`. An empty value
56
- * is shown as it is — "not set" reveals nothing.
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 {};
@@ -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
- for (const key of enabledEntries(read(LEGACY_SECRET_KEYS_KEY, {})))
43
- levels.set(key, 'secret');
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(key, 'secret');
72
+ levels.set(k, 'secret');
49
73
  else if (level === 'sensitive')
50
- levels.set(key, 'sensitive');
74
+ levels.set(k, 'sensitive');
51
75
  else
52
- levels.delete(key);
76
+ levels.delete(k);
53
77
  }
54
78
  }
55
79
  else {
56
- for (const key of enabledEntries(declared))
57
- levels.set(key, 'secret');
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`. An empty value
96
- * is shown as it is — "not set" reveals nothing.
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
- return viewer.secret ? value : hideNestedSecrets(value);
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;