diff --git a/.github/workflows/nextjs.yml b/.github/workflows/nextjs.yml new file mode 100644 index 0000000..1db5b32 --- /dev/null +++ b/.github/workflows/nextjs.yml @@ -0,0 +1,48 @@ +name: Next.js PDF starter +on: + pull_request: + paths: ['examples/nextjs/**', '.github/workflows/nextjs.yml'] + push: + branches: [main] + paths: ['examples/nextjs/**', '.github/workflows/nextjs.yml'] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: nextjs-${{ github.ref }} + cancel-in-progress: true +jobs: + standalone: + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-latest, node: '22' } + - { os: ubuntu-latest, node: '24' } + - { os: windows-latest, node: '24' } + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + defaults: + run: + working-directory: examples/nextjs + env: + NEXT_TELEMETRY_DISABLED: '1' + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + cache: npm + cache-dependency-path: examples/nextjs/package-lock.json + - run: npm ci --ignore-scripts --no-audit --no-fund + - run: npm run preview:pdf + - run: npm run build + - run: npm run verify + - uses: actions/upload-artifact@v4 + if: always() + with: + name: nextjs-${{ matrix.os }}-${{ matrix.node }} + path: | + examples/nextjs/output/ + examples/nextjs/public/invoice.png + retention-days: 14 diff --git a/README.md b/README.md index 0bc7b0a..9525687 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,14 @@ npm run example Open `output/invoice/invoice.pdf` and `page-1.png`. The [three-page report sources](examples/report.html) demonstrate pagination, tables, and vector artwork. +## Use it in Next.js + +For a web application, start with the [Next.js PDF download example](examples/nextjs). +It includes a designed invoice, an App Router handler, production build settings +for the worker and bundled assets, and a standalone-server verification script. +The route bounds rendering and returns private PDF attachments from fictional +data; connect your own authenticated record lookup when adapting it. + ## Fonts and assets Supply file bytes explicitly. Additional fonts are registered alongside the diff --git a/examples/nextjs/.gitignore b/examples/nextjs/.gitignore new file mode 100644 index 0000000..ec3b8a6 --- /dev/null +++ b/examples/nextjs/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.next/ +output/ +.env*.local diff --git a/examples/nextjs/LICENSE b/examples/nextjs/LICENSE new file mode 100644 index 0000000..4c99bbd --- /dev/null +++ b/examples/nextjs/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/nextjs/README.md b/examples/nextjs/README.md new file mode 100644 index 0000000..aab6ac9 --- /dev/null +++ b/examples/nextjs/README.md @@ -0,0 +1,101 @@ +# Fullbleed PDF downloads in Next.js + +A runnable App Router starter that serves a designed invoice from a Node.js +route. It includes the HTML/CSS, an actual PDF preview, and an isolated +standalone-server check. All invoice data is fictional; the public fixture +lookup is not an authentication system. + +## Run it + +Use Node.js 22 or newer. From this directory: + +```sh +npm ci --ignore-scripts +npm run build +npm start +``` + +Open `http://127.0.0.1:3000` and select **Download sample PDF**. The route +`/api/invoices/NS-1042` returns the one-page invoice as an attachment. Unknown +IDs return 404. Use `npm run dev` while editing the app. `PORT` changes the port; +`FULLBLEED_HOST` changes the production server's loopback binding. + +The lockfile pins Next.js 16.3.8, React 19.3.0 and the released Fullbleed Node +0.1.1 tarball (engine 2.5.5). Fullbleed does not yet have a configured npm +registry publisher; the dependency uses the verified GitHub release URL. +Installing this starter needs no Python, Rust, system fonts or browser renderer. + +## Adapt the document + +Edit `templates/invoice.html` and `templates/invoice.css`. The sample uses +bundled Inter, DM Serif Display and Bebas Neue. `lib/invoice.mjs` inserts an +escaped customer name from the fictional fixture. Amounts and dates are sample +content, not an invoicing ledger or tax calculation. + +After changing the template, regenerate and inspect the PDF and preview: + +```sh +npm run preview:pdf +``` + +Open `output/invoice.pdf` and `public/invoice.png`, then rebuild the app. The +checked-in image is output from Fullbleed, not a browser screenshot or mockup. + +## Keep the renderer on the server + +The route declares `runtime = 'nodejs'`. It uses filesystem reads, worker +threads and WebAssembly, so this integration needs the Node runtime. +`serverExternalPackages: ['fullbleed']` keeps Next.js from bundling the engine's +Node entrypoint. Explicit output tracing includes the worker, WASM, fonts, +WASI dependency and template files needed by the standalone server. +See Next.js's [external-package configuration](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages) +and [standalone output documentation](https://nextjs.org/docs/app/api-reference/config/next-config-js/output). + +`npm run build` copies the public preview and browser chunks into +`.next/standalone`, after Next generates it. That folder is the deployment +artifact. Start its `server.js` with Node on a host that supports workers and +WebAssembly; set the host's normal `PORT` and `HOSTNAME` environment variables. +The local start script defaults to loopback. Hosting-provider limits and +serverless or Edge deployments have not been verified by this starter. + +## Adapt the route for real accounts + +Replace `getDemoInvoice(id)` with a session-authenticated lookup that verifies +the invoice belongs to that account **before rendering**. Keep order fields +escaped. Never accept arbitrary HTML, filesystem paths or remote asset URLs +from this public endpoint. + +The example renders at most one document at a time per server process. Extra +requests receive `503 BUSY` and `Retry-After: 2`; they do not enter an unbounded +queue. Successful PDFs have `private, no-store`, a fixed filename, a complete +content length and `nosniff`. Jobs have a 15-second rendering deadline and a +five-page cap, and receive the request's abort signal. No generated PDF is +written to application storage by the route. + +Use shared rate/usage controls for multiple processes or replicas. Measure your +host's memory and CPU before increasing capacity; each render worker can grow +to the package's 512 MiB WASM ceiling. Private document delivery also requires +your application's normal access controls, monitoring and retention policies. + +## Verify the production artifact + +After building: + +```sh +npm run verify +``` + +The check copies the standalone artifact to a fresh temporary directory outside +the app and starts it on an available loopback port. It compares a real HTTP +PDF to the direct engine output, checks headers and missing IDs, sends a +concurrent burst, then verifies recovery from actual missing-glyph and page-cap +failures. Client JavaScript is checked for accidental engine/worker inclusion. +Results and the downloaded PDF are retained under `output/`. + +Temporary artifact directories use the `fullbleed-nextjs-` prefix and are kept +for inspection. The verification script closes its own server when it finishes. +These checks cover a fictional fixture; they do not establish application +authorization, PDF standards compliance, or a hosting provider's limits. + +[Fullbleed Node API](https://github.com/fullbleed-engine/fullbleed-node) · +[Documentation](https://docs.fullbleed.dev/) · [MIT license](LICENSE) diff --git a/examples/nextjs/app/api/invoices/[id]/route.js b/examples/nextjs/app/api/invoices/[id]/route.js new file mode 100644 index 0000000..28ec29a --- /dev/null +++ b/examples/nextjs/app/api/invoices/[id]/route.js @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +import { renderPdf, FullbleedError } from 'fullbleed'; +import { getDemoInvoice, invoiceTemplate } from '../../../../lib/invoice.mjs'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +// One active render per process; there is no unbounded waiting queue. +// Add authentication and a shared rate/usage limit for a deployed application. +let active = 0; +const privateHeaders = { + 'Cache-Control': 'private, no-store', + 'X-Content-Type-Options': 'nosniff', +}; +const errorResponse = (status, code, message, extra = {}) => Response.json( + { error: { code, message } }, { status, headers: { ...privateHeaders, ...extra } }, +); + +export async function GET(request, { params }) { + const { id } = await params; + const invoice = getDemoInvoice(id); + if (!invoice) return errorResponse(404, 'NOT_FOUND', 'Sample invoice not found.'); + if (active >= 1) return errorResponse(503, 'BUSY', 'A document is being prepared. Try again shortly.', { 'Retry-After': '2' }); + active++; + try { + const template = await invoiceTemplate(invoice); + const { pdf } = await renderPdf({ ...template, maxPages: 5, timeoutMs: 15_000, signal: request.signal }); + return new Response(pdf, { + headers: { + ...privateHeaders, + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="invoice-${invoice.id}.pdf"`, + 'Content-Length': String(pdf.length), + }, + }); + } catch (error) { + const code = error instanceof FullbleedError ? error.code : 'UNEXPECTED'; + // Log an operational code only; never log customer data or document input. + console.error('Fullbleed invoice render failed:', code); + if (code === 'TIMEOUT') return errorResponse(504, code, 'Document preparation timed out.'); + if (code === 'ABORTED') return errorResponse(408, code, 'Document preparation was cancelled.'); + return errorResponse(500, 'RENDER_FAILED', 'The document could not be prepared.'); + } finally { + active--; + } +} diff --git a/examples/nextjs/app/globals.css b/examples/nextjs/app/globals.css new file mode 100644 index 0000000..04bdfa4 --- /dev/null +++ b/examples/nextjs/app/globals.css @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: MIT */ +:root { color-scheme: light; --ink: #203b32; --muted: #627169; --paper: #f7f5ee; --accent: #bb4e2c; } +* { box-sizing: border-box; } +body { margin: 0; background: var(--paper); color: var(--ink); font: 16px/1.6 system-ui, sans-serif; } +a { color: inherit; text-underline-offset: .2em; } +.nav, main, footer { max-width: 1240px; margin: auto; padding-left: 40px; padding-right: 40px; } +.nav { min-height: 104px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #cdd2c8; } +.wordmark { font-weight: 850; font-size: 30px; letter-spacing: -1.5px; text-decoration: none; } +.wordmark span { color: var(--accent); } +.eyebrow { font-size: 10px; font-weight: 750; letter-spacing: 2px; } +.hero { display: grid; grid-template-columns: 1fr .88fr; align-items: center; gap: 76px; padding: 62px 0 76px; } +h1 { font: 56px/1.06 Georgia, serif; letter-spacing: -2px; margin: 22px 0; } +h1 em { color: var(--accent); font-weight: 400; } +.lede { max-width: 430px; color: var(--muted); font-size: 17px; line-height: 1.7; margin-bottom: 30px; } +.button { display: inline-flex; justify-content: space-between; gap: 38px; align-items: center; background: var(--ink); color: white; padding: 15px 22px; font-weight: 650; text-decoration: none; } +.button:hover { background: #345949; } +a:focus-visible { outline: 3px solid var(--accent); outline-offset: 5px; } +.caption { font-size: 11px; color: var(--muted); margin-top: 13px; } +.note { border-top: 1px solid #cdd2c8; max-width: 430px; padding-top: 28px; margin-top: 45px; } +.note span, .steps article > span { font-size: 10px; font-weight: 750; letter-spacing: 1.3px; } +.note p { font-size: 13px; color: var(--muted); margin-bottom: 0; } +.paper { margin: 0; padding: 18px 18px 0; background: #dedfd3; border: 1px solid #cdd2c8; } +.paper img { display: block; width: 100%; height: auto; box-shadow: 0 7px 18px #203b3214; } +.paper figcaption { font-size: 10px; text-align: center; padding: 12px 0; color: #4b5d51; } +.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 42px; padding: 35px 0 52px; border-top: 1px solid #cdd2c8; } +.steps article > span { color: var(--accent); } +.steps h2 { font-size: 17px; line-height: 1.4; margin: 12px 0; } +.steps p { color: var(--muted); font-size: 13px; } +footer { border-top: 1px solid #cdd2c8; display: flex; gap: 28px; padding-top: 24px; padding-bottom: 28px; font-size: 11px; } +footer span { margin-right: auto; } +@media (max-width: 850px) { .hero { gap: 30px; } h1 { font-size: 43px; } } +@media (max-width: 650px) { + .nav, main, footer { padding-left: 22px; padding-right: 22px; } + .nav { min-height: 82px; } .hero { grid-template-columns: 1fr; gap: 35px; padding: 36px 0; } + h1 { font-size: 44px; } .lede { font-size: 16px; } .note { margin-top: 30px; padding-top: 20px; } + .steps { grid-template-columns: 1fr; gap: 24px; } .steps p { margin-bottom: 0; } + footer { flex-wrap: wrap; gap: 14px 25px; } footer span { width: 100%; } +} diff --git a/examples/nextjs/app/layout.js b/examples/nextjs/app/layout.js new file mode 100644 index 0000000..4e3b2d8 --- /dev/null +++ b/examples/nextjs/app/layout.js @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +import './globals.css'; + +export const metadata = { + title: 'A better PDF download | Fullbleed + Next.js', + description: 'A runnable Next.js starter serving a designed invoice with Fullbleed. Fictional sample data.', +}; + +export default function Layout({ children }) { + return {children}; +} diff --git a/examples/nextjs/app/page.js b/examples/nextjs/app/page.js new file mode 100644 index 0000000..06c6c33 --- /dev/null +++ b/examples/nextjs/app/page.js @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +export default function Page() { + return <> +
fullbleed.NEXT.JS STARTER
+
+
+
+

