@jwilleke/ngdpbase-types 5.26.0 → 5.28.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 +8 -2
- package/lib/packages/ngdpbase-types/testing.d.ts +7 -3
- package/lib/packages/ngdpbase-types/testing.js +7 -3
- package/lib/src/context/ActorContext.d.ts +15 -2
- package/lib/src/context/ActorContext.js +20 -0
- package/lib/src/converters/DocxConverter.d.ts +30 -0
- package/lib/src/converters/MarkdownConverter.d.ts +31 -0
- package/lib/src/export/ChromeServicePdfProvider.d.ts +52 -0
- package/lib/src/export/ChromeServicePdfProvider.js +153 -0
- package/lib/src/export/LocalChromePdfProvider.d.ts +25 -0
- package/lib/src/export/LocalChromePdfProvider.js +33 -0
- package/lib/src/export/PdfProvider.d.ts +51 -0
- package/lib/src/export/PdfProvider.js +37 -0
- package/lib/src/export/pdfEngine.d.ts +113 -0
- package/lib/src/export/pdfEngine.js +195 -0
- package/lib/src/ice/converterShim.d.ts +5 -0
- package/lib/src/ice/fileHandling.d.ts +30 -0
- package/lib/src/ice/ledger.d.ts +37 -0
- package/lib/src/ice/runImport.d.ts +51 -0
- package/lib/src/ice/types.d.ts +284 -0
- package/lib/src/mail/MailProvider.d.ts +15 -0
- package/lib/src/mail/NodemailerMailProvider.d.ts +1 -1
- package/lib/src/managers/AddonsManager.d.ts +25 -5
- package/lib/src/managers/AssetManager.d.ts +7 -3
- package/lib/src/managers/EmailManager.d.ts +32 -5
- package/lib/src/managers/ExportManager.d.ts +62 -0
- package/lib/src/managers/ExportManager.js +152 -2
- package/lib/src/managers/ImportManager.d.ts +611 -0
- package/lib/src/managers/OrganizationManager.d.ts +5 -3
- package/lib/src/managers/PageManager.d.ts +77 -1
- package/lib/src/managers/PersonManager.d.ts +36 -11
- package/lib/src/managers/RoleManager.d.ts +76 -17
- package/lib/src/managers/UserManager.d.ts +55 -2
- package/lib/src/security/PolicyDecisionPoint.d.ts +18 -9
- package/lib/src/security/PolicyInformationPoint.d.ts +48 -10
- package/lib/src/types/Config.d.ts +2 -0
- package/lib/src/types/User.d.ts +1 -1
- package/lib/src/utils/addonHost.d.ts +23 -2
- package/lib/src/utils/addonHost.js +25 -2
- package/lib/src/utils/auditEventNames.generated.d.ts +10 -10
- package/lib/src/utils/auditEventNames.generated.js +10 -10
- package/lib/src/utils/contactAccess.d.ts +30 -0
- package/lib/src/utils/frontmatterAccess.d.ts +54 -11
- package/lib/src/utils/importRunSummary.d.ts +64 -0
- package/lib/src/utils/keyedSerial.d.ts +31 -0
- package/lib/src/utils/ncmNotify.d.ts +30 -0
- package/lib/src/utils/privateStoreAccess.js +1 -3
- package/lib/src/utils/privateStoreImport.d.ts +58 -0
- package/lib/src/utils/roleChangeError.d.ts +25 -0
- package/lib/src/utils/subjectMayDo.d.ts +2 -2
- package/lib/src/utils/zipArchive.d.ts +71 -0
- package/package.json +1 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ICE — import, convert, export (#1979): the shapes the engine, its shims and
|
|
3
|
+
* its receivers share. The design and every decision (D1–D11) are in
|
|
4
|
+
* docs/planning/ice-planing.md; this file is their code form.
|
|
5
|
+
*
|
|
6
|
+
* - A __shim__ knows one outside format and nothing else (D2). It declares
|
|
7
|
+
* its direction, what it accepts, what it produces and which receivers it
|
|
8
|
+
* may hand to (D3, D7). Shims chain: one's output is the next one's input (D9).
|
|
9
|
+
* - A __receiver__ is a door that already exists — PageManager for pages, the
|
|
10
|
+
* upload door for files, an add-on's own manager for its records (D4). The
|
|
11
|
+
* engine hands it each result and never writes storage itself. The receiver
|
|
12
|
+
* checks its own existing permission with the context the run carries (D5).
|
|
13
|
+
* - The __engine__ (ImportManager) owns everything else: sources, unzip, the
|
|
14
|
+
* noise filter, fingerprints, skipping unchanged files, per-file status,
|
|
15
|
+
* keeping the original, and running the work as a background job.
|
|
16
|
+
*
|
|
17
|
+
* Only importing is built in the first slice. `out` / `both` shims and the
|
|
18
|
+
* `write` half are declared so export (#1974) and feeds (D10) fit later
|
|
19
|
+
* without changing these shapes.
|
|
20
|
+
*/
|
|
21
|
+
import type { ActorContext } from '../context/ActorContext.js';
|
|
22
|
+
import type { GuardedFetchOptions, GuardedResponse } from '../http/guardedFetch.js';
|
|
23
|
+
/** D3: which way a shim moves data. Most are `in`; PDF (#1974) would be `both`. */
|
|
24
|
+
export type ShimDirection = 'in' | 'out' | 'both';
|
|
25
|
+
/**
|
|
26
|
+
* The kind of a piece of content moving through the engine. `file` is raw
|
|
27
|
+
* input bytes; `html` and `ncm` are core's; an add-on's own kinds are named
|
|
28
|
+
* `<slug>.<kind>`, e.g. `phr.health-record`.
|
|
29
|
+
*/
|
|
30
|
+
export type IceKind = string;
|
|
31
|
+
/** A structured note from a shim, the same shape as a converter's warning. */
|
|
32
|
+
export interface IceWarning {
|
|
33
|
+
kind: string;
|
|
34
|
+
detail: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* One piece of content: a file the engine read, or what a shim produced from
|
|
38
|
+
* it. A shim's output becomes the next shim's input, or a receiver's.
|
|
39
|
+
*/
|
|
40
|
+
export interface IceItem {
|
|
41
|
+
kind: IceKind;
|
|
42
|
+
/** The name it arrived under: the upload's file name, or `archive.zip/inner/path`. */
|
|
43
|
+
name: string;
|
|
44
|
+
mediaType?: string;
|
|
45
|
+
/** Raw bytes (a `file`, or binary output). */
|
|
46
|
+
bytes?: Buffer;
|
|
47
|
+
/** Text content (`html`, `ncm`, an XML document). */
|
|
48
|
+
text?: string;
|
|
49
|
+
/** A structured record, for record kinds such as health records. */
|
|
50
|
+
data?: unknown;
|
|
51
|
+
/** Facts about the content: a title, an author, a source system's id. */
|
|
52
|
+
metadata?: Record<string, unknown>;
|
|
53
|
+
warnings?: IceWarning[];
|
|
54
|
+
/**
|
|
55
|
+
* The item's own id or time, for a shim whose duplicates are per item
|
|
56
|
+
* (a feed entry): an item whose key the engine has seen before is skipped.
|
|
57
|
+
*/
|
|
58
|
+
itemKey?: string;
|
|
59
|
+
}
|
|
60
|
+
/** What a shim takes. A file matches by extension or media type; a chained input by kind. */
|
|
61
|
+
export interface ShimAccepts {
|
|
62
|
+
/** Lower-case, with the dot: `['.xml']`. */
|
|
63
|
+
extensions?: string[];
|
|
64
|
+
mediaTypes?: string[];
|
|
65
|
+
/** Kinds produced by another shim that this one reads (D9), e.g. `['html']`. */
|
|
66
|
+
kinds?: IceKind[];
|
|
67
|
+
/**
|
|
68
|
+
* "Can I read this?" (D7), on the content. When present it must answer yes
|
|
69
|
+
* even for a file whose extension matches; it is also how a file with no
|
|
70
|
+
* matching extension is claimed.
|
|
71
|
+
*/
|
|
72
|
+
canRead?(input: IceItem): boolean;
|
|
73
|
+
}
|
|
74
|
+
/** What the engine hands a shim besides its input. */
|
|
75
|
+
export interface ShimTools {
|
|
76
|
+
/**
|
|
77
|
+
* Follow-up fetches go through the engine (design rule): the one guarded
|
|
78
|
+
* path, with the site's egress policy, User-Agent and timeout.
|
|
79
|
+
*/
|
|
80
|
+
fetch(url: string, options?: Omit<GuardedFetchOptions, 'policy' | 'transport' | 'resolver'>): Promise<GuardedResponse>;
|
|
81
|
+
/** This shim's options from site configuration (D6): `ngdpbase.import.shims.<id>.options`. */
|
|
82
|
+
options: Record<string, unknown>;
|
|
83
|
+
}
|
|
84
|
+
/** What a shim's read returns. */
|
|
85
|
+
export interface ShimReadResult {
|
|
86
|
+
items: IceItem[];
|
|
87
|
+
/**
|
|
88
|
+
* Set when the shim read only part of its input (phr's `readCcda` returns
|
|
89
|
+
* records plus a partial note): the file's status is then `partial`, with this.
|
|
90
|
+
*/
|
|
91
|
+
partial?: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A shim as its author writes it (D7). `name` is unique within its owner; the
|
|
95
|
+
* engine names it `<owner>.<name>` — `core.markdown`, `phr.ccda`.
|
|
96
|
+
*/
|
|
97
|
+
export interface IceShim {
|
|
98
|
+
name: string;
|
|
99
|
+
/** Shown to people choosing a format. */
|
|
100
|
+
label?: string;
|
|
101
|
+
direction: ShimDirection;
|
|
102
|
+
accepts: ShimAccepts;
|
|
103
|
+
/** The kind its output items have. */
|
|
104
|
+
produces: IceKind;
|
|
105
|
+
/**
|
|
106
|
+
* Receivers it may hand to directly (ids, `core.pages` or `<slug>.<name>`).
|
|
107
|
+
* Empty for a shim whose output only feeds another shim.
|
|
108
|
+
*/
|
|
109
|
+
receivers: string[];
|
|
110
|
+
/** Two duplicate kinds (design rule): a whole file by fingerprint, or each item by `itemKey`. Default `file`. */
|
|
111
|
+
duplicates?: 'file' | 'item';
|
|
112
|
+
/** Inbound: read one input. Required for `in` and `both`. */
|
|
113
|
+
read?(input: IceItem, tools: ShimTools): Promise<ShimReadResult>;
|
|
114
|
+
/** Outbound (export, #1974): not run in the first slice. Required for `out` and `both`. */
|
|
115
|
+
write?(items: IceItem[], tools: ShimTools): Promise<IceItem[]>;
|
|
116
|
+
}
|
|
117
|
+
/** A shim the engine holds: its full id, and who registered it. */
|
|
118
|
+
export interface RegisteredShim extends IceShim {
|
|
119
|
+
id: string;
|
|
120
|
+
owner: string;
|
|
121
|
+
}
|
|
122
|
+
/** D11: what the person importing chose to do with the original file. */
|
|
123
|
+
export type OriginalChoice = 'cite' | 'keep' | 'none';
|
|
124
|
+
/**
|
|
125
|
+
* D11: the original-file choices a receiver offers, and which one is chosen
|
|
126
|
+
* when the user picks none (#2021). The import screen offers only `allowed`,
|
|
127
|
+
* and the engine refuses a run asking for any other.
|
|
128
|
+
*/
|
|
129
|
+
export interface IceOriginalChoices {
|
|
130
|
+
allowed: OriginalChoice[];
|
|
131
|
+
/** One of `allowed`. Default: `ngdpbase.import.engine.default-original` when allowed, else the first allowed. */
|
|
132
|
+
default?: OriginalChoice;
|
|
133
|
+
}
|
|
134
|
+
/** A citation to the origin (D11): the receiver renders it, as a footnote on a page. */
|
|
135
|
+
export interface IceCitation {
|
|
136
|
+
name: string;
|
|
137
|
+
sha256: string;
|
|
138
|
+
importedAt: string;
|
|
139
|
+
}
|
|
140
|
+
/** Where a kept original went (D11). */
|
|
141
|
+
export interface KeptOriginal {
|
|
142
|
+
name: string;
|
|
143
|
+
sha256: string;
|
|
144
|
+
/** The keeping door's own reference: an attachment id, a record id. */
|
|
145
|
+
ref: string;
|
|
146
|
+
}
|
|
147
|
+
/** What a receiver is told about the item it is handed. */
|
|
148
|
+
export interface ReceiveInfo {
|
|
149
|
+
runId: string;
|
|
150
|
+
/** The file the item came from. */
|
|
151
|
+
source: {
|
|
152
|
+
name: string;
|
|
153
|
+
sha256: string;
|
|
154
|
+
};
|
|
155
|
+
/** The shims the item passed through, in order. */
|
|
156
|
+
chain: string[];
|
|
157
|
+
citation?: IceCitation;
|
|
158
|
+
original?: KeptOriginal;
|
|
159
|
+
}
|
|
160
|
+
/** A receiver's answer for one item. `refused` is its existing permission saying no (D5). */
|
|
161
|
+
export interface ReceiveOutcome {
|
|
162
|
+
status: 'stored' | 'duplicate' | 'refused';
|
|
163
|
+
/** Where it went: a page name, an attachment id, a record id. */
|
|
164
|
+
ref?: string;
|
|
165
|
+
message?: string;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* A receiver as its owner writes it (D4). Its doors are the receiver's own
|
|
169
|
+
* existing ones; each checks its own permission with the context given (D5).
|
|
170
|
+
*/
|
|
171
|
+
export interface IceReceiver {
|
|
172
|
+
name: string;
|
|
173
|
+
label?: string;
|
|
174
|
+
/** The kinds it takes, e.g. `['ncm']`, `['file']`, `['phr.health-record']`. */
|
|
175
|
+
accepts: IceKind[];
|
|
176
|
+
/** May this context use this receiver? Its existing permission — the import screen shows only these (D5). */
|
|
177
|
+
mayUse(ctx: ActorContext): Promise<boolean>;
|
|
178
|
+
/** Hand one item to the receiver's own door. */
|
|
179
|
+
receive(item: IceItem, ctx: ActorContext, info: ReceiveInfo): Promise<ReceiveOutcome>;
|
|
180
|
+
/**
|
|
181
|
+
* Keep the original file itself (D11: phr keeps originals in its own
|
|
182
|
+
* store). Without this, `keep` hands the original to the files receiver.
|
|
183
|
+
*/
|
|
184
|
+
keepOriginal?(original: IceItem, ctx: ActorContext): Promise<ReceiveOutcome>;
|
|
185
|
+
/** The original-file choices this receiver offers (D11). Without it: all three. */
|
|
186
|
+
originals?: IceOriginalChoices;
|
|
187
|
+
}
|
|
188
|
+
export interface RegisteredReceiver extends IceReceiver {
|
|
189
|
+
id: string;
|
|
190
|
+
owner: string;
|
|
191
|
+
}
|
|
192
|
+
/** One file handed to the engine. A `.zip` is unpacked by the engine. */
|
|
193
|
+
export interface IceUpload {
|
|
194
|
+
name: string;
|
|
195
|
+
bytes: Buffer;
|
|
196
|
+
mediaType?: string;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* A source (D1). The first slice reads uploads; a URL fetched on a schedule
|
|
200
|
+
* (D10, feeds) and a server folder join this union later.
|
|
201
|
+
*/
|
|
202
|
+
export type IceSource = {
|
|
203
|
+
type: 'upload';
|
|
204
|
+
files: IceUpload[];
|
|
205
|
+
};
|
|
206
|
+
/** One import, as a caller asks for it. */
|
|
207
|
+
export interface IceRunRequest {
|
|
208
|
+
source: IceSource;
|
|
209
|
+
/** The receiver id the results go to: `core.pages`, `phr.health-records`. */
|
|
210
|
+
receiver: string;
|
|
211
|
+
/** Pin the first shim (a format the person chose) instead of detecting one. */
|
|
212
|
+
shim?: string;
|
|
213
|
+
/** D11. Default `ngdpbase.import.engine.default-original`. */
|
|
214
|
+
original?: OriginalChoice;
|
|
215
|
+
/**
|
|
216
|
+
* Whose fingerprints and item keys this run is checked against. Default
|
|
217
|
+
* `<receiver>/<username>`: the same file imported again by the same person
|
|
218
|
+
* into the same receiver is skipped. A caller with its own notion of a
|
|
219
|
+
* source (phr's source id) passes it.
|
|
220
|
+
*/
|
|
221
|
+
scope?: string;
|
|
222
|
+
}
|
|
223
|
+
/** Per-file status (first slice). */
|
|
224
|
+
export type IceFileStatus = 'imported' | 'partial' | 'unchanged' | 'duplicate' | 'noise' | 'unreadable' | 'refused' | 'failed';
|
|
225
|
+
export interface IceFileReport {
|
|
226
|
+
name: string;
|
|
227
|
+
sha256?: string;
|
|
228
|
+
size: number;
|
|
229
|
+
status: IceFileStatus;
|
|
230
|
+
message?: string;
|
|
231
|
+
/** The shims the file went through. */
|
|
232
|
+
chain: string[];
|
|
233
|
+
/** Where each item went. */
|
|
234
|
+
refs: string[];
|
|
235
|
+
/** Items stored, and items skipped as already seen. */
|
|
236
|
+
stored: number;
|
|
237
|
+
duplicates: number;
|
|
238
|
+
original?: KeptOriginal;
|
|
239
|
+
warnings: IceWarning[];
|
|
240
|
+
}
|
|
241
|
+
export interface IceRunReport {
|
|
242
|
+
runId: string;
|
|
243
|
+
receiver: string;
|
|
244
|
+
scope: string;
|
|
245
|
+
/** Who it ran as. */
|
|
246
|
+
username: string;
|
|
247
|
+
state: 'queued' | 'running' | 'done' | 'failed';
|
|
248
|
+
startedAt?: string;
|
|
249
|
+
finishedAt?: string;
|
|
250
|
+
error?: string;
|
|
251
|
+
files: IceFileReport[];
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* A kept original as the ledger remembers it, with the door that kept it
|
|
255
|
+
* (a receiver's own `keepOriginal`, or `core.files`). A later run of the same
|
|
256
|
+
* fingerprint through the same door reuses it instead of keeping another copy.
|
|
257
|
+
*/
|
|
258
|
+
export interface LedgerOriginal extends KeptOriginal {
|
|
259
|
+
door: string;
|
|
260
|
+
}
|
|
261
|
+
/** What the engine remembers about one file in a scope. */
|
|
262
|
+
export interface LedgerFileEntry {
|
|
263
|
+
name: string;
|
|
264
|
+
sha256: string;
|
|
265
|
+
size: number;
|
|
266
|
+
status: IceFileStatus;
|
|
267
|
+
message?: string;
|
|
268
|
+
at: string;
|
|
269
|
+
refs: string[];
|
|
270
|
+
/** The original kept for this fingerprint, whatever became of the conversion (D11). */
|
|
271
|
+
original?: LedgerOriginal;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Where the engine keeps fingerprints, per-file status and item keys, per
|
|
275
|
+
* scope. One interface so a database table can replace the file store when
|
|
276
|
+
* the application database is on.
|
|
277
|
+
*/
|
|
278
|
+
export interface IceLedger {
|
|
279
|
+
lastFile(scope: string, sha256: string): Promise<LedgerFileEntry | null>;
|
|
280
|
+
recordFile(scope: string, entry: LedgerFileEntry): Promise<void>;
|
|
281
|
+
hasItem(scope: string, itemKey: string): Promise<boolean>;
|
|
282
|
+
recordItem(scope: string, itemKey: string): Promise<void>;
|
|
283
|
+
files(scope: string): Promise<LedgerFileEntry[]>;
|
|
284
|
+
}
|
|
@@ -20,7 +20,22 @@ export interface MailMessage {
|
|
|
20
20
|
text: string;
|
|
21
21
|
/** Optional HTML body */
|
|
22
22
|
html?: string;
|
|
23
|
+
/** Files sent with the message (#1974); their total is capped by `ngdpbase.mail.attachments.max-bytes`. */
|
|
24
|
+
attachments?: MailAttachment[];
|
|
23
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* One file attached to a message (#1974), as Nodemailer takes it. The content
|
|
28
|
+
* is never logged: a log line says how many files and how many bytes.
|
|
29
|
+
*/
|
|
30
|
+
export interface MailAttachment {
|
|
31
|
+
/** The name the recipient sees, e.g. `invoice-2026-0042.pdf`. */
|
|
32
|
+
filename: string;
|
|
33
|
+
/** Its media type, e.g. `application/pdf`. */
|
|
34
|
+
contentType: string;
|
|
35
|
+
content: Buffer;
|
|
36
|
+
}
|
|
37
|
+
/** How many files and bytes a message carries — the only thing a log line says about attachments. */
|
|
38
|
+
export declare function describeAttachments(attachments: MailAttachment[] | undefined): string;
|
|
24
39
|
/**
|
|
25
40
|
* Common interface for all mail transports.
|
|
26
41
|
*/
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* ngdpbase.mail.provider.smtp.pass
|
|
10
10
|
* ngdpbase.mail.provider.smtp.from
|
|
11
11
|
*/
|
|
12
|
-
import type
|
|
12
|
+
import { type MailMessage, type MailProvider } from './MailProvider.js';
|
|
13
13
|
export interface SmtpConfig {
|
|
14
14
|
host: string;
|
|
15
15
|
port: number;
|
|
@@ -121,8 +121,12 @@ export interface AddonModule {
|
|
|
121
121
|
* "section was not rendered at all" (addon disabled mid-session, cached
|
|
122
122
|
* form). Without it, absent checkbox fields look identical to "user
|
|
123
123
|
* submitted no such field" and you may silently clobber stored values.
|
|
124
|
+
*
|
|
125
|
+
* `ctx` is the signed-in user's own context, forwarded from the request:
|
|
126
|
+
* pass it to your manager's door (and to the PDP) instead of rebuilding a
|
|
127
|
+
* subject from `user` (#2018).
|
|
124
128
|
*/
|
|
125
|
-
profileSection?(user: AddonProfileUser): Promise<AddonProfileSection | null> | AddonProfileSection | null;
|
|
129
|
+
profileSection?(user: AddonProfileUser, ctx: ActorContext): Promise<AddonProfileSection | null> | AddonProfileSection | null;
|
|
126
130
|
/**
|
|
127
131
|
* Optional save handler paired with `profileSection()` (#534).
|
|
128
132
|
*
|
|
@@ -238,6 +242,19 @@ export interface AddonManifest {
|
|
|
238
242
|
family?: string;
|
|
239
243
|
/** #1965: `false` keeps plumbing (e.g. elasticsearch) out of the add-on listings. */
|
|
240
244
|
listed?: boolean;
|
|
245
|
+
/**
|
|
246
|
+
* #1979 (ICE, D6): the names of the import shims this add-on registers at
|
|
247
|
+
* load with `host.registerShim`, e.g. `["ccda", "fhir"]` (ids `<slug>.ccda`,
|
|
248
|
+
* `<slug>.fhir`). The host refuses a shim not named here. Site configuration
|
|
249
|
+
* switches each one: `ngdpbase.import.shims.<slug>.<name>.enabled`.
|
|
250
|
+
*/
|
|
251
|
+
shims?: string[];
|
|
252
|
+
/**
|
|
253
|
+
* #1979 (ICE, D4): the names of the receivers this add-on registers with
|
|
254
|
+
* `host.registerReceiver` — its own manager's door for what shims produce,
|
|
255
|
+
* e.g. `["health-records"]`. The host refuses a receiver not named here.
|
|
256
|
+
*/
|
|
257
|
+
receivers?: string[];
|
|
241
258
|
}
|
|
242
259
|
/**
|
|
243
260
|
* One add-on as the listings show it (#1965), from `AddonsManager.listedAddons()`.
|
|
@@ -660,7 +677,7 @@ declare class AddonsManager extends BaseManager {
|
|
|
660
677
|
* Result order follows registration order of the addons map so the
|
|
661
678
|
* displayed section order is stable across renders.
|
|
662
679
|
*/
|
|
663
|
-
getProfileSections(user: AddonProfileUser): Promise<AddonProfileSectionEntry[]>;
|
|
680
|
+
getProfileSections(user: AddonProfileUser, ctx: ActorContext): Promise<AddonProfileSectionEntry[]>;
|
|
664
681
|
/**
|
|
665
682
|
* #1234: say at load when an addon still implements the pre-#1234 hook.
|
|
666
683
|
*
|
|
@@ -676,7 +693,7 @@ declare class AddonsManager extends BaseManager {
|
|
|
676
693
|
* `saveProfileSection()` handler (#534).
|
|
677
694
|
*
|
|
678
695
|
* Runs SEQUENTIALLY — each addon's read-modify-write of user preferences
|
|
679
|
-
* (
|
|
696
|
+
* (`updateUser`'s function form, #2025) must see the
|
|
680
697
|
* previous addon's writes. Parallel execution would create a same-snapshot
|
|
681
698
|
* race where two addons read identical preferences, each merge their own
|
|
682
699
|
* keys, and the second writer clobbers the first writer's changes.
|
|
@@ -715,8 +732,11 @@ declare class AddonsManager extends BaseManager {
|
|
|
715
732
|
* documents: they guard the per-person switch, so only such an add-on has any.
|
|
716
733
|
*/
|
|
717
734
|
isPerPerson(slug: string): boolean;
|
|
718
|
-
/**
|
|
719
|
-
|
|
735
|
+
/**
|
|
736
|
+
* #1963: the loaded add-ons whose manifest says `perPerson: true`, in load
|
|
737
|
+
* order — every slug an `addon:<slug>` page audience can match (#1964).
|
|
738
|
+
*/
|
|
739
|
+
perPersonAddons(): string[];
|
|
720
740
|
/**
|
|
721
741
|
* The per-person add-ons this subject has turned on (#1963): each loaded
|
|
722
742
|
* add-on whose manifest says `perPerson: true` and whose switch, the
|
|
@@ -67,10 +67,11 @@ declare class AssetManager extends BaseManager {
|
|
|
67
67
|
/**
|
|
68
68
|
* Return all AssetRecords referenced by the given page slug.
|
|
69
69
|
*
|
|
70
|
-
* O(1) index lookup + one
|
|
70
|
+
* O(1) index lookup + one provider lookup per referenced asset.
|
|
71
71
|
* Stale entries (asset deleted or provider removed) are silently filtered out.
|
|
72
|
+
* Nothing for a caller who may not read assets (`asset-read`, #2018).
|
|
72
73
|
*/
|
|
73
|
-
getAssetsForPage(slug: string): Promise<AssetRecord[]>;
|
|
74
|
+
getAssetsForPage(ctx: ActorContext, slug: string): Promise<AssetRecord[]>;
|
|
74
75
|
/**
|
|
75
76
|
* Remove a page's entry from the reverse index.
|
|
76
77
|
* Call this when a page is deleted or renamed.
|
|
@@ -103,8 +104,11 @@ declare class AssetManager extends BaseManager {
|
|
|
103
104
|
/**
|
|
104
105
|
* Retrieve a single asset by id. If providerId is given only that provider
|
|
105
106
|
* is checked; otherwise all providers are tried in registration order.
|
|
107
|
+
* Null for a caller who may not read assets (`asset-read`, asked of the PDP
|
|
108
|
+
* with `ctx`, as FileManager asks for the public place; #2018).
|
|
106
109
|
*/
|
|
107
|
-
getById(id: string, providerId?: string): Promise<AssetRecord | null>;
|
|
110
|
+
getById(ctx: ActorContext, id: string, providerId?: string): Promise<AssetRecord | null>;
|
|
111
|
+
private lookup;
|
|
108
112
|
/**
|
|
109
113
|
* Generate or retrieve a cached thumbnail.
|
|
110
114
|
* Requires the provider to declare the 'thumbnail' capability.
|
|
@@ -20,17 +20,34 @@
|
|
|
20
20
|
* running count, not one per email. `send()` still rejects, so a caller with
|
|
21
21
|
* a person waiting can tell them.
|
|
22
22
|
*
|
|
23
|
+
* `sendTo(ctx, …, { attachments })` attaches files, a PDF from
|
|
24
|
+
* `ExportManager.htmlToPdf` for one (#1974), up to
|
|
25
|
+
* `ngdpbase.mail.attachments.max-bytes` in total. A log line names how many
|
|
26
|
+
* files and bytes, never their names or content.
|
|
27
|
+
*
|
|
23
28
|
* @see docs/admin/email-setup.md for operator setup instructions
|
|
24
29
|
* @see {@link https://github.com/jwilleke/ngdpbase/issues/456}
|
|
25
30
|
*/
|
|
26
31
|
import BaseManager from './BaseManager.js';
|
|
27
32
|
import type { BackupData } from './BaseManager.js';
|
|
28
33
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
29
|
-
import type { MailMessage, MailProvider } from '../mail/MailProvider.js';
|
|
34
|
+
import type { MailAttachment, MailMessage, MailProvider } from '../mail/MailProvider.js';
|
|
35
|
+
import type { ActorContext } from '../context/ActorContext.js';
|
|
36
|
+
export type { MailAttachment } from '../mail/MailProvider.js';
|
|
30
37
|
/** The degraded reason while sends fail; cleared by the next success. */
|
|
31
38
|
export declare const SEND_FAILING_REASON = "outbound email is failing \u2014 see the server log and the administrator notification";
|
|
32
39
|
/** Title of the administrator notice. */
|
|
33
40
|
export declare const SEND_FAILING_TITLE = "Email could not be sent";
|
|
41
|
+
/** An attachment the message cannot carry (#1974): refused before sending, so it is not a delivery failure. */
|
|
42
|
+
export declare class AttachmentRefusedError extends Error {
|
|
43
|
+
readonly code = "ATTACHMENT_REFUSED";
|
|
44
|
+
constructor(message: string);
|
|
45
|
+
}
|
|
46
|
+
/** What a caller may add after the body (#1974). */
|
|
47
|
+
export interface SendOptions {
|
|
48
|
+
/** Files to attach, e.g. a PDF from `ExportManager.htmlToPdf`. */
|
|
49
|
+
attachments?: MailAttachment[];
|
|
50
|
+
}
|
|
34
51
|
declare class EmailManager extends BaseManager implements MailProvider {
|
|
35
52
|
private provider;
|
|
36
53
|
private providerName;
|
|
@@ -39,6 +56,7 @@ declare class EmailManager extends BaseManager implements MailProvider {
|
|
|
39
56
|
private noticeWindowMs;
|
|
40
57
|
private notice;
|
|
41
58
|
private failuresSinceSuccess;
|
|
59
|
+
private attachmentMaxBytes;
|
|
42
60
|
constructor(engine: WikiEngine);
|
|
43
61
|
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
44
62
|
/**
|
|
@@ -49,16 +67,25 @@ declare class EmailManager extends BaseManager implements MailProvider {
|
|
|
49
67
|
*/
|
|
50
68
|
send(message: MailMessage): Promise<void>;
|
|
51
69
|
/**
|
|
52
|
-
*
|
|
70
|
+
* Build and send a message in one call.
|
|
71
|
+
*
|
|
72
|
+
* @param ctx - Who is sending (security-posture P1); named in the log line when the message carries attachments
|
|
73
|
+
* @param to - Recipient address
|
|
74
|
+
* @param subject - Subject line
|
|
75
|
+
* @param text - Plain-text body
|
|
76
|
+
* @param html - Optional HTML body
|
|
77
|
+
* @param options - Attachments (#1974); their total is capped by `ngdpbase.mail.attachments.max-bytes`
|
|
53
78
|
*/
|
|
54
|
-
sendTo(to: string, subject: string, text: string, html?: string): Promise<void>;
|
|
79
|
+
sendTo(ctx: ActorContext, to: string, subject: string, text: string, html?: string, options?: SendOptions): Promise<void>;
|
|
55
80
|
/**
|
|
56
81
|
* Send without waiting, for a sender nobody is waiting on (a form's copy, a
|
|
57
82
|
* reservation notice). Never rejects: a failure is already logged and
|
|
58
83
|
* reported to administrators by `send()` (#1916), so there is nothing left
|
|
59
|
-
* for the caller to do with it.
|
|
84
|
+
* for the caller to do with it. An attachment over the limit is logged here.
|
|
60
85
|
*/
|
|
61
|
-
sendInBackground(to: string, subject: string, text: string, html?: string): void;
|
|
86
|
+
sendInBackground(ctx: ActorContext, to: string, subject: string, text: string, html?: string, options?: SendOptions): void;
|
|
87
|
+
/** Refuse attachments a mail relay would choke on, or that are not what they say. */
|
|
88
|
+
private checkAttachments;
|
|
62
89
|
/** Whether sends are failing right now: degraded by a failed send, not yet cleared by a success. */
|
|
63
90
|
isDeliveryFailing(): boolean;
|
|
64
91
|
private reportFailure;
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import type { ActorContext } from '../context/ActorContext.js';
|
|
2
2
|
import BaseManager from './BaseManager.js';
|
|
3
3
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
4
|
+
import { PdfUnavailableError, type PdfJob, type PdfJobResult, type PdfOptions, type ChromiumResolution } from '../export/pdfEngine.js';
|
|
5
|
+
import type { Transport } from '../http/guardedFetch.js';
|
|
6
|
+
export { PdfUnavailableError };
|
|
7
|
+
export type { PdfOptions, PdfMargin, PdfPageSize } from '../export/pdfEngine.js';
|
|
8
|
+
/**
|
|
9
|
+
* The needs-attention reason while PDF export has no engine (#1974). Set only
|
|
10
|
+
* when something asked for a PDF and could not have one; cleared by the next
|
|
11
|
+
* render that works.
|
|
12
|
+
*/
|
|
13
|
+
export declare const PDF_UNAVAILABLE_REASON = "PDF export unavailable: choose a PDF provider (local-chrome or chrome-service) in ngdpbase.export.pdf.provider, or fix the one chosen (the server log says why)";
|
|
4
14
|
/**
|
|
5
15
|
* Export file metadata interface
|
|
6
16
|
*/
|
|
@@ -41,6 +51,14 @@ export interface PageForExport {
|
|
|
41
51
|
}
|
|
42
52
|
declare class ExportManager extends BaseManager {
|
|
43
53
|
private exportDirectory;
|
|
54
|
+
/** What the providers call out to; replaced in tests. A site never sets these. */
|
|
55
|
+
protected pdfRenderer: (job: PdfJob) => Promise<PdfJobResult>;
|
|
56
|
+
protected chromiumResolver: (configuredPath: string) => Promise<ChromiumResolution>;
|
|
57
|
+
protected pdfServiceTransport: Transport | undefined;
|
|
58
|
+
protected pdfNormalizer: (pdf: Uint8Array, timeoutMs: number) => Promise<Uint8Array>;
|
|
59
|
+
/** Renders running now, and renders waiting for a slot (`ngdpbase.export.pdf.max-concurrent`). */
|
|
60
|
+
private pdfActive;
|
|
61
|
+
private pdfWaiting;
|
|
44
62
|
/**
|
|
45
63
|
* Creates a new ExportManager instance
|
|
46
64
|
*
|
|
@@ -70,6 +88,50 @@ declare class ExportManager extends BaseManager {
|
|
|
70
88
|
* @returns The CSV document
|
|
71
89
|
*/
|
|
72
90
|
toCsv(headers: readonly string[], rows: readonly (readonly unknown[])[]): string;
|
|
91
|
+
/**
|
|
92
|
+
* A rendered HTML document (print CSS and all) as PDF bytes (#1974).
|
|
93
|
+
*
|
|
94
|
+
* - __A provider renders it:__ `ngdpbase.export.pdf.provider` — `none`
|
|
95
|
+
* (default), `local-chrome` (a Chromium on this machine) or
|
|
96
|
+
* `chrome-service` (a Chromium service by URL, e.g. Gotenberg); see
|
|
97
|
+
* `src/export/PdfProvider.ts`. Read on every render, so a change applies
|
|
98
|
+
* without a restart.
|
|
99
|
+
* - __Sandboxed:__ `local-chrome` runs Chromium with JavaScript off and every
|
|
100
|
+
* request refused except `data:` URIs, so images, stylesheets and fonts
|
|
101
|
+
* must be inlined. A `chrome-service` is reached through the egress policy,
|
|
102
|
+
* and what its Chromium may load is the service's own setting.
|
|
103
|
+
* - __Deterministic:__ the same HTML on the same core version, provider and
|
|
104
|
+
* engine version gives the same bytes (no dates, no random ID), so an
|
|
105
|
+
* add-on can store the document's SHA-256.
|
|
106
|
+
* - __Off the request path:__ a local render, and the metadata pass on a
|
|
107
|
+
* service's PDF, each run in a child process; everything is bounded by
|
|
108
|
+
* `ngdpbase.export.pdf.timeout-ms`, and at most
|
|
109
|
+
* `ngdpbase.export.pdf.max-concurrent` run at once while the rest wait.
|
|
110
|
+
* - __Optional:__ with no engine (provider `none`, or the chosen one cannot
|
|
111
|
+
* be used) it throws `PdfUnavailableError` (`code: 'PDF_UNAVAILABLE'`),
|
|
112
|
+
* logs why, and this manager shows "needs attention" on the admin
|
|
113
|
+
* dashboard until a render works. A site that never asks for a PDF is
|
|
114
|
+
* never flagged.
|
|
115
|
+
*
|
|
116
|
+
* Records no audit event: it reads nothing and changes nothing. The caller
|
|
117
|
+
* that issues the document records that, with its own event.
|
|
118
|
+
*
|
|
119
|
+
* @param ctx - Who is asking; named in the log line
|
|
120
|
+
* @param html - A complete HTML document
|
|
121
|
+
* @param options - Page size, margins, printBackground; an `@page` rule in the HTML wins
|
|
122
|
+
* @returns The PDF
|
|
123
|
+
*/
|
|
124
|
+
htmlToPdf(ctx: ActorContext, html: string, options?: PdfOptions): Promise<Buffer>;
|
|
125
|
+
/**
|
|
126
|
+
* The provider `ngdpbase.export.pdf.provider` names, built for this render
|
|
127
|
+
* from the current settings. A name that is not a provider is unavailable,
|
|
128
|
+
* with the reason, as `none` is.
|
|
129
|
+
*/
|
|
130
|
+
private pdfProvider;
|
|
131
|
+
/** Log why, flag the manager (unless something worse is already shown), and refuse. */
|
|
132
|
+
private pdfUnavailable;
|
|
133
|
+
private acquirePdfSlot;
|
|
134
|
+
private releasePdfSlot;
|
|
73
135
|
/**
|
|
74
136
|
* Initialize the ExportManager
|
|
75
137
|
*
|