From 3fdd439cef938e9cc28f0df5b97692e9e492801c Mon Sep 17 00:00:00 2001 From: Musab Date: Tue, 1 Sep 2026 05:06:41 +0500 Subject: [PATCH 1/2] feat: add in-app-video-creation example Companion code for the "Add video creation to your app without building an editor" guide: an embedded Studio SDK editor, a quick form, and a one-click headless render, all through one render proxy that keeps the API key server-side, records render ownership, validates asset URLs on both render paths, and rate limits per user. --- README.md | 1 + examples/in-app-video-creation/.env.example | 2 + examples/in-app-video-creation/.gitignore | 2 + examples/in-app-video-creation/README.md | 83 + examples/in-app-video-creation/index.html | 243 ++ examples/in-app-video-creation/main.js | 261 ++ .../in-app-video-creation/package-lock.json | 2141 +++++++++++++++++ examples/in-app-video-creation/package.json | 21 + .../in-app-video-creation/public/promo.json | 69 + examples/in-app-video-creation/server.js | 348 +++ examples/in-app-video-creation/template.json | 72 + examples/in-app-video-creation/vite.config.js | 11 + 12 files changed, 3254 insertions(+) create mode 100644 examples/in-app-video-creation/.env.example create mode 100644 examples/in-app-video-creation/.gitignore create mode 100644 examples/in-app-video-creation/README.md create mode 100644 examples/in-app-video-creation/index.html create mode 100644 examples/in-app-video-creation/main.js create mode 100644 examples/in-app-video-creation/package-lock.json create mode 100644 examples/in-app-video-creation/package.json create mode 100644 examples/in-app-video-creation/public/promo.json create mode 100644 examples/in-app-video-creation/server.js create mode 100644 examples/in-app-video-creation/template.json create mode 100644 examples/in-app-video-creation/vite.config.js diff --git a/README.md b/README.md index ebd77b7..0b0a675 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Clone this repository, or open the directory of the example you want. Each examp - [bulk-csv-videos](examples/bulk-csv-videos) renders one video per row of a CSV from a single template with merge fields, tracked in a resumable manifest, with an optional AI step where Claude writes each row's headline and image prompt. Companion code for [Generate videos in bulk with an API and an AI agent](https://shotstack.io/learn/bulk-create-videos-from-csv-and-ai/). - [first-render](examples/first-render) the very basics: submit an Edit, poll the render status, and print the output URL, in Node.js and Python. Start here if you are new to the API. Companion code for [Render your first video with the Shotstack API](https://shotstack.io/learn/render-your-first-video-shotstack-api/). +- [in-app-video-creation](examples/in-app-video-creation) lets a user create the same promo video three ways: an embedded Studio SDK editor, a quick form, and a one-click headless render, all through one render proxy that keeps the API key server-side. Companion code for [Add video creation to your app without building an editor](https://shotstack.io/learn/add-video-creation-to-your-app/). - [instagram-ai-video](examples/instagram-ai-video) generates a script, voiceover and background image with AI, renders a 1080x1920 video, and publishes it as an Instagram Reel. Companion code for [How to automate Instagram posts with AI video](https://shotstack.io/learn/automate-instagram-posts-with-ai-video/). - [rapidreels](examples/rapidreels) creates faceless short-form videos using generative AI. [View demo](https://shotstack.io/demos/social-media-video-maker/). - [reelestate](examples/reelestate) turns static real estate images into fully edited video slideshows. [View demo](https://shotstack.io/demos/real-estate-video-listing-maker/). diff --git a/examples/in-app-video-creation/.env.example b/examples/in-app-video-creation/.env.example new file mode 100644 index 0000000..5e8bfd2 --- /dev/null +++ b/examples/in-app-video-creation/.env.example @@ -0,0 +1,2 @@ +# https://dashboard.shotstack.io/register +SHOTSTACK_API_KEY= diff --git a/examples/in-app-video-creation/.gitignore b/examples/in-app-video-creation/.gitignore new file mode 100644 index 0000000..713d500 --- /dev/null +++ b/examples/in-app-video-creation/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.env diff --git a/examples/in-app-video-creation/README.md b/examples/in-app-video-creation/README.md new file mode 100644 index 0000000..b1a54e6 --- /dev/null +++ b/examples/in-app-video-creation/README.md @@ -0,0 +1,83 @@ +# In-app video creation + +One promo video, three ways for a user to create it in your app: an embedded Studio SDK editor, a +quick form, and a single button with no interface at all. All three paths submit to the same render +proxy. The proxy holds the API key, records which user owns each render, validates user-supplied +asset URLs, and rate limits each user. + +Companion code for [Add video creation to your app without building an editor](https://shotstack.io/learn/add-video-creation-to-your-app/). + +## Requirements + +- A [Shotstack account](https://dashboard.shotstack.io/register) and your **sandbox** API key + (dashboard menu under your account name, top right, under **API Keys**) +- Node.js 20 or later +- A browser with WebGL for the editor tab. If WebGL is not available, the app shows a message and + the other two tabs still work. + +Sandbox renders are watermarked, and your account needs at least one credit to use the environment. + +## Setup + +```bash +git clone https://github.com/shotstack/shotstack-cookbook.git +cd shotstack-cookbook/examples/in-app-video-creation +npm install +``` + +Copy the environment file. Add your sandbox key to `.env`. + +```bash +cp .env.example .env +``` + +## Run + +The example runs as two processes. Open two terminals. + +In terminal 1, load the environment file and start the render proxy. Only this process reads the +API key. Start it first: + +```bash +set -a +source .env +set +a +npm run server +``` + +In terminal 2, start the web app: + +```bash +npm run dev +``` + +Open http://localhost:5173 in your browser. + +## What happens + +The render proxy starts on port 8787 and the web app on port 5173. The app shows three tabs above +one shared gallery: + +- **Editor** mounts the Studio SDK with the edit in `public/promo.json`. Change the video, then + click **Render this edit**. The app saves a draft on every change and restores it on reload. + **Start over** discards the draft. +- **Quick form** sends three form fields as merge values for the template in `template.json`. The + proxy creates the template on the first form render and prints the template id. Set + `SHOTSTACK_TEMPLATE_ID` to reuse a template between restarts. +- **One click** builds the Edit JSON in code and submits it. No editor and no form. + +Each path shows its progress in a status line. A sandbox render finishes in under a minute. When a +render is done, the app adds the video to the gallery. The gallery only shows renders that belong +to the current user, because the proxy records the owner of every render before it responds. + +The proxy limits each user to 10 renders per hour. Set `RATE_LIMIT` to change the cap. The proxy +rejects user-supplied asset URLs that are not HTTPS, resolve to a private address, or are larger +than 100 MB. + +The proxy renders in the sandbox by default. To render without the watermark, set +`SHOTSTACK_ENV=v1` and use your production key. Templates belong to the environment they were +created in, so the proxy creates a new one on the first form render in each environment. + +The webhook receiver at `/hooks/render` does not fire on localhost, so the app polls instead. To +test the webhook, expose the proxy through a tunnel and set `callback` on a render. See the guide +for details. diff --git a/examples/in-app-video-creation/index.html b/examples/in-app-video-creation/index.html new file mode 100644 index 0000000..f62ac3d --- /dev/null +++ b/examples/in-app-video-creation/index.html @@ -0,0 +1,243 @@ + + + + + + In-app video creation: three paths + + + + +
+

Make a promo video

+
One template, three ways to drive it.
+
+ +
+ + + +
+ + +
+
+
+

+ + +
+ + + + + + + +
+ +
+ + + + diff --git a/examples/in-app-video-creation/main.js b/examples/in-app-video-creation/main.js new file mode 100644 index 0000000..82f6e68 --- /dev/null +++ b/examples/in-app-video-creation/main.js @@ -0,0 +1,261 @@ +/** + * Three ways to let a user make the same video: + * + * 1. Embedded editor: the Studio SDK, user composes freely + * 2. Form-to-video: a template plus merge fields, no editor + * 3. Headless: no UI at all, the app assembles the edit + * + * All three end at the same render proxy and the same gallery. + * + * The Shotstack API key never appears here. Renders go through /api/render on + * our own server, which holds the key. A key in browser code exposes your + * entire render budget to anyone who opens devtools. + */ + +import { + Edit, + Canvas, + Controls, + Timeline, + UIController +} from '@shotstack/shotstack-studio'; + +const DRAFT_KEY = 'demo:draft'; + +// --------------------------------------------------------------------------- +// Tabs +// --------------------------------------------------------------------------- + +const tabs = document.querySelectorAll('.tab'); +tabs.forEach(tab => { + tab.addEventListener('click', () => { + tabs.forEach(t => { + const selected = t === tab; + t.setAttribute('aria-selected', String(selected)); + document.getElementById(`panel-${t.dataset.panel}`).hidden = !selected; + }); + }); +}); + +// --------------------------------------------------------------------------- +// Option 1: embedded editor +// --------------------------------------------------------------------------- + +const editorStatus = document.getElementById('editor-status'); +let edit; + +async function mountEditor() { + // promo.json holds real asset URLs, not merge-field placeholders. The editor + // renders what it loads, so a "{{ FOOTAGE }}" string here would be treated as + // a URL and fail. Placeholders belong in the template used by the form path. + const saved = localStorage.getItem(DRAFT_KEY); + const template = saved + ? JSON.parse(saved) + : await (await fetch('/promo.json')).json(); + + edit = new Edit(template); + const canvas = new Canvas(edit); + + // mergeFields: true adds a Merge Fields panel (off by default). The SDK also + // resolves a merge array client-side, so an edit carrying one previews the + // replaced text rather than the raw placeholder. + // selectionHandles: false would make this look-but-don't-touch. + const ui = UIController.create(edit, canvas, { mergeFields: true }); + + await canvas.load(); + await edit.load(); + + const timeline = new Timeline( + edit, + document.querySelector('[data-shotstack-timeline]') + ); + await timeline.load(); + + const controls = new Controls(edit); + await controls.load(); + + // The SDK persists nothing on its own. A saved getEdit() snapshot is a draft. + // In a real app this is a debounced POST keyed by user; localStorage only + // survives on one device. + edit.events.on('edit:changed', () => { + editorStatus.textContent = `${edit.totalDuration.toFixed(1)}s · draft saved`; + localStorage.setItem(DRAFT_KEY, JSON.stringify(edit.getEdit())); + }); + + return { edit, canvas, ui, timeline, controls }; +} + +try { + await mountEditor(); +} catch (err) { + // The SDK renders through WebGL and throws WebGLUnsupportedError where it + // isn't available. Locked-down and virtualized browsers land here. + editorStatus.textContent = + "The editor can't run in this browser. Use the Quick form tab instead."; + console.error(err); +} + +document + .getElementById('render-from-editor') + .addEventListener('click', async e => { + if (!edit) return; + await run(e.target, editorStatus, () => + // getEdit() returns the same JSON shape the render API accepts. + submitRender({ edit: edit.getEdit() }) + ); + }); + +// Discarding a draft is the other half of saving one. Without this the editor +// loads whatever you last touched, forever, with no way back to the template. +document.getElementById('reset-draft').addEventListener('click', () => { + localStorage.removeItem(DRAFT_KEY); + location.reload(); +}); + +// --------------------------------------------------------------------------- +// Option 3: form to video +// --------------------------------------------------------------------------- + +const formStatus = document.getElementById('form-status'); + +document.getElementById('quick-form').addEventListener('submit', async e => { + e.preventDefault(); + const data = Object.fromEntries(new FormData(e.target)); + + await run(e.target.querySelector('button'), formStatus, () => + submitRender({ + merge: [ + { find: 'HEADLINE', replace: data.headline }, + { find: 'FONT', replace: data.font }, + { find: 'FOOTAGE', replace: data.footage } + ] + }) + ); +}); + +// --------------------------------------------------------------------------- +// Option 2: headless +// --------------------------------------------------------------------------- + +const headlessStatus = document.getElementById('headless-status'); + +document + .getElementById('render-headless') + .addEventListener('click', async e => { + // No editor, no form. The app already knows what the video should say. + const listing = { + headline: '12 new listings this week.', + footageUrl: + 'https://shotstack-assets.s3-ap-southeast-2.amazonaws.com/footage/city-timelapse.mp4' + }; + + await run(e.target, headlessStatus, () => + submitRender({ + edit: { + timeline: { + background: '#000000', + tracks: [ + { + // tracks[0] is the TOP layer: text first, background last. + clips: [ + { + asset: { + type: 'rich-text', + text: listing.headline, + font: { + family: 'Montserrat', + size: 48, + weight: 700, + color: '#ffffff' + }, + stroke: { width: 3, color: '#000000' }, + align: { horizontal: 'center', vertical: 'middle' } + }, + start: 0.5, + length: 4.5, + width: 1000, + height: 260 + } + ] + }, + { + clips: [ + { + asset: { type: 'video', src: listing.footageUrl }, + start: 0, + length: 5, + // crop fills the frame and preserves aspect ratio. + // cover would STRETCH the footage, not the CSS meaning. + fit: 'crop' + } + ] + } + ] + }, + output: { format: 'mp4', size: { width: 1280, height: 720 } } + } + }) + ); + }); + +// --------------------------------------------------------------------------- +// Shared: submit, poll, display +// --------------------------------------------------------------------------- + +async function run(button, statusEl, submit) { + button.disabled = true; + statusEl.textContent = 'Submitting…'; + + try { + const renderId = await submit(); + await pollAndShow(renderId, statusEl); + } catch (err) { + statusEl.textContent = `Failed: ${err.message}`; + } finally { + button.disabled = false; + } +} + +async function submitRender(payload) { + const res = await fetch('/api/render', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + const body = await res.json(); + if (!res.ok) throw new Error(body.error ?? res.statusText); + return body.id; +} + +/** + * Polling is fine for one user watching a spinner. Anything server-side or at + * volume should use the callback webhook; see server.js. + */ +async function pollAndShow(renderId, statusEl) { + for (let attempt = 0; attempt < 60; attempt++) { + await new Promise(r => setTimeout(r, 3000)); + + const res = await fetch(`/api/render/${renderId}`); + const { status, url, error } = await res.json(); + + statusEl.textContent = `${status}…`; + + if (status === 'done') { + statusEl.textContent = 'Done.'; + addToGallery(url); + return url; + } + + if (status === 'failed') throw new Error(error ?? 'render failed'); + } + + throw new Error('timed out'); +} + +function addToGallery(url) { + const video = document.createElement('video'); + video.src = url; + video.controls = true; + document.getElementById('gallery').prepend(video); +} diff --git a/examples/in-app-video-creation/package-lock.json b/examples/in-app-video-creation/package-lock.json new file mode 100644 index 0000000..ce736b8 --- /dev/null +++ b/examples/in-app-video-creation/package-lock.json @@ -0,0 +1,2141 @@ +{ + "name": "in-app-video-creation", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "in-app-video-creation", + "version": "1.0.0", + "dependencies": { + "@shotstack/shotstack-studio": "^2.15.1" + }, + "devDependencies": { + "vite": "^5.4.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "license": "MIT", + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.0.tgz", + "integrity": "sha512-3hNKJObUK7JsCF9aJlVCs1J0/KE/gGfZNeK8MO1ge6bB3aicr5walGme9t9No1f/oyk9GgvdAT/rjSdsx3gbIw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.0.tgz", + "integrity": "sha512-ZIja19/BiGz2puhki+WUYSRriwFeFJ8Mi9eK3hZdSS85w4Y60cuEAJVhMCfKwswQkKkUtrnzdKMBuO7TupvexA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.0.tgz", + "integrity": "sha512-hImggWc82jqZVpEsFR9S7PE9OQYjq/H/D7vwCGB6X1jRH+UVBP1+1niJTPBOat1B154T6GKK7/kcFtoWgjgFzQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.0.tgz", + "integrity": "sha512-hlJRy6d+kWLKVOG/+1rEvNQVURZ0DxxRPJsLmEWwhwiXZUJc0BF5o9esALHSEP4CoJK4wChRtj3hnyBgVx2oWA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.0.tgz", + "integrity": "sha512-5Hru4T3RXkosRQafcjelv7AUzw9mXqmGYsxnzeDDOWveFCJyEPMSJltvGCM+jfH98seOCbfwm9KyFg6Jm5FhAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.0.tgz", + "integrity": "sha512-LTUl9jS8WsLSUGaxQZKQkxfluOJRpgvBuxxdM4pYcjib+di8AU4OzQc6+L6SzGMLcKc9H0RAjojRatBhTMqYdg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.0.tgz", + "integrity": "sha512-Iz931SAZf+WVDzpjk52Q3ffW3zw0YflFwEZMgs036Wfu1kX/LrwT9wGjsuSqyduqefUkl91/vTdAjn8hQu5ezA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.0.tgz", + "integrity": "sha512-pFEQ5eFK4JusgN1K6KkO9DKP/Hi1WMJOkF8Ch03/khTc4bFbCKkCCsJG4YcOMOW9bI4XbT2/eMAWxhO0xaWgPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.0.tgz", + "integrity": "sha512-jnvr8NrLHiZ3NCiOKWqDbkI4Ah+QDrqtZ+sddPZBltEb1mQ2coSvCSJYfict+oAwcm0c970oTmVySpjKP/lnaA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.0.tgz", + "integrity": "sha512-y2j9/Gfd5joqiqxdP/L1smqjQ+uAx3C4N0EC7bDHrnZEEH8ToM/OC5p3uHvtj4Lq591aHj+ArL01UDLNwT5HgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.0.tgz", + "integrity": "sha512-qwdhh9N6Gge/hC4pL9S1tQp0iKwhSl/dYjg7+RGp9k26iRGRi5MqqUyKGOXIWli0zOcuy5Y2wIH/jk2ry6i/jA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@pixi/colord": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", + "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==", + "license": "MIT" + }, + "node_modules/@resvg/resvg-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", + "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==", + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@resvg/resvg-js-android-arm-eabi": "2.6.2", + "@resvg/resvg-js-android-arm64": "2.6.2", + "@resvg/resvg-js-darwin-arm64": "2.6.2", + "@resvg/resvg-js-darwin-x64": "2.6.2", + "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", + "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", + "@resvg/resvg-js-linux-arm64-musl": "2.6.2", + "@resvg/resvg-js-linux-x64-gnu": "2.6.2", + "@resvg/resvg-js-linux-x64-musl": "2.6.2", + "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", + "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", + "@resvg/resvg-js-win32-x64-msvc": "2.6.2" + } + }, + "node_modules/@resvg/resvg-js-android-arm-eabi": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz", + "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-android-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz", + "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz", + "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz", + "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz", + "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz", + "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz", + "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", + "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", + "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-arm64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz", + "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-ia32-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz", + "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==", + "cpu": [ + "ia32" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-x64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz", + "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-wasm": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.6.2.tgz", + "integrity": "sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==", + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shotstack/schemas": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@shotstack/schemas/-/schemas-1.18.0.tgz", + "integrity": "sha512-Fh9dxIzBqLyWf6jc1pCvpQDdk8OFRO+y1zka5D5gBlUWn2Zih4p7MH1SxsVCMFqkyfQ32D1TpjRD/3roEAIN5g==", + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@shotstack/shotstack-canvas": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@shotstack/shotstack-canvas/-/shotstack-canvas-2.11.0.tgz", + "integrity": "sha512-X8C510X8dFy053Nkl8zS6IAV5ePmvbg5LXZafaTjzPVY6Uk/mK1t7QEzOT8v3KmaaBdJVs4zHh47JAISL0ZrDA==", + "hasInstallScript": true, + "dependencies": { + "@resvg/resvg-js": "^2.6.2", + "@resvg/resvg-wasm": "^2.6.2", + "@shotstack/schemas": "1.17.0", + "bidi-js": "^1.0.3", + "canvas": "npm:@napi-rs/canvas@1.0.0", + "ffmpeg-static": "^5.2.0", + "fontkit": "^2.0.4", + "harfbuzzjs": "0.4.12", + "lru-cache": "~11.2.5", + "mp4-muxer": "^5.1.3", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@shotstack/shotstack-canvas/node_modules/@shotstack/schemas": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@shotstack/schemas/-/schemas-1.17.0.tgz", + "integrity": "sha512-ilFAJcO8vZ9X/vC0uri8ZX+cM1BMnvx0cnUiU4sdVbPmJVdN0uta2XBL1rYWC1G/0GZUSZc9/ql0EwDSk0n4mw==", + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@shotstack/shotstack-studio": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@shotstack/shotstack-studio/-/shotstack-studio-2.18.0.tgz", + "integrity": "sha512-Yw73jEXXVAzmSTclaHf/MVsfodVQNyT26BI2gdXahtsJuhs2kARMX7ZNGnGrxxvK1QS2UHXQ3n6Q5hOeDbmz7A==", + "license": "PolyForm Shield License 1.0.0", + "dependencies": { + "@shotstack/schemas": "1.18.0", + "@shotstack/shotstack-canvas": "^2.10.2", + "howler": "^2.2.4", + "mediabunny": "^1.11.2", + "opentype.js": "^1.3.4", + "pixi-filters": "^6.0.5", + "pixi.js": "^8.15.0", + "zod": "^4.0.0" + }, + "engines": { + "node": ">=20 <=24" + } + }, + "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/@types/dom-mediacapture-transform": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.12.tgz", + "integrity": "sha512-d7/QsLRwF864A5mgIM/YrfiglHoYn7zgCcAoJgW404r+2DwnNr7EBbLnCWpmOMgH8y0te73L1AV6H1bmauaWFw==", + "license": "MIT", + "dependencies": { + "@types/dom-webcodecs": "*" + } + }, + "node_modules/@types/dom-webcodecs": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", + "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", + "license": "MIT" + }, + "node_modules/@types/earcut": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-3.0.0.tgz", + "integrity": "sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/gradient-parser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@types/gradient-parser/-/gradient-parser-0.1.5.tgz", + "integrity": "sha512-r7K3NkJz3A95WkVVmjs0NcchhHstC2C/VIYNX4JC6tieviUNo774FFeOHjThr3Vw/WCeMP9kAT77MKbIRlO/4w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", + "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/wicg-file-system-access": { + "version": "2020.9.8", + "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.8.tgz", + "integrity": "sha512-ggMz8nOygG7d/stpH40WVaNvBwuyYLnrg5Mbyf6bmsj/8+gb6Ei4ZZ9/4PNpcPNTT8th9Q8sM8wYmWGjMWLX/A==", + "license": "MIT" + }, + "node_modules/@webgpu/types": { + "version": "0.1.72", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.72.tgz", + "integrity": "sha512-0cF7RFM2edNoiIS1ODJp0/Gzv4/xSXhwoR0YCza+OWpJWtn4wmo9DvK91aLlH9+uUnwIriP7ZiC3WitmyhuzBw==", + "license": "BSD-3-Clause" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", + "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/canvas": { + "name": "@napi-rs/canvas", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.0.tgz", + "integrity": "sha512-Jqxcy1XOIqj+lH9sl1GT+il6GR3uQv13vI2mrwubP3uT8Olak2ClDrK2RnxlQKjwv8BRr4b3ug0YR7c6hBX8wg==", + "license": "MIT", + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "1.0.0", + "@napi-rs/canvas-darwin-arm64": "1.0.0", + "@napi-rs/canvas-darwin-x64": "1.0.0", + "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.0", + "@napi-rs/canvas-linux-arm64-gnu": "1.0.0", + "@napi-rs/canvas-linux-arm64-musl": "1.0.0", + "@napi-rs/canvas-linux-riscv64-gnu": "1.0.0", + "@napi-rs/canvas-linux-x64-gnu": "1.0.0", + "@napi-rs/canvas-linux-x64-musl": "1.0.0", + "@napi-rs/canvas-win32-arm64-msvc": "1.0.0", + "@napi-rs/canvas-win32-x64-msvc": "1.0.0" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, + "node_modules/earcut": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.3.tgz", + "integrity": "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==", + "license": "ISC" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/ffmpeg-static": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz", + "integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==", + "hasInstallScript": true, + "license": "GPL-3.0-or-later", + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "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/gifuct-js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz", + "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==", + "license": "MIT", + "dependencies": { + "js-binary-schema-parser": "^2.0.3" + } + }, + "node_modules/harfbuzzjs": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/harfbuzzjs/-/harfbuzzjs-0.4.12.tgz", + "integrity": "sha512-Y2JTrft+uKS1qodeG8Fz04dIBF7+cjl4txiUgiNwox5xyuDTGPukY87BTsXWWxZSexEo66la2o131M9u7p1u2g==", + "license": "MIT" + }, + "node_modules/howler": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz", + "integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==", + "license": "MIT" + }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "license": "MIT", + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/http-response-object/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ismobilejs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", + "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==", + "license": "MIT" + }, + "node_modules/js-binary-schema-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz", + "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/mediabunny": { + "version": "1.55.5", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.55.5.tgz", + "integrity": "sha512-m0v6y8FGXiK+HKOc3AZqU+kJPLYsSKaLitmQNQIIHZKIGlTwQ34OF+X6Ul0K8iV4b03oPse10apwuoqbvmKeAA==", + "license": "MPL-2.0", + "workspaces": [ + ".", + "packages/*" + ], + "dependencies": { + "@types/dom-mediacapture-transform": "^0.1.11", + "@types/dom-webcodecs": "0.1.13" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + } + }, + "node_modules/mp4-muxer": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/mp4-muxer/-/mp4-muxer-5.2.2.tgz", + "integrity": "sha512-dhozjTywI0h2qFzeShagt8YYw811fh1XlwiDCE2f6Aeqf6xG2CyuShoSa5E0AZDO8pPF0JOZ3wOmWBNWIGdSpQ==", + "deprecated": "This library is superseded by Mediabunny. Please migrate to it.", + "license": "MIT", + "dependencies": { + "@types/dom-webcodecs": "^0.1.6", + "@types/wicg-file-system-access": "^2020.9.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "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/opentype.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz", + "integrity": "sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw==", + "license": "MIT", + "dependencies": { + "string.prototype.codepointat": "^0.2.1", + "tiny-inflate": "^1.0.3" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, + "node_modules/parse-svg-path": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.2.0.tgz", + "integrity": "sha512-Tf7FFIrguPKQwzD4pWnYkR2VOv3raoHeKED80Bm+BYHI3KxC8KsgsGC5+fSMzAGDA6UEk4bHvmi+RsjmL3khpg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pixi-filters": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/pixi-filters/-/pixi-filters-6.1.5.tgz", + "integrity": "sha512-Ewb/J+kxAbaNN+0/ATJbglAJG+skGJfh7BIDP3ILIDdD6wWk1p0pGa25pVf1T8hGBOQSUNVAmwwJBwkj+cyLLA==", + "license": "MIT", + "dependencies": { + "@types/gradient-parser": "^0.1.2" + }, + "peerDependencies": { + "pixi.js": ">=8.0.0-0" + } + }, + "node_modules/pixi.js": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.20.1.tgz", + "integrity": "sha512-akLVBMLvQbaEViqsmVraK0Njer1q4Q4lm4iNKuzvBiFrV8q+6/+HluJN3sWIwO663IBeQtUUS/CaXFJZs6ffXQ==", + "license": "MIT", + "workspaces": [ + "examples", + "playground" + ], + "dependencies": { + "@pixi/colord": "^2.9.6", + "@types/earcut": "^3.0.0", + "@webgpu/types": "^0.1.69", + "@xmldom/xmldom": "^0.8.15", + "earcut": "^3.0.2", + "eventemitter3": "^5.0.1", + "gifuct-js": "^2.1.2", + "ismobilejs": "^1.1.1", + "parse-svg-path": "^0.2.0", + "tiny-lru": "^11.4.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tiny-lru": { + "version": "11.4.7", + "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz", + "integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "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" + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", + "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/examples/in-app-video-creation/package.json b/examples/in-app-video-creation/package.json new file mode 100644 index 0000000..bc8590d --- /dev/null +++ b/examples/in-app-video-creation/package.json @@ -0,0 +1,21 @@ +{ + "name": "in-app-video-creation", + "version": "1.0.0", + "private": true, + "description": "Embedded editor, form to video and headless rendering, side by side", + "type": "module", + "scripts": { + "dev": "vite", + "server": "node server.js", + "format": "npx prettier@3 --write ." + }, + "engines": { + "node": ">=20" + }, + "dependencies": { + "@shotstack/shotstack-studio": "^2.15.1" + }, + "devDependencies": { + "vite": "^5.4.0" + } +} diff --git a/examples/in-app-video-creation/public/promo.json b/examples/in-app-video-creation/public/promo.json new file mode 100644 index 0000000..42f62c8 --- /dev/null +++ b/examples/in-app-video-creation/public/promo.json @@ -0,0 +1,69 @@ +{ + "timeline": { + "background": "#000000", + "tracks": [ + { + "clips": [ + { + "asset": { + "type": "rich-text", + "text": "Your message here", + "font": { + "family": "Montserrat", + "size": 48, + "weight": 700, + "color": "#ffffff" + }, + "stroke": { + "width": 3, + "color": "#000000" + }, + "align": { + "horizontal": "center", + "vertical": "middle" + } + }, + "start": 0.5, + "length": 4.5, + "width": 1000, + "height": 260 + } + ] + }, + { + "clips": [ + { + "asset": { + "type": "video", + "src": "https://shotstack-assets.s3-ap-southeast-2.amazonaws.com/footage/city-timelapse.mp4" + }, + "start": 0, + "length": 5, + "fit": "crop" + } + ] + }, + { + "clips": [ + { + "asset": { + "type": "audio", + "src": "https://s3-ap-southeast-2.amazonaws.com/shotstack-assets/music/moment.mp3", + "volume": 0.35, + "effect": "fadeOut" + }, + "start": 0, + "length": "end" + } + ] + } + ] + }, + "output": { + "format": "mp4", + "size": { + "width": 1280, + "height": 720 + } + } +} diff --git a/examples/in-app-video-creation/server.js b/examples/in-app-video-creation/server.js new file mode 100644 index 0000000..1477772 --- /dev/null +++ b/examples/in-app-video-creation/server.js @@ -0,0 +1,348 @@ +/** + * Render proxy. + * + * Four jobs, in order of importance: + * 1. Hold the API key server-side. The browser never sees it. + * 2. Persist render_id -> user_id. There is no endpoint that lists renders, + * so without this you can never build "show me my videos". + * 3. Validate user-supplied URLs before they reach the render API. + * 4. Rate limit per user, so one account can't drain the render budget. + * + * POST /api/render { edit } or { merge } -> { id } + * GET /api/render/:id -> { status, url, error } + * GET /api/renders -> this user's renders + * POST /hooks/render -> 200 (webhook receiver) + * + * Usage: + * export SHOTSTACK_API_KEY=your_sandbox_key + * node server.js # creates the form template on first render + */ + +import { createServer } from 'node:http'; +import { readFile } from 'node:fs/promises'; +import { lookup } from 'node:dns/promises'; + +const PORT = Number(process.env.PORT ?? 8787); +const ENV = process.env.SHOTSTACK_ENV ?? 'stage'; +const API = `https://api.shotstack.io/edit/${ENV}`; +const API_KEY = process.env.SHOTSTACK_API_KEY; + +/** + * Per-user render cap per window. Credits are account-level; this is yours. + * + * Note the check-then-record shape below: the count is written after a submit + * succeeds, so a batch of simultaneous requests can all pass the check before + * any of them records. Fine for a demo, not for production. There you would + * want an atomic increment in Redis or your database. + */ +const RATE_LIMIT = Number(process.env.RATE_LIMIT ?? 10); +const RATE_WINDOW_MS = 60 * 60 * 1000; + +/** Largest asset we'll let a user point us at. */ +const MAX_ASSET_BYTES = 100 * 1024 * 1024; + +if (!API_KEY) { + console.error( + 'SHOTSTACK_API_KEY is not set. Get a free sandbox key at dashboard.shotstack.io' + ); + process.exit(1); +} + +if (!['stage', 'v1'].includes(ENV)) { + console.error('SHOTSTACK_ENV must be stage or v1.'); + process.exit(1); +} + +const headers = { + 'Content-Type': 'application/json', + 'x-api-key': API_KEY +}; + +/** Stand-in for your database. */ +const renders = []; // { renderId, userId, status, url, createdAt } +const rateLog = new Map(); // userId -> timestamps + +let templateId = process.env.SHOTSTACK_TEMPLATE_ID ?? null; + +const json = (res, code, body) => { + res.writeHead(code, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(body)); +}; + +const readBody = req => + new Promise(resolve => { + let body = ''; + req.on('data', c => (body += c)); + req.on('end', () => resolve(body)); + }); + +/** Reduce an API error response to one line the user can act on. */ +async function apiError(res) { + const text = await res.text(); + try { + const body = JSON.parse(text); + return ( + body.errors?.[0]?.detail ?? + body.response?.error ?? + body.response?.message ?? + body.message ?? + text + ); + } catch { + return text; + } +} + +// --------------------------------------------------------------------------- +// Template bootstrap, so the form path works on a fresh clone +// --------------------------------------------------------------------------- + +async function ensureTemplate() { + if (templateId) return templateId; + + const template = JSON.parse( + await readFile(new URL('./template.json', import.meta.url), 'utf8') + ); + + const res = await fetch(`${API}/templates`, { + method: 'POST', + headers, + body: JSON.stringify(template) + }); + + if (!res.ok) + throw new Error(`could not create template: ${await apiError(res)}`); + + const { response } = await res.json(); + templateId = response.id; + console.log(`Form template created: ${templateId}`); + return templateId; +} + +// --------------------------------------------------------------------------- +// Rate limiting +// --------------------------------------------------------------------------- + +function withinRateLimit(userId) { + const now = Date.now(); + const recent = (rateLog.get(userId) ?? []).filter( + t => now - t < RATE_WINDOW_MS + ); + rateLog.set(userId, recent); + return recent.length < RATE_LIMIT; +} + +// --------------------------------------------------------------------------- +// URL validation +// +// Shotstack fetches assets server-side, but accepting arbitrary user URLs is +// still your problem. Reject anything that isn't https on a public host, then +// HEAD it to check type and size before spending a render on it. +// +// Both render paths go through this: the merge values from the form, and every +// asset src inside a submitted edit. The endpoint is plain HTTP, so treat +// everything that arrives on it as user-supplied, whichever tab sent it. +// +// One known gap: the DNS record can change between the lookup check and the +// HEAD request. Production code should pin the resolved address for the +// request instead of resolving twice. +// --------------------------------------------------------------------------- + +const PRIVATE_V4 = + /^(0\.|10\.|127\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/; + +async function validateAssetUrl(raw) { + let url; + try { + url = new URL(raw); + } catch { + return { ok: false, reason: 'not a valid URL' }; + } + + if (url.protocol !== 'https:') return { ok: false, reason: 'must be https' }; + + // Resolve and check the address, not just the hostname. A public name can + // point at a private address. + try { + const { address } = await lookup(url.hostname); + if ( + PRIVATE_V4.test(address) || + address === '::1' || + address.startsWith('fd') + ) { + return { ok: false, reason: 'resolves to a private address' }; + } + } catch { + return { ok: false, reason: 'host does not resolve' }; + } + + let head; + try { + head = await fetch(url, { method: 'HEAD', redirect: 'follow' }); + } catch { + return { ok: false, reason: 'unreachable' }; + } + + if (!head.ok) return { ok: false, reason: `responded ${head.status}` }; + + const size = Number(head.headers.get('content-length') ?? 0); + if (size > MAX_ASSET_BYTES) { + return { ok: false, reason: `too large (${Math.round(size / 1e6)} MB)` }; + } + + return { ok: true }; +} + +/** Collect every URL-shaped string in an edit (asset src values and the like). */ +function collectUrls(node, urls = []) { + if (Array.isArray(node)) { + node.forEach(n => collectUrls(n, urls)); + } else if (node && typeof node === 'object') { + Object.values(node).forEach(v => collectUrls(v, urls)); + } else if (typeof node === 'string' && /^https?:\/\//i.test(node)) { + urls.push(node); + } + return urls; +} + +/** Check every user-supplied URL before submitting. */ +async function validateUrls(urls) { + for (const url of urls) { + const result = await validateAssetUrl(url); + if (!result.ok) throw new Error(`${url}: ${result.reason}`); + } +} + +// --------------------------------------------------------------------------- +// Render submission +// --------------------------------------------------------------------------- + +async function submitRender(payload) { + if (payload.edit) { + await validateUrls(collectUrls(payload.edit)); + + const res = await fetch(`${API}/render`, { + method: 'POST', + headers, + body: JSON.stringify(payload.edit) + }); + if (!res.ok) throw new Error(await apiError(res)); + return (await res.json()).response.id; + } + + await validateUrls(collectUrls(payload.merge ?? [])); + const id = await ensureTemplate(); + + const res = await fetch(`${API}/templates/render`, { + method: 'POST', + headers, + body: JSON.stringify({ id, merge: payload.merge }) + }); + if (!res.ok) throw new Error(await apiError(res)); + return (await res.json()).response.id; +} + +// --------------------------------------------------------------------------- + +async function handle(req, res) { + const url = new URL(req.url, `http://localhost:${PORT}`); + + // Stand-in for your auth. Never trust a user id from the client in real code. + const userId = req.headers['x-demo-user'] ?? 'demo-user'; + + if (req.method === 'POST' && url.pathname === '/api/render') { + if (!withinRateLimit(userId)) { + return json(res, 429, { + error: `Limit of ${RATE_LIMIT} renders per hour reached` + }); + } + + try { + const id = await submitRender(JSON.parse(await readBody(req))); + + // Persist BEFORE responding. There is no endpoint that lists renders, so + // this row is the only record that this render belongs to this user. + renders.push({ + renderId: id, + userId, + status: 'queued', + url: null, + createdAt: new Date().toISOString() + }); + rateLog.set(userId, [...(rateLog.get(userId) ?? []), Date.now()]); + + return json(res, 200, { id }); + } catch (err) { + return json(res, 500, { error: err.message }); + } + } + + if (req.method === 'GET' && url.pathname === '/api/renders') { + return json( + res, + 200, + renders.filter(r => r.userId === userId) + ); + } + + if (req.method === 'GET' && url.pathname.startsWith('/api/render/')) { + const id = url.pathname.split('/').pop(); + + // Only surface renders this user owns. + const row = renders.find(r => r.renderId === id && r.userId === userId); + if (!row) return json(res, 404, { error: 'not found' }); + + const r = await fetch(`${API}/render/${id}`, { headers }); + if (!r.ok) return json(res, 502, { error: await apiError(r) }); + + const { response } = await r.json(); + Object.assign(row, { status: response.status, url: response.url ?? null }); + + return json(res, 200, { + status: response.status, + url: response.url ?? null, + error: response.error ?? null + }); + } + + // Nothing here fires in local development: Shotstack can't reach localhost, + // and none of the demo's renders set a `callback`, so the app polls instead. + // Expose this endpoint through a tunnel (ngrok, Cloudflare) and add + // `callback: "/hooks/render"` to a render to exercise it. + // + // Respond within 10 seconds or Shotstack retries. Do the work afterwards. + if (req.method === 'POST' && url.pathname === '/hooks/render') { + const body = await readBody(req); + json(res, 200, { received: true }); + + let payload; + try { + payload = JSON.parse(body); + } catch { + return; + } + const row = renders.find(r => r.renderId === payload.id); + + // Failures fire this hook too. Branch on status, don't assume a url. + if (payload.status === 'failed') { + if (row) row.status = 'failed'; + console.error(`render ${payload.id} failed: ${payload.error}`); + } else { + if (row) Object.assign(row, { status: payload.status, url: payload.url }); + console.log(`render ${payload.id} done: ${payload.url}`); + } + return; + } + + json(res, 404, { error: 'not found' }); +} + +createServer((req, res) => { + handle(req, res).catch(err => { + console.error(err.message); + if (!res.headersSent) json(res, 500, { error: err.message }); + else res.end(); + }); +}).listen(PORT, () => { + console.log(`Render proxy on http://localhost:${PORT}`); +}); diff --git a/examples/in-app-video-creation/template.json b/examples/in-app-video-creation/template.json new file mode 100644 index 0000000..2f4f523 --- /dev/null +++ b/examples/in-app-video-creation/template.json @@ -0,0 +1,72 @@ +{ + "name": "In-app promo template", + "template": { + "timeline": { + "background": "#000000", + "tracks": [ + { + "clips": [ + { + "asset": { + "type": "rich-text", + "text": "{{ HEADLINE }}", + "font": { + "family": "{{ FONT }}", + "size": 48, + "weight": 700, + "color": "#ffffff" + }, + "stroke": { + "width": 3, + "color": "#000000" + }, + "align": { + "horizontal": "center", + "vertical": "middle" + } + }, + "start": 0.5, + "length": 4.5, + "width": 1000, + "height": 260 + } + ] + }, + { + "clips": [ + { + "asset": { + "type": "video", + "src": "{{ FOOTAGE }}" + }, + "start": 0, + "length": 5, + "fit": "crop" + } + ] + }, + { + "clips": [ + { + "asset": { + "type": "audio", + "src": "https://s3-ap-southeast-2.amazonaws.com/shotstack-assets/music/moment.mp3", + "volume": 0.35, + "effect": "fadeOut" + }, + "start": 0, + "length": "end" + } + ] + } + ] + }, + "output": { + "format": "mp4", + "size": { + "width": 1280, + "height": 720 + } + } + } +} diff --git a/examples/in-app-video-creation/vite.config.js b/examples/in-app-video-creation/vite.config.js new file mode 100644 index 0000000..287ae7a --- /dev/null +++ b/examples/in-app-video-creation/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { + // The browser never holds the API key. Render calls go to server.js. + proxy: { + '/api': 'http://localhost:8787', + '/hooks': 'http://localhost:8787' + } + } +}); From 0a1e4db739228149c99fab2454a947f2c2ac49c1 Mon Sep 17 00:00:00 2001 From: Musab Date: Tue, 1 Sep 2026 06:17:21 +0500 Subject: [PATCH 2/2] fix: address code review on in-app-video-creation Server: re-fetch the render from the API in the webhook receiver instead of trusting the unauthenticated payload's url, widen the private-address check to IPv4-mapped, unique-local and link-local IPv6, match leading whitespace in the asset-URL collector so " https://..." can't skip validation, and cap the request body. Client: register the form, headless and reset handlers without waiting on the editor mount so they work while it loads, seed the gallery from the user's own renders on load, add the missing audio track to the headless edit so all three paths produce the same video, and report a missing render proxy clearly instead of a JSON parse error. --- examples/in-app-video-creation/main.js | 60 +++++++++++++++++++--- examples/in-app-video-creation/server.js | 65 +++++++++++++++++------- 2 files changed, 100 insertions(+), 25 deletions(-) diff --git a/examples/in-app-video-creation/main.js b/examples/in-app-video-creation/main.js index 82f6e68..d35b315 100644 --- a/examples/in-app-video-creation/main.js +++ b/examples/in-app-video-creation/main.js @@ -85,15 +85,16 @@ async function mountEditor() { return { edit, canvas, ui, timeline, controls }; } -try { - await mountEditor(); -} catch (err) { +// Mount without blocking. The editor loads assets over the network, and a +// top-level await here would leave the form and headless handlers below +// unregistered until it finished, so those tabs would do nothing meanwhile. +mountEditor().catch(err => { // The SDK renders through WebGL and throws WebGLUnsupportedError where it // isn't available. Locked-down and virtualized browsers land here. editorStatus.textContent = "The editor can't run in this browser. Use the Quick form tab instead."; console.error(err); -} +}); document .getElementById('render-from-editor') @@ -189,6 +190,22 @@ document fit: 'crop' } ] + }, + { + // The same music the editor and form paths use, so all three + // produce the same video rather than a silent one here. + clips: [ + { + asset: { + type: 'audio', + src: 'https://s3-ap-southeast-2.amazonaws.com/shotstack-assets/music/moment.mp3', + volume: 0.35, + effect: 'fadeOut' + }, + start: 0, + length: 'end' + } + ] } ] }, @@ -223,8 +240,15 @@ async function submitRender(payload) { body: JSON.stringify(payload) }); - const body = await res.json(); - if (!res.ok) throw new Error(body.error ?? res.statusText); + // A 500 from the Vite proxy when the render server is down has an empty body, + // so read defensively rather than assuming JSON. + const body = await res.json().catch(() => null); + if (!res.ok || !body?.id) { + throw new Error( + body?.error ?? + `${res.status}. Is the render proxy running? Start it with npm run server.` + ); + } return body.id; } @@ -237,8 +261,11 @@ async function pollAndShow(renderId, statusEl) { await new Promise(r => setTimeout(r, 3000)); const res = await fetch(`/api/render/${renderId}`); - const { status, url, error } = await res.json(); + const body = await res.json().catch(() => ({})); + if (!res.ok) + throw new Error(body.error ?? `status check failed (${res.status})`); + const { status, url, error } = body; statusEl.textContent = `${status}…`; if (status === 'done') { @@ -259,3 +286,22 @@ function addToGallery(url) { video.controls = true; document.getElementById('gallery').prepend(video); } + +// On load, show this user's finished renders. The proxy records the owner of +// every render, so /api/renders returns only theirs, and the gallery survives +// a page reload instead of only filling as renders finish this session. +async function loadGallery() { + try { + const rows = await fetch('/api/renders').then(r => (r.ok ? r.json() : [])); + for (const row of rows.reverse()) { + const render = await fetch(`/api/render/${row.renderId}`) + .then(r => (r.ok ? r.json() : null)) + .catch(() => null); + if (render?.status === 'done' && render.url) addToGallery(render.url); + } + } catch { + // The gallery just starts empty if the proxy is unreachable. + } +} + +loadGallery(); diff --git a/examples/in-app-video-creation/server.js b/examples/in-app-video-creation/server.js index 1477772..39206b4 100644 --- a/examples/in-app-video-creation/server.js +++ b/examples/in-app-video-creation/server.js @@ -69,11 +69,20 @@ const json = (res, code, body) => { res.end(JSON.stringify(body)); }; +const MAX_BODY_BYTES = 1024 * 1024; // an edit is small; cap it so a huge POST can't grow the heap + const readBody = req => - new Promise(resolve => { + new Promise((resolve, reject) => { let body = ''; - req.on('data', c => (body += c)); + req.on('data', c => { + body += c; + if (body.length > MAX_BODY_BYTES) { + reject(new Error('request body too large')); + req.destroy(); + } + }); req.on('end', () => resolve(body)); + req.on('error', reject); }); /** Reduce an API error response to one line the user can act on. */ @@ -151,6 +160,19 @@ function withinRateLimit(userId) { const PRIVATE_V4 = /^(0\.|10\.|127\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/; +function isPrivateAddress(address) { + // An IPv4-mapped IPv6 address (::ffff:169.254.169.254) hides a v4 address; unwrap it. + const v4 = address.replace(/^::ffff:/i, ''); + if (PRIVATE_V4.test(v4)) return true; + const a = address.toLowerCase(); + return ( + a === '::1' || // loopback + a.startsWith('fc') || // unique local, fc00::/7 + a.startsWith('fd') || + a.startsWith('fe80') // link-local + ); +} + async function validateAssetUrl(raw) { let url; try { @@ -165,11 +187,7 @@ async function validateAssetUrl(raw) { // point at a private address. try { const { address } = await lookup(url.hostname); - if ( - PRIVATE_V4.test(address) || - address === '::1' || - address.startsWith('fd') - ) { + if (isPrivateAddress(address)) { return { ok: false, reason: 'resolves to a private address' }; } } catch { @@ -185,7 +203,9 @@ async function validateAssetUrl(raw) { if (!head.ok) return { ok: false, reason: `responded ${head.status}` }; - const size = Number(head.headers.get('content-length') ?? 0); + // A missing content-length (chunked responses) means we can't size the asset + // here, so it passes this check; the render itself still enforces plan limits. + const size = Number(head.headers.get('content-length')); if (size > MAX_ASSET_BYTES) { return { ok: false, reason: `too large (${Math.round(size / 1e6)} MB)` }; } @@ -199,7 +219,9 @@ function collectUrls(node, urls = []) { node.forEach(n => collectUrls(n, urls)); } else if (node && typeof node === 'object') { Object.values(node).forEach(v => collectUrls(v, urls)); - } else if (typeof node === 'string' && /^https?:\/\//i.test(node)) { + } else if (typeof node === 'string' && /^\s*https?:\/\//i.test(node)) { + // Match leading whitespace too, so " https://…" can't skip validation and + // then be submitted verbatim. new URL() normalizes the surrounding space. urls.push(node); } return urls; @@ -322,15 +344,22 @@ async function handle(req, res) { return; } const row = renders.find(r => r.renderId === payload.id); - - // Failures fire this hook too. Branch on status, don't assume a url. - if (payload.status === 'failed') { - if (row) row.status = 'failed'; - console.error(`render ${payload.id} failed: ${payload.error}`); - } else { - if (row) Object.assign(row, { status: payload.status, url: payload.url }); - console.log(`render ${payload.id} done: ${payload.url}`); - } + if (!row) return; + + // The webhook body is unauthenticated: anyone who can reach this URL can + // POST it. Treat it only as a signal to re-fetch the render from the API + // with our key, so a forged payload can't write an attacker's url onto a + // user's render. + const verified = await fetch(`${API}/render/${payload.id}`, { headers }) + .then(r => (r.ok ? r.json() : null)) + .catch(() => null); + if (!verified) return; + + const { status, url: renderUrl, error } = verified.response; + Object.assign(row, { status, url: renderUrl ?? null }); + if (status === 'failed') + console.error(`render ${payload.id} failed: ${error}`); + else console.log(`render ${payload.id} ${status}: ${renderUrl ?? ''}`); return; }