diff --git a/.gitattributes b/.gitattributes index cdbaf3a..4f75624 100644 --- a/.gitattributes +++ b/.gitattributes @@ -175,3 +175,10 @@ docs/assets/styling-notebook-alpha11/*.pdf binary # React starter baselines are verified by exact PDF bytes. docs/assets/react-starter/baseline/*.pdf binary + +# Keep the locked Workers download reproducible and preserve exact PDF/PNG bytes. +examples/cloudflare-pdf/** text eol=lf +docs/assets/cloudflare-starter/** -text +docs/assets/cloudflare-starter/*.pdf binary +docs/assets/cloudflare-starter/*.png binary +docs/assets/cloudflare-starter/*.zip binary diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bdcc20f..9e0cc5c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -872,6 +872,22 @@ jobs: retention-days: 14 - name: Build and exercise the downloadable Next.js starter run: python tools/verify_nextjs_starter.py + - name: Verify the locked Workers invoice download in local workerd + run: | + python -m pip install pypdf==6.19.0 pillow==12.3.0 + python tools/build_cloudflare_starter.py + git diff --exit-code -- docs/assets/cloudflare-starter/project.zip docs/assets/cloudflare-starter/source.json + python tools/verify_cloudflare_starter.py + - uses: actions/upload-artifact@v4 + if: always() + with: + name: cloudflare-starter-verification + path: | + output/cloudflare-starter-verification/*.json + output/cloudflare-starter-verification/*.log + output/cloudflare-starter-verification/*.pdf + output/cloudflare-starter-verification/*.png + retention-days: 14 - uses: actions/upload-artifact@v4 if: always() with: diff --git a/docs/assets/cloudflare-starter/invoice.pdf b/docs/assets/cloudflare-starter/invoice.pdf new file mode 100644 index 0000000..0cf628a Binary files /dev/null and b/docs/assets/cloudflare-starter/invoice.pdf differ diff --git a/docs/assets/cloudflare-starter/invoice.png b/docs/assets/cloudflare-starter/invoice.png new file mode 100644 index 0000000..4596057 Binary files /dev/null and b/docs/assets/cloudflare-starter/invoice.png differ diff --git a/docs/assets/cloudflare-starter/long-invoice.pdf b/docs/assets/cloudflare-starter/long-invoice.pdf new file mode 100644 index 0000000..7fd6d6b Binary files /dev/null and b/docs/assets/cloudflare-starter/long-invoice.pdf differ diff --git a/docs/assets/cloudflare-starter/project.zip b/docs/assets/cloudflare-starter/project.zip new file mode 100644 index 0000000..6483b72 Binary files /dev/null and b/docs/assets/cloudflare-starter/project.zip differ diff --git a/docs/assets/cloudflare-starter/source.json b/docs/assets/cloudflare-starter/source.json new file mode 100644 index 0000000..eaddfeb --- /dev/null +++ b/docs/assets/cloudflare-starter/source.json @@ -0,0 +1,113 @@ +{ + "schema": "fullbleed.cloudflare-starter.v1", + "prefix": "fullbleed-cloudflare-invoice/", + "packageVersion": "0.8.0", + "engineVersion": "2.5.29", + "wrangler": "4.151.3", + "sourceCommit": "e9582e417e8cbe49e41aabd0c695524920a1840d", + "sourceRepository": "https://github.com/fullbleed-engine/fullbleed-node", + "release": "https://github.com/fullbleed-engine/fullbleed-node/releases/tag/v0.8.0", + "packageIntegrity": "sha512-olEU8ARb8i5ASStXX7uGAZ2RaGaKj2uL8cL81su+M46xo3AdxcGL3j8SCcaXEmTJUzKE0o5E6djykPTbC0u2FA==", + "packageSha256": "86af460db1f29c94e65daff3563f9f64f0f52e0af47f02adf3ff39cb2ece1691", + "wasmSha256": "bbcf9b09a60a38d9a884f227fd7f7d31e17e4d611c91027775059f05e2bb46f2", + "localOnly": true, + "hostedVerified": false, + "freePlanVerified": false, + "sourceNote": "SDK examples/cloudflare source; npm lockfile and this release record added for the downloadable project.", + "files": [ + { + "path": ".gitignore", + "bytes": 55, + "sha256": "7a1f0472a0d8ee63343cb58345d9af19d1e71310d9c696d9d1578e7bc743262b" + }, + { + "path": "LICENSE", + "bytes": 1079, + "sha256": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9" + }, + { + "path": "README.md", + "bytes": 2919, + "sha256": "503a8caf5cd39a4f310badcf417b006cdfc874f71be42fde5943aef3d57db34c" + }, + { + "path": "package-lock.json", + "bytes": 50461, + "sha256": "07818436d4e5486c1694bc38652a792e0093448cd12baf63bd718f4c01a8308b" + }, + { + "path": "package.json", + "bytes": 499, + "sha256": "fe2c82d9f68ba4fe91f69ba3f32a3dd21cb455f72ca8820929e889924490795c" + }, + { + "path": "release.json", + "bytes": 804, + "sha256": "57521c01fcbcc6eed69c6e7ad8838e0e1b4f44a4d6be304c8229084d1d9d92ae" + }, + { + "path": "sample.json", + "bytes": 733, + "sha256": "82a94cc4f15bd26d7060bc201a6b3f7ff36307875158b9b0a7e8aeb5ab2cbfb9" + }, + { + "path": "src/document.mjs", + "bytes": 3509, + "sha256": "72153fd3fbc3ca1183e801fdc53a2a9ddf7a9f4c046f7b6c5bea8bff63bf7b06" + }, + { + "path": "src/handler.mjs", + "bytes": 5997, + "sha256": "8b2de5cbddb531481baaf5adcd6865f11e9d69fbff5b95c7f294cfed95f15e4e" + }, + { + "path": "src/index.mjs", + "bytes": 173, + "sha256": "a955aabac33e5524ac96c55213264ecf338d53486d45362b58253ae67177727e" + }, + { + "path": "src/styles.mjs", + "bytes": 3219, + "sha256": "1cd818aec8cf2e9def15b51ce043bfdfcfc06626fd9df63372f34f9a43160a59" + }, + { + "path": "test/fixtures.mjs", + "bytes": 2628, + "sha256": "2292c21a378d8afc466c2c9c8cd541322f71dcf8cfdd9b8bdabafa9d71fe17f0" + }, + { + "path": "test/request.test.mjs", + "bytes": 4179, + "sha256": "7f180dd62aa03231d8fd2d97a72cb9f14b5ddbc97e97165c31562f6256c3d951" + }, + { + "path": "test/worker.mjs", + "bytes": 3156, + "sha256": "e2355fc855877fc66e35554be2e72bc29412141fa53b4aeb59313c388146f1dc" + }, + { + "path": "wrangler.json", + "bytes": 251, + "sha256": "ddf7b4df5ae09c70ab9d3be8b1a8026b6f8951971ee9602b2dce88614200c3bf" + } + ], + "zipBytes": 25246, + "zipSha256": "1aa32c075f65844fba69bb0909985c60d35e40ef488c2f889bbae2e9303d9ccd", + "assets": [ + { + "file": "invoice.pdf", + "bytes": 103212, + "sha256": "17437a93f960b80d16bbbe92f573c7c62a9d9d39eca6899fb9b642ebfb9feca3" + }, + { + "file": "invoice.png", + "bytes": 100558, + "sha256": "4714cda69ce852490602986efc07793de605bd6ca92172e21ab91243268bfb13" + }, + { + "file": "long-invoice.pdf", + "bytes": 114406, + "sha256": "2a36293e76043cb65cb2a208c28f43c526299ed17e0972ba0e0ccbff1ff071a5" + } + ] +} diff --git a/docs/assets/cloudflare-starter/verification.json b/docs/assets/cloudflare-starter/verification.json new file mode 100644 index 0000000..14a30fc --- /dev/null +++ b/docs/assets/cloudflare-starter/verification.json @@ -0,0 +1,185 @@ +{ + "ok": true, + "checkedAt": "2026-10-21T00:20:10.418993+00:00", + "packageVersion": "0.8.0", + "engineVersion": "2.5.29", + "sourceCommit": "e9582e417e8cbe49e41aabd0c695524920a1840d", + "zipSha256": "1aa32c075f65844fba69bb0909985c60d35e40ef488c2f889bbae2e9303d9ccd", + "packageIntegrity": "sha512-olEU8ARb8i5ASStXX7uGAZ2RaGaKj2uL8cL81su+M46xo3AdxcGL3j8SCcaXEmTJUzKE0o5E6djykPTbC0u2FA==", + "platform": "win32", + "wrangler": "4.151.3", + "workerd": "1.20261019.1", + "commands": [ + { + "name": "install", + "exitCode": 0 + }, + { + "name": "input-tests", + "exitCode": 0 + }, + { + "name": "bundle", + "exitCode": 0 + }, + { + "name": "http-tests", + "exitCode": 0 + }, + { + "name": "node-preview", + "exitCode": 0 + } + ], + "records": [ + { + "name": "sample", + "pdf": "17437a93f960b80d16bbbe92f573c7c62a9d9d39eca6899fb9b642ebfb9feca3", + "bytes": 103212, + "minPages": 1, + "maxPages": 1, + "text": [ + "Maple & Finch", + "$8,100.00", + "01 October 2026" + ], + "trackedText": [ + "NS-1042" + ], + "pages": 1 + }, + { + "name": "minimum", + "pdf": "825a084cd377f4ed6b3cfe7e378be636e2cca2675b8f1bab467966e4418ec4c8", + "bytes": 105334, + "minPages": 1, + "maxPages": 1, + "text": [ + "$0.00", + "This invoice has been settled." + ], + "trackedText": [ + "NS-1043" + ], + "pages": 1 + }, + { + "name": "maximum-total", + "pdf": "63c2bdd3d78a0656cff4b6919c302a4ce0385621ea28acc6fbb96426324b36ff", + "bytes": 102597, + "minPages": 1, + "maxPages": 1, + "text": [ + "$999,999.99" + ], + "trackedText": [ + "NS-1044" + ], + "pages": 1 + }, + { + "name": "long", + "pdf": "2a36293e76043cb65cb2a208c28f43c526299ed17e0972ba0e0ccbff1ff071a5", + "bytes": 114406, + "minPages": 3, + "maxPages": 10, + "text": [ + "Work package 30:", + "$9,000.00" + ], + "trackedText": [ + "NS-1045" + ], + "pages": 5 + }, + { + "name": "escaped", + "pdf": "6f1191f16e4f4023cc35d9426cd26ebb7ee1e37a2da6209df3105bcfd19b2fbf", + "bytes": 103773, + "minPages": 1, + "maxPages": 2, + "text": [ + " & Partners", + "$8,100.00" + ], + "trackedText": [ + "NS-1046" + ], + "pages": 1 + }, + { + "name": "after-errors", + "pdf": "17437a93f960b80d16bbbe92f573c7c62a9d9d39eca6899fb9b642ebfb9feca3", + "bytes": 103212, + "minPages": 1, + "maxPages": 1, + "text": [ + "Maple & Finch", + "$8,100.00", + "01 October 2026" + ], + "trackedText": [ + "NS-1042" + ], + "pages": 1 + } + ], + "errors": [ + { + "name": "json", + "status": 400, + "code": "INVALID_JSON" + }, + { + "name": "content-type", + "status": 415, + "code": "JSON_REQUIRED" + }, + { + "name": "body-limit", + "status": 413, + "code": "BODY_TOO_LARGE" + }, + { + "name": "number", + "status": 400, + "code": "INVALID_INVOICE" + }, + { + "name": "date", + "status": 400, + "code": "INVALID_INVOICE" + }, + { + "name": "money", + "status": 400, + "code": "INVALID_INVOICE" + }, + { + "name": "total", + "status": 400, + "code": "INVALID_INVOICE" + }, + { + "name": "row-limit", + "status": 400, + "code": "INVALID_INVOICE" + }, + { + "name": "unknown", + "status": 400, + "code": "INVALID_INVOICE" + }, + { + "name": "glyph", + "status": 422, + "code": "MISSING_GLYPHS" + } + ], + "recovery": true, + "serverStopped": true, + "previewMatches": true, + "localOnly": true, + "hostedVerified": false, + "freePlanVerified": false +} diff --git a/docs/examples.md b/docs/examples.md index 0c6a455..2e93d88 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -131,6 +131,7 @@ Start with the [invoice guide](guides/invoices.md), or The Python starter also includes a [tested Docker image](guides/python-docker.md). For Node applications, download the [six-invoice queue project](guides/node-render-queue.md) with editable HTML/CSS and limits on active and waiting renders. +For Cloudflare Workers, try the [invoice endpoint with local runtime checks](guides/cloudflare-pdf.md). For C# web applications, try the [ASP.NET Core download starter](guides/aspnet-pdf.md). For Rust applications, use the [Axum PDF download starter](guides/axum-pdf.md) with an editable invoice template and bounded rendering work. diff --git a/docs/getting-started/node.md b/docs/getting-started/node.md index 65be4a3..9701247 100644 --- a/docs/getting-started/node.md +++ b/docs/getting-started/node.md @@ -28,6 +28,8 @@ for an explicit face. Review appearance and line breaks when upgrading; the [font-family guide](../engine/font-registration.md) explains font selection. For rendering inside a web page, [use the browser entry and starter](../guides/browser-pdf.md). +For Cloudflare Workers, [use the Workers adapter and invoice endpoint](../guides/cloudflare-pdf.md) +introduced in package 0.8.0. This guide's invoice download remains pinned to 0.7.1. ## Create your first PDF diff --git a/docs/guides/cloudflare-pdf.md b/docs/guides/cloudflare-pdf.md new file mode 100644 index 0000000..5ee9577 --- /dev/null +++ b/docs/guides/cloudflare-pdf.md @@ -0,0 +1,154 @@ +--- +title: Generate PDFs in Cloudflare Workers with HTML and CSS +description: Render PDF invoices inside a Cloudflare Worker with Fullbleed's bundled WebAssembly engine and fonts. Download an editable invoice endpoint with local workerd tests. +--- +# Generate PDFs in Cloudflare Workers + +Import `renderPdf` from `fullbleed/cloudflare` to turn static HTML/CSS into PDF +bytes inside a Worker. Wrangler bundles the engine and fonts with your code. +The renderer uses an in-memory filesystem; document content does not need to +leave the Worker for a PDF service. + +The adapter is verified in **local workerd on Windows and Linux**. Hosted CPU, +memory, startup behavior and free-plan suitability have not been qualified. +Measure representative documents in your deployment before choosing a plan or +workload limits. [Cloudflare's platform limits](https://developers.cloudflare.com/workers/platform/limits/) +apply to the entire isolate, including WebAssembly memory. + +## Run a complete invoice endpoint + +[Download the Workers invoice starter](../assets/cloudflare-starter/project.zip){ .md-button .md-button--primary } +[Open the generated PDF](../assets/cloudflare-starter/invoice.pdf){ .md-button } +[See a five-page invoice](../assets/cloudflare-starter/long-invoice.pdf){ .md-button } + +[](../assets/cloudflare-starter/invoice.pdf) + +The project uses **npm package 0.8.0**, **engine 2.5.29**, and **Wrangler 4.151.3**. +It accepts structured invoice JSON, applies an editable template, and returns a +PDF attachment. The sample organization and customer are fictional. + +Extract the ZIP and, with Node.js 22 or newer, run: + +```sh +npm ci --ignore-scripts +npm test +npm run build +npm run dev +``` + +`build` produces a local bundle without deploying. `dev` starts the local Worker +at `http://127.0.0.1:8787`; no Cloudflare account is needed. In a second terminal: + +```sh +curl --fail-with-body http://127.0.0.1:8787/invoice \ + -H "Content-Type: application/json" \ + --data-binary @sample.json --output invoice.pdf +``` + +Open `invoice.pdf`. On Windows PowerShell, use `curl.exe` and put that command on +one line. Run `npm run test:worker` while the server is running to check sample, +zero-total, maximum-total, long, escaped-text and invalid invoices. The test uses +synthetic data and saves its PDFs in `output/`. + +## Use your own HTML and CSS + +The adapter accepts the same static document sources as the Node renderer: + +```js +import { renderPdf } from 'fullbleed/cloudflare'; + +export default { + async fetch() { + const result = await renderPdf({ + html: '
Design and delivery: $1,870.00
', + css: '@page { size: A4; margin: 20mm } h1 { color: #173b35 }', + maxPages: 5, + }); + return new Response(result.pdf, { + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': 'attachment; filename="invoice.pdf"', + 'Cache-Control': 'no-store', + }, + }); + }, +}; +``` + +This is a complete Worker entry file. Keep document input behind your +application's authorization and workload limits. HTML scripts do not +run, and document URLs do not fetch remote resources. Supply images and custom +fonts as byte arrays through `assets` and `fonts`. + +Add this rule to an existing Wrangler configuration so the bundled TrueType +fonts become data modules: + +```json +{ + "rules": [{ "type": "Data", "globs": ["**/*.ttf"], "fallthrough": false }] +} +``` + +Wrangler already recognizes the engine's compiled `.wasm` import. The download +includes the complete configuration; see Cloudflare's +[WebAssembly module documentation](https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/) +for the runtime mechanism. + +## Edit the starter's design + +| File | What to change | +| --- | --- | +| `sample.json` | Customer, reference, dates, paid status, and line items. | +| `src/document.mjs` | Headings, labels, document sections, and table rows. | +| `src/styles.mjs` | Page geometry, fonts, colors, spacing, and class rules. | +| `src/handler.mjs` | Input schema, limits, and download headers. | +| `src/index.mjs` | The platform renderer import. | + +This template uses [JavaScript function components](javascript-components.md). +`className` adds an HTML class, and a matching rule in the document's +`StyleSheet` supplies its styling. `Invoice({ data }).renderPdf(renderPdf)` passes +the generated HTML and CSS to the Workers renderer. You can replace that call +with your own HTML/CSS template. + +For standalone HTML, use `Invoice({ data }).toHtml({ includeStyles: true })`. +The embedded stylesheet also renders to PDF with `css: ''` and +`defaultStyles: false`. `includeStyles: false` exports HTML without styles. + +The sample accepts 1–30 rows and at most 32 KiB of JSON. Rates are nonnegative +integer cents, quantities are integers, dates use `YYYY-MM-DD`, and the total is +limited to $999,999.99. It uses USD and does not calculate tax. Tables flow across +pages, with a ten-page render limit. Change these bounds together with layout +tests for your own documents. + +## Connect it to your application + +The starter disables `workers_dev` and `preview_urls` and defines no public +route. Its local endpoint has no authentication. Configure an authenticated +application route or private service binding before deployment, apply rate and +concurrency limits, and keep invoice data out of logs. The handler stores no +invoices or PDFs and returns `Cache-Control: no-store`. + +Rendering is synchronous inside the isolate and returns a settled Promise. +Each call gets a fresh engine instance, but that does not guarantee that previous +allocations have already been garbage-collected. `timeoutMs`, `signal`, and +`isolation` are rejected on this entry. Cloudflare enforces CPU and memory limits; +page and input limits are not guarantees about either resource. + +The adapter defaults to 20 pages, caps HTML/CSS at 1,000,000 UTF-8 bytes, and +caps caller-provided fonts and assets at 8 MiB combined. Optional previews use +additional memory. These are ordinary PDFs; this entry does not expose PDF/A, +PDF/UA, PDF/X, or VDP profiles. + +## Verification and source + +The release checks the installed npm tarball in real local workerd, compares +PDF and preview bytes with Node, and exercises custom fonts, SVG assets, +components, embedded styles, input limits, missing glyphs and failure recovery. +The downloadable project is also installed from its lockfile and exercised over +local HTTP. Independent readers check the retained PDF contents, and the +sample and long-invoice layouts are visually reviewed. + +[Download source manifest](../assets/cloudflare-starter/source.json) · +[Local verification record](../assets/cloudflare-starter/verification.json) · +[SDK guide](https://github.com/fullbleed-engine/fullbleed-node/blob/v0.8.0/docs/cloudflare.md) · +[Release evidence](https://github.com/fullbleed-engine/fullbleed-node/releases/tag/v0.8.0) diff --git a/examples/cloudflare-pdf/.gitignore b/examples/cloudflare-pdf/.gitignore new file mode 100644 index 0000000..54434c7 --- /dev/null +++ b/examples/cloudflare-pdf/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.wrangler/ +dist/ +output/ +.dev.vars +.env* diff --git a/examples/cloudflare-pdf/LICENSE b/examples/cloudflare-pdf/LICENSE new file mode 100644 index 0000000..4c99bbd --- /dev/null +++ b/examples/cloudflare-pdf/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Fullbleed Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/cloudflare-pdf/README.md b/examples/cloudflare-pdf/README.md new file mode 100644 index 0000000..d90ac27 --- /dev/null +++ b/examples/cloudflare-pdf/README.md @@ -0,0 +1,67 @@ +# A Fullbleed invoice Worker + +Send structured invoice JSON and receive a styled PDF attachment. This starter +uses `fullbleed/cloudflare` with a fixed, editable component template, bundled +fonts, integer-cent totals, and flowing tables. It stores no invoices or PDFs. +Northstar Studio and the sample customer are fictional. + +## Run locally + +Use Node.js 22 or newer: + +```sh +npm install +npm test +npm run build +npm run dev +``` + +In a second terminal: + +```sh +curl --fail-with-body http://127.0.0.1:8787/invoice \ + -H "Content-Type: application/json" \ + --data-binary @sample.json --output invoice.pdf +npm run test:worker +``` + +In Windows PowerShell, use `curl.exe` and place the command on one line. +`test:worker` submits synthetic invoices and retains PDFs under `output/`. +Set `FULLBLEED_WORKER_URL` to change the local test address. + +## Change the design + +- `src/document.mjs`: text, function components, fields, and table rows. +- `src/styles.mjs`: page size, margins, colors, typography, and class rules. +- `src/handler.mjs`: request schema, response headers, and validation limits. +- `src/index.mjs`: the Workers adapter import. + +`className` connects a component to a rule in the document's `StyleSheet`. +`Invoice({ data }).renderPdf(renderPdf)` passes both generated HTML and CSS. +For a browser HTML export, use `.toHtml({ includeStyles: true })`. You can also +replace the component template with your own HTML/CSS and call `renderPdf`. + +The sample accepts 1–30 rows, 32 KiB of JSON, dates in `YYYY-MM-DD` format, +nonnegative integer-cent rates, integer quantities, and totals up to $999,999.99. +PDFs have a 10-page limit. Change these bounds alongside representative layout +tests. The template uses USD and does not calculate tax. Invoice identifiers are +restricted to safe filename characters. Text is escaped by the component API. + +## Connect it to an application + +The configuration disables public Workers and preview URLs and includes no +route. The local endpoint has no authentication. Before providing an external +route, add your application's authentication, authorization and rate limits; +or call it through a private Worker service binding. Keep real invoices out of +logs. `npm run build` bundles locally and does not deploy anything. + +Rendering runs synchronously inside an isolate and returns a settled Promise. +The SDK does not expose cancellation or a timeout for Workers. Page and input +limits do not bound CPU time or memory. Local workerd checks are retained in the +SDK repository; hosted CPU, memory, startup and free-plan suitability have not +been qualified. Measure your real documents against +[Cloudflare's current limits](https://developers.cloudflare.com/workers/platform/limits/) +before choosing deployment settings. + +See the [adapter guide](https://github.com/fullbleed-engine/fullbleed-node/blob/main/docs/cloudflare.md) +for custom assets, fonts, options and the supported execution model. diff --git a/examples/cloudflare-pdf/package-lock.json b/examples/cloudflare-pdf/package-lock.json new file mode 100644 index 0000000..372d06b --- /dev/null +++ b/examples/cloudflare-pdf/package-lock.json @@ -0,0 +1,1572 @@ +{ + "name": "fullbleed-cloudflare-invoice", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fullbleed-cloudflare-invoice", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "fullbleed": "0.8.0" + }, + "devDependencies": { + "wrangler": "4.151.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@bjorn3/browser_wasi_shim": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.4.2.tgz", + "integrity": "sha512-/iHkCVUG3VbcbmEHn5iIUpIrh7a7WPiwZ3sHy4HZKZzBdSadwdddYDZAII2zBvQYV0Lfi8naZngPCN7WPHI/hA==", + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.2.tgz", + "integrity": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20261019.1.tgz", + "integrity": "sha512-5ZILbAta/fBQvhmAYeCE7dKMMYIr7N9wJEX50fEGLKSIwGtvT0HzSfHNIK/O7cBQ4gHQ2WfIiPClvwcye6E0fA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20261019.1.tgz", + "integrity": "sha512-cww2Ab6lVMxpTiEEjKmPI58p5EMo0vYTZSH+3WkqXJdQgc3ES66GuL247+U2B2YWajhhu2tqj7jySrXneU749g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20261019.1.tgz", + "integrity": "sha512-4++VRi9vkQROYqrpZYOixIJuVDmujkwrT4kszsxNFlvcmsX6Mr2GumvxIN3gIixS3vJUh3Umdtkthqj5X0S3Xw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20261019.1.tgz", + "integrity": "sha512-75PVtJOwfjScQ53sbLtQXI7D8yP1c21t+WEUPaYbqkzaz4ZqfR5wAd+zuWs8G5Q7kOj2SQIvEfY5Sn8Y+UVrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20261019.1.tgz", + "integrity": "sha512-z49fvKTETMCnG6wnMcQwxx9CpryFceiKz04FRjq26EkhCyzRNJ6c3UXhyZz3r5f1CXnnBZc5DcU8ZouPkUJCqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.24", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz", + "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fullbleed": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/fullbleed/-/fullbleed-0.8.0.tgz", + "integrity": "sha512-olEU8ARb8i5ASStXX7uGAZ2RaGaKj2uL8cL81su+M46xo3AdxcGL3j8SCcaXEmTJUzKE0o5E6djykPTbC0u2FA==", + "license": "MIT", + "dependencies": { + "@bjorn3/browser_wasi_shim": "0.4.2" + }, + "bin": { + "fullbleed-browser-assets": "src/copy-browser-assets.cjs" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniflare": { + "version": "5.20261019.1-beta", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20261019.1-beta.tgz", + "integrity": "sha512-1n7MN2UqZnPniyYAqNuV1YEAaja66ozKGVCc5ZTmYQhTubnoAwGLccwb+ZWYLOajuw3pOaJ70u//bZMuURlRew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.5", + "undici": "7.29.1", + "workerd": "1.20261019.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/undici": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20261019.1.tgz", + "integrity": "sha512-UUYRUq/7DmeCl11YriNbcVbjUZVLtt8uuCXFu+nz77hGFf8d4DEVXoCYtnA+MlBatE4+wxoj47DVqzK3xGBAiw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20261019.1", + "@cloudflare/workerd-darwin-arm64": "1.20261019.1", + "@cloudflare/workerd-linux-64": "1.20261019.1", + "@cloudflare/workerd-linux-arm64": "1.20261019.1", + "@cloudflare/workerd-windows-64": "1.20261019.1" + } + }, + "node_modules/wrangler": { + "version": "4.151.3", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.151.3.tgz", + "integrity": "sha512-mzPQLmkAhZUV7CdiAhE+hiIAJlS9xPL6vZe1B8oQFuWxFu7nmQnvHAVjcAcabwSGkXFXH9uspzXwHOFrfbQmqQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.2", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.2", + "miniflare": "5.20261019.1-beta", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20261019.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^5.20261019.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} diff --git a/examples/cloudflare-pdf/package.json b/examples/cloudflare-pdf/package.json new file mode 100644 index 0000000..e01cdaa --- /dev/null +++ b/examples/cloudflare-pdf/package.json @@ -0,0 +1,16 @@ +{ + "name": "fullbleed-cloudflare-invoice", + "version": "1.0.0", + "private": true, + "type": "module", + "license": "MIT", + "engines": { "node": ">=22" }, + "scripts": { + "dev": "wrangler dev --local --ip 127.0.0.1 --port 8787 --inspector-port 0", + "build": "wrangler deploy --dry-run --outdir dist", + "test": "node --test test/request.test.mjs", + "test:worker": "node test/worker.mjs" + }, + "dependencies": { "fullbleed": "0.8.0" }, + "devDependencies": { "wrangler": "4.151.3" } +} diff --git a/examples/cloudflare-pdf/release.json b/examples/cloudflare-pdf/release.json new file mode 100644 index 0000000..b21188e --- /dev/null +++ b/examples/cloudflare-pdf/release.json @@ -0,0 +1,15 @@ +{ + "packageVersion": "0.8.0", + "engineVersion": "2.5.29", + "wrangler": "4.151.3", + "sourceCommit": "e9582e417e8cbe49e41aabd0c695524920a1840d", + "sourceRepository": "https://github.com/fullbleed-engine/fullbleed-node", + "release": "https://github.com/fullbleed-engine/fullbleed-node/releases/tag/v0.8.0", + "packageIntegrity": "sha512-olEU8ARb8i5ASStXX7uGAZ2RaGaKj2uL8cL81su+M46xo3AdxcGL3j8SCcaXEmTJUzKE0o5E6djykPTbC0u2FA==", + "packageSha256": "86af460db1f29c94e65daff3563f9f64f0f52e0af47f02adf3ff39cb2ece1691", + "wasmSha256": "bbcf9b09a60a38d9a884f227fd7f7d31e17e4d611c91027775059f05e2bb46f2", + "localOnly": true, + "hostedVerified": false, + "freePlanVerified": false, + "sourceNote": "SDK examples/cloudflare source; npm lockfile and this release record added for the downloadable project." +} diff --git a/examples/cloudflare-pdf/sample.json b/examples/cloudflare-pdf/sample.json new file mode 100644 index 0000000..6370866 --- /dev/null +++ b/examples/cloudflare-pdf/sample.json @@ -0,0 +1,31 @@ +{ + "number": "NS-1042", + "customer": "Maple & Finch", + "period": "September 2026", + "issued": "2026-10-01", + "due": "2026-10-15", + "paid": false, + "items": [ + { + "name": "Discovery & direction", + "detail": "Stakeholder workshops and a clear creative brief.", + "quantity": 12, + "rate_cents": 15000, + "progress": 100 + }, + { + "name": "Identity system", + "detail": "Typography, color, and the pieces that bring it together.", + "quantity": 24, + "rate_cents": 15000, + "progress": 100 + }, + { + "name": "Digital experience", + "detail": "A considered home for a growing brand.", + "quantity": 18, + "rate_cents": 15000, + "progress": 75 + } + ] +} diff --git a/examples/cloudflare-pdf/src/document.mjs b/examples/cloudflare-pdf/src/document.mjs new file mode 100644 index 0000000..7a77a33 --- /dev/null +++ b/examples/cloudflare-pdf/src/document.mjs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +import { component, Document, Box, Text, Paragraph, Strong, Header, Footer, Heading, + LineBreak, Section, LayoutGrid, Table, TableCaption, THead, TBody, Tr, Th, Td } from 'fullbleed/ui'; +import { invoiceStyles } from './styles.mjs'; + +export function money(cents) { + if (!Number.isSafeInteger(cents)) throw new TypeError('Amounts must be safe integer cents'); + const value = Math.abs(cents); + return (cents < 0 ? '-' : '') + '$' + Math.floor(value / 100).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + + '.' + String(value % 100).padStart(2, '0'); +} +export const Field = component(({ label, value }) => + Box(Text({ className: 'label' }, label), Paragraph({ className: 'value' }, value))); +export const WorkRow = component(({ item }) => Tr( + Td({ className: 'work' }, Paragraph({ className: 'item-name' }, item.name), + Paragraph({ className: 'detail' }, item.detail), + Box({ className: 'progress-track', 'aria-hidden': true }, + Box({ className: 'progress-fill', style: { width: `${item.progress}%` } }))), + Td({ className: 'number hours' }, item.quantity), + Td({ className: 'number rate' }, money(item.rate_cents)), + Td({ className: 'number amount' }, money(item.quantity * item.rate_cents)))); +export const WorkTable = component(({ items }) => Table( + TableCaption('The work, in detail'), + THead(Tr( + Th({ className: 'work', scope: 'col' }, 'Project'), + Th({ className: 'number hours', scope: 'col' }, 'Hours'), + Th({ className: 'number rate', scope: 'col' }, 'Rate'), + Th({ className: 'number amount', scope: 'col' }, 'Amount'))), + TBody(items.map(item => WorkRow({ item }))))); +export const Total = component(({ items, due, paid }) => Section({ className: 'total', 'aria-label': 'Invoice total' }, + Text({ className: 'label' }, paid ? 'Paid in full' : 'Amount due · USD'), + Paragraph({ className: 'total-amount' }, money(items.reduce((sum, item) => sum + item.quantity * item.rate_cents, 0))), + Paragraph({ className: 'total-note' }, paid ? 'Thank you.' : `Due ${due}`))); +export const PaymentNote = component(({ paid }) => Box( + Paragraph({ className: 'note' }, Strong('Onward, together.'), + 'Please include the invoice number with your payment. We’re grateful to be part of what comes next.'), + paid && Paragraph({ style: { fontSize: '8pt', marginTop: '6pt' } }, 'This invoice has been settled.'))); +export const Invoice = component(({ data }) => Document({ title: 'Northstar Studio / Invoice', lang: 'en', styles: invoiceStyles }, + Header({ className: 'brand' }, Paragraph({ className: 'wordmark' }, 'NORTHSTAR / STUDIO'), + Paragraph({ className: 'brand-note' }, 'Independent design.\nThoughtful by nature.')), + Section({ className: 'intro' }, Paragraph({ className: 'eyebrow' }, `Invoice ${data.number} · ${data.period}`), + Heading({ level: 1 }, 'Good work.', LineBreak(), 'Clear numbers.'), + Paragraph({ className: 'lede' }, 'A shared vision, made tangible. Here’s the work we brought to life together.')), + LayoutGrid({ className: 'meta', 'aria-label': 'Invoice details' }, Field({ label: 'Prepared for', value: data.customer }), + Field({ label: 'Issued', value: data.issued }), Field({ label: 'Payment due', value: data.due })), + WorkTable({ items: data.items }), + LayoutGrid({ className: 'summary', 'aria-label': 'Payment summary' }, PaymentNote({ paid: data.paid }), Total(data)), + Footer({ className: 'footer' }, 'NORTHSTAR STUDIO / FICTIONAL DEMONSTRATION / GENERATED WITH FULLBLEED'))); diff --git a/examples/cloudflare-pdf/src/handler.mjs b/examples/cloudflare-pdf/src/handler.mjs new file mode 100644 index 0000000..6ec8d9c --- /dev/null +++ b/examples/cloudflare-pdf/src/handler.mjs @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT +import { Invoice } from './document.mjs'; + +const BODY_LIMIT = 32 * 1024; +const MAX_CENTS = 99_999_999; +class RequestError extends Error { + constructor(status, code, message) { super(message); Object.assign(this, { status, code }); } +} +const invalid = message => { throw new RequestError(400, 'INVALID_INVOICE', message); }; +const json = (data, status = 200, headers = {}) => Response.json(data, { + status, headers: { 'Cache-Control': 'no-store', ...headers }, +}); +function fields(value, keys, label) { + if (!value || typeof value !== 'object' || Array.isArray(value)) invalid(`${label} must be an object.`); + for (const key of Object.keys(value)) if (!keys.includes(key)) invalid(`${label} has an unknown field.`); +} +function text(value, max, label) { + if (typeof value !== 'string' || !value.trim() || value.length > max || /[\u0000-\u001f\u007f]/u.test(value)) { + invalid(`${label} must contain 1 to ${max} characters without control characters.`); + } + return value.trim(); +} +function integer(value, min, max, label) { + if (!Number.isSafeInteger(value) || value < min || value > max) invalid(`${label} must be an integer from ${min} to ${max}.`); + return value; +} +function date(value, label) { + if (typeof value !== 'string' || !/^20\d{2}-\d{2}-\d{2}$/.test(value)) invalid(`${label} must be a date in YYYY-MM-DD format (2000–2099).`); + const stamp = new Date(`${value}T00:00:00.000Z`); + if (!Number.isFinite(stamp.getTime()) || stamp.toISOString().slice(0, 10) !== value) invalid(`${label} is not a calendar date.`); + return stamp.toLocaleDateString('en-GB', { day: '2-digit', month: 'long', year: 'numeric', timeZone: 'UTC' }); +} +export function validateInvoice(data) { + fields(data, ['number', 'customer', 'period', 'issued', 'due', 'paid', 'items'], 'Invoice'); + const number = text(data.number, 32, 'number'); + if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(number)) invalid('number must use letters, digits, underscores or hyphens.'); + const customer = text(data.customer, 80, 'customer'); + const period = text(data.period, 32, 'period'); + const issued = date(data.issued, 'issued'); + const due = date(data.due, 'due'); + if (data.due < data.issued) invalid('due must be on or after issued.'); + if (typeof data.paid !== 'boolean') invalid('paid must be a boolean.'); + if (!Array.isArray(data.items) || data.items.length < 1 || data.items.length > 30) invalid('items must contain 1 to 30 rows.'); + let total = 0; + const items = data.items.map((item, i) => { + const label = `items[${i}]`; + fields(item, ['name', 'detail', 'quantity', 'rate_cents', 'progress'], label); + const name = text(item.name, 60, `${label}.name`); + const detail = text(item.detail, 160, `${label}.detail`); + const quantity = integer(item.quantity, 1, 999, `${label}.quantity`); + const rate_cents = integer(item.rate_cents, 0, MAX_CENTS, `${label}.rate_cents`); + const progress = integer(item.progress, 0, 100, `${label}.progress`); + total += quantity * rate_cents; + if (total > MAX_CENTS) invalid('The invoice total must not exceed $999,999.99.'); + return { name, detail, quantity, rate_cents, progress }; + }); + return { number, customer, period, issued, due, paid: data.paid, items }; +} +async function readJson(request) { + if ((request.headers.get('Content-Type') ?? '').split(';')[0].trim().toLowerCase() !== 'application/json') { + throw new RequestError(415, 'JSON_REQUIRED', 'Send application/json.'); + } + // Enforce the limit on actual streamed bytes, including requests without Content-Length. + const reader = request.body?.getReader(); + if (!reader) throw new RequestError(400, 'INVALID_JSON', 'Send a JSON invoice.'); + let size = 0; + const chunks = []; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + size += value.byteLength; + if (size > BODY_LIMIT) { + await reader.cancel().catch(() => {}); + throw new RequestError(413, 'BODY_TOO_LARGE', 'JSON must not exceed 32 KiB.'); + } + chunks.push(value); + } + } finally { reader.releaseLock(); } + const bytes = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; } + try { return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)); } + catch { throw new RequestError(400, 'INVALID_JSON', 'Send valid UTF-8 JSON.'); } +} +export function invoiceHandler(renderPdf) { + return { + async fetch(request) { + const path = new URL(request.url).pathname; + if (path === '/health' && request.method === 'GET') return json({ ok: true }); + if (path !== '/invoice') return json({ error: 'NOT_FOUND', message: 'POST JSON to /invoice.' }, 404); + if (request.method !== 'POST') return json({ error: 'METHOD_NOT_ALLOWED', message: 'Use POST.' }, 405, { Allow: 'POST' }); + try { + const data = validateInvoice(await readJson(request)); + const result = await Invoice({ data }).renderPdf(renderPdf, { maxPages: 10, defaultStyles: false }); + return new Response(result.pdf, { headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="invoice-${data.number}.pdf"`, + 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', + } }); + } catch (error) { + if (error instanceof RequestError) return json({ error: error.code, message: error.message }, error.status); + if (error?.code === 'MISSING_GLYPHS') return json({ error: 'MISSING_GLYPHS', message: 'This invoice needs an additional font. Register suitable font bytes in the Worker.' }, 422); + if (error?.code === 'PAGE_LIMIT') return json({ error: 'PAGE_LIMIT', message: 'This invoice exceeds the 10-page limit.' }, 422); + // Do not echo renderer errors or log invoice/customer data. + return json({ error: 'RENDER_FAILED', message: 'The invoice could not be rendered.' }, 500); + } + }, + }; +} diff --git a/examples/cloudflare-pdf/src/index.mjs b/examples/cloudflare-pdf/src/index.mjs new file mode 100644 index 0000000..897b536 --- /dev/null +++ b/examples/cloudflare-pdf/src/index.mjs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT +import { renderPdf } from 'fullbleed/cloudflare'; +import { invoiceHandler } from './handler.mjs'; + +export default invoiceHandler(renderPdf); diff --git a/examples/cloudflare-pdf/src/styles.mjs b/examples/cloudflare-pdf/src/styles.mjs new file mode 100644 index 0000000..22e3177 --- /dev/null +++ b/examples/cloudflare-pdf/src/styles.mjs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +import { StyleSheet } from "fullbleed/ui"; + +export const invoiceStyles = new StyleSheet({ + "@page": {"size": "A4", "margin": "17mm 18mm"}, + "body": {"margin": "0", "fontFamily": "Inter", "fontSize": "10pt", "lineHeight": "1.5", "color": "#233c36", "background": "#fff"}, + "*": {"boxSizing": "border-box"}, + "h1, h2, p": {"margin": "0"}, + ".brand": {"display": "grid", "gridTemplateColumns": "1fr 1fr", "paddingBottom": "16pt", "borderBottom": "2pt solid #233c36"}, + ".wordmark": {"fontSize": "15pt", "fontWeight": "800", "letterSpacing": "-0.5pt"}, + ".brand-note": {"textAlign": "right", "fontSize": "8pt", "color": "#63756e"}, + ".intro": {"paddingTop": "26pt", "paddingBottom": "24pt"}, + ".eyebrow": {"color": "#a0442e", "fontSize": "8pt", "fontWeight": "700", "letterSpacing": "1.5pt", "textTransform": "uppercase"}, + "h1": {"fontSize": "42pt", "fontWeight": "700", "lineHeight": "1.08", "letterSpacing": "-2pt", "margin": "9pt 0 13pt"}, + ".lede": {"width": "80%", "fontSize": "11pt", "color": "#63756e"}, + ".meta": {"display": "grid", "gridTemplateColumns": "2fr 1fr 1fr", "gap": "14pt", "borderTop": "1pt solid #d3ded8", "paddingTop": "13pt", "marginBottom": "26pt"}, + ".label": {"display": "block", "fontSize": "7pt", "letterSpacing": "1pt", "textTransform": "uppercase", "color": "#63756e", "marginBottom": "4pt"}, + ".value": {"fontWeight": "600"}, + "table": {"width": "100%", "borderCollapse": "collapse", "tableLayout": "fixed"}, + "caption": {"textAlign": "left", "fontSize": "8pt", "textTransform": "uppercase", "letterSpacing": "1pt", "paddingBottom": "10pt", "fontWeight": "700"}, + "th": {"textAlign": "left", "padding": "8pt 0", "borderTop": "1pt solid #233c36", "borderBottom": "1pt solid #233c36", "fontSize": "7pt", "textTransform": "uppercase", "letterSpacing": "0.8pt"}, + "td": {"padding": "13pt 0", "borderBottom": "1pt solid #d3ded8", "verticalAlign": "top"}, + ".work": {"width": "52%", "paddingRight": "16pt"}, + ".hours": {"width": "10%"}, + ".rate": {"width": "18%"}, + ".amount": {"width": "20%"}, + ".number": {"textAlign": "right", "fontVariantNumeric": "tabular-nums"}, + ".item-name": {"fontSize": "10pt", "fontWeight": "650"}, + ".detail": {"marginTop": "3pt", "fontSize": "8pt", "color": "#63756e"}, + ".progress-track": {"height": "3pt", "width": "100%", "background": "#e6ece8", "marginTop": "8pt"}, + ".progress-fill": {"height": "3pt", "background": "#b55b3e"}, + ".summary": {"display": "grid", "gridTemplateColumns": "1fr 1fr", "gap": "24pt", "marginTop": "24pt"}, + ".note": {"paddingTop": "8pt", "fontSize": "9pt", "color": "#63756e"}, + ".note strong": {"display": "block", "color": "#233c36", "marginBottom": "5pt"}, + ".total": {"background": "#233c36", "color": "#fff", "padding": "18pt", "borderRadius": "3pt"}, + ".total .label": {"color": "#d4e4dc"}, + ".total-amount": {"fontSize": "26pt", "fontWeight": "650", "letterSpacing": "-0.8pt", "marginTop": "4pt"}, + ".total-note": {"color": "#d4e4dc", "fontSize": "8pt", "marginTop": "6pt"}, + ".footer": {"marginTop": "24pt", "borderTop": "1pt solid #d3ded8", "paddingTop": "10pt", "fontSize": "7pt", "color": "#63756e"}, + "tr, .summary, .meta": {"breakInside": "avoid"}, +}); diff --git a/examples/cloudflare-pdf/test/fixtures.mjs b/examples/cloudflare-pdf/test/fixtures.mjs new file mode 100644 index 0000000..d66ad0a --- /dev/null +++ b/examples/cloudflare-pdf/test/fixtures.mjs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +import { readFile } from 'node:fs/promises'; +const sample = JSON.parse(await readFile(new URL('../sample.json', import.meta.url), 'utf8')); +export const cases = [ + { name: 'sample', data: sample, minPages: 1, maxPages: 1, + text: ['Maple & Finch', '$8,100.00', '01 October 2026'] }, + { name: 'minimum', data: { ...sample, number: 'NS-1043', paid: true, + items: [{ ...sample.items[0], quantity: 1, rate_cents: 0, progress: 0 }] }, + minPages: 1, maxPages: 1, text: ['$0.00', 'This invoice has been settled.'] }, + { name: 'maximum-total', data: { ...sample, number: 'NS-1044', + items: [{ ...sample.items[0], quantity: 1, rate_cents: 99999999 }] }, + minPages: 1, maxPages: 1, text: ['$999,999.99'] }, + { name: 'long', data: { ...sample, number: 'NS-1045', + customer: 'Maple & Finch International Design and Community Research Partnership', + items: Array.from({ length: 30 }, (_, i) => ({ name: `Work package ${i + 1}: design, research and delivery`, + detail: 'A considered set of activities, documented decisions and practical resources for the next phase of the project.', + quantity: 2, rate_cents: 15000, progress: 75 })) }, + minPages: 3, maxPages: 10, text: ['Work package 30:', '$9,000.00'] }, + { name: 'escaped', data: { ...sample, number: 'NS-1046', customer: ' & Partners' }, + minPages: 1, maxPages: 2, text: [' & Partners', '$8,100.00'] }, +]; +export const invalidCases = [ + { name: 'json', body: '{', status: 400, code: 'INVALID_JSON' }, + { name: 'content-type', body: JSON.stringify(sample), contentType: 'text/plain', status: 415, code: 'JSON_REQUIRED' }, + { name: 'body-limit', body: JSON.stringify({ padding: 'x'.repeat(32769) }), status: 413, code: 'BODY_TOO_LARGE' }, + { name: 'number', data: { ...sample, number: '../private' }, status: 400, code: 'INVALID_INVOICE' }, + { name: 'date', data: { ...sample, due: '2026-02-30' }, status: 400, code: 'INVALID_INVOICE' }, + { name: 'money', data: { ...sample, items: [{ ...sample.items[0], rate_cents: 1.5 }] }, status: 400, code: 'INVALID_INVOICE' }, + { name: 'total', data: { ...sample, items: [{ ...sample.items[0], rate_cents: 99999999 }] }, status: 400, code: 'INVALID_INVOICE' }, + { name: 'row-limit', data: { ...sample, items: Array(31).fill(sample.items[0]) }, status: 400, code: 'INVALID_INVOICE' }, + { name: 'unknown', data: { ...sample, html: '