@jwilleke/ngdpbase-types 5.25.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.
Files changed (89) hide show
  1. package/README.md +1 -1
  2. package/lib/packages/ngdpbase-types/index.d.ts +9 -2
  3. package/lib/packages/ngdpbase-types/testing.d.ts +7 -3
  4. package/lib/packages/ngdpbase-types/testing.js +7 -3
  5. package/lib/src/converters/DocxConverter.d.ts +30 -0
  6. package/lib/src/converters/MarkdownConverter.d.ts +31 -0
  7. package/lib/src/export/ChromeServicePdfProvider.d.ts +52 -0
  8. package/lib/src/export/ChromeServicePdfProvider.js +153 -0
  9. package/lib/src/export/LocalChromePdfProvider.d.ts +25 -0
  10. package/lib/src/export/LocalChromePdfProvider.js +33 -0
  11. package/lib/src/export/PdfProvider.d.ts +51 -0
  12. package/lib/src/export/PdfProvider.js +37 -0
  13. package/lib/src/export/pdfEngine.d.ts +113 -0
  14. package/lib/src/export/pdfEngine.js +195 -0
  15. package/lib/src/http/egressPolicy.js +61 -0
  16. package/lib/src/http/guardedElasticsearch.d.ts +27 -0
  17. package/lib/src/http/guardedElasticsearch.js +99 -0
  18. package/lib/src/ice/converterShim.d.ts +5 -0
  19. package/lib/src/ice/fileHandling.d.ts +30 -0
  20. package/lib/src/ice/ledger.d.ts +37 -0
  21. package/lib/src/ice/runImport.d.ts +51 -0
  22. package/lib/src/ice/types.d.ts +262 -0
  23. package/lib/src/mail/MailProvider.d.ts +15 -0
  24. package/lib/src/mail/NodemailerMailProvider.d.ts +1 -1
  25. package/lib/src/managers/AddonsManager.d.ts +55 -3
  26. package/lib/src/managers/AttachmentManager.d.ts +4 -5
  27. package/lib/src/managers/AuthManager.d.ts +150 -32
  28. package/lib/src/managers/BackgroundJobManager.d.ts +22 -12
  29. package/lib/src/managers/BackupManager.d.ts +779 -0
  30. package/lib/src/managers/BackupManager.js +2042 -0
  31. package/lib/src/managers/BaseManager.d.ts +156 -6
  32. package/lib/src/managers/BaseManager.js +64 -2
  33. package/lib/src/managers/CommentManager.d.ts +6 -6
  34. package/lib/src/managers/ConsentManager.d.ts +130 -0
  35. package/lib/src/managers/DatabaseManager.d.ts +6 -0
  36. package/lib/src/managers/DatabaseManager.js +25 -15
  37. package/lib/src/managers/EmailManager.d.ts +32 -5
  38. package/lib/src/managers/ExportManager.d.ts +62 -0
  39. package/lib/src/managers/ExportManager.js +147 -2
  40. package/lib/src/managers/FootnoteManager.d.ts +6 -6
  41. package/lib/src/managers/ImportManager.d.ts +590 -0
  42. package/lib/src/managers/OrganizationManager.d.ts +8 -6
  43. package/lib/src/managers/PageManager.d.ts +61 -1
  44. package/lib/src/managers/PersonManager.d.ts +8 -6
  45. package/lib/src/managers/RoleManager.d.ts +6 -8
  46. package/lib/src/providers/BaseAttachmentProvider.d.ts +4 -4
  47. package/lib/src/providers/BaseBackupProvider.d.ts +77 -0
  48. package/lib/src/providers/BaseBackupProvider.js +52 -0
  49. package/lib/src/providers/BasicAttachmentProvider.d.ts +4 -4
  50. package/lib/src/providers/FileBackupProvider.d.ts +34 -0
  51. package/lib/src/providers/FileBackupProvider.js +116 -0
  52. package/lib/src/providers/FileConsentProvider.d.ts +18 -0
  53. package/lib/src/providers/SqliteDatabaseProvider.d.ts +2 -2
  54. package/lib/src/providers/SqliteDatabaseProvider.js +12 -11
  55. package/lib/src/providers/attachmentFilesBackup.d.ts +5 -4
  56. package/lib/src/providers/databaseMigrations.js +45 -0
  57. package/lib/src/security/PolicyDecisionPoint.d.ts +14 -0
  58. package/lib/src/security/PolicyInformationPoint.d.ts +25 -2
  59. package/lib/src/types/Config.d.ts +2 -0
  60. package/lib/src/types/Consent.d.ts +90 -0
  61. package/lib/src/types/express.d.ts +6 -0
  62. package/lib/src/utils/addonHost.d.ts +45 -2
  63. package/lib/src/utils/addonHost.js +39 -3
  64. package/lib/src/utils/auditEventNames.generated.d.ts +20 -0
  65. package/lib/src/utils/auditEventNames.generated.js +20 -0
  66. package/lib/src/utils/backupCrypto.js +275 -0
  67. package/lib/src/utils/backupDocument.d.ts +36 -0
  68. package/lib/src/utils/backupDocument.js +99 -0
  69. package/lib/src/utils/browserDescription.d.ts +11 -0
  70. package/lib/src/utils/directoryBackup.d.ts +48 -48
  71. package/lib/src/utils/directoryBackup.js +291 -0
  72. package/lib/src/utils/frontmatterAccess.d.ts +54 -11
  73. package/lib/src/utils/importRunSummary.d.ts +64 -0
  74. package/lib/src/utils/instanceEnvBackup.d.ts +46 -0
  75. package/lib/src/utils/instanceEnvBackup.js +120 -0
  76. package/lib/src/utils/instanceEnvSecret.js +79 -0
  77. package/lib/src/utils/ipLocation.d.ts +11 -0
  78. package/lib/src/utils/ncmNotify.d.ts +30 -0
  79. package/lib/src/utils/privateStoreImport.d.ts +58 -0
  80. package/lib/src/utils/schedule.js +171 -0
  81. package/lib/src/utils/secretFileMode.js +78 -0
  82. package/lib/src/utils/siteRestore.d.ts +13 -16
  83. package/lib/src/utils/siteRestore.js +33 -6
  84. package/lib/src/utils/treeArchive.d.ts +2 -0
  85. package/lib/src/utils/treeArchive.js +242 -0
  86. package/lib/src/utils/version.js +408 -0
  87. package/lib/src/utils/zipArchive.d.ts +71 -0
  88. package/package.json +6 -1
  89. package/lib/src/utils/administrators.d.ts +0 -21
