@jwilleke/ngdpbase-types 5.25.0 → 5.27.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/README.md +1 -1
- package/lib/packages/ngdpbase-types/index.d.ts +9 -2
- package/lib/packages/ngdpbase-types/testing.d.ts +7 -3
- package/lib/packages/ngdpbase-types/testing.js +7 -3
- package/lib/src/converters/DocxConverter.d.ts +30 -0
- package/lib/src/converters/MarkdownConverter.d.ts +31 -0
- package/lib/src/export/ChromeServicePdfProvider.d.ts +52 -0
- package/lib/src/export/ChromeServicePdfProvider.js +153 -0
- package/lib/src/export/LocalChromePdfProvider.d.ts +25 -0
- package/lib/src/export/LocalChromePdfProvider.js +33 -0
- package/lib/src/export/PdfProvider.d.ts +51 -0
- package/lib/src/export/PdfProvider.js +37 -0
- package/lib/src/export/pdfEngine.d.ts +113 -0
- package/lib/src/export/pdfEngine.js +195 -0
- package/lib/src/http/egressPolicy.js +61 -0
- package/lib/src/http/guardedElasticsearch.d.ts +27 -0
- package/lib/src/http/guardedElasticsearch.js +99 -0
- package/lib/src/ice/converterShim.d.ts +5 -0
- package/lib/src/ice/fileHandling.d.ts +30 -0
- package/lib/src/ice/ledger.d.ts +37 -0
- package/lib/src/ice/runImport.d.ts +51 -0
- package/lib/src/ice/types.d.ts +262 -0
- package/lib/src/mail/MailProvider.d.ts +15 -0
- package/lib/src/mail/NodemailerMailProvider.d.ts +1 -1
- package/lib/src/managers/AddonsManager.d.ts +55 -3
- package/lib/src/managers/AttachmentManager.d.ts +4 -5
- package/lib/src/managers/AuthManager.d.ts +150 -32
- package/lib/src/managers/BackgroundJobManager.d.ts +22 -12
- package/lib/src/managers/BackupManager.d.ts +779 -0
- package/lib/src/managers/BackupManager.js +2042 -0
- package/lib/src/managers/BaseManager.d.ts +156 -6
- package/lib/src/managers/BaseManager.js +64 -2
- package/lib/src/managers/CommentManager.d.ts +6 -6
- package/lib/src/managers/ConsentManager.d.ts +130 -0
- package/lib/src/managers/DatabaseManager.d.ts +6 -0
- package/lib/src/managers/DatabaseManager.js +25 -15
- package/lib/src/managers/EmailManager.d.ts +32 -5
- package/lib/src/managers/ExportManager.d.ts +62 -0
- package/lib/src/managers/ExportManager.js +147 -2
- package/lib/src/managers/FootnoteManager.d.ts +6 -6
- package/lib/src/managers/ImportManager.d.ts +590 -0
- package/lib/src/managers/OrganizationManager.d.ts +8 -6
- package/lib/src/managers/PageManager.d.ts +61 -1
- package/lib/src/managers/PersonManager.d.ts +8 -6
- package/lib/src/managers/RoleManager.d.ts +6 -8
- package/lib/src/providers/BaseAttachmentProvider.d.ts +4 -4
- package/lib/src/providers/BaseBackupProvider.d.ts +77 -0
- package/lib/src/providers/BaseBackupProvider.js +52 -0
- package/lib/src/providers/BasicAttachmentProvider.d.ts +4 -4
- package/lib/src/providers/FileBackupProvider.d.ts +34 -0
- package/lib/src/providers/FileBackupProvider.js +116 -0
- package/lib/src/providers/FileConsentProvider.d.ts +18 -0
- package/lib/src/providers/SqliteDatabaseProvider.d.ts +2 -2
- package/lib/src/providers/SqliteDatabaseProvider.js +12 -11
- package/lib/src/providers/attachmentFilesBackup.d.ts +5 -4
- package/lib/src/providers/databaseMigrations.js +45 -0
- package/lib/src/security/PolicyDecisionPoint.d.ts +14 -0
- package/lib/src/security/PolicyInformationPoint.d.ts +25 -2
- package/lib/src/types/Config.d.ts +2 -0
- package/lib/src/types/Consent.d.ts +90 -0
- package/lib/src/types/express.d.ts +6 -0
- package/lib/src/utils/addonHost.d.ts +45 -2
- package/lib/src/utils/addonHost.js +39 -3
- package/lib/src/utils/auditEventNames.generated.d.ts +20 -0
- package/lib/src/utils/auditEventNames.generated.js +20 -0
- package/lib/src/utils/backupCrypto.js +275 -0
- package/lib/src/utils/backupDocument.d.ts +36 -0
- package/lib/src/utils/backupDocument.js +99 -0
- package/lib/src/utils/browserDescription.d.ts +11 -0
- package/lib/src/utils/directoryBackup.d.ts +48 -48
- package/lib/src/utils/directoryBackup.js +291 -0
- package/lib/src/utils/frontmatterAccess.d.ts +54 -11
- package/lib/src/utils/importRunSummary.d.ts +64 -0
- package/lib/src/utils/instanceEnvBackup.d.ts +46 -0
- package/lib/src/utils/instanceEnvBackup.js +120 -0
- package/lib/src/utils/instanceEnvSecret.js +79 -0
- package/lib/src/utils/ipLocation.d.ts +11 -0
- package/lib/src/utils/ncmNotify.d.ts +30 -0
- package/lib/src/utils/privateStoreImport.d.ts +58 -0
- package/lib/src/utils/schedule.js +171 -0
- package/lib/src/utils/secretFileMode.js +78 -0
- package/lib/src/utils/siteRestore.d.ts +13 -16
- package/lib/src/utils/siteRestore.js +33 -6
- package/lib/src/utils/treeArchive.d.ts +2 -0
- package/lib/src/utils/treeArchive.js +242 -0
- package/lib/src/utils/version.js +408 -0
- package/lib/src/utils/zipArchive.d.ts +71 -0
- package/package.json +6 -1
- package/lib/src/utils/administrators.d.ts +0 -21
|
@@ -184,10 +184,20 @@ export type PageRead<T> = {
|
|
|
184
184
|
export interface PageConvertResult extends NcmResult {
|
|
185
185
|
fixes: FixChange[];
|
|
186
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* A write refused because the page's `audience` leaves the writer out (#1964).
|
|
189
|
+
* Someone outside a page's audience may neither read nor edit it; an
|
|
190
|
+
* administrator (`admin-system`) is the exception. Routes answer it with 403.
|
|
191
|
+
*/
|
|
192
|
+
export declare class PageAudienceRefusedError extends Error {
|
|
193
|
+
readonly pageName: string;
|
|
194
|
+
constructor(pageName: string);
|
|
195
|
+
}
|
|
187
196
|
export declare class PageContentValidationError extends Error {
|
|
188
197
|
readonly validationErrors: FilterValidationError[];
|
|
189
198
|
constructor(pageName: string, validationErrors: FilterValidationError[]);
|
|
190
199
|
}
|
|
200
|
+
import { type PageAction } from '../utils/pageUrl.js';
|
|
191
201
|
/** One vault page with data outside its vault (#1842), as the repair actions offer it. */
|
|
192
202
|
export interface VaultRepairItem {
|
|
193
203
|
owner: string;
|
|
@@ -796,10 +806,22 @@ declare class PageManager extends BaseManager implements CatalogSource {
|
|
|
796
806
|
* the normal getPage() path sanitises or normalises away. Returns null
|
|
797
807
|
* when the page is unknown or the provider doesn't support raw reads.
|
|
798
808
|
*/
|
|
799
|
-
getRawPageContent(identifier: string): Promise<{
|
|
809
|
+
getRawPageContent(identifier: string, ctx: ActorContext): Promise<{
|
|
800
810
|
filePath: string;
|
|
801
811
|
content: string;
|
|
802
812
|
} | null>;
|
|
813
|
+
/**
|
|
814
|
+
* The write door's audience check (#1964, operator 2026-10-23): someone a
|
|
815
|
+
* page's `audience` leaves out may neither read nor edit it, by any route —
|
|
816
|
+
* the editor, the raw editor, the API, a rename, a delete. The rule is the
|
|
817
|
+
* PIP's (`audienceAdmits`, the same one Tier 1 applies), asked with the
|
|
818
|
+
* caller's context; an administrator (`admin-system`) is admitted. A page
|
|
819
|
+
* that states no audience is not checked here — the routes ask the full
|
|
820
|
+
* page decision before they call the door.
|
|
821
|
+
*
|
|
822
|
+
* @throws PageAudienceRefusedError when the audience leaves `ctx` out
|
|
823
|
+
*/
|
|
824
|
+
private assertAudienceAdmits;
|
|
803
825
|
/**
|
|
804
826
|
* Admin-override save (#689): the admin "Edit raw" textarea, split into
|
|
805
827
|
* frontmatter and body and written through the door (#1462).
|
|
@@ -1289,6 +1311,29 @@ declare class PageManager extends BaseManager implements CatalogSource {
|
|
|
1289
1311
|
* @param action - Legacy verb (`view`, `edit`) or policy action; default `view`
|
|
1290
1312
|
*/
|
|
1291
1313
|
filterPagesFor(subject: ActorContext | PermissionSubject | null | undefined, names: readonly string[], action?: string): Promise<string[]>;
|
|
1314
|
+
/**
|
|
1315
|
+
* The `addon:<slug>` entries on a page that can match nobody (#1964): the
|
|
1316
|
+
* slugs it names in `audience` or `access` that are not a loaded per-person
|
|
1317
|
+
* add-on. Such an entry still decides — it refuses everyone it would have
|
|
1318
|
+
* let in — so the author is told.
|
|
1319
|
+
*
|
|
1320
|
+
* @param metadata - The page's frontmatter
|
|
1321
|
+
* @returns The slugs, in the order the page names them
|
|
1322
|
+
*/
|
|
1323
|
+
unmatchableAddonAudience(metadata: Partial<PageFrontmatter> | null | undefined): string[];
|
|
1324
|
+
/** Warn, naming the page and each slug, about {@link unmatchableAddonAudience}. */
|
|
1325
|
+
private warnUnmatchableAddonAudience;
|
|
1326
|
+
/**
|
|
1327
|
+
* At startup, once add-ons have loaded (#1964): warn about every page whose
|
|
1328
|
+
* `audience` or `access` names an add-on that can match nobody — unknown,
|
|
1329
|
+
* not per-person, or not loaded.
|
|
1330
|
+
*
|
|
1331
|
+
* @returns Each page and slug warned about
|
|
1332
|
+
*/
|
|
1333
|
+
reportUnmatchableAddonAudiences(): Promise<Array<{
|
|
1334
|
+
page: string;
|
|
1335
|
+
slug: string;
|
|
1336
|
+
}>>;
|
|
1292
1337
|
/**
|
|
1293
1338
|
* Get all page titles (explicit alias for getAllPages)
|
|
1294
1339
|
* Prefer this for new code that only needs page names.
|
|
@@ -1405,6 +1450,21 @@ declare class PageManager extends BaseManager implements CatalogSource {
|
|
|
1405
1450
|
creator: string;
|
|
1406
1451
|
store: string;
|
|
1407
1452
|
} | null>;
|
|
1453
|
+
/**
|
|
1454
|
+
* The URL for `action` on the page named `pageName` (#1813): `/view/Main`,
|
|
1455
|
+
* or for a vault page `/vaults/{owner}/{vault}/{title}` and its actions below
|
|
1456
|
+
* it (#1456). The door an add-on asks; core's routes and views share the
|
|
1457
|
+
* same `utils/pageUrl`. Reads no data and no config.
|
|
1458
|
+
*/
|
|
1459
|
+
pageUrl(pageName: string, action?: PageAction): string;
|
|
1460
|
+
/**
|
|
1461
|
+
* The page name of `title` in `owner`'s vault `vault`, or in their default
|
|
1462
|
+
* vault when no vault is given (#1813): `vaults/{owner}/{vault}/{title}`.
|
|
1463
|
+
* The default vault is the system-category entry marked default, read live
|
|
1464
|
+
* through ConfigurationManager (#1504). It names a page; it does not say the
|
|
1465
|
+
* page exists or that anyone may open it.
|
|
1466
|
+
*/
|
|
1467
|
+
privatePageName(owner: string, title: string, vault?: string): string;
|
|
1408
1468
|
refreshPageList(): Promise<void>;
|
|
1409
1469
|
/**
|
|
1410
1470
|
* #1374: rewrite the provider's persistent page index from its last disk scan
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import BaseManager, { type
|
|
1
|
+
import BaseManager, { type BackupFolder, type ManagerStats } from './BaseManager.js';
|
|
2
2
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
3
3
|
import type { Person, PersonInput, PersonUpdate } from '../types/Person.js';
|
|
4
4
|
import type { PersonProvider } from '../types/PersonProvider.js';
|
|
@@ -48,11 +48,13 @@ declare class PersonManager extends BaseManager {
|
|
|
48
48
|
private linkOffers;
|
|
49
49
|
constructor(engine: WikiEngine);
|
|
50
50
|
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
/** `ngdpbase.application.persons.storagedir`, resolved: where the records are. */
|
|
52
|
+
private configuredStorageDir;
|
|
53
|
+
/**
|
|
54
|
+
* Person records are in the backup (#1876), as `<stem>.persons.tar` beside the document. BackupManager backs it up, stages it in a site restore and puts it
|
|
55
|
+
* in place at the next start, before the records are read (#1985).
|
|
56
|
+
*/
|
|
57
|
+
backupFolders(): BackupFolder[];
|
|
56
58
|
getProvider(): PersonProvider | null;
|
|
57
59
|
getById(id: string): Promise<Person | null>;
|
|
58
60
|
getByIdentifier(identifier: string): Promise<Person | null>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import BaseManager, { type
|
|
1
|
+
import BaseManager, { type BackupFolder } from './BaseManager.js';
|
|
2
2
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
3
3
|
import type { Role, RoleUpdate } from '../types/Role.js';
|
|
4
4
|
import type { RoleProvider } from '../types/RoleProvider.js';
|
|
@@ -33,15 +33,13 @@ declare class RoleManager extends BaseManager {
|
|
|
33
33
|
* as is any other `additionalProperty` entry.
|
|
34
34
|
*/
|
|
35
35
|
private stripCatalogueCopies;
|
|
36
|
+
/** `ngdpbase.application.roles.storagedir`, resolved: where the records are. */
|
|
37
|
+
private configuredStorageDir;
|
|
36
38
|
/**
|
|
37
|
-
* Role membership is in the backup (#1876): the role records folder, as
|
|
38
|
-
*
|
|
39
|
-
* have no roles.
|
|
39
|
+
* Role membership is in the backup (#1876): the role records folder, as `<stem>.roles.tar` beside the document. Without it restored users would have no roles. BackupManager backs it up, stages it in a site restore and puts it
|
|
40
|
+
* in place at the next start, before the records are read (#1985).
|
|
40
41
|
*/
|
|
41
|
-
|
|
42
|
-
/** Staged for the next start with the rest of a site restore (#1876); the live records are untouched. */
|
|
43
|
-
restore(backupData: BackupData, options?: ManagerRestoreOptions): Promise<void>;
|
|
44
|
-
restoreIsStaged(): boolean;
|
|
42
|
+
backupFolders(): BackupFolder[];
|
|
45
43
|
/** Provider accessor, mainly for tests. */
|
|
46
44
|
getProvider(): RoleProvider | null;
|
|
47
45
|
getById(id: string): Promise<Role | null>;
|
|
@@ -4,7 +4,7 @@ import type { WikiEngine } from '../types/WikiEngine.js';
|
|
|
4
4
|
import { ProviderInfo } from './BasePageProvider.js';
|
|
5
5
|
import BaseProvider from './BaseProvider.js';
|
|
6
6
|
import type { AttachmentFilesBackupData, AttachmentFilesBackupOutcome } from './attachmentFilesBackup.js';
|
|
7
|
-
import type {
|
|
7
|
+
import type { BackupArchiveTarget, ManagerRestoreOptions, SiteRestoreStaging, StagedRestoreAtStart } from '../managers/BaseManager.js';
|
|
8
8
|
/**
|
|
9
9
|
* File information for attachment uploads
|
|
10
10
|
*/
|
|
@@ -194,13 +194,13 @@ declare abstract class BaseAttachmentProvider extends BaseProvider implements At
|
|
|
194
194
|
* streamed, bytes as stored. A provider without it can't be backed up whole,
|
|
195
195
|
* and AttachmentManager's backup says so.
|
|
196
196
|
*/
|
|
197
|
-
backupFiles?(
|
|
197
|
+
backupFiles?(archive: (part: string) => BackupArchiveTarget): Promise<AttachmentFilesBackupOutcome>;
|
|
198
198
|
/** Stage the files and metadata from a backup for the next start, as part of a site restore (#1876). */
|
|
199
|
-
stageFilesRestore?(providerBackup: Record<string, unknown>, files: AttachmentFilesBackupData,
|
|
199
|
+
stageFilesRestore?(providerBackup: Record<string, unknown>, files: AttachmentFilesBackupData, readArchive: NonNullable<ManagerRestoreOptions['readArchive']>, siteRestore: SiteRestoreStaging): Promise<{
|
|
200
200
|
skippedVaults: string[];
|
|
201
201
|
}>;
|
|
202
202
|
/** What the start did with a staged restore of the files (#1876), once; null when there was none. */
|
|
203
|
-
takeRestoreAtStart?():
|
|
203
|
+
takeRestoreAtStart?(): StagedRestoreAtStart | null;
|
|
204
204
|
/**
|
|
205
205
|
* Shutdown the provider (cleanup resources)
|
|
206
206
|
* @returns {Promise<void>}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseBackupProvider - Base class for backup storage providers (Issue #170)
|
|
3
|
+
*
|
|
4
|
+
* Abstracts ONLY the backup storage target — where the assembled archive is
|
|
5
|
+
* written/read/listed/pruned. The *content* side (calling backup() on every
|
|
6
|
+
* manager, gzip, JSON, validation, retention policy, the auto-backup
|
|
7
|
+
* scheduler) stays in BackupManager and is unchanged: per-manager
|
|
8
|
+
* serialization was already provider-abstracted via `manager.backup()`, so
|
|
9
|
+
* #170 deliberately does not re-abstract it.
|
|
10
|
+
*
|
|
11
|
+
* Consistent with the engine-based Base*Provider family
|
|
12
|
+
* (Search/Audit/Cache/...). Unlike BaseLoggingProvider (#169), backup runs
|
|
13
|
+
* well after engine init, so the standard engine + initialize() shape applies.
|
|
14
|
+
*
|
|
15
|
+
* @class BaseBackupProvider
|
|
16
|
+
* @abstract
|
|
17
|
+
*
|
|
18
|
+
* @see {@link FileBackupProvider} for the default local-filesystem implementation
|
|
19
|
+
* @see {@link BackupManager} for usage
|
|
20
|
+
*/
|
|
21
|
+
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
22
|
+
import BaseProvider from './BaseProvider.js';
|
|
23
|
+
/** Metadata for a stored backup object */
|
|
24
|
+
export interface BackupObjectInfo {
|
|
25
|
+
filename: string;
|
|
26
|
+
path: string;
|
|
27
|
+
size: number;
|
|
28
|
+
created: Date;
|
|
29
|
+
modified: Date;
|
|
30
|
+
}
|
|
31
|
+
/** Provider metadata, consistent with the other Base*Provider classes */
|
|
32
|
+
export interface BackupProviderInfo {
|
|
33
|
+
name: string;
|
|
34
|
+
version: string;
|
|
35
|
+
description: string;
|
|
36
|
+
features: string[];
|
|
37
|
+
}
|
|
38
|
+
declare abstract class BaseBackupProvider extends BaseProvider {
|
|
39
|
+
protected engine: WikiEngine;
|
|
40
|
+
protected initialized: boolean;
|
|
41
|
+
constructor(engine: WikiEngine);
|
|
42
|
+
/** Load config and prepare the storage target (e.g. ensure the directory). */
|
|
43
|
+
abstract initialize(): Promise<void>;
|
|
44
|
+
/** The resolved backup container location (directory / bucket prefix). */
|
|
45
|
+
abstract getBackupDirectory(): string;
|
|
46
|
+
/** Ensure the storage container exists. */
|
|
47
|
+
abstract ensureContainer(): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Persist a backup object.
|
|
50
|
+
*
|
|
51
|
+
* @param filename - Object name (no path)
|
|
52
|
+
* @param data - Serialized (optionally gzip-compressed) payload
|
|
53
|
+
* @returns The full path / identifier of the written object
|
|
54
|
+
*/
|
|
55
|
+
abstract writeBackup(filename: string, data: string | Buffer): Promise<string>;
|
|
56
|
+
/** Read a backup object by full path or bare filename. */
|
|
57
|
+
abstract readBackup(idOrPath: string): Promise<Buffer>;
|
|
58
|
+
/** Whether a backup object exists, by full path or bare filename. */
|
|
59
|
+
abstract backupExists(idOrPath: string): Promise<boolean>;
|
|
60
|
+
/** All objects in the container (BackupManager applies its own name filter). */
|
|
61
|
+
abstract listBackupObjects(): Promise<BackupObjectInfo[]>;
|
|
62
|
+
/** Delete a backup object by full path or bare filename. */
|
|
63
|
+
abstract removeBackup(idOrPath: string): Promise<void>;
|
|
64
|
+
/** Re-point the storage target at runtime (admin changes the directory). */
|
|
65
|
+
abstract setBackupDirectory(directory: string): Promise<void>;
|
|
66
|
+
/** Provider metadata. */
|
|
67
|
+
getProviderInfo(): BackupProviderInfo;
|
|
68
|
+
/**
|
|
69
|
+
* Free bytes at the storage target, or null when this provider can't tell
|
|
70
|
+
* (#1563). BackupManager compares it with the last backup's size before
|
|
71
|
+
* writing, and shows it on Admin → Backup.
|
|
72
|
+
*/
|
|
73
|
+
freeSpace(): Promise<number | null>;
|
|
74
|
+
/** Health check — overridable by providers with remote dependencies. */
|
|
75
|
+
isHealthy(): Promise<boolean>;
|
|
76
|
+
}
|
|
77
|
+
export default BaseBackupProvider;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseBackupProvider - Base class for backup storage providers (Issue #170)
|
|
3
|
+
*
|
|
4
|
+
* Abstracts ONLY the backup storage target — where the assembled archive is
|
|
5
|
+
* written/read/listed/pruned. The *content* side (calling backup() on every
|
|
6
|
+
* manager, gzip, JSON, validation, retention policy, the auto-backup
|
|
7
|
+
* scheduler) stays in BackupManager and is unchanged: per-manager
|
|
8
|
+
* serialization was already provider-abstracted via `manager.backup()`, so
|
|
9
|
+
* #170 deliberately does not re-abstract it.
|
|
10
|
+
*
|
|
11
|
+
* Consistent with the engine-based Base*Provider family
|
|
12
|
+
* (Search/Audit/Cache/...). Unlike BaseLoggingProvider (#169), backup runs
|
|
13
|
+
* well after engine init, so the standard engine + initialize() shape applies.
|
|
14
|
+
*
|
|
15
|
+
* @class BaseBackupProvider
|
|
16
|
+
* @abstract
|
|
17
|
+
*
|
|
18
|
+
* @see {@link FileBackupProvider} for the default local-filesystem implementation
|
|
19
|
+
* @see {@link BackupManager} for usage
|
|
20
|
+
*/
|
|
21
|
+
import BaseProvider from './BaseProvider.js';
|
|
22
|
+
class BaseBackupProvider extends BaseProvider {
|
|
23
|
+
engine;
|
|
24
|
+
initialized;
|
|
25
|
+
constructor(engine) {
|
|
26
|
+
super();
|
|
27
|
+
this.engine = engine;
|
|
28
|
+
this.initialized = false;
|
|
29
|
+
}
|
|
30
|
+
/** Provider metadata. */
|
|
31
|
+
getProviderInfo() {
|
|
32
|
+
return {
|
|
33
|
+
name: 'BaseBackupProvider',
|
|
34
|
+
version: '1.0.0',
|
|
35
|
+
description: 'Base backup storage provider interface',
|
|
36
|
+
features: []
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Free bytes at the storage target, or null when this provider can't tell
|
|
41
|
+
* (#1563). BackupManager compares it with the last backup's size before
|
|
42
|
+
* writing, and shows it on Admin → Backup.
|
|
43
|
+
*/
|
|
44
|
+
async freeSpace() {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/** Health check — overridable by providers with remote dependencies. */
|
|
48
|
+
async isHealthy() {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export default BaseBackupProvider;
|
|
@@ -5,7 +5,7 @@ import type { AssetProvider, AssetRecord, AssetQuery, AssetPage, AssetInput, Ass
|
|
|
5
5
|
import type { CreativeWork } from '../types/Schema.js';
|
|
6
6
|
import type { ActorContext } from '../context/ActorContext.js';
|
|
7
7
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
8
|
-
import {
|
|
8
|
+
import type { BackupArchiveTarget, ManagerRestoreOptions, SiteRestoreStaging, StagedRestoreAtStart } from '../managers/BaseManager.js';
|
|
9
9
|
import { type AttachmentFilesBackupData, type AttachmentFilesBackupOutcome } from './attachmentFilesBackup.js';
|
|
10
10
|
/**
|
|
11
11
|
* Schema.org Person metadata
|
|
@@ -449,16 +449,16 @@ declare class BasicAttachmentProvider extends BaseAttachmentProvider implements
|
|
|
449
449
|
/** Where the attachment files are, for their backup and staged restore (#1876). */
|
|
450
450
|
private filePlaces;
|
|
451
451
|
/** The files themselves, beside the backup document (#1876): the shared pool and every vault's, as stored. */
|
|
452
|
-
backupFiles(
|
|
452
|
+
backupFiles(archive: (part: string) => BackupArchiveTarget): Promise<AttachmentFilesBackupOutcome>;
|
|
453
453
|
/**
|
|
454
454
|
* Stage the files and the shared metadata from a backup for the next start
|
|
455
455
|
* (#1876). The metadata is the document's; the live files and metadata are
|
|
456
456
|
* not touched.
|
|
457
457
|
*/
|
|
458
|
-
stageFilesRestore(providerBackup: Record<string, unknown>, files: AttachmentFilesBackupData,
|
|
458
|
+
stageFilesRestore(providerBackup: Record<string, unknown>, files: AttachmentFilesBackupData, readArchive: NonNullable<ManagerRestoreOptions['readArchive']>, siteRestore: SiteRestoreStaging): Promise<{
|
|
459
459
|
skippedVaults: string[];
|
|
460
460
|
}>;
|
|
461
|
-
takeRestoreAtStart():
|
|
461
|
+
takeRestoreAtStart(): StagedRestoreAtStart | null;
|
|
462
462
|
/**
|
|
463
463
|
* Restore provider data from backup
|
|
464
464
|
* @param backupData - Backup data
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileBackupProvider - Default backup storage provider (Issue #170)
|
|
3
|
+
*
|
|
4
|
+
* Local-filesystem storage target for BackupManager. This is the I/O
|
|
5
|
+
* behaviour BackupManager performed inline before #170 (fs-extra against
|
|
6
|
+
* `ngdpbase.backup.directory`), extracted verbatim behind
|
|
7
|
+
* {@link BaseBackupProvider} so the storage backend is swappable without
|
|
8
|
+
* changing the orchestration/serialization logic.
|
|
9
|
+
*
|
|
10
|
+
* @class FileBackupProvider
|
|
11
|
+
* @extends BaseBackupProvider
|
|
12
|
+
*/
|
|
13
|
+
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
14
|
+
import BaseBackupProvider, { BackupObjectInfo, BackupProviderInfo } from './BaseBackupProvider.js';
|
|
15
|
+
declare class FileBackupProvider extends BaseBackupProvider {
|
|
16
|
+
private backupDirectory;
|
|
17
|
+
constructor(engine: WikiEngine);
|
|
18
|
+
initialize(): Promise<void>;
|
|
19
|
+
getBackupDirectory(): string;
|
|
20
|
+
ensureContainer(): Promise<void>;
|
|
21
|
+
writeBackup(filename: string, data: string | Buffer): Promise<string>;
|
|
22
|
+
readBackup(idOrPath: string): Promise<Buffer>;
|
|
23
|
+
backupExists(idOrPath: string): Promise<boolean>;
|
|
24
|
+
listBackupObjects(): Promise<BackupObjectInfo[]>;
|
|
25
|
+
removeBackup(idOrPath: string): Promise<void>;
|
|
26
|
+
setBackupDirectory(directory: string): Promise<void>;
|
|
27
|
+
/** Free bytes on the backup directory's filesystem for this process (#1563). */
|
|
28
|
+
freeSpace(): Promise<number | null>;
|
|
29
|
+
getProviderInfo(): BackupProviderInfo;
|
|
30
|
+
/** Absolute paths (from listBackupObjects / createBackup) pass through; bare
|
|
31
|
+
* names are resolved against the backup directory. */
|
|
32
|
+
private resolve;
|
|
33
|
+
}
|
|
34
|
+
export default FileBackupProvider;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileBackupProvider - Default backup storage provider (Issue #170)
|
|
3
|
+
*
|
|
4
|
+
* Local-filesystem storage target for BackupManager. This is the I/O
|
|
5
|
+
* behaviour BackupManager performed inline before #170 (fs-extra against
|
|
6
|
+
* `ngdpbase.backup.directory`), extracted verbatim behind
|
|
7
|
+
* {@link BaseBackupProvider} so the storage backend is swappable without
|
|
8
|
+
* changing the orchestration/serialization logic.
|
|
9
|
+
*
|
|
10
|
+
* @class FileBackupProvider
|
|
11
|
+
* @extends BaseBackupProvider
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'fs-extra';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { statfs } from 'node:fs/promises';
|
|
16
|
+
import BaseBackupProvider from './BaseBackupProvider.js';
|
|
17
|
+
import { chmodSecretFile, ensureSecretDir, SECRET_FILE_MODE } from '../utils/secretFileMode.js';
|
|
18
|
+
import logger from '../utils/logger.js';
|
|
19
|
+
class FileBackupProvider extends BaseBackupProvider {
|
|
20
|
+
backupDirectory = '';
|
|
21
|
+
constructor(engine) {
|
|
22
|
+
super(engine);
|
|
23
|
+
}
|
|
24
|
+
async initialize() {
|
|
25
|
+
const configManager = this.engine.getManager('ConfigurationManager');
|
|
26
|
+
if (!configManager) {
|
|
27
|
+
throw new Error('FileBackupProvider requires ConfigurationManager');
|
|
28
|
+
}
|
|
29
|
+
// Uses getResolvedDataPath to support INSTANCE_DATA_FOLDER (same key/default
|
|
30
|
+
// BackupManager used before #170).
|
|
31
|
+
this.backupDirectory = configManager.getResolvedDataPath('ngdpbase.backup.directory', './data/backups');
|
|
32
|
+
await this.ensureContainer();
|
|
33
|
+
this.initialized = true;
|
|
34
|
+
}
|
|
35
|
+
getBackupDirectory() {
|
|
36
|
+
return this.backupDirectory;
|
|
37
|
+
}
|
|
38
|
+
async ensureContainer() {
|
|
39
|
+
if (this.backupDirectory) {
|
|
40
|
+
// #1560: backups hold the user store; owner-only where the filesystem allows.
|
|
41
|
+
const warning = ensureSecretDir(this.backupDirectory);
|
|
42
|
+
if (warning)
|
|
43
|
+
logger.warn(warning);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async writeBackup(filename, data) {
|
|
47
|
+
const target = path.join(this.backupDirectory, filename);
|
|
48
|
+
await fs.writeFile(target, data, { mode: SECRET_FILE_MODE });
|
|
49
|
+
const warning = chmodSecretFile(target); // #1560: the mode above is filtered by the umask
|
|
50
|
+
if (warning)
|
|
51
|
+
logger.warn(warning);
|
|
52
|
+
return target;
|
|
53
|
+
}
|
|
54
|
+
async readBackup(idOrPath) {
|
|
55
|
+
return fs.readFile(this.resolve(idOrPath));
|
|
56
|
+
}
|
|
57
|
+
async backupExists(idOrPath) {
|
|
58
|
+
return fs.pathExists(this.resolve(idOrPath));
|
|
59
|
+
}
|
|
60
|
+
async listBackupObjects() {
|
|
61
|
+
if (!this.backupDirectory)
|
|
62
|
+
return [];
|
|
63
|
+
const dir = this.backupDirectory;
|
|
64
|
+
const files = await fs.readdir(dir);
|
|
65
|
+
const objects = await Promise.all(files.map(async (filename) => {
|
|
66
|
+
const filePath = path.join(dir, filename);
|
|
67
|
+
let stats;
|
|
68
|
+
try {
|
|
69
|
+
stats = await fs.stat(filePath);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
// #1563: a file removed between the listing and its stat (a backup
|
|
73
|
+
// pruned meanwhile) is no longer there to list; anything else is a fault.
|
|
74
|
+
if (err.code === 'ENOENT')
|
|
75
|
+
return null;
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
filename,
|
|
80
|
+
path: filePath,
|
|
81
|
+
size: stats.size,
|
|
82
|
+
created: stats.birthtime,
|
|
83
|
+
modified: stats.mtime
|
|
84
|
+
};
|
|
85
|
+
}));
|
|
86
|
+
return objects.filter((o) => o !== null);
|
|
87
|
+
}
|
|
88
|
+
async removeBackup(idOrPath) {
|
|
89
|
+
await fs.remove(this.resolve(idOrPath));
|
|
90
|
+
}
|
|
91
|
+
async setBackupDirectory(directory) {
|
|
92
|
+
this.backupDirectory = directory;
|
|
93
|
+
await this.ensureContainer();
|
|
94
|
+
}
|
|
95
|
+
/** Free bytes on the backup directory's filesystem for this process (#1563). */
|
|
96
|
+
async freeSpace() {
|
|
97
|
+
if (!this.backupDirectory)
|
|
98
|
+
return null;
|
|
99
|
+
const stats = await statfs(this.backupDirectory);
|
|
100
|
+
return Number(stats.bavail) * Number(stats.bsize);
|
|
101
|
+
}
|
|
102
|
+
getProviderInfo() {
|
|
103
|
+
return {
|
|
104
|
+
name: 'FileBackupProvider',
|
|
105
|
+
version: '1.0.0',
|
|
106
|
+
description: 'Local-filesystem backup storage (default)',
|
|
107
|
+
features: ['local-fs']
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Absolute paths (from listBackupObjects / createBackup) pass through; bare
|
|
111
|
+
* names are resolved against the backup directory. */
|
|
112
|
+
resolve(idOrPath) {
|
|
113
|
+
return path.isAbsolute(idOrPath) ? idOrPath : path.join(this.backupDirectory, idOrPath);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export default FileBackupProvider;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ConsentRecord } from '../types/Consent.js';
|
|
2
|
+
declare class FileConsentProvider {
|
|
3
|
+
private readonly dir;
|
|
4
|
+
constructor(dir: string);
|
|
5
|
+
/** Create the folder owner-only; tighten one that is wider. */
|
|
6
|
+
initialize(): void;
|
|
7
|
+
/** The folder the records live in: what a backup copies. */
|
|
8
|
+
get directory(): string;
|
|
9
|
+
/** One file per account, named by the account's stored username. */
|
|
10
|
+
private fileFor;
|
|
11
|
+
/** Every record of `username`, oldest first; none when the file does not exist. Unreadable lines are reported and skipped. */
|
|
12
|
+
list(username: string): Promise<ConsentRecord[]>;
|
|
13
|
+
/** Add one record to `username`'s file, flushed to the device before returning. */
|
|
14
|
+
append(username: string, record: ConsentRecord): Promise<void>;
|
|
15
|
+
/** Remove `username`'s file (the account was deleted). True when there was one. */
|
|
16
|
+
remove(username: string): Promise<boolean>;
|
|
17
|
+
}
|
|
18
|
+
export default FileConsentProvider;
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* shared memory, which network filesystems do not provide, so the file's
|
|
9
9
|
* location is checked first (`refuseNetworkFilesystem`).
|
|
10
10
|
*/
|
|
11
|
-
import
|
|
11
|
+
import type DatabaseDriver from 'better-sqlite3-multiple-ciphers';
|
|
12
12
|
import BaseDatabaseProvider from './BaseDatabaseProvider.js';
|
|
13
13
|
import type { ProviderDurability } from './BaseProvider.js';
|
|
14
14
|
import { type Migration, type MigrationReport } from './sqliteMigrations.js';
|
|
15
|
-
export type SqliteHandle = InstanceType<typeof
|
|
15
|
+
export type SqliteHandle = InstanceType<typeof DatabaseDriver>;
|
|
16
16
|
/** The lock wait when none is configured: the driver's own default, stated (#1710). */
|
|
17
17
|
export declare const DEFAULT_BUSY_TIMEOUT_MS = 5000;
|
|
18
18
|
declare class SqliteDatabaseProvider extends BaseDatabaseProvider<SqliteHandle> {
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* key is given (#1536). Ported from yourPHR (yourphr#617).
|
|
4
|
-
*
|
|
5
|
-
* Opened and migrated in the constructor, so every provider built over the
|
|
6
|
-
* handle sees a finished schema. WAL mode with `synchronous = FULL`: a
|
|
7
|
-
* committed transaction is on the device before the call returns. WAL needs
|
|
8
|
-
* shared memory, which network filesystems do not provide, so the file's
|
|
9
|
-
* location is checked first (`refuseNetworkFilesystem`).
|
|
10
|
-
*/
|
|
11
|
-
import Database from 'better-sqlite3-multiple-ciphers';
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import path from 'node:path';
|
|
12
3
|
import { existsSync, statSync } from 'node:fs';
|
|
13
4
|
import BaseDatabaseProvider from './BaseDatabaseProvider.js';
|
|
14
5
|
import { runMigrations } from './sqliteMigrations.js';
|
|
15
6
|
import { refuseNetworkFilesystem } from './sqliteLocation.js';
|
|
7
|
+
/**
|
|
8
|
+
* The driver, loaded from the entry its package.json names (#1537). Run from
|
|
9
|
+
* source by tsx — the operator scripts, `npm run audit:verify -- --database` —
|
|
10
|
+
* the bare package name follows tsconfig's `paths`, which maps it to the type
|
|
11
|
+
* declarations, for `import`, `require` and `require.resolve` alike; its
|
|
12
|
+
* `package.json` is not mapped.
|
|
13
|
+
*/
|
|
14
|
+
const requireHere = createRequire(import.meta.url);
|
|
15
|
+
const driverPackage = requireHere.resolve('better-sqlite3-multiple-ciphers/package.json');
|
|
16
|
+
const Database = requireHere(path.join(path.dirname(driverPackage), requireHere(driverPackage).main));
|
|
16
17
|
/** The lock wait when none is configured: the driver's own default, stated (#1710). */
|
|
17
18
|
export const DEFAULT_BUSY_TIMEOUT_MS = 5000;
|
|
18
19
|
class SqliteDatabaseProvider extends BaseDatabaseProvider {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { BackupArchiveTarget, ManagerRestoreOptions, SiteRestoreStaging, StagedRestoreAtStart } from '../managers/BaseManager.js';
|
|
2
|
+
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
2
3
|
import { type PrivateStoreLayout } from '../utils/privateStorePath.js';
|
|
3
4
|
/** Where the attachment files live on this site. */
|
|
4
5
|
export interface AttachmentFilePlaces {
|
|
@@ -33,7 +34,7 @@ export interface AttachmentFilesBackupOutcome {
|
|
|
33
34
|
warnings: string[];
|
|
34
35
|
}
|
|
35
36
|
/** Write the attachment files beside the backup document: the pool, then every vault's. Never base64, never whole in memory. */
|
|
36
|
-
export declare function backupAttachmentFiles(places: AttachmentFilePlaces,
|
|
37
|
+
export declare function backupAttachmentFiles(places: AttachmentFilePlaces, archive: (part: string) => BackupArchiveTarget): Promise<AttachmentFilesBackupOutcome>;
|
|
37
38
|
/**
|
|
38
39
|
* Stage the attachment files from a backup for the next start, and record
|
|
39
40
|
* the part in the site restore. All or nothing: on any failure every staged
|
|
@@ -41,7 +42,7 @@ export declare function backupAttachmentFiles(places: AttachmentFilePlaces, dire
|
|
|
41
42
|
*
|
|
42
43
|
* @returns the vaults the backup holds that this site doesn't have (not staged).
|
|
43
44
|
*/
|
|
44
|
-
export declare function stageAttachmentFiles(places: AttachmentFilePlaces,
|
|
45
|
+
export declare function stageAttachmentFiles(places: AttachmentFilePlaces, readArchive: NonNullable<ManagerRestoreOptions['readArchive']>, data: AttachmentFilesBackupData, metadataJson: string, siteRestore: SiteRestoreStaging): Promise<{
|
|
45
46
|
skippedVaults: string[];
|
|
46
47
|
}>;
|
|
47
48
|
/**
|
|
@@ -49,4 +50,4 @@ export declare function stageAttachmentFiles(places: AttachmentFilePlaces, direc
|
|
|
49
50
|
* read: only the pool, its metadata file and vault attachment paths may be
|
|
50
51
|
* replaced, whatever the manifest says.
|
|
51
52
|
*/
|
|
52
|
-
export declare function applyAttachmentFilesAtStart(
|
|
53
|
+
export declare function applyAttachmentFilesAtStart(engine: WikiEngine, places: AttachmentFilePlaces): Promise<StagedRestoreAtStart | null>;
|
|
@@ -42,6 +42,51 @@ const DATABASE_MIGRATIONS = [
|
|
|
42
42
|
up: (db) => {
|
|
43
43
|
db.exec('ALTER TABLE job_state ADD COLUMN rule TEXT');
|
|
44
44
|
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
// #1537: the audit log in the database. `row_id` is the order records were
|
|
48
|
+
// written, which is chain order; `seq` repeats after a chain restart, so
|
|
49
|
+
// it is not the key. `record` is the stamped record exactly as hashed;
|
|
50
|
+
// the other columns are copies of its fields for the indexes. A retention
|
|
51
|
+
// checkpoint holds the seq and hash of the last record it removed, so
|
|
52
|
+
// what remains verifies from it.
|
|
53
|
+
id: '20261022120000',
|
|
54
|
+
description: 'audit log: audit_events, audit_checkpoints',
|
|
55
|
+
up: (db) => {
|
|
56
|
+
db.exec(`CREATE TABLE audit_events (
|
|
57
|
+
row_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
+
id TEXT NOT NULL UNIQUE,
|
|
59
|
+
seq INTEGER NOT NULL,
|
|
60
|
+
prev_hash TEXT NOT NULL,
|
|
61
|
+
hash TEXT NOT NULL,
|
|
62
|
+
timestamp TEXT NOT NULL,
|
|
63
|
+
event_type TEXT NOT NULL,
|
|
64
|
+
user TEXT,
|
|
65
|
+
actor TEXT,
|
|
66
|
+
owner TEXT,
|
|
67
|
+
resource TEXT,
|
|
68
|
+
action TEXT,
|
|
69
|
+
result TEXT,
|
|
70
|
+
severity TEXT,
|
|
71
|
+
record TEXT NOT NULL
|
|
72
|
+
)`);
|
|
73
|
+
db.exec('CREATE INDEX audit_events_by_time ON audit_events (timestamp)');
|
|
74
|
+
db.exec('CREATE INDEX audit_events_by_type ON audit_events (event_type, timestamp)');
|
|
75
|
+
db.exec('CREATE INDEX audit_events_by_user ON audit_events (user, timestamp)');
|
|
76
|
+
db.exec('CREATE INDEX audit_events_by_actor ON audit_events (actor, timestamp)');
|
|
77
|
+
db.exec('CREATE INDEX audit_events_by_owner ON audit_events (owner, timestamp)');
|
|
78
|
+
db.exec('CREATE INDEX audit_events_by_resource ON audit_events (resource, timestamp)');
|
|
79
|
+
db.exec(`CREATE TABLE audit_checkpoints (
|
|
80
|
+
checkpoint_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
+
through_row INTEGER NOT NULL,
|
|
82
|
+
seq INTEGER NOT NULL,
|
|
83
|
+
hash TEXT NOT NULL,
|
|
84
|
+
removed INTEGER NOT NULL,
|
|
85
|
+
cutoff TEXT NOT NULL,
|
|
86
|
+
retention_days INTEGER NOT NULL,
|
|
87
|
+
created_at TEXT NOT NULL
|
|
88
|
+
)`);
|
|
89
|
+
}
|
|
45
90
|
}
|
|
46
91
|
];
|
|
47
92
|
export default DATABASE_MIGRATIONS;
|