@jwilleke/ngdpbase-types 5.25.0 → 5.26.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 (59) hide show
  1. package/README.md +1 -1
  2. package/lib/packages/ngdpbase-types/index.d.ts +4 -0
  3. package/lib/src/http/egressPolicy.js +61 -0
  4. package/lib/src/http/guardedElasticsearch.d.ts +27 -0
  5. package/lib/src/http/guardedElasticsearch.js +99 -0
  6. package/lib/src/managers/AddonsManager.d.ts +37 -1
  7. package/lib/src/managers/AttachmentManager.d.ts +4 -5
  8. package/lib/src/managers/AuthManager.d.ts +150 -32
  9. package/lib/src/managers/BackgroundJobManager.d.ts +22 -12
  10. package/lib/src/managers/BackupManager.d.ts +779 -0
  11. package/lib/src/managers/BackupManager.js +2042 -0
  12. package/lib/src/managers/BaseManager.d.ts +156 -6
  13. package/lib/src/managers/BaseManager.js +64 -2
  14. package/lib/src/managers/CommentManager.d.ts +6 -6
  15. package/lib/src/managers/ConsentManager.d.ts +130 -0
  16. package/lib/src/managers/DatabaseManager.d.ts +6 -0
  17. package/lib/src/managers/DatabaseManager.js +25 -15
  18. package/lib/src/managers/FootnoteManager.d.ts +6 -6
  19. package/lib/src/managers/OrganizationManager.d.ts +8 -6
  20. package/lib/src/managers/PageManager.d.ts +16 -0
  21. package/lib/src/managers/PersonManager.d.ts +8 -6
  22. package/lib/src/managers/RoleManager.d.ts +6 -8
  23. package/lib/src/providers/BaseAttachmentProvider.d.ts +4 -4
  24. package/lib/src/providers/BaseBackupProvider.d.ts +77 -0
  25. package/lib/src/providers/BaseBackupProvider.js +52 -0
  26. package/lib/src/providers/BasicAttachmentProvider.d.ts +4 -4
  27. package/lib/src/providers/FileBackupProvider.d.ts +34 -0
  28. package/lib/src/providers/FileBackupProvider.js +116 -0
  29. package/lib/src/providers/FileConsentProvider.d.ts +18 -0
  30. package/lib/src/providers/SqliteDatabaseProvider.d.ts +2 -2
  31. package/lib/src/providers/SqliteDatabaseProvider.js +12 -11
  32. package/lib/src/providers/attachmentFilesBackup.d.ts +5 -4
  33. package/lib/src/providers/databaseMigrations.js +45 -0
  34. package/lib/src/security/PolicyDecisionPoint.d.ts +14 -0
  35. package/lib/src/types/Consent.d.ts +90 -0
  36. package/lib/src/types/express.d.ts +6 -0
  37. package/lib/src/utils/addonHost.d.ts +22 -0
  38. package/lib/src/utils/addonHost.js +14 -1
  39. package/lib/src/utils/auditEventNames.generated.d.ts +20 -0
  40. package/lib/src/utils/auditEventNames.generated.js +20 -0
  41. package/lib/src/utils/backupCrypto.js +275 -0
  42. package/lib/src/utils/backupDocument.d.ts +36 -0
  43. package/lib/src/utils/backupDocument.js +99 -0
  44. package/lib/src/utils/browserDescription.d.ts +11 -0
  45. package/lib/src/utils/directoryBackup.d.ts +48 -48
  46. package/lib/src/utils/directoryBackup.js +291 -0
  47. package/lib/src/utils/instanceEnvBackup.d.ts +46 -0
  48. package/lib/src/utils/instanceEnvBackup.js +120 -0
  49. package/lib/src/utils/instanceEnvSecret.js +79 -0
  50. package/lib/src/utils/ipLocation.d.ts +11 -0
  51. package/lib/src/utils/schedule.js +171 -0
  52. package/lib/src/utils/secretFileMode.js +78 -0
  53. package/lib/src/utils/siteRestore.d.ts +13 -16
  54. package/lib/src/utils/siteRestore.js +33 -6
  55. package/lib/src/utils/treeArchive.d.ts +2 -0
  56. package/lib/src/utils/treeArchive.js +242 -0
  57. package/lib/src/utils/version.js +408 -0
  58. package/package.json +6 -1
  59. package/lib/src/utils/administrators.d.ts +0 -21