package/README.md CHANGED
@@ -11,7 +11,7 @@ import type { AddonModule, AddonHost, WikiEngine } from '@jwilleke/ngdpbase-type
11
11
  import { buildAddonHost, openAddonTestDatabase, toCsv, formatters } from '@jwilleke/ngdpbase-types/testing';
12
12
  ```
13
13
 
14
- - The main entry is types only. Importing it at run time fails, on purpose.
14
+ - The main entry is types only. Importing it at run time fails, on purpose. An add-on takes core's values from `host` (`AddonHost`: the request and audit helpers, the logger, `guardedFetch`, the egress tools, `isAtLeast`) and a manager's job from `engine.getManager` (such as `PageManager.pageUrl` or `PolicyDecisionPoint.usersHolding`); it never imports core's values ([#1813](https://github.com/jwilleke/ngdpbase/issues/1813)).
15
15
  - `/testing` is for tests. `buildAddonHost(slug, defaults, setEnabledForPerson?, engine?)` is the real host; `host.setEnabledForPerson` calls the setter you pass (it refuses without one), and `engine` names the system principal `host.systemContext(reason)` uses (core's fallback without one). `openAddonTestDatabase(owner, { migrations, ledgerTable })` is the handle `openAddonDatabase` returns, in memory. `toCsv` is `ExportManager.toCsv`. `formatters` is a plugin's `context.formatters`.
16
16
 
17
17
  The add-on guide is [docs/guides/addons-developer-guide.md](https://github.com/jwilleke/ngdpbase/blob/master/docs/guides/addons-developer-guide.md).
@@ -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,8 +28,15 @@ 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';
34
+ export type { default as PageManager } from '../../src/managers/PageManager.js';
35
+ export type { PageAction } from '../../src/utils/pageUrl.js';
36
+ export type { default as ConsentManager, ConsentScopeStatus } from '../../src/managers/ConsentManager.js';
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, IceUpload, IceSource, IceRunRequest, IceRunReport, IceFileReport, IceFileStatus } from '../../src/ice/types.js';
33
40
  export type { default as PersonManager, LinkOfferOutcome } from '../../src/managers/PersonManager.js';
34
41
  export type { default as OrganizationManager } from '../../src/managers/OrganizationManager.js';
35
42
  export type { Person, PersonAffiliation, PersonInput, PersonUpdate } from '../../src/types/Person.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
@@ -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
+ }
@@ -0,0 +1,113 @@
1
+ /** Page sizes Chromium knows by name. */
2
+ export declare const PDF_PAGE_SIZES: readonly ["Letter", "Legal", "Tabloid", "Ledger", "A0", "A1", "A2", "A3", "A4", "A5", "A6"];
3
+ export type PdfPageSize = (typeof PDF_PAGE_SIZES)[number];
4
+ export interface PdfMargin {
5
+ top?: string;
6
+ right?: string;
7
+ bottom?: string;
8
+ left?: string;
9
+ }
10
+ /**
11
+ * What a caller may set. An `@page` rule in the HTML (size, margin) wins over
12
+ * these, as it does when a browser prints.
13
+ */
14
+ export interface PdfOptions {
15
+ /** Page size; default `ngdpbase.export.pdf.page-size`. */
16
+ format?: PdfPageSize;
17
+ /** Page margins, each a CSS length (`'20mm'`, `'0.5in'`); default none. */
18
+ margin?: PdfMargin;
19
+ /** Print background colours and images; default `true`. */
20
+ printBackground?: boolean;
21
+ }
22
+ /** Options after validation and defaults. */
23
+ export interface ResolvedPdfOptions {
24
+ format: PdfPageSize;
25
+ margin: PdfMargin;
26
+ printBackground: boolean;
27
+ }
28
+ /** One local Chromium render, as the child receives it. */
29
+ export interface PdfJob {
30
+ kind: 'render';
31
+ html: string;
32
+ executablePath: string;
33
+ options: ResolvedPdfOptions;
34
+ timeoutMs: number;
35
+ }
36
+ /** A PDF made elsewhere, to normalise in the child. */
37
+ export interface PdfNormalizeJob {
38
+ kind: 'normalize';
39
+ pdf: Uint8Array;
40
+ timeoutMs: number;
41
+ }
42
+ /** Anything the child is asked to do. */
43
+ export type PdfChildJob = PdfJob | PdfNormalizeJob;
44
+ /** What the child sends back. `launch` means Chromium could not start: the engine is unavailable. */
45
+ export type PdfJobResult = {
46
+ ok: true;
47
+ pdf: Uint8Array;
48
+ } | {
49
+ ok: false;
50
+ stage: 'launch' | 'render';
51
+ error: string;
52
+ };
53
+ /**
54
+ * No PDF engine: `playwright-core` is not installed, no Chromium was found, or
55
+ * it could not start. An add-on catches it by `code` (it cannot import core
56
+ * values) and falls back to HTML.
57
+ */
58
+ export declare class PdfUnavailableError extends Error {
59
+ readonly code = "PDF_UNAVAILABLE";
60
+ constructor(message: string, options?: ErrorOptions);
61
+ }
62
+ /**
63
+ * Validate a caller's options and fill the defaults. Throws on anything
64
+ * Chromium would misread, so a typo is an error rather than a different page.
65
+ *
66
+ * @param options - The caller's options
67
+ * @param defaultFormat - `ngdpbase.export.pdf.page-size`
68
+ */
69
+ export declare function resolvePdfOptions(options: PdfOptions | undefined, defaultFormat: string): ResolvedPdfOptions;
70
+ /** Where Chromium or Chrome usually is, per platform. Checked after the configured path and Playwright's own. */
71
+ export declare const SYSTEM_CHROMIUM_PATHS: Record<string, string[]>;
72
+ export type ChromiumResolution = {
73
+ ok: true;
74
+ executablePath: string;
75
+ source: 'configured' | 'playwright' | 'system';
76
+ } | {
77
+ ok: false;
78
+ reason: string;
79
+ };
80
+ /** Hooks for tests; a site passes nothing. */
81
+ export interface ResolveDeps {
82
+ exists?: (p: string) => boolean;
83
+ platform?: string;
84
+ playwrightInstalled?: () => boolean;
85
+ /** Playwright's own Chromium, or null when it cannot say. */
86
+ playwrightChromium?: () => Promise<string | null>;
87
+ }
88
+ /** Whether `playwright-core` is installed (it is an optional dependency). */
89
+ export declare function playwrightInstalled(): boolean;
90
+ /**
91
+ * Find a Chromium to render with. Read on every call, so installing Chromium
92
+ * or setting the path takes effect without a restart.
93
+ *
94
+ * @param configuredPath - `ngdpbase.export.pdf.provider.local-chrome.chromium-path` ('' = find one)
95
+ */
96
+ export declare function resolveChromium(configuredPath: string, deps?: ResolveDeps): Promise<ChromiumResolution>;
97
+ export declare function childEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
98
+ /**
99
+ * Render one job in a child process. Resolves with the child's answer; rejects
100
+ * only when the child timed out, crashed or never answered.
101
+ *
102
+ * @param job - The render
103
+ */
104
+ export declare function renderPdfInChild(job: PdfJob): Promise<PdfJobResult>;
105
+ /**
106
+ * Normalise a PDF in a child process (`normalizePdf`, the same pass a local
107
+ * render gets). Rejects when the bytes are not a PDF pdf-lib can read, or the
108
+ * child times out.
109
+ *
110
+ * @param pdf - A PDF a PDF service returned
111
+ * @param timeoutMs - The child is killed after this
112
+ */
113
+ export declare function normalizePdfInChild(pdf: Uint8Array, timeoutMs: number): Promise<Uint8Array>;