@jwilleke/ngdpbase-types 5.21.0 → 5.23.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 -3
- package/lib/src/managers/AttachmentManager.d.ts +64 -12
- package/lib/src/managers/AuthManager.d.ts +29 -6
- package/lib/src/managers/BackgroundJobManager.d.ts +65 -1
- package/lib/src/managers/BaseManager.d.ts +14 -0
- package/lib/src/managers/BaseManager.js +9 -0
- package/lib/src/managers/CommentManager.d.ts +10 -1
- package/lib/src/managers/DatabaseManager.d.ts +24 -17
- package/lib/src/managers/DatabaseManager.js +72 -29
- 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/SessionStatsManager.d.ts +94 -0
- package/lib/src/managers/UserManager.d.ts +36 -1
- 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 +7 -0
- package/lib/src/providers/SqliteDatabaseProvider.js +16 -0
- 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/types/express.d.ts +2 -0
- 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/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/privateStoreTakeout.d.ts +2 -2
- package/lib/src/utils/sessionAccount.d.ts +26 -0
- package/lib/src/utils/sessionEnd.d.ts +1 -1
- 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 +2 -2
- package/lib/src/utils/startupState.d.ts +0 -65
- package/lib/src/utils/startupState.js +0 -115
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A whole-site restore, staged for the next start, all or nothing (#1876).
|
|
3
|
+
*
|
|
4
|
+
* The databases already restore this way (#1723): each is copied to
|
|
5
|
+
* `<live>.staged`, and the next start renames it into place before anything
|
|
6
|
+
* opens it. This extends the same shape to every part of the site whose
|
|
7
|
+
* owning manager stages its restore (attachments, role membership, persons,
|
|
8
|
+
* organizations, footnotes and comments). Each owner does its own work, as
|
|
9
|
+
* Manager-SOT asks; this module only holds the one record that binds the
|
|
10
|
+
* parts together, so they go in together or not at all.
|
|
11
|
+
*
|
|
12
|
+
* The record is the __site restore manifest__ (`ngdpbase.backup.restore-manifest`,
|
|
13
|
+
* default `${FAST_STORAGE}/site-restore.json`):
|
|
14
|
+
*
|
|
15
|
+
* - __Staging__ (BackupManager.restoreFromFile): the manifest is written in
|
|
16
|
+
* state `staging` before any part is staged. Each owner stages its files
|
|
17
|
+
* beside its live ones and records its part — the pairs `{ live, staged }`
|
|
18
|
+
* it will swap. Any failure discards every part already recorded and the
|
|
19
|
+
* manifest. Only when every part is staged is the state set to `staged`:
|
|
20
|
+
* that write is the commit.
|
|
21
|
+
* - __Applying__ (each owner's `initialize`, before it reads its data): the
|
|
22
|
+
* owner asks {@link gatePart}. A part is applied only when the manifest is
|
|
23
|
+
* committed and every part that is still pending has all of its staged
|
|
24
|
+
* files in place, so a set that lost a part applies none of it. Each live
|
|
25
|
+
* path steps aside as `<live>.pre-restore`, and the staged one takes its
|
|
26
|
+
* name: a rename, never a write into live data. The part is then marked
|
|
27
|
+
* applied in the manifest, so a start that stops half-way picks up where it
|
|
28
|
+
* stopped instead of seeing a "missing" part.
|
|
29
|
+
* - __Refused__: an interrupted staging, a part whose staged files are gone,
|
|
30
|
+
* or a tampered manifest (an owner checks every live path it is asked to
|
|
31
|
+
* replace) sets the manifest to `refused`, and each part's staged files are
|
|
32
|
+
* set aside as `*.incomplete` — never applied, never retried.
|
|
33
|
+
* - __Done__: once every part is applied or set aside, the manifest is
|
|
34
|
+
* renamed `<manifest>.done`, which BackupManager reports to admins at the
|
|
35
|
+
* end of the start and keeps as `<manifest>.last`.
|
|
36
|
+
*
|
|
37
|
+
* Everything here is synchronous file renames on the owner's own paths: it
|
|
38
|
+
* runs once at start, before the owner serves anything.
|
|
39
|
+
*/
|
|
40
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
41
|
+
import path from 'node:path';
|
|
42
|
+
/** The config key naming the site restore manifest. */
|
|
43
|
+
export const SITE_RESTORE_MANIFEST_KEY = 'ngdpbase.backup.restore-manifest';
|
|
44
|
+
/** Its default: beside the other instance data in FAST_STORAGE. */
|
|
45
|
+
export const SITE_RESTORE_MANIFEST_DEFAULT = './data/site-restore.json';
|
|
46
|
+
/** The staged copy of a live path: beside it, so staging and applying name the same path. */
|
|
47
|
+
export const stagedPathFor = (live) => `${live}.staged`;
|
|
48
|
+
/** Where a live path is kept once a restore has replaced it. */
|
|
49
|
+
export const keptPathFor = (live) => `${live}.pre-restore`;
|
|
50
|
+
/** The manifest file for this site, read through the configuration (never a constant). */
|
|
51
|
+
export function siteRestoreFile(configManager) {
|
|
52
|
+
return configManager?.getResolvedDataPath(SITE_RESTORE_MANIFEST_KEY, SITE_RESTORE_MANIFEST_DEFAULT) ?? SITE_RESTORE_MANIFEST_DEFAULT;
|
|
53
|
+
}
|
|
54
|
+
function isFile(file) {
|
|
55
|
+
try {
|
|
56
|
+
return statSync(file).isFile();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The manifest, or null when there is none. Throws when a file is there but
|
|
64
|
+
* is not a manifest: a restore record that can't be read is never ignored.
|
|
65
|
+
*/
|
|
66
|
+
export function readSiteRestore(file) {
|
|
67
|
+
if (!isFile(file))
|
|
68
|
+
return null;
|
|
69
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
70
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.parts) || !['staging', 'staged', 'refused'].includes(parsed.state)) {
|
|
71
|
+
throw new Error(`${path.basename(file)} is not a site restore manifest`);
|
|
72
|
+
}
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
function writeSiteRestore(file, manifest) {
|
|
76
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
77
|
+
const tmp = `${file}.tmp`;
|
|
78
|
+
writeFileSync(tmp, JSON.stringify(manifest, null, 2) + '\n', { mode: 0o600 });
|
|
79
|
+
renameSync(tmp, file);
|
|
80
|
+
}
|
|
81
|
+
/** Start staging a site restore: the record exists before any part is staged, so an interrupted staging is recognised. */
|
|
82
|
+
export function beginSiteRestore(file, meta) {
|
|
83
|
+
if (isFile(file))
|
|
84
|
+
throw new Error('A site restore is already staged; cancel it or replace it first');
|
|
85
|
+
writeSiteRestore(file, { version: 1, state: 'staging', ...meta, parts: [] });
|
|
86
|
+
}
|
|
87
|
+
/** Record one owner's staged part. Only while staging. */
|
|
88
|
+
export function recordStagedPart(file, part) {
|
|
89
|
+
const manifest = readSiteRestore(file);
|
|
90
|
+
if (!manifest || manifest.state !== 'staging')
|
|
91
|
+
throw new Error('No site restore is being staged');
|
|
92
|
+
manifest.parts = [...manifest.parts.filter((p) => p.manager !== part.manager), { manager: part.manager, swaps: part.swaps, ...(part.discard ? { discard: part.discard } : {}) }];
|
|
93
|
+
writeSiteRestore(file, manifest);
|
|
94
|
+
}
|
|
95
|
+
/** The commit: every part is staged. */
|
|
96
|
+
export function commitSiteRestore(file) {
|
|
97
|
+
const manifest = readSiteRestore(file);
|
|
98
|
+
if (!manifest || manifest.state !== 'staging')
|
|
99
|
+
throw new Error('No site restore is being staged');
|
|
100
|
+
for (const part of manifest.parts) {
|
|
101
|
+
const missing = part.swaps.filter((s) => !existsSync(s.staged));
|
|
102
|
+
if (missing.length > 0)
|
|
103
|
+
throw new Error(`${part.manager} recorded staged files that are not there: ${missing.map((s) => path.basename(s.staged)).join(', ')}`);
|
|
104
|
+
}
|
|
105
|
+
manifest.state = 'staged';
|
|
106
|
+
writeSiteRestore(file, manifest);
|
|
107
|
+
return manifest;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Throw a staged (or half-staged) site restore away: every part's staged
|
|
111
|
+
* files and the manifest are removed. Live data is never touched.
|
|
112
|
+
*/
|
|
113
|
+
export function discardSiteRestore(file) {
|
|
114
|
+
let manifest;
|
|
115
|
+
try {
|
|
116
|
+
manifest = readSiteRestore(file);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
manifest = null;
|
|
120
|
+
}
|
|
121
|
+
for (const part of manifest?.parts ?? []) {
|
|
122
|
+
for (const swap of part.swaps)
|
|
123
|
+
rmSync(swap.staged, { recursive: true, force: true });
|
|
124
|
+
for (const extra of part.discard ?? [])
|
|
125
|
+
rmSync(extra, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
rmSync(file, { force: true });
|
|
128
|
+
return manifest;
|
|
129
|
+
}
|
|
130
|
+
/** Move a staged path aside as `*.incomplete`, so it is neither applied nor retried. */
|
|
131
|
+
export function setAsidePath(staged) {
|
|
132
|
+
if (!existsSync(staged))
|
|
133
|
+
return;
|
|
134
|
+
const target = `${staged}.incomplete`;
|
|
135
|
+
rmSync(target, { recursive: true, force: true });
|
|
136
|
+
renameSync(staged, target);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Whether `manager`'s part of a site restore is to be applied now. `none`:
|
|
140
|
+
* there is no site restore, or it has no part for this manager, or that part
|
|
141
|
+
* is done. `apply`: the restore is committed and every part still pending has
|
|
142
|
+
* its staged files. `set-aside`: it isn't, and the set is marked refused.
|
|
143
|
+
*/
|
|
144
|
+
export function gatePart(file, manager) {
|
|
145
|
+
let manifest;
|
|
146
|
+
try {
|
|
147
|
+
manifest = readSiteRestore(file);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
// A record that can't be read applies nothing. It is left where it is,
|
|
151
|
+
// untouched — the path may be misconfigured to name some other file —
|
|
152
|
+
// and every start says so until it is fixed.
|
|
153
|
+
return { decision: 'set-aside', reason: `the site restore manifest ${file} cannot be read: ${err.message}` };
|
|
154
|
+
}
|
|
155
|
+
if (!manifest)
|
|
156
|
+
return { decision: 'none' };
|
|
157
|
+
const part = manifest.parts.find((p) => p.manager === manager);
|
|
158
|
+
if (!part || part.status)
|
|
159
|
+
return { decision: 'none' };
|
|
160
|
+
if (manifest.state === 'staging') {
|
|
161
|
+
return refuse(file, manifest, part, 'the site restore was still being staged when the server stopped (interrupted staging)');
|
|
162
|
+
}
|
|
163
|
+
if (manifest.state === 'refused') {
|
|
164
|
+
return { decision: 'set-aside', reason: manifest.reason ?? 'the site restore was refused', part };
|
|
165
|
+
}
|
|
166
|
+
const setAsideAlready = manifest.parts.filter((p) => p.status === 'set-aside').map((p) => p.manager);
|
|
167
|
+
if (setAsideAlready.length > 0) {
|
|
168
|
+
return refuse(file, manifest, part, `part of it (${setAsideAlready.join(', ')}) was not applied, and the parts belong together`);
|
|
169
|
+
}
|
|
170
|
+
const missing = manifest.parts
|
|
171
|
+
.filter((p) => !p.status)
|
|
172
|
+
.flatMap((p) => p.swaps.filter((s) => !existsSync(s.staged)).map((s) => `${p.manager}: ${path.basename(s.staged)}`));
|
|
173
|
+
if (missing.length > 0) {
|
|
174
|
+
return refuse(file, manifest, part, `staged files are missing (${missing.join(', ')}), and the parts belong together`);
|
|
175
|
+
}
|
|
176
|
+
return { decision: 'apply', part, manifest };
|
|
177
|
+
}
|
|
178
|
+
function refuse(file, manifest, part, reason) {
|
|
179
|
+
manifest.state = 'refused';
|
|
180
|
+
manifest.reason = reason;
|
|
181
|
+
writeSiteRestore(file, manifest);
|
|
182
|
+
return { decision: 'set-aside', reason, part };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Record what became of `manager`'s part. A part set aside refuses the rest
|
|
186
|
+
* of the set. When every part is done the manifest becomes `<file>.done`.
|
|
187
|
+
*/
|
|
188
|
+
export function markPart(file, manager, status, note) {
|
|
189
|
+
let manifest;
|
|
190
|
+
try {
|
|
191
|
+
manifest = readSiteRestore(file);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!manifest)
|
|
197
|
+
return;
|
|
198
|
+
const part = manifest.parts.find((p) => p.manager === manager);
|
|
199
|
+
if (!part)
|
|
200
|
+
return;
|
|
201
|
+
part.status = status;
|
|
202
|
+
if (note)
|
|
203
|
+
part.note = note;
|
|
204
|
+
if (status === 'set-aside' && manifest.state !== 'refused') {
|
|
205
|
+
manifest.state = 'refused';
|
|
206
|
+
manifest.reason = note ?? `${manager}'s part was not applied`;
|
|
207
|
+
}
|
|
208
|
+
writeSiteRestore(file, manifest);
|
|
209
|
+
if (manifest.parts.every((p) => p.status)) {
|
|
210
|
+
const done = `${file}.done`;
|
|
211
|
+
rmSync(done, { force: true });
|
|
212
|
+
renameSync(file, done);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Apply `manager`'s part of a staged site restore, at start, before the
|
|
217
|
+
* manager reads its data. `allowed` says which live paths this manager may
|
|
218
|
+
* replace, so a tampered manifest cannot rename an arbitrary file.
|
|
219
|
+
* `strays` lists staged paths of this manager's that exist on disk, found by
|
|
220
|
+
* the manager: with no committed restore naming them they are set aside.
|
|
221
|
+
* Returns null when there was nothing to do.
|
|
222
|
+
*/
|
|
223
|
+
export function applyPartAtStart(file, manager, options) {
|
|
224
|
+
const gate = gatePart(file, manager);
|
|
225
|
+
if (gate.decision === 'none') {
|
|
226
|
+
const strays = options.strays?.() ?? [];
|
|
227
|
+
if (strays.length === 0)
|
|
228
|
+
return null;
|
|
229
|
+
for (const s of strays)
|
|
230
|
+
setAsidePath(s);
|
|
231
|
+
return { applied: [], refused: `staged files with no site restore naming them (an interrupted staging): ${strays.map((s) => path.basename(s)).join(', ')} — set aside as *.incomplete` };
|
|
232
|
+
}
|
|
233
|
+
if (gate.decision === 'set-aside') {
|
|
234
|
+
for (const swap of gate.part?.swaps ?? [])
|
|
235
|
+
setAsidePath(swap.staged);
|
|
236
|
+
for (const s of options.strays?.() ?? [])
|
|
237
|
+
setAsidePath(s);
|
|
238
|
+
if (gate.part)
|
|
239
|
+
markPart(file, manager, 'set-aside', gate.reason);
|
|
240
|
+
return { applied: [], refused: gate.reason };
|
|
241
|
+
}
|
|
242
|
+
const bad = gate.part.swaps.find((s) => !options.allowed(s) || s.staged !== stagedPathFor(s.live));
|
|
243
|
+
if (bad) {
|
|
244
|
+
const reason = `the site restore manifest names ${bad.live}, which is not ${manager}'s to replace`;
|
|
245
|
+
for (const swap of gate.part.swaps)
|
|
246
|
+
if (swap.staged === stagedPathFor(swap.live))
|
|
247
|
+
setAsidePath(swap.staged);
|
|
248
|
+
markPart(file, manager, 'set-aside', reason);
|
|
249
|
+
return { applied: [], refused: reason };
|
|
250
|
+
}
|
|
251
|
+
const applied = [];
|
|
252
|
+
try {
|
|
253
|
+
for (const swap of gate.part.swaps) {
|
|
254
|
+
const kept = keptPathFor(swap.live);
|
|
255
|
+
rmSync(kept, { recursive: true, force: true });
|
|
256
|
+
if (existsSync(swap.live))
|
|
257
|
+
renameSync(swap.live, kept);
|
|
258
|
+
mkdirSync(path.dirname(swap.live), { recursive: true });
|
|
259
|
+
renameSync(swap.staged, swap.live);
|
|
260
|
+
applied.push({ live: swap.live, kept });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
const reason = `applying ${manager}'s part failed after ${applied.length} of ${gate.part.swaps.length} paths: ${err.message}`;
|
|
265
|
+
markPart(file, manager, 'set-aside', reason);
|
|
266
|
+
return { applied, refused: reason };
|
|
267
|
+
}
|
|
268
|
+
markPart(file, manager, 'applied');
|
|
269
|
+
return { applied };
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* What a start did with a site restore, for BackupManager to tell admins at
|
|
273
|
+
* the end of the start: the finished record (`<file>.done`, then kept as
|
|
274
|
+
* `<file>.last`), or a record with parts still pending — their managers did
|
|
275
|
+
* not start — which is then refused and its pending parts set aside.
|
|
276
|
+
*/
|
|
277
|
+
export function finishSiteRestoreAtBoot(file) {
|
|
278
|
+
const done = `${file}.done`;
|
|
279
|
+
let manifest;
|
|
280
|
+
try {
|
|
281
|
+
manifest = readSiteRestore(file);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
manifest = null;
|
|
285
|
+
}
|
|
286
|
+
if (manifest) {
|
|
287
|
+
if (manifest.state === 'staging')
|
|
288
|
+
refuse(file, manifest, manifest.parts[0], 'the site restore was still being staged when the server stopped (interrupted staging)');
|
|
289
|
+
const pending = manifest.parts.filter((p) => !p.status);
|
|
290
|
+
for (const part of pending) {
|
|
291
|
+
for (const swap of part.swaps)
|
|
292
|
+
setAsidePath(swap.staged);
|
|
293
|
+
for (const extra of part.discard ?? [])
|
|
294
|
+
setAsidePath(extra);
|
|
295
|
+
markPart(file, part.manager, 'set-aside', `${part.manager} did not apply its part at start, so it was set aside`);
|
|
296
|
+
}
|
|
297
|
+
if (manifest.parts.length === 0 && isFile(file)) {
|
|
298
|
+
rmSync(done, { force: true });
|
|
299
|
+
renameSync(file, done);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!isFile(done))
|
|
303
|
+
return null;
|
|
304
|
+
const finished = JSON.parse(readFileSync(done, 'utf8'));
|
|
305
|
+
const last = `${file}.last`;
|
|
306
|
+
rmSync(last, { force: true });
|
|
307
|
+
renameSync(done, last);
|
|
308
|
+
return finished;
|
|
309
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface TreeArchiveOutcome {
|
|
2
|
+
/** Regular files written. */
|
|
3
|
+
files: number;
|
|
4
|
+
/** Their bytes. */
|
|
5
|
+
bytes: number;
|
|
6
|
+
/** Entries left out because they are not a file or a folder (a symbolic link), relative to the root. */
|
|
7
|
+
skipped: string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Write `entries` (paths relative to `root`; default: everything in it) into
|
|
11
|
+
* `target`, streamed. `exclude` leaves out a path relative to `root`, in
|
|
12
|
+
* posix form. A missing `root` writes an empty archive. Never overwrites.
|
|
13
|
+
*/
|
|
14
|
+
export declare function writeTreeArchive(target: string, root: string, options?: {
|
|
15
|
+
entries?: string[];
|
|
16
|
+
exclude?: (relative: string) => boolean;
|
|
17
|
+
backupKey?: string;
|
|
18
|
+
}): Promise<TreeArchiveOutcome>;
|
|
19
|
+
/**
|
|
20
|
+
* Extract `archive` into `dest`, which is emptied first, streamed.
|
|
21
|
+
* `select(relative)` picks the entries to extract (default: all). Refuses —
|
|
22
|
+
* removing `dest` — an entry that is not a file or a folder, an absolute
|
|
23
|
+
* path, or one that climbs out (`..`).
|
|
24
|
+
*/
|
|
25
|
+
export declare function extractTreeArchive(archive: string, dest: string, options?: {
|
|
26
|
+
select?: (relative: string) => boolean;
|
|
27
|
+
strip?: number;
|
|
28
|
+
backupKey?: string;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
files: number;
|
|
31
|
+
bytes: number;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Every entry in an archive, plain or encrypted, read as a stream: what a
|
|
35
|
+
* caller checks before extracting. `onEntry` gets each entry's path and type.
|
|
36
|
+
*/
|
|
37
|
+
export declare function listTreeArchive(archive: string, onEntry: (entry: {
|
|
38
|
+
path: string;
|
|
39
|
+
type: string;
|
|
40
|
+
}) => void, options?: {
|
|
41
|
+
backupKey?: string;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
/** The first `n` bytes of a file, read without reading the rest. */
|
|
44
|
+
export declare function readHead(file: string, n: number): Promise<Buffer>;
|
|
@@ -49,5 +49,11 @@ export declare function sameUsername(a: unknown, b: unknown): boolean;
|
|
|
49
49
|
* checked separately by `isSystemPrincipal`, because an operator chooses it.
|
|
50
50
|
*/
|
|
51
51
|
export declare const RESERVED_USERNAMES: readonly string[];
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* The prefix of a person's identifier when the person has no account (#1930):
|
|
54
|
+
* `person:<uuid>`. No username may start with it, so an account-less person's
|
|
55
|
+
* identifier can never be, or later become, someone's username.
|
|
56
|
+
*/
|
|
57
|
+
export declare const ACCOUNTLESS_PERSON_PREFIX = "person:";
|
|
58
|
+
/** True when the name belongs to a principal rather than a person, or has the account-less person prefix. */
|
|
53
59
|
export declare function isReservedUsername(username: unknown): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jwilleke/ngdpbase-types",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.23.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",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"ipaddr.js": "^2.5.0",
|
|
47
47
|
"jose": "^6.2.12",
|
|
48
48
|
"linkedom": "^0.18.13",
|
|
49
|
-
"markdown-it": "^14.3.
|
|
49
|
+
"markdown-it": "^14.3.3",
|
|
50
50
|
"minimatch": "^10.2.6",
|
|
51
51
|
"minisearch": "7.2.0",
|
|
52
52
|
"multer": "^2.4.0",
|
|
@@ -1,65 +0,0 @@
|
|
|
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;
|
|
@@ -1,115 +0,0 @@
|
|
|
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
|
-
}
|