@@ -1,4 +1,4 @@
1
- import BaseManager, { type BackupData, type ManagerBackupOptions, type ManagerRestoreOptions } from './BaseManager.js';
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
- * `<stem>.roles.tar` beside the document. Without it restored users would
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
- backup(options?: ManagerBackupOptions): Promise<BackupData>;
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 { PartOutcome } from '../utils/siteRestore.js';
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?(directory: string, stem: string): Promise<AttachmentFilesBackupOutcome>;
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, directory: string, siteRestore: string): Promise<{
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?(): PartOutcome | null;
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 { type PartOutcome } from '../utils/siteRestore.js';
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(directory: string, stem: string): Promise<AttachmentFilesBackupOutcome>;
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, directory: string, siteRestore: string): Promise<{
458
+ stageFilesRestore(providerBackup: Record<string, unknown>, files: AttachmentFilesBackupData, readArchive: NonNullable<ManagerRestoreOptions['readArchive']>, siteRestore: SiteRestoreStaging): Promise<{
459
459
  skippedVaults: string[];
460
460
  }>;
461
- takeRestoreAtStart(): PartOutcome | null;
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 Database from 'better-sqlite3-multiple-ciphers';
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 Database>;
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
- * The application database as one SQLite file, encrypted with SQLCipher when a
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 { type PartOutcome } from '../utils/siteRestore.js';
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, directory: string, stem: string): Promise<AttachmentFilesBackupOutcome>;
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, directory: string, data: AttachmentFilesBackupData, metadataJson: string, siteRestore: string): Promise<{
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(places: AttachmentFilePlaces, siteRestore: string): PartOutcome | null;
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;
@@ -146,6 +146,20 @@ export declare class PolicyDecisionPoint extends BaseManager {
146
146
  * cannot be asked by accident. The subject is the PIP's, resolved live.
147
147
  */
148
148
  userHoldsPermission(username: string, action: string): Promise<boolean>;
149
+ /**
150
+ * The usernames of every account that holds a permission, each asked through
151
+ * {@link userHoldsPermission} (#1813; was `utils/administrators.ts`). With
152
+ * `admin-system` this is "who are the administrators": the people an
153
+ * operator problem is reported to (#1431 7b, #1916), never a role name.
154
+ *
155
+ * An empty answer means "nobody to tell", and a caller sending a
156
+ * notification must then only log it: an empty `targetUsers` list means
157
+ * every user in NotificationManager.
158
+ *
159
+ * @returns The usernames, or null when there is no UserManager to list
160
+ * accounts from (tests, a half-booted engine); treat that like nobody
161
+ */
162
+ usersHolding(action: string): Promise<string[] | null>;
149
163
  /**
150
164
  * The permissions a user's roles give them, for display (#1431 step 10).
151
165
  *
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Consent to a site's documents (#1603): the shapes ConsentManager and its
3
+ * callers share, and what an add-on registers.
4
+ */
5
+ /** Whose document it is: core's (`core`) or an add-on's slug. */
6
+ export type ConsentScope = string;
7
+ /** Where a person is asked to accept (#1603): `?scope=` (core when absent) and `?returnTo=`. */
8
+ export declare const CONSENT_PATH = "/consent";
9
+ /** The scope of core's own documents, `ngdpbase.consent.documents`. */
10
+ export declare const CORE_CONSENT_SCOPE = "core";
11
+ /**
12
+ * A document a person is asked to accept, as it stands now: a page whose
13
+ * front matter carries `version:` and `effective:`. Read from the page each
14
+ * time, never copied.
15
+ */
16
+ export interface ConsentDocument {
17
+ /** The page's slug, which is also the document's id in a record (`terms-of-service`, `phr-privacy-policy`). */
18
+ id: string;
19
+ scope: ConsentScope;
20
+ /** The page's title, to show. */
21
+ title: string;
22
+ /** The page's `version:`, as a string. A change asks everyone again. */
23
+ version: string;
24
+ /** The page's `effective:` date (`YYYY-MM-DD`), or null when it names none. */
25
+ effective: string | null;
26
+ }
27
+ /** What a person saw when they accepted: each document and the version shown. */
28
+ export interface ConsentSeen {
29
+ id: string;
30
+ version: string;
31
+ }
32
+ /**
33
+ * One acceptance, one line of the person's file. Append-only: a later
34
+ * version is a later line, nothing is rewritten.
35
+ */
36
+ export interface ConsentRecord {
37
+ kind: 'accept';
38
+ document: string;
39
+ scope: ConsentScope;
40
+ version: string;
41
+ effective: string | null;
42
+ /** ISO 8601 time the person accepted. */
43
+ acceptedAt: string;
44
+ /**
45
+ * The account this was accepted on, by its creation time (#1937): a later
46
+ * account that takes the same username never inherits it.
47
+ */
48
+ account?: string;
49
+ /** Who and how: the account, where from, and the way they were asked (`sign-up`, `sign-in`, `add-on`). */
50
+ context: {
51
+ by: string;
52
+ how: string;
53
+ origin: string;
54
+ ipAddress?: string;
55
+ };
56
+ }
57
+ /** One document's state for a person, for "Your consents" on /profile. */
58
+ export interface ConsentStatus {
59
+ document: ConsentDocument;
60
+ /** Their acceptance of the current version, or null: not accepted (never, or only an older version). */
61
+ current: ConsentRecord | null;
62
+ /** Their latest acceptance of any version, or null. */
63
+ latest: ConsentRecord | null;
64
+ }
65
+ /** What an add-on registers at load (#1603). */
66
+ export interface AddonConsentRegistration {
67
+ /**
68
+ * Page slugs of the add-on's documents, each starting `<slug>-`
69
+ * (`phr-terms-of-service`, `phr-privacy-policy`). Shipped as the add-on's
70
+ * own pages, with `version:` and `effective:` front matter.
71
+ */
72
+ documents: string[];
73
+ /**
74
+ * URL path prefixes where a person uses the add-on in the browser
75
+ * (`/phr`). A person who has it on and has not accepted a changed version
76
+ * is asked there before going on. Optional: without it they are asked
77
+ * only when they next turn it on.
78
+ */
79
+ paths?: string[];
80
+ }
81
+ /**
82
+ * Thrown when a per-person add-on is turned on before its documents are
83
+ * accepted (#1603). `documents` names what is still to accept.
84
+ */
85
+ export declare class ConsentRequiredError extends Error {
86
+ readonly scope: ConsentScope;
87
+ readonly documents: string[];
88
+ readonly name = "ConsentRequiredError";
89
+ constructor(scope: ConsentScope, documents: string[]);
90
+ }
@@ -45,6 +45,12 @@ declare module 'express-session' {
45
45
  pendingRedirect?: string;
46
46
  /** A verified outside identity no account has connected, waiting for the person to sign in another way (#1743). */
47
47
  pendingConnection?: PendingConnection;
48
+ /**
49
+ * Set at sign-in when the person has core terms to accept at their current
50
+ * version (#1603); the consent gate holds the session to the prompt until
51
+ * they accept (or sign out). Cleared by the acceptance.
52
+ */
53
+ consentPending?: boolean;
48
54
  /** When this session last made a request, epoch ms — kept only while an idle timeout is set (#1546). */
49
55
  lastActivity?: number;
50
56
  /** When this session signed in, epoch ms: the start of `ngdpbase.session.max-age` (#1767). A re-authentication never moves it. */