@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,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.20.0",
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",