From c4607c079b257512260d8bc8fd26ab4548c32b76 Mon Sep 17 00:00:00 2001 From: "elliot.fouchy" Date: Sat, 31 Oct 2026 03:20:58 +0100 Subject: [PATCH 1/3] feat: extract the existing Open WebUI deployment Preserve the runtime and configuration from pollinations/pollinations at 76898077586a6047046772e3566fcb27307cc1ae. Add isolated production CI and health verification. Co-authored-by: voodoohop <5099901+voodoohop@users.noreply.github.com> --- .github/workflows/deploy.yml | 55 ++ .gitignore | 6 + README.md | 241 +++++ package-lock.json | 1599 +++++++++++++++++++++++++++++++++ package.json | 27 + scripts/apply-ui-defaults.mjs | 47 + scripts/push-image.sh | 25 + scripts/push-secrets.mjs | 61 ++ scripts/verify.mjs | 32 + scripts/verify.test.mjs | 29 + ui-defaults.js | 50 ++ worker.js | 201 +++++ wrangler.jsonc | 84 ++ 13 files changed, 2457 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/apply-ui-defaults.mjs create mode 100644 scripts/push-image.sh create mode 100644 scripts/push-secrets.mjs create mode 100644 scripts/verify.mjs create mode 100644 scripts/verify.test.mjs create mode 100644 ui-defaults.js create mode 100644 worker.js create mode 100644 wrangler.jsonc diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..11d51f6 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,55 @@ +name: Deploy / Cloudflare production + +on: + pull_request: + push: + branches: [main, production] + workflow_dispatch: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Require production for manual deployment + if: github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/production' + run: | + echo 'Run production deployments from the production branch.' >&2 + exit 1 + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-node@v5 + with: + node-version: '24' + cache: npm + - run: npm ci --ignore-scripts --no-audit --no-fund + - run: npm test + - run: npm run check -- --env="" + - run: npm run check -- --env staging + + deploy: + needs: check + if: github.ref == 'refs/heads/production' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + runs-on: ubuntu-latest + environment: production + concurrency: + group: openwebui-production + cancel-in-progress: false + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-node@v5 + with: + node-version: '24' + - run: npm ci --ignore-scripts --no-audit --no-fund + - name: Deploy the existing Worker and container + env: + CLOUDFLARE_ACCOUNT_ID: b6ec751c0862027ba269faf7029b2501 + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + run: node node_modules/wrangler/bin/wrangler.js deploy --env="" + - name: Verify public endpoints and database + run: npm run verify diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c54994 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.dev.vars +.wrangler/ +temp/ +.env* +.dev.vars.* diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0b0d87 --- /dev/null +++ b/README.md @@ -0,0 +1,241 @@ +# Open WebUI for Pollinations + +[![Made with Pollinations](https://raw.githubusercontent.com/pollinations/pollinations/main/packages/ui/src/brand/badge-made-with.svg)](https://pollinations.ai) + +Pollinations deployment and integration for upstream Open WebUI. The runtime +image, database, accounts, chat history, OAuth registration and current domains +are retained from the monorepo deployment. + +Hosted [Open WebUI](https://github.com/open-webui/open-webui) with Pollinations +as its **only** login provider. Users sign in with their Pollinations account; +the consent screen mints a budgeted `sk_` which Open WebUI forwards to +`gen.pollinations.ai` per user (`auth_type: system_oauth`), so every chat is +paid from the signed-in user's own wallet. + +- Public: https://openwebui.pollinations.ai (origin https://openwebui.myceli.ai) +- Staging: https://openwebui-staging.elliot-b6e.workers.dev +- Login: OAuth 2.1 code + PKCE against the environment's Enter URL. Public + client, no secret. Discovery is the RFC 8414 document; Pollinations serves no + `openid-configuration` alias. + +## Layout + +| File | Role | +|------|------| +| `worker.js` | Container class with every Open WebUI setting as env vars, plus a keepalive cron | +| `ui-defaults.js` | Pinned models, prompt suggestions and banner new users see first | +| `scripts/apply-ui-defaults.mjs` | Write those defaults into an existing database through the admin API | +| `wrangler.jsonc` | Container (pre-built image), custom domains, staging env | +| `scripts/push-image.sh` | Mirror the upstream image into the Cloudflare registry | +| `scripts/push-secrets.mjs` | Push `secrets/secrets.vars.json` (sops) to the Worker | +| `.github/workflows/deploy.yml` | Checks pull requests; deploys the existing Worker from `production` | +| `scripts/verify.mjs` | Checks both production addresses and their database health | + +No Dockerfile. Cloudflare cannot pull `ghcr.io`, so the upstream `-slim` image is +pushed once to `registry.cloudflare.com` and referenced from `wrangler.jsonc`. +Deploys then need no Docker, locally or in CI. + +## State + +Container disk is wiped on every sleep. Users, chats and configuration live in Postgres (one +instance on the `monitoring-agents` box, databases `openwebui` and +`openwebui_staging`): +`DATABASE_URL` holds users, chats, config, and the pgvector store. Uploaded +files are still on local disk and do not survive a restart; switch to R2 via +`STORAGE_PROVIDER=s3` when that matters. + +RAG embeds locally with the bundled `sentence-transformers/all-MiniLM-L6-v2` +(`RAG_EMBEDDING_ENGINE` unset). RAG, image and audio all authenticate with a +single static key rather than the per-user OAuth token the chat connection uses, +so pointing them at gen would bill every user's documents to one wallet. The +cost is a ~90 MB model download onto the ephemeral disk after a restart. + +Runtime secrets already stored on each Cloudflare Worker (the encrypted backup +is `secrets/secrets.vars.json`, with separate production and staging sections): + +- `WEBUI_SECRET_KEY`: session signing key. Changing it logs everyone out. +- `DATABASE_URL`: Postgres connection string (staging uses its own database). + +## Config vars are seeded once, not on every boot + +Production uses Chat Completions; staging uses Responses with +`ENABLE_RESPONSES_API_STATEFUL=false`. Staging connects only to staging Gen +and staging Enter, and has no external MCP tool server. Both discover the full +model catalog (`MODEL_IDS=[]`). Managed agents' own configured MCP tools are independent of Open +WebUI's external tool servers. + +Titles, tags and follow-up suggestions use `openai/gpt-5-nano`, not the selected +chat model. For existing databases, set `task.model.external` to that ID; this +global setting applies to existing users too. + +On an existing staging database, also update `openai.api_base_urls`, +`openai.api_configs` (`api_type` and `model_ids`), and +`tool_server.connections` to match the staging configuration. Preserve +users/chats and unrelated settings. Restart the staging container to apply +the new OAuth environment. Its `OAUTH_CLIENT_ID` must be registered in staging +Enter; production client registrations do not exist in staging automatically. + +Every setting in `DEFAULT_CONFIG` (`OPENAI_API_CONFIGS`, `TOOL_SERVER_CONNECTIONS`, +...) is written to the Postgres `config` table only when the key is *missing*: +`Config.seed_defaults` inserts new keys and `Config.get` reads the stored row +first. Editing one of those env vars on a database that has already booted does +nothing. Change the stored row instead, or use the admin API where one exists +(`POST /api/v1/retrieval/embedding/update` both writes the config and rebuilds +the in-memory embedding function, which a bare `UPDATE` does not): + +```bash +ssh community-monitor "sudo docker exec openwebui-postgres \ + psql -U openwebui -d openwebui -c \ + \"select key, value::text from config where key = 'tool_server.connections';\"" +``` + +## What a new user sees + +The catalog behind the connection has 300+ entries, media and community models +included, so the defaults in [`ui-defaults.js`](ui-defaults.js) shape the first +minute: + +- Four everyday models that need no paid balance are pinned for users who have + not pinned any, and new chats open on `openai/gpt-5.4-nano`. The rest of the + picker keeps the order gen serves it in. +- Four prompt suggestions replace the upstream ones. +- A dismissible banner links directly to the user's balance and top-up pages. +- No "share to Open WebUI Community" button (it uploads chats to openwebui.com) + and no arena models. + +Edit `ui-defaults.js` to change them. `worker.js` seeds them as env vars, which +Open WebUI reads only while the setting is missing (see above), so a database +that has already booted keeps the old values. Apply them with the admin API +instead; an admin key or JWT is needed, and the script reads each form before +changing it so `DEFAULT_MODEL_METADATA` (builtin tools off) is not reset: + +```bash +OWUI_URL=https://openwebui.pollinations.ai OWUI_ENTER_URL=https://enter.pollinations.ai OWUI_TOKEN= node scripts/apply-ui-defaults.mjs +``` + +No restart is needed; users see the change on their next page load. A user's +own pins and settings are not overwritten. + +## Restarting the container + +`envVars` on the Container class are applied when the container *starts*, and a +`wrangler deploy` does not restart a running instance (nor does a shorter +`sleepAfter`). To force a fresh container, delete the container application and +deploy again. Users, chats and configuration remain in Postgres; uploaded +files on the ephemeral disk are lost, so check for files before restarting: + +```bash +npx wrangler containers list # find the app id +npx wrangler containers delete +npx wrangler deploy --env staging # retry once; the first + # attempt after a delete can + # fail on the durable object +``` + +The new container cold-starts in a few minutes while it pulls the ~1.5 GB image. +For production the deploy half must run through this repository’s +`Deploy / Cloudflare production` GitHub Actions workflow on `production`. + +## Tool servers + +`https://mcp.pollinations.ai/` (the endpoint is the root path; `/mcp` 404s) is +registered as an MCP tool server with `auth_type: system_oauth`, the same +per-user consent key as the model connection, so a generation started from a +tool call is billed to the signed-in user. Two fields are easy to miss: +`config.enable` must be true, and `config.access_grants` must carry an explicit +public read grant — an empty grant list means admin-only, not everyone. + +That MCP server is opt-in per chat. Open WebUI's *builtin* tools are not: it +appends their specs to every request coming from its UI unless the model sets +`meta.capabilities.builtin_tools` false (`utils/middleware.py`). Models fetched +from a connection have no row in the `model` table, so the only lever is the +global default, `DEFAULT_MODEL_METADATA` / `models.default_metadata`, which +`utils/models.py` applies to them wholesale. We set it to +`{"capabilities": {"builtin_tools": false}}` — without it, managed agents and +community models that reject a `tools` field 400 on every UI chat. + +`models.base_models_cache` is false and `Config.get_many` reads the row per +request, so changing that config row takes effect immediately, for existing +chats and users too. No container restart, no per-chat migration. + +## Update the image + +```bash +# Needs a Docker daemon; use the monitoring-agents EC2 engine over SSH. +DOCKER_HOST=ssh://community-monitor npm run push-image -- 0.11.3 +# then bump containers[].image in wrangler.jsonc (both envs) +``` + +## Development and deployment + +```bash +npm ci --ignore-scripts +npm test +npm run check -- --env="" # production dry run, no deployment +npm run check -- --env staging # staging dry run, no deployment +npm run verify # read-only production health checks +``` + +Changes go through a pull request into `main`, then a separate promotion pull +request from `main` to `production`. A push to `production` deploys automatically; +manual runs must also select `production`. Pull requests and `main` only run +checks. Production deployment always runs through GitHub Actions. + +The `production` GitHub environment needs `CLOUDFLARE_API_TOKEN` for the existing +Cloudflare account and Worker. Restrict that environment to the `production` +branch. The check job has no deployment credential; the deployment job installs +locked dependencies with lifecycle scripts disabled and exposes the token only +to Wrangler. The account ID is public configuration in the workflow. + +Ordinary deploys retain the Worker’s existing `DATABASE_URL` and +`WEBUI_SECRET_KEY`. CI does not receive the database password or SOPS key and +does not push secrets, reseed database settings, rebuild the image or delete the +container. Secret changes are separate, explicitly approved maintenance. + +Staging retains its existing configuration and database. An authorized operator +can run `npm run deploy:staging`; changes to its secrets are separate. The +staging endpoint currently returns HTTP 403 (recorded before extraction on +2026-10-31), so do not treat a successful dry run as a live staging test. + +After the first deployment from this repository, verify the active Worker +version and bindings, Pollinations login, existing chat history, a new chat and +the MCP integration before retiring the monorepo deployment. Retiring the old +`apps/openwebui/deploy.json` prevents two repositories from deploying the same +Worker. The domain move to `.app` and any sandbox hosting experiment are separate +changes. + +To roll back a code change, revert it through `main` and promote the revert to +`production`. To roll back the repository handoff, disable this repository’s +deployment workflow before restoring monorepo deployment ownership. Keep the +same Worker, Durable Object, image and database; do not recreate them. + +## Placement + +The container is constrained to `ENAM` (`containers[].constraints.regions` in +`wrangler.jsonc`) because Postgres is in AWS us-east-1. Unconstrained, Cloudflare +starts the container nearest to whichever request woke it (it ran in Riga), and +every DB session then costs ~3 transatlantic round trips (~0.45 s), which made +each page load ~20 s. A deploy that changes the constraint rolls the single +instance to the new region: expect ~5 min of 500/503 while the 1.5 GB image +cold-starts. Verified on staging 2026-09-16: `/health/db` 0.9 s → 0.3 s. + +## Login requirements on the Pollinations side + +- App Key (`pk_`) with the exact redirect URIs registered: + `https://openwebui.pollinations.ai/oauth/oidc/callback` and the staging one. + Non-loopback redirects must be HTTPS. +- `OAUTH_CLIENT_SECRET` stays empty. With a secret, authlib switches to Basic + auth and drops `client_id` from the token request, which the Pollinations + token endpoint rejects. +- `OAUTH_SCOPES=profile`: email is only returned with that scope. A user who + unticks "profile" on the consent screen cannot log in. +- There is no refresh grant. The consent key expiry (`OAUTH_AUTHORIZE_PARAMS`) + is the re-login interval for API access. + +## Source and attribution + +Extracted from [Pollinations’ Open WebUI integration at `7689807758`](https://github.com/pollinations/pollinations/tree/76898077586a6047046772e3566fcb27307cc1ae/apps/openwebui). +[Earlier integration history and contributors](https://github.com/pollinations/pollinations/commits/76898077586a6047046772e3566fcb27307cc1ae/apps/openwebui) +remain available in the original repository. This integration is MIT licensed; +[upstream Open WebUI](https://github.com/open-webui/open-webui) retains its own +license and attribution. This repository consumes its pre-built image. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cd531ba --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1599 @@ +{ + "name": "openwebui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openwebui", + "version": "1.0.0", + "dependencies": { + "@cloudflare/containers": "^0.3.7" + }, + "devDependencies": { + "wrangler": "^4.110.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@cloudflare/containers": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@cloudflare/containers/-/containers-0.3.7.tgz", + "integrity": "sha512-DM9dm3FnIBSyiSJ1FLavKwl/lk3oAmTaynCzZQ9pZR0ncRPquSxkxd8Nu2MFILxmDDsPkxKsSNEh9mHHMty4Fw==", + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260831.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260831.1.tgz", + "integrity": "sha512-oyZ8xhu+gYTvoxV/sn6NRmTHK95RhEO1Dk54/6oPb0Uu70w7ZeRoCjkJ5aNmfS8Vrkdu6+oL0HNg6EcC61uQ2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260831.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260831.1.tgz", + "integrity": "sha512-s6Go53KPnoXZ1sTGBZ3en3otfHDuMPJhiwXMYWU21JkJQkpoeRt6HFUwM0GPhK3YhXWm+8baGMvCGZYS/KA9eA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260831.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260831.1.tgz", + "integrity": "sha512-WxNKBgjKgeYTolW3yl1Lt3Lu67UlxdeyzWYi9MIqrKBdyQcz+UNG36RevSBf8rv1sTWapRW234VX2keZ+wXapA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260831.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260831.1.tgz", + "integrity": "sha512-JTF9+9clUT3gaCq7Xnmd+Q/wEMaitpngSTOec/Ffb/r3xexA9XwNJVFSOKfk6q61flHGjAYJ4H9B7Mu5Qur49w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260831.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260831.1.tgz", + "integrity": "sha512-do+KDYw0PABwsrKUQIccWBZB70kqKcADoSnvzJ8pvMaWUVB4qaCspEZYfm97WNdtY1wt8mlKYqIJyYUNOkTvQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz", + "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.1" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz", + "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.1" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz", + "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.2" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz", + "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz", + "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz", + "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz", + "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz", + "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz", + "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz", + "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz", + "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz", + "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz", + "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz", + "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz", + "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz", + "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz", + "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz", + "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz", + "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.1" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz", + "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz", + "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.1" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz", + "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz", + "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.2" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz", + "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz", + "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz", + "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.24", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz", + "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "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/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniflare": { + "version": "5.20260831.0-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260831.0-alpha.tgz", + "integrity": "sha512-Hwgh1VDUiPCPGQKODQfUmy7hRAje1D55icB+9png3ueiM64rlSM87nSrtqpxAD+DlLWI4ehnYBuECaXV43zGmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.2", + "undici": "7.29.0", + "workerd": "1.20260831.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", + "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.2", + "@img/sharp-darwin-x64": "0.35.2", + "@img/sharp-freebsd-wasm32": "0.35.2", + "@img/sharp-libvips-darwin-arm64": "1.3.1", + "@img/sharp-libvips-darwin-x64": "1.3.1", + "@img/sharp-libvips-linux-arm": "1.3.1", + "@img/sharp-libvips-linux-arm64": "1.3.1", + "@img/sharp-libvips-linux-ppc64": "1.3.1", + "@img/sharp-libvips-linux-riscv64": "1.3.1", + "@img/sharp-libvips-linux-s390x": "1.3.1", + "@img/sharp-libvips-linux-x64": "1.3.1", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", + "@img/sharp-libvips-linuxmusl-x64": "1.3.1", + "@img/sharp-linux-arm": "0.35.2", + "@img/sharp-linux-arm64": "0.35.2", + "@img/sharp-linux-ppc64": "0.35.2", + "@img/sharp-linux-riscv64": "0.35.2", + "@img/sharp-linux-s390x": "0.35.2", + "@img/sharp-linux-x64": "0.35.2", + "@img/sharp-linuxmusl-arm64": "0.35.2", + "@img/sharp-linuxmusl-x64": "0.35.2", + "@img/sharp-webcontainers-wasm32": "0.35.2", + "@img/sharp-win32-arm64": "0.35.2", + "@img/sharp-win32-ia32": "0.35.2", + "@img/sharp-win32-x64": "0.35.2" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20260831.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260831.1.tgz", + "integrity": "sha512-A2LwrkBel/FnKABPfeBAMiL6v70+rugnunqQRfWsWZjlhsTZoBScWUVunMy/xLCGLjWCQL2zp39AVR6aO0jurQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260831.1", + "@cloudflare/workerd-darwin-arm64": "1.20260831.1", + "@cloudflare/workerd-linux-64": "1.20260831.1", + "@cloudflare/workerd-linux-arm64": "1.20260831.1", + "@cloudflare/workerd-windows-64": "1.20260831.1" + } + }, + "node_modules/wrangler": { + "version": "4.128.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.128.0.tgz", + "integrity": "sha512-jNXy9e8/pbx8iqTzXPiuflnitKJZoAfEUSUUDLW87bwyeMvJ7kb3yQMSbxEcfNdfHqJW38KRcKaLljOYV4N/4w==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.1", + "miniflare": "5.20260831.0-alpha", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260831.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^5.20260831.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..073c74d --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "openwebui", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "test": "node --check worker.js && node --check ui-defaults.js && node --check scripts/apply-ui-defaults.mjs && node --check scripts/push-secrets.mjs && node --test scripts/verify.test.mjs", + "verify": "node scripts/verify.mjs", + "check": "wrangler deploy --dry-run", + "deploy": "wrangler deploy", + "deploy:staging": "wrangler deploy --env staging", + "push-secrets": "sops exec-file --no-fifo secrets/secrets.vars.json 'node scripts/push-secrets.mjs {} production'", + "push-secrets:staging": "sops exec-file --no-fifo secrets/secrets.vars.json 'node scripts/push-secrets.mjs {} staging'", + "push-image": "bash scripts/push-image.sh", + "containers:list": "wrangler containers list", + "containers:images": "wrangler containers images list" + }, + "dependencies": { + "@cloudflare/containers": "^0.3.7" + }, + "devDependencies": { + "wrangler": "^4.110.0" + }, + "engines": { + "node": ">=22" + } +} diff --git a/scripts/apply-ui-defaults.mjs b/scripts/apply-ui-defaults.mjs new file mode 100644 index 0000000..51f0855 --- /dev/null +++ b/scripts/apply-ui-defaults.mjs @@ -0,0 +1,47 @@ +import { banners, PINNED_MODELS, PROMPT_SUGGESTIONS } from "../ui-defaults.js"; + +// Open WebUI stores these settings on first boot and ignores the env vars +// afterwards, so an existing install has to be changed through the admin API: +// OWUI_URL=https://openwebui.pollinations.ai \ +// OWUI_ENTER_URL=https://enter.pollinations.ai OWUI_TOKEN= \ +// node scripts/apply-ui-defaults.mjs +const base = process.env.OWUI_URL?.replace(/\/$/, ""); +const token = process.env.OWUI_TOKEN; +const enterUrl = process.env.OWUI_ENTER_URL?.replace(/\/$/, ""); +if (!base || !token || !enterUrl) { + console.error("Set OWUI_URL, OWUI_ENTER_URL, and OWUI_TOKEN (an admin API key or JWT)."); + process.exit(1); +} + +async function api(path, body) { + const response = await fetch(`${base}/api/v1${path}`, { + method: body ? "POST" : "GET", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: body && JSON.stringify(body), + }); + if (!response.ok) { + throw new Error(`${path}: ${response.status} ${await response.text()}`); + } + return response.json(); +} + +// The models and admin forms replace every field they carry, so read them +// first and change only ours. Skipping that would reset +// DEFAULT_MODEL_METADATA, which turns off the builtin tools managed agents +// reject. +await api("/configs/models", { + ...(await api("/configs/models")), + DEFAULT_MODELS: PINNED_MODELS[0], + DEFAULT_PINNED_MODELS: PINNED_MODELS.join(","), +}); +await api("/configs/suggestions", { suggestions: PROMPT_SUGGESTIONS }); +await api("/configs/banners", { banners: banners(enterUrl) }); +await api("/auths/admin/config", { + ...(await api("/auths/admin/config")), + ENABLE_COMMUNITY_SHARING: false, +}); +await api("/evaluations/config", { ENABLE_EVALUATION_ARENA_MODELS: false }); +console.log(`Applied UI defaults to ${base}`); diff --git a/scripts/push-image.sh b/scripts/push-image.sh new file mode 100644 index 0000000..6d43015 --- /dev/null +++ b/scripts/push-image.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Mirror the upstream Open WebUI image into the Cloudflare registry so deploys +# need no Docker build. Cloudflare Containers cannot pull from ghcr.io directly. +# +# Needs a Docker daemon. None runs on our Macs: point DOCKER_HOST at the +# monitoring-agents EC2 box (Docker Engine installed there, 2026-09-03): +# DOCKER_HOST=ssh://ubuntu@3.221.108.127 npm run push-image +# Containers run linux/amd64, so the pull is pinned to that platform even +# though the box is arm64. +set -euo pipefail + +VERSION=${1:-0.11.3} +ACCOUNT_ID=b6ec751c0862027ba269faf7029b2501 +# The OAuth login spans two accounts; pin the one the apps deploy to. +export CLOUDFLARE_ACCOUNT_ID=$ACCOUNT_ID +SOURCE="ghcr.io/open-webui/open-webui:v${VERSION}-slim" +TARGET="open-webui:${VERSION}-slim" + +docker pull --platform linux/amd64 "$SOURCE" +docker tag "$SOURCE" "$TARGET" +npx wrangler containers push "$TARGET" + +echo +echo "Now set containers[].image in wrangler.jsonc (both envs) to:" +echo " registry.cloudflare.com/${ACCOUNT_ID}/${TARGET}" diff --git a/scripts/push-secrets.mjs b/scripts/push-secrets.mjs new file mode 100644 index 0000000..be72024 --- /dev/null +++ b/scripts/push-secrets.mjs @@ -0,0 +1,61 @@ +import { spawn } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +// secrets.vars.json is keyed by environment: { production: {...}, staging: {...} } +const REQUIRED_SECRET_NAMES = ["WEBUI_SECRET_KEY", "DATABASE_URL"]; + +const secretPath = process.argv[2]; +const target = process.argv[3] || "production"; + +if (!secretPath) { + console.error( + "Usage: node scripts/push-secrets.mjs [production|staging|local]", + ); + process.exit(1); +} + +const all = JSON.parse(await readFile(secretPath, "utf8")); +const section = all[target === "local" ? "staging" : target]; +if (!section) { + throw new Error(`No "${target}" section in ${secretPath}`); +} + +const secrets = {}; +for (const name of REQUIRED_SECRET_NAMES) { + if (!section[name]) { + throw new Error(`Missing required secret ${name} for ${target}`); + } + secrets[name] = section[name]; +} + +if (target === "local") { + const lines = Object.entries(secrets).map(([key, value]) => { + const escaped = String(value) + .replaceAll("\\", "\\\\") + .replaceAll('"', '\\"'); + return `${key}="${escaped}"`; + }); + await writeFile(".dev.vars", `${lines.join("\n")}\n`); + console.log("Wrote .dev.vars"); + process.exit(0); +} + +const dir = await mkdtemp(join(tmpdir(), "openwebui-secrets-")); +const filteredPath = join(dir, "secrets.json"); +const args = ["wrangler", "secret", "bulk", filteredPath]; +if (target === "staging") args.push("--env", "staging"); + +try { + await writeFile(filteredPath, JSON.stringify(secrets, null, 2)); + await new Promise((resolve, reject) => { + const child = spawn("npx", args, { stdio: "inherit" }); + child.on("exit", (code) => { + if (code === 0) resolve(); + else reject(new Error(`wrangler secret bulk failed with ${code}`)); + }); + }); +} finally { + await rm(dir, { recursive: true, force: true }); +} diff --git a/scripts/verify.mjs b/scripts/verify.mjs new file mode 100644 index 0000000..e3d973b --- /dev/null +++ b/scripts/verify.mjs @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import { pathToFileURL } from "node:url"; + +export async function verifyHealth(url) { + const response = await fetch(url, { + redirect: "error", + signal: AbortSignal.timeout(15_000), + }); + assert.equal(response.status, 200, `${url}: HTTP ${response.status}`); + assert.equal((await response.json()).status, true, `${url}: unhealthy`); +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + for (const origin of [ + "https://openwebui.pollinations.ai", + "https://openwebui.myceli.ai", + ]) { + for (const path of ["/health", "/health/db"]) { + const url = `${origin}${path}`; + for (let attempt = 1; ; attempt++) { + try { + await verifyHealth(url); + console.log(`Healthy: ${url}`); + break; + } catch (error) { + if (attempt === 12) throw error; + await new Promise((resolve) => setTimeout(resolve, 5_000)); + } + } + } + } +} diff --git a/scripts/verify.test.mjs b/scripts/verify.test.mjs new file mode 100644 index 0000000..b3b9560 --- /dev/null +++ b/scripts/verify.test.mjs @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import { once } from "node:events"; +import { createServer } from "node:http"; +import { test } from "node:test"; +import { verifyHealth } from "./verify.mjs"; + +test("health verification requires a successful, healthy JSON response", async () => { + let status = 200; + let body = JSON.stringify({ status: true }); + const server = createServer((_request, response) => { + response.writeHead(status, { "Content-Type": "application/json" }); + response.end(body); + }).listen(0, "127.0.0.1"); + await once(server, "listening"); + const url = `http://127.0.0.1:${server.address().port}/health`; + try { + await verifyHealth(url); + body = JSON.stringify({ status: false }); + await assert.rejects(verifyHealth(url), /unhealthy/); + body = JSON.stringify({ status: true }); + status = 503; + await assert.rejects(verifyHealth(url), /HTTP 503/); + status = 200; + body = "Sign in"; + await assert.rejects(verifyHealth(url), SyntaxError); + } finally { + await new Promise((resolve) => server.close(resolve)); + } +}); diff --git a/ui-defaults.js b/ui-defaults.js new file mode 100644 index 0000000..0f797ef --- /dev/null +++ b/ui-defaults.js @@ -0,0 +1,50 @@ +// What a new Open WebUI user sees first. worker.js seeds these as env vars on +// a fresh database; scripts/apply-ui-defaults.mjs writes the same values into +// an existing one. + +// Pinned for users who have not pinned any: everyday text models that need no +// paid balance, cheapest first. The first one is also the default for new +// chats. The rest of the picker keeps the order gen serves it in. Ids that +// leave the catalog drop out. +export const PINNED_MODELS = [ + "openai/gpt-5.4-nano", + "openai/gpt-6-luna", + "openai/gpt-6.1-sol", + "moonshotai/kimi-k3", +]; + +// Ideas that work with any chat model (the upstream set is about options +// trading and kids' art). +export const PROMPT_SUGGESTIONS = [ + { + title: ["Explain this code", "paste it and ask what it does"], + content: + "Explain what a piece of code does, step by step, and point out any bugs. Ask me to paste it first.", + }, + { + title: ["Draft a message", "that is clear and polite"], + content: + "Help me write a short, friendly message to a colleague who missed a deadline. Ask me what happened first.", + }, + { + title: ["Compare options", "and recommend one"], + content: + "I'm choosing between two options. Ask me what they are, then compare them in a table and recommend one.", + }, + { + title: ["Teach me something", "in five minutes"], + content: + "Pick a topic you think I'd enjoy, teach me the key idea in five short paragraphs, then quiz me with three questions.", + }, +]; + +// Chats are billed to the signed-in user's own wallet: say so once. +export const banners = (enterUrl) => [ + { + id: "own-pollen", + type: "info", + content: `Chats and generations use your own Pollen. [Check your balance](${enterUrl}/pollen) or [top up](${enterUrl}/top-up).`, + dismissible: true, + timestamp: 0, + }, +]; diff --git a/worker.js b/worker.js new file mode 100644 index 0000000..ebc5943 --- /dev/null +++ b/worker.js @@ -0,0 +1,201 @@ +import { env as workerEnv } from "cloudflare:workers"; +import { Container, getContainer } from "@cloudflare/containers"; +import { banners, PINNED_MODELS, PROMPT_SUGGESTIONS } from "./ui-defaults.js"; + +const CONTAINER_NAME = "primary"; +const WEBUI_URL = required("WEBUI_URL"); +const GEN_URL = required("GEN_URL"); + +function required(name) { + const value = workerEnv[name]; + if (!value) { + throw new Error(`Missing required Worker var or secret: ${name}`); + } + return value; +} + +/** + * Open WebUI with Pollinations as its only login provider. The consent-minted + * sk_ is forwarded to gen.pollinations.ai per user (auth_type system_oauth), + * so every chat is paid from the signed-in user's own wallet. + * + * Container disk is ephemeral, so all state lives in Postgres (DATABASE_URL, + * which also hosts the pgvector store). Uploaded files are still local and do + * not survive a container restart; move them to R2 (STORAGE_PROVIDER=s3) when + * that matters. + */ +export class OpenWebUIContainer extends Container { + defaultPort = 8080; + requiredPorts = [8080]; + sleepAfter = "30m"; + envVars = { + PORT: "8080", + WEBUI_URL, + WEBUI_SECRET_KEY: required("WEBUI_SECRET_KEY"), + DATABASE_URL: required("DATABASE_URL"), + VECTOR_DB: "pgvector", + + // Login: Pollinations OAuth 2.1 (code + PKCE S256, public client). + // OAUTH_CLIENT_SECRET must stay empty: authlib then uses token auth + // "none" and sends client_id in the body, which /api/oauth/token needs. + ENABLE_OAUTH_SIGNUP: "true", + OAUTH_PROVIDER_NAME: "Pollinations", + OAUTH_CLIENT_ID: required("OAUTH_CLIENT_ID"), + OAUTH_CLIENT_SECRET: "", + OAUTH_CODE_CHALLENGE_METHOD: "S256", + OPENID_PROVIDER_URL: `${required("ENTER_URL")}/.well-known/oauth-authorization-server`, + OPENID_REDIRECT_URI: `${WEBUI_URL}/oauth/oidc/callback`, + OAUTH_SCOPES: "profile", + OAUTH_USERNAME_CLAIM: "name", + OAUTH_EMAIL_CLAIM: "email", + OAUTH_PICTURE_CLAIM: "picture", + OAUTH_AUTHORIZE_PARAMS: JSON.stringify({ expiry: 365, budget: 20 }), + OAUTH_MERGE_ACCOUNTS_BY_EMAIL: "true", + OAUTH_AUTO_REDIRECT: "true", + // Pollinations is the only way in. + ENABLE_LOGIN_FORM: "false", + ENABLE_PASSWORD_AUTH: "false", + ENABLE_SIGNUP: "false", + // Anyone with a Pollinations account may chat; they pay with their own pollen. + DEFAULT_USER_ROLE: "user", + // Models fetched from a connection have no row in the model table, and + // get_filtered_models() shows unconfigured models to admins only. Without + // this every non-admin gets an empty model picker. + BYPASS_MODEL_ACCESS_CONTROL: "true", + // The picker lists the whole catalog (300+ entries, media models + // included). ui-defaults.js pins a few everyday models and opens new + // chats on the cheap default; the rest keeps gen's order. Seeded once: + // see the README for existing installs. + DEFAULT_MODELS: PINNED_MODELS[0], + DEFAULT_PINNED_MODELS: PINNED_MODELS.join(","), + DEFAULT_PROMPT_SUGGESTIONS: JSON.stringify(PROMPT_SUGGESTIONS), + WEBUI_BANNERS: JSON.stringify(banners(required("ENTER_URL"))), + // This is a shared host: no "share to Open WebUI Community" button + // (it uploads chats to openwebui.com), and no blind-comparison arena. + ENABLE_COMMUNITY_SHARING: "false", + ENABLE_EVALUATION_ARENA_MODELS: "false", + // Titles, tags and follow-ups need text, even when the chat model generates media. + // Non-reasoning on purpose: gpt-5-nano spent 256-1024 reasoning tokens and + // 3-12 s per title/tags/follow-up call. Seed value only; the live value is + // the `task.model.external` row in the config table. + TASK_MODEL_EXTERNAL: "openai/gpt-5.4-nano", + + // Model backend: gen.pollinations.ai, bearer = the user's OAuth sk_. + ENABLE_OLLAMA_API: "false", + ENABLE_RESPONSES_API_STATEFUL: "false", + OPENAI_API_BASE_URLS: GEN_URL, + OPENAI_API_KEYS: "", + OPENAI_API_CONFIGS: JSON.stringify({ + 0: { + enable: true, + auth_type: "system_oauth", + key: "", + prefix_id: "", + api_type: required("API_TYPE"), + model_ids: required("MODEL_IDS"), + connection_type: "external", + tags: [], + }, + }), + + // Open WebUI injects its own builtin tools (time, chat history, ask_user) + // into every request that originates from its UI, unless the model says + // otherwise. Models fetched from a connection have no row in the model + // table, so utils/models.py applies this default metadata to them + // wholesale. Without it every chat carries tool specs, which managed + // agents and community models that do not support tool calling reject. + // The MCP tool server below is unaffected: it is opt-in per chat. + DEFAULT_MODEL_METADATA: JSON.stringify({ + capabilities: { builtin_tools: false }, + }), + + // Pollinations MCP as a tool server, on the same per-user consent key as + // the model connection above: generation from a tool call is billed to + // the signed-in user, and getBalance reports their own wallet. + // access_grants is required — without it the server is admin-only. + // Both of these are seeded into the DB only on a first boot with an + // empty config table; afterwards the stored row wins and editing this + // does nothing (Config.seed_defaults inserts missing keys only). + TOOL_SERVER_CONNECTIONS: JSON.stringify( + workerEnv.MCP_URL + ? [ + { + url: workerEnv.MCP_URL, + path: "", + type: "mcp", + auth_type: "system_oauth", + key: "", + config: { + enable: true, + access_grants: [ + { + principal_type: "user", + principal_id: "*", + permission: "read", + }, + ], + }, + info: { + id: "pollinations", + name: "Pollinations", + description: + "Generate images, video, audio, text and embeddings from your own wallet.", + }, + }, + ] + : [], + ), + + // RAG embeds with the bundled SentenceTransformers model, not gen. + // The RAG, image and audio subsystems all authenticate with a single + // static key rather than the per-user OAuth token the chat connection + // uses, so pointing them at gen would bill every user's documents to + // one wallet. Local embedding keeps that off a shared budget; the cost + // is a ~90 MB model download onto the ephemeral disk after a restart. + ENABLE_VERSION_UPDATE_CHECK: "false", + + // Cache the gen /v1/models fetch per user. The upstream default is 1 s, + // so every page load refetched and rebuilt the ~360-model list, which + // took 6-8 s on the 0.5 vCPU instance and gated the whole page. + MODELS_CACHE_TTL: "300", + }; +} + +function openwebui(env) { + return getContainer(env.OPENWEBUI, CONTAINER_NAME); +} + +export default { + async fetch(request, env) { + const response = await openwebui(env).fetch(request); + if (response.webSocket) return response; + // Send the full chat URL as the referrer when a user follows a link + // out of a chat, so enter's top-up and key pages can bring them back + // to that chat. The browser default would send the origin only. + const headers = new Headers(response.headers); + headers.set("Referrer-Policy", "no-referrer-when-downgrade"); + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); + }, + + // Keepalive: a request every 5 minutes resets sleepAfter. + async scheduled(_controller, env, ctx) { + ctx.waitUntil( + openwebui(env) + .fetch(new Request(`${WEBUI_URL}/health`)) + .then((response) => { + if (!response.ok) { + console.warn( + `Open WebUI health returned ${response.status}`, + ); + } + }) + .catch((error) => { + console.error("Open WebUI health check failed", error); + }), + ); + }, +}; diff --git a/wrangler.jsonc b/wrangler.jsonc new file mode 100644 index 0000000..3d98a32 --- /dev/null +++ b/wrangler.jsonc @@ -0,0 +1,84 @@ +{ + "name": "openwebui", + "main": "worker.js", + "compatibility_date": "2026-01-01", + // Pre-built upstream image pushed to the Cloudflare registry by + // scripts/push-image.sh. No Dockerfile: every setting is an env var, so the + // deploy needs no Docker and CI never builds an image. + "containers": [ + { + "class_name": "OpenWebUIContainer", + "image": "registry.cloudflare.com/b6ec751c0862027ba269faf7029b2501/open-webui:0.11.3-slim", + "max_instances": 1, + "instance_type": "standard-4", + // Postgres lives in AWS us-east-1. Without a constraint Cloudflare starts + // the container wherever the waking request is nearest (it landed in + // Riga), and every DB session then costs ~3 transatlantic round trips. + // Changing this does not move a running container: delete the container + // application and deploy again (see README). + "constraints": { "regions": ["ENAM"] } + } + ], + "durable_objects": { + "bindings": [{ "name": "OPENWEBUI", "class_name": "OpenWebUIContainer" }] + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["OpenWebUIContainer"] }], + // Same shape as floret/observability: myceli origin first, public alias second. + "routes": [ + { + "pattern": "openwebui.myceli.ai", + "zone_name": "myceli.ai", + "custom_domain": true + }, + { + "pattern": "openwebui.pollinations.ai", + "zone_name": "pollinations.ai", + "custom_domain": true + } + ], + // Keep the single instance warm; a cold start pulls a ~1.5 GB image. + "triggers": { "crons": ["*/5 * * * *"] }, + "vars": { + "WEBUI_URL": "https://openwebui.pollinations.ai", + "GEN_URL": "https://gen.pollinations.ai/v1", + "ENTER_URL": "https://enter.pollinations.ai", + "MCP_URL": "https://mcp.pollinations.ai/", + "API_TYPE": "chat_completions", + "MODEL_IDS": [], + "OAUTH_CLIENT_ID": "pk_NVZ2LTQeblbbw1P8" + }, + "observability": { "enabled": true }, + // Secrets (pushed by scripts/push-secrets.mjs from secrets/secrets.vars.json): + // - WEBUI_SECRET_KEY + // - DATABASE_URL + "env": { + "staging": { + "name": "openwebui-staging", + "workers_dev": true, + "routes": [], + "containers": [ + { + "class_name": "OpenWebUIContainer", + "image": "registry.cloudflare.com/b6ec751c0862027ba269faf7029b2501/open-webui:0.11.3-slim", + "max_instances": 1, + "instance_type": "standard-1", + "constraints": { "regions": ["ENAM"] } + } + ], + "durable_objects": { + "bindings": [{ "name": "OPENWEBUI", "class_name": "OpenWebUIContainer" }] + }, + "triggers": { "crons": [] }, + "vars": { + "WEBUI_URL": "https://openwebui-staging.elliot-b6e.workers.dev", + "GEN_URL": "https://staging.gen.pollinations.ai/v1", + "ENTER_URL": "https://staging.enter.pollinations.ai", + // No production MCP connection in the staging instance. + "MCP_URL": "", + "API_TYPE": "responses", + "MODEL_IDS": [], + "OAUTH_CLIENT_ID": "pk_sF4w5mlijzIgoCeh" + } + } + } +} From ad001c9f8b65742894bd3fbcdd53908299852c7b Mon Sep 17 00:00:00 2001 From: "elliot.fouchy" Date: Sat, 31 Oct 2026 03:26:32 +0100 Subject: [PATCH 2/3] chore: preserve the approved encrypted runtime backup --- .sops.yaml | 3 +++ secrets/secrets.vars.json | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 .sops.yaml create mode 100644 secrets/secrets.vars.json diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..06827f9 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,3 @@ +creation_rules: + - path_regex: secrets/.*\.vars\.json$ + age: age1k85e3hjd2tv3wtjv7npjtmp9pwr5cfda22hyz9ajg06uqel3cc5s6c34rd,age1ry4pg28f38yrnd5nhdl2zrkh8f7vgfu0p6hdu8lwpvpa7vz8ag6qqxy4d6 diff --git a/secrets/secrets.vars.json b/secrets/secrets.vars.json new file mode 100644 index 0000000..f749ee3 --- /dev/null +++ b/secrets/secrets.vars.json @@ -0,0 +1,26 @@ +{ + "production": { + "WEBUI_SECRET_KEY": "ENC[AES256_GCM,data:Rjs588shtAK9CZLTDnfxasUd/xteLf/H0/3GyD8zuviunlL5r/Fgi6CNhYQJy44mC6PO39ukxftk8fzS1ehCDQ==,iv:F9y7ILK3sCMEUolCkr1lzx+z94yr+XqGeIiuE9bLoKw=,tag:APS0Yhl3uSaJv73e/XVnXQ==,type:str]", + "DATABASE_URL": "ENC[AES256_GCM,data:vicW/6raz6js3N86Vb9BL7CreZmXDaNrKnLjvh9jYIBH4DvnnUKgmy92ztbY47IdvNnaiV8P9A+HXoMYKZw2DQiX7sOuxoREFll9dldF2Pm5TJIPARFV8TGHMzW/p0ZyPKWC5NcHd+RLoB98jmY8AA7hzxE=,iv:V3YmQmrnPGs4eDaUt5Kc3a5AU+UeNXfIO25PLKGWuEs=,tag:82KOW+yvI+zg2PYnAkheGw==,type:str]" + }, + "staging": { + "WEBUI_SECRET_KEY": "ENC[AES256_GCM,data:yeS+oX8Rn0ufjn7NxEfVg75JNo4+rYg6O9Vwm0RRfyhhYvU6cJHASsfxivW5Y4pzZTY3MKlKb/jSwJosm0V7CA==,iv:Tf51DINhm8F//fnB5Fb1gp+QLr72Qs7sFN9kqz5GTcs=,tag:n1O0qJbnQmwRhZ0QszXahw==,type:str]", + "DATABASE_URL": "ENC[AES256_GCM,data:I2Wr4g3jhwzIGFME1AJ7yfH1abbxL99PAUZMDtWfk7q4RWTezjJwkZbWTcnLPe4HOkqxdSjtwYOmkFbX1hfIVx/9Nf7ls5xcD0KBZdRLBlGGtmovVPC0qcsCa/o5QSG7C/a2vDAX3jEhZC9gdmCtb4rpYrJ+JLDBeenbMQ==,iv:KnqlrP5MmhWHwTN9YiZLXmvsPhpwjANdK5t62fuOHs8=,tag:jZewJb74TCLdGqWhrxCTrg==,type:str]" + }, + "sops": { + "age": [ + { + "recipient": "age1k85e3hjd2tv3wtjv7npjtmp9pwr5cfda22hyz9ajg06uqel3cc5s6c34rd", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3cmRwY0p6S1BaY1B2MHVw\nNDR3bUNGWW5TVXA2M3BtVzVpRFkwZWJ3OHpjCjRxb005ZXJLVjFHUXBZaGxkanpk\nVS9iYS93S3h5bUl5R3k4N21TTFhXaUkKLS0tIElCOW84T28vYVN5UldtS3JZWkcr\nOUN5L1M2OVhQcGo3dEhLcFUyZWdwazQK9l/DTG6SZuOh68g/pnfAsgrMmgx82QLZ\nXjPneYC8r2Op4KBMKwUpywI7dQB5bi/CNWrq8XUACmd1bhmxf0AY/Q==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1ry4pg28f38yrnd5nhdl2zrkh8f7vgfu0p6hdu8lwpvpa7vz8ag6qqxy4d6", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2ekQ0R2hHbTd6QlR2L2lG\nbE1mbDJyT2VZOGhLODNld2toTENpYlZRUXowCnJWZ1FOSHRGY0JYdlF5RERPRy8r\ncExFNStzSnl1SVl1cHUzTW4vSmVXWDgKLS0tIDZzMHZ4eWYwNnBpK1dJRk9OQkk0\nRDVRZ3dWYWhmSExoTGdNYXpGQzBjL3MK/NNRRF5W4BhEKipKla/Qfa0x81ZIR8DE\nmzm9tIY3VSxFNRjSCYiO6FNrB4K/mqXGzqwPcBltk5Jl/wMs90cHGg==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-09-03T01:37:03Z", + "mac": "ENC[AES256_GCM,data:4UhhicfH6FfNMichKMBZ/HQbKz42xAnr/AfANm3sbK5Lw+gKWXkjP9X825FB+iFBCbPDcIfPxsTwB5Y9EkOlSDuTUU92T3z1BMFk8lZtCMpz0BgXT8CL8KQmlsbsiKsy0AWT4Z1j/q5u3U1qt7hvljpSg7ORD1y49beMeKGkTg0=,iv:d7jF7cjlCVMYk4pFUy/9R8E+h4mS5C2cg+FJay6t2Ns=,tag:aq946OvkOkLNTvEw4BZl4A==,type:str]", + "unencrypted_suffix": "_unencrypted", + "version": "3.11.0" + } +} From f530c59b744e783d7a1c1b766867c5adbbbb06c3 Mon Sep 17 00:00:00 2001 From: "elliot.fouchy" Date: Sat, 31 Oct 2026 03:30:48 +0100 Subject: [PATCH 3/3] ci: verify existing Worker secrets before deploying --- .github/workflows/deploy.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 11d51f6..fa90dbd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -50,6 +50,11 @@ jobs: env: CLOUDFLARE_ACCOUNT_ID: b6ec751c0862027ba269faf7029b2501 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - run: node node_modules/wrangler/bin/wrangler.js deploy --env="" + run: | + curl --fail --silent --show-error \ + "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/workers/scripts/openwebui/settings" \ + --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ + | jq --exit-status '[.result.bindings[] | select(.type == "secret_text") | .name] | contains(["DATABASE_URL", "WEBUI_SECRET_KEY"])' + node node_modules/wrangler/bin/wrangler.js deploy --env="" - name: Verify public endpoints and database run: npm run verify