@jwilleke/ngdpbase-types 5.21.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 -3
- 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 +65 -1
- package/lib/src/managers/BaseManager.d.ts +14 -0
- package/lib/src/managers/BaseManager.js +9 -0
- package/lib/src/managers/CommentManager.d.ts +10 -1
- package/lib/src/managers/DatabaseManager.d.ts +24 -17
- package/lib/src/managers/DatabaseManager.js +72 -29
- package/lib/src/managers/EmailManager.d.ts +42 -0
- package/lib/src/managers/FootnoteManager.d.ts +10 -1
- package/lib/src/managers/NotificationManager.d.ts +13 -0
- package/lib/src/managers/OrganizationManager.d.ts +40 -6
- package/lib/src/managers/PersonManager.d.ts +106 -9
- package/lib/src/managers/RoleManager.d.ts +19 -1
- package/lib/src/managers/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 +7 -0
- package/lib/src/providers/SqliteDatabaseProvider.js +16 -0
- package/lib/src/providers/SqliteJobStateProvider.d.ts +10 -0
- package/lib/src/providers/attachmentFilesBackup.d.ts +52 -0
- package/lib/src/providers/sqliteBackup.d.ts +6 -0
- package/lib/src/providers/sqliteBackup.js +16 -6
- package/lib/src/security/permissions.generated.d.ts +4 -0
- package/lib/src/types/Asset.d.ts +10 -0
- package/lib/src/types/Config.d.ts +11 -0
- package/lib/src/types/Organization.d.ts +7 -0
- package/lib/src/types/Person.d.ts +27 -3
- package/lib/src/types/PersonProvider.d.ts +6 -0
- package/lib/src/types/WikiEngine.d.ts +9 -21
- package/lib/src/utils/addonPageSync.d.ts +24 -0
- package/lib/src/utils/administrators.d.ts +21 -0
- package/lib/src/utils/auditEventNames.generated.d.ts +4 -0
- package/lib/src/utils/auditEventNames.generated.js +4 -0
- package/lib/src/utils/backupCrypto.d.ts +28 -0
- package/lib/src/utils/backupKey.d.ts +23 -0
- package/lib/src/utils/backupKey.js +37 -0
- package/lib/src/utils/contactAccess.d.ts +58 -0
- package/lib/src/utils/databaseKey.d.ts +7 -0
- package/lib/src/utils/databaseKey.js +7 -0
- package/lib/src/utils/directoryBackup.d.ts +78 -0
- package/lib/src/utils/emailForLog.d.ts +20 -0
- package/lib/src/utils/maintenanceState.d.ts +153 -0
- package/lib/src/utils/privateStoreTakeout.d.ts +2 -2
- package/lib/src/utils/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
- package/lib/src/utils/startupState.d.ts +0 -65
- package/lib/src/utils/startupState.js +0 -115
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
* Implements the MailProvider interface so it can be passed directly to
|
|
13
13
|
* MagicLinkAuthProvider without any changes to that class.
|
|
14
14
|
*
|
|
15
|
+
* A failed send is never silent (#1916). Every sender goes through `send()`,
|
|
16
|
+
* so it is reported here, once for all of them: logged (provider, recipient
|
|
17
|
+
* domain, subject, error — never the body or the full address), the manager
|
|
18
|
+
* shows degraded until the next send succeeds, and administrators get one
|
|
19
|
+
* notification per `ngdpbase.mail.failure-notice.window-minutes` carrying a
|
|
20
|
+
* running count, not one per email. `send()` still rejects, so a caller with
|
|
21
|
+
* a person waiting can tell them.
|
|
22
|
+
*
|
|
15
23
|
* @see docs/admin/email-setup.md for operator setup instructions
|
|
16
24
|
* @see {@link https://github.com/jwilleke/ngdpbase/issues/456}
|
|
17
25
|
*/
|
|
@@ -19,21 +27,55 @@ import BaseManager from './BaseManager.js';
|
|
|
19
27
|
import type { BackupData } from './BaseManager.js';
|
|
20
28
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
21
29
|
import type { MailMessage, MailProvider } from '../mail/MailProvider.js';
|
|
30
|
+
/** The degraded reason while sends fail; cleared by the next success. */
|
|
31
|
+
export declare const SEND_FAILING_REASON = "outbound email is failing \u2014 see the server log and the administrator notification";
|
|
32
|
+
/** Title of the administrator notice. */
|
|
33
|
+
export declare const SEND_FAILING_TITLE = "Email could not be sent";
|
|
22
34
|
declare class EmailManager extends BaseManager implements MailProvider {
|
|
23
35
|
private provider;
|
|
24
36
|
private providerName;
|
|
25
37
|
private from;
|
|
26
38
|
private enabled;
|
|
39
|
+
private noticeWindowMs;
|
|
40
|
+
private notice;
|
|
41
|
+
private failuresSinceSuccess;
|
|
27
42
|
constructor(engine: WikiEngine);
|
|
28
43
|
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
29
44
|
/**
|
|
30
45
|
* Send an email. Injects the configured from address if the message omits it.
|
|
46
|
+
*
|
|
47
|
+
* A failure is reported here (#1916) and then rethrown, so a caller with a
|
|
48
|
+
* person waiting can tell them it was not sent.
|
|
31
49
|
*/
|
|
32
50
|
send(message: MailMessage): Promise<void>;
|
|
33
51
|
/**
|
|
34
52
|
* Convenience wrapper — build and send a message in one call.
|
|
35
53
|
*/
|
|
36
54
|
sendTo(to: string, subject: string, text: string, html?: string): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Send without waiting, for a sender nobody is waiting on (a form's copy, a
|
|
57
|
+
* reservation notice). Never rejects: a failure is already logged and
|
|
58
|
+
* reported to administrators by `send()` (#1916), so there is nothing left
|
|
59
|
+
* for the caller to do with it.
|
|
60
|
+
*/
|
|
61
|
+
sendInBackground(to: string, subject: string, text: string, html?: string): void;
|
|
62
|
+
/** Whether sends are failing right now: degraded by a failed send, not yet cleared by a success. */
|
|
63
|
+
isDeliveryFailing(): boolean;
|
|
64
|
+
private reportFailure;
|
|
65
|
+
private reportSuccess;
|
|
66
|
+
/**
|
|
67
|
+
* One notice per window, with a running count (#1916). A failure inside an
|
|
68
|
+
* open window updates that notice; the first failure after it opens a new
|
|
69
|
+
* one. The notice is created with `escalate: false`, so it is never emailed:
|
|
70
|
+
* that send would fail and report another failure.
|
|
71
|
+
*
|
|
72
|
+
* Never throws — it runs off the failed send, and a notice that cannot be
|
|
73
|
+
* written is already covered by the error log above.
|
|
74
|
+
*/
|
|
75
|
+
private noticeAdministrators;
|
|
76
|
+
private createNotice;
|
|
77
|
+
private noticeMessage;
|
|
78
|
+
private notifications;
|
|
37
79
|
/** Name of the active provider: 'console' | 'smtp' */
|
|
38
80
|
getProviderName(): string;
|
|
39
81
|
/** Effective from address (smtp.from or mail.from) */
|
|
@@ -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 ActorContext } from '../context/ActorContext.js';
|
|
4
4
|
import { type VaultPageNotesOutside, type PageRef } from '../utils/pageNotes.js';
|
|
@@ -17,6 +17,15 @@ export default class FootnoteManager extends BaseManager {
|
|
|
17
17
|
private readonly outsideCheck;
|
|
18
18
|
constructor(engine: WikiEngine);
|
|
19
19
|
initialize(): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* The public folder is in the backup (#1876), as `<stem>.footnotes.tar` beside
|
|
22
|
+
* the document: the public pages' footnotes. A vault page's are in its vault, which
|
|
23
|
+
* PageManager backs up with the vault, sealed as stored.
|
|
24
|
+
*/
|
|
25
|
+
backup(options?: ManagerBackupOptions): Promise<BackupData>;
|
|
26
|
+
/** Staged for the next start with the rest of a site restore (#1876); the live folder is untouched. */
|
|
27
|
+
restore(backupData: BackupData, options?: ManagerRestoreOptions): Promise<void>;
|
|
28
|
+
restoreIsStaged(): boolean;
|
|
20
29
|
isEnabled(): boolean;
|
|
21
30
|
/** The public folder and the vaults, as configured now (#1839). */
|
|
22
31
|
private roots;
|
|
@@ -25,6 +25,12 @@ export interface NotificationInput {
|
|
|
25
25
|
level?: 'info' | 'warning' | 'error' | 'success';
|
|
26
26
|
targetUsers?: string[];
|
|
27
27
|
expiresAt?: Date | null;
|
|
28
|
+
/**
|
|
29
|
+
* False: never email this notification, whatever the escalation settings
|
|
30
|
+
* (#1916). A notice that email is failing must not itself be emailed — the
|
|
31
|
+
* send would fail and report another failure. Not stored.
|
|
32
|
+
*/
|
|
33
|
+
escalate?: boolean;
|
|
28
34
|
}
|
|
29
35
|
/**
|
|
30
36
|
* Notification statistics
|
|
@@ -102,6 +108,13 @@ declare class NotificationManager extends BaseManager {
|
|
|
102
108
|
* @returns {Promise<string>} Notification ID
|
|
103
109
|
*/
|
|
104
110
|
createNotification(notification: NotificationInput): Promise<string>;
|
|
111
|
+
/**
|
|
112
|
+
* Replace an active notification's message in place (#1916) — a running
|
|
113
|
+
* count on one notice instead of a new notice per event. Never escalates:
|
|
114
|
+
* the notification was already escalated, or not, when it was created.
|
|
115
|
+
* @returns false when there is no such notification
|
|
116
|
+
*/
|
|
117
|
+
updateNotificationMessage(notificationId: string, message: string): Promise<boolean>;
|
|
105
118
|
/**
|
|
106
119
|
* Add a notification (alias for createNotification for backward compatibility)
|
|
107
120
|
* @param {NotificationInput} notification - Notification object
|
|
@@ -1,7 +1,9 @@
|
|
|
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 { Organization, OrganizationUpdate } from '../types/Organization.js';
|
|
3
|
+
import type { Organization, OrganizationInput, OrganizationUpdate } from '../types/Organization.js';
|
|
4
4
|
import type { OrganizationProvider } from '../types/OrganizationProvider.js';
|
|
5
|
+
import type { Person } from '../types/Person.js';
|
|
6
|
+
import type { ActorContext } from '../context/ActorContext.js';
|
|
5
7
|
/**
|
|
6
8
|
* Subset of install-form data this manager needs to seed the anchor org.
|
|
7
9
|
* Kept narrow so InstallService and tests can pass plain objects without
|
|
@@ -34,6 +36,8 @@ declare class OrganizationManager extends BaseManager {
|
|
|
34
36
|
readonly description = "Canonical Organization records (#617)";
|
|
35
37
|
private provider;
|
|
36
38
|
private providerClass?;
|
|
39
|
+
/** `ngdpbase.application.organization.storagedir`, resolved at start: what a backup copies (#1876). */
|
|
40
|
+
private storageDir;
|
|
37
41
|
private installOrgCache;
|
|
38
42
|
private byIdCache;
|
|
39
43
|
private byFileCache;
|
|
@@ -41,6 +45,11 @@ declare class OrganizationManager extends BaseManager {
|
|
|
41
45
|
constructor(engine: WikiEngine);
|
|
42
46
|
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
43
47
|
/** Provider accessor, mainly for tests. */
|
|
48
|
+
/** Organization records are in the backup (#1876), as `<stem>.organizations.tar` beside the document. */
|
|
49
|
+
backup(options?: ManagerBackupOptions): Promise<BackupData>;
|
|
50
|
+
/** Staged for the next start with the rest of a site restore (#1876); the live records are untouched. */
|
|
51
|
+
restore(backupData: BackupData, options?: ManagerRestoreOptions): Promise<void>;
|
|
52
|
+
restoreIsStaged(): boolean;
|
|
44
53
|
getProvider(): OrganizationProvider | null;
|
|
45
54
|
/**
|
|
46
55
|
* Create the anchor organization from the instance's own identity (#1027).
|
|
@@ -81,10 +90,35 @@ declare class OrganizationManager extends BaseManager {
|
|
|
81
90
|
getByFile(filename: string): Promise<Organization | null>;
|
|
82
91
|
/** #1006: how many organizations this instance holds. Count only. */
|
|
83
92
|
getManagerStats(): Promise<ManagerStats>;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
/** Every organization, for a caller who may read contacts (`contact-read`). */
|
|
94
|
+
list(ctx: ActorContext): Promise<Organization[]>;
|
|
95
|
+
/** One organization by `@id`, for a caller who may read contacts (`contact-read`). */
|
|
96
|
+
get(ctx: ActorContext, id: string): Promise<Organization | null>;
|
|
97
|
+
/**
|
|
98
|
+
* Organizations whose `name`, `legalName`, `email` or a contact point's
|
|
99
|
+
* email contains `query`, ignoring case; an empty query matches all. At
|
|
100
|
+
* most `limit` (`ngdpbase.application.organization.search.limit` by default).
|
|
101
|
+
*/
|
|
102
|
+
search(ctx: ActorContext, query: string, options?: {
|
|
103
|
+
limit?: number;
|
|
104
|
+
}): Promise<Organization[]>;
|
|
105
|
+
/**
|
|
106
|
+
* The people affiliated with an organization, read from each person's
|
|
107
|
+
* `affiliation` (#1930): the link is stored once, on the person. Each comes
|
|
108
|
+
* with the role they hold there, when one is given.
|
|
109
|
+
*/
|
|
110
|
+
people(ctx: ActorContext, organizationId: string): Promise<Array<{
|
|
111
|
+
person: Person;
|
|
112
|
+
role?: string;
|
|
113
|
+
}>>;
|
|
114
|
+
/**
|
|
115
|
+
* Create an organization. `name` is required; `@id` defaults to the `url`,
|
|
116
|
+
* else a new `urn:uuid:`. `filename` is for the install anchor (seeding);
|
|
117
|
+
* other records take one from the url or name.
|
|
118
|
+
*/
|
|
119
|
+
create(ctx: ActorContext, org: OrganizationInput, filename?: string): Promise<Organization>;
|
|
120
|
+
update(ctx: ActorContext, id: string, patch: OrganizationUpdate): Promise<Organization | null>;
|
|
121
|
+
delete(ctx: ActorContext, id: string): Promise<boolean>;
|
|
88
122
|
/**
|
|
89
123
|
* Clear all caches. Called on every write — the simplest correct
|
|
90
124
|
* invalidation. Per-key invalidation can come later if needed.
|
|
@@ -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
|
|
@@ -36,6 +36,13 @@ declare class SqliteDatabaseProvider extends BaseDatabaseProvider<SqliteHandle>
|
|
|
36
36
|
busyTimeoutMs?: number;
|
|
37
37
|
});
|
|
38
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;
|
|
39
46
|
/** Read an existing file read-only: the key opens it and it passes the quick check, or this throws. */
|
|
40
47
|
private static probe;
|
|
41
48
|
/**
|
|
@@ -62,6 +62,22 @@ class SqliteDatabaseProvider extends BaseDatabaseProvider {
|
|
|
62
62
|
db.pragma("cipher='sqlcipher'");
|
|
63
63
|
db.pragma(`key='${key.replace(/'/g, "''")}'`);
|
|
64
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
|
+
}
|
|
65
81
|
/** Read an existing file read-only: the key opens it and it passes the quick check, or this throws. */
|
|
66
82
|
static probe(file, key) {
|
|
67
83
|
const db = new Database(file, { readonly: true, fileMustExist: true });
|
|
@@ -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;
|