A SMALL ROUTE. A BETTER DOCUMENT.

+

Make the download
worth opening.

+

Serve a designed PDF from your Next.js app. Start with this studio invoice, then make the HTML and CSS your own.

+ Download sample PDF +

One page · Fictional invoice NS-1042 · No sign-in

+
YOUR APP. YOUR DOCUMENTS.

The PDF is generated on your Node.js server with bundled fonts. Your HTML and CSS define the printed page.

+
+
Actual Fullbleed PDF: a Northstar Studio invoice with cream paper, green tables, oversized type and an orange accent.
Actual PDF output · Northstar Studio
+
+
+
01 / DESIGN

Keep your HTML & CSS.

Edit the included template. Use the bundled typefaces or supply your own font files.

+
02 / CONNECT

Use your application data.

The sample route looks up a fictional invoice. Connect your own authorized record lookup when adapting it.

+
03 / DELIVER

Send a real PDF.

A normal download link calls the server route. Rendering has a page limit, deadline and capacity limit.

+
+
+ + ; +} diff --git a/examples/nextjs/lib/invoice.mjs b/examples/nextjs/lib/invoice.mjs new file mode 100644 index 0000000..b112f39 --- /dev/null +++ b/examples/nextjs/lib/invoice.mjs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +// Fictional public fixture, not an authorization implementation. In your app, +// replace this with an authenticated lookup scoped to the requesting account. +const invoices = new Map([ + ['NS-1042', { id: 'NS-1042', customer: 'Maple & Finch' }], +]); + +export function getDemoInvoice(id) { + return invoices.get(id); +} + +const escapeHtml = value => String(value).replace(/[&<>"']/g, char => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', +})[char]); + +export async function invoiceTemplate(invoice) { + const [html, css] = await Promise.all([ + readFile(join(process.cwd(), 'templates/invoice.html'), 'utf8'), + readFile(join(process.cwd(), 'templates/invoice.css'), 'utf8'), + ]); + return { html: html.replace('{{customer}}', () => escapeHtml(invoice.customer)), css }; +} diff --git a/examples/nextjs/next.config.mjs b/examples/nextjs/next.config.mjs new file mode 100644 index 0000000..5cfa072 --- /dev/null +++ b/examples/nextjs/next.config.mjs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +import { fileURLToPath } from 'node:url'; + +const root = fileURLToPath(new URL('.', import.meta.url)); + +export default { + output: 'standalone', + poweredByHeader: false, + serverExternalPackages: ['fullbleed'], + outputFileTracingRoot: root, + turbopack: { root }, + outputFileTracingIncludes: { + '/api/invoices/*': [ + './templates/**/*', + './node_modules/fullbleed/**/*', + './node_modules/@bjorn3/browser_wasi_shim/**/*', + ], + }, +}; diff --git a/examples/nextjs/package-lock.json b/examples/nextjs/package-lock.json new file mode 100644 index 0000000..02fcffa --- /dev/null +++ b/examples/nextjs/package-lock.json @@ -0,0 +1,1035 @@ +{ + "name": "fullbleed-nextjs-starter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fullbleed-nextjs-starter", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "fullbleed": "https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.1.1/fullbleed-0.1.1.tgz", + "next": "16.3.8", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "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/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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==", + "license": "MIT", + "optional": true, + "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" + ], + "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" + ], + "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==", + "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" + ], + "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" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "musl" + ], + "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" + ], + "libc": [ + "musl" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "musl" + ], + "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" + ], + "libc": [ + "musl" + ], + "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==", + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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/@next/env": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.8.tgz", + "integrity": "sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.8.tgz", + "integrity": "sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.8.tgz", + "integrity": "sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.8.tgz", + "integrity": "sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.8.tgz", + "integrity": "sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.8.tgz", + "integrity": "sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.8.tgz", + "integrity": "sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.8.tgz", + "integrity": "sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.8.tgz", + "integrity": "sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "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==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fullbleed": { + "version": "0.1.1", + "resolved": "https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.1.1/fullbleed-0.1.1.tgz", + "integrity": "sha512-kWVXlLp0ZYlXRcaiOg0GMJyM8ll0+1R1dV5LokzaoBZwzsNZ2Lz84g5ZH0/PvIN3kFd5An4ZvwGKn5VUR00TtA==", + "license": "MIT", + "dependencies": { + "@bjorn3/browser_wasi_shim": "0.4.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/next/-/next-16.3.8.tgz", + "integrity": "sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.3.8", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.23", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.3.8", + "@next/swc-darwin-x64": "16.3.8", + "@next/swc-linux-arm64-gnu": "16.3.8", + "@next/swc-linux-arm64-musl": "16.3.8", + "@next/swc-linux-x64-gnu": "16.3.8", + "@next/swc-linux-x64-musl": "16.3.8", + "@next/swc-win32-arm64-msvc": "16.3.8", + "@next/swc-win32-x64-msvc": "16.3.8", + "sharp": "^0.35.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "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==", + "license": "ISC", + "optional": true, + "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==", + "license": "Apache-2.0", + "optional": true, + "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/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "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==", + "license": "0BSD" + } + } +} diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json new file mode 100644 index 0000000..238786c --- /dev/null +++ b/examples/nextjs/package.json @@ -0,0 +1,22 @@ +{ + "name": "fullbleed-nextjs-starter", + "version": "1.0.0", + "private": true, + "type": "module", + "license": "MIT", + "engines": { "node": ">=22" }, + "scripts": { + "dev": "next dev --hostname 127.0.0.1", + "build": "next build", + "postbuild": "node tools/copy-public.mjs", + "start": "node tools/start.mjs", + "preview:pdf": "node tools/render-preview.mjs", + "verify": "node tools/verify.mjs" + }, + "dependencies": { + "fullbleed": "https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.1.1/fullbleed-0.1.1.tgz", + "next": "16.3.8", + "react": "19.3.0", + "react-dom": "19.3.0" + } +} diff --git a/examples/nextjs/public/invoice.png b/examples/nextjs/public/invoice.png new file mode 100644 index 0000000..82d648f Binary files /dev/null and b/examples/nextjs/public/invoice.png differ diff --git a/examples/nextjs/templates/invoice.css b/examples/nextjs/templates/invoice.css new file mode 100644 index 0000000..235023f --- /dev/null +++ b/examples/nextjs/templates/invoice.css @@ -0,0 +1,48 @@ +@page { size: A4; margin: 0; } +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { font-family: Inter; font-size: 10pt; line-height: 1.45; color: #17382e; } +h1, h2, h3, p, figure { margin: 0; } +h1, h2, h3 { font-weight: 400; } +.page { width: 210mm; height: 296.9mm; padding: 15mm 16mm; position: relative; break-after: page; background: #f8f5ec; } +.page:last-child { break-after: auto; } +.kicker { font-size: 7.2pt; font-weight: 600; letter-spacing: 1.4pt; text-transform: uppercase; } +.serif { font-family: 'DM Serif Display'; font-weight: 400; } +.display { font-family: 'Bebas Neue'; font-weight: 400; } +.muted { color: #5b6c64; } +.small { font-size: 8pt; } +.rule { border-top: .6pt solid #9eafa4; } +.row { display: flex; justify-content: space-between; gap: 16pt; } +.grid-2 { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: 24pt; } +.grid-2 > * { min-width: 0; } +.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16pt; } +.footer { position: absolute; left: 16mm; right: 16mm; bottom: 12mm; border-top: .6pt solid #9eafa4; padding-top: 8pt; display: flex; justify-content: space-between; gap: 12pt; font-size: 6.6pt; letter-spacing: .45pt; } +table { width: 100%; border-collapse: collapse; font-size: 9pt; } +th { font-size: 7.5pt; font-weight: 600; text-align: left; } +th, td { padding: 10pt 0; border-bottom: .55pt solid #bdc8bf; vertical-align: top; } +.number { text-align: right; white-space: nowrap; } +.money { font-variant-numeric: tabular-nums; } +strong { font-weight: 600; } +a { color: inherit; } + +.invoice .masthead { display: grid; grid-template-columns: 1.05fr .8fr 1fr; gap: 18pt; align-items: center; padding-bottom: 22pt; border-bottom: 1pt solid #17382e; } +.invoice .brand { font: 31pt/.88 'DM Serif Display'; letter-spacing: -1.3pt; } +.invoice .brand span { display: block; line-height: .98; } +.invoice .brand small { display: block; font: 6.5pt Inter; letter-spacing: 2.1pt; margin-top: 11pt; } +.invoice .brand-note { border-left: 1pt solid #c24929; padding: 4pt 0 4pt 16pt; line-height: 1.8; } +.invoice .address { font-size: 7.5pt; line-height: 1.7; border-left: 1pt solid #c24929; padding-left: 16pt; } +.invoice .title-row { display: grid; grid-template-columns: 3.3fr 1fr; gap: 20pt; align-items: center; padding: 22pt 0 16pt; } +.invoice h1 { font: 91pt/.9 'Bebas Neue'; letter-spacing: .3pt; } +.invoice .title-note { color: #b44123; font: 14pt/1.2 'Bebas Neue'; letter-spacing: .5pt; } +.invoice .details { border-top: .6pt solid #9eafa4; border-bottom: .6pt solid #9eafa4; padding: 15pt 0; margin-bottom: 10pt; } +.invoice .details p { margin-top: 5pt; font-size: 8.2pt; } +.invoice .details > div + div { border-left: .6pt solid #c7cdc4; padding-left: 14pt; } +.invoice th { color: #52665a; } +.invoice td { padding: 12pt 0; } +.invoice .item-note { color: #617069; font-size: 8pt; margin-top: 4pt; } +.invoice .amounts { display: grid; grid-template-columns: 1fr 1.25fr; gap: 27pt; margin-top: 23pt; } +.invoice .thanks { font: italic 22pt/1.2 'DM Serif Display'; color: #b44123; padding-top: 8pt; } +.invoice .summary-line { display: flex; justify-content: space-between; padding: 5pt 0; font-size: 8.5pt; } +.invoice .total { background: #17382e; color: #fff8e8; padding: 16pt; margin-top: 9pt; border-left: 4pt solid #d96238; } +.invoice .total .value { font: 38pt/1.1 'Bebas Neue'; margin-top: 7pt; } +.invoice .payment { margin-top: 20pt; padding-top: 12pt; border-top: .6pt solid #9eafa4; display: grid; grid-template-columns: 1fr 1fr; gap: 24pt; font-size: 8pt; } diff --git a/examples/nextjs/templates/invoice.html b/examples/nextjs/templates/invoice.html new file mode 100644 index 0000000..bb0d7e9 --- /dev/null +++ b/examples/nextjs/templates/invoice.html @@ -0,0 +1,12 @@ +Northstar Studio — Invoice NS-1042 +
+
NorthstarStudioDESIGN WITH DIRECTION
Brand
Digital
Experiences
That matter.
42 Cedar Avenue
Portland, OR 97205
hello@example.invalid
northstar.example.invalid
+

