@jwilleke/ngdpbase-types 5.20.0 → 5.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/packages/ngdpbase-types/index.d.ts +5 -0
- package/lib/src/mail/MailProvider.d.ts +2 -0
- package/lib/src/managers/AddonsManager.d.ts +3 -8
- package/lib/src/managers/AttachmentManager.d.ts +64 -12
- package/lib/src/managers/AuthManager.d.ts +24 -4
- package/lib/src/managers/BackgroundJobManager.d.ts +71 -1
- package/lib/src/managers/BaseManager.d.ts +20 -0
- package/lib/src/managers/BaseManager.js +9 -0
- package/lib/src/managers/CommentManager.d.ts +10 -1
- package/lib/src/managers/ConfigurationManager.d.ts +16 -1
- package/lib/src/managers/DatabaseManager.d.ts +53 -16
- package/lib/src/managers/DatabaseManager.js +172 -38
- package/lib/src/managers/EmailManager.d.ts +42 -0
- package/lib/src/managers/FootnoteManager.d.ts +10 -1
- package/lib/src/managers/NotificationManager.d.ts +13 -0
- package/lib/src/managers/OrganizationManager.d.ts +40 -6
- package/lib/src/managers/PersonManager.d.ts +106 -9
- package/lib/src/managers/RoleManager.d.ts +19 -1
- package/lib/src/managers/UserManager.d.ts +15 -0
- package/lib/src/providers/BaseAttachmentProvider.d.ts +14 -0
- package/lib/src/providers/BasicAttachmentProvider.d.ts +26 -2
- package/lib/src/providers/SqliteDatabaseProvider.d.ts +12 -0
- package/lib/src/providers/SqliteDatabaseProvider.js +50 -7
- package/lib/src/providers/SqliteJobStateProvider.d.ts +10 -0
- package/lib/src/providers/attachmentFilesBackup.d.ts +52 -0
- package/lib/src/providers/sqliteBackup.d.ts +6 -0
- package/lib/src/providers/sqliteBackup.js +16 -6
- package/lib/src/security/permissions.generated.d.ts +4 -0
- package/lib/src/types/Asset.d.ts +10 -0
- package/lib/src/types/Config.d.ts +11 -0
- package/lib/src/types/Organization.d.ts +7 -0
- package/lib/src/types/Person.d.ts +27 -3
- package/lib/src/types/PersonProvider.d.ts +6 -0
- package/lib/src/types/WikiEngine.d.ts +9 -21
- package/lib/src/utils/addonConfigLayer.d.ts +19 -0
- package/lib/src/utils/addonConfigLayer.js +45 -3
- package/lib/src/utils/addonPageSync.d.ts +24 -0
- package/lib/src/utils/administrators.d.ts +21 -0
- package/lib/src/utils/auditEventNames.generated.d.ts +4 -0
- package/lib/src/utils/auditEventNames.generated.js +4 -0
- package/lib/src/utils/backupCrypto.d.ts +28 -0
- package/lib/src/utils/backupKey.d.ts +23 -0
- package/lib/src/utils/backupKey.js +37 -0
- package/lib/src/utils/configPaths.d.ts +55 -0
- package/lib/src/utils/configPaths.js +133 -0
- package/lib/src/utils/contactAccess.d.ts +58 -0
- package/lib/src/utils/databaseKey.d.ts +7 -0
- package/lib/src/utils/databaseKey.js +7 -0
- package/lib/src/utils/directoryBackup.d.ts +78 -0
- package/lib/src/utils/emailForLog.d.ts +20 -0
- package/lib/src/utils/maintenanceState.d.ts +153 -0
- package/lib/src/utils/pluginFormatters.d.ts +6 -1
- package/lib/src/utils/pluginFormatters.js +5 -2
- package/lib/src/utils/privateStoreTakeout.d.ts +2 -2
- package/lib/src/utils/redactSecrets.js +31 -11
- package/lib/src/utils/sensitiveValues.d.ts +49 -3
- package/lib/src/utils/sensitiveValues.js +139 -11
- package/lib/src/utils/siteRestore.d.ts +110 -0
- package/lib/src/utils/siteRestore.js +309 -0
- package/lib/src/utils/treeArchive.d.ts +44 -0
- package/lib/src/utils/username.d.ts +7 -1
- package/package.json +1 -1
|
@@ -1,12 +1,37 @@
|
|
|
1
|
-
import BaseManager, { type ManagerStats } from './BaseManager.js';
|
|
1
|
+
import BaseManager, { type BackupData, type ManagerBackupOptions, type ManagerRestoreOptions, type ManagerStats } from './BaseManager.js';
|
|
2
2
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
3
|
-
import type { Person, PersonUpdate } from '../types/Person.js';
|
|
3
|
+
import type { Person, PersonInput, PersonUpdate } from '../types/Person.js';
|
|
4
4
|
import type { PersonProvider } from '../types/PersonProvider.js';
|
|
5
|
+
import { type ActorContext } from '../context/ActorContext.js';
|
|
6
|
+
/** What the confirmation page shows: the person the link would join. */
|
|
7
|
+
export interface LinkOfferDetails {
|
|
8
|
+
personName: string | null;
|
|
9
|
+
email: string;
|
|
10
|
+
}
|
|
11
|
+
/** The outcome of offering a sign-up link (#1930). */
|
|
12
|
+
export type LinkOfferOutcome = 'sent' | 'no-match' | 'several' | 'unavailable' | 'failed';
|
|
5
13
|
/**
|
|
6
|
-
* PersonManager — canonical core record for persons (#617)
|
|
14
|
+
* PersonManager — canonical core record for persons (#617), and the door for
|
|
15
|
+
* contacts (#1930).
|
|
16
|
+
*
|
|
17
|
+
* A person may have an account or not. Each account has exactly one person
|
|
18
|
+
* (`identifier` = the username); a person with no account has the identifier
|
|
19
|
+
* `person:<uuid>`, which no username may take. UserManager calls the door on
|
|
20
|
+
* the account lifecycle under a system context.
|
|
21
|
+
*
|
|
22
|
+
* The door: `create`, `update`, `delete`, `get`, `list`, `search` and
|
|
23
|
+
* `linkAccount` take the caller's context first and ask the PDP: `contact-read`
|
|
24
|
+
* to list, search or get; `contact-manage` to change (`linkAccount` also `user-edit`). Each change is recorded as
|
|
25
|
+
* the `contact-manage` audit event. A refusal throws `ContactRefusedError`.
|
|
26
|
+
* `getById` / `getByIdentifier` stay as context-free lookups for core's own
|
|
27
|
+
* hot paths (RoleManager resolves roles through them on every request).
|
|
28
|
+
*
|
|
29
|
+
* A later sign-up joins a person with no account in one of two ways: the new
|
|
30
|
+
* account confirms an emailed link (`offerSignUpLink` → `confirmSignUpLink`),
|
|
31
|
+
* or an administrator links them (`linkAccount`). Either way the existing
|
|
32
|
+
* person keeps its `@id` and takes the account; the person made for the new
|
|
33
|
+
* account is folded into it and removed, so there is never a second person.
|
|
7
34
|
*
|
|
8
|
-
* Each User has at most one paired Person record. UserManager calls
|
|
9
|
-
* `create`/`update`/`delete` here on the matching user lifecycle events.
|
|
10
35
|
* `getByUserIdentifier(username)` remains as a lazy-migration entry point
|
|
11
36
|
* for legacy users that pre-date the sync wiring.
|
|
12
37
|
*/
|
|
@@ -14,20 +39,91 @@ declare class PersonManager extends BaseManager {
|
|
|
14
39
|
readonly description = "Canonical Person records (#617)";
|
|
15
40
|
private provider;
|
|
16
41
|
private providerClass?;
|
|
42
|
+
/** `ngdpbase.application.persons.storagedir`, resolved at start: what a backup copies (#1876). */
|
|
43
|
+
private storageDir;
|
|
17
44
|
private byIdentifierCache;
|
|
18
45
|
private byIdCache;
|
|
19
46
|
private metricsRef;
|
|
47
|
+
/** Sign-up link offers by token (#1930). In memory: a restart drops them, and an administrator links by hand. */
|
|
48
|
+
private linkOffers;
|
|
20
49
|
constructor(engine: WikiEngine);
|
|
21
50
|
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
51
|
+
/** Person records are in the backup (#1876), as `<stem>.persons.tar` beside the document. */
|
|
52
|
+
backup(options?: ManagerBackupOptions): Promise<BackupData>;
|
|
53
|
+
/** Staged for the next start with the rest of a site restore (#1876); the live records are untouched. */
|
|
54
|
+
restore(backupData: BackupData, options?: ManagerRestoreOptions): Promise<void>;
|
|
55
|
+
restoreIsStaged(): boolean;
|
|
22
56
|
getProvider(): PersonProvider | null;
|
|
23
57
|
getById(id: string): Promise<Person | null>;
|
|
24
58
|
getByIdentifier(identifier: string): Promise<Person | null>;
|
|
25
59
|
/** #1006: how many people this instance holds. Count only, never records. */
|
|
26
60
|
getManagerStats(): Promise<ManagerStats>;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
61
|
+
/** True when the person has no account: its identifier is `person:<uuid>` (#1930). */
|
|
62
|
+
static isAccountless(person: Pick<Person, 'identifier'>): boolean;
|
|
63
|
+
/** Every person, for a caller who may read contacts (`contact-read`). */
|
|
64
|
+
list(ctx: ActorContext): Promise<Person[]>;
|
|
65
|
+
/** One person by `@id`, for a caller who may read contacts (`contact-read`). */
|
|
66
|
+
get(ctx: ActorContext, id: string): Promise<Person | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Persons whose name (`name`, `givenName`, `familyName`) or `email` contains
|
|
69
|
+
* `query`, ignoring case; an empty query matches everyone. `accountless`
|
|
70
|
+
* narrows to persons with no account. At most `limit` results
|
|
71
|
+
* (`ngdpbase.application.persons.search.limit` by default).
|
|
72
|
+
*/
|
|
73
|
+
search(ctx: ActorContext, query: string, options?: {
|
|
74
|
+
limit?: number;
|
|
75
|
+
accountless?: boolean;
|
|
76
|
+
}): Promise<Person[]>;
|
|
77
|
+
/**
|
|
78
|
+
* Create a person. With no `identifier` the person has no account and gets
|
|
79
|
+
* `person:<uuid>`. An `identifier` must be an existing account that has no
|
|
80
|
+
* person yet: that is how UserManager makes the person for a new account.
|
|
81
|
+
*/
|
|
82
|
+
create(ctx: ActorContext, input: PersonInput): Promise<Person>;
|
|
83
|
+
/** Change a person's fields, affiliations included. `@id` and `identifier` never change here. */
|
|
84
|
+
update(ctx: ActorContext, id: string, patch: PersonUpdate): Promise<Person | null>;
|
|
85
|
+
delete(ctx: ActorContext, id: string): Promise<boolean>;
|
|
86
|
+
/**
|
|
87
|
+
* An administrator joins a person who has no account to an account (#1930),
|
|
88
|
+
* for when the email differs or none was given. Asks `contact-manage` and
|
|
89
|
+
* `user-edit`. The person keeps its `@id`; the account's own person, if it
|
|
90
|
+
* has one, is folded in and removed.
|
|
91
|
+
*/
|
|
92
|
+
linkAccount(ctx: ActorContext, personId: string, username: string): Promise<Person>;
|
|
93
|
+
/**
|
|
94
|
+
* Offer a new account the person with no account who has the same email
|
|
95
|
+
* ("Is this you?"): a single-use link is mailed to that address, and the
|
|
96
|
+
* account joins the person only when it confirms. Called by UserManager
|
|
97
|
+
* after an account is created. Skipped when email links are not available
|
|
98
|
+
* on this site; nothing is offered when no person, or more than one, has
|
|
99
|
+
* that address (an administrator links those by hand). `ctx` is the context
|
|
100
|
+
* the account was created under, named in the log.
|
|
101
|
+
*/
|
|
102
|
+
offerSignUpLink(ctx: ActorContext, username: string): Promise<LinkOfferOutcome>;
|
|
103
|
+
/** What the confirmation page shows, for the signed-in account the link was sent to; null for an unknown, used, expired or other account's link. */
|
|
104
|
+
signUpLinkOffer(ctx: ActorContext, token: string): Promise<LinkOfferDetails | null>;
|
|
105
|
+
/**
|
|
106
|
+
* The new account confirms the emailed link: it is joined to the person.
|
|
107
|
+
* Single use. The link is the authority — it proves the address and is
|
|
108
|
+
* bound to the account it was sent for — so no permission is asked; the
|
|
109
|
+
* change is recorded under the account's own context. Null for a link that
|
|
110
|
+
* is unknown, used, expired, or another account's.
|
|
111
|
+
*/
|
|
112
|
+
confirmSignUpLink(ctx: ActorContext, token: string): Promise<Person | null>;
|
|
113
|
+
/** The new account says the record is not theirs: the link is dropped. */
|
|
114
|
+
declineSignUpLink(ctx: ActorContext, token: string): boolean;
|
|
115
|
+
private liveOffer;
|
|
116
|
+
private sweepLinkOffers;
|
|
117
|
+
/**
|
|
118
|
+
* Join `username` to the person `personId`, who has no account. The person
|
|
119
|
+
* takes the account's identifier and keeps its `@id`; fields it lacks are
|
|
120
|
+
* filled from the account's own person, that person's role memberships move
|
|
121
|
+
* to it, and that person is removed. Never a second person.
|
|
122
|
+
*/
|
|
123
|
+
private join;
|
|
124
|
+
/** Each affiliation names an organization that exists; `role` is trimmed text. Throws on any other shape. */
|
|
125
|
+
private checkedAffiliations;
|
|
126
|
+
private configNumber;
|
|
31
127
|
/**
|
|
32
128
|
* Clear all caches. Called on every write — the simplest correct
|
|
33
129
|
* invalidation. Per-key invalidation can come later if needed.
|
|
@@ -45,4 +141,5 @@ declare class PersonManager extends BaseManager {
|
|
|
45
141
|
private getMetrics;
|
|
46
142
|
private normalizeProviderName;
|
|
47
143
|
}
|
|
144
|
+
export { ContactRefusedError } from '../utils/contactAccess.js';
|
|
48
145
|
export default PersonManager;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import BaseManager from './BaseManager.js';
|
|
1
|
+
import BaseManager, { type BackupData, type ManagerBackupOptions, type ManagerRestoreOptions } 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';
|
|
@@ -8,6 +8,8 @@ declare class RoleManager extends BaseManager {
|
|
|
8
8
|
readonly description = "Canonical OrganizationRole records (#617 follow-up)";
|
|
9
9
|
private provider;
|
|
10
10
|
private providerClass?;
|
|
11
|
+
/** `ngdpbase.application.roles.storagedir`, resolved at start: what a backup copies (#1876). */
|
|
12
|
+
private storageDir;
|
|
11
13
|
private memberCache;
|
|
12
14
|
private byOrgPositionCache;
|
|
13
15
|
private metricsRef;
|
|
@@ -31,6 +33,15 @@ declare class RoleManager extends BaseManager {
|
|
|
31
33
|
* as is any other `additionalProperty` entry.
|
|
32
34
|
*/
|
|
33
35
|
private stripCatalogueCopies;
|
|
36
|
+
/**
|
|
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.
|
|
40
|
+
*/
|
|
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;
|
|
34
45
|
/** Provider accessor, mainly for tests. */
|
|
35
46
|
getProvider(): RoleProvider | null;
|
|
36
47
|
getById(id: string): Promise<Role | null>;
|
|
@@ -102,6 +113,13 @@ declare class RoleManager extends BaseManager {
|
|
|
102
113
|
* @returns how many accounts were given the role (0 once it has been given)
|
|
103
114
|
*/
|
|
104
115
|
grantToEveryAccountOnce(roleName: string, usernames: readonly string[], ctx: JobContext): Promise<number>;
|
|
116
|
+
/**
|
|
117
|
+
* Move every membership held by one Person `@id` to another (#1930): when
|
|
118
|
+
* PersonManager joins an account to an existing person, the roles the
|
|
119
|
+
* account's person held go with the account. One write per role record;
|
|
120
|
+
* returns how many records changed. PersonManager's join records the change.
|
|
121
|
+
*/
|
|
122
|
+
moveMemberships(fromPersonId: string, toPersonId: string): Promise<number>;
|
|
105
123
|
/** Remove a deleted account's Person from every role it held. */
|
|
106
124
|
removeAllMemberships(username: string): Promise<void>;
|
|
107
125
|
/**
|
|
@@ -461,6 +461,14 @@ declare class UserManager extends BaseManager {
|
|
|
461
461
|
limit?: number;
|
|
462
462
|
activeOnly?: boolean;
|
|
463
463
|
}, ctx: ActorContext): Promise<Omit<User, 'password'>[]>;
|
|
464
|
+
/**
|
|
465
|
+
* The account lifecycle reaches PersonManager's door under a system context
|
|
466
|
+
* (#1930): keeping an account's person in step with the account is core's
|
|
467
|
+
* own bookkeeping, not the caller's contact management, so it never depends
|
|
468
|
+
* on the caller holding `contact-manage`. The reason names the account and
|
|
469
|
+
* who changed it, so the record says why.
|
|
470
|
+
*/
|
|
471
|
+
private personSyncContext;
|
|
464
472
|
/**
|
|
465
473
|
* Persist a Person record paired with a newly-created User. The install's
|
|
466
474
|
* anchor org (when configured) is referenced via `memberOf`; without it
|
|
@@ -468,6 +476,13 @@ declare class UserManager extends BaseManager {
|
|
|
468
476
|
* thrown — auth must still succeed if Person storage is degraded.
|
|
469
477
|
*/
|
|
470
478
|
private syncPersonOnCreate;
|
|
479
|
+
/**
|
|
480
|
+
* At sign-up, offer the new account a person with no account who has the
|
|
481
|
+
* same email (#1930): PersonManager mails an "Is this you?" link, and joins
|
|
482
|
+
* them only when the account confirms. Skipped when email is off. Never
|
|
483
|
+
* fails the account write.
|
|
484
|
+
*/
|
|
485
|
+
private offerContactLink;
|
|
471
486
|
private syncPersonOnUpdate;
|
|
472
487
|
private syncPersonOnDelete;
|
|
473
488
|
createSession(username: string, additionalData?: Record<string, unknown>): Promise<string>;
|
|
@@ -3,6 +3,8 @@ import type { StoreFileEntry, StoreFileLocation } from '../types/Provider.js';
|
|
|
3
3
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
4
4
|
import { ProviderInfo } from './BasePageProvider.js';
|
|
5
5
|
import BaseProvider from './BaseProvider.js';
|
|
6
|
+
import type { AttachmentFilesBackupData, AttachmentFilesBackupOutcome } from './attachmentFilesBackup.js';
|
|
7
|
+
import type { PartOutcome } from '../utils/siteRestore.js';
|
|
6
8
|
/**
|
|
7
9
|
* File information for attachment uploads
|
|
8
10
|
*/
|
|
@@ -187,6 +189,18 @@ declare abstract class BaseAttachmentProvider extends BaseProvider implements At
|
|
|
187
189
|
* @returns {Promise<void>}
|
|
188
190
|
*/
|
|
189
191
|
abstract restore(backupData: Record<string, unknown>): Promise<void>;
|
|
192
|
+
/**
|
|
193
|
+
* Write the attachment files themselves beside the backup document (#1876),
|
|
194
|
+
* streamed, bytes as stored. A provider without it can't be backed up whole,
|
|
195
|
+
* and AttachmentManager's backup says so.
|
|
196
|
+
*/
|
|
197
|
+
backupFiles?(directory: string, stem: string): Promise<AttachmentFilesBackupOutcome>;
|
|
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<{
|
|
200
|
+
skippedVaults: string[];
|
|
201
|
+
}>;
|
|
202
|
+
/** What the start did with a staged restore of the files (#1876), once; null when there was none. */
|
|
203
|
+
takeRestoreAtStart?(): PartOutcome | null;
|
|
190
204
|
/**
|
|
191
205
|
* Shutdown the provider (cleanup resources)
|
|
192
206
|
* @returns {Promise<void>}
|
|
@@ -5,6 +5,8 @@ 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';
|
|
9
|
+
import { type AttachmentFilesBackupData, type AttachmentFilesBackupOutcome } from './attachmentFilesBackup.js';
|
|
8
10
|
/**
|
|
9
11
|
* Schema.org Person metadata
|
|
10
12
|
*/
|
|
@@ -122,6 +124,8 @@ export declare function safeStoredExtension(originalName: string, mimeType?: str
|
|
|
122
124
|
*/
|
|
123
125
|
declare class BasicAttachmentProvider extends BaseAttachmentProvider implements AssetProvider {
|
|
124
126
|
private storageDirectory;
|
|
127
|
+
/** What the start did with a staged restore of the files (#1876). */
|
|
128
|
+
private restoreAtStart;
|
|
125
129
|
/** Pre-#1386 attachments/{legacyprivateroot}/{user}/ — migrate-FROM and read fallback only. */
|
|
126
130
|
private legacyPrivateDir;
|
|
127
131
|
private pagesDirectory;
|
|
@@ -149,8 +153,15 @@ declare class BasicAttachmentProvider extends BaseAttachmentProvider implements
|
|
|
149
153
|
/**
|
|
150
154
|
* Initialize the provider
|
|
151
155
|
* All configuration access via ConfigurationManager
|
|
156
|
+
*
|
|
157
|
+
* @param options.readOnly - Open for reading only (#1852): load the index and
|
|
158
|
+
* change nothing on disk. No staged restore is applied, no folder is
|
|
159
|
+
* created, and neither start-up migration (legacy private files, stale
|
|
160
|
+
* paths) runs. For an operator report such as the files dry run.
|
|
152
161
|
*/
|
|
153
|
-
initialize(
|
|
162
|
+
initialize(options?: {
|
|
163
|
+
readOnly?: boolean;
|
|
164
|
+
}): Promise<void>;
|
|
154
165
|
/**
|
|
155
166
|
* Verify that the attachment storage directory is accessible.
|
|
156
167
|
*
|
|
@@ -210,7 +221,7 @@ declare class BasicAttachmentProvider extends BaseAttachmentProvider implements
|
|
|
210
221
|
* @param fileBuffer - File data
|
|
211
222
|
* @param fileInfo - { originalName, mimeType, size }
|
|
212
223
|
* @param metadata - Schema.org CreativeWork metadata
|
|
213
|
-
* @param user -
|
|
224
|
+
* @param user - The uploader: `username` is recorded as author and editor (#1926)
|
|
214
225
|
* @param options - Privacy-aware storage options
|
|
215
226
|
* @returns Complete attachment metadata
|
|
216
227
|
* @private
|
|
@@ -435,6 +446,19 @@ declare class BasicAttachmentProvider extends BaseAttachmentProvider implements
|
|
|
435
446
|
* @returns Backup data
|
|
436
447
|
*/
|
|
437
448
|
backup(): Promise<BackupData>;
|
|
449
|
+
/** Where the attachment files are, for their backup and staged restore (#1876). */
|
|
450
|
+
private filePlaces;
|
|
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>;
|
|
453
|
+
/**
|
|
454
|
+
* Stage the files and the shared metadata from a backup for the next start
|
|
455
|
+
* (#1876). The metadata is the document's; the live files and metadata are
|
|
456
|
+
* not touched.
|
|
457
|
+
*/
|
|
458
|
+
stageFilesRestore(providerBackup: Record<string, unknown>, files: AttachmentFilesBackupData, directory: string, siteRestore: string): Promise<{
|
|
459
|
+
skippedVaults: string[];
|
|
460
|
+
}>;
|
|
461
|
+
takeRestoreAtStart(): PartOutcome | null;
|
|
438
462
|
/**
|
|
439
463
|
* Restore provider data from backup
|
|
440
464
|
* @param backupData - Backup data
|
|
@@ -35,6 +35,16 @@ declare class SqliteDatabaseProvider extends BaseDatabaseProvider<SqliteHandle>
|
|
|
35
35
|
constructor(file: string, key: string, ledger: Migration[], ledgerTable?: string, options?: {
|
|
36
36
|
busyTimeoutMs?: number;
|
|
37
37
|
});
|
|
38
|
+
private static unlock;
|
|
39
|
+
/**
|
|
40
|
+
* Read a database file once, read-only, under `key`, and close it (#1876):
|
|
41
|
+
* nothing is migrated or written. Used for the copy a restore kept as
|
|
42
|
+
* `*.pre-restore`, to read what the site had recorded just before the
|
|
43
|
+
* restore replaced it.
|
|
44
|
+
*/
|
|
45
|
+
static readOnce<T>(file: string, key: string, read: (db: SqliteHandle) => T): T;
|
|
46
|
+
/** Read an existing file read-only: the key opens it and it passes the quick check, or this throws. */
|
|
47
|
+
private static probe;
|
|
38
48
|
/**
|
|
39
49
|
* Run a further migration ledger on this connection, recorded in its own
|
|
40
50
|
* table: an add-on that depends on the add-on owning this database brings
|
|
@@ -49,6 +59,8 @@ declare class SqliteDatabaseProvider extends BaseDatabaseProvider<SqliteHandle>
|
|
|
49
59
|
encrypted: boolean;
|
|
50
60
|
};
|
|
51
61
|
integrityOk(): boolean;
|
|
62
|
+
/** SQLite's quick check; throws what the driver throws (a wrong key: "file is not a database"). */
|
|
63
|
+
private quickCheckOk;
|
|
52
64
|
getDurability(): ProviderDurability;
|
|
53
65
|
close(): void;
|
|
54
66
|
}
|
|
@@ -36,14 +36,17 @@ class SqliteDatabaseProvider extends BaseDatabaseProvider {
|
|
|
36
36
|
super();
|
|
37
37
|
this.file = file;
|
|
38
38
|
refuseNetworkFilesystem(file);
|
|
39
|
+
// #1892: an existing file is first read through a read-only connection.
|
|
40
|
+
// A wrong or missing key, or a damaged file, fails there, and nothing is
|
|
41
|
+
// written: a read-write connection that fails to read still checkpoints
|
|
42
|
+
// the WAL into the file on close, or deletes a WAL it can't parse. Only
|
|
43
|
+
// the -shm index, which SQLite rebuilds from the WAL, may be rewritten.
|
|
44
|
+
if (existsSync(file))
|
|
45
|
+
SqliteDatabaseProvider.probe(file, key);
|
|
39
46
|
this.db = new Database(file, { timeout: options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS });
|
|
40
47
|
this.encrypted = key !== '';
|
|
41
48
|
try {
|
|
42
|
-
|
|
43
|
-
this.db.pragma("cipher='sqlcipher'");
|
|
44
|
-
this.db.pragma(`key='${key.replace(/'/g, "''")}'`);
|
|
45
|
-
}
|
|
46
|
-
// The first read: a wrong key fails here, before anything is written.
|
|
49
|
+
SqliteDatabaseProvider.unlock(this.db, key);
|
|
47
50
|
this.db.pragma('journal_mode = WAL');
|
|
48
51
|
this.db.pragma('synchronous = FULL');
|
|
49
52
|
this.migrations = runMigrations(this.db, ledger, ledgerTable);
|
|
@@ -53,6 +56,42 @@ class SqliteDatabaseProvider extends BaseDatabaseProvider {
|
|
|
53
56
|
throw err;
|
|
54
57
|
}
|
|
55
58
|
}
|
|
59
|
+
static unlock(db, key) {
|
|
60
|
+
if (!key)
|
|
61
|
+
return;
|
|
62
|
+
db.pragma("cipher='sqlcipher'");
|
|
63
|
+
db.pragma(`key='${key.replace(/'/g, "''")}'`);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Read a database file once, read-only, under `key`, and close it (#1876):
|
|
67
|
+
* nothing is migrated or written. Used for the copy a restore kept as
|
|
68
|
+
* `*.pre-restore`, to read what the site had recorded just before the
|
|
69
|
+
* restore replaced it.
|
|
70
|
+
*/
|
|
71
|
+
static readOnce(file, key, read) {
|
|
72
|
+
const db = new Database(file, { readonly: true, fileMustExist: true });
|
|
73
|
+
try {
|
|
74
|
+
SqliteDatabaseProvider.unlock(db, key);
|
|
75
|
+
return read(db);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
db.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Read an existing file read-only: the key opens it and it passes the quick check, or this throws. */
|
|
82
|
+
static probe(file, key) {
|
|
83
|
+
const db = new Database(file, { readonly: true, fileMustExist: true });
|
|
84
|
+
try {
|
|
85
|
+
SqliteDatabaseProvider.unlock(db, key);
|
|
86
|
+
const rows = db.pragma('quick_check');
|
|
87
|
+
if (String(rows[0]?.quick_check ?? '').toLowerCase() !== 'ok') {
|
|
88
|
+
throw new Error(`The database ${file} failed its integrity check — nothing was migrated or written`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
db.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
56
95
|
/**
|
|
57
96
|
* Run a further migration ledger on this connection, recorded in its own
|
|
58
97
|
* table: an add-on that depends on the add-on owning this database brings
|
|
@@ -70,13 +109,17 @@ class SqliteDatabaseProvider extends BaseDatabaseProvider {
|
|
|
70
109
|
}
|
|
71
110
|
integrityOk() {
|
|
72
111
|
try {
|
|
73
|
-
|
|
74
|
-
return String(rows[0]?.quick_check ?? '').toLowerCase() === 'ok';
|
|
112
|
+
return this.quickCheckOk();
|
|
75
113
|
}
|
|
76
114
|
catch {
|
|
77
115
|
return false;
|
|
78
116
|
}
|
|
79
117
|
}
|
|
118
|
+
/** SQLite's quick check; throws what the driver throws (a wrong key: "file is not a database"). */
|
|
119
|
+
quickCheckOk() {
|
|
120
|
+
const rows = this.db.pragma('quick_check');
|
|
121
|
+
return String(rows[0]?.quick_check ?? '').toLowerCase() === 'ok';
|
|
122
|
+
}
|
|
80
123
|
getDurability() {
|
|
81
124
|
return { bufferedForMs: 0, bufferedRecords: 0, fsync: true };
|
|
82
125
|
}
|
|
@@ -39,3 +39,13 @@ declare class SqliteJobStateProvider implements JobStateProvider {
|
|
|
39
39
|
releaseLock(jobId: string, slot: string, owner: string): Promise<void>;
|
|
40
40
|
}
|
|
41
41
|
export default SqliteJobStateProvider;
|
|
42
|
+
/**
|
|
43
|
+
* Every job's runs started after `since`, from a database handle (#1876): the
|
|
44
|
+
* application database a restore kept as `*.pre-restore`, read just after it
|
|
45
|
+
* was replaced, so the restore's notice names the runs the restored job
|
|
46
|
+
* history no longer has. The same table `listRuns` reads.
|
|
47
|
+
*/
|
|
48
|
+
export declare function runsStartedAfter(db: Handle, since: string): Array<{
|
|
49
|
+
jobId: string;
|
|
50
|
+
run: JobRunRecord;
|
|
51
|
+
}>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type PartOutcome } from '../utils/siteRestore.js';
|
|
2
|
+
import { type PrivateStoreLayout } from '../utils/privateStorePath.js';
|
|
3
|
+
/** Where the attachment files live on this site. */
|
|
4
|
+
export interface AttachmentFilePlaces {
|
|
5
|
+
/** The shared pool folder. */
|
|
6
|
+
pool: string;
|
|
7
|
+
/** The shared pool's metadata file (inside the pool by default). */
|
|
8
|
+
metadataFile: string;
|
|
9
|
+
/** The pages folder, which holds the vaults. */
|
|
10
|
+
pagesDirectory: string | null;
|
|
11
|
+
layout: PrivateStoreLayout;
|
|
12
|
+
}
|
|
13
|
+
/** The files part of AttachmentManager's entry in the backup document. */
|
|
14
|
+
export interface AttachmentFilesBackupData {
|
|
15
|
+
format: 1;
|
|
16
|
+
pool: {
|
|
17
|
+
archive: string;
|
|
18
|
+
files: number;
|
|
19
|
+
bytes: number;
|
|
20
|
+
};
|
|
21
|
+
vaults: {
|
|
22
|
+
archive: string;
|
|
23
|
+
files: number;
|
|
24
|
+
bytes: number;
|
|
25
|
+
stores: string[];
|
|
26
|
+
};
|
|
27
|
+
/** `backupKeyFingerprint()` of the key both archives are encrypted under; absent when they are not. */
|
|
28
|
+
keyFingerprint?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface AttachmentFilesBackupOutcome {
|
|
31
|
+
data: AttachmentFilesBackupData;
|
|
32
|
+
/** Parts left out (a symbolic link, a vault folder whose name can't be a path segment), each named. */
|
|
33
|
+
warnings: string[];
|
|
34
|
+
}
|
|
35
|
+
/** 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
|
+
/**
|
|
38
|
+
* Stage the attachment files from a backup for the next start, and record
|
|
39
|
+
* the part in the site restore. All or nothing: on any failure every staged
|
|
40
|
+
* path is removed and nothing is recorded.
|
|
41
|
+
*
|
|
42
|
+
* @returns the vaults the backup holds that this site doesn't have (not staged).
|
|
43
|
+
*/
|
|
44
|
+
export declare function stageAttachmentFiles(places: AttachmentFilePlaces, directory: string, data: AttachmentFilesBackupData, metadataJson: string, siteRestore: string): Promise<{
|
|
45
|
+
skippedVaults: string[];
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Put staged attachment files in place, at start, before the metadata is
|
|
49
|
+
* read: only the pool, its metadata file and vault attachment paths may be
|
|
50
|
+
* replaced, whatever the manifest says.
|
|
51
|
+
*/
|
|
52
|
+
export declare function applyAttachmentFilesAtStart(places: AttachmentFilePlaces, siteRestore: string): PartOutcome | null;
|
|
@@ -126,3 +126,9 @@ export interface ApplyOutcome {
|
|
|
126
126
|
* be replaced, so a tampered manifest cannot rename an arbitrary file.
|
|
127
127
|
*/
|
|
128
128
|
export declare function applyStagedRestore(applicationFile: string, allowed: (live: string) => boolean): ApplyOutcome | null;
|
|
129
|
+
/**
|
|
130
|
+
* Set a staged restore aside without applying any of it (#1876): every
|
|
131
|
+
* `*.db.staged` beside the application database and the manifest become
|
|
132
|
+
* `*.incomplete`. Used when the site restore it belongs to is not applied.
|
|
133
|
+
*/
|
|
134
|
+
export declare function setAsideStagedRestore(applicationFile: string, reason: string): ApplyOutcome;
|
|
@@ -307,12 +307,7 @@ export function applyStagedRestore(applicationFile, allowed) {
|
|
|
307
307
|
move(f, `${f}.incomplete`);
|
|
308
308
|
return { applied: [], refused: `staged database files with no restore manifest (an interrupted staging): ${leftovers.map((f) => path.basename(f)).join(', ')} — set aside as *.incomplete` };
|
|
309
309
|
}
|
|
310
|
-
const setAside = (reason) =>
|
|
311
|
-
for (const f of stray())
|
|
312
|
-
move(f, `${f}.incomplete`);
|
|
313
|
-
move(manifestFile, `${manifestFile}.incomplete`);
|
|
314
|
-
return { applied: [], refused: reason };
|
|
315
|
-
};
|
|
310
|
+
const setAside = (reason) => setAsideStagedRestore(applicationFile, reason);
|
|
316
311
|
let manifest;
|
|
317
312
|
try {
|
|
318
313
|
manifest = JSON.parse(readFileSync(manifestFile, 'utf8'));
|
|
@@ -351,6 +346,21 @@ export function applyStagedRestore(applicationFile, allowed) {
|
|
|
351
346
|
unlinkSync(manifestFile);
|
|
352
347
|
return { applied };
|
|
353
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Set a staged restore aside without applying any of it (#1876): every
|
|
351
|
+
* `*.db.staged` beside the application database and the manifest become
|
|
352
|
+
* `*.incomplete`. Used when the site restore it belongs to is not applied.
|
|
353
|
+
*/
|
|
354
|
+
export function setAsideStagedRestore(applicationFile, reason) {
|
|
355
|
+
const manifestFile = manifestPath(applicationFile);
|
|
356
|
+
const dir = path.dirname(applicationFile);
|
|
357
|
+
const staged = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.db.staged')).map((f) => path.join(dir, f)) : [];
|
|
358
|
+
for (const f of staged)
|
|
359
|
+
move(f, `${f}.incomplete`);
|
|
360
|
+
if (existsSync(manifestFile))
|
|
361
|
+
move(manifestFile, `${manifestFile}.incomplete`);
|
|
362
|
+
return { applied: [], refused: reason };
|
|
363
|
+
}
|
|
354
364
|
/** A rename; a copy and unlink when the two paths are on different devices. */
|
|
355
365
|
function move(from, to) {
|
|
356
366
|
try {
|
|
@@ -22,6 +22,10 @@ export type CorePermission =
|
|
|
22
22
|
| 'comment-create'
|
|
23
23
|
/** Change any configuration setting, and download a backup (#1525). Admin only by default; asks for a fresh sign-in when listed in ngdpbase.auth.step-up */
|
|
24
24
|
| 'config-manage'
|
|
25
|
+
/** Create, edit, delete and link contacts: persons (with or without an account) and organizations, and a person's affiliations, through PersonManager and OrganizationManager (#1930). Reading them is contact-read. Admins by default; an add-on grants it to its own roles in its own policy */
|
|
26
|
+
| 'contact-manage'
|
|
27
|
+
/** List, search and view contacts: persons (with or without an account) and organizations, and their affiliations, through PersonManager and OrganizationManager, and the /admin/contacts and /admin/organizations views (#1930). Admins and demo admins by default; an add-on grants it to its own roles in its own policy */
|
|
28
|
+
| 'contact-read'
|
|
25
29
|
/** Browse and view the media library (not delegated to anonymous visitors) */
|
|
26
30
|
| 'media-read'
|
|
27
31
|
/** Create new pages */
|
package/lib/src/types/Asset.d.ts
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
* while AttachmentManager / MediaManager remain thin wrappers.
|
|
7
7
|
*/
|
|
8
8
|
import type { ActorContext } from '../context/ActorContext.js';
|
|
9
|
+
/**
|
|
10
|
+
* What an attachment record says when it was stored with no uploader, in the
|
|
11
|
+
* pool's `author.name`, and what `uploadedBy` shows for a record with none.
|
|
12
|
+
* Only records written before #1926 carry it: a new upload names its
|
|
13
|
+
* uploader's username or is refused. Not a user; the files layer reads it as
|
|
14
|
+
* "no owner" (#1852).
|
|
15
|
+
*/
|
|
16
|
+
export declare const UNKNOWN_UPLOADER = "Unknown";
|
|
9
17
|
/**
|
|
10
18
|
* Capabilities that an AssetProvider may declare.
|
|
11
19
|
* Used to advertise which optional operations the provider supports.
|
|
@@ -207,6 +215,8 @@ export interface AssetRecord {
|
|
|
207
215
|
author?: string;
|
|
208
216
|
/** ISO 8601 timestamp of the last metadata or file modification — schema.org/dateModified */
|
|
209
217
|
dateModified?: string;
|
|
218
|
+
/** Username of whoever last changed it, when the provider records one — schema.org/editor */
|
|
219
|
+
editor?: string;
|
|
210
220
|
/** Licensing and rights information */
|
|
211
221
|
usageRights?: AssetUsageRights;
|
|
212
222
|
/** Download counts and access statistics */
|
|
@@ -116,6 +116,11 @@ export interface WikiConfig {
|
|
|
116
116
|
*/
|
|
117
117
|
'ngdpbase.auth.throttle.max-lock-minutes': number;
|
|
118
118
|
'ngdpbase.mail.rate-limit.enabled': boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Outbound email failures: one administrator notice per this many minutes,
|
|
121
|
+
* counting every failure inside it (#1916). Default: 60.
|
|
122
|
+
*/
|
|
123
|
+
'ngdpbase.mail.failure-notice.window-minutes': number;
|
|
119
124
|
/**
|
|
120
125
|
* Anti-spam: max submissions per IP per `window-minutes` window before
|
|
121
126
|
* the 429 trips (#670 Phase E). Default: 5.
|
|
@@ -305,6 +310,12 @@ export interface ApplicationOrganizationConfig {
|
|
|
305
310
|
'ngdpbase.application.persons.provider.default': string;
|
|
306
311
|
/** Active Person provider class (lowercased; falls back to .default) */
|
|
307
312
|
'ngdpbase.application.persons.provider': string;
|
|
313
|
+
/** #1930: the most results `PersonManager.search` returns when the caller gives no limit */
|
|
314
|
+
'ngdpbase.application.persons.search.limit': number;
|
|
315
|
+
/** #1930: minutes the emailed "Is this you?" sign-up link stays usable */
|
|
316
|
+
'ngdpbase.application.persons.link-offer.ttl-minutes': number;
|
|
317
|
+
/** #1930: the most results `OrganizationManager.search` returns when the caller gives no limit */
|
|
318
|
+
'ngdpbase.application.organization.search.limit': number;
|
|
308
319
|
/** Storage directory for OrganizationRole JSON files (one per (org, namedPosition) pair) */
|
|
309
320
|
'ngdpbase.application.roles.storagedir': string;
|
|
310
321
|
/** Default Role provider class (lowercased) */
|
|
@@ -71,6 +71,13 @@ export interface Organization {
|
|
|
71
71
|
/** Allow extension fields (additionalProperty, etc.) */
|
|
72
72
|
[key: string]: unknown;
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* What a caller gives `OrganizationManager.create` (#1930): the fields, with
|
|
76
|
+
* `@id` optional (it defaults to the `url`, else a new `urn:uuid:`).
|
|
77
|
+
*/
|
|
78
|
+
export type OrganizationInput = Partial<Organization> & {
|
|
79
|
+
name: string;
|
|
80
|
+
};
|
|
74
81
|
/**
|
|
75
82
|
* Patch shape for update operations.
|
|
76
83
|
* `@context`/`@type` are immutable; `@id` cannot be changed via update.
|