@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.
Files changed (52) hide show
  1. package/lib/packages/ngdpbase-types/index.d.ts +8 -2
  2. package/lib/packages/ngdpbase-types/testing.d.ts +7 -3
  3. package/lib/packages/ngdpbase-types/testing.js +7 -3
  4. package/lib/src/context/ActorContext.d.ts +15 -2
  5. package/lib/src/context/ActorContext.js +20 -0
  6. package/lib/src/converters/DocxConverter.d.ts +30 -0
  7. package/lib/src/converters/MarkdownConverter.d.ts +31 -0
  8. package/lib/src/export/ChromeServicePdfProvider.d.ts +52 -0
  9. package/lib/src/export/ChromeServicePdfProvider.js +153 -0
  10. package/lib/src/export/LocalChromePdfProvider.d.ts +25 -0
  11. package/lib/src/export/LocalChromePdfProvider.js +33 -0
  12. package/lib/src/export/PdfProvider.d.ts +51 -0
  13. package/lib/src/export/PdfProvider.js +37 -0
  14. package/lib/src/export/pdfEngine.d.ts +113 -0
  15. package/lib/src/export/pdfEngine.js +195 -0
  16. package/lib/src/ice/converterShim.d.ts +5 -0
  17. package/lib/src/ice/fileHandling.d.ts +30 -0
  18. package/lib/src/ice/ledger.d.ts +37 -0
  19. package/lib/src/ice/runImport.d.ts +51 -0
  20. package/lib/src/ice/types.d.ts +284 -0
  21. package/lib/src/mail/MailProvider.d.ts +15 -0
  22. package/lib/src/mail/NodemailerMailProvider.d.ts +1 -1
  23. package/lib/src/managers/AddonsManager.d.ts +25 -5
  24. package/lib/src/managers/AssetManager.d.ts +7 -3
  25. package/lib/src/managers/EmailManager.d.ts +32 -5
  26. package/lib/src/managers/ExportManager.d.ts +62 -0
  27. package/lib/src/managers/ExportManager.js +152 -2
  28. package/lib/src/managers/ImportManager.d.ts +611 -0
  29. package/lib/src/managers/OrganizationManager.d.ts +5 -3
  30. package/lib/src/managers/PageManager.d.ts +77 -1
  31. package/lib/src/managers/PersonManager.d.ts +36 -11
  32. package/lib/src/managers/RoleManager.d.ts +76 -17
  33. package/lib/src/managers/UserManager.d.ts +55 -2
  34. package/lib/src/security/PolicyDecisionPoint.d.ts +18 -9
  35. package/lib/src/security/PolicyInformationPoint.d.ts +48 -10
  36. package/lib/src/types/Config.d.ts +2 -0
  37. package/lib/src/types/User.d.ts +1 -1
  38. package/lib/src/utils/addonHost.d.ts +23 -2
  39. package/lib/src/utils/addonHost.js +25 -2
  40. package/lib/src/utils/auditEventNames.generated.d.ts +10 -10
  41. package/lib/src/utils/auditEventNames.generated.js +10 -10
  42. package/lib/src/utils/contactAccess.d.ts +30 -0
  43. package/lib/src/utils/frontmatterAccess.d.ts +54 -11
  44. package/lib/src/utils/importRunSummary.d.ts +64 -0
  45. package/lib/src/utils/keyedSerial.d.ts +31 -0
  46. package/lib/src/utils/ncmNotify.d.ts +30 -0
  47. package/lib/src/utils/privateStoreAccess.js +1 -3
  48. package/lib/src/utils/privateStoreImport.d.ts +58 -0
  49. package/lib/src/utils/roleChangeError.d.ts +25 -0
  50. package/lib/src/utils/subjectMayDo.d.ts +2 -2
  51. package/lib/src/utils/zipArchive.d.ts +71 -0
  52. 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 { JobContext } from './JobContext.js';
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
+ }