@jwilleke/ngdpbase-types 5.26.0 → 5.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/packages/ngdpbase-types/index.d.ts +5 -2
- package/lib/packages/ngdpbase-types/testing.d.ts +7 -3
- package/lib/packages/ngdpbase-types/testing.js +7 -3
- package/lib/src/converters/DocxConverter.d.ts +30 -0
- package/lib/src/converters/MarkdownConverter.d.ts +31 -0
- package/lib/src/export/ChromeServicePdfProvider.d.ts +52 -0
- package/lib/src/export/ChromeServicePdfProvider.js +153 -0
- package/lib/src/export/LocalChromePdfProvider.d.ts +25 -0
- package/lib/src/export/LocalChromePdfProvider.js +33 -0
- package/lib/src/export/PdfProvider.d.ts +51 -0
- package/lib/src/export/PdfProvider.js +37 -0
- package/lib/src/export/pdfEngine.d.ts +113 -0
- package/lib/src/export/pdfEngine.js +195 -0
- package/lib/src/ice/converterShim.d.ts +5 -0
- package/lib/src/ice/fileHandling.d.ts +30 -0
- package/lib/src/ice/ledger.d.ts +37 -0
- package/lib/src/ice/runImport.d.ts +51 -0
- package/lib/src/ice/types.d.ts +262 -0
- package/lib/src/mail/MailProvider.d.ts +15 -0
- package/lib/src/mail/NodemailerMailProvider.d.ts +1 -1
- package/lib/src/managers/AddonsManager.d.ts +18 -2
- package/lib/src/managers/EmailManager.d.ts +32 -5
- package/lib/src/managers/ExportManager.d.ts +62 -0
- package/lib/src/managers/ExportManager.js +147 -2
- package/lib/src/managers/ImportManager.d.ts +590 -0
- package/lib/src/managers/PageManager.d.ts +45 -1
- package/lib/src/security/PolicyInformationPoint.d.ts +25 -2
- package/lib/src/types/Config.d.ts +2 -0
- package/lib/src/utils/addonHost.d.ts +23 -2
- package/lib/src/utils/addonHost.js +25 -2
- package/lib/src/utils/frontmatterAccess.d.ts +54 -11
- package/lib/src/utils/importRunSummary.d.ts +64 -0
- package/lib/src/utils/ncmNotify.d.ts +30 -0
- package/lib/src/utils/privateStoreImport.d.ts +58 -0
- package/lib/src/utils/zipArchive.d.ts +71 -0
- package/package.json +1 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTML → PDF engine (#1974): the options and the error every provider
|
|
3
|
+
* shares, and the child process the `local-chrome` provider renders in
|
|
4
|
+
* (headless Chromium, driven by `playwright-core`, both optional at run time).
|
|
5
|
+
*
|
|
6
|
+
* - `resolveChromium` finds a Chromium to run: the configured path, then the
|
|
7
|
+
* one Playwright installed (`npx playwright install chromium`), then the
|
|
8
|
+
* usual system locations. Nothing is downloaded at `npm install`.
|
|
9
|
+
* - `renderPdfInChild` renders one document in a child process
|
|
10
|
+
* (`pdfRenderWorker.ts`), killed at the timeout, so the site never waits on
|
|
11
|
+
* Chromium or on PDF parsing.
|
|
12
|
+
* - `normalizePdfInChild` gives a PDF a PDF service made (the
|
|
13
|
+
* `chrome-service` provider) the same metadata pass, in the same kind of
|
|
14
|
+
* child, so parsing it never runs on the thread that answers requests.
|
|
15
|
+
*
|
|
16
|
+
* `ExportManager.htmlToPdf` is the door, through its providers
|
|
17
|
+
* (`PdfProvider.ts`); nothing else calls these.
|
|
18
|
+
*/
|
|
19
|
+
import { fork } from 'node:child_process';
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
/** Page sizes Chromium knows by name. */
|
|
23
|
+
export const PDF_PAGE_SIZES = ['Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6'];
|
|
24
|
+
/** A CSS length Chromium's PDF margins accept: a number and px, in, cm or mm. */
|
|
25
|
+
const MARGIN = /^\d+(\.\d+)?(px|in|cm|mm)$/;
|
|
26
|
+
/**
|
|
27
|
+
* No PDF engine: `playwright-core` is not installed, no Chromium was found, or
|
|
28
|
+
* it could not start. An add-on catches it by `code` (it cannot import core
|
|
29
|
+
* values) and falls back to HTML.
|
|
30
|
+
*/
|
|
31
|
+
export class PdfUnavailableError extends Error {
|
|
32
|
+
code = 'PDF_UNAVAILABLE';
|
|
33
|
+
constructor(message, options) {
|
|
34
|
+
super(message, options);
|
|
35
|
+
this.name = 'PdfUnavailableError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validate a caller's options and fill the defaults. Throws on anything
|
|
40
|
+
* Chromium would misread, so a typo is an error rather than a different page.
|
|
41
|
+
*
|
|
42
|
+
* @param options - The caller's options
|
|
43
|
+
* @param defaultFormat - `ngdpbase.export.pdf.page-size`
|
|
44
|
+
*/
|
|
45
|
+
export function resolvePdfOptions(options, defaultFormat) {
|
|
46
|
+
const opts = options ?? {};
|
|
47
|
+
if (typeof opts !== 'object')
|
|
48
|
+
throw new TypeError('htmlToPdf: options must be an object');
|
|
49
|
+
const format = opts.format ?? defaultFormat;
|
|
50
|
+
if (!PDF_PAGE_SIZES.includes(format)) {
|
|
51
|
+
throw new TypeError(`htmlToPdf: format must be one of ${PDF_PAGE_SIZES.join(', ')} (got ${JSON.stringify(format)})`);
|
|
52
|
+
}
|
|
53
|
+
const margin = {};
|
|
54
|
+
for (const side of ['top', 'right', 'bottom', 'left']) {
|
|
55
|
+
const value = opts.margin?.[side];
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
if (typeof value !== 'string' || !MARGIN.test(value)) {
|
|
59
|
+
throw new TypeError(`htmlToPdf: margin.${side} must be a length such as '20mm' or '0.5in' (got ${JSON.stringify(value)})`);
|
|
60
|
+
}
|
|
61
|
+
margin[side] = value;
|
|
62
|
+
}
|
|
63
|
+
const printBackground = opts.printBackground ?? true;
|
|
64
|
+
if (typeof printBackground !== 'boolean')
|
|
65
|
+
throw new TypeError('htmlToPdf: printBackground must be a boolean');
|
|
66
|
+
return { format: format, margin, printBackground };
|
|
67
|
+
}
|
|
68
|
+
/** Where Chromium or Chrome usually is, per platform. Checked after the configured path and Playwright's own. */
|
|
69
|
+
export const SYSTEM_CHROMIUM_PATHS = {
|
|
70
|
+
linux: ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/lib/chromium/chromium', '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/snap/bin/chromium'],
|
|
71
|
+
darwin: ['/Applications/Chromium.app/Contents/MacOS/Chromium', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'],
|
|
72
|
+
win32: ['C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe']
|
|
73
|
+
};
|
|
74
|
+
const requireHere = createRequire(import.meta.url);
|
|
75
|
+
/** Whether `playwright-core` is installed (it is an optional dependency). */
|
|
76
|
+
export function playwrightInstalled() {
|
|
77
|
+
try {
|
|
78
|
+
requireHere.resolve('playwright-core');
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function playwrightChromiumPath() {
|
|
86
|
+
try {
|
|
87
|
+
const { chromium } = await import('playwright-core');
|
|
88
|
+
return chromium.executablePath() || null;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Find a Chromium to render with. Read on every call, so installing Chromium
|
|
96
|
+
* or setting the path takes effect without a restart.
|
|
97
|
+
*
|
|
98
|
+
* @param configuredPath - `ngdpbase.export.pdf.provider.local-chrome.chromium-path` ('' = find one)
|
|
99
|
+
*/
|
|
100
|
+
export async function resolveChromium(configuredPath, deps = {}) {
|
|
101
|
+
const exists = deps.exists ?? existsSync;
|
|
102
|
+
if (!(deps.playwrightInstalled ?? playwrightInstalled)()) {
|
|
103
|
+
return { ok: false, reason: 'playwright-core is not installed (it is an optional dependency; npm install without --omit=optional)' };
|
|
104
|
+
}
|
|
105
|
+
if (configuredPath) {
|
|
106
|
+
return exists(configuredPath)
|
|
107
|
+
? { ok: true, executablePath: configuredPath, source: 'configured' }
|
|
108
|
+
: { ok: false, reason: `ngdpbase.export.pdf.provider.local-chrome.chromium-path is set, but nothing is there: ${configuredPath}` };
|
|
109
|
+
}
|
|
110
|
+
const bundled = await (deps.playwrightChromium ?? playwrightChromiumPath)();
|
|
111
|
+
if (bundled && exists(bundled))
|
|
112
|
+
return { ok: true, executablePath: bundled, source: 'playwright' };
|
|
113
|
+
for (const candidate of SYSTEM_CHROMIUM_PATHS[deps.platform ?? process.platform] ?? []) {
|
|
114
|
+
if (exists(candidate))
|
|
115
|
+
return { ok: true, executablePath: candidate, source: 'system' };
|
|
116
|
+
}
|
|
117
|
+
return { ok: false, reason: 'no Chromium found: install Chromium, or set ngdpbase.export.pdf.provider.local-chrome.chromium-path' };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The environment the child gets: enough to find a home, a temp folder and
|
|
121
|
+
* fonts, and none of the site's secrets (the session secret, keys and
|
|
122
|
+
* passwords the site holds in its environment).
|
|
123
|
+
*/
|
|
124
|
+
const CHILD_ENV_KEYS = [
|
|
125
|
+
'PATH', 'HOME', 'TMPDIR', 'TMP', 'TEMP', 'LANG', 'LC_ALL', 'PLAYWRIGHT_BROWSERS_PATH',
|
|
126
|
+
'FONTCONFIG_PATH', 'FONTCONFIG_FILE', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_RUNTIME_DIR',
|
|
127
|
+
'SystemRoot', 'LOCALAPPDATA', 'USERPROFILE', 'APPDATA', 'ProgramFiles', 'ProgramFiles(x86)'
|
|
128
|
+
];
|
|
129
|
+
export function childEnv(env = process.env) {
|
|
130
|
+
const out = {};
|
|
131
|
+
for (const key of CHILD_ENV_KEYS)
|
|
132
|
+
if (env[key] !== undefined)
|
|
133
|
+
out[key] = env[key];
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Render one job in a child process. Resolves with the child's answer; rejects
|
|
138
|
+
* only when the child timed out, crashed or never answered.
|
|
139
|
+
*
|
|
140
|
+
* @param job - The render
|
|
141
|
+
*/
|
|
142
|
+
export function renderPdfInChild(job) {
|
|
143
|
+
return runPdfChild(job);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Normalise a PDF in a child process (`normalizePdf`, the same pass a local
|
|
147
|
+
* render gets). Rejects when the bytes are not a PDF pdf-lib can read, or the
|
|
148
|
+
* child times out.
|
|
149
|
+
*
|
|
150
|
+
* @param pdf - A PDF a PDF service returned
|
|
151
|
+
* @param timeoutMs - The child is killed after this
|
|
152
|
+
*/
|
|
153
|
+
export async function normalizePdfInChild(pdf, timeoutMs) {
|
|
154
|
+
const result = await runPdfChild({ kind: 'normalize', pdf, timeoutMs });
|
|
155
|
+
if (!result.ok)
|
|
156
|
+
throw new Error(`the PDF could not be read: ${result.error}`);
|
|
157
|
+
return result.pdf;
|
|
158
|
+
}
|
|
159
|
+
function runPdfChild(job) {
|
|
160
|
+
// Compiled, the worker is the .js beside this file. From source (vitest,
|
|
161
|
+
// tsx) it is the .ts, which needs tsx's loader.
|
|
162
|
+
const fromSource = import.meta.url.endsWith('.ts');
|
|
163
|
+
const entry = new URL(fromSource ? './pdfRenderWorker.ts' : './pdfRenderWorker.js', import.meta.url);
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const child = fork(entry, [], {
|
|
166
|
+
execArgv: fromSource ? ['--import', 'tsx'] : [],
|
|
167
|
+
env: childEnv(),
|
|
168
|
+
serialization: 'advanced',
|
|
169
|
+
stdio: ['ignore', 'ignore', 'pipe', 'ipc']
|
|
170
|
+
});
|
|
171
|
+
let settled = false;
|
|
172
|
+
let stderr = '';
|
|
173
|
+
child.stderr?.on('data', (chunk) => { if (stderr.length < 2000)
|
|
174
|
+
stderr += chunk.toString(); });
|
|
175
|
+
const settle = (fn) => {
|
|
176
|
+
if (settled)
|
|
177
|
+
return;
|
|
178
|
+
settled = true;
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
fn();
|
|
181
|
+
};
|
|
182
|
+
const timer = setTimeout(() => {
|
|
183
|
+
settle(() => reject(new Error(`PDF render timed out after ${job.timeoutMs} ms`)));
|
|
184
|
+
child.kill('SIGKILL');
|
|
185
|
+
}, job.timeoutMs);
|
|
186
|
+
child.once('message', (result) => {
|
|
187
|
+
settle(() => resolve(result.ok ? { ok: true, pdf: Buffer.from(result.pdf) } : result));
|
|
188
|
+
});
|
|
189
|
+
child.once('error', (err) => settle(() => reject(err)));
|
|
190
|
+
child.once('exit', (code, signal) => {
|
|
191
|
+
settle(() => reject(new Error(`PDF render process exited (${signal ?? code}) without an answer${stderr ? `: ${stderr.trim().split('\n')[0]}` : ''}`)));
|
|
192
|
+
});
|
|
193
|
+
child.send(job);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IContentConverter } from '../converters/IContentConverter.js';
|
|
2
|
+
import type { IceShim } from './types.js';
|
|
3
|
+
/** The receiver today's converters hand to. */
|
|
4
|
+
export declare const PAGES_RECEIVER = "core.pages";
|
|
5
|
+
export declare function converterShim(converter: IContentConverter, tableClasses: () => string[]): IceShim;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ZipReadLimits } from '../utils/zipArchive.js';
|
|
2
|
+
import type { IceUpload } from './types.js';
|
|
3
|
+
/** A file's fingerprint: SHA-256 of its bytes, hex. */
|
|
4
|
+
export declare function fingerprint(bytes: Buffer): string;
|
|
5
|
+
/**
|
|
6
|
+
* Is this path noise — something an operating system or archiver adds that is
|
|
7
|
+
* nobody's data? Patterns (configuration, `ngdpbase.import.engine.noise`):
|
|
8
|
+
*
|
|
9
|
+
* - `name/` — any path with a directory segment of that name (`__MACOSX/`);
|
|
10
|
+
* - `prefix*` / `*suffix` — a file name starting or ending so (`._*`);
|
|
11
|
+
* - anything else — a file name exactly (`.DS_Store`), case-insensitive.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isNoise(filePath: string, patterns: readonly string[]): boolean;
|
|
14
|
+
/** Is this upload a zip archive the engine unpacks? By its name, or its media type. */
|
|
15
|
+
export declare function isZip(upload: IceUpload): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Unpack the zips among the uploads into the files they hold, named
|
|
18
|
+
* `archive.zip/inner/path`. A nested zip is unpacked too. Everything else is
|
|
19
|
+
* passed through. Limits are per archive (`ZipReadLimits`), so a small
|
|
20
|
+
* archive that expands enormously stops at the budget. An archive that cannot
|
|
21
|
+
* be read is returned in `unreadable`, with why; the rest go on.
|
|
22
|
+
*/
|
|
23
|
+
export declare function expandUploads(uploads: readonly IceUpload[], limits: ZipReadLimits, maxDepth: number, depth?: number): {
|
|
24
|
+
files: IceUpload[];
|
|
25
|
+
unreadable: Array<{
|
|
26
|
+
name: string;
|
|
27
|
+
size: number;
|
|
28
|
+
message: string;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { IceLedger, LedgerFileEntry } from './types.js';
|
|
2
|
+
interface ScopeFile {
|
|
3
|
+
scope: string;
|
|
4
|
+
files: LedgerFileEntry[];
|
|
5
|
+
items: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Keeps the newest entry per fingerprint, at most `maxEntries` files and item keys per scope. */
|
|
8
|
+
declare abstract class ScopedLedger implements IceLedger {
|
|
9
|
+
private readonly maxEntries;
|
|
10
|
+
constructor(maxEntries: number);
|
|
11
|
+
protected abstract load(scope: string): Promise<ScopeFile>;
|
|
12
|
+
protected abstract save(data: ScopeFile): Promise<void>;
|
|
13
|
+
/** One writer per scope at a time, so two runs in one scope cannot lose each other's entries. */
|
|
14
|
+
private readonly chains;
|
|
15
|
+
private serial;
|
|
16
|
+
lastFile(scope: string, sha256: string): Promise<LedgerFileEntry | null>;
|
|
17
|
+
recordFile(scope: string, entry: LedgerFileEntry): Promise<void>;
|
|
18
|
+
hasItem(scope: string, itemKey: string): Promise<boolean>;
|
|
19
|
+
recordItem(scope: string, itemKey: string): Promise<void>;
|
|
20
|
+
files(scope: string): Promise<LedgerFileEntry[]>;
|
|
21
|
+
}
|
|
22
|
+
/** The ledger in memory: for tests, and for a site that keeps no import history. */
|
|
23
|
+
export declare class MemoryIceLedger extends ScopedLedger {
|
|
24
|
+
private readonly scopes;
|
|
25
|
+
constructor(maxEntries?: number);
|
|
26
|
+
protected load(scope: string): Promise<ScopeFile>;
|
|
27
|
+
protected save(data: ScopeFile): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/** One JSON file per scope, named by the scope's hash (a scope holds a username). */
|
|
30
|
+
export declare class FileIceLedger extends ScopedLedger {
|
|
31
|
+
private readonly dir;
|
|
32
|
+
constructor(dir: string, maxEntries: number);
|
|
33
|
+
private fileFor;
|
|
34
|
+
protected load(scope: string): Promise<ScopeFile>;
|
|
35
|
+
protected save(data: ScopeFile): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ActorContext } from '../context/ActorContext.js';
|
|
2
|
+
import type { ZipReadLimits } from '../utils/zipArchive.js';
|
|
3
|
+
import type { IceItem, IceLedger, IceRunReport, IceRunRequest, OriginalChoice, RegisteredReceiver, RegisteredShim, ShimTools } from './types.js';
|
|
4
|
+
/** Configuration the engine reads (each a key under `ngdpbase.import.engine.`). */
|
|
5
|
+
export interface IceEngineSettings {
|
|
6
|
+
noise: string[];
|
|
7
|
+
zip: ZipReadLimits;
|
|
8
|
+
maxZipDepth: number;
|
|
9
|
+
maxFileBytes: number;
|
|
10
|
+
maxChainDepth: number;
|
|
11
|
+
defaultOriginal: OriginalChoice;
|
|
12
|
+
}
|
|
13
|
+
export interface IceRunDeps {
|
|
14
|
+
/** Enabled shims, in registration order. */
|
|
15
|
+
shims: RegisteredShim[];
|
|
16
|
+
receivers: ReadonlyMap<string, RegisteredReceiver>;
|
|
17
|
+
ledger: IceLedger;
|
|
18
|
+
settings: IceEngineSettings;
|
|
19
|
+
tools(shim: RegisteredShim): ShimTools;
|
|
20
|
+
now?: () => Date;
|
|
21
|
+
}
|
|
22
|
+
/** The id of the receiver that keeps an original when the chosen receiver does not keep its own (D11). */
|
|
23
|
+
export declare const FILES_RECEIVER = "core.files";
|
|
24
|
+
/** The scope a run's fingerprints are checked in, unless the caller names one. */
|
|
25
|
+
export declare function defaultScope(receiverId: string, ctx: ActorContext): string;
|
|
26
|
+
export interface ChainLeaf {
|
|
27
|
+
item: IceItem;
|
|
28
|
+
chain: string[];
|
|
29
|
+
}
|
|
30
|
+
/** What a chain needs: the shims it may continue through, and where it stops. */
|
|
31
|
+
export interface ChainSpec {
|
|
32
|
+
shims: RegisteredShim[];
|
|
33
|
+
tools(shim: RegisteredShim): ShimTools;
|
|
34
|
+
maxChainDepth: number;
|
|
35
|
+
/** Does this item, from this shim, stop here? */
|
|
36
|
+
done(shim: RegisteredShim, item: IceItem): boolean;
|
|
37
|
+
/** Named in the error when nothing takes an item further. */
|
|
38
|
+
target: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Run a shim, then the shims after it (D9), until each item is done. What
|
|
42
|
+
* came before travels with each item: earlier metadata under later, earlier
|
|
43
|
+
* warnings first. Partial notes are collected into `partial`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function convertChain(shim: RegisteredShim, input: IceItem, chain: string[], spec: ChainSpec, partial: string[]): Promise<ChainLeaf[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Run one import. Fills and returns `report`, which the caller may be
|
|
48
|
+
* showing while it runs. Never throws for one file's failure; that file's
|
|
49
|
+
* status says why.
|
|
50
|
+
*/
|
|
51
|
+
export declare function runImport(deps: IceRunDeps, request: IceRunRequest, ctx: ActorContext, report: IceRunReport): Promise<IceRunReport>;
|
|
@@ -0,0 +1,262 @@
|
|
|
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
|
+
/** A citation to the origin (D11): the receiver renders it, as a footnote on a page. */
|
|
125
|
+
export interface IceCitation {
|
|
126
|
+
name: string;
|
|
127
|
+
sha256: string;
|
|
128
|
+
importedAt: string;
|
|
129
|
+
}
|
|
130
|
+
/** Where a kept original went (D11). */
|
|
131
|
+
export interface KeptOriginal {
|
|
132
|
+
name: string;
|
|
133
|
+
sha256: string;
|
|
134
|
+
/** The keeping door's own reference: an attachment id, a record id. */
|
|
135
|
+
ref: string;
|
|
136
|
+
}
|
|
137
|
+
/** What a receiver is told about the item it is handed. */
|
|
138
|
+
export interface ReceiveInfo {
|
|
139
|
+
runId: string;
|
|
140
|
+
/** The file the item came from. */
|
|
141
|
+
source: {
|
|
142
|
+
name: string;
|
|
143
|
+
sha256: string;
|
|
144
|
+
};
|
|
145
|
+
/** The shims the item passed through, in order. */
|
|
146
|
+
chain: string[];
|
|
147
|
+
citation?: IceCitation;
|
|
148
|
+
original?: KeptOriginal;
|
|
149
|
+
}
|
|
150
|
+
/** A receiver's answer for one item. `refused` is its existing permission saying no (D5). */
|
|
151
|
+
export interface ReceiveOutcome {
|
|
152
|
+
status: 'stored' | 'duplicate' | 'refused';
|
|
153
|
+
/** Where it went: a page name, an attachment id, a record id. */
|
|
154
|
+
ref?: string;
|
|
155
|
+
message?: string;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* A receiver as its owner writes it (D4). Its doors are the receiver's own
|
|
159
|
+
* existing ones; each checks its own permission with the context given (D5).
|
|
160
|
+
*/
|
|
161
|
+
export interface IceReceiver {
|
|
162
|
+
name: string;
|
|
163
|
+
label?: string;
|
|
164
|
+
/** The kinds it takes, e.g. `['ncm']`, `['file']`, `['phr.health-record']`. */
|
|
165
|
+
accepts: IceKind[];
|
|
166
|
+
/** May this context use this receiver? Its existing permission — the import screen shows only these (D5). */
|
|
167
|
+
mayUse(ctx: ActorContext): Promise<boolean>;
|
|
168
|
+
/** Hand one item to the receiver's own door. */
|
|
169
|
+
receive(item: IceItem, ctx: ActorContext, info: ReceiveInfo): Promise<ReceiveOutcome>;
|
|
170
|
+
/**
|
|
171
|
+
* Keep the original file itself (D11: phr keeps originals in its own
|
|
172
|
+
* store). Without this, `keep` hands the original to the files receiver.
|
|
173
|
+
*/
|
|
174
|
+
keepOriginal?(original: IceItem, ctx: ActorContext): Promise<ReceiveOutcome>;
|
|
175
|
+
}
|
|
176
|
+
export interface RegisteredReceiver extends IceReceiver {
|
|
177
|
+
id: string;
|
|
178
|
+
owner: string;
|
|
179
|
+
}
|
|
180
|
+
/** One file handed to the engine. A `.zip` is unpacked by the engine. */
|
|
181
|
+
export interface IceUpload {
|
|
182
|
+
name: string;
|
|
183
|
+
bytes: Buffer;
|
|
184
|
+
mediaType?: string;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* A source (D1). The first slice reads uploads; a URL fetched on a schedule
|
|
188
|
+
* (D10, feeds) and a server folder join this union later.
|
|
189
|
+
*/
|
|
190
|
+
export type IceSource = {
|
|
191
|
+
type: 'upload';
|
|
192
|
+
files: IceUpload[];
|
|
193
|
+
};
|
|
194
|
+
/** One import, as a caller asks for it. */
|
|
195
|
+
export interface IceRunRequest {
|
|
196
|
+
source: IceSource;
|
|
197
|
+
/** The receiver id the results go to: `core.pages`, `phr.health-records`. */
|
|
198
|
+
receiver: string;
|
|
199
|
+
/** Pin the first shim (a format the person chose) instead of detecting one. */
|
|
200
|
+
shim?: string;
|
|
201
|
+
/** D11. Default `ngdpbase.import.engine.default-original`. */
|
|
202
|
+
original?: OriginalChoice;
|
|
203
|
+
/**
|
|
204
|
+
* Whose fingerprints and item keys this run is checked against. Default
|
|
205
|
+
* `<receiver>/<username>`: the same file imported again by the same person
|
|
206
|
+
* into the same receiver is skipped. A caller with its own notion of a
|
|
207
|
+
* source (phr's source id) passes it.
|
|
208
|
+
*/
|
|
209
|
+
scope?: string;
|
|
210
|
+
}
|
|
211
|
+
/** Per-file status (first slice). */
|
|
212
|
+
export type IceFileStatus = 'imported' | 'partial' | 'unchanged' | 'duplicate' | 'noise' | 'unreadable' | 'refused' | 'failed';
|
|
213
|
+
export interface IceFileReport {
|
|
214
|
+
name: string;
|
|
215
|
+
sha256?: string;
|
|
216
|
+
size: number;
|
|
217
|
+
status: IceFileStatus;
|
|
218
|
+
message?: string;
|
|
219
|
+
/** The shims the file went through. */
|
|
220
|
+
chain: string[];
|
|
221
|
+
/** Where each item went. */
|
|
222
|
+
refs: string[];
|
|
223
|
+
/** Items stored, and items skipped as already seen. */
|
|
224
|
+
stored: number;
|
|
225
|
+
duplicates: number;
|
|
226
|
+
original?: KeptOriginal;
|
|
227
|
+
warnings: IceWarning[];
|
|
228
|
+
}
|
|
229
|
+
export interface IceRunReport {
|
|
230
|
+
runId: string;
|
|
231
|
+
receiver: string;
|
|
232
|
+
scope: string;
|
|
233
|
+
/** Who it ran as. */
|
|
234
|
+
username: string;
|
|
235
|
+
state: 'queued' | 'running' | 'done' | 'failed';
|
|
236
|
+
startedAt?: string;
|
|
237
|
+
finishedAt?: string;
|
|
238
|
+
error?: string;
|
|
239
|
+
files: IceFileReport[];
|
|
240
|
+
}
|
|
241
|
+
/** What the engine remembers about one file in a scope. */
|
|
242
|
+
export interface LedgerFileEntry {
|
|
243
|
+
name: string;
|
|
244
|
+
sha256: string;
|
|
245
|
+
size: number;
|
|
246
|
+
status: IceFileStatus;
|
|
247
|
+
message?: string;
|
|
248
|
+
at: string;
|
|
249
|
+
refs: string[];
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Where the engine keeps fingerprints, per-file status and item keys, per
|
|
253
|
+
* scope. One interface so a database table can replace the file store when
|
|
254
|
+
* the application database is on.
|
|
255
|
+
*/
|
|
256
|
+
export interface IceLedger {
|
|
257
|
+
lastFile(scope: string, sha256: string): Promise<LedgerFileEntry | null>;
|
|
258
|
+
recordFile(scope: string, entry: LedgerFileEntry): Promise<void>;
|
|
259
|
+
hasItem(scope: string, itemKey: string): Promise<boolean>;
|
|
260
|
+
recordItem(scope: string, itemKey: string): Promise<void>;
|
|
261
|
+
files(scope: string): Promise<LedgerFileEntry[]>;
|
|
262
|
+
}
|
|
@@ -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;
|
|
@@ -238,6 +238,19 @@ export interface AddonManifest {
|
|
|
238
238
|
family?: string;
|
|
239
239
|
/** #1965: `false` keeps plumbing (e.g. elasticsearch) out of the add-on listings. */
|
|
240
240
|
listed?: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* #1979 (ICE, D6): the names of the import shims this add-on registers at
|
|
243
|
+
* load with `host.registerShim`, e.g. `["ccda", "fhir"]` (ids `<slug>.ccda`,
|
|
244
|
+
* `<slug>.fhir`). The host refuses a shim not named here. Site configuration
|
|
245
|
+
* switches each one: `ngdpbase.import.shims.<slug>.<name>.enabled`.
|
|
246
|
+
*/
|
|
247
|
+
shims?: string[];
|
|
248
|
+
/**
|
|
249
|
+
* #1979 (ICE, D4): the names of the receivers this add-on registers with
|
|
250
|
+
* `host.registerReceiver` — its own manager's door for what shims produce,
|
|
251
|
+
* e.g. `["health-records"]`. The host refuses a receiver not named here.
|
|
252
|
+
*/
|
|
253
|
+
receivers?: string[];
|
|
241
254
|
}
|
|
242
255
|
/**
|
|
243
256
|
* One add-on as the listings show it (#1965), from `AddonsManager.listedAddons()`.
|
|
@@ -715,8 +728,11 @@ declare class AddonsManager extends BaseManager {
|
|
|
715
728
|
* documents: they guard the per-person switch, so only such an add-on has any.
|
|
716
729
|
*/
|
|
717
730
|
isPerPerson(slug: string): boolean;
|
|
718
|
-
/**
|
|
719
|
-
|
|
731
|
+
/**
|
|
732
|
+
* #1963: the loaded add-ons whose manifest says `perPerson: true`, in load
|
|
733
|
+
* order — every slug an `addon:<slug>` page audience can match (#1964).
|
|
734
|
+
*/
|
|
735
|
+
perPersonAddons(): string[];
|
|
720
736
|
/**
|
|
721
737
|
* The per-person add-ons this subject has turned on (#1963): each loaded
|
|
722
738
|
* add-on whose manifest says `perPerson: true` and whose switch, the
|