INVOICE

GOOD WORK.
CLEAR DETAILS.
LET'S MAKE
WHAT'S NEXT.

+

Prepared for

{{customer}}
Attn. Avery Morgan
Portland, Oregon

The project

Autumn launch
Brand workshop, digital
design & handoff

Invoice NS-1042

Issued · 01 October 2026
Due · 31 October 2026
Currency · USD

+ + +
DESCRIPTIONHOURSRATEAMOUNT
Discovery & brand workshop

Positioning, priorities, and a clear creative direction

8$125.00$1,000.00
Digital design & implementation

A cohesive visual system, from layout to launch

6$95.00$570.00
Review & studio handoff

Final refinements, documentation, and delivery

2$150.00$300.00
+

A little clarity.
A lot of possibility.

Thank you for inviting us
to help shape your next chapter.

Subtotal$1,870.00
Tax · 0% in this sample$0.00
TOTAL DUE · USD
$1,870.00
+

Payment details

Reference NS-1042 with your payment.
Payment instructions are supplied separately.

A note from the studio

Questions about a line item?
Write to hello@example.invalid.

+ +
diff --git a/examples/nextjs/tools/copy-public.mjs b/examples/nextjs/tools/copy-public.mjs new file mode 100644 index 0000000..3fa2158 --- /dev/null +++ b/examples/nextjs/tools/copy-public.mjs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT +import { cp } from 'node:fs/promises'; +// Next's standalone output does not copy public files or browser chunks itself. +await cp('public', '.next/standalone/public', { recursive: true }); +await cp('.next/static', '.next/standalone/.next/static', { recursive: true }); diff --git a/examples/nextjs/tools/render-preview.mjs b/examples/nextjs/tools/render-preview.mjs new file mode 100644 index 0000000..d203792 --- /dev/null +++ b/examples/nextjs/tools/render-preview.mjs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +import { mkdir, writeFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { renderPdf, version } from 'fullbleed'; +import { getDemoInvoice, invoiceTemplate } from '../lib/invoice.mjs'; + +const result = await renderPdf({ ...await invoiceTemplate(getDemoInvoice('NS-1042')), previewDpi: 96, maxPages: 5, timeoutMs: 15_000 }); +if (result.pages !== 1 || result.missingGlyphs !== 0) throw new Error('Review the sample layout before publishing.'); +await mkdir('public', { recursive: true }); +await mkdir('output', { recursive: true }); +await writeFile('public/invoice.png', result.previews[0]); +await writeFile('output/invoice.pdf', result.pdf); +const hash = value => createHash('sha256').update(value).digest('hex'); +const record = { nodePackage: version, engineVersion: result.engineVersion, pages: result.pages, missingGlyphs: result.missingGlyphs, pdfSha256: hash(result.pdf), previewSha256: hash(result.previews[0]) }; +await writeFile('output/preview.json', JSON.stringify(record, null, 2) + '\n'); +console.log(JSON.stringify(record)); diff --git a/examples/nextjs/tools/start.mjs b/examples/nextjs/tools/start.mjs new file mode 100644 index 0000000..59b410f --- /dev/null +++ b/examples/nextjs/tools/start.mjs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +process.env.HOSTNAME = process.env.FULLBLEED_HOST || '127.0.0.1'; +process.env.NODE_ENV = 'production'; +await import(pathToFileURL(resolve('.next/standalone/server.js')).href); diff --git a/examples/nextjs/tools/verify.mjs b/examples/nextjs/tools/verify.mjs new file mode 100644 index 0000000..3ca0671 --- /dev/null +++ b/examples/nextjs/tools/verify.mjs @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +// Exercise the production artifact with fictional data, outside the source tree. +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { cp, mkdir, mkdtemp, readFile, readdir, writeFile } from 'node:fs/promises'; +import { createServer } from 'node:net'; +import { tmpdir, platform } from 'node:os'; +import { join, resolve } from 'node:path'; +import { setTimeout as delay } from 'node:timers/promises'; +import { renderPdf, version, engineVersion } from 'fullbleed'; +import { getDemoInvoice, invoiceTemplate } from '../lib/invoice.mjs'; + +const checks = []; +const check = (name, condition) => { assert.ok(condition, name); checks.push({ name, passed: true }); console.log(`${name}: passed`); }; +const hash = data => createHash('sha256').update(data).digest('hex'); +const project = process.cwd(); +const folder = await mkdtemp(join(tmpdir(), 'fullbleed-nextjs-')); +await mkdir('output', { recursive: true }); +await cp('.next/standalone', folder, { recursive: true, dereference: true }); +await cp('public', join(folder, 'public'), { recursive: true }); +await cp('.next/static', join(folder, '.next/static'), { recursive: true }); +check('standalone artifact is isolated from the source application', !resolve(folder).startsWith(resolve(project))); +const manifest = JSON.parse(await readFile(join(folder, 'node_modules/fullbleed/dist/build.json'), 'utf8')); +for (const [name, expected] of Object.entries(manifest.files)) { + const bytes = await readFile(join(folder, 'node_modules/fullbleed', name)); + assert.equal(hash(bytes), expected.sha256, name); +} +check('standalone tracing retains the verified engine and all bundled fonts', manifest.engineVersion === engineVersion); + +const socket = createServer(); +await new Promise(done => socket.listen(0, '127.0.0.1', done)); +const port = socket.address().port; +await new Promise(done => socket.close(done)); +const base = `http://127.0.0.1:${port}`; +const child = spawn(process.execPath, ['server.js'], { cwd: folder, windowsHide: true, env: { ...process.env, NODE_ENV: 'production', NEXT_TELEMETRY_DISABLED: '1', HOSTNAME: '127.0.0.1', PORT: String(port) }, stdio: ['ignore', 'pipe', 'pipe'] }); +let logs = ''; +for (const stream of [child.stdout, child.stderr]) stream.on('data', data => { logs = (logs + data.toString()).slice(-50000); }); +const stopped = new Promise((done, reject) => { child.once('error', reject); child.once('close', (code, signal) => done({ code, signal })); }); +stopped.catch(() => {}); +const stop = async () => { if (child.exitCode === null && child.signalCode === null) child.kill(); await stopped; }; +const get = (path, options = {}) => fetch(base + path, { ...options, signal: AbortSignal.timeout(25000) }); +let serving = false; +try { + let ready = false; + for (let attempt = 0; attempt < 120; attempt++) { + if (child.exitCode !== null) throw new Error(`Standalone server stopped.\n${logs}`); + try { ready = (await get('/')).status === 200; } catch {} + if (ready) break; + await delay(250); + } + check('isolated production server starts on loopback', ready); + const html = await (await get('/')).text(); + check('app exposes the sample download without a prefetched render', html.includes('Download sample PDF') && html.includes('href="/api/invoices/NS-1042"') && !html.includes('rel="prefetch"')); + const image = await get('/invoice.png'); + check('standalone server includes the actual PDF preview asset', image.status === 200 && hash(Buffer.from(await image.arrayBuffer())) === hash(await readFile('public/invoice.png'))); + const expected = await renderPdf({ ...await invoiceTemplate(getDemoInvoice('NS-1042')), maxPages: 5, timeoutMs: 15000 }); + const url = '/api/invoices/NS-1042'; + async function pdfResponse(response) { + const bytes = Buffer.from(await response.arrayBuffer()); + assert.equal(response.status, 200); + assert.equal(response.headers.get('content-type'), 'application/pdf'); + assert.equal(response.headers.get('content-disposition'), 'attachment; filename="invoice-NS-1042.pdf"'); + assert.equal(response.headers.get('content-length'), String(bytes.length)); + assert.match(response.headers.get('cache-control'), /private.*no-store/); + assert.equal(response.headers.get('x-content-type-options'), 'nosniff'); + assert.equal(hash(bytes), hash(expected.pdf)); + return bytes; + } + const bytes = await pdfResponse(await get(url)); + await writeFile('output/route.pdf', bytes); + check('production route returns complete private PDF bytes with a useful filename', true); + const missing = await get('/api/invoices/NS-9999'); + check('unknown invoice IDs return a private JSON error', missing.status === 404 && /no-store/.test(missing.headers.get('cache-control')) && (await missing.json()).error.code === 'NOT_FOUND'); + check('unsupported writes are rejected by the route', (await get(url, { method: 'POST', body: '{}' })).status === 405); + + const burst = await Promise.all(Array.from({ length: 12 }, async () => { + const response = await get(url); + if (response.status === 200) { await pdfResponse(response); return 200; } + assert.equal(response.status, 503); + assert.equal(response.headers.get('retry-after'), '2'); + assert.match(response.headers.get('cache-control'), /no-store/); + assert.equal((await response.json()).error.code, 'BUSY'); + return 503; + })); + check('a concurrent burst receives explicit capacity errors instead of an unbounded queue', burst.includes(200) && burst.includes(503)); + await pdfResponse(await get(url)); + check('normal PDF downloads recover after a capacity burst', true); + + const templatePath = join(folder, 'templates/invoice.html'); + const original = await readFile(templatePath); + try { + await writeFile(templatePath, '

Integral: \u2a0c

'); + const failure = await get(url); + const body = await failure.json(); + check('a real missing-glyph failure returns a generic non-PDF error', failure.status === 500 && body.error.code === 'RENDER_FAILED' && !JSON.stringify(body).includes('Integral')); + } finally { await writeFile(templatePath, original); } + await pdfResponse(await get(url)); + check('a valid document renders after an engine failure', true); + try { + await writeFile(templatePath, '

Page limit sample

'.repeat(7)); + const failure = await get(url); + check('the route enforces its page cap against a real multipage document', failure.status === 500 && (await failure.json()).error.code === 'RENDER_FAILED' && logs.includes('PAGE_LIMIT')); + } finally { await writeFile(templatePath, original); } + await pdfResponse(await get(url)); + check('a valid document renders after the page limit rejects a job', true); + + async function jsFiles(directory) { + const result = []; + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isDirectory()) result.push(...await jsFiles(path)); + else if (entry.name.endsWith('.js')) result.push(path); + } + return result; + } + const chunks = await jsFiles(join(folder, '.next/static')); + for (const file of chunks) assert.doesNotMatch(await readFile(file, 'utf8'), /browser_wasi_shim|engine\.wasm|node:worker_threads/); + check('client JavaScript excludes the server PDF engine and worker', chunks.length > 0); + const info = JSON.parse(await readFile('package.json', 'utf8')); + const record = { checkedAt: new Date().toISOString(), node: process.version, platform: platform(), next: info.dependencies.next, nodePackage: version, engineVersion, + packageLockSha256: hash(await readFile('package-lock.json')), base, standaloneFolder: folder, pages: expected.pages, pdfSha256: hash(bytes), burst, checks, + scope: 'Real next build standalone server with released Fullbleed tarball and fictional data. No hosted platform or application-authentication claim.' }; + await writeFile('output/verification.json', JSON.stringify(record, null, 2) + '\n'); + await writeFile('output/server.log', logs); + if (process.argv.includes('--serve')) { + serving = true; + console.log(`FULLBLEED_NEXT_PREVIEW_READY ${base}`); + process.once('SIGINT', async () => { await stop(); process.exit(0); }); + process.once('SIGTERM', async () => { await stop(); process.exit(0); }); + await stopped; + } +} catch (error) { + await writeFile('output/failure.json', JSON.stringify({ error: error.stack, checks, base, standaloneFolder: folder }, null, 2) + '\n'); + await writeFile('output/server.log', logs); + throw error; +} finally { + if (!serving) await stop(); +}