diff --git a/.dev.vars.example b/.dev.vars.example new file mode 100644 index 0000000..772bb9a --- /dev/null +++ b/.dev.vars.example @@ -0,0 +1,4 @@ +GITHUB_APP_ID= +GITHUB_APP_INSTALLATION_ID= +GITHUB_APP_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----" +WORKFLOW_WEBHOOK_SECRET= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06e6294 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +.wrangler/ +.dev.vars +dist/ +coverage/ +.DS_Store + +# Keep the approved asset; local design exploration renders remain untracked. +design/explorations/ +design/concepts/ +design/approved/*runtime-preview.png diff --git a/README.md b/README.md index 9a520cf..7db7d1a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,145 @@ -# EverOS-Bot -Contributor recognition and community automation bot for EverOS. +# EverOS Bot + +Contributor recognition and community automation for +[EverOS](https://github.com/EverMind-AI/EverOS). + +The first release generates a **First Engram** card when a real contributor +opens their first issue or has their first pull request merged. The card is +stored as an immutable PNG and posted back to the originating GitHub thread by +a GitHub App. + +![EverOS First Engram contributor card](design/approved/everos-contributor-card-v1.png) + +## Architecture + +This project follows the webhook service boundary used by +[`nexu-io/open-design-contributor-card`](https://github.com/nexu-io/open-design-contributor-card): + +```text +EverOS GitHub event + │ + ▼ +thin GitHub Actions relay ── HMAC-SHA256 ──▶ Cloudflare Worker + │ + ┌──────────────────────────────┼────────────────────┐ + ▼ ▼ ▼ + GitHub App API D1 R2 + stats + issue comment contributor state generated PNG +``` + +- The workflow never checks out or executes pull-request code. +- `POST /api/github/events` verifies the exact raw body with + `X-EverOS-Signature`. +- D1 stores contributor state, delivery-level ingestion records, and card + events. +- R2 stores immutable generated PNG cards. +- A hidden comment marker makes GitHub posting recoverable after partial + failures. +- GitHub notifications provide the email delivery path; the Worker does not + collect contributor email addresses. + +## V1 behavior + +- Supported events: `issues.opened` and merged `pull_request_target.closed`. +- Bot accounts and missing actors are ignored. +- The first qualifying event posts one welcome card. +- Later events update points and statistics without posting another welcome. +- GitHub search results use an event floor so a just-opened issue or just-merged + PR is represented even during search-index propagation delay. +- Points: first merged PR `30`, later merged PRs `12`, opened issue `5`. + +## Local setup + +Requirements: Node.js 20+, pnpm 10, Wrangler, and a Cloudflare account. + +```bash +pnpm install +pnpm build:card-base +pnpm typecheck +pnpm test +``` + +Create local resources and apply the migration: + +```bash +pnpm wrangler d1 create everos-bot +pnpm wrangler r2 bucket create everos-bot-card-assets +pnpm wrangler r2 bucket create everos-bot-card-assets-preview +pnpm wrangler d1 migrations apply everos-bot --local +``` + +Replace the placeholder D1 IDs in `wrangler.jsonc`, then: + +```bash +cp .dev.vars.example .dev.vars +pnpm dev +``` + +## GitHub App + +Create a GitHub App and install it on `EverMind-AI/EverOS`. + +Repository permissions: + +- Issues: **Read and write** +- Pull requests: **Read** +- Metadata: **Read** + +Set Worker secrets: + +```bash +pnpm wrangler secret put GITHUB_APP_ID +pnpm wrangler secret put GITHUB_APP_INSTALLATION_ID +pnpm wrangler secret put GITHUB_APP_PRIVATE_KEY +pnpm wrangler secret put WORKFLOW_WEBHOOK_SECRET +``` + +Deploy with `pnpm deploy`, then add these Actions secrets to the EverOS repo: + +- `EVEROS_BOT_WEBHOOK_URL`: Worker origin, without a trailing slash +- `EVEROS_BOT_WEBHOOK_SECRET`: same value as the Worker secret + +Copy +[`examples/everos-repository/.github/workflows/everos-bot-relay.yml`](examples/everos-repository/.github/workflows/everos-bot-relay.yml) +into the EverOS repository. + +## Relay contract + +The relay posts JSON to `POST /api/github/events`: + +```json +{ + "repository": "EverMind-AI/EverOS", + "eventName": "issues", + "action": "opened", + "deliveryId": "123456789-1", + "triggeredAt": "2026-07-22T10:00:00Z", + "payload": {} +} +``` + +The raw body is signed with `WORKFLOW_WEBHOOK_SECRET`; the header format is +`X-EverOS-Signature: sha256=`. + +## Card assets + +- Approved reference: `design/approved/everos-contributor-card-v1.png` +- Runtime base: `assets/card/everos-contributor-card-base.png` +- Rebuild command: `pnpm build:card-base` + +Dynamic fields are the GitHub avatar, username, archive number, points, merged +PR count, and issue count. The four-curve EverMind logo and the GitHub Primer +Octicons are part of the locked base artwork. + +## Validation + +```bash +pnpm validate +``` + +## Attribution + +The service boundary was informed by the Apache-2.0 licensed +[`nexu-io/open-design-contributor-card`](https://github.com/nexu-io/open-design-contributor-card). +GitHub contribution symbols use +[`primer/octicons`](https://github.com/primer/octicons), MIT licensed. diff --git a/assets/card/everos-contributor-card-base.png b/assets/card/everos-contributor-card-base.png new file mode 100644 index 0000000..1e90005 Binary files /dev/null and b/assets/card/everos-contributor-card-base.png differ diff --git a/assets/card/git-pull-request-16.svg b/assets/card/git-pull-request-16.svg new file mode 100644 index 0000000..a1cb1ef --- /dev/null +++ b/assets/card/git-pull-request-16.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/card/issue-opened-16.svg b/assets/card/issue-opened-16.svg new file mode 100644 index 0000000..914e086 --- /dev/null +++ b/assets/card/issue-opened-16.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/card/star-fill-16.svg b/assets/card/star-fill-16.svg new file mode 100644 index 0000000..adf9882 --- /dev/null +++ b/assets/card/star-fill-16.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/design/approved/everos-contributor-card-v1.png b/design/approved/everos-contributor-card-v1.png new file mode 100644 index 0000000..eaa2411 Binary files /dev/null and b/design/approved/everos-contributor-card-v1.png differ diff --git a/examples/everos-repository/.github/workflows/everos-bot-relay.yml b/examples/everos-repository/.github/workflows/everos-bot-relay.yml new file mode 100644 index 0000000..8db124a --- /dev/null +++ b/examples/everos-repository/.github/workflows/everos-bot-relay.yml @@ -0,0 +1,48 @@ +name: EverOS Bot Relay + +on: + issues: + types: [opened] + pull_request_target: + types: [closed] + +permissions: + contents: read + +jobs: + relay: + # The Worker handles the final event check too. This avoids sending unmerged PRs. + if: github.event_name != 'pull_request_target' || github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - name: Build signed relay envelope + env: + EVENT_NAME: ${{ github.event_name }} + EVENT_ACTION: ${{ github.event.action }} + REPOSITORY: ${{ github.repository }} + DELIVERY_ID: ${{ github.run_id }}-${{ github.run_attempt }} + WEBHOOK_SECRET: ${{ secrets.EVEROS_BOT_WEBHOOK_SECRET }} + run: | + jq -n \ + --arg repository "$REPOSITORY" \ + --arg eventName "$EVENT_NAME" \ + --arg action "$EVENT_ACTION" \ + --arg deliveryId "$DELIVERY_ID" \ + --arg triggeredAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --slurpfile payload "$GITHUB_EVENT_PATH" \ + '{repository:$repository,eventName:$eventName,action:$action,deliveryId:$deliveryId,triggeredAt:$triggeredAt,payload:$payload[0]}' \ + > relay.json + + digest="$(openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" relay.json | awk '{print $NF}')" + printf 'SIGNATURE=sha256=%s\n' "$digest" >> "$GITHUB_ENV" + + - name: Relay event to EverOS Bot + env: + WEBHOOK_URL: ${{ secrets.EVEROS_BOT_WEBHOOK_URL }} + run: | + curl --fail-with-body --silent --show-error \ + --retry 3 --retry-all-errors \ + -X POST "$WEBHOOK_URL/api/github/events" \ + -H 'content-type: application/json' \ + -H "x-everos-signature: $SIGNATURE" \ + --data-binary @relay.json diff --git a/migrations/0001_init.sql b/migrations/0001_init.sql new file mode 100644 index 0000000..0871825 --- /dev/null +++ b/migrations/0001_init.sql @@ -0,0 +1,43 @@ +CREATE TABLE IF NOT EXISTS contributors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + login TEXT NOT NULL UNIQUE COLLATE NOCASE, + avatar_url TEXT, + points INTEGER NOT NULL DEFAULT 0, + prs_merged INTEGER NOT NULL DEFAULT 0, + issues_opened INTEGER NOT NULL DEFAULT 0, + welcome_delivery_id TEXT, + welcomed_at TEXT, + last_seen_at TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS card_events ( + event_id TEXT PRIMARY KEY, + delivery_id TEXT NOT NULL UNIQUE, + contributor_id INTEGER NOT NULL, + recipient_login TEXT NOT NULL, + surface TEXT NOT NULL, + thread_number INTEGER NOT NULL, + comment_url TEXT, + card_object_key TEXT NOT NULL, + points INTEGER NOT NULL, + prs_merged INTEGER NOT NULL, + issues_opened INTEGER NOT NULL, + created_at TEXT NOT NULL, + FOREIGN KEY (contributor_id) REFERENCES contributors(id) +); + +CREATE INDEX IF NOT EXISTS idx_card_events_recipient_created_at + ON card_events(recipient_login, created_at DESC); + +CREATE TABLE IF NOT EXISTS ingestion_events ( + delivery_id TEXT PRIMARY KEY, + event_name TEXT NOT NULL, + action TEXT, + actor_login TEXT, + outcome TEXT NOT NULL, + card_event_id TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); diff --git a/package.json b/package.json new file mode 100644 index 0000000..fb5c18e --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "everos-bot", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build:card-base": "node scripts/build-card-base.mjs", + "preview:card": "tsx scripts/render-card-preview.ts", + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "check": "wrangler deploy --dry-run", + "typegen": "wrangler types", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "validate": "tsc --noEmit && vitest run && wrangler deploy --dry-run" + }, + "dependencies": { + "@resvg/resvg-wasm": "^2.6.2", + "jose": "^6.1.0", + "react": "^19.2.0", + "satori": "^0.12.1" + }, + "devDependencies": { + "@cloudflare/workers-types": "^5.20260722.1", + "@types/node": "^24.0.0", + "@types/react": "^19.2.0", + "sharp": "^0.34.3", + "tsx": "^4.20.0", + "typescript": "^5.9.0", + "vitest": "^3.2.4", + "wrangler": "^4.26.0" + }, + "packageManager": "pnpm@10.13.1" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..840a8f7 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1781 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@resvg/resvg-wasm': + specifier: ^2.6.2 + version: 2.6.2 + jose: + specifier: ^6.1.0 + version: 6.2.4 + react: + specifier: ^19.2.0 + version: 19.2.8 + satori: + specifier: ^0.12.1 + version: 0.12.2 + devDependencies: + '@cloudflare/workers-types': + specifier: ^5.20260722.1 + version: 5.20260722.1 + '@types/node': + specifier: ^24.0.0 + version: 24.13.3 + '@types/react': + specifier: ^19.2.0 + version: 19.2.17 + sharp: + specifier: ^0.34.3 + version: 0.34.5 + tsx: + specifier: ^4.20.0 + version: 4.23.1 + typescript: + specifier: ^5.9.0 + version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/node@24.13.3)(tsx@4.23.1) + wrangler: + specifier: ^4.26.0 + version: 4.113.0(@cloudflare/workers-types@5.20260722.1) + +packages: + + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260721.1': + resolution: {integrity: sha512-VivNMhiEdZIB4JBWxf1RMJGROErv53qmQ+dvhjA1evrCouvqRYW718VqDideU3PSV7Ythl5Df48NqZYWoaEHpQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260721.1': + resolution: {integrity: sha512-k7oye1ZiuwnnBBA2eTMduconr/ud5ZxFtRNTsYwMdmJeeeislw2+M72otrHxxvybCP7JWPPlJ38uhfajpcyhOA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260721.1': + resolution: {integrity: sha512-hon0lW4ZQ4boAVgaw+0ZFTNS8v5MWPWvK0HZnt4tDpKYnDUviLZawtUW3KqvFmCQTipVHl1S34j3J8Eqb93hGQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260721.1': + resolution: {integrity: sha512-nAl+HRQqpX5b7xVwWcvLPZmCk8NQ2yjI0yvJTWcHiRswbMEg1ZZckVmjJUAn0PHzZARbCSyIV7v3UjM+SPRmIQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260721.1': + resolution: {integrity: sha512-9paFG5cMTKz/CRixnEEnZbe5uvFPBFSDthxJHANfCWhUtBj49GSL1FPIokIg+Q+H8DGJEExU0lL92LtxD0lTxQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@5.20260722.1': + resolution: {integrity: sha512-8+kivCgFGzwrAfNOWgSpzy/VDvmT/i5KWBgQhnygv3d1kajNn6mCYTbLKpouG0aY8mXjhv+IQm1a8r2K/H4pqQ==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@resvg/resvg-wasm@2.6.2': + resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} + engines: {node: '>= 10'} + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@shuding/opentype.js@1.4.0-beta.0': + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} + engines: {node: '>= 8.0.0'} + hasBin: true + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.17': + resolution: {integrity: sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + css-background-parser@0.1.0: + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} + + css-box-shadow@1.0.0-3: + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} + + css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + + css-gradient-parser@0.0.16: + resolution: {integrity: sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==} + engines: {node: '>=16'} + + css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.7.5: + resolution: {integrity: sha512-QieYf//cis6ywHNi5qW1+PXPQ4bC+XVJAtS4AXIML8P76GroEiOxm/oQtn1f02UkJY1+KsXMJcC+R2v/Eg4G3g==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hex-rgb@4.3.0: + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} + engines: {node: '>=6'} + + jose@6.2.4: + resolution: {integrity: sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + miniflare@4.20260721.0: + resolution: {integrity: sha512-fBLaCxZ2i/nPH8iyLzvza0C8/sSF4sjD1ma1Skf+pkZVK0TlaW5ujHJlUHwcwR66v2JZt+Q28d4DCX/oaLG0cA==} + engines: {node: '>=22.0.0'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + parse-css-color@0.2.1: + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.22: + resolution: {integrity: sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==} + engines: {node: ^10 || ^12 || >=14} + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + satori@0.12.2: + resolution: {integrity: sha512-3C/laIeE6UUe9A+iQ0A48ywPVCCMKCNSTU5Os101Vhgsjd3AAxGNjyq0uAA8kulMPK5n0csn8JlxPN9riXEjLA==} + engines: {node: '>=16'} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string.prototype.codepointat@0.2.1: + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + workerd@1.20260721.1: + resolution: {integrity: sha512-b/DWhpV0jTudzQpLhDovcOgBz233386q+3Hbari7CLCNT9UXxjQziSTZ9yCoKdT2K3TSx5jrwlOisq8hlLWXYg==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.113.0: + resolution: {integrity: sha512-ROGzSloJv0y21It6Oc9LaruNcu1tdiQ/XzL3Jc3YkFjzXEMXzTqVhA8vQaGMTdZHTjFP0PVcwAHNgaw3gXu4wA==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^5.20260721.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + 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 + + yoga-wasm-web@0.3.3: + resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + +snapshots: + + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260721.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260721.1 + + '@cloudflare/workerd-darwin-64@1.20260721.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260721.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260721.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260721.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260721.1': + optional: true + + '@cloudflare/workers-types@5.20260722.1': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.11.2 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@resvg/resvg-wasm@2.6.2': {} + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@shuding/opentype.js@1.4.0-beta.0': + dependencies: + fflate: 0.7.5 + string.prototype.codepointat: 0.2.1 + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.17': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.1))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@24.13.3)(tsx@4.23.1) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + assertion-error@2.0.1: {} + + base64-js@0.0.8: {} + + blake3-wasm@2.1.5: {} + + cac@6.7.14: {} + + camelize@1.0.1: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + color-name@1.1.4: {} + + cookie@1.1.1: {} + + css-background-parser@0.1.0: {} + + css-box-shadow@1.0.0-3: {} + + css-color-keywords@1.0.0: {} + + css-gradient-parser@0.0.16: {} + + css-to-react-native@3.2.0: + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + detect-libc@2.1.2: {} + + emoji-regex@10.6.0: {} + + error-stack-parser-es@1.0.5: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.28.1: + 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 + + escape-html@1.0.3: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + fflate@0.7.5: {} + + fsevents@2.3.3: + optional: true + + hex-rgb@4.3.0: {} + + jose@6.2.4: {} + + js-tokens@9.0.1: {} + + kleur@4.1.5: {} + + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + miniflare@4.20260721.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.28.0 + workerd: 1.20260721.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ms@2.1.3: {} + + nanoid@3.3.16: {} + + pako@0.2.9: {} + + parse-css-color@0.2.1: + dependencies: + color-name: 1.1.4 + hex-rgb: 4.3.0 + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + postcss-value-parser@4.2.0: {} + + postcss@8.5.22: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react@19.2.8: {} + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + satori@0.12.2: + dependencies: + '@shuding/opentype.js': 1.4.0-beta.0 + css-background-parser: 0.1.0 + css-box-shadow: 1.0.0-3 + css-gradient-parser: 0.0.16 + css-to-react-native: 3.2.0 + emoji-regex: 10.6.0 + escape-html: 1.0.3 + linebreak: 1.1.0 + parse-css-color: 0.2.1 + postcss-value-parser: 4.2.0 + yoga-wasm-web: 0.3.3 + + semver@7.8.5: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string.prototype.codepointat@0.2.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-color@10.2.2: {} + + tiny-inflate@1.0.3: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + tslib@2.8.1: + optional: true + + tsx@4.23.1: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + undici@7.28.0: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + + vite-node@3.2.4(@types/node@24.13.3)(tsx@4.23.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@24.13.3)(tsx@4.23.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@24.13.3)(tsx@4.23.1): + dependencies: + esbuild: 0.28.1 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.22 + rollup: 4.62.2 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + fsevents: 2.3.3 + tsx: 4.23.1 + + vitest@3.2.7(@types/node@24.13.3)(tsx@4.23.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.1)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@24.13.3)(tsx@4.23.1) + vite-node: 3.2.4(@types/node@24.13.3)(tsx@4.23.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + workerd@1.20260721.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260721.1 + '@cloudflare/workerd-darwin-arm64': 1.20260721.1 + '@cloudflare/workerd-linux-64': 1.20260721.1 + '@cloudflare/workerd-linux-arm64': 1.20260721.1 + '@cloudflare/workerd-windows-64': 1.20260721.1 + + wrangler@4.113.0(@cloudflare/workers-types@5.20260722.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260721.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 4.20260721.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260721.1 + optionalDependencies: + '@cloudflare/workers-types': 5.20260722.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ws@8.21.0: {} + + yoga-wasm-web@0.3.3: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.17 + cookie: 1.1.1 + youch-core: 0.3.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..ccbac80 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "." diff --git a/scripts/build-card-base.mjs b/scripts/build-card-base.mjs new file mode 100644 index 0000000..77a937c --- /dev/null +++ b/scripts/build-card-base.mjs @@ -0,0 +1,30 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import sharp from "sharp"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const input = path.join(root, "design", "approved", "everos-contributor-card-v1.png"); +const output = path.join(root, "assets", "card", "everos-contributor-card-base.png"); + +const cream = "#F6E9C6"; +const ink = "#15110D"; + +// Remove only fields filled by the Worker. The background, four-curve EverMind +// logo, official Primer Octicons, card frames, labels, and copy remain immutable. +const dynamicFieldMask = Buffer.from(` + + + + + + + +`); + +await sharp(input) + .composite([{ input: dynamicFieldMask, left: 0, top: 0 }]) + .png({ compressionLevel: 9 }) + .toFile(output); + +console.log(output); diff --git a/scripts/render-card-preview.ts b/scripts/render-card-preview.ts new file mode 100644 index 0000000..7ae9ea5 --- /dev/null +++ b/scripts/render-card-preview.ts @@ -0,0 +1,61 @@ +import fs from "node:fs/promises"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { Resvg, initWasm } from "@resvg/resvg-wasm"; +import satori from "satori"; + +import { CARD_HEIGHT, CARD_WIDTH, FirstEngramCard } from "../src/cards/FirstEngramCard"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const require = createRequire(import.meta.url); +const basePath = path.join(root, "assets", "card", "everos-contributor-card-base.png"); +const wasmPath = require.resolve("@resvg/resvg-wasm/index_bg.wasm"); +const output = path.join(root, "design", "approved", "everos-contributor-card-runtime-preview.png"); + +const urls = { + inter400: "https://cdn.jsdelivr.net/fontsource/fonts/inter@5.2.8/latin-400-normal.ttf", + inter700: "https://cdn.jsdelivr.net/fontsource/fonts/inter@5.2.8/latin-700-normal.ttf", + playfair700: "https://cdn.jsdelivr.net/fontsource/fonts/playfair-display@5.2.8/latin-700-normal.ttf", +}; + +async function remoteBuffer(url: string): Promise { + const response = await fetch(url); + if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status}`); + return response.arrayBuffer(); +} + +const [base, wasm, inter400, inter700, playfair700, avatar] = await Promise.all([ + fs.readFile(basePath), + fs.readFile(wasmPath), + remoteBuffer(urls.inter400), + remoteBuffer(urls.inter700), + remoteBuffer(urls.playfair700), + remoteBuffer("https://avatars.githubusercontent.com/u/583231?s=460&v=4"), +]); + +await initWasm(wasm); + +const node = FirstEngramCard({ + username: "everos-contributor", + avatarDataUrl: `data:image/png;base64,${Buffer.from(avatar).toString("base64")}`, + archiveNumber: 7, + points: 5, + prsMerged: 0, + issuesOpened: 1, + baseImageDataUrl: `data:image/png;base64,${base.toString("base64")}`, +}); + +const svg = await satori(node as Parameters[0], { + width: CARD_WIDTH, + height: CARD_HEIGHT, + fonts: [ + { name: "Inter", data: inter400, weight: 400, style: "normal" }, + { name: "Inter", data: inter700, weight: 700, style: "normal" }, + { name: "Playfair", data: playfair700, weight: 700, style: "normal" }, + ], +}); + +await fs.writeFile(output, new Resvg(svg).render().asPng()); +console.log(output); diff --git a/src/card.ts b/src/card.ts new file mode 100644 index 0000000..f40040c --- /dev/null +++ b/src/card.ts @@ -0,0 +1,7 @@ +export function cardObjectKey(eventId: string): string { + return `cards/${eventId}.png`; +} + +export function publicCardUrl(publicBaseUrl: string, eventId: string): string { + return `${publicBaseUrl.replace(/\/$/, "")}/cards/${encodeURIComponent(eventId)}.png`; +} diff --git a/src/cards/FirstEngramCard.tsx b/src/cards/FirstEngramCard.tsx new file mode 100644 index 0000000..3e6f51e --- /dev/null +++ b/src/cards/FirstEngramCard.tsx @@ -0,0 +1,127 @@ +import type { FirstEngramCardProps } from "./types"; + +export const CARD_WIDTH = 1003; +export const CARD_HEIGHT = 1600; + +const INK = "#15110D"; + +function displayNumber(value: number): string { + return Math.max(0, Math.trunc(value)).toLocaleString("en-US"); +} + +export function FirstEngramCard(props: FirstEngramCardProps) { + const archive = String(Math.max(1, props.archiveNumber)).padStart(3, "0"); + const username = `@${props.username.slice(0, 32)}`; + const usernameFontSize = username.length > 26 ? 15 : username.length > 21 ? 17 : username.length > 17 ? 20 : 27; + + return ( +
+ + + {props.avatarDataUrl ? ( +
+ +
+ ) : null} + +
+ {`CONTRIBUTOR ARCHIVE · ${archive}`} +
+ +
+ {username} +
+ + + + +
+ ); +} + +function StatNumber({ left, value }: { left: number; value: number }) { + const text = displayNumber(value); + return ( +
3 ? 39 : 52, + fontWeight: 700, + lineHeight: 1, + }} + > + {text} +
+ ); +} diff --git a/src/cards/types.ts b/src/cards/types.ts new file mode 100644 index 0000000..1ab4098 --- /dev/null +++ b/src/cards/types.ts @@ -0,0 +1,9 @@ +export interface FirstEngramCardProps { + username: string; + avatarDataUrl: string | null; + archiveNumber: number; + points: number; + prsMerged: number; + issuesOpened: number; + baseImageDataUrl: string; +} diff --git a/src/comments.ts b/src/comments.ts new file mode 100644 index 0000000..1b4500c --- /dev/null +++ b/src/comments.ts @@ -0,0 +1,27 @@ +import type { ContributorCardModel } from "./types"; + +export function firstEngramMarker(eventId: string): string { + return ``; +} + +export function firstEngramComment( + card: ContributorCardModel, + cardImageUrl: string, + repositoryUrl: string, + eventId: string, +): string { + return [ + firstEngramMarker(eventId), + `### ✦ Your first Engram in EverOS, @${card.username}`, + "", + `EverOS First Engram contributor card for @${card.username}`, + "", + "> Every great contribution begins with a single spark.", + "", + "Your first mark is now part of EverOS memory. Thank you for helping us build open, editable memory infrastructure for AI agents.", + "", + `**${card.points} points** · ${card.prsMerged} merged PRs · ${card.issuesOpened} issues`, + "", + `Explore the project: [EverOS](${repositoryUrl})`, + ].join("\n"); +} diff --git a/src/context.ts b/src/context.ts new file mode 100644 index 0000000..dd2dac5 --- /dev/null +++ b/src/context.ts @@ -0,0 +1,39 @@ +import type { EventContext } from "./types"; + +function actor(user: { login?: string; avatar_url?: string } | null | undefined) { + return { + login: user?.login?.trim() ?? "", + avatarUrl: user?.avatar_url ?? null, + }; +} + +export function extractContext(eventName: string, payload: any): EventContext | null { + if (eventName === "issues" && payload.action === "opened" && payload.issue?.number) { + return { + actor: actor(payload.issue.user), + threadNumber: payload.issue.number, + reason: "opened issue", + surface: "issue", + }; + } + + if ( + eventName === "pull_request_target" && + payload.action === "closed" && + payload.pull_request?.merged === true && + payload.pull_request?.number + ) { + return { + actor: actor(payload.pull_request.user), + threadNumber: payload.pull_request.number, + reason: "merged PR", + surface: "pull_request", + }; + } + + return null; +} + +export function isBotLogin(login: string): boolean { + return /\[bot\]$/i.test(login) || /^bot-/i.test(login) || /-bot$/i.test(login); +} diff --git a/src/github.ts b/src/github.ts new file mode 100644 index 0000000..781f450 --- /dev/null +++ b/src/github.ts @@ -0,0 +1,114 @@ +import { importPKCS8, SignJWT } from "jose"; +import type { ContributorStats } from "./types"; + +interface GitHubIssueComment { + body: string | null; + html_url: string; + created_at: string; +} + +export class GitHubAppClient { + private installationToken: Promise | null = null; + + constructor( + private readonly appId: string, + private readonly installationId: string, + private readonly privateKeyPem: string, + private readonly fetchImpl: typeof fetch = fetch, + ) {} + + async fetchContributorStats(owner: string, repo: string, login: string): Promise { + const base = `repo:${owner}/${repo}`; + const [prsMerged, issuesOpened] = await Promise.all([ + this.searchCount(`${base} is:pr is:merged author:${login}`), + this.searchCount(`${base} is:issue author:${login}`), + ]); + return { prsMerged, issuesOpened }; + } + + async findIssueCommentByMarker( + owner: string, + repo: string, + issueNumber: number, + marker: string, + ): Promise { + const comments = await this.api( + `/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=100`, + ); + return comments.find((comment) => comment.body?.includes(marker)) ?? null; + } + + async postIssueComment( + owner: string, + repo: string, + issueNumber: number, + body: string, + ): Promise { + return this.api(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, { + method: "POST", + body: JSON.stringify({ body }), + }); + } + + private async searchCount(query: string): Promise { + const result = await this.api<{ total_count: number }>( + `/search/issues?q=${encodeURIComponent(query)}&per_page=1`, + ); + return result.total_count; + } + + private async api(path: string, init?: RequestInit): Promise { + const token = await this.getInstallationToken(); + const response = await this.fetchImpl(`https://api.github.com${path}`, { + ...init, + headers: { + accept: "application/vnd.github+json", + authorization: `Bearer ${token}`, + "content-type": "application/json", + "user-agent": "everos-bot", + "x-github-api-version": "2022-11-28", + ...(init?.headers ?? {}), + }, + }); + if (!response.ok) { + throw new Error(`GitHub API ${response.status}: ${await response.text()}`); + } + return response.json() as Promise; + } + + private getInstallationToken(): Promise { + this.installationToken ??= this.createInstallationToken(); + return this.installationToken; + } + + private async createInstallationToken(): Promise { + const now = Math.floor(Date.now() / 1000); + const key = await importPKCS8(this.privateKeyPem.replace(/\\n/g, "\n"), "RS256"); + const jwt = await new SignJWT({}) + .setProtectedHeader({ alg: "RS256" }) + .setIssuedAt(now - 60) + .setExpirationTime(now + 9 * 60) + .setIssuer(this.appId) + .sign(key); + + const response = await this.fetchImpl( + `https://api.github.com/app/installations/${this.installationId}/access_tokens`, + { + method: "POST", + headers: { + accept: "application/vnd.github+json", + authorization: `Bearer ${jwt}`, + "content-type": "application/json", + "user-agent": "everos-bot", + "x-github-api-version": "2022-11-28", + }, + }, + ); + if (!response.ok) { + throw new Error(`GitHub installation token ${response.status}: ${await response.text()}`); + } + const data = await response.json() as { token?: string }; + if (!data.token) throw new Error("GitHub installation token response omitted token"); + return data.token; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..17d8e99 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,288 @@ +import { cardObjectKey, publicCardUrl } from "./card"; +import { firstEngramComment, firstEngramMarker } from "./comments"; +import { extractContext, isBotLogin } from "./context"; +import { GitHubAppClient } from "./github"; +import { renderContributorCard } from "./render"; +import { pointsFromStats } from "./scoring"; +import { verifyRelaySignature } from "./signature"; +import { + claimWelcome, + ensureContributor, + getIngestionEvent, + insertCardEvent, + recordIngestionEvent, + updateContributor, +} from "./storage"; +import type { + ContributorCardModel, + ContributorStats, + RelayEnvelope, + RuntimeEnv, +} from "./types"; + +function json(data: unknown, init?: ResponseInit): Response { + return new Response(JSON.stringify(data, null, 2), { + ...init, + headers: { + "content-type": "application/json; charset=utf-8", + ...(init?.headers ?? {}), + }, + }); +} + +function required(value: string | undefined, name: string): string { + if (!value) throw new Error(`Missing required configuration: ${name}`); + return value; +} + +function parseRepository(value: string): { owner: string; repo: string; url: string } { + const [owner, repo, extra] = value.split("/"); + if (!owner || !repo || extra) throw new Error(`Invalid repository: ${value}`); + return { owner, repo, url: `https://github.com/${owner}/${repo}` }; +} + +function eventId(deliveryId: string, login: string): string { + const safeDelivery = deliveryId.replace(/[^a-zA-Z0-9_.-]/g, "-").slice(0, 120); + const safeLogin = login.replace(/[^a-zA-Z0-9-]/g, "-").slice(0, 39); + return `${safeLogin}-first-engram-${safeDelivery}`; +} + +function statsWithEventFloor( + stats: ContributorStats, + reason: "opened issue" | "merged PR", +): ContributorStats { + return { + prsMerged: reason === "merged PR" ? Math.max(1, stats.prsMerged) : stats.prsMerged, + issuesOpened: reason === "opened issue" ? Math.max(1, stats.issuesOpened) : stats.issuesOpened, + }; +} + +async function processRelay(env: RuntimeEnv, envelope: RelayEnvelope): Promise { + const now = new Date().toISOString(); + const allowedRepository = env.ALLOWED_REPOSITORY ?? "EverMind-AI/EverOS"; + if (envelope.repository.toLowerCase() !== allowedRepository.toLowerCase()) { + return json({ ok: false, error: "repository_not_allowed" }, { status: 403 }); + } + + if (!envelope.deliveryId) { + return json({ ok: false, error: "delivery_id_required" }, { status: 400 }); + } + + const duplicate = await getIngestionEvent(env.DB, envelope.deliveryId); + if (duplicate) { + return json({ ok: true, duplicate: true, outcome: duplicate.outcome }); + } + + const context = extractContext(envelope.eventName, envelope.payload); + if (!context) { + await recordIngestionEvent(env.DB, { + deliveryId: envelope.deliveryId, + eventName: envelope.eventName, + action: envelope.action ?? null, + actorLogin: null, + outcome: "unsupported_event", + cardEventId: null, + now, + }); + return json({ ok: true, skipped: true, reason: "unsupported_event" }); + } + + if (!context.actor.login || isBotLogin(context.actor.login)) { + await recordIngestionEvent(env.DB, { + deliveryId: envelope.deliveryId, + eventName: envelope.eventName, + action: envelope.action ?? null, + actorLogin: context.actor.login || null, + outcome: "bot_or_missing_actor", + cardEventId: null, + now, + }); + return json({ ok: true, skipped: true, reason: "bot_or_missing_actor" }); + } + + const { owner, repo, url: repositoryUrl } = parseRepository(envelope.repository); + const github = new GitHubAppClient( + required(env.GITHUB_APP_ID, "GITHUB_APP_ID"), + required(env.GITHUB_APP_INSTALLATION_ID, "GITHUB_APP_INSTALLATION_ID"), + required(env.GITHUB_APP_PRIVATE_KEY, "GITHUB_APP_PRIVATE_KEY"), + ); + + const [rawStats, contributor] = await Promise.all([ + github.fetchContributorStats(owner, repo, context.actor.login), + ensureContributor(env.DB, context.actor.login, context.actor.avatarUrl, now), + ]); + const stats = statsWithEventFloor(rawStats, context.reason); + const points = pointsFromStats(stats); + + if (contributor.welcomedAt) { + await updateContributor( + env.DB, + contributor.id, + context.actor.avatarUrl, + stats, + points, + now, + ); + await recordIngestionEvent(env.DB, { + deliveryId: envelope.deliveryId, + eventName: envelope.eventName, + action: envelope.action ?? null, + actorLogin: context.actor.login, + outcome: "state_updated", + cardEventId: null, + now, + }); + return json({ ok: true, actor: context.actor.login, welcomed: false, points, stats }); + } + + const ownsWelcome = await claimWelcome(env.DB, contributor.id, envelope.deliveryId, now); + if (!ownsWelcome) { + await updateContributor( + env.DB, + contributor.id, + context.actor.avatarUrl, + stats, + points, + now, + ); + await recordIngestionEvent(env.DB, { + deliveryId: envelope.deliveryId, + eventName: envelope.eventName, + action: envelope.action ?? null, + actorLogin: context.actor.login, + outcome: "welcome_in_progress", + cardEventId: null, + now, + }); + return json({ ok: true, actor: context.actor.login, welcomed: false, reason: "welcome_in_progress" }); + } + + const id = eventId(envelope.deliveryId, context.actor.login); + const objectKey = cardObjectKey(id); + const card: ContributorCardModel = { + username: context.actor.login, + avatarUrl: context.actor.avatarUrl ?? "", + archiveNumber: contributor.id, + points, + prsMerged: stats.prsMerged, + issuesOpened: stats.issuesOpened, + }; + + await env.CARD_ASSETS.put( + objectKey, + await renderContributorCard(card, env.CARD_TEMPLATE_URL), + { httpMetadata: { contentType: "image/png" } }, + ); + + const marker = firstEngramMarker(id); + const existingComment = await github.findIssueCommentByMarker( + owner, + repo, + context.threadNumber, + marker, + ); + const comment = existingComment ?? await github.postIssueComment( + owner, + repo, + context.threadNumber, + firstEngramComment(card, publicCardUrl(env.PUBLIC_BASE_URL, id), repositoryUrl, id), + ); + + await insertCardEvent(env.DB, { + eventId: id, + deliveryId: envelope.deliveryId, + contributorId: contributor.id, + recipientLogin: context.actor.login, + surface: context.surface, + threadNumber: context.threadNumber, + commentUrl: comment.html_url, + cardObjectKey: objectKey, + points, + prsMerged: stats.prsMerged, + issuesOpened: stats.issuesOpened, + createdAt: comment.created_at || now, + }); + await updateContributor( + env.DB, + contributor.id, + context.actor.avatarUrl, + stats, + points, + now, + comment.created_at || now, + ); + await recordIngestionEvent(env.DB, { + deliveryId: envelope.deliveryId, + eventName: envelope.eventName, + action: envelope.action ?? null, + actorLogin: context.actor.login, + outcome: existingComment ? "existing_comment_recovered" : "welcome_comment_posted", + cardEventId: id, + now, + }); + + return json({ + ok: true, + actor: context.actor.login, + welcomed: true, + recovered: Boolean(existingComment), + points, + stats, + cardEventId: id, + }); +} + +async function handleCardAsset(env: RuntimeEnv, request: Request, url: URL): Promise { + const name = decodeURIComponent(url.pathname.replace(/^\/cards\//, "")); + const eventId = name.replace(/\.png$/, ""); + const object = await env.CARD_ASSETS.get(cardObjectKey(eventId)); + if (!object) return new Response("not found", { status: 404 }); + + return new Response(request.method === "HEAD" ? null : object.body, { + headers: { + "content-type": object.httpMetadata?.contentType ?? "image/png", + "cache-control": "public, max-age=31536000, immutable", + etag: object.httpEtag, + }, + }); +} + +export default { + async fetch(request: Request, env: RuntimeEnv): Promise { + const url = new URL(request.url); + + if (request.method === "GET" && url.pathname === "/healthz") { + return json({ ok: true, service: "everos-bot" }); + } + + if ((request.method === "GET" || request.method === "HEAD") && url.pathname.startsWith("/cards/")) { + return handleCardAsset(env, request, url); + } + + if (request.method === "POST" && url.pathname === "/api/github/events") { + const contentLength = Number(request.headers.get("content-length") ?? 0); + if (contentLength > 10 * 1024 * 1024) { + return new Response("payload too large", { status: 413 }); + } + + const secret = required(env.WORKFLOW_WEBHOOK_SECRET, "WORKFLOW_WEBHOOK_SECRET"); + const raw = await request.arrayBuffer(); + const signatureOk = await verifyRelaySignature( + raw, + request.headers.get("x-everos-signature"), + secret, + ); + if (!signatureOk) return new Response("invalid signature", { status: 401 }); + + let envelope: RelayEnvelope; + try { + envelope = JSON.parse(new TextDecoder().decode(raw)) as RelayEnvelope; + } catch { + return json({ ok: false, error: "invalid_json" }, { status: 400 }); + } + return processRelay(env, envelope); + } + + return new Response("not found", { status: 404 }); + }, +} satisfies ExportedHandler; diff --git a/src/render.ts b/src/render.ts new file mode 100644 index 0000000..9195c84 --- /dev/null +++ b/src/render.ts @@ -0,0 +1,105 @@ +import { Resvg, initWasm } from "@resvg/resvg-wasm"; +import satori from "satori"; + +import resvgWasmModule from "@resvg/resvg-wasm/index_bg.wasm"; +import { CARD_HEIGHT, CARD_WIDTH, FirstEngramCard } from "./cards/FirstEngramCard"; +import type { ContributorCardModel } from "./types"; + +const INTER_400_URL = "https://cdn.jsdelivr.net/fontsource/fonts/inter@5.2.8/latin-400-normal.ttf"; +const INTER_700_URL = "https://cdn.jsdelivr.net/fontsource/fonts/inter@5.2.8/latin-700-normal.ttf"; +const PLAYFAIR_700_URL = "https://cdn.jsdelivr.net/fontsource/fonts/playfair-display@5.2.8/latin-700-normal.ttf"; +export const DEFAULT_TEMPLATE_URL = + "https://raw.githubusercontent.com/EverMind-AI/EverOS-Bot/main/assets/card/everos-contributor-card-base.png"; + +let resvgReady = false; +let cachedStaticAssets: Promise<{ + inter400: ArrayBuffer; + inter700: ArrayBuffer; + playfair700: ArrayBuffer; + baseImageDataUrl: string; +}> | null = null; + +async function ensureResvg(): Promise { + if (resvgReady) return; + await initWasm(resvgWasmModule); + resvgReady = true; +} + +function arrayBufferToBase64(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + let binary = ""; + for (let offset = 0; offset < bytes.length; offset += 0x8000) { + binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000)); + } + return btoa(binary); +} + +async function fetchDataUrl(url: string, fallbackContentType = "image/png"): Promise { + const response = await fetch(url, { headers: { "user-agent": "everos-bot" } }); + if (!response.ok) throw new Error(`Asset fetch ${response.status}: ${url}`); + const contentType = response.headers.get("content-type") ?? fallbackContentType; + return `data:${contentType};base64,${arrayBufferToBase64(await response.arrayBuffer())}`; +} + +async function fetchBuffer(url: string): Promise { + const response = await fetch(url, { headers: { "user-agent": "everos-bot" } }); + if (!response.ok) throw new Error(`Font fetch ${response.status}: ${url}`); + return response.arrayBuffer(); +} + +function staticAssets(templateUrl: string) { + cachedStaticAssets ??= Promise.all([ + fetchBuffer(INTER_400_URL), + fetchBuffer(INTER_700_URL), + fetchBuffer(PLAYFAIR_700_URL), + fetchDataUrl(templateUrl), + ]).then(([inter400, inter700, playfair700, baseImageDataUrl]) => ({ + inter400, + inter700, + playfair700, + baseImageDataUrl, + })); + return cachedStaticAssets; +} + +async function optionalAvatarDataUrl(url: string): Promise { + if (!url) return null; + try { + return await fetchDataUrl(url); + } catch { + return null; + } +} + +export async function renderContributorCard( + card: ContributorCardModel, + templateUrl = DEFAULT_TEMPLATE_URL, +): Promise { + await ensureResvg(); + const [{ inter400, inter700, playfair700, baseImageDataUrl }, avatarDataUrl] = await Promise.all([ + staticAssets(templateUrl), + optionalAvatarDataUrl(card.avatarUrl), + ]); + + const node = FirstEngramCard({ + username: card.username, + avatarDataUrl, + archiveNumber: card.archiveNumber, + points: card.points, + prsMerged: card.prsMerged, + issuesOpened: card.issuesOpened, + baseImageDataUrl, + }); + + const svg = await satori(node as Parameters[0], { + width: CARD_WIDTH, + height: CARD_HEIGHT, + fonts: [ + { name: "Inter", data: inter400, weight: 400, style: "normal" }, + { name: "Inter", data: inter700, weight: 700, style: "normal" }, + { name: "Playfair", data: playfair700, weight: 700, style: "normal" }, + ], + }); + + return new Resvg(svg).render().asPng(); +} diff --git a/src/scoring.ts b/src/scoring.ts new file mode 100644 index 0000000..b77e4cd --- /dev/null +++ b/src/scoring.ts @@ -0,0 +1,14 @@ +import type { ContributorStats } from "./types"; + +export const SCORE_WEIGHTS = { + firstMergedPr: 30, + subsequentMergedPr: 12, + issueOpened: 5, +} as const; + +export function pointsFromStats(stats: ContributorStats): number { + const prPoints = stats.prsMerged > 0 + ? SCORE_WEIGHTS.firstMergedPr + Math.max(0, stats.prsMerged - 1) * SCORE_WEIGHTS.subsequentMergedPr + : 0; + return prPoints + stats.issuesOpened * SCORE_WEIGHTS.issueOpened; +} diff --git a/src/signature.ts b/src/signature.ts new file mode 100644 index 0000000..f3f44ba --- /dev/null +++ b/src/signature.ts @@ -0,0 +1,28 @@ +function fromHex(value: string): ArrayBuffer | null { + if (!/^[0-9a-f]+$/i.test(value) || value.length % 2 !== 0) return null; + const buffer = new ArrayBuffer(value.length / 2); + const bytes = new Uint8Array(buffer); + for (let index = 0; index < bytes.length; index += 1) { + bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16); + } + return buffer; +} + +export async function verifyRelaySignature( + rawBody: ArrayBuffer, + header: string | null, + secret: string, +): Promise { + if (!header?.startsWith("sha256=")) return false; + const signature = fromHex(header.slice("sha256=".length)); + if (!signature) return false; + + const key = await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(secret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["verify"], + ); + return crypto.subtle.verify("HMAC", key, signature, rawBody); +} diff --git a/src/storage.ts b/src/storage.ts new file mode 100644 index 0000000..381bc74 --- /dev/null +++ b/src/storage.ts @@ -0,0 +1,171 @@ +import type { CardEventRecord, ContributorRecord, ContributorStats } from "./types"; + +interface ContributorRow { + id: number; + login: string; + avatar_url: string | null; + points: number; + prs_merged: number; + issues_opened: number; + welcome_delivery_id: string | null; + welcomed_at: string | null; + last_seen_at: string; + created_at: string; + updated_at: string; +} + +function toContributor(row: ContributorRow): ContributorRecord { + return { + id: row.id, + login: row.login, + avatarUrl: row.avatar_url, + points: row.points, + prsMerged: row.prs_merged, + issuesOpened: row.issues_opened, + welcomeDeliveryId: row.welcome_delivery_id, + welcomedAt: row.welcomed_at, + lastSeenAt: row.last_seen_at, + createdAt: row.created_at, + updatedAt: row.updated_at, + }; +} + +export async function getContributor(db: D1Database, login: string): Promise { + const row = await db.prepare(` + SELECT id, login, avatar_url, points, prs_merged, issues_opened, welcome_delivery_id, + welcomed_at, last_seen_at, created_at, updated_at + FROM contributors + WHERE login = ? COLLATE NOCASE + `).bind(login).first(); + return row ? toContributor(row) : null; +} + +export async function ensureContributor( + db: D1Database, + login: string, + avatarUrl: string | null, + now: string, +): Promise { + await db.prepare(` + INSERT OR IGNORE INTO contributors ( + login, avatar_url, points, prs_merged, issues_opened, + welcome_delivery_id, welcomed_at, last_seen_at, created_at, updated_at + ) VALUES (?, ?, 0, 0, 0, NULL, NULL, ?, ?, ?) + `).bind(login, avatarUrl, now, now, now).run(); + + const contributor = await getContributor(db, login); + if (!contributor) throw new Error(`Failed to create contributor: ${login}`); + return contributor; +} + +export async function claimWelcome( + db: D1Database, + contributorId: number, + deliveryId: string, + now: string, +): Promise { + const result = await db.prepare(` + UPDATE contributors + SET welcome_delivery_id = ?, updated_at = ? + WHERE id = ? + AND welcomed_at IS NULL + AND (welcome_delivery_id IS NULL OR welcome_delivery_id = ?) + `).bind(deliveryId, now, contributorId, deliveryId).run(); + return (result.meta.changes ?? 0) > 0; +} + +export async function updateContributor( + db: D1Database, + contributorId: number, + avatarUrl: string | null, + stats: ContributorStats, + points: number, + now: string, + welcomedAt?: string, +): Promise { + await db.prepare(` + UPDATE contributors + SET avatar_url = COALESCE(?, avatar_url), + points = ?, + prs_merged = ?, + issues_opened = ?, + welcomed_at = COALESCE(?, welcomed_at), + last_seen_at = ?, + updated_at = ? + WHERE id = ? + `).bind( + avatarUrl, + points, + stats.prsMerged, + stats.issuesOpened, + welcomedAt ?? null, + now, + now, + contributorId, + ).run(); +} + +export async function getIngestionEvent( + db: D1Database, + deliveryId: string, +): Promise<{ delivery_id: string; outcome: string } | null> { + return db.prepare(` + SELECT delivery_id, outcome FROM ingestion_events WHERE delivery_id = ? + `).bind(deliveryId).first<{ delivery_id: string; outcome: string }>(); +} + +export async function recordIngestionEvent( + db: D1Database, + args: { + deliveryId: string; + eventName: string; + action: string | null; + actorLogin: string | null; + outcome: string; + cardEventId: string | null; + now: string; + }, +): Promise { + await db.prepare(` + INSERT INTO ingestion_events ( + delivery_id, event_name, action, actor_login, outcome, + card_event_id, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(delivery_id) DO UPDATE SET + outcome = excluded.outcome, + card_event_id = excluded.card_event_id, + updated_at = excluded.updated_at + `).bind( + args.deliveryId, + args.eventName, + args.action, + args.actorLogin, + args.outcome, + args.cardEventId, + args.now, + args.now, + ).run(); +} + +export async function insertCardEvent(db: D1Database, event: CardEventRecord): Promise { + await db.prepare(` + INSERT OR IGNORE INTO card_events ( + event_id, delivery_id, contributor_id, recipient_login, surface, + thread_number, comment_url, card_object_key, points, prs_merged, + issues_opened, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).bind( + event.eventId, + event.deliveryId, + event.contributorId, + event.recipientLogin, + event.surface, + event.threadNumber, + event.commentUrl, + event.cardObjectKey, + event.points, + event.prsMerged, + event.issuesOpened, + event.createdAt, + ).run(); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..6524b75 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,66 @@ +export type ContributionSurface = "issue" | "pull_request"; + +export interface ContributorStats { + prsMerged: number; + issuesOpened: number; +} + +export interface ContributorRecord extends ContributorStats { + id: number; + login: string; + avatarUrl: string | null; + points: number; + welcomeDeliveryId: string | null; + welcomedAt: string | null; + lastSeenAt: string; + createdAt: string; + updatedAt: string; +} + +export interface EventContext { + actor: { login: string; avatarUrl: string | null }; + threadNumber: number; + reason: "opened issue" | "merged PR"; + surface: ContributionSurface; +} + +export interface RelayEnvelope { + repository: string; + eventName: string; + action?: string; + deliveryId: string; + triggeredAt?: string; + payload: unknown; +} + +export interface ContributorCardModel extends ContributorStats { + username: string; + avatarUrl: string; + archiveNumber: number; + points: number; +} + +export interface CardEventRecord extends ContributorStats { + eventId: string; + deliveryId: string; + contributorId: number; + recipientLogin: string; + surface: ContributionSurface; + threadNumber: number; + commentUrl: string | null; + cardObjectKey: string; + points: number; + createdAt: string; +} + +export interface RuntimeEnv { + DB: D1Database; + CARD_ASSETS: R2Bucket; + PUBLIC_BASE_URL: string; + CARD_TEMPLATE_URL?: string; + ALLOWED_REPOSITORY?: string; + GITHUB_APP_ID?: string; + GITHUB_APP_INSTALLATION_ID?: string; + GITHUB_APP_PRIVATE_KEY?: string; + WORKFLOW_WEBHOOK_SECRET?: string; +} diff --git a/src/wasm.d.ts b/src/wasm.d.ts new file mode 100644 index 0000000..aff905f --- /dev/null +++ b/src/wasm.d.ts @@ -0,0 +1,4 @@ +declare module "*.wasm" { + const module: WebAssembly.Module; + export default module; +} diff --git a/tests/card.test.ts b/tests/card.test.ts new file mode 100644 index 0000000..b781f97 --- /dev/null +++ b/tests/card.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; + +import { cardObjectKey, publicCardUrl } from "../src/card"; +import { firstEngramComment, firstEngramMarker } from "../src/comments"; + +describe("First Engram card", () => { + it("uses immutable PNG object keys", () => { + expect(cardObjectKey("alice-first-engram-1")).toBe("cards/alice-first-engram-1.png"); + expect(publicCardUrl("https://bot.example/", "alice-first-engram-1")) + .toBe("https://bot.example/cards/alice-first-engram-1.png"); + }); + + it("includes an idempotency marker and the card", () => { + const eventId = "alice-first-engram-1"; + const comment = firstEngramComment({ + username: "alice", + avatarUrl: "", + archiveNumber: 1, + points: 5, + prsMerged: 0, + issuesOpened: 1, + }, "https://bot.example/cards/a.png", "https://github.com/EverMind-AI/EverOS", eventId); + + expect(comment).toContain(firstEngramMarker(eventId)); + expect(comment).toContain("Your first Engram in EverOS"); + expect(comment).toContain("5 points"); + }); +}); diff --git a/tests/context.test.ts b/tests/context.test.ts new file mode 100644 index 0000000..a7b0883 --- /dev/null +++ b/tests/context.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; + +import { extractContext, isBotLogin } from "../src/context"; + +describe("extractContext", () => { + it("accepts a newly opened issue", () => { + expect(extractContext("issues", { + action: "opened", + issue: { + number: 12, + user: { login: "alice", avatar_url: "https://example.com/avatar.png" }, + }, + })).toEqual({ + actor: { login: "alice", avatarUrl: "https://example.com/avatar.png" }, + threadNumber: 12, + reason: "opened issue", + surface: "issue", + }); + }); + + it("accepts only merged pull requests", () => { + expect(extractContext("pull_request_target", { + action: "closed", + pull_request: { merged: true, number: 7, user: { login: "bob" } }, + })?.reason).toBe("merged PR"); + + expect(extractContext("pull_request_target", { + action: "closed", + pull_request: { merged: false, number: 8, user: { login: "bob" } }, + })).toBeNull(); + }); + + it("filters bot-style logins", () => { + expect(isBotLogin("dependabot[bot]")).toBe(true); + expect(isBotLogin("everos-bot")).toBe(true); + expect(isBotLogin("real-contributor")).toBe(false); + }); +}); diff --git a/tests/scoring.test.ts b/tests/scoring.test.ts new file mode 100644 index 0000000..a4d51c9 --- /dev/null +++ b/tests/scoring.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; + +import { pointsFromStats } from "../src/scoring"; + +describe("pointsFromStats", () => { + it("awards five points for the first issue", () => { + expect(pointsFromStats({ prsMerged: 0, issuesOpened: 1 })).toBe(5); + }); + + it("weights the first merged PR more heavily", () => { + expect(pointsFromStats({ prsMerged: 1, issuesOpened: 0 })).toBe(30); + expect(pointsFromStats({ prsMerged: 2, issuesOpened: 1 })).toBe(47); + }); +}); diff --git a/tests/signature.test.ts b/tests/signature.test.ts new file mode 100644 index 0000000..2b677f4 --- /dev/null +++ b/tests/signature.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; + +import { verifyRelaySignature } from "../src/signature"; + +async function signature(secret: string, body: ArrayBuffer): Promise { + const key = await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(secret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], + ); + const digest = new Uint8Array(await crypto.subtle.sign("HMAC", key, body)); + return `sha256=${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`; +} + +describe("verifyRelaySignature", () => { + it("accepts the exact signed body and rejects tampering", async () => { + const body = new TextEncoder().encode('{"event":"issues"}').buffer; + const header = await signature("test-secret", body); + expect(await verifyRelaySignature(body, header, "test-secret")).toBe(true); + + const tampered = new TextEncoder().encode('{"event":"push"}').buffer; + expect(await verifyRelaySignature(tampered, header, "test-secret")).toBe(false); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7194cf7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "WebWorker"], + "types": ["@cloudflare/workers-types", "vitest/globals"], + "strict": true, + "noUncheckedIndexedAccess": true, + "noEmit": true, + "allowImportingTsExtensions": false, + "jsx": "react-jsx", + "skipLibCheck": true + }, + "include": ["src", "tests", "vitest.config.ts"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..faa6d98 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["tests/**/*.test.ts"], + }, +}); diff --git a/wrangler.jsonc b/wrangler.jsonc new file mode 100644 index 0000000..2e492a5 --- /dev/null +++ b/wrangler.jsonc @@ -0,0 +1,39 @@ +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "everos-bot", + "main": "src/index.ts", + "compatibility_date": "2026-07-22", + "compatibility_flags": ["nodejs_compat"], + "rules": [ + { + "type": "CompiledWasm", + "globs": ["**/*.wasm"], + "fallthrough": false + } + ], + "observability": { + "enabled": true, + "head_sampling_rate": 1 + }, + "vars": { + "PUBLIC_BASE_URL": "https://everos-bot.evermind.workers.dev", + "ALLOWED_REPOSITORY": "EverMind-AI/EverOS", + "CARD_TEMPLATE_URL": "https://raw.githubusercontent.com/EverMind-AI/EverOS-Bot/main/assets/card/everos-contributor-card-base.png" + }, + "d1_databases": [ + { + "binding": "DB", + "database_name": "everos-bot", + "database_id": "00000000-0000-0000-0000-000000000000", + "preview_database_id": "00000000-0000-0000-0000-000000000000", + "migrations_dir": "migrations" + } + ], + "r2_buckets": [ + { + "binding": "CARD_ASSETS", + "bucket_name": "everos-bot-card-assets", + "preview_bucket_name": "everos-bot-card-assets-preview" + } + ] +}