@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
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* only, so the published entry carries no JavaScript.
|
|
16
16
|
*/
|
|
17
17
|
export type { AddonModule, AddonManifest, AddonListing } from '../../src/managers/AddonsManager.js';
|
|
18
|
-
export type { AddonHost, PersonSwitchSetter } from '../../src/utils/addonHost.js';
|
|
18
|
+
export type { AddonHost, AddonIceDeclaration, PersonSwitchSetter } from '../../src/utils/addonHost.js';
|
|
19
19
|
export type { WikiEngine } from '../../src/types/WikiEngine.js';
|
|
20
20
|
export type { ApiContext } from '../../src/context/ApiContext.js';
|
|
21
21
|
export type { ActorContext } from '../../src/context/ActorContext.js';
|
|
@@ -28,14 +28,20 @@ export type { JobRunContext } from '../../src/managers/BackgroundJobManager.js';
|
|
|
28
28
|
export type { default as DatabaseManager, AddonDatabaseOptions } from '../../src/managers/DatabaseManager.js';
|
|
29
29
|
export type { Migration } from '../../src/providers/sqliteMigrations.js';
|
|
30
30
|
export type { AddonDatabaseDeclaration, DatabaseEncrypt } from '../../src/utils/addonDatabaseRules.js';
|
|
31
|
-
export type { default as ExportManager } from '../../src/managers/ExportManager.js';
|
|
31
|
+
export type { default as ExportManager, PdfOptions, PdfMargin, PdfPageSize, PdfUnavailableError } from '../../src/managers/ExportManager.js';
|
|
32
|
+
export type { default as EmailManager, SendOptions, MailAttachment, AttachmentRefusedError } from '../../src/managers/EmailManager.js';
|
|
32
33
|
export type { default as PolicyDecisionPoint } from '../../src/security/PolicyDecisionPoint.js';
|
|
33
34
|
export type { default as PageManager } from '../../src/managers/PageManager.js';
|
|
34
35
|
export type { PageAction } from '../../src/utils/pageUrl.js';
|
|
35
36
|
export type { default as ConsentManager, ConsentScopeStatus } from '../../src/managers/ConsentManager.js';
|
|
36
37
|
export type { AddonConsentRegistration, ConsentDocument, ConsentRecord, ConsentRequiredError, ConsentScope, ConsentSeen, ConsentStatus } from '../../src/types/Consent.js';
|
|
38
|
+
export type { default as ImportManager } from '../../src/managers/ImportManager.js';
|
|
39
|
+
export type { IceShim, IceReceiver, IceItem, IceKind, IceWarning, ShimDirection, ShimAccepts, ShimTools, ShimReadResult, ReceiveInfo, ReceiveOutcome, IceCitation, KeptOriginal, OriginalChoice, IceOriginalChoices, IceUpload, IceSource, IceRunRequest, IceRunReport, IceFileReport, IceFileStatus } from '../../src/ice/types.js';
|
|
40
|
+
export type { default as UserManager, UserUpdateInput, UserUpdateFn } from '../../src/managers/UserManager.js';
|
|
41
|
+
export type { User, UserPreferences } from '../../src/types/User.js';
|
|
37
42
|
export type { default as PersonManager, LinkOfferOutcome } from '../../src/managers/PersonManager.js';
|
|
38
43
|
export type { default as OrganizationManager } from '../../src/managers/OrganizationManager.js';
|
|
39
44
|
export type { Person, PersonAffiliation, PersonInput, PersonUpdate } from '../../src/types/Person.js';
|
|
40
45
|
export type { Organization, OrganizationInput, OrganizationUpdate } from '../../src/types/Organization.js';
|
|
41
46
|
export type { ContactRefusedError } from '../../src/utils/contactAccess.js';
|
|
47
|
+
export type { default as RoleManager } from '../../src/managers/RoleManager.js';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import ExportManager from '../../src/managers/ExportManager.js';
|
|
5
5
|
import { type AddonDatabaseOptions } from '../../src/managers/DatabaseManager.js';
|
|
6
6
|
import { type SqliteHandle } from '../../src/providers/SqliteDatabaseProvider.js';
|
|
7
|
-
import { buildAddonHost as buildHost, type AddonHost, type PersonSwitchSetter } from '../../src/utils/addonHost.js';
|
|
7
|
+
import { buildAddonHost as buildHost, type AddonHost, type AddonIceDeclaration, type PersonSwitchSetter } from '../../src/utils/addonHost.js';
|
|
8
8
|
export { formatters } from '../../src/managers/PluginManager.js';
|
|
9
9
|
/**
|
|
10
10
|
* Core's `buildAddonHost`, with the audit registry bound to this add-on's
|
|
@@ -19,9 +19,13 @@ export { formatters } from '../../src/managers/PluginManager.js';
|
|
|
19
19
|
* test (#1963); a site binds core's setter. Without one it refuses.
|
|
20
20
|
* @param engine - Optional: an engine (or stand-in) whose PolicyInformationPoint
|
|
21
21
|
* names the system principal for `host.systemContext`; without one it is
|
|
22
|
-
* core's fallback principal
|
|
22
|
+
* core's fallback principal. Its `ImportManager` (a real one, or a
|
|
23
|
+
* stand-in) is where `host.registerShim` and `host.registerReceiver` register
|
|
24
|
+
* @param manifest - Optional: the add-on's manifest (the `ngdpbase` block of
|
|
25
|
+
* its package.json); its `shims` and `receivers` are the names the host lets
|
|
26
|
+
* it register (#1979)
|
|
23
27
|
*/
|
|
24
|
-
export declare function buildAddonHost(slug: string, addonDefaults: Record<string, unknown>, setEnabledForPerson?: PersonSwitchSetter, engine?: Parameters<typeof buildHost>[3]): AddonHost;
|
|
28
|
+
export declare function buildAddonHost(slug: string, addonDefaults: Record<string, unknown>, setEnabledForPerson?: PersonSwitchSetter, engine?: Parameters<typeof buildHost>[3], manifest?: AddonIceDeclaration | null): AddonHost;
|
|
25
29
|
/** The add-on whose declaration a test database is checked against: its slug and its own `config/default-config.json`. */
|
|
26
30
|
export interface AddonTestDatabaseDeclarer {
|
|
27
31
|
slug: string;
|
|
@@ -18,11 +18,15 @@ export { formatters } from '../../src/managers/PluginManager.js';
|
|
|
18
18
|
* test (#1963); a site binds core's setter. Without one it refuses.
|
|
19
19
|
* @param engine - Optional: an engine (or stand-in) whose PolicyInformationPoint
|
|
20
20
|
* names the system principal for `host.systemContext`; without one it is
|
|
21
|
-
* core's fallback principal
|
|
21
|
+
* core's fallback principal. Its `ImportManager` (a real one, or a
|
|
22
|
+
* stand-in) is where `host.registerShim` and `host.registerReceiver` register
|
|
23
|
+
* @param manifest - Optional: the add-on's manifest (the `ngdpbase` block of
|
|
24
|
+
* its package.json); its `shims` and `receivers` are the names the host lets
|
|
25
|
+
* it register (#1979)
|
|
22
26
|
*/
|
|
23
|
-
export function buildAddonHost(slug, addonDefaults, setEnabledForPerson, engine) {
|
|
27
|
+
export function buildAddonHost(slug, addonDefaults, setEnabledForPerson, engine, manifest) {
|
|
24
28
|
bindAuditEvents((key, fallback) => addonDefaults[key] ?? fallback);
|
|
25
|
-
return buildHost(slug, addonDefaults, setEnabledForPerson, engine);
|
|
29
|
+
return buildHost(slug, addonDefaults, setEnabledForPerson, engine, manifest);
|
|
26
30
|
}
|
|
27
31
|
/**
|
|
28
32
|
* An add-on's database as `DatabaseManager.openAddonDatabase` returns it, in
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* the call arrived under, and the address it came from. What does not: roles.
|
|
14
14
|
* Authority is resolved at the moment of each decision, never carried.
|
|
15
15
|
*/
|
|
16
|
-
import type { PermissionSubject } from '../managers/UserManager.js';
|
|
17
|
-
import type
|
|
16
|
+
import type { PermissionSubject, JobSubject } from '../managers/UserManager.js';
|
|
17
|
+
import { type JobContext } from './JobContext.js';
|
|
18
18
|
export type ActorContext = PermissionSubject | JobContext;
|
|
19
19
|
/**
|
|
20
20
|
* The fields an audit record takes from a context.
|
|
@@ -35,6 +35,19 @@ export interface ActorAttribution {
|
|
|
35
35
|
metadata: Record<string, unknown>;
|
|
36
36
|
}
|
|
37
37
|
export declare function isJobContext(ctx: ActorContext): ctx is JobContext;
|
|
38
|
+
/**
|
|
39
|
+
* The one place a context becomes the subject a decision reads (#2022).
|
|
40
|
+
*
|
|
41
|
+
* Called by the deciders — `PolicyDecisionPoint` (`decide`, `permits`,
|
|
42
|
+
* `holds`) and `PolicyInformationPoint`'s page questions — and by nobody
|
|
43
|
+
* else: a caller hands them its context as given. A `JobContext` carries
|
|
44
|
+
* identity, never authority, so it becomes a `JobSubject` whose roles policy
|
|
45
|
+
* resolves now, keeping its token, share and proxy claims
|
|
46
|
+
* (`toPermissionSubject`). Anything else is already a subject and passes
|
|
47
|
+
* through. The object guard is #1173's: `'x' in 'jim'` throws. Not part of
|
|
48
|
+
* the add-on API; `npm run lint:context` holds callers to it.
|
|
49
|
+
*/
|
|
50
|
+
export declare function decisionSubject<S extends PermissionSubject | JobSubject>(input: ActorContext | S | null | undefined): PermissionSubject | JobSubject | S | null | undefined;
|
|
38
51
|
/**
|
|
39
52
|
* Who acted, for the record — read from the context, never guessed.
|
|
40
53
|
*
|
|
@@ -1,8 +1,28 @@
|
|
|
1
|
+
import { toPermissionSubject } from './JobContext.js';
|
|
1
2
|
import { attributedTo } from './bootActions.js';
|
|
2
3
|
import { proxyAttribution } from '../utils/proxyClaim.js';
|
|
3
4
|
export function isJobContext(ctx) {
|
|
4
5
|
return 'origin' in ctx && 'requestedAt' in ctx;
|
|
5
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* The one place a context becomes the subject a decision reads (#2022).
|
|
9
|
+
*
|
|
10
|
+
* Called by the deciders — `PolicyDecisionPoint` (`decide`, `permits`,
|
|
11
|
+
* `holds`) and `PolicyInformationPoint`'s page questions — and by nobody
|
|
12
|
+
* else: a caller hands them its context as given. A `JobContext` carries
|
|
13
|
+
* identity, never authority, so it becomes a `JobSubject` whose roles policy
|
|
14
|
+
* resolves now, keeping its token, share and proxy claims
|
|
15
|
+
* (`toPermissionSubject`). Anything else is already a subject and passes
|
|
16
|
+
* through. The object guard is #1173's: `'x' in 'jim'` throws. Not part of
|
|
17
|
+
* the add-on API; `npm run lint:context` holds callers to it.
|
|
18
|
+
*/
|
|
19
|
+
export function decisionSubject(input) {
|
|
20
|
+
if (typeof input !== 'object' || input === null)
|
|
21
|
+
return input;
|
|
22
|
+
// A JobSubject is not an ActorContext, and is not a job context either.
|
|
23
|
+
const ctx = input;
|
|
24
|
+
return isJobContext(ctx) ? toPermissionSubject(ctx) : input;
|
|
25
|
+
}
|
|
6
26
|
/**
|
|
7
27
|
* Who acted, for the record — read from the context, never guessed.
|
|
8
28
|
*
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Word (.docx) converter (#1131) — the first BINARY source format.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately thin: mammoth (pure JS, no native deps) turns the document
|
|
5
|
+
* into clean HTML, and ImportManager routes that HTML through the EXISTING
|
|
6
|
+
* html→NCM path — turndown, §2.4 link normalization, §2.1 table up-convert,
|
|
7
|
+
* §2.2 image localization, and the #1125 footnote transfer all come from
|
|
8
|
+
* the funnel, not from here. The converter's own contribution is the
|
|
9
|
+
* `convertBuffer` contract that text converters don't need.
|
|
10
|
+
*
|
|
11
|
+
* Known limitation, recorded on the issue: docx footnotes arrive as
|
|
12
|
+
* mammoth's anchor/list HTML rather than `[^id]` markdown, so they survive
|
|
13
|
+
* as ordinary links instead of sidecar records. Mapping them is a follow-up.
|
|
14
|
+
*/
|
|
15
|
+
import type { IContentConverter, ConversionResult } from './IContentConverter.js';
|
|
16
|
+
export default class DocxConverter implements IContentConverter {
|
|
17
|
+
readonly formatId = "docx";
|
|
18
|
+
readonly formatName = "Word (.docx)";
|
|
19
|
+
readonly fileExtensions: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Never claims by content (#879 extension-first): a docx read as UTF-8 is
|
|
22
|
+
* zip garbage, and probing it risks both false negatives and another
|
|
23
|
+
* converter mis-claiming. detectFormat's extension pass finds this
|
|
24
|
+
* converter; the content pass must stay silent.
|
|
25
|
+
*/
|
|
26
|
+
canHandle(_content: string, _filename: string): boolean;
|
|
27
|
+
/** A docx has no meaningful text form — the binary contract is the only door. */
|
|
28
|
+
convert(_content: string): ConversionResult;
|
|
29
|
+
convertBuffer(buffer: Buffer): Promise<ConversionResult>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown Pass-Through Converter
|
|
3
|
+
*
|
|
4
|
+
* Imports `.md` and `.markdown` files into ngdpbase without format conversion.
|
|
5
|
+
* Parses existing YAML frontmatter and exposes it as metadata so ImportManager
|
|
6
|
+
* can preserve recognised fields (title, slug, uuid, audience, system-category,
|
|
7
|
+
* user-keywords, author, source-url, etc.) and generate fresh values for any
|
|
8
|
+
* that are missing (uuid, lastModified).
|
|
9
|
+
*
|
|
10
|
+
* @module MarkdownConverter
|
|
11
|
+
*/
|
|
12
|
+
import { IContentConverter, ConversionResult } from './IContentConverter.js';
|
|
13
|
+
declare class MarkdownConverter implements IContentConverter {
|
|
14
|
+
readonly formatId = "markdown";
|
|
15
|
+
readonly formatName = "Markdown";
|
|
16
|
+
readonly fileExtensions: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Parse frontmatter and return body content unchanged.
|
|
19
|
+
*
|
|
20
|
+
* Existing frontmatter fields are extracted as metadata so ImportManager's
|
|
21
|
+
* `buildFrontmatter` can honour them (e.g. preserve a uuid that already
|
|
22
|
+
* exists, carry over audience restrictions, etc.).
|
|
23
|
+
*/
|
|
24
|
+
convert(content: string): ConversionResult;
|
|
25
|
+
/**
|
|
26
|
+
* Accepts .md / .markdown by extension; also matches content that starts
|
|
27
|
+
* with a YAML frontmatter block or a Markdown heading.
|
|
28
|
+
*/
|
|
29
|
+
canHandle(content: string, filename: string): boolean;
|
|
30
|
+
}
|
|
31
|
+
export default MarkdownConverter;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Transport } from '../http/guardedFetch.js';
|
|
2
|
+
import type { Resolver } from '../http/guardedLookup.js';
|
|
3
|
+
import type { EgressPolicy } from '../http/ssrf.js';
|
|
4
|
+
import { type PdfPageSize, type ResolvedPdfOptions } from './pdfEngine.js';
|
|
5
|
+
import type { PdfProvider, PdfRenderRequest, PdfRendered } from './PdfProvider.js';
|
|
6
|
+
/** Gotenberg's HTML → PDF route, relative to the configured service URL. */
|
|
7
|
+
export declare const GOTENBERG_HTML_ROUTE = "forms/chromium/convert/html";
|
|
8
|
+
/**
|
|
9
|
+
* The named page sizes in inches, as Chromium's (and Playwright's) print uses
|
|
10
|
+
* them, so a service render and a local one get the same paper.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PAPER_INCHES: Record<PdfPageSize, readonly [number, number]>;
|
|
13
|
+
export interface ChromeServiceSettings {
|
|
14
|
+
/** `ngdpbase.export.pdf.provider.chrome-service.url` — the service's base URL. */
|
|
15
|
+
url: string;
|
|
16
|
+
/** The site's egress policy (`resolveEgressPolicy`). */
|
|
17
|
+
policy: EgressPolicy;
|
|
18
|
+
/** The largest answer accepted, in bytes. */
|
|
19
|
+
maxBytes: number;
|
|
20
|
+
}
|
|
21
|
+
/** Hooks for tests; a site passes nothing. */
|
|
22
|
+
export interface ChromeServiceDeps {
|
|
23
|
+
transport?: Transport;
|
|
24
|
+
resolver?: Resolver;
|
|
25
|
+
normalize?: (pdf: Uint8Array, timeoutMs: number) => Promise<Uint8Array>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The form fields for one render. Every margin is sent, `0` when the caller
|
|
29
|
+
* set none, because Gotenberg's own default margin is not zero and a local
|
|
30
|
+
* render's is.
|
|
31
|
+
*
|
|
32
|
+
* @param options - The validated options
|
|
33
|
+
*/
|
|
34
|
+
export declare function gotenbergFields(options: ResolvedPdfOptions): Array<[string, string]>;
|
|
35
|
+
/**
|
|
36
|
+
* A `multipart/form-data` body: the fields, then the document as `files`
|
|
37
|
+
* named `index.html` (the name Gotenberg's HTML route requires).
|
|
38
|
+
*
|
|
39
|
+
* @param html - The document
|
|
40
|
+
* @param fields - Name and value pairs
|
|
41
|
+
* @param boundary - The part boundary
|
|
42
|
+
*/
|
|
43
|
+
export declare function multipartBody(html: string, fields: Array<[string, string]>, boundary: string): Buffer;
|
|
44
|
+
/** The service URL with the route appended (a path on the URL is kept). */
|
|
45
|
+
export declare function serviceEndpoint(base: string): string;
|
|
46
|
+
export declare class ChromeServicePdfProvider implements PdfProvider {
|
|
47
|
+
private readonly settings;
|
|
48
|
+
private readonly deps;
|
|
49
|
+
readonly name: "chrome-service";
|
|
50
|
+
constructor(settings: ChromeServiceSettings, deps?: ChromeServiceDeps);
|
|
51
|
+
render({ html, options, timeoutMs }: PdfRenderRequest): Promise<PdfRendered>;
|
|
52
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chrome-service` (#1974): a separate Chromium service reached by URL, for a
|
|
3
|
+
* site whose own image carries no browser (the Docker image). It speaks
|
|
4
|
+
* Gotenberg's HTML route: `POST <url>/forms/chromium/convert/html`, a
|
|
5
|
+
* multipart form with the document as `files` (`index.html`) and the page
|
|
6
|
+
* settings as fields; the answer is the PDF.
|
|
7
|
+
*
|
|
8
|
+
* - Outbound through `guardedFetch` under the site's egress policy. A service
|
|
9
|
+
* on the LAN or in the cluster is on a private address, which the policy
|
|
10
|
+
* refuses until the operator allows its range
|
|
11
|
+
* (`ngdpbase.security.egress.allowed-ranges`); loopback is never allowed.
|
|
12
|
+
* - The whole request is bounded by `ngdpbase.export.pdf.timeout-ms`, and the
|
|
13
|
+
* answer by `ngdpbase.export.pdf.provider.chrome-service.max-bytes`.
|
|
14
|
+
* - The PDF goes through the same `normalizePdf` pass a local render gets, in
|
|
15
|
+
* a child process, so the same HTML gives the same bytes for a given service
|
|
16
|
+
* version.
|
|
17
|
+
*
|
|
18
|
+
* What the service's Chromium may load (remote images, files) is the
|
|
19
|
+
* service's own setting, not this request's; the setup guide says how to
|
|
20
|
+
* lock it down.
|
|
21
|
+
*/
|
|
22
|
+
import { randomUUID } from 'node:crypto';
|
|
23
|
+
import { guardedFetch } from '../http/guardedFetch.js';
|
|
24
|
+
import { PdfUnavailableError, normalizePdfInChild } from './pdfEngine.js';
|
|
25
|
+
/** Gotenberg's HTML → PDF route, relative to the configured service URL. */
|
|
26
|
+
export const GOTENBERG_HTML_ROUTE = 'forms/chromium/convert/html';
|
|
27
|
+
/**
|
|
28
|
+
* The named page sizes in inches, as Chromium's (and Playwright's) print uses
|
|
29
|
+
* them, so a service render and a local one get the same paper.
|
|
30
|
+
*/
|
|
31
|
+
export const PAPER_INCHES = {
|
|
32
|
+
Letter: [8.5, 11],
|
|
33
|
+
Legal: [8.5, 14],
|
|
34
|
+
Tabloid: [11, 17],
|
|
35
|
+
Ledger: [17, 11],
|
|
36
|
+
A0: [33.1, 46.8],
|
|
37
|
+
A1: [23.4, 33.1],
|
|
38
|
+
A2: [16.54, 23.4],
|
|
39
|
+
A3: [11.7, 16.54],
|
|
40
|
+
A4: [8.27, 11.7],
|
|
41
|
+
A5: [5.83, 8.27],
|
|
42
|
+
A6: [4.13, 5.83]
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The form fields for one render. Every margin is sent, `0` when the caller
|
|
46
|
+
* set none, because Gotenberg's own default margin is not zero and a local
|
|
47
|
+
* render's is.
|
|
48
|
+
*
|
|
49
|
+
* @param options - The validated options
|
|
50
|
+
*/
|
|
51
|
+
export function gotenbergFields(options) {
|
|
52
|
+
const [width, height] = PAPER_INCHES[options.format];
|
|
53
|
+
const margin = (side) => options.margin[side] ?? '0';
|
|
54
|
+
return [
|
|
55
|
+
['paperWidth', `${width}in`],
|
|
56
|
+
['paperHeight', `${height}in`],
|
|
57
|
+
['marginTop', margin('top')],
|
|
58
|
+
['marginRight', margin('right')],
|
|
59
|
+
['marginBottom', margin('bottom')],
|
|
60
|
+
['marginLeft', margin('left')],
|
|
61
|
+
['preferCssPageSize', 'true'],
|
|
62
|
+
['printBackground', String(options.printBackground)]
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A `multipart/form-data` body: the fields, then the document as `files`
|
|
67
|
+
* named `index.html` (the name Gotenberg's HTML route requires).
|
|
68
|
+
*
|
|
69
|
+
* @param html - The document
|
|
70
|
+
* @param fields - Name and value pairs
|
|
71
|
+
* @param boundary - The part boundary
|
|
72
|
+
*/
|
|
73
|
+
export function multipartBody(html, fields, boundary) {
|
|
74
|
+
const parts = [];
|
|
75
|
+
for (const [name, value] of fields) {
|
|
76
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`));
|
|
77
|
+
}
|
|
78
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="files"; filename="index.html"\r\nContent-Type: text/html; charset=utf-8\r\n\r\n`));
|
|
79
|
+
parts.push(Buffer.from(html, 'utf8'));
|
|
80
|
+
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
|
|
81
|
+
return Buffer.concat(parts);
|
|
82
|
+
}
|
|
83
|
+
/** The service URL with the route appended (a path on the URL is kept). */
|
|
84
|
+
export function serviceEndpoint(base) {
|
|
85
|
+
const url = new URL(base);
|
|
86
|
+
if (!url.pathname.endsWith('/'))
|
|
87
|
+
url.pathname += '/';
|
|
88
|
+
return new URL(GOTENBERG_HTML_ROUTE, url).toString();
|
|
89
|
+
}
|
|
90
|
+
export class ChromeServicePdfProvider {
|
|
91
|
+
settings;
|
|
92
|
+
deps;
|
|
93
|
+
name = 'chrome-service';
|
|
94
|
+
constructor(settings, deps = {}) {
|
|
95
|
+
this.settings = settings;
|
|
96
|
+
this.deps = deps;
|
|
97
|
+
}
|
|
98
|
+
async render({ html, options, timeoutMs }) {
|
|
99
|
+
const { url, policy, maxBytes } = this.settings;
|
|
100
|
+
if (!url)
|
|
101
|
+
throw new PdfUnavailableError('ngdpbase.export.pdf.provider.chrome-service.url is not set');
|
|
102
|
+
let endpoint;
|
|
103
|
+
try {
|
|
104
|
+
endpoint = serviceEndpoint(url);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
throw new PdfUnavailableError('ngdpbase.export.pdf.provider.chrome-service.url is not a URL');
|
|
108
|
+
}
|
|
109
|
+
const where = new URL(endpoint).origin;
|
|
110
|
+
const boundary = `ngdpbase-pdf-${randomUUID()}`;
|
|
111
|
+
const body = multipartBody(html, gotenbergFields(options), boundary);
|
|
112
|
+
const started = Date.now();
|
|
113
|
+
let response;
|
|
114
|
+
try {
|
|
115
|
+
response = await withDeadline(guardedFetch(endpoint, {
|
|
116
|
+
policy,
|
|
117
|
+
method: 'POST',
|
|
118
|
+
body,
|
|
119
|
+
headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}`, Accept: 'application/pdf' },
|
|
120
|
+
timeoutMs,
|
|
121
|
+
maxBytes,
|
|
122
|
+
...(this.deps.transport ? { transport: this.deps.transport } : {}),
|
|
123
|
+
...(this.deps.resolver ? { resolver: this.deps.resolver } : {})
|
|
124
|
+
}), timeoutMs);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
const message = err.message;
|
|
128
|
+
// Too slow or too big: the service ran, and this render failed.
|
|
129
|
+
if (/timed out|too large/.test(message))
|
|
130
|
+
throw new Error(`PDF render failed: the PDF service at ${where} ${message}`, { cause: err });
|
|
131
|
+
// Refused by the egress policy, or nothing answering: no engine to use.
|
|
132
|
+
throw new PdfUnavailableError(`the PDF service at ${where} could not be reached: ${message}`, { cause: err });
|
|
133
|
+
}
|
|
134
|
+
if (response.status !== 200) {
|
|
135
|
+
const said = response.body.subarray(0, 200).toString('utf8').replace(/\s+/g, ' ').trim();
|
|
136
|
+
throw new Error(`PDF render failed: the PDF service at ${where} answered ${response.status}${said ? `: ${said}` : ''}`);
|
|
137
|
+
}
|
|
138
|
+
if (response.body.subarray(0, 5).toString('latin1') !== '%PDF-') {
|
|
139
|
+
throw new Error(`PDF render failed: the PDF service at ${where} answered, but not with a PDF`);
|
|
140
|
+
}
|
|
141
|
+
const remaining = Math.max(1, timeoutMs - (Date.now() - started));
|
|
142
|
+
const pdf = await (this.deps.normalize ?? normalizePdfInChild)(response.body, remaining);
|
|
143
|
+
return { pdf, engine: 'chrome-service' };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** `guardedFetch`'s timeout is per socket wait; this bounds the whole request. */
|
|
147
|
+
function withDeadline(work, timeoutMs) {
|
|
148
|
+
let timer;
|
|
149
|
+
const deadline = new Promise((_, reject) => {
|
|
150
|
+
timer = setTimeout(() => reject(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
151
|
+
});
|
|
152
|
+
return Promise.race([work, deadline]).finally(() => clearTimeout(timer));
|
|
153
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `local-chrome` (#1974): a Chromium already on the machine — the configured
|
|
3
|
+
* path (`ngdpbase.export.pdf.provider.local-chrome.chromium-path`), or one
|
|
4
|
+
* `resolveChromium` finds. Each render is its own child process
|
|
5
|
+
* (`pdfRenderWorker.ts`) with JavaScript off and every request refused except
|
|
6
|
+
* `data:` URIs, killed at the timeout.
|
|
7
|
+
*/
|
|
8
|
+
import { type ChromiumResolution, type PdfJob, type PdfJobResult } from './pdfEngine.js';
|
|
9
|
+
import type { PdfProvider, PdfRenderRequest, PdfRendered } from './PdfProvider.js';
|
|
10
|
+
/** Hooks for tests; a site passes nothing. */
|
|
11
|
+
export interface LocalChromeDeps {
|
|
12
|
+
render?: (job: PdfJob) => Promise<PdfJobResult>;
|
|
13
|
+
resolve?: (configuredPath: string) => Promise<ChromiumResolution>;
|
|
14
|
+
}
|
|
15
|
+
export declare class LocalChromePdfProvider implements PdfProvider {
|
|
16
|
+
private readonly chromiumPath;
|
|
17
|
+
private readonly deps;
|
|
18
|
+
readonly name: "local-chrome";
|
|
19
|
+
/**
|
|
20
|
+
* @param chromiumPath - The configured Chromium ('' = find one)
|
|
21
|
+
* @param deps - Test hooks
|
|
22
|
+
*/
|
|
23
|
+
constructor(chromiumPath: string, deps?: LocalChromeDeps);
|
|
24
|
+
render({ html, options, timeoutMs }: PdfRenderRequest): Promise<PdfRendered>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `local-chrome` (#1974): a Chromium already on the machine — the configured
|
|
3
|
+
* path (`ngdpbase.export.pdf.provider.local-chrome.chromium-path`), or one
|
|
4
|
+
* `resolveChromium` finds. Each render is its own child process
|
|
5
|
+
* (`pdfRenderWorker.ts`) with JavaScript off and every request refused except
|
|
6
|
+
* `data:` URIs, killed at the timeout.
|
|
7
|
+
*/
|
|
8
|
+
import { PdfUnavailableError, renderPdfInChild, resolveChromium } from './pdfEngine.js';
|
|
9
|
+
export class LocalChromePdfProvider {
|
|
10
|
+
chromiumPath;
|
|
11
|
+
deps;
|
|
12
|
+
name = 'local-chrome';
|
|
13
|
+
/**
|
|
14
|
+
* @param chromiumPath - The configured Chromium ('' = find one)
|
|
15
|
+
* @param deps - Test hooks
|
|
16
|
+
*/
|
|
17
|
+
constructor(chromiumPath, deps = {}) {
|
|
18
|
+
this.chromiumPath = chromiumPath;
|
|
19
|
+
this.deps = deps;
|
|
20
|
+
}
|
|
21
|
+
async render({ html, options, timeoutMs }) {
|
|
22
|
+
const engine = await (this.deps.resolve ?? ((p) => resolveChromium(p)))(this.chromiumPath);
|
|
23
|
+
if (!engine.ok)
|
|
24
|
+
throw new PdfUnavailableError(engine.reason);
|
|
25
|
+
const result = await (this.deps.render ?? renderPdfInChild)({ kind: 'render', html, executablePath: engine.executablePath, options, timeoutMs });
|
|
26
|
+
if (!result.ok) {
|
|
27
|
+
if (result.stage === 'launch')
|
|
28
|
+
throw new PdfUnavailableError(`Chromium at ${engine.executablePath} could not start: ${result.error}`);
|
|
29
|
+
throw new Error(`PDF render failed: ${result.error}`);
|
|
30
|
+
}
|
|
31
|
+
return { pdf: result.pdf, engine: `local-chrome:${engine.source}` };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PdfProvider — how ExportManager turns HTML into PDF (#1974).
|
|
3
|
+
*
|
|
4
|
+
* `ngdpbase.export.pdf.provider` chooses one, as `ngdpbase.mail.provider` and
|
|
5
|
+
* `ngdpbase.database.provider` do for their managers:
|
|
6
|
+
*
|
|
7
|
+
* - `none` (default) — `NonePdfProvider`: no PDF; `htmlToPdf` throws
|
|
8
|
+
* `PdfUnavailableError` and an add-on falls back to HTML.
|
|
9
|
+
* - `local-chrome` — `LocalChromePdfProvider`: a Chromium already on the
|
|
10
|
+
* machine, run in a child process with no network.
|
|
11
|
+
* - `chrome-service` — `ChromeServicePdfProvider`: a separate Chromium service
|
|
12
|
+
* reached by URL (Gotenberg's HTML route), through `guardedFetch`.
|
|
13
|
+
*
|
|
14
|
+
* A provider renders one document and returns the normalised bytes
|
|
15
|
+
* (`normalizePdf`), so the same HTML gives the same bytes for a given provider
|
|
16
|
+
* and engine version. It throws `PdfUnavailableError` when it has no engine to
|
|
17
|
+
* use (not set up, not reachable, cannot start) and a plain `Error` when the
|
|
18
|
+
* engine ran and the render failed. Another engine (e.g. WeasyPrint) is a new
|
|
19
|
+
* class here and one more name in `PDF_PROVIDERS`.
|
|
20
|
+
*/
|
|
21
|
+
import { type ResolvedPdfOptions } from './pdfEngine.js';
|
|
22
|
+
/** The values `ngdpbase.export.pdf.provider` takes. */
|
|
23
|
+
export declare const PDF_PROVIDERS: readonly ["none", "local-chrome", "chrome-service"];
|
|
24
|
+
export type PdfProviderName = (typeof PDF_PROVIDERS)[number];
|
|
25
|
+
/** One render, after the options are validated and the defaults filled. */
|
|
26
|
+
export interface PdfRenderRequest {
|
|
27
|
+
html: string;
|
|
28
|
+
options: ResolvedPdfOptions;
|
|
29
|
+
/** The whole render, including any wait on a PDF service. */
|
|
30
|
+
timeoutMs: number;
|
|
31
|
+
}
|
|
32
|
+
export interface PdfRendered {
|
|
33
|
+
/** The PDF, normalised. */
|
|
34
|
+
pdf: Uint8Array;
|
|
35
|
+
/** What made it, for the log line (`local-chrome:system`, `chrome-service`). */
|
|
36
|
+
engine: string;
|
|
37
|
+
}
|
|
38
|
+
export interface PdfProvider {
|
|
39
|
+
readonly name: PdfProviderName;
|
|
40
|
+
render(request: PdfRenderRequest): Promise<PdfRendered>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `none`: PDF export is off. Also stands in for a provider name that is not
|
|
44
|
+
* one, so a typo is reported the same way — unavailable, with the reason.
|
|
45
|
+
*/
|
|
46
|
+
export declare class NonePdfProvider implements PdfProvider {
|
|
47
|
+
private readonly reason;
|
|
48
|
+
readonly name: "none";
|
|
49
|
+
constructor(reason?: string);
|
|
50
|
+
render(): Promise<PdfRendered>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PdfProvider — how ExportManager turns HTML into PDF (#1974).
|
|
3
|
+
*
|
|
4
|
+
* `ngdpbase.export.pdf.provider` chooses one, as `ngdpbase.mail.provider` and
|
|
5
|
+
* `ngdpbase.database.provider` do for their managers:
|
|
6
|
+
*
|
|
7
|
+
* - `none` (default) — `NonePdfProvider`: no PDF; `htmlToPdf` throws
|
|
8
|
+
* `PdfUnavailableError` and an add-on falls back to HTML.
|
|
9
|
+
* - `local-chrome` — `LocalChromePdfProvider`: a Chromium already on the
|
|
10
|
+
* machine, run in a child process with no network.
|
|
11
|
+
* - `chrome-service` — `ChromeServicePdfProvider`: a separate Chromium service
|
|
12
|
+
* reached by URL (Gotenberg's HTML route), through `guardedFetch`.
|
|
13
|
+
*
|
|
14
|
+
* A provider renders one document and returns the normalised bytes
|
|
15
|
+
* (`normalizePdf`), so the same HTML gives the same bytes for a given provider
|
|
16
|
+
* and engine version. It throws `PdfUnavailableError` when it has no engine to
|
|
17
|
+
* use (not set up, not reachable, cannot start) and a plain `Error` when the
|
|
18
|
+
* engine ran and the render failed. Another engine (e.g. WeasyPrint) is a new
|
|
19
|
+
* class here and one more name in `PDF_PROVIDERS`.
|
|
20
|
+
*/
|
|
21
|
+
import { PdfUnavailableError } from './pdfEngine.js';
|
|
22
|
+
/** The values `ngdpbase.export.pdf.provider` takes. */
|
|
23
|
+
export const PDF_PROVIDERS = ['none', 'local-chrome', 'chrome-service'];
|
|
24
|
+
/**
|
|
25
|
+
* `none`: PDF export is off. Also stands in for a provider name that is not
|
|
26
|
+
* one, so a typo is reported the same way — unavailable, with the reason.
|
|
27
|
+
*/
|
|
28
|
+
export class NonePdfProvider {
|
|
29
|
+
reason;
|
|
30
|
+
name = 'none';
|
|
31
|
+
constructor(reason = 'ngdpbase.export.pdf.provider is none: set it to local-chrome or chrome-service') {
|
|
32
|
+
this.reason = reason;
|
|
33
|
+
}
|
|
34
|
+
render() {
|
|
35
|
+
return Promise.reject(new PdfUnavailableError(this.reason));
|
|
36
|
+
}
|
|
37
|
+
}
|