polotno-node 3.14.0 → 3.15.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/README.md CHANGED
@@ -80,8 +80,10 @@ const instance = await createInstance({
80
80
  // for full Chrome. Ignored if you pass `browser`.
81
81
  headless: true,
82
82
 
83
- // useFontCache - cache Google Fonts responses (in memory + on disk in the
84
- // OS temp folder, shared across processes) and serve them to every page.
83
+ // useFontCache - cache Google Fonts responses (in memory + on disk in a
84
+ // private folder in the OS temp folder, shared across this user's processes)
85
+ // and serve them to every page. Only https responses from
86
+ // fonts.googleapis.com and fonts.gstatic.com are cached.
85
87
  // Every page starts with an empty HTTP cache, so without it each render
86
88
  // re-downloads the same fonts and bursts of parallel renders may get
87
89
  // throttled by Google Fonts (fonts time out and text falls back to a
@@ -93,6 +95,9 @@ const instance = await createInstance({
93
95
  // - Modify headers like User-Agent to access protected image resources
94
96
  // - Add authentication tokens or credentials to requests
95
97
  // - Log or monitor network traffic
98
+ // It runs after polotno-node's own handling: local files and cached fonts
99
+ // are answered before it, and requests the URL policy blocks never reach it
100
+ // (see "Restricted hosts" and "Local files").
96
101
  requestInterceptor: (request) => {
97
102
  const targetUrl = request.url();
98
103
  if (/\.(png|jpe?g)(\?|$)/i.test(targetUrl)) {
@@ -321,7 +326,7 @@ Native audio processing and finalization can also take time without intermediate
321
326
  Custom clients with older video exporters can report fewer stages.
322
327
 
323
328
  The inactivity timer runs in Node, so it can report a blocked renderer without another CDP request.
324
- It cannot fire while Node itself is blocked, including during synchronous media conversion or base64 conversion.
329
+ It cannot fire while Node itself is blocked, for example during base64 conversion.
325
330
  An independent parent-process monitor is necessary to detect that condition during the block.
326
331
  Existing `protocolTimeout` and navigation timeouts still apply.
327
332
 
@@ -360,7 +365,7 @@ By default, when exporting video and **only** when using the default local clien
360
365
  - Download all **remote** video elements (`type: 'video'`) and `json.audios` sources into a temporary folder
361
366
  - Deduplicate downloads (same URL is downloaded once)
362
367
  - Probe downloaded remote videos with `ffprobe`
363
- - Conditionally normalize incompatible downloaded videos into MP4 with H.264 video, `yuv420p`, and AAC audio for better Chromium render compatibility
368
+ - Conditionally normalize incompatible downloaded videos into MP4 with H.264 video, `yuv420p`, and AAC audio for better Chromium render compatibility. `ffprobe` and `ffmpeg` run as child processes and do not block Node. Each run is stopped at a time limit: a probe after 30 seconds (the video is then used as it is), and a conversion after 10 minutes (the export then fails).
364
369
  - Rewrite `src` to a URL that `polotno-node` answers from the temporary folder
365
370
 
366
371
  For best compatibility, use MP4 with H.264 (`yuv420p`) video and AAC audio.
@@ -381,17 +386,42 @@ A `file:` source anywhere in a design — an image, an SVG, a video, an audio tr
381
386
 
382
387
  Because the bytes come from Node, a browser running somewhere else (`browser: await puppeteer.connect(...)`, browserless) can render a design that names files on this machine.
383
388
 
384
- ### Restricted media hosts
389
+ Local files are served before, and without, your `requestInterceptor`: it never sees these reads, so it cannot block them. If you render designs from users you do not trust, turn local files off:
385
390
 
386
- Server-side downloads block cloud-metadata addresses (e.g. `169.254.169.254`) by default. Normal URLs, `localhost`, and private hosts still work. To restrict further:
391
+ ```js
392
+ await instance.jsonToImageBase64(json, { allowLocalFiles: false });
393
+ ```
394
+
395
+ With `allowLocalFiles: false`, a design that names any `file:` source is rejected with a `DESIGN_INVALID` error before rendering. Each `details.errors` entry names the field. The remote media that a video export downloads for itself is still served.
396
+
397
+ Whatever `allowLocalFiles` is set to, the render page cannot read files directly: a `file:` request from the page outside the client's own folder is refused. A `file:` URL that a script inside `instance.run()` loads itself (not through the design) is refused too: the script gets a `403` whose text explains why, and the refusal is logged with `console.warn`. Put the `file:` URL in the design instead, or read the file in Node and pass its contents in.
398
+
399
+ ### Restricted hosts
400
+
401
+ Every http(s) request the render makes is checked first: each request the render page sends (images, SVGs, fonts, media, and each redirect hop), and the server-side media downloads of a video export. Cloud-metadata addresses (e.g. `169.254.169.254`) are blocked by default. Normal URLs, `localhost`, and private hosts still work. A blocked asset fails like any asset that does not load (for example `IMAGE_FAILED`, unless `skipImageError` is set). The check runs before your `requestInterceptor`. To restrict further, in any export method:
387
402
 
388
403
  ```js
389
- await instance.jsonToVideoFile(json, 'out.mp4', {
404
+ await instance.jsonToImageBase64(json, {
390
405
  blockPrivateNetwork: true, // also block loopback / private IPs
391
406
  fetchGuard: (url) => isAllowed(url), // custom allow rule (checked per redirect hop)
392
407
  });
393
408
  ```
394
409
 
410
+ A `fetchGuard` replaces the built-in checks for the design's URLs. The client's own services skip both options, so a strict guard cannot cost the render its license or its fonts: the Polotno license check (`https://api.polotno.com`) and Google Fonts (`https://fonts.googleapis.com`, `https://fonts.gstatic.com`).
411
+
412
+ Each blocked request is logged with `console.warn`, naming the URL and the rule that refused it.
413
+
414
+ The guard is also published for your own fetches of design URLs:
415
+
416
+ ```js
417
+ import { assertUrlAllowed, guardedFetch } from 'polotno-node/ssrf-guard';
418
+
419
+ await assertUrlAllowed(url, { blockPrivateNetwork: true }); // throws FETCH_FAILED
420
+ const res = await guardedFetch(url, { blockPrivateNetwork: true }); // checks every redirect hop
421
+ ```
422
+
423
+ Hostnames are resolved and every address is checked. A hostile DNS server can still answer differently to the browser (DNS rebinding); to close that, also restrict egress at the network level.
424
+
395
425
  ### `attrs.assetLoadTimeout`
396
426
 
397
427
  You can add `assetLoadTimeout` attribute to `attrs` object. It will be used to set timeout for loading assets. By default it is 30000ms.