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
+
+
+
+
+
+
+
+
+ Editor
+
+
+ Quick form
+
+
+ One click
+
+
+
+
+
+
+
+
+ Render this edit
+ Start over
+
+
+
+
+
+
+
+
+ No editor and no form. The app already knows enough to build the video,
+ so it assembles the Edit JSON itself and renders it. This is the shape
+ you want when video is an output of your product rather than its
+ purpose.
+
+
+ Generate this week's listing video
+
+
+
+
+
+
+
+
+
diff --git a/examples/in-app-video-creation/main.js b/examples/in-app-video-creation/main.js
new file mode 100644
index 0000000..d35b315
--- /dev/null
+++ b/examples/in-app-video-creation/main.js
@@ -0,0 +1,307 @@
+/**
+ * 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 };
+}
+
+// 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')
+ .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'
+ }
+ ]
+ },
+ {
+ // 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'
+ }
+ ]
+ }
+ ]
+ },
+ 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)
+ });
+
+ // 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;
+}
+
+/**
+ * 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 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') {
+ 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);
+}
+
+// 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/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..39206b4
--- /dev/null
+++ b/examples/in-app-video-creation/server.js
@@ -0,0 +1,377 @@
+/**
+ * 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 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, reject) => {
+ let body = '';
+ 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. */
+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\.)/;
+
+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 {
+ 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 (isPrivateAddress(address)) {
+ 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}` };
+
+ // 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)` };
+ }
+
+ 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' && /^\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;
+}
+
+/** 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);
+ 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;
+ }
+
+ 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'
+ }
+ }
+});