@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.
Files changed (58) hide show
  1. package/lib/packages/ngdpbase-types/index.d.ts +5 -0
  2. package/lib/src/mail/MailProvider.d.ts +2 -0
  3. package/lib/src/managers/AddonsManager.d.ts +3 -3
  4. package/lib/src/managers/AttachmentManager.d.ts +64 -12
  5. package/lib/src/managers/AuthManager.d.ts +29 -6
  6. package/lib/src/managers/BackgroundJobManager.d.ts +65 -1
  7. package/lib/src/managers/BaseManager.d.ts +14 -0
  8. package/lib/src/managers/BaseManager.js +9 -0
  9. package/lib/src/managers/CommentManager.d.ts +10 -1
  10. package/lib/src/managers/DatabaseManager.d.ts +24 -17
  11. package/lib/src/managers/DatabaseManager.js +72 -29
  12. package/lib/src/managers/EmailManager.d.ts +42 -0
  13. package/lib/src/managers/FootnoteManager.d.ts +10 -1
  14. package/lib/src/managers/NotificationManager.d.ts +13 -0
  15. package/lib/src/managers/OrganizationManager.d.ts +40 -6
  16. package/lib/src/managers/PersonManager.d.ts +106 -9
  17. package/lib/src/managers/RoleManager.d.ts +19 -1
  18. package/lib/src/managers/SessionStatsManager.d.ts +94 -0
  19. package/lib/src/managers/UserManager.d.ts +36 -1
  20. package/lib/src/providers/BaseAttachmentProvider.d.ts +14 -0
  21. package/lib/src/providers/BasicAttachmentProvider.d.ts +26 -2
  22. package/lib/src/providers/SqliteDatabaseProvider.d.ts +7 -0
  23. package/lib/src/providers/SqliteDatabaseProvider.js +16 -0
  24. package/lib/src/providers/SqliteJobStateProvider.d.ts +10 -0
  25. package/lib/src/providers/attachmentFilesBackup.d.ts +52 -0
  26. package/lib/src/providers/sqliteBackup.d.ts +6 -0
  27. package/lib/src/providers/sqliteBackup.js +16 -6
  28. package/lib/src/security/permissions.generated.d.ts +4 -0
  29. package/lib/src/types/Asset.d.ts +10 -0
  30. package/lib/src/types/Config.d.ts +11 -0
  31. package/lib/src/types/Organization.d.ts +7 -0
  32. package/lib/src/types/Person.d.ts +27 -3
  33. package/lib/src/types/PersonProvider.d.ts +6 -0
  34. package/lib/src/types/WikiEngine.d.ts +9 -21
  35. package/lib/src/types/express.d.ts +2 -0
  36. package/lib/src/utils/addonPageSync.d.ts +24 -0
  37. package/lib/src/utils/administrators.d.ts +21 -0
  38. package/lib/src/utils/auditEventNames.generated.d.ts +4 -0
  39. package/lib/src/utils/auditEventNames.generated.js +4 -0
  40. package/lib/src/utils/backupCrypto.d.ts +28 -0
  41. package/lib/src/utils/backupKey.d.ts +23 -0
  42. package/lib/src/utils/backupKey.js +37 -0
  43. package/lib/src/utils/contactAccess.d.ts +58 -0
  44. package/lib/src/utils/databaseKey.d.ts +7 -0
  45. package/lib/src/utils/databaseKey.js +7 -0
  46. package/lib/src/utils/directoryBackup.d.ts +78 -0
  47. package/lib/src/utils/emailForLog.d.ts +20 -0
  48. package/lib/src/utils/maintenanceState.d.ts +153 -0
  49. package/lib/src/utils/privateStoreTakeout.d.ts +2 -2
  50. package/lib/src/utils/sessionAccount.d.ts +26 -0
  51. package/lib/src/utils/sessionEnd.d.ts +1 -1
  52. package/lib/src/utils/siteRestore.d.ts +110 -0
  53. package/lib/src/utils/siteRestore.js +309 -0
  54. package/lib/src/utils/treeArchive.d.ts +44 -0
  55. package/lib/src/utils/username.d.ts +7 -1
  56. package/package.json +2 -2
  57. package/lib/src/utils/startupState.d.ts +0 -65
  58. package/lib/src/utils/startupState.js +0 -115
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The environment variable holding the key every backup is encrypted under.
3
+ * Its own secret, not the database key: the copy that travels and the copy
4
+ * that stays should not fall to the same compromise. Never a config key.
5
+ */
6
+ export declare const BACKUP_KEY_ENV = "NGDPBASE_BACKUP_KEY";
7
+ /**
8
+ * A short fingerprint of a backup key (#1723, D3): the first 16 hex digits of
9
+ * an HMAC of a fixed label under the key. Recorded in each backup so a restore
10
+ * under another key says so plainly; it never reveals the key.
11
+ */
12
+ export declare function backupKeyFingerprint(key: string): string;
13
+ /**
14
+ * How every part left out of a backup for want of the key says so: the
15
+ * message starts with this. BackupManager recognises such a part by it
16
+ * (`isLeftOutForNoKey`): those are covered by its banner and one-time
17
+ * notice, never a notice per backup (operator, #1912).
18
+ */
19
+ export declare const LEFT_OUT_FOR_NO_KEY = "NGDPBASE_BACKUP_KEY is not set, so ";
20
+ /** Whether a backup warning or error says a part was left out only because there is no backup key. */
21
+ export declare function isLeftOutForNoKey(message: string): boolean;
22
+ /** The backup key as set now, trimmed; '' when there is none. Read at use, never cached. */
23
+ export declare function currentBackupKey(): string;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The backup key (#1723, #1561, #1876): the one secret every encrypted part of
3
+ * a backup is made under — the database files (SQLCipher, `sqliteBackup.ts`),
4
+ * and the backup document, the file archives and the credentials store
5
+ * (`backupCrypto.ts`). One declaration here; DatabaseManager and the file
6
+ * parts read it from this module.
7
+ */
8
+ import { createHmac } from 'node:crypto';
9
+ /**
10
+ * The environment variable holding the key every backup is encrypted under.
11
+ * Its own secret, not the database key: the copy that travels and the copy
12
+ * that stays should not fall to the same compromise. Never a config key.
13
+ */
14
+ export const BACKUP_KEY_ENV = 'NGDPBASE_BACKUP_KEY';
15
+ /**
16
+ * A short fingerprint of a backup key (#1723, D3): the first 16 hex digits of
17
+ * an HMAC of a fixed label under the key. Recorded in each backup so a restore
18
+ * under another key says so plainly; it never reveals the key.
19
+ */
20
+ export function backupKeyFingerprint(key) {
21
+ return createHmac('sha256', key).update('ngdpbase backup key fingerprint v1').digest('hex').slice(0, 16);
22
+ }
23
+ /**
24
+ * How every part left out of a backup for want of the key says so: the
25
+ * message starts with this. BackupManager recognises such a part by it
26
+ * (`isLeftOutForNoKey`): those are covered by its banner and one-time
27
+ * notice, never a notice per backup (operator, #1912).
28
+ */
29
+ export const LEFT_OUT_FOR_NO_KEY = `${BACKUP_KEY_ENV} is not set, so `;
30
+ /** Whether a backup warning or error says a part was left out only because there is no backup key. */
31
+ export function isLeftOutForNoKey(message) {
32
+ return message.startsWith(LEFT_OUT_FOR_NO_KEY);
33
+ }
34
+ /** The backup key as set now, trimmed; '' when there is none. Read at use, never cached. */
35
+ export function currentBackupKey() {
36
+ return (process.env[BACKUP_KEY_ENV] ?? '').trim();
37
+ }
@@ -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
+ }
@@ -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[];
@@ -0,0 +1,26 @@
1
+ export type AccountEndReason = 'account-deleted' | 'account-disabled' | 'password-changed';
2
+ interface AccountLike {
3
+ createdAt?: unknown;
4
+ isActive?: boolean;
5
+ sessionGeneration?: number;
6
+ }
7
+ interface SessionLike {
8
+ accountCreatedAt?: unknown;
9
+ signedInAt?: unknown;
10
+ sessionGeneration?: unknown;
11
+ }
12
+ /** The value a session is bound to: the account's creation time, or '' for a record without one. */
13
+ export declare function accountBindingOf(account: AccountLike | null | undefined): string;
14
+ /**
15
+ * What one request (or the sweep) does to a signed-in session, given its
16
+ * account as stored now (null when there is none). `bind` is set on a session
17
+ * signed in before the binding existed that still belongs to its account: the
18
+ * caller writes it to `accountCreatedAt`, once.
19
+ */
20
+ export declare function checkSessionAccount(stored: SessionLike | Record<string, unknown>, account: AccountLike | null | undefined): {
21
+ end: AccountEndReason;
22
+ } | {
23
+ end: null;
24
+ bind?: string;
25
+ };
26
+ export {};
@@ -1,4 +1,4 @@
1
- export type SessionEndReason = 'logout' | 'idle-timeout' | 'max-age' | 'password-changed' | 'superseded';
1
+ export type SessionEndReason = 'logout' | 'idle-timeout' | 'max-age' | 'password-changed' | 'superseded' | 'account-deleted' | 'account-disabled';
2
2
  export declare function endSession(engine: {
3
3
  getManager(name: string): unknown;
4
4
  }, session: {
@@ -0,0 +1,110 @@
1
+ /** The config key naming the site restore manifest. */
2
+ export declare const SITE_RESTORE_MANIFEST_KEY = "ngdpbase.backup.restore-manifest";
3
+ /** Its default: beside the other instance data in FAST_STORAGE. */
4
+ export declare const SITE_RESTORE_MANIFEST_DEFAULT = "./data/site-restore.json";
5
+ /** One live path and the staged copy that replaces it at the next start. */
6
+ export interface StagedSwap {
7
+ live: string;
8
+ staged: string;
9
+ }
10
+ /** One manager's share of a site restore. */
11
+ export interface SiteRestorePart {
12
+ manager: string;
13
+ swaps: StagedSwap[];
14
+ /** Further files to remove when the restore is discarded (DatabaseManager's own manifest). */
15
+ discard?: string[];
16
+ status?: 'applied' | 'set-aside';
17
+ note?: string;
18
+ }
19
+ export interface SiteRestoreManifest {
20
+ version: 1;
21
+ state: 'staging' | 'staged' | 'refused';
22
+ reason?: string;
23
+ /** When staging began. */
24
+ stagedAt: string;
25
+ /** When the backup it came from was taken. */
26
+ backup: string;
27
+ /** The backup document's filename. */
28
+ source: string;
29
+ stagedBy: string;
30
+ /** The backup of the live data taken just before staging: restoring it undoes this one. */
31
+ safetyBackup?: string;
32
+ parts: SiteRestorePart[];
33
+ }
34
+ /** The staged copy of a live path: beside it, so staging and applying name the same path. */
35
+ export declare const stagedPathFor: (live: string) => string;
36
+ /** Where a live path is kept once a restore has replaced it. */
37
+ export declare const keptPathFor: (live: string) => string;
38
+ /** The manifest file for this site, read through the configuration (never a constant). */
39
+ export declare function siteRestoreFile(configManager: {
40
+ getResolvedDataPath(key: string, def: string): string;
41
+ } | null | undefined): string;
42
+ /**
43
+ * The manifest, or null when there is none. Throws when a file is there but
44
+ * is not a manifest: a restore record that can't be read is never ignored.
45
+ */
46
+ export declare function readSiteRestore(file: string): SiteRestoreManifest | null;
47
+ /** Start staging a site restore: the record exists before any part is staged, so an interrupted staging is recognised. */
48
+ export declare function beginSiteRestore(file: string, meta: Omit<SiteRestoreManifest, 'version' | 'state' | 'parts'>): void;
49
+ /** Record one owner's staged part. Only while staging. */
50
+ export declare function recordStagedPart(file: string, part: SiteRestorePart): void;
51
+ /** The commit: every part is staged. */
52
+ export declare function commitSiteRestore(file: string): SiteRestoreManifest;
53
+ /**
54
+ * Throw a staged (or half-staged) site restore away: every part's staged
55
+ * files and the manifest are removed. Live data is never touched.
56
+ */
57
+ export declare function discardSiteRestore(file: string): SiteRestoreManifest | null;
58
+ /** Move a staged path aside as `*.incomplete`, so it is neither applied nor retried. */
59
+ export declare function setAsidePath(staged: string): void;
60
+ export type PartGate = {
61
+ decision: 'none';
62
+ } | {
63
+ decision: 'apply';
64
+ part: SiteRestorePart;
65
+ manifest: SiteRestoreManifest;
66
+ } | {
67
+ decision: 'set-aside';
68
+ reason: string;
69
+ part?: SiteRestorePart;
70
+ };
71
+ /**
72
+ * Whether `manager`'s part of a site restore is to be applied now. `none`:
73
+ * there is no site restore, or it has no part for this manager, or that part
74
+ * is done. `apply`: the restore is committed and every part still pending has
75
+ * its staged files. `set-aside`: it isn't, and the set is marked refused.
76
+ */
77
+ export declare function gatePart(file: string, manager: string): PartGate;
78
+ /**
79
+ * Record what became of `manager`'s part. A part set aside refuses the rest
80
+ * of the set. When every part is done the manifest becomes `<file>.done`.
81
+ */
82
+ export declare function markPart(file: string, manager: string, status: 'applied' | 'set-aside', note?: string): void;
83
+ export interface PartOutcome {
84
+ /** Each live path now restored, with where its previous content was kept. */
85
+ applied: Array<{
86
+ live: string;
87
+ kept: string;
88
+ }>;
89
+ /** Why nothing (or, after a failed rename, not everything) was applied. */
90
+ refused?: string;
91
+ }
92
+ /**
93
+ * Apply `manager`'s part of a staged site restore, at start, before the
94
+ * manager reads its data. `allowed` says which live paths this manager may
95
+ * replace, so a tampered manifest cannot rename an arbitrary file.
96
+ * `strays` lists staged paths of this manager's that exist on disk, found by
97
+ * the manager: with no committed restore naming them they are set aside.
98
+ * Returns null when there was nothing to do.
99
+ */
100
+ export declare function applyPartAtStart(file: string, manager: string, options: {
101
+ allowed: (swap: StagedSwap) => boolean;
102
+ strays?: () => string[];
103
+ }): PartOutcome | null;
104
+ /**
105
+ * What a start did with a site restore, for BackupManager to tell admins at
106
+ * the end of the start: the finished record (`<file>.done`, then kept as
107
+ * `<file>.last`), or a record with parts still pending — their managers did
108
+ * not start — which is then refused and its pending parts set aside.
109
+ */
110
+ export declare function finishSiteRestoreAtBoot(file: string): SiteRestoreManifest